Skip to main content
The TopicManager class handles the creation and management of forum topics in the operator group, ensuring each user has a dedicated topic for their support conversation.

Class: TopicManager

Constructor

TopicManager(db: Database, operator_group_id: int)
Creates a new TopicManager instance.
db
Database
required
Database instance for storing conversation mappings
operator_group_id
int
required
Telegram chat ID of the operator group with forum topics enabled
Example:
from support_bot.db import Database
from support_bot.topic_manager import TopicManager

db = Database("./support_bot.sqlite3")
topics = TopicManager(db, operator_group_id=-1001234567890)

Public Methods

ensure_topic()

await topics.ensure_topic(bot: Bot, user: User) -> TopicRef
Ensures a forum topic exists for the user. If an active conversation already exists, returns the existing topic. Otherwise, creates a new forum topic and sends an introduction message.
bot
Bot
required
Aiogram Bot instance
user
User
required
Telegram User object representing the user
return
TopicRef
A TopicRef containing user_id and topic_id
Example:
topic = await topics.ensure_topic(bot, message.from_user)
print(f"User {topic.user_id} → Topic {topic.topic_id}")
Behavior:
  • Acquires a per-user lock to prevent race conditions
  • Checks for existing active conversation
  • Creates a new forum topic with format: "Full Name (@username) [user_id]"
  • Saves the conversation mapping to the database
  • Sends an introduction message with user details:
    New conversation.
    User: Alice Smith
    ID: 123456789
    Username: @alice
    

copy_user_message_to_topic()

await topics.copy_user_message_to_topic(bot: Bot, message: Message) -> TopicRef
Copies a user’s message to their forum topic in the operator group. Handles reply chains, link previews, and error recovery.
bot
Bot
required
Aiogram Bot instance
message
Message
required
The message from the user to copy
return
TopicRef
A TopicRef for the user’s topic
Example:
# From handlers/user.py
await topics.copy_user_message_to_topic(bot, message)
Features:
  • Preserves reply chains using find_linked_message_id()
  • Handles TelegramForbiddenError for messages with restricted content (e.g., links)
    • Falls back to sending text with link preview disabled
  • Detects closed/deleted topics via TelegramBadRequest
    • Automatically deactivates old conversation and creates new topic
  • Logs bidirectional message links for reply threading
Error Handling:
try:
    copy_result = await bot.copy_message(
        chat_id=self._operator_group_id,
        from_chat_id=message.chat.id,
        message_id=message.message_id,
        message_thread_id=topic.topic_id,
        reply_parameters=reply_params,
    )
except TelegramForbiddenError:
    # Fallback for links: send as text with preview disabled
    if message.content_type == "text" and _message_has_links(message):
        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,
        )
except TelegramBadRequest as err:
    if _is_thread_missing(err):
        # Topic was closed/deleted - create new one
        await self._db.deactivate_conversation(message.from_user.id)
        topic = await self.ensure_topic(bot, message.from_user)

Internal Methods

These methods are used internally by the TopicManager but are documented here for completeness.

_lock_for()

await topics._lock_for(user_id: int) -> asyncio.Lock
Returns a per-user lock to prevent concurrent topic creation for the same user.

_build_reply_params()

await topics._build_reply_params(
    *,
    source_chat_id: int,
    source_message: Message | None,
    target_chat_id: int
) -> ReplyParameters | None
Builds ReplyParameters for preserving reply chains when copying messages. Returns None if the source message is not a reply or if no linked message exists. Example usage:
reply_params = await self._build_reply_params(
    source_chat_id=message.chat.id,
    source_message=message.reply_to_message,
    target_chat_id=self._operator_group_id,
)
await topics._log_message_link(
    *,
    user_id: int,
    source_chat_id: int,
    source_message_id: int,
    target_chat_id: int,
    target_message_id: int
) -> None
Logs a bidirectional message link in a transaction. Creates two links:
  1. Source → Target
  2. Target → Source
This enables reply threading in both directions. Example:
# From topic_manager.py:202-218
async with self._db.transaction():
    await self._db.log_message_link(
        user_id=user_id,
        source_chat_id=source_chat_id,
        source_message_id=source_message_id,
        target_chat_id=target_chat_id,
        target_message_id=target_message_id,
        commit=False,
    )
    await self._db.log_message_link(
        user_id=user_id,
        source_chat_id=target_chat_id,
        source_message_id=target_message_id,
        target_chat_id=source_chat_id,
        target_message_id=source_message_id,
        commit=False,
    )

_extract_message_id()

@staticmethod
TopicManager._extract_message_id(result: object) -> int
Extracts the message ID from bot.copy_message() result, handling both MessageId objects and raw integers.

Data Models

TopicRef

@dataclass(frozen=True)
class TopicRef:
    user_id: int
    topic_id: int
Represents a reference to a user’s forum topic.
user_id
int
Telegram user ID
topic_id
int
Forum topic (message thread) ID in the operator group
Example:
topic = TopicRef(user_id=123456789, topic_id=456)
print(f"User {topic.user_id} has topic {topic.topic_id}")

Helper Functions

These internal helper functions support the TopicManager’s functionality.

_topic_name()

_topic_name(user: User) -> str
Generates a forum topic name from a user’s profile:
  • Format: "Full Name (@username) [user_id]"
  • Truncated to 128 characters (Telegram’s limit)
  • Falls back to “User” if no name is available
Example:
user = User(id=123, first_name="Alice", last_name="Smith", username="alice")
name = _topic_name(user)
# Returns: "Alice Smith (@alice) [123]"

_is_thread_missing()

_is_thread_missing(err: TelegramBadRequest) -> bool
Detects if a TelegramBadRequest error indicates a missing or closed forum topic. Detected error messages:
  • “message thread not found”
  • “message thread is not found”
  • “thread not found”
  • “topic” + “closed”
_message_has_links(message: Message) -> bool
Checks if a message contains URLs or text links. Used to determine if link preview should be disabled when falling back from copy_message() to send_message(). Detection:
  • Checks for url and text_link entities
  • Searches text for http://, https://, t.me/, www.

Build docs developers (and LLMs) love