fix: wyr votes never counted — reactions arrive as ReactionEvent not UnknownEvent
Lint / Shell (shellcheck) (push) Successful in 12s
Lint / JS (eslint) (push) Successful in 8s
Lint / Python (ruff) (push) Successful in 11s
Lint / Python deps (pip-audit) (push) Successful in 49s
Lint / Secret scan (gitleaks) (push) Successful in 5s

nio has a dedicated ReactionEvent type with .reacts_to and .key attributes.
The callback was registered for UnknownEvent so reaction events were silently
dropped. Register for ReactionEvent and use its native attributes; keep the
UnknownEvent fallback for edge cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 14:00:03 -04:00
parent 126979f5cb
commit 80d77a8a0f
2 changed files with 23 additions and 23 deletions
+20 -22
View File
@@ -70,40 +70,38 @@ class Callbacks:
await wrapped(self.client, room.room_id, event.sender, args)
async def reaction(self, room, event):
"""Handle m.reaction events (sent as UnknownEvent by matrix-nio)."""
# Ignore events from before startup
"""Handle ReactionEvent (nio's native reaction type)."""
if self.startup_sync_token is None:
return
# Ignore our own reactions
if event.sender == MATRIX_USER_ID:
return
# m.reaction events come as UnknownEvent with type "m.reaction"
reacted_event_id = event.reacts_to
key = event.key
logger.info("reaction: key=%r target=%s sender=%s", key, reacted_event_id[:16], event.sender)
await handle_welcome_reaction(self.client, room.room_id, event.sender, reacted_event_id, key)
record_wyr_vote(reacted_event_id, event.sender, key)
async def unknown_event(self, room, event):
"""Fallback handler for UnknownEvent — catches any m.reaction not parsed by nio."""
if self.startup_sync_token is None:
return
if event.sender == MATRIX_USER_ID:
return
if not hasattr(event, "source"):
logger.info("reaction: event has no source attr, type=%s sender=%s", type(event).__name__, event.sender)
return
event_type = event.source.get("type", "")
content = event.source.get("content", {})
relates_to = content.get("m.relates_to", {})
rel_type = relates_to.get("rel_type", "")
reacted_event_id = relates_to.get("event_id", "")
key = relates_to.get("key", "")
logger.info(
"reaction: type=%s rel_type=%s key=%r target=%s sender=%s",
event_type, rel_type, key, reacted_event_id[:16] if reacted_event_id else "", event.sender,
)
if rel_type != "m.annotation":
if relates_to.get("rel_type") != "m.annotation":
return
await handle_welcome_reaction(
self.client, room.room_id, event.sender, reacted_event_id, key
)
from commands import _WYR_POLLS
logger.info("reaction: wyr polls active=%s matched=%s", list(_WYR_POLLS.keys()), reacted_event_id in _WYR_POLLS)
reacted_event_id = relates_to.get("event_id", "")
key = relates_to.get("key", "")
logger.info("unknown_event reaction: key=%r target=%s sender=%s", key, reacted_event_id[:16], event.sender)
await handle_welcome_reaction(self.client, room.room_id, event.sender, reacted_event_id, key)
record_wyr_vote(reacted_event_id, event.sender, key)
async def member(self, room, event):