From 153b164284aed96b0a287da0f1118eedfeadab7b Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 20 May 2026 23:47:10 +0200 Subject: [PATCH] feat(journal): farm-journal maubot plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records what people did each day, scoped per-room/sender/timestamp. Stored in maubot's per-instance SQLite plugin DB. Commands: - !journal record an entry - !journal show [@u] last 10 entries, optionally filtered by user - !journal today everything logged today (UTC) The command shape requires @command.new(arg_fallthrough=False) so the parent's pass_raw=True `text` argument doesn't swallow subcommand keywords like "show" and "today" — same pattern the upstream maubot reminder plugin uses. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 +++ journal/journal.py | 88 +++++++++++++++++++++++++++++++++++++++++++++ journal/maubot.yaml | 9 +++++ 3 files changed, 101 insertions(+) create mode 100644 .gitignore create mode 100644 journal/journal.py create mode 100644 journal/maubot.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc37320 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.mbp +__pycache__/ +*.pyc +.venv/ diff --git a/journal/journal.py b/journal/journal.py new file mode 100644 index 0000000..fdb3a3e --- /dev/null +++ b/journal/journal.py @@ -0,0 +1,88 @@ +from datetime import datetime, timezone +from typing import Optional + +from maubot import MessageEvent, Plugin +from maubot.handlers import command +from mautrix.util.async_db import Connection, UpgradeTable + +upgrade_table = UpgradeTable() + + +@upgrade_table.register(description="Initial schema") +async def upgrade_v1(conn: Connection) -> None: + await conn.execute( + """ + CREATE TABLE entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user TEXT NOT NULL, + room TEXT NOT NULL, + ts BIGINT NOT NULL, + text TEXT NOT NULL + ) + """ + ) + await conn.execute("CREATE INDEX entries_user_ts ON entries (user, ts DESC)") + await conn.execute("CREATE INDEX entries_ts ON entries (ts DESC)") + + +def _fmt(rows) -> str: + if not rows: + return "No entries." + out = [] + for r in rows: + when = datetime.fromtimestamp(r["ts"] / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M") + out.append(f"- **{r['user']}** _({when} UTC)_: {r['text']}") + return "\n".join(out) + + +class JournalBot(Plugin): + @classmethod + def get_db_upgrade_table(cls) -> UpgradeTable: + return upgrade_table + + @command.new( + "journal", + help="Farm journal — record what you did today", + require_subcommand=False, + arg_fallthrough=False, + ) + @command.argument("text", pass_raw=True, required=False) + async def journal(self, evt: MessageEvent, text: str = "") -> None: + if not text: + await evt.reply( + "Usage:\n" + "- `!journal ` — record an entry\n" + "- `!journal show [@user]` — last 10 entries (optionally filtered by user)\n" + "- `!journal today` — all entries from today" + ) + return + + await self.database.execute( + "INSERT INTO entries (user, room, ts, text) VALUES ($1, $2, $3, $4)", + evt.sender, evt.room_id, evt.timestamp, text, + ) + await evt.reply(f"📓 Logged for {evt.sender}.") + + @journal.subcommand("show", help="Show recent entries, optionally filtered by user") + @command.argument("user", required=False) + async def show(self, evt: MessageEvent, user: Optional[str] = None) -> None: + if user: + rows = await self.database.fetch( + "SELECT user, ts, text FROM entries WHERE user = $1 ORDER BY ts DESC LIMIT 10", + user, + ) + else: + rows = await self.database.fetch( + "SELECT user, ts, text FROM entries ORDER BY ts DESC LIMIT 10", + ) + await evt.reply(_fmt(rows)) + + @journal.subcommand("today", help="All entries from today (UTC) across users") + async def today(self, evt: MessageEvent) -> None: + midnight = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0) + cutoff_ms = int(midnight.timestamp() * 1000) + rows = await self.database.fetch( + "SELECT user, ts, text FROM entries WHERE ts >= $1 ORDER BY ts ASC", + cutoff_ms, + ) + await evt.reply(_fmt(rows)) diff --git a/journal/maubot.yaml b/journal/maubot.yaml new file mode 100644 index 0000000..9352d83 --- /dev/null +++ b/journal/maubot.yaml @@ -0,0 +1,9 @@ +maubot: 0.1.0 +id: dev.aiolabs.journal +version: 0.1.3 +license: AGPL-3.0-or-later +modules: + - journal +main_class: JournalBot +database: true +database_type: asyncpg