await message.answer( "<b>Welcome to Our Support!</b>\n\n" "Our team will respond <i>shortly</i>.\n\n" "Please use the format:\n" "<code>Issue: [description]</code>")
3
Restart the bot
After making changes, restart your bot:
# For systemdsudo systemctl restart telegram-support-bot# For Dockerdocker restart support-bot
The default topic name includes the user’s full name, username, and ID:
support_bot/topic_manager.py (lines 19-24)
def _topic_name(user: User) -> str: base = user.full_name or "User" if user.username: base = f"{base} (@{user.username})" name = f"{base} [{user.id}]" return name[:128]
Edit the _topic_name function in support_bot/topic_manager.py:
def _topic_name(user: User) -> str: # Just show username and ID if user.username: return f"@{user.username} ({user.id})" # Fallback to first name and ID return f"{user.first_name or 'User'} ({user.id})"
Option 2: Add emojis
Make topics more visual with emojis:
def _topic_name(user: User) -> str: base = user.full_name or "User" if user.username: base = f"👤 {base} (@{user.username})" else: base = f"👤 {base}" name = f"{base} • {user.id}" return name[:128]
Option 3: Include timestamp
Add the current time to the topic name:
from datetime import datetimedef _topic_name(user: User) -> str: timestamp = datetime.now().strftime("%H:%M") base = user.full_name or "User" if user.username: base = f"{base} (@{user.username})" name = f"[{timestamp}] {base} [{user.id}]" return name[:128]
Telegram limits topic names to 128 characters. The function automatically truncates longer names with [:128].
Open support_bot/handlers/user.py and add a new handler:
@router.message(F.text == "/help", F.chat.type == "private")async def help_command( message: Message, bot: Bot, db: Database, topics: TopicManager) -> None: await message.answer( "<b>Available Commands:</b>\n" "/start - Start a new conversation\n" "/help - Show this help message\n" "/status - Check your support ticket status\n\n" "Just send a message to contact support!" )
2
Add a status command
Create a handler to show conversation status:
@router.message(F.text == "/status", F.chat.type == "private")async def status_command( message: Message, db: Database) -> None: if message.from_user is None: return conversation = await db.get_active_conversation(message.from_user.id) if conversation: await message.answer( "✅ You have an active support conversation.\n" "Our team will respond soon!" ) else: await message.answer( "📭 You don't have any active conversations.\n" "Send a message to start one!" )
3
Register the router
The router is automatically registered in main.py:
Add handlers for photos, documents, or other media:
@router.message(F.photo, F.chat.type == "private")async def photo_message( message: Message, bot: Bot, db: Database, topics: TopicManager) -> None: if message.from_user is None: return await _log_user_message(db, message, log_messages=True) await topics.copy_user_message_to_topic(bot, message) await message.answer("📸 Photo received! Our team will review it.")
The any_private_message handler (line 50 in user.py) catches all messages that don’t match specific handlers, so most content types are already handled.
CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY, username TEXT, first_name TEXT, last_name TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);CREATE TABLE IF NOT EXISTS conversations ( user_id INTEGER PRIMARY KEY, topic_id INTEGER NOT NULL, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE);CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, direction TEXT NOT NULL, -- 'user' or 'operator' chat_id INTEGER NOT NULL, message_id INTEGER NOT NULL, content_type TEXT NOT NULL, text TEXT, caption TEXT, file_id TEXT, payload_json TEXT, created_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE);CREATE TABLE IF NOT EXISTS message_links ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, source_chat_id INTEGER NOT NULL, source_message_id INTEGER NOT NULL, target_chat_id INTEGER NOT NULL, target_message_id INTEGER NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE);
Edit support_bot/db.py and add your custom table in the init method:
async def init(self) -> None: await self.conn.executescript( """ -- Existing tables... CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, tag_name TEXT NOT NULL, created_at TEXT NOT NULL, FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_tags_user_id ON tags(user_id); """ ) await self.conn.commit()
2
Add database methods
Add methods to interact with your custom table:
async def add_tag(self, user_id: int, tag_name: str) -> None: await self.conn.execute( "INSERT INTO tags (user_id, tag_name, created_at) VALUES (?, ?, ?)", (user_id, tag_name, _now_iso()), ) await self.conn.commit()async def get_user_tags(self, user_id: int) -> list[str]: cur = await self.conn.execute( "SELECT tag_name FROM tags WHERE user_id = ?", (user_id,), ) rows = await cur.fetchall() await cur.close() return [row[0] for row in rows]
The bot disables link previews when copying messages with links:
support_bot/topic_manager.py (lines 115-124)
if message.content_type == "text" and _message_has_links(message): try: sent = await bot.send_message( chat_id=self._operator_group_id, message_thread_id=topic.topic_id, text=message.text or "", entities=message.entities, link_preview_options=LinkPreviewOptions(is_disabled=True), reply_parameters=reply_params, )
To enable link previews, change is_disabled=True to is_disabled=False.