Skip to content

feat(live): discord activity — assistir e comentar a live de dentro do discord - #532

Draft
tecrodrigocastro wants to merge 9 commits into
he4rt:feature/live-mvpfrom
tecrodrigocastro:feature/live-mvp
Draft

feat(live): discord activity — assistir e comentar a live de dentro do discord#532
tecrodrigocastro wants to merge 9 commits into
he4rt:feature/live-mvpfrom
tecrodrigocastro:feature/live-mvp

Conversation

@tecrodrigocastro

Copy link
Copy Markdown

Contexto

Em cima do MVP de live (#530), abre a mesma experiência (player + chat) como Discord
Activity — o mini-app embutido no painel de voz do Discord. Nenhum arquivo do #530 foi
alterado; tudo aqui é composição por cima dos componentes que já existem.

Duas descobertas moldaram a implementação:

  • O ponto de entrada de uma Activity é sempre / — o Discord não permite apontar pra
    outra rota, então a detecção de contexto acontece na própria rota raiz do portal.
  • O iframe roda a partir de discordsays.com (terceiro pro documento top-level do
    Discord) com CSP restrita a 'self' — exigiu ajuste de sessão (SameSite=None) e
    reescrita de URLs absolutas pra relativas.

Alterações

Módulo identity

  • FindConnectedUser: resolve User a partir de identidade externa já vinculada, só
    leitura — diferente de ResolveExternalIdentity, nunca cria identidade.

Módulo integration-discord

  • redirectUri de ExchangeCodeForToken vira opcional: o authorize() do SDK da
    Activity troca code por token sem redirect_uri, diferente do login web.
  • Activity/: AuthenticateActivityUser (troca code → token → resolve User vinculado),
    ActivityAuthResult (DTO), ActivityAuthController + rota POST /discord-activity/auth,
    isolada das rotas de auth existentes.

Módulo live

  • HlsProxyController + rota GET /discord-activity/hls/{path}: repassa o HLS do
    mediamtx pelo próprio domínio da app — a CSP do iframe só permite mídia da própria origem.
  • mediamtx.yml: exclui read/playback do hook de auth HTTP — já eram sempre liberados
    no código; chamar o hook mesmo assim criava round-trip redundante e, no servidor de dev
    single-threaded, um deadlock real.

Módulo portal

  • LiveActivityPage + live-activity.blade.php + layout activity.blade.php: versão
    enxuta da live (player + chat, sem navbar) servida dentro do iframe do Discord.
  • discord-activity.js: bootstrap do @discord/embedded-app-sdk (ready()
    authorize() → troca de código no backend → authenticate()), com interceptação de
    links pra abrir fora do iframe (sandboxed sem allow-popups).
  • A rota / do PortalServiceProvider decide entre Homepage e LiveActivityPage
    conforme o query param frame_id que o SDK injeta.

App (transversal)

  • PrepareDiscordActivityContext (middleware): ajusta session.same_site pra none no
    contexto da Activity — sem isso o cookie de sessão não sobrevive no iframe de terceiros.
  • RewriteDiscordActivityAssetUrls (listener de RequestHandled): reescreve URLs
    absolutas pro próprio host como relativas na resposta — precisa rodar depois que o
    Livewire injeta seu script, que só acontece depois de toda a pipeline de middleware.
  • bootstrap/app.php: TrustProxies confia em qualquer proxy só quando APP_ENV=local
    — necessário pra testar via túnel (cloudflared/ngrok), zero efeito em produção.

Fora desta v1 (follow-up)

  • Chat em tempo real dentro da Activity (Reverb roda numa porta separada, bloqueada pela
    mesma CSP — precisaria de um segundo túnel/proxy em produção). Hoje: manda mensagem,
    não vê a de quem está fora até recarregar.
  • Fluxo de "vincular conta Discord" acontece fora do iframe (abre em navegador externo
    via openExternalLink do SDK).
  • CORS de produção do HLS (mediamtx) pra origem real da Activity (discordsays.com
    varia por app).

Plano de Testes

  • vendor/bin/pint --dirty --format agent
  • vendor/bin/phpstan analyse --memory-limit=2G — 0 erros
  • php artisan test --compact — 1730/1732 testes, 5926 assertions (as 2 falhas são
    pré-existentes em CommunityRetrospectivePageTest, não relacionadas — confirmado
    revertendo os arquivos desta PR e reproduzindo a mesma falha)
  • Testado manualmente dentro do Discord (Activity real, canal de voz): vídeo, envio
    de chat e vínculo de conta Discord funcionando

Issues Relacionadas

Nenhuma issue vinculada.

Resolve User a partir de provider+external_account_id sem criar identidade.
…auth

Fluxo de authorize() de Activity não usa redirect_uri, diferente do login web.
… activity

TrustProxies local, middleware de sessão e listener que relativiza URLs pra CSP.
…idade vinculada

Troca code por token, resolve o User já conectado e loga a sessão da Activity.
LiveActivityPage sem navbar, SDK do Discord no client, rota / decide por frame_id.
CSP do iframe só permite mídia da própria origem; repassa pro mediamtx por trás.
Read/playback já eram sempre liberados; round-trip redundante travava o dev single-thread.
Mesmo app do developer portal serve o login web e a discord activity.
Pedido do Clinton no review — mesmo padrão de outro módulo.
Comment thread bootstrap/app.php
// Local, testando via túnel (cloudflared/ngrok), a conexão chega via loopback,
// fora dos IPs publicados da Cloudflare: sem confiar em '*', o Laravel não vê
// X-Forwarded-Proto e gera url()/asset() em http://, que o túnel recusa.
if (env('APP_ENV') === 'local') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (env('APP_ENV') === 'local') {
if(app()->isLocal()){

Route::post('live/ingest/webhook', IngestWebhookController::class)->name('live.ingest-webhook');

Route::get('discord-activity/hls/{path}', HlsProxyController::class)
->where('path', '.*')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

->where('path', '[A-Za-z0-9._-]+.(m3u8|mp4|ts|m4s)')

Definir um padrão de entrada evita possíveis ataques direto por essa rota.

// sem cookie jar, o Guzzle não carrega o Set-Cookie entre os hops do redirect e
// o hook de auth do mediamtx rejeita a requisição seguinte. As sub-playlists
// (áudio/vídeo) vêm com `?session=...` no manifest — repassa a query também.
$upstream = Http::withOptions(['cookies' => new CookieJar])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Esse Http::get() bufferiza o corpo inteiro em memória e segura o worker do FPM durante todo o download do segmento. Dá pra tirar o PHP do caminho quente com um proxy_pass no nginx para /discord-activity/hls/ direto no mediamtx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants