Skip to content

feat(events): reaction-add/remove handlers - #4

Merged
mortik merged 1 commit into
mainfrom
feat/reaction-handlers
May 27, 2026
Merged

feat(events): reaction-add/remove handlers#4
mortik merged 1 commit into
mainfrom
feat/reaction-handlers

Conversation

@mortik

@mortik mortik commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

  • src/events/shared.ts — pure helpers: emojiToString (unicode vs custom format) and findMatchingVerification.
  • src/events/reactionAdd.ts — on a matching reaction, fetch the member, skip if they already have the role, otherwise grant it.
  • src/events/reactionRemove.ts — on a matching reaction removal, no-op if `on_remove: keep`, otherwise revoke if they hold the role.
  • src/client.ts — adds the `GuildMessageReactions` intent. Crucially also adds `Partials.User` alongside the existing Message/Channel/Reaction partials, because `MESSAGE_REACTION_REMOVE` only ships `user_id` (no member object like the add event does). Without `Partials.User`, discord.js silently drops the remove event for any uncached user — including everyone the bot has never seen before.
  • `src/sweep.ts` / `src/rest.ts` — `DiscordRestLike` gains `removeMemberRole`; the rest of the sweep is unchanged.
  • 11 new unit tests for the matcher and emoji-string helpers (32 total now).

Implements step 5 of `docs/exec-plans/v1.md`.

Test plan

  • `pnpm test` — 32 tests pass
  • `pnpm lint`, `pnpm typecheck`, `pnpm build` clean
  • Live smoke against the real verification message (with `on_remove: keep` so no role changes risked):
    • Without `Partials.User`: raw `MESSAGE_REACTION_REMOVE` packet arrived but `Events.MessageReactionRemove` never emitted — debugged by reading `node_modules/discord.js/.../Action.js` `getUser`.
    • With `Partials.User`: both add and remove handlers fire as expected. Confirmed via a clean log sequence of `reactionRemove: matched` (no-op, keep) followed by `reactionAdd: matched` → `reactionAdd: granted role`.

Follow-ups

  • Step 6: cron-scheduled sweep on top of the existing on-startup sweep.
  • The privileged `GuildMembers` intent is still not required for v1 — sweep-side revocation (enumerating role holders who didn't react) would need it, but real-time `reactionRemove` covers the common case.

🤖 Generated with Claude Code

When a configured ✅ reaction lands on the configured message, grant
the role. When it is removed, revoke the role if on_remove=revoke
(default keep). Handler decisions are unit-tested via the shared
matcher; the live event wiring is verified end-to-end.

- src/events/shared.ts: pure matcher + emoji-string helpers
- src/events/reactionAdd.ts, reactionRemove.ts: handlers
- src/client.ts: add GuildMessageReactions intent + Partials.User
  alongside Message/Channel/Reaction (Partials.User is required for
  MESSAGE_REACTION_REMOVE on uncached users — the gateway payload
  only ships user_id, not a member object like the add event)
- src/rest.ts, src/sweep.ts: add removeMemberRole to DiscordRestLike
- src/index.ts: register both handlers
- test/events/shared.test.ts: 11 tests for emoji + matcher

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mortik
mortik merged commit 8d5c44d into main May 27, 2026
1 check passed
@mortik
mortik deleted the feat/reaction-handlers branch May 27, 2026 16:31
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