feat(journal): farm-journal maubot plugin

Records what people did each day, scoped per-room/sender/timestamp.
Stored in maubot's per-instance SQLite plugin DB.

Commands:
- !journal <text>     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) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-20 23:47:10 +02:00
commit 153b164284
3 changed files with 101 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.mbp
__pycache__/
*.pyc
.venv/

88
journal/journal.py Normal file
View file

@ -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 <what you did>` — 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))

9
journal/maubot.yaml Normal file
View file

@ -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