#javascript #frontend #linux #debug #dns

widget 'agora jogando' com Lanyard: o DNS brasileiro quase arruinou tudo

Miuna Hamasaki

o site tinha "world.execute(me); Mili" hardcoded no widget de música. literalmente hardcoded. sempre essa música, independente do q eu tivesse ouvindo. dava até vergonha. >_<

ai decidi arrumar isso pra valer, queria q os widgets refletissem o q eu realmente estou jogando e estudando em tempo real. simples de explicar, menos simples de fazer num site 100% estático no GitHub Pages sem backend nenhum.

Analisando as Opções

cada widget tem uma fonte de dados diferente, e num site estático vc não pode fazer nada server-side. as opções viáveis eram:

Widget Melhor opção Como funciona Dificuldade
ouvindo agora Last.fm + Web Scrobbler extensão scrobble pro Last.fm, site faz fetch na API deles preguiça u_u
agora jogando Lanyard Discord detecta o jogo via Rich Presence, Lanyard expõe como API pública baixa
now studying status.json manual arquivo JSON no repo, site faz fetch, atualizo via git push mt baixa

Steam API seria outra opção pra gaming, mas tem CORS bloqueado e precisaria de um proxy. o Lanyard tem CORS liberado, é gratuito, open source, e o Discord já detecta os jogos automaticamente. escolha óbvia.

LIMITAÇÃO NO LINUX

Discord Rich Presence só funciona com o app desktop, não o web. e no Linux a detecção de jogos é irregular, jogos Steam nativos geralmente funcionam, mas jogos via Proton/Wine são sorte, o Discord as vezes vê o processo wine em vez do jogo em si. no meu Nobara Linux tem funcionado razoavelmente bem.

A Implementação

a parte do código foi tranquila. três mudanças:

1. criei o status.json na raiz do site:

{
  "studying": "Lua & C & C++"
}

2. adicionei IDs nos elementos HTML dos widgets e separei gaming de studying (antes tava tudo misturado num widget só com label errado):

<!-- widget gaming -->
<div class="track" id="game-track">—</div>
<div class="music-bars" id="game-bars" style="display:none">...</div>

<!-- widget studying (novo) -->
<div class="track" id="study-track">—</div>

3. ~40 linhas de JS inline no final do HTML. Lanyard com polling de 60s, status.json fetch único no load:

// Lanyard: polling a cada 60s
(function () {
  var LANYARD = 'https://api.lanyard.rest/v1/users/SEU_ID';

  function fetchGame() {
    fetch(LANYARD)
      .then(function (r) { return r.json(); })
      .then(function (d) {
        var track = document.getElementById('game-track');
        var bars  = document.getElementById('game-bars');
        if (!track) return;
        var acts = d.data && d.data.activities;
        var game = acts && acts.find(function (a) { return a.type === 0; });
        if (game) {
          track.textContent = game.name;
          if (bars) bars.style.display = '';   // mostra as barrinhas
        } else {
          track.textContent = 'nada no momento';
          if (bars) bars.style.display = 'none';
        }
      })
      .catch(function () {});
  }

  fetchGame();
  setInterval(fetchGame, 60000);
})();

// status.json: fetch único no load
(function () {
  fetch('/status.json')
    .then(function (r) { return r.json(); })
    .then(function (d) {
      var el = document.getElementById('study-track');
      if (el && d.studying) el.textContent = d.studying;
    })
    .catch(function () {});
})();

o studying funciona na primeira tacada. o gaming... não. (ꐦ°᷄д°᷅)

O Widget Não Atualizava

abri o localhost:8080 e o widget de gaming ficou mostrando "—". o de studying funcionou beleza (apareceu "Lua & C & C++"), mas o gaming nada.

abri o console do browser e rodei o fetch manualmente:

fetch('https://api.lanyard.rest/v1/users/1368778707334070343')
  .then(r=>r.json()).then(d=>console.log(d))

o retorno foi:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at https://api.lanyard.rest/v1/users/...
(Reason: CORS request did not succeed). Status code: (null).

status (null). isso não é erro de CORS header, é falha de rede. o browser não chegou nem a receber resposta. fui pro terminal:

curl -s https://api.lanyard.rest/v1/users/1368778707334070343

output: absolutamente nada. nenhum JSON, nenhum erro visível. ¯\_(ツ)_/¯

ai rodei com verbose:

curl -v https://api.lanyard.rest/v1/users/1368778707334070343 2>&1 | head -30
* Could not resolve host: api.lanyard.rest
* Closing connection
curl: (6) Could not resolve host: api.lanyard.rest

DNS não resolvia o domínio. o servidor existe, o site existe, mas o meu DNS local simplesmente não sabia quem era api.lanyard.rest.

Diagnóstico: DNS do ISP Não Tem o Registro

testei com o DNS do Google pra confirmar:

nslookup api.lanyard.rest 8.8.8.8
Server:    8.8.8.8
Address:   8.8.8.8#53

Non-authoritative answer:
api.lanyard.rest  canonical name = lanyard.gateway.railway.app.
Name:  lanyard.gateway.railway.app
Address: 151.101.2.15

com o 8.8.8.8 resolveu na hora. o problema é claro, o DNS padrão que eu tava usando não tinha o registro pro domínio .rest. o Lanyard fica hospedado no Railway (lanyard.gateway.railway.app) com IP 151.101.2.15.

Fix Rápido: /etc/hosts

com o IP em mãos, botei direto no /etc/hosts pra não depender de resolução DNS:

echo "151.101.2.15 api.lanyard.rest" | sudo tee -a /etc/hosts

testei:

curl -s https://api.lanyard.rest/v1/users/1368778707334070343
{"data":{"activities":[{"name":"Sixtar Gate STARTRAIL","type":0,...}],
"discord_status":"online",...},"success":true}

JSON lindo na tela. recarreguei o localhost:8080 e o widget apareceu, Sixtar Gate STARTRAIL. (✿◡‿◡)

/etc/hosts É PALIATIVO

o IP do Railway pode mudar. a solução permanente é trocar o DNS do sistema pra 1.1.1.1 (Cloudflare) ou 8.8.8.8 (Google) nas configurações de rede. no Nobara/Fedora: Settings → Network → sua conexão → aba IPv4 → DNS.

Resultado Final

Widget Antes Depois
agora jogando hardcoded, label errado ("now studying:") Discord Rich Presence via Lanyard, polling 60s
now studying hardcoded junto com o widget de gaming widget separado, lê /status.json do repo
ouvindo agora hardcoded "world.execute(me); Mili" ainda hardcoded (Last.fm fica pra depois)

pra atualizar o studying é só editar o status.json e dar git push. simples. nao tem motivo pra complicar o q é manual por natureza.

Lição

TROQUE SEU DNS

se vc usa o DNS padrão do seu ISP no Brasil, tem chance real de domínios com TLDs menos comuns (.rest, .dev, etc.) simplesmente não resolverem. o ISP não é obrigado a ter todos os registros atualizados.

troca pra 1.1.1.1 ou 8.8.8.8 e esquece o problema. é uma mudança de 30 segundos que salva dor de cabeça futura. (⌐■_■)

e se vc tiver um erro de CORS com status (null) no browser, não é CORS, é rede. o browser reporta qualquer falha de fetch cross-origin como "CORS error". vai direto pro terminal e testa com curl -v.

update: migrando pro Cloudflare Pages

mudei de GitHub Pages pro Cloudflare Pages pra ganhar edge functions. com isso, o widget "agora jogando" ganhou uma camada a mais de privacidade e performance.

antes, o client-side fazia fetch direto pro api.lanyard.rest, expondo meu Discord ID (1368778707334070343) no HTML pra qualquer um inspecionar. agora tem uma edge function em /api/now-playing que faz o fetch no servidor, cacheia por 60s, e o cliente só vê o proxy local. menos exposição, mais rápido, e a CSP ficou mais limpa também.

// antes (client-side, ID exposto)
fetch('https://api.lanyard.rest/v1/users/1368778707334070343')

// depois (edge function, ID escondido)
fetch('/api/now-playing')

a function é literalmente ~10 linhas. o Cloudflare faz o fetch, aplica cache no edge, e devolve o JSON. o navegador nunca toca no domínio externo.

POR QUE ISSO IMPORTA

IDs de usuário do Discord não são segredos de estado, mas expor qualquer identificador sem necessidade é má prática. se alguém quiser fazer scraping da minha presença 24/7, agora precisa bater no meu domínio. eu controlo o rate limit e o cache.

fora isso, o Cloudflare Pages ainda entrega o site mais perto dos visitantes (CDN global) e tem suporte nativo a functions. o GitHub Pages é ótimo pra sites 100% estáticos, mas pra qualquer coisa dinâmica, edge é o caminho.