fix(journal): ignore message edits so they stop recording duplicates

An edited `!journal ...` message was recorded as a brand-new entry, so
one message edited five times produced five near-identical rows.

Matrix delivers an edit as a fresh m.room.message, and mautrix swaps its
content for `m.new_content` during deserialization
(mautrix/types/event/message.py:393-396), stripping the "* " fallback
prefix. The handler therefore saw something byte-identical to a new
command. Neither @command.passive nor @command.new filters edits, so
guard on evt.content.get_edit() in the plugin.

Also corrects the README quirk that claimed edits could never reach the
bot — it asserted the opposite of the actual behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019wNB1sgkcBGijWjKTjtwG8
This commit is contained in:
Padreug 2026-09-14 16:56:20 +02:00
commit 3dc7eab2ab
3 changed files with 32 additions and 6 deletions

View file

@ -58,11 +58,26 @@ rather keep IDs monotonic across resets.)
## Known quirks ## Known quirks
- **Edited messages don't re-trigger the bot.** Matrix sends edits as - **Editing a recorded entry does nothing.** Edits are deliberately
a separate `m.replace` event that bots don't react to. If you typed ignored (since v0.2.1), so fixing a typo in a `!journal` message
`!journal` then edited the message to add content, the bot saw only leaves the stored entry unchanged. Send a fresh `!journal` message
the empty `!journal` and won't record. Send a fresh message instead instead of editing.
of editing.
This bullet previously claimed the opposite — that edits can't reach
the bot at all. That was wrong, and the bug it hid was noisy: mautrix
swaps an edit's content for `m.new_content` before the plugin sees it
(`mautrix/types/event/message.py:393-396`), stripping the `* `
fallback prefix. The handler therefore received something identical
to a brand-new command and recorded a **duplicate entry on every
edit** — one journal message edited five times produced five rows.
Neither `@command.passive` nor `@command.new` filters edits, so any
maubot plugin matching on message bodies needs its own
`if evt.content.get_edit(): return` guard.
- **Reactions are harmless.** They arrive as `m.reaction`, and
`@command.passive` only registers on `EventType.ROOM_MESSAGE` with
`msgtype` in `(m.text,)`, so a 👍 on a `!journal` message can never
trigger a recording.
- **`!journal show <random text>` runs the show query with that text - **`!journal show <random text>` runs the show query with that text
as the user filter.** If it doesn't match any MXID, you get as the user filter.** If it doesn't match any MXID, you get
"No entries." Use a fully-qualified MXID like `@pat:ariege.io`. "No entries." Use a fully-qualified MXID like `@pat:ariege.io`.

View file

@ -58,6 +58,17 @@ class JournalBot(Plugin):
@command.passive(regex=_JOURNAL_RE) @command.passive(regex=_JOURNAL_RE)
async def journal(self, evt: MessageEvent, match) -> None: async def journal(self, evt: MessageEvent, match) -> None:
# Ignore edits. An edit arrives as a *fresh* m.room.message whose
# content mautrix transparently replaces with `m.new_content`
# (mautrix/types/event/message.py:393-396), stripping the "* "
# fallback prefix. The result is byte-identical to a new command,
# so every edit of a `!journal ...` message would record another
# duplicate entry. The relation survives that swap, which is what
# get_edit() reads. Neither @command.passive nor @command.new
# filters edits, so plugins must do it themselves.
if evt.content.get_edit():
return
rest = (match[1] or "").strip() rest = (match[1] or "").strip()
if not rest: if not rest:

View file

@ -1,6 +1,6 @@
maubot: 0.1.0 maubot: 0.1.0
id: dev.aiolabs.journal id: dev.aiolabs.journal
version: 0.2.0 version: 0.2.1
license: AGPL-3.0-or-later license: AGPL-3.0-or-later
modules: modules:
- journal - journal