From b6189a17fff93c87567f40e5730771f1724bfd64 Mon Sep 17 00:00:00 2001 From: Jean Galea <1651502+jgalea@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:54:51 +0100 Subject: [PATCH 1/2] feat: store reply_to_msg_id and reply_to_top_id for forum topic support Adds two new columns to the messages table: - reply_to_msg_id: the message being replied to - reply_to_top_id: the forum topic root message ID This enables grouping messages by forum topic in supergroups, which is essential for summarizing conversations by category. Includes automatic schema migration for existing databases. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/tg_cli/client.py | 17 ++++++++++++++ src/tg_cli/db.py | 55 +++++++++++++++++++++++++++++++++----------- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/tg_cli/client.py b/src/tg_cli/client.py index b11cabd..6d88fba 100644 --- a/src/tg_cli/client.py +++ b/src/tg_cli/client.py @@ -209,6 +209,13 @@ async def fetch_history( if ts and ts.tzinfo is None: ts = ts.replace(tzinfo=timezone.utc) + # Extract reply_to IDs (reply_to_top_id = topic ID in forum groups) + reply_to_msg_id = None + reply_to_top_id = None + if msg.reply_to: + reply_to_msg_id = getattr(msg.reply_to, "reply_to_msg_id", None) + reply_to_top_id = getattr(msg.reply_to, "reply_to_top_id", None) + batch.append( dict( chat_id=chat_id, @@ -218,6 +225,8 @@ async def fetch_history( sender_name=sender_name, content=content, timestamp=ts or datetime.now(timezone.utc), + reply_to_msg_id=reply_to_msg_id, + reply_to_top_id=reply_to_top_id, ) ) @@ -352,6 +361,12 @@ async def handler(event): if ts and ts.tzinfo is None: ts = ts.replace(tzinfo=timezone.utc) + reply_to_msg_id = None + reply_to_top_id = None + if msg.reply_to: + reply_to_msg_id = getattr(msg.reply_to, "reply_to_msg_id", None) + reply_to_top_id = getattr(msg.reply_to, "reply_to_top_id", None) + db.insert_message( chat_id=chat.id, chat_name=chat_name, @@ -360,6 +375,8 @@ async def handler(event): sender_name=sender_name, content=content, timestamp=ts or datetime.now(timezone.utc), + reply_to_msg_id=reply_to_msg_id, + reply_to_top_id=reply_to_top_id, ) time_str = ts.strftime("%H:%M:%S") if ts else "??:??:??" diff --git a/src/tg_cli/db.py b/src/tg_cli/db.py index c54edfc..f5ced07 100644 --- a/src/tg_cli/db.py +++ b/src/tg_cli/db.py @@ -16,20 +16,27 @@ _CREATE_TABLE = """ CREATE TABLE IF NOT EXISTS messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - platform TEXT NOT NULL DEFAULT 'telegram', - chat_id INTEGER NOT NULL, - chat_name TEXT, - msg_id INTEGER NOT NULL, - sender_id INTEGER, - sender_name TEXT, - content TEXT, - timestamp TEXT NOT NULL, - raw_json TEXT, + id INTEGER PRIMARY KEY AUTOINCREMENT, + platform TEXT NOT NULL DEFAULT 'telegram', + chat_id INTEGER NOT NULL, + chat_name TEXT, + msg_id INTEGER NOT NULL, + sender_id INTEGER, + sender_name TEXT, + content TEXT, + timestamp TEXT NOT NULL, + raw_json TEXT, + reply_to_msg_id INTEGER, + reply_to_top_id INTEGER, UNIQUE(platform, chat_id, msg_id) ); """ +_MIGRATIONS = [ + "ALTER TABLE messages ADD COLUMN reply_to_msg_id INTEGER", + "ALTER TABLE messages ADD COLUMN reply_to_top_id INTEGER", +] + _CREATE_INDEX = """ CREATE INDEX IF NOT EXISTS idx_messages_chat_ts ON messages(chat_id, timestamp); CREATE INDEX IF NOT EXISTS idx_messages_content ON messages(content); @@ -64,6 +71,16 @@ def __init__(self, db_path: Path | str | None = None): self.conn.row_factory = sqlite3.Row self.conn.execute("PRAGMA journal_mode=WAL") self.conn.executescript(_CREATE_TABLE + _CREATE_INDEX) + self._run_migrations() + + def _run_migrations(self): + """Apply schema migrations that add columns to existing tables.""" + for stmt in _MIGRATIONS: + try: + self.conn.execute(stmt) + self.conn.commit() + except sqlite3.OperationalError: + pass # Column already exists def __enter__(self): return self @@ -118,6 +135,8 @@ def insert_message( content: str | None, timestamp: datetime, raw_json: dict[str, Any] | None = None, + reply_to_msg_id: int | None = None, + reply_to_top_id: int | None = None, ) -> bool: """Insert a message, returns True if inserted (not duplicate).""" try: @@ -132,9 +151,11 @@ def insert_message( sender_name, content, timestamp, - raw_json + raw_json, + reply_to_msg_id, + reply_to_top_id ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( platform, chat_id, @@ -145,6 +166,8 @@ def insert_message( content, timestamp.isoformat(), json.dumps(raw_json, ensure_ascii=False) if raw_json else None, + reply_to_msg_id, + reply_to_top_id, ), ) self.conn.commit() @@ -175,6 +198,8 @@ def insert_batch(self, messages: list[dict], platform: str = "telegram") -> int: else m["timestamp"] ), json.dumps(m["raw_json"], ensure_ascii=False) if m.get("raw_json") else None, + m.get("reply_to_msg_id"), + m.get("reply_to_top_id"), ) for m in messages ] @@ -191,9 +216,11 @@ def insert_batch(self, messages: list[dict], platform: str = "telegram") -> int: sender_name, content, timestamp, - raw_json + raw_json, + reply_to_msg_id, + reply_to_top_id ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", rows, ) self.conn.commit() From cac87d957bcf45417c1ee7b6b1896dcfbbf9a874 Mon Sep 17 00:00:00 2001 From: Jean Galea <1651502+jgalea@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:56:54 +0100 Subject: [PATCH 2/2] feat: add --reply-to and --no-preview flags to send command Enables sending to specific forum topics and suppressing link previews. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/tg_cli/cli/tg.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/tg_cli/cli/tg.py b/src/tg_cli/cli/tg.py index bda15ba..b5c6daa 100644 --- a/src/tg_cli/cli/tg.py +++ b/src/tg_cli/cli/tg.py @@ -403,13 +403,20 @@ async def _run(): @tg_group.command("send") @click.argument("chat") @click.argument("message") +@click.option("-r", "--reply-to", type=int, help="Message ID to reply to (topic ID for forum groups)") +@click.option("--no-preview", is_flag=True, help="Disable link preview") @structured_output_options -def tg_send(chat: str, message: str, as_json: bool, as_yaml: bool): +def tg_send(chat: str, message: str, reply_to: int | None, no_preview: bool, as_json: bool, as_yaml: bool): """Send a MESSAGE to CHAT (name, username, or numeric ID).""" async def _run(): async with connect() as client: - msg = await client.send_message(_parse_chat(chat), message) + msg = await client.send_message( + _parse_chat(chat), + message, + reply_to=reply_to, + link_preview=not no_preview, + ) return msg msg = asyncio.run(_run())