Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions modules/channels/telegram/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ class TelegramSession(Base):
# Composite primary key: bot_id + user_id
bot_id: Mapped[str] = mapped_column(String(50), primary_key=True, default="default")
user_id: Mapped[int] = mapped_column(Integer, primary_key=True)
chat_session_id: Mapped[str] = mapped_column(
chat_session_id: Mapped[Optional[str]] = mapped_column(
String(50),
ForeignKey("chat_sessions.id", ondelete="CASCADE"),
ForeignKey("chat_sessions.id", ondelete="SET NULL"),
nullable=True,
)
username: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
first_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
Expand Down
18 changes: 18 additions & 0 deletions modules/channels/telegram/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,24 @@ async def admin_get_bot_instance_sessions(
return {"sessions": sessions}


@router.post("/instances/{instance_id}/register-user")
async def register_bot_user(instance_id: str, request: dict):
"""Register a Telegram user for a bot instance (called by bot middleware, no auth)."""
user_id = request.get("user_id")
if not user_id:
raise HTTPException(status_code=400, detail="user_id required")

await telegram_session_service.register_user(
user_id=user_id,
bot_id=instance_id,
username=request.get("username"),
first_name=request.get("first_name"),
last_name=request.get("last_name"),
)

return {"status": "ok"}


@router.post("/instances/{instance_id}/sessions")
async def admin_create_bot_instance_session(instance_id: str, request: dict):
"""Create/register a session for a bot instance (used by telegram_bot_service)"""
Expand Down
60 changes: 60 additions & 0 deletions modules/channels/telegram/service.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Telegram channel services."""

import logging
from datetime import datetime
from typing import Any, Dict, List, Optional

from db.database import AsyncSessionLocal
from db.repositories import BotInstanceRepository, TelegramRepository, UserIdentityRepository
from db.retry import retry_on_busy
from modules.channels.telegram.models import TelegramSession


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -171,6 +173,64 @@ async def set_session(

await session.commit()

async def register_user(
self,
user_id: int,
bot_id: str,
username: Optional[str] = None,
first_name: Optional[str] = None,
last_name: Optional[str] = None,
) -> None:
"""Register a Telegram user for a bot (without chat session)."""
async with AsyncSessionLocal() as session:
repo = TelegramRepository(session, bot_id=bot_id)
existing = await repo.get_session(user_id, bot_id=bot_id)
if existing:
# Update user info
from sqlalchemy import select

result = await session.execute(
select(TelegramSession).where(
TelegramSession.bot_id == bot_id,
TelegramSession.user_id == user_id,
)
)
ts = result.scalar_one_or_none()
if ts:
ts.username = username
ts.first_name = first_name
ts.last_name = last_name
ts.updated = datetime.utcnow()
else:
ts = TelegramSession(
bot_id=bot_id,
user_id=user_id,
chat_session_id=None,
username=username,
first_name=first_name,
last_name=last_name,
)
session.add(ts)

# Track identity
try:
identity_repo = UserIdentityRepository(session)
display = first_name or username or str(user_id)
await identity_repo.find_or_create(
provider="telegram",
provider_uid=str(user_id),
display_name=display,
metadata_dict={
"username": username,
"first_name": first_name,
"last_name": last_name,
},
)
except Exception:
logger.debug("Failed to track Telegram identity for %s", user_id, exc_info=True)

await session.commit()

async def get_all_sessions(self, bot_id: Optional[str] = None) -> List[dict]:
"""Get all sessions (optionally for specific bot)."""
async with AsyncSessionLocal() as session:
Expand Down
58 changes: 58 additions & 0 deletions scripts/migrate_telegram_sessions_nullable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Make telegram_sessions.chat_session_id nullable.

Allows registering users before they have a chat session.
"""

import sqlite3
import sys


DB_PATH = sys.argv[1] if len(sys.argv) > 1 else "data/secretary.db"


def migrate(db_path: str) -> None:
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=OFF")

# Check current schema
schema = conn.execute("SELECT sql FROM sqlite_master WHERE name='telegram_sessions'").fetchone()
if not schema:
print("telegram_sessions table not found — nothing to do")
return

# SQLite doesn't support ALTER COLUMN, so we recreate the table
conn.executescript("""
BEGIN;

CREATE TABLE telegram_sessions_new (
bot_id VARCHAR(50) NOT NULL DEFAULT 'default',
user_id INTEGER NOT NULL,
chat_session_id VARCHAR(50) REFERENCES chat_sessions(id) ON DELETE SET NULL,
username VARCHAR(100),
first_name VARCHAR(100),
last_name VARCHAR(100),
created DATETIME DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (bot_id, user_id)
);

INSERT INTO telegram_sessions_new
SELECT bot_id, user_id, chat_session_id, username, first_name, last_name, created, updated
FROM telegram_sessions;

DROP TABLE telegram_sessions;

ALTER TABLE telegram_sessions_new RENAME TO telegram_sessions;

CREATE INDEX ix_telegram_sessions_bot_user ON telegram_sessions(bot_id, user_id);

COMMIT;
""")
conn.execute("PRAGMA foreign_keys=ON")
conn.close()
print(f"Migration done: chat_session_id is now nullable in {db_path}")


if __name__ == "__main__":
migrate(DB_PATH)
Loading