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()) 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()