Skip to content

feat(slack): announce new #support threads in Slack - #5

Merged
crstian19 merged 2 commits into
mainfrom
feat/slack-support-thread-notifications
Aug 20, 2026
Merged

crstian19 merged 2 commits into
mainfrom
feat/slack-support-thread-notifications

Conversation

@crstian19

Copy link
Copy Markdown
Contributor

Qué hace

Cada vez que se abre un hilo en un canal listado en SUPPORT_CHANNEL_IDS (p. ej. #support), el bot publica un aviso en Slack vía Incoming Webhook con título del hilo (link clicable), autor, canal y preview del mensaje inicial. Funciona con canales de texto y de foro.

Con SLACK_WEBHOOK_URL vacío el notifier es un no-op y el bot se comporta exactamente como hoy.

Decisiones

  • Incoming Webhook en vez de bot token + chat.postMessage: una env var, sin OAuth ni scopes. El canal destino se fija en la app de Slack.
  • SUPPORT_CHANNEL_IDS nueva en vez de reusar ALLOWED_CHANNELS, que responde a otra pregunta (dónde responde el bot, no de qué avisa).
  • httpx, no aiohttp: httpx es dependencia explícita; aiohttp sólo entra transitivamente por discord.py.
  • Reintentos en 5xx y 429 (0.5/1/2 s); ninguno en el resto de 4xx — un webhook revocado no mejora reintentando.
  • Se escapan &, <, > en todo campo de usuario, así un título de hilo no puede forjar un link de Slack.

Dos detalles de discord.py

  • on_thread_create sólo se despacha con newly_created, así que las reconexiones del gateway no duplican avisos y no hace falta deduplicar.
  • THREAD_CREATE puede adelantarse al mensaje inicial de un post de foro, y ese race se manifiesta como NotFound. Por eso hay un reintento en NotFound y ninguno en Forbidden.

Seguridad: el secreto del webhook en los logs

bot/config.py pone el root logger en INFO y httpx heredaba, así que logueaba cada request con la URL completa. Como el secreto del webhook va en el path, en producción cada hilo de #support habría escrito ese secreto en los logs del contenedor. httpx baja a WARNING; hay un test que falla si alguien lo revierte.

Salió al hacer una prueba real contra un webhook; los tests usan MockTransport y nunca pasan por ese logger.

CI roto en main (primer commit, independiente)

ruff format --check . fallaba en main: ruff 0.16 formatea el Python dentro de bloques markdown y el pin es ruff>=0.4.0 sin techo. Se arregla excluyendo, no reformateando: bot/docs/knowledge/ es un mirror del API remoto de docs —fallback de DOCS_USE_REMOTE=remote y línea base del diff de shadow— así que reformatearlo reportaría drift falso a perpetuidad.

Verificación

  • 61 tests (26 nuevos): payload, escapado, truncado, política de reintentos, filtrado por canal, race del foro, caché vacía (owner/parent a None) y que un fallo de Slack no propague al event loop.
  • ruff check y ruff format --check limpios.
  • Imagen Docker construida y smoke test dentro del contenedor.
  • Envío real a un webhook de Slack: HTTP 200 y mensaje recibido en el canal.

Antes de mergear

El .env del servidor no lo toca el deploy. Hay que añadir a mano en el nodo, antes del merge:

SLACK_WEBHOOK_URL=<webhook>
SUPPORT_CHANNEL_IDS=<ID de #support>

🤖 Generated with Claude Code

Cristian Gutierrez and others added 2 commits August 20, 2026 10:15
Ruff 0.16 formats Python inside markdown code blocks, and the pin is
`ruff>=0.4.0` with no upper bound, so `ruff format --check .` started
failing on main as soon as ruff updated.

Exclude rather than reformat: bot/docs/knowledge/ mirrors the remote docs
API. It is the fallback for DOCS_USE_REMOTE=remote and the baseline the
shadow-mode hash diff compares against, so reformatting it would report
permanent false drift.

The exclusion is narrow — markdown outside the knowledge base still gets
formatted, and `ruff check` never looked at markdown to begin with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Post a Slack message through an Incoming Webhook whenever a thread is
created in one of the channels listed in SUPPORT_CHANNEL_IDS. The message
carries the thread title as a link, the author, the channel, and a preview
of the opening message. Works for both text and forum channels.

Design notes:

- Incoming Webhook over a bot token: one env var, no OAuth or scopes. The
  destination channel is fixed in the Slack app.
- A new SUPPORT_CHANNEL_IDS rather than reusing ALLOWED_CHANNELS, which
  answers a different question (where the bot replies, not what it reports).
- httpx, not aiohttp: httpx is an explicit dependency; aiohttp only comes in
  transitively through discord.py.
- discord.py only dispatches on_thread_create for newly created threads, so
  gateway re-joins cannot double-notify and no dedupe is needed.
- THREAD_CREATE can outrun a forum post's opening message, which surfaces as
  NotFound, so that case gets one retry. Forbidden gets none.
- User-controlled fields are HTML-escaped so a thread title cannot forge a
  Slack link, and 5xx/429 are retried while other 4xx are not.

Also drop httpx to WARNING: it logs every request at INFO with the full URL,
and the webhook secret lives in that URL, so the default would write the
secret to the container logs on every notification.

With SLACK_WEBHOOK_URL unset the notifier is a no-op and the bot behaves
exactly as before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@crstian19
crstian19 merged commit d2fc0cf into main Aug 20, 2026
2 checks passed
@crstian19
crstian19 deleted the feat/slack-support-thread-notifications branch August 20, 2026 08:35
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.

2 participants