Skip to content

feat(mod): facelift /clear, /who, /register — branding + bug fixes#60

Merged
Xiza73 merged 8 commits into
devfrom
feat/mod-simple
May 8, 2026
Merged

feat(mod): facelift /clear, /who, /register — branding + bug fixes#60
Xiza73 merged 8 commits into
devfrom
feat/mod-simple

Conversation

@Xiza73

@Xiza73 Xiza73 commented May 8, 2026

Copy link
Copy Markdown
Owner

Summary

Batch del facelift de mod — la mitad destructiva/admin. La otra mitad (/cums y /nextcum) va en un PR separado.

/clear

  • 🧹 Embed branded (mod red, footer estándar): `Chat limpiado` — uno ephemeral al mod y otro público autoborrado en el canal
  • Error embed amistoso cuando `bulkDelete` falla (mensajes >14 días o el bot sin permisos): `No se pudo limpiar el chat` con el motivo
  • Bug: removido el código muerto (triple parseInt, `if (!amount)` inalcanzable) y la doble validación de permisos que chequeaba al member dos veces con el comentario "Maybe the bot can't delete messages" que no matcheaba la lógica
  • description más clara: `Limpia el chat` → `Limpia mensajes del canal en bloque`

/who

  • 🎂 Embed branded (mod red, footer). Title `🎂 CUMpleaños` se queda — el juego de palabras es la gracia.
  • Bug: el field name era un espacio en blanco ` ` como hack de layout. Ahora `📅 Fecha`.
  • Description ahora muestra mention + nombre real
  • Nuevo `private:` opt-in (default: público — los cumples se comparten)

/register

  • ✅/❌ Embed branded (mod red, footer) según resultado: `Miembro registrado` / `Error al registrar`
  • Validación de rangos antes del DAO: `day` 1–31, `month` 1–12 → notice ephemeral. Antes podía colar fechas inválidas a Mongo.
  • Fields `👤 Miembro` y `🎂 Cumpleaños` con `allowedMentions: { users: [] }` para no pingar al registrado
  • Nuevo `private:` opt-in (default: público — operación admin visible al equipo)

Test plan

  • `pnpm test` → 239/239 (was 229, +10 nuevos: clear x5, who x2, register x3)
  • `tsc --noEmit` → limpio

Smoke en Discord

  • `/clear amount:5` → ephemeral al mod + notice público que se borra a los 5s
  • `/clear amount:0` → notice ephemeral
  • `/clear` (default 1) → borra 1 mensaje, notice singular
  • `/clear` con mensajes >14 días → embed de error friendly
  • `/who user:@diego` → embed CUMpleaños rojo
  • `/who name:Diego` → idem
  • `/who` (sin args) → tu propio cumple
  • `/who user:@diego private:true` → solo lo ves vos
  • `/register name:John user:@john day:5 month:11` → ✅
  • `/register name:John user:@john day:99 month:5` → ephemeral 'día'
  • `/register name:John user:@john day:5 month:13` → ephemeral 'mes'

Nota: este PR no toca `/say` (PR #59 lo hace). Mergear en cualquier orden.

Xiza73 added 8 commits May 7, 2026 23:19
Batch facelift for the destructive/admin half of mod (the birthday-listing
half, /cums and /nextcum, comes in a second PR).

/clear:
- branded result embed (mod red, footer 'Xiza Bot vX.Y.Z'): '🧹 Chat limpiado'
  ephemeral to the moderator AND public auto-deleting in the channel
- friendly error embed when bulkDelete throws (eg. >14d old messages, missing
  bot perms): '❌ No se pudo limpiar el chat'
- removed dead code: triple parseInt, unreachable 'if (!amount)' branch (since
  amount > 0 was already validated)
- removed duplicate permission check that re-checked the SAME member's perms
  with a 'Maybe the bot can't delete messages' comment that didn't match the
  code — the actual bot-perm check belongs at Discord's Send/Manage gate or
  the bulkDelete throw, not a redundant member check
- amount default 1, capped at config.maxDeleteMessages, rejected if <= 0
- description tightened: 'Limpia el chat' → 'Limpia mensajes del canal en bloque'

/who:
- branded embed (mod red, footer): title kept as '🎂 CUMpleaños' (the wordplay
  is the charm)
- field name was a single space ' ' as a layout hack — now '📅 Fecha'
- description shows the Discord mention AND the real name
- new 'private' opt-in (default: public — birthday lookups are usually shared)

/register:
- branded result embed (mod red, footer): '✅ Miembro registrado' on success,
  '❌ Error al registrar' on DAO non-200
- range validation BEFORE the DAO call: day 1–31, month 1–12 → ephemeral
  notice, prevents bad data sneaking into Mongo
- '👤 Miembro' and '🎂 Cumpleaños' fields, with allowedMentions: { users: [] }
  so the embed doesn't ping the registered user
- new 'private' opt-in (default: public — admin op visible to the team)

Tests: clear (+5: bulk-delete embed shape, branding, singular phrasing,
bulkDelete-throws path, ttl-delete via fake timers), who (+2: branded embed
shape, public/private toggle), register (+3: error title path, day range,
month range, private opt-in).
… /who

Two follow-ups on the mod-simple PR after owner review:

/clear:
- Replied-only recap embed (📋 Eliminados (N)) lists each deleted message
  with author, relative timestamp and content. Helps the moderator review
  what got nuked without blasting it back into the channel.
- Image (and other) attachments are downloaded BEFORE Discord drops the
  CDN URL and re-uploaded as fresh files in the recap reply. The user
  asked specifically for files, not embedded URLs — embedded image URLs
  can break in seconds once the source message is deleted; uploaded files
  stay alive.
- Caps: ≤ 10 attachments per Discord's per-message limit, ≤ 3.8 KB recap
  text per the 4096-char embed description budget. Truncation footer when
  hit.
- Empty-result branch: bulkDelete returning 0 messages now shows a friendly
  '🧹 Nada para limpiar' embed (Discord can't bulk-delete >14d messages).
- Public auto-deleting count notice in the channel kept as before — it's
  for context, the recap is for the moderator.

/who:
- Real name is now the embed title (was '🎂 CUMpleaños'); the wordplay
  was cute but buried the actual identity, which is what the moderator
  is looking for.
- Description is the Discord mention.
- Fields: 👤 Discord (mention + tag), 🎂 Cumpleaños, 📅 Próximo cumple
  (Discord <t:UNIX:R> relative time), 📆 Cuenta creada. When in a guild,
  also: 🎉 En el servidor desde, ✨ Roles principales (top 3 by position,
  excluding @everyone).
- Helper next-birthday calc rolls into next year if the day already passed.
- allowedMentions: { parse: [] } so the embed mention/role tags don't ping.

Tests: clear (+2: recap-shape with multiple deleted messages, fetch-and-
re-upload of attachments, empty-result fallback), who (+2: title-is-real-
name, guild-only fields when guild is present).
The recap is an internal log of what got nuked — meant to persist in the
moderator's DM history regardless of who runs the command. Ephemeral was
wrong: it expires and feels like a UI affordance instead of a record.

- Recap goes to interaction.user.createDM().send({ embeds, files })
- Ephemeral reply is now a brief '✅ Listo. Te envié el detalle por DM.'
  confirmation
- DM-fails fallback: if createDM throws (DMs disabled by the moderator),
  reply ephemerally with the recap inline + a 'no te pude enviar DM' note,
  so the moderator never loses the data
- Public auto-deleting count notice in the channel unchanged

Test mock: createMockInteraction now provides a default user.createDM that
returns a stubbed DMChannel. Tests rewritten to assert: (a) DM gets the
recap with embeds+files, (b) ephemeral reply is the brief confirmation,
(c) DM-fails path falls back to inline ephemeral, (d) attachment re-upload
shows up in the DM payload.
…rmation

Owner reported three bugs in the DM-recap version:
1. 'Unknown interaction' on multi-message clears
2. Image attachments lost from the DM (only the first message arrived)
3. Pointless 'Te envié el detalle por DM' ephemeral cluttering the channel

Root cause for (1) and (2): the work was sequential and bulkDelete ran
BEFORE we read the messages or downloaded their attachments. With multi-
message clears, the parallel(ish) attachment fetches blew past Discord's
3s acknowledgement window. And once bulkDelete fired, the CDN URLs the
recap relied on were already invalid for any attachment that hadn't been
read yet.

New flow:
1. Acknowledge instantly via deferReply({ ephemeral }) so we have the
   15-min interaction token instead of the 3s reply window.
2. channel.messages.fetch({ limit: amountToDelete }) — capture the
   messages while they're still alive.
3. Download every attachment in parallel via Promise.all on the still-valid
   CDN URLs.
4. channel.bulkDelete(messageIds, true) — delete by ID, since we already
   have the full Message objects we need for the recap.
5. dm.send the recap with re-uploaded files.
6. Public auto-deleting count notice in the channel (unchanged).
7. interaction.deleteReply() removes the deferred ephemeral entirely on
   the happy path — no '✅ Listo' confirmation surfaces in the channel.
   The recap is in DM, the count notice is in the channel; both already
   give all the feedback the moderator needs.

Edge cases preserved:
- DM disabled → editReply with the recap inline + 'no te pude enviar DM'
  (the only path that surfaces an ephemeral, since otherwise the recap
  would be lost).
- bulkDelete returns 0 → editReply with '🧹 Nada para limpiar'.
- bulkDelete throws → editReply with '❌ No se pudo limpiar el chat' +
  the underlying error.

Mocks: createMockInteraction now provides deleteReply. Tests rewritten
to drive the new flow: ordering check via call-order array (fetch →
bulk-delete), DM payload assertions, deleteReply on happy path,
editReply on fallback paths.

Tests: 243/243. Type fix on the parallel-download Promise return
annotation so Buffer<ArrayBuffer> narrows to DownloadedAttachment without
fighting Node 22's stricter Buffer generic.
…ete polish

The messageDelete event listener (events/message/messageDelete.ts) already
DMs the owner for every individual cached delete in the gmi2 channel.
discord.js fires messageDelete for cached messages that go through bulkDelete
too, so the simple /clear amount:1 case was producing two DMs: one from the
listener (single-message embed) and one from /clear (the recap).

Owner spec to dedup:
- /clear amount:1, text-only      → SKIP recap (listener has it)
- /clear amount:1, has attachment → SEND recap (image is high-signal)
- /clear amount:N (N > 1)         → SEND recap (multi-message context)
- manual deletion                 → listener handles it as before

Implementation: const skipRecap = messageArr.length === 1 && !hasAttachments.
The deferReply still happens (we need it during the fetch/download phase
even on the skip path), but the DM step is gated by skipRecap and the
deleteReply runs the same way regardless.

While I was in there, polished the listener:
- 'un imagen' → 'una imagen' typo (well, killed the line entirely — the
  embed-only path makes the duplicate content text redundant)
- Branded: title '🗑️ Mensaje eliminado', mod-red color (#ED4245),
  brand footer, timestamp
- Image and text cases now share the same embed shape — image case still
  re-uploads the URL via files: [url] so it survives the source delete,
  but the embed (with description if any) is the same as the text path.
  Old code dropped to a content-only DM for images, which had no title
  and no footer.
- Description omitted entirely when there's no text (instead of leaking
  empty trailing newlines from string-concat).
- Removed the noisy debug console.logs in the early-return guards.

Tests: 247/247.
- /clear: +2 (single text-only skips DM + still sends count notice; single
  with attachment DOES DM).
- messageDelete listener: +2 (image case carries the same branded embed,
  no-text image case has no description), updated existing assertions to
  the new branded shape.
The 🗑️ title up top was breaking the illusion the embed should give: the
DM is meant to read like the original message preserved (avatar + name +
content + original timestamp), not as a 'deletion log' entry. Owner asked
for the 'deleted' indicator to live at the end instead.

- setTitle removed
- setTimestamp now uses message.createdTimestamp (when the message was
  posted) rather than new Date() (when it got deleted) — reinforces the
  'this is the message' feel
- Footer text combines the deletion hint with the brand:
  '🗑️ Eliminado · Xiza Bot vX.Y.Z'
- mod-red color stays as the visual signal that something was removed

Tests updated: title is undefined, footer matches new pattern.
…r count

messageDelete listener:
- Footer now reads just '🗑️ Eliminado' (no brand). The 'Xiza Bot vX.Y.Z'
  trailer was killing the same illusion the title removal was trying to
  preserve — the embed should look like the message itself, not like a
  bot-branded notice.

/clear public count notice:
- 'Se eliminaron **1** mensaje.' → 'Se eliminó **1** mensaje.' (correct
  Spanish singular conjugation). Plural path stays as 'Se eliminaron N
  mensajes.'

Tests: +1 (plural path), restored singular assertion that got dropped in
the earlier flow rewrite.
The embed had setDescription('<@{id}>') and a '👤 Discord' field whose
value was '<@{id}> ({tag})' — same mention twice, stacked vertically.
Dropping the description; the field already carries the mention plus the
extra Discord tag context.
@Xiza73 Xiza73 merged commit d883873 into dev May 8, 2026
2 checks passed
@Xiza73 Xiza73 deleted the feat/mod-simple branch May 8, 2026 15:40
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.

1 participant