From 39687bce679c0a8f4212e64afa8f38e3eb603395 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 15:45:55 +0800 Subject: [PATCH 01/68] feat: add telegram topics support Adds support for Telegram topics by: - Adding topic association database model to track topic linkages - Implementing thread_id handling in message processing - Adding topic group configuration option - Enhancing message editing and reply handling for topics - Improving error handling and logging for topic operations This change allows mapping slave chats to Telegram topics within a designated forum group, enabling better organization of multi-chat conversations. --- efb_telegram_master/__init__.py | 8 +- efb_telegram_master/bot_manager.py | 5 + efb_telegram_master/chat_binding.py | 2 +- efb_telegram_master/db.py | 61 +++++++ efb_telegram_master/master_message.py | 7 + efb_telegram_master/slave_message.py | 239 ++++++++++++++++++-------- efb_telegram_master/utils.py | 1 + 7 files changed, 248 insertions(+), 75 deletions(-) diff --git a/efb_telegram_master/__init__.py b/efb_telegram_master/__init__.py index 8331ac8a..15d06cfa 100644 --- a/efb_telegram_master/__init__.py +++ b/efb_telegram_master/__init__.py @@ -128,6 +128,9 @@ def __init__(self, instance_id: InstanceID = None): self.commands: CommandsManager = CommandsManager(self) self.chat_binding: ChatBindingManager = ChatBindingManager(self) self.slave_messages: SlaveMessageProcessor = SlaveMessageProcessor(self) + self.topic_group: Optional[TelegramChatID] = None + if self.config.get('topic_group', None): + self.topic_group: Optional[TelegramChatID] = TelegramChatID(self.config['topic_group']) if not self.flag('auto_locale'): self.translator = translation("efb_telegram_master", @@ -322,9 +325,10 @@ def start(self, update: Update, context: CallbackContext): assert isinstance(update.effective_message, telegram.Message) assert isinstance(update.effective_chat, telegram.Chat) if context.args: # Group binding command - if update.effective_message.chat.type != telegram.Chat.PRIVATE or \ + if (update.effective_message.chat.type != telegram.Chat.PRIVATE and update.effective_chat.id != self.topic_group) or \ (update.effective_message.forward_from_chat and - update.effective_message.forward_from_chat.type == telegram.Chat.CHANNEL): + update.effective_message.forward_from_chat.type == telegram.Chat.CHANNEL and + update.effective_message.forward_from_chat.id != self.topic_group): self.chat_binding.link_chat(update, context.args) else: self.bot_manager.send_message(update.effective_chat.id, diff --git a/efb_telegram_master/bot_manager.py b/efb_telegram_master/bot_manager.py index 116633c7..80e86d34 100644 --- a/efb_telegram_master/bot_manager.py +++ b/efb_telegram_master/bot_manager.py @@ -543,6 +543,11 @@ def answer_callback_query(self, *args, prefix="", suffix="", text=None, *args, text=prefix + text + suffix, **kwargs ) + @Decorators.retry_on_timeout + @Decorators.retry_on_chat_migration + def create_forum_topic(self, *args, **kwargs): + return self.updater.bot.create_forum_topic(*args, **kwargs) + @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration def set_chat_title(self, *args, **kwargs): diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index b6accf56..18075010 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -475,7 +475,7 @@ def build_link_action_message(self, chat: ETMChatType, def link_chat_exec(self, update: Update, context: CallbackContext) -> int: """ - Action to link a chat. Triggered by callback message with status `Flags.EXEC_LINK`. + Action to link a chat. Triggered by callback message with status `Flags.LINK_EXEC`. """ assert isinstance(update, Update) assert update.effective_chat diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 0a3a0669..63aad0a3 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -55,6 +55,11 @@ class Meta: database = database +class TopicAssoc(BaseModel): + topic_chat_id = TextField() + message_thread_id = TextField() + slave_uid = TextField() + class ChatAssoc(BaseModel): master_uid = TextField() slave_uid = TextField() @@ -67,6 +72,8 @@ class MsgLog(BaseModel): """Editable message ID from Telegram if ``master_msg_id`` is not editable and a separate one is sent. """ + master_message_thread_id = TextField(null=True) + """Message thread ID from Telegram""" slave_message_id = TextField() """Message from slave channel.""" text = TextField() @@ -374,6 +381,60 @@ def get_chat_assoc(master_uid: Optional[EFBChannelChatIDStr] = None, except DoesNotExist: return [] + def add_topic_assoc(self, message_thread_id: EFBChannelChatIDStr, + slave_uid: EFBChannelChatIDStr, + topic_chat_id: int): + """ + Add topic associations (topic links). + One Master channel with many Slave channel. + + Args: + message_thread_id (str): thread UID in topic + slave_uid (str): Slave channel UID ("%(channel_id)s.%(chat_id)s") + """ + return TopicAssoc.create(topic_chat_id=topic_chat_id, message_thread_id=message_thread_id, slave_uid=slave_uid) + + @staticmethod + def get_topic_thread_id(topic_chat_id: int, + slave_uid: Optional[EFBChannelChatIDStr] + ) -> int: + """ + Get topic association (topic link) information. + Only one parameter is to be provided. + + Args: + topic_chat_id (int): The topic UID + slave_uid (str): Slave channel UID ("%(channel_id)s.%(chat_id)s") + + Returns: + The message thread_id + """ + try: + return int(TopicAssoc.select(TopicAssoc.message_thread_id)\ + .where(TopicAssoc.slave_uid == slave_uid, TopicAssoc.topic_chat_id == topic_chat_id).first().message_thread_id) + except DoesNotExist: + return None + + def get_topic_slave(topic_chat_id: int, + message_thread_id: int + ) -> Optional[EFBChannelChatIDStr]: + """ + Get topic association (topic link) information. + Only one parameter is to be provided. + + Args: + topic_chat_id (int): The topic UID + message_thread_id (int): The message thread ID + + Returns: + Slave channel UID ("%(channel_id)s.%(chat_id)s") + """ + try: + return TopicAssoc.select(TopicAssoc.slave_uid)\ + .where(TopicAssoc.message_thread_id == message_thread_id, TopicAssoc.topic_chat_id == topic_chat_id).first().slave_uid + except DoesNotExist: + return None + def add_or_update_message_log(self, msg: ETMMsg, master_message: Message, diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index 726f7bc0..7aa0ce83 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -162,10 +162,17 @@ def msg(self, update: Update, context: CallbackContext): quote = message.reply_to_message is not None self.logger.debug("[%s] Chat %s is singly-linked to %s", mid, message.chat, destination) + if destination is None: + if thread_id and TelegramChatID(update.effective_chat.id) == self.channel.topic_group: + destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=self.channel.topic_group) + if destination: + quote = message.reply_to_message is not None + if destination is None: # not singly linked quote = False self.logger.debug("[%s] Chat %s is not singly-linked", mid, update.effective_chat) reply_to = message.reply_to_message + thread_id = message.message_thread_id cached_dest = self.chat_dest_cache.get(str(message.chat.id)) if reply_to: self.logger.debug("[%s] Message is quote-replying to %s", mid, reply_to) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 8c4e9eeb..e47e73d8 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -34,7 +34,7 @@ from .locale_mixin import LocaleMixin from .message import ETMMsg from .msg_type import get_msg_type -from .utils import TelegramChatID, TelegramMessageID, OldMsgID +from .utils import TelegramChatID, TelegramTopicThreadID, TelegramMessageID, OldMsgID if TYPE_CHECKING: from . import TelegramChannel @@ -90,7 +90,7 @@ def send_message(self, msg: Message) -> Message: xid = msg.uid self.logger.debug("[%s] Slave message delivered to ETM.\n%s", xid, msg) - msg_template, tg_dest = self.get_slave_msg_dest(msg) + msg_template, (tg_dest, thread_id) = self.get_slave_msg_dest(msg) silent = self.is_silent(msg) if silent is None: @@ -117,14 +117,16 @@ def send_message(self, msg: Message) -> Message: 'but it does not exist in database. Sending new message instead.', msg.uid) - self.dispatch_message(msg, msg_template, old_msg_id, tg_dest, silent) + self.dispatch_message(msg, msg_template, old_msg_id, tg_dest, thread_id, silent) except Exception as e: self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", repr(msg), repr(e), traceback.format_exc()) return msg def dispatch_message(self, msg: Message, msg_template: str, - old_msg_id: Optional[OldMsgID], tg_dest: TelegramChatID, + old_msg_id: Optional[OldMsgID], + tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], silent: bool = False): """Dispatch with header, destination and Telegram message ID and destinations.""" @@ -143,6 +145,8 @@ def dispatch_message(self, msg: Message, msg_template: str, else: self.logger.debug("[%s] Target message has database entry: %s.", msg.uid, log) target_msg = utils.message_id_str_to_id(log.master_msg_id) + # Assuming target_msg = (chat_id, message_id). Thread ID might need separate handling/DB storage. + # We only check if the reply target is in the same main chat. Replying across topics is allowed by Telegram. if not target_msg or target_msg[0] != int(tg_dest): self.logger.error('[%s] Trying to reply to a message not from this chat. ' 'Message destination: %s. Target message: %s.', @@ -168,46 +172,47 @@ def dispatch_message(self, msg: Message, msg_template: str, # Type dispatching if msg.type == MsgType.Text: - tg_msg = self.slave_message_text(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_text(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.Link: - tg_msg = self.slave_message_link(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_link(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.Sticker: - tg_msg = self.slave_message_sticker(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_sticker(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.Image: if self.flag("send_image_as_file"): - tg_msg = self.slave_message_file(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_file(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) else: - tg_msg = self.slave_message_image(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_image(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.Animation: - tg_msg = self.slave_message_animation(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_animation(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.File: - tg_msg = self.slave_message_file(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_file(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.Voice: - tg_msg = self.slave_message_voice(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_voice(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.Location: - tg_msg = self.slave_message_location(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_location(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.Video: - tg_msg = self.slave_message_video(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, + tg_msg = self.slave_message_video(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) elif msg.type == MsgType.Status: # Status messages are not to be recorded in databases - return self.slave_message_status(msg, tg_dest) + return self.slave_message_status(msg, tg_dest, thread_id) elif msg.type == MsgType.Unsupported: - tg_msg = self.slave_message_unsupported(msg, tg_dest, msg_template, reactions, old_msg_id, + tg_msg = self.slave_message_unsupported(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) else: self.bot.send_chat_action(tg_dest, ChatAction.TYPING) tg_msg = self.bot.send_message(tg_dest, prefix=msg_template, suffix=reactions, disable_notification=silent, + message_thread_id=thread_id, text=self._('Unknown type of message "{0}". (UT01)') .format(msg.type.name)) @@ -225,20 +230,23 @@ def dispatch_message(self, msg: Message, msg_template: str, self.db.add_or_update_message_log(etm_msg, tg_msg, old_msg_id) # self.logger.debug("[%s] Message inserted/updated to the database.", xid) - def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Optional[TelegramChatID]]: + def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[TelegramChatID], Optional[TelegramTopicThreadID]]]: """Get the Telegram destination of a message with its header. Returns: msg_template (str): header of the message. - tg_dest (Optional[str]): Telegram destination chat, None if muted. + (Optional[TelegramChatID], Optional[TelegramTopicID]): Telegram destination chat ID and thread ID, None if muted. """ xid = msg.uid msg.chat = self.chat_manager.update_chat_obj(msg.chat) - msg.author = self.chat_manager.get_or_enrol_member(msg.chat, msg.author) + author = self.chat_manager.get_or_enrol_member(msg.chat, msg.author) + msg.author = author chat_uid = utils.chat_id_to_str(chat=msg.chat) tg_chats = self.db.get_chat_assoc(slave_uid=chat_uid) tg_chat = None + tg_dest: Optional[TelegramChatID] = None + thread_id: Optional[TelegramTopicThreadID] = None if tg_chats: tg_chat = tg_chats[0] @@ -257,6 +265,20 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Optional[TelegramChatID if tg_chat: # if this chat is linked tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1])) + elif self.channel.topic_group: + tg_dest = self.channel.topic_group + thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) + if not thread_id: + thread_id: int = self.bot.create_forum_topic( + chat_id=self.channel.topic_group, + name=author + ) + self.db.add_topic_assoc( + topic_chat_id=self.channel.topic_group, + message_thread_id=thread_id, + slave_uid=chat_uid, + ) + return "", (tg_dest, thread_id) else: singly_linked = False @@ -267,7 +289,8 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Optional[TelegramChatID if self.chat_dest_cache.get(str(tg_dest)) != chat_uid: self.chat_dest_cache.remove(str(tg_dest)) - return msg_template, tg_dest + return msg_template, (tg_dest, thread_id) + def html_substitutions(self, msg: Message) -> str: """Build a Telegram-flavored HTML string for message text substitutions.""" @@ -294,7 +317,8 @@ def html_substitutions(self, msg: Message) -> str: return html.escape(text) return text - def slave_message_text(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_text(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -303,15 +327,15 @@ def slave_message_text(self, msg: Message, tg_dest: TelegramChatID, msg_template Send message as text to Telegram. Args: - msg: Message - tg_dest: Telegram Chat ID + msg (Message): Message + tg_dest (TelegramChatID): Telegram Chat ID + thread_id (Optional[TelegramTopicID]): Telegram Thread ID msg_template: Header of the message reactions: Footer of the message old_msg_id: Telegram message ID to edit target_msg_id: Telegram message ID to reply to reply_markup: Reply markup to be added to the message silent: Silent notification of the message when sending - Returns: The telegram bot message object sent """ @@ -325,6 +349,7 @@ def slave_message_text(self, msg: Message, tg_dest: TelegramChatID, msg_template text=text, prefix=msg_template, suffix=reactions, parse_mode='HTML', reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) else: @@ -338,7 +363,8 @@ def slave_message_text(self, msg: Message, tg_dest: TelegramChatID, msg_template self.logger.debug("[%s] Processed and sent as text message", msg.uid) return tg_msg - def slave_message_link(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_link(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -368,6 +394,7 @@ def slave_message_link(self, msg: Message, tg_dest: TelegramChatID, msg_template prefix=msg_template, suffix=reactions, parse_mode="HTML", reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) @@ -381,7 +408,8 @@ def slave_message_link(self, msg: Message, tg_dest: TelegramChatID, msg_template IMG_SIZE_MAX_RATIO = 10 """Threshold of aspect ratio (longer side to shorter side) to send as file, used alone.""" - def slave_message_image(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_image(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -442,7 +470,8 @@ def slave_message_image(self, msg: Message, tg_dest: TelegramChatID, msg_templat edit_media = False self.bot.send_message(chat_id=old_msg_id[0], reply_to_message_id=old_msg_id[1], text=file_too_large) else: - message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, text=text, + message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, text=text, parse_mode="HTML", reply_markup=reply_markup, disable_notification=silent, prefix=msg_template, suffix=reactions) message.reply_text(file_too_large) @@ -458,22 +487,28 @@ def slave_message_image(self, msg: Message, tg_dest: TelegramChatID, msg_templat media = InputMediaDocument(file) else: media = InputMediaPhoto(file) - self.bot.edit_message_media(chat_id=old_msg_id[0], message_id=old_msg_id[1], media=media) + self.bot.edit_message_media(chat_id=old_msg_id[0], message_id=old_msg_id[1], media=media, + reply_markup=reply_markup) return self.bot.edit_message_caption(chat_id=old_msg_id[0], message_id=old_msg_id[1], reply_markup=reply_markup, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML") - except telegram.error.BadRequest: - # Send as an reply if cannot edit previous message. - if old_msg_id[0] == str(target_msg_id): - target_msg_id = target_msg_id or old_msg_id[1] + except telegram.error.BadRequest as e: + self.logger.warning("[%s] Failed to edit media/caption (BadRequest: %s). Sending new message instead.", msg.uid, e) + # Send as a reply if cannot edit previous message. + # Check if the target is within the same chat_id (thread_id doesn't matter for this check) + if old_msg_id[0] == str(tg_dest): + target_msg_id = target_msg_id or old_msg_id[1] # Reply to the original message msg.file.seek(0) + # Fall through to send a new message + # Sending new message (either initially or as fallback from edit) if send_as_file: assert msg.path file = self.process_file_obj(msg.file, msg.path) return self.bot.send_document(tg_dest, file, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML", filename=msg.filename, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) else: @@ -483,28 +518,32 @@ def slave_message_image(self, msg: Message, tg_dest: TelegramChatID, msg_templat return self.bot.send_photo(tg_dest, file, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML", reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) except telegram.error.BadRequest as e: self.logger.error('[%s] Failed to send it as image, sending as document. Reason: %s', msg.uid, e) assert msg.path + msg.file.seek(0) # Rewind file pointer file = self.process_file_obj(msg.file, msg.path) return self.bot.send_document(tg_dest, file, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML", filename=msg.filename, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) finally: if msg.file: msg.file.close() - def slave_message_animation(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_animation(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, silent: bool = None) -> telegram.Message: - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO) + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO) # UPLOAD_VIDEO_NOTE might be better? self.logger.debug("[%s] Message is an Animation; Path: %s; MIME: %s", msg.uid, msg.path, msg.mime) if msg.path: @@ -524,7 +563,8 @@ def slave_message_animation(self, msg: Message, tg_dest: TelegramChatID, msg_tem edit_media = False self.bot.send_message(chat_id=old_msg_id[0], reply_to_message_id=old_msg_id[1], text=file_too_large) else: - message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, text=text, + message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, text=text, parse_mode="HTML", reply_markup=reply_markup, disable_notification=silent, prefix=msg_template, suffix=reactions) @@ -535,7 +575,8 @@ def slave_message_animation(self, msg: Message, tg_dest: TelegramChatID, msg_tem if edit_media: assert msg.file and msg.path file = self.process_file_obj(msg.file, msg.path) - self.bot.edit_message_media(chat_id=old_msg_id[0], message_id=old_msg_id[1], media=InputMediaAnimation(file)) + self.bot.edit_message_media(chat_id=old_msg_id[0], message_id=old_msg_id[1], media=InputMediaAnimation(file), + reply_markup=reply_markup) return self.bot.edit_message_caption(chat_id=old_msg_id[0], message_id=old_msg_id[1], prefix=msg_template, suffix=reactions, reply_markup=reply_markup, @@ -548,13 +589,15 @@ def slave_message_animation(self, msg: Message, tg_dest: TelegramChatID, msg_tem prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML", reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) finally: if msg.file is not None: msg.file.close() - def slave_message_sticker(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_sticker(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[InlineKeyboardMarkup] = None, @@ -569,11 +612,17 @@ def slave_message_sticker(self, msg: Message, tg_dest: TelegramChatID, msg_templ self.logger.debug("[%s] Size of %s is %s.", msg.uid, msg.path, os.stat(msg.path).st_size) try: + # If only media changed (e.g., replaced sticker), send new one replying to old. + # Telegram doesn't support editing sticker media directly. if msg.edit_media and old_msg_id is not None: - target_msg_id = old_msg_id[1] - old_msg_id = None + if old_msg_id[0] == str(tg_dest): + target_msg_id = old_msg_id[1] # Set reply target to the message being "edited" + old_msg_id = None # Force sending a new message + + # If not editing media, but have old_msg_id, try editing reply_markup (e.g., for reactions) if old_msg_id and not msg.edit_media: try: + # Editing reply markup doesn't involve thread_id return self.bot.edit_message_reply_markup(chat_id=old_msg_id[0], message_id=old_msg_id[1], reply_markup=sticker_reply_markup) except TelegramError: @@ -582,6 +631,7 @@ def slave_message_sticker(self, msg: Message, tg_dest: TelegramChatID, msg_templ reply_markup=reply_markup, disable_notification=silent) + # Sending a new sticker (initial send or edit_media fallback) else: webp_img = None @@ -591,7 +641,9 @@ def slave_message_sticker(self, msg: Message, tg_dest: TelegramChatID, msg_templ self.bot.send_message(chat_id=old_msg_id[0], reply_to_message_id=old_msg_id[1], text=file_too_large) else: + # Send placeholder text first message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, text=self.html_substitutions(msg), parse_mode="HTML", reply_markup=reply_markup, disable_notification=silent, @@ -606,12 +658,15 @@ def slave_message_sticker(self, msg: Message, tg_dest: TelegramChatID, msg_templ webp_img.seek(0) file = self.process_file_obj(webp_img, webp_img.name) return self.bot.send_sticker(tg_dest, file, reply_markup=sticker_reply_markup, + message_thread_id=thread_id, reply_to_message_id=target_msg_id, disable_notification=silent) except IOError: + self.logger.warning("[%s] Failed to convert image to webp sticker, sending as document.", msg.uid) assert msg.file and msg.path file = self.process_file_obj(msg.file, msg.path) return self.bot.send_document(tg_dest, file, prefix=msg_template, suffix=reactions, + message_thread_id=thread_id, caption=msg.text, filename=msg.filename, reply_to_message_id=target_msg_id, reply_markup=reply_markup, @@ -638,11 +693,13 @@ def build_chat_info_inline_keyboard(msg: Message, msg_template: str, reactions: description.append([InlineKeyboardButton(msg.text, callback_data="void")]) if reactions: description.append([InlineKeyboardButton(reactions, callback_data="void")]) - sticker_reply_markup = reply_markup or InlineKeyboardMarkup([]) - sticker_reply_markup.inline_keyboard = description + sticker_reply_markup.inline_keyboard - return sticker_reply_markup + effective_reply_markup = reply_markup if isinstance(reply_markup, InlineKeyboardMarkup) else InlineKeyboardMarkup([]) + effective_reply_markup.inline_keyboard = description + effective_reply_markup.inline_keyboard + return effective_reply_markup - def slave_message_file(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + + def slave_message_file(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -682,7 +739,8 @@ def slave_message_file(self, msg: Message, tg_dest: TelegramChatID, msg_template edit_media = False self.bot.send_message(chat_id=old_msg_id[0], reply_to_message_id=old_msg_id[1], text=file_too_large) else: - message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, text=text, + message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, text=text, parse_mode="HTML", reply_markup=reply_markup, disable_notification=silent, prefix=msg_template, suffix=reactions) @@ -704,13 +762,15 @@ def slave_message_file(self, msg: Message, tg_dest: TelegramChatID, msg_template prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML", filename=file_name, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) finally: if msg.file is not None: msg.file.close() - def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -730,7 +790,8 @@ def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, msg_templat edit_media = False self.bot.send_message(chat_id=old_msg_id[0], reply_to_message_id=old_msg_id[1], text=file_too_large) else: - message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, text=text, + message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, text=text, parse_mode="HTML", reply_markup=reply_markup, disable_notification=silent, prefix=msg_template, suffix=reactions) @@ -739,34 +800,51 @@ def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, msg_templat if old_msg_id: if edit_media: - # Cannot edit voice message content, send a new one instead + self.logger.warning("[%s] Cannot edit voice message media. Sending new message instead.", msg.uid) msg_template += " " + self._("[Edited]") if str(tg_dest) == old_msg_id[0]: target_msg_id = target_msg_id or old_msg_id[1] + old_msg_id = None # Force sending new message below else: return self.bot.edit_message_caption(chat_id=old_msg_id[0], message_id=old_msg_id[1], reply_markup=reply_markup, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML") - assert msg.file is not None - with tempfile.NamedTemporaryFile() as f: - pydub.AudioSegment.from_file(msg.file).export(f, format="ogg", codec="libopus", - parameters=['-vbr', 'on']) - file = self.process_file_obj(f, f.name) - tg_msg = self.bot.send_voice(tg_dest, file, prefix=msg_template, suffix=reactions, - caption=text, parse_mode="HTML", - reply_to_message_id=target_msg_id, reply_markup=reply_markup, - disable_notification=silent) + # Sending new message (initial or fallback) + if not old_msg_id: # Ensure we are in the 'send new' path + assert msg.file is not None + with tempfile.NamedTemporaryFile(suffix=".ogg") as f: # Ensure correct suffix for pydub + try: + pydub.AudioSegment.from_file(msg.file).export(f.name, format="ogg", codec="libopus", + parameters=['-vbr', 'on']) + # process_file_obj might return URI or file object. send_voice expects content or path. + processed_path = self.process_file_obj(f, f.name) # Get path/URI + # Send using the path/URI + tg_msg = self.bot.send_voice(tg_dest, processed_path, prefix=msg_template, suffix=reactions, + caption=text, parse_mode="HTML", + reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, + disable_notification=silent) + return tg_msg + except pydub.exceptions.CouldntDecodeError as e: + self.logger.error("[%s] Failed to decode audio file for conversion: %s. Sending as file.", msg.uid, e) + msg.file.seek(0) + # Fallback to sending as a generic file + return self.slave_message_file(msg, tg_dest, thread_id, msg_template, reactions, + old_msg_id=None, # Ensure it sends as new + target_msg_id=target_msg_id, reply_markup=reply_markup, silent=silent) return tg_msg finally: if msg.file is not None: msg.file.close() - def slave_message_location(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_location(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[InlineKeyboardMarkup] = None, silent: bool = False) -> telegram.Message: - # TODO: Move msg_template to caption during MTProto migration (if we ever had a chance to do that). + # Location messages cannot be edited in content by bots. + # If an edit request comes, send a new message replying to the old one. self.bot.send_chat_action(tg_dest, ChatAction.FIND_LOCATION) assert (isinstance(msg.attributes, LocationAttribute)) attributes: LocationAttribute = msg.attributes @@ -787,10 +865,12 @@ def slave_message_location(self, msg: Message, tg_dest: TelegramChatID, msg_temp # TODO: Use live location if possible? Lift live location messages to EFB Framework? return self.bot.send_location(tg_dest, latitude=attributes.latitude, longitude=attributes.longitude, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=location_reply_markup, disable_notification=silent) - def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -803,7 +883,7 @@ def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, msg_templat if placeholder_flag == "emoji": text = "🎥" elif placeholder_flag == "text": - text = self._("Sent a file.") + text = self._("Sent a video.") else: text = "" else: @@ -817,7 +897,8 @@ def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, msg_templat edit_media = False self.bot.send_message(chat_id=old_msg_id[0], reply_to_message_id=old_msg_id[1], text=file_too_large) else: - message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, text=text, + message = self.bot.send_message(chat_id=tg_dest, reply_to_message_id=target_msg_id, + message_thread_id=thread_id, text=text, parse_mode="HTML", reply_markup=reply_markup, disable_notification=silent, prefix=msg_template, suffix=reactions) @@ -828,7 +909,8 @@ def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, msg_templat if edit_media: assert msg.file is not None and msg.path is not None file = self.process_file_obj(msg.file, msg.path) - self.bot.edit_message_media(chat_id=old_msg_id[0], message_id=old_msg_id[1], media=InputMediaVideo(file)) + self.bot.edit_message_media(chat_id=old_msg_id[0], message_id=old_msg_id[1], media=InputMediaVideo(file), + reply_markup=reply_markup) return self.bot.edit_message_caption(chat_id=old_msg_id[0], message_id=old_msg_id[1], reply_markup=reply_markup, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML") assert msg.file is not None and msg.path is not None @@ -836,20 +918,22 @@ def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, msg_templat return self.bot.send_video(tg_dest, file, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML", reply_to_message_id=target_msg_id, + message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) finally: if msg.file is not None: msg.file.close() - def slave_message_unsupported(self, msg: Message, tg_dest: TelegramChatID, msg_template: str, reactions: str, + def slave_message_unsupported(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, silent: bool = False) -> telegram.Message: self.logger.debug("[%s] Sending as an unsupported message.", msg.uid) + # Note: send_chat_action for unsupported might need adjustment if PTB changes behavior self.bot.send_chat_action(tg_dest, ChatAction.TYPING) - if msg.text: text = self.html_substitutions(msg) else: @@ -860,21 +944,22 @@ def slave_message_unsupported(self, msg: Message, tg_dest: TelegramChatID, msg_t text=text, parse_mode="HTML", prefix=msg_template + " " + self._("(unsupported)"), suffix=reactions, - reply_to_message_id=target_msg_id, reply_markup=reply_markup, + reply_to_message_id=target_msg_id, message_thread_id=thread_id, reply_markup=reply_markup, disable_notification=silent) else: - # Cannot change reply_to_message_id when editing a message + # Cannot change reply_to_message_id or thread_id when editing a message tg_msg = self.bot.edit_message_text(chat_id=old_msg_id[0], message_id=old_msg_id[1], text=text, parse_mode="HTML", - prefix=msg_template + " " + self._("(unsupported)"), + prefix=msg_template + " " + self._("(unsupported) [Edited]"), # Mark as edited suffix=reactions, reply_markup=reply_markup) self.logger.debug("[%s] Processed and sent as text message", msg.uid) return tg_msg - def slave_message_status(self, msg: Message, tg_dest: TelegramChatID): + def slave_message_status(self, msg: Message, tg_dest: TelegramChatID, + thread_id: Optional[TelegramTopicThreadID]): attributes = msg.attributes assert isinstance(attributes, StatusAttribute) if attributes.status_type is StatusAttribute.Types.TYPING: @@ -919,11 +1004,17 @@ def send_status(self, status: Status): if not self.channel.flag('prevent_message_removal'): self.bot.delete_message(*old_msg_id) return - except TelegramError: + except TelegramError as e: + self.logger.warning("Failed to delete message %s.%s: %s. Sending notification instead.", *old_msg_id, e) pass + thread_id = None + if old_msg.master_message_thread_id: + thread_id = TelegramTopicThreadID(old_msg.master_message_thread_id) self.bot.send_message(chat_id=old_msg_id[0], text=self._("Message is removed in remote chat."), - reply_to_message_id=old_msg_id[1]) + reply_to_message_id=old_msg_id[1], + message_thread_id=thread_id, # Send notification in the correct thread + disable_notification=True) # Probably silent notification else: self.logger.info('Was supposed to delete a message, ' 'but it does not exist in database: %s', status) @@ -953,14 +1044,18 @@ def update_reactions(self, status: MessageReactionsUpdate): old_msg: ETMMsg = old_msg_db.build_etm_msg(chat_manager=self.chat_manager) old_msg.reactions = status.reactions - old_msg.edit = True + old_msg.edit = True # Mark as edit so dispatch knows it's an update + old_msg.edit_media = False # Ensure media is not considered edited msg_template, _ = self.get_slave_msg_dest(old_msg) effective_msg = old_msg_db.master_msg_id_alt or old_msg_db.master_msg_id chat_id, msg_id = utils.message_id_str_to_id(effective_msg) + thread_id = None + if old_msg.master_message_thread_id: + thread_id = TelegramTopicThreadID(old_msg.master_message_thread_id) # Go through the ordinary update process - self.dispatch_message(old_msg, msg_template, old_msg_id=(chat_id, msg_id), tg_dest=chat_id) + self.dispatch_message(old_msg, msg_template, old_msg_id=(chat_id, msg_id), tg_dest=chat_id, thread_id=thread_id) def generate_message_template(self, msg: Message, singly_linked: bool) -> str: msg_prefix = "" # For group member name diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index f6b9cc77..526dd2b7 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -27,6 +27,7 @@ TelegramChatID = NewType('TelegramChatID', int) +TelegramTopicThreadID = NewType('TelegramTopicThreadID', int) TelegramMessageID = NewType('TelegramMessageID', int) TgChatMsgIDStr = NewType('TgChatMsgIDStr', str) EFBChannelChatIDStr = NewType('EFBChannelChatIDStr', str) From 9376bec2f913a079ebe7654895578e0b115c57ef Mon Sep 17 00:00:00 2001 From: "jiz4oh (aider)" Date: Sat, 12 Apr 2025 16:34:11 +0800 Subject: [PATCH 02/68] feat: Add master_message_thread_id column to msglog table --- efb_telegram_master/db.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 63aad0a3..1df70b3a 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -204,6 +204,8 @@ def __init__(self, channel: 'TelegramChannel'): self._migrate(2) elif "file_unique_id" not in msg_log_columns: self._migrate(3) + elif "master_message_thread_id" not in msg_log_columns: + self._migrate(4) self.logger.debug("Database migration finished...") def stop_worker(self): @@ -254,6 +256,12 @@ def _migrate(i: int): migrate( migrator.add_column("msglog", "file_unique_id", MsgLog.file_unique_id) ) + if i <= 4: + # Migration 4: Add column for message thread ID to message log table + # 2025APR12 + migrate( + migrator.add_column("msglog", "master_message_thread_id", MsgLog.master_message_thread_id) + ) def add_chat_assoc(self, master_uid: EFBChannelChatIDStr, slave_uid: EFBChannelChatIDStr, @@ -463,6 +471,7 @@ def add_or_update_message_log(self, row.master_msg_id = master_msg_id row.master_msg_id_alt = master_msg_id_alt + row.master_message_thread_id = str(master_message.message_thread_id) if master_message.message_thread_id else None row.text = msg.text row.slave_origin_uid = chat_id_to_str(chat=msg.chat) row.slave_member_uid = chat_id_to_str(chat=msg.author) From 1ba102c51b18bf7f872b6e6c036f1dc7ee51ce75 Mon Sep 17 00:00:00 2001 From: "jiz4oh (aider)" Date: Sat, 12 Apr 2025 16:35:52 +0800 Subject: [PATCH 03/68] fix: Ensure TopicAssoc table is created during database initialization --- efb_telegram_master/db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 1df70b3a..a5fa89f2 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -191,7 +191,7 @@ def __init__(self, channel: 'TelegramChannel'): self.logger.debug("Database loaded.") self.logger.debug("Checking database migration...") - if not ChatAssoc.table_exists(): + if not ChatAssoc.table_exists() or not TopicAssoc.table_exists(): self._create() else: msg_log_columns = {i.name for i in database.get_columns("msglog")} @@ -216,7 +216,7 @@ def _create(): """ Initializing tables. """ - database.create_tables([ChatAssoc, MsgLog, SlaveChatInfo]) + database.create_tables([ChatAssoc, MsgLog, SlaveChatInfo, TopicAssoc]) @staticmethod def _migrate(i: int): From 2d811e05fc9661f4368a537ef119fdf73f4f47d3 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 17:02:47 +0800 Subject: [PATCH 04/68] fix: improve topic message handling and assoc lookup - Add null check when looking up topic associations to prevent errors - Fix thread_id initialization order in master message processing - Fix topic creation to use proper author name field --- efb_telegram_master/db.py | 6 ++++-- efb_telegram_master/master_message.py | 2 +- efb_telegram_master/slave_message.py | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index a5fa89f2..e27bb5f2 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -418,8 +418,10 @@ def get_topic_thread_id(topic_chat_id: int, The message thread_id """ try: - return int(TopicAssoc.select(TopicAssoc.message_thread_id)\ - .where(TopicAssoc.slave_uid == slave_uid, TopicAssoc.topic_chat_id == topic_chat_id).first().message_thread_id) + assoc = TopicAssoc.select(TopicAssoc.message_thread_id)\ + .where(TopicAssoc.slave_uid == slave_uid, TopicAssoc.topic_chat_id == topic_chat_id).first() + if assoc: + return int(assoc.message_thread_id) except DoesNotExist: return None diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index 7aa0ce83..fca7cf4e 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -163,6 +163,7 @@ def msg(self, update: Update, context: CallbackContext): self.logger.debug("[%s] Chat %s is singly-linked to %s", mid, message.chat, destination) if destination is None: + thread_id = message.message_thread_id if thread_id and TelegramChatID(update.effective_chat.id) == self.channel.topic_group: destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=self.channel.topic_group) if destination: @@ -172,7 +173,6 @@ def msg(self, update: Update, context: CallbackContext): quote = False self.logger.debug("[%s] Chat %s is not singly-linked", mid, update.effective_chat) reply_to = message.reply_to_message - thread_id = message.message_thread_id cached_dest = self.chat_dest_cache.get(str(message.chat.id)) if reply_to: self.logger.debug("[%s] Message is quote-replying to %s", mid, reply_to) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index e47e73d8..99fa726d 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -269,10 +269,11 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram tg_dest = self.channel.topic_group thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) if not thread_id: - thread_id: int = self.bot.create_forum_topic( + topic: ForumTopic = self.bot.create_forum_topic( chat_id=self.channel.topic_group, - name=author + name=author.name ) + thread_id = topic.message_thread_id self.db.add_topic_assoc( topic_chat_id=self.channel.topic_group, message_thread_id=thread_id, From 0a23092d0145ba89c7ab6927d52e800214fa19f5 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 17:12:44 +0800 Subject: [PATCH 05/68] fix: add thread support to telegram chat actions Add message_thread_id parameter to all send_chat_action calls to properly show typing indicators and upload status in topic threads. Modify bot_manager to handle thread_id parameter by converting it to the proper api_kwargs format. --- efb_telegram_master/bot_manager.py | 2 ++ efb_telegram_master/slave_message.py | 32 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/efb_telegram_master/bot_manager.py b/efb_telegram_master/bot_manager.py index 80e86d34..c07f8aa9 100644 --- a/efb_telegram_master/bot_manager.py +++ b/efb_telegram_master/bot_manager.py @@ -445,6 +445,8 @@ def send_photo(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration def send_chat_action(self, *args, **kwargs): + if kwargs.get('message_thread_id'): + kwargs['api_kwargs'] = { "message_thread_id": kwargs.pop('message_thread_id') } return self.updater.bot.send_chat_action(*args, **kwargs) @Decorators.retry_on_timeout diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 99fa726d..128be7b8 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -209,7 +209,7 @@ def dispatch_message(self, msg: Message, msg_template: str, tg_msg = self.slave_message_unsupported(msg, tg_dest, thread_id, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) else: - self.bot.send_chat_action(tg_dest, ChatAction.TYPING) + self.bot.send_chat_action(tg_dest, ChatAction.TYPING, message_thread_id=thread_id) tg_msg = self.bot.send_message(tg_dest, prefix=msg_template, suffix=reactions, disable_notification=silent, message_thread_id=thread_id, @@ -341,7 +341,7 @@ def slave_message_text(self, msg: Message, tg_dest: TelegramChatID, The telegram bot message object sent """ self.logger.debug("[%s] Sending as a text message.", msg.uid) - self.bot.send_chat_action(tg_dest, ChatAction.TYPING) + self.bot.send_chat_action(tg_dest, ChatAction.TYPING, message_thread_id=thread_id) text = self.html_substitutions(msg) @@ -370,7 +370,7 @@ def slave_message_link(self, msg: Message, tg_dest: TelegramChatID, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, silent: bool = False) -> telegram.Message: - self.bot.send_chat_action(tg_dest, ChatAction.TYPING) + self.bot.send_chat_action(tg_dest, ChatAction.TYPING, message_thread_id=thread_id) assert isinstance(msg.attributes, LinkAttribute) attributes: LinkAttribute = msg.attributes @@ -416,7 +416,7 @@ def slave_message_image(self, msg: Message, tg_dest: TelegramChatID, reply_markup: Optional[ReplyMarkup] = None, silent: bool = False) -> telegram.Message: assert msg.file - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO) + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO, message_thread_id=thread_id) self.logger.debug("[%s] Message is of %s type; Path: %s; MIME: %s", msg.uid, msg.type, msg.path, msg.mime) if msg.path: self.logger.debug("[%s] Size of %s is %s.", msg.uid, msg.path, os.stat(msg.path).st_size) @@ -544,7 +544,7 @@ def slave_message_animation(self, msg: Message, tg_dest: TelegramChatID, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, silent: bool = None) -> telegram.Message: - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO) # UPLOAD_VIDEO_NOTE might be better? + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO, message_thread_id=thread_id) # UPLOAD_VIDEO_NOTE might be better? self.logger.debug("[%s] Message is an Animation; Path: %s; MIME: %s", msg.uid, msg.path, msg.mime) if msg.path: @@ -604,7 +604,7 @@ def slave_message_sticker(self, msg: Message, tg_dest: TelegramChatID, reply_markup: Optional[InlineKeyboardMarkup] = None, silent: bool = False) -> telegram.Message: - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO) + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO, message_thread_id=thread_id) sticker_reply_markup = self.build_chat_info_inline_keyboard(msg, msg_template, reactions, reply_markup) @@ -705,7 +705,7 @@ def slave_message_file(self, msg: Message, tg_dest: TelegramChatID, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, silent: bool = False) -> telegram.Message: - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_DOCUMENT) + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_DOCUMENT, message_thread_id=thread_id) if msg.filename is None and msg.path is not None: file_name = os.path.basename(msg.path) @@ -776,7 +776,7 @@ def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, silent: bool = False) -> telegram.Message: - self.bot.send_chat_action(tg_dest, ChatAction.RECORD_AUDIO) + self.bot.send_chat_action(tg_dest, ChatAction.RECORD_AUDIO, message_thread_id=thread_id) if msg.text: text = self.html_substitutions(msg) else: @@ -846,7 +846,7 @@ def slave_message_location(self, msg: Message, tg_dest: TelegramChatID, silent: bool = False) -> telegram.Message: # Location messages cannot be edited in content by bots. # If an edit request comes, send a new message replying to the old one. - self.bot.send_chat_action(tg_dest, ChatAction.FIND_LOCATION) + self.bot.send_chat_action(tg_dest, ChatAction.FIND_LOCATION, message_thread_id=thread_id) assert (isinstance(msg.attributes, LocationAttribute)) attributes: LocationAttribute = msg.attributes self.logger.info("[%s] Sending as a Telegram venue.\nlat: %s, long: %s\ntitle: %s\naddress: %s", @@ -876,7 +876,7 @@ def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, silent: bool = False) -> telegram.Message: - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_VIDEO) + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_VIDEO, message_thread_id=thread_id) if msg.text: text = self.html_substitutions(msg) elif msg_template: @@ -934,7 +934,7 @@ def slave_message_unsupported(self, msg: Message, tg_dest: TelegramChatID, silent: bool = False) -> telegram.Message: self.logger.debug("[%s] Sending as an unsupported message.", msg.uid) # Note: send_chat_action for unsupported might need adjustment if PTB changes behavior - self.bot.send_chat_action(tg_dest, ChatAction.TYPING) + self.bot.send_chat_action(tg_dest, ChatAction.TYPING, message_thread_id=thread_id) if msg.text: text = self.html_substitutions(msg) else: @@ -964,15 +964,15 @@ def slave_message_status(self, msg: Message, tg_dest: TelegramChatID, attributes = msg.attributes assert isinstance(attributes, StatusAttribute) if attributes.status_type is StatusAttribute.Types.TYPING: - self.bot.send_chat_action(tg_dest, ChatAction.TYPING) + self.bot.send_chat_action(tg_dest, ChatAction.TYPING, message_thread_id=thread_id) elif attributes.status_type is StatusAttribute.Types.UPLOADING_VOICE: - self.bot.send_chat_action(tg_dest, ChatAction.RECORD_AUDIO) + self.bot.send_chat_action(tg_dest, ChatAction.RECORD_AUDIO, message_thread_id=thread_id) elif attributes.status_type is StatusAttribute.Types.UPLOADING_IMAGE: - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO) + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_PHOTO, message_thread_id=thread_id) elif attributes.status_type is StatusAttribute.Types.UPLOADING_VIDEO: - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_VIDEO) + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_VIDEO, message_thread_id=thread_id) elif attributes.status_type is StatusAttribute.Types.UPLOADING_FILE: - self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_DOCUMENT) + self.bot.send_chat_action(tg_dest, ChatAction.UPLOAD_DOCUMENT, message_thread_id=thread_id) def send_status(self, status: Status): if isinstance(status, ChatUpdates): From b6bcd822679694fc850f3ade0b9b5bf0974dae0b Mon Sep 17 00:00:00 2001 From: "jiz4oh (aider)" Date: Sat, 12 Apr 2025 17:13:39 +0800 Subject: [PATCH 06/68] fix: Mark get_topic_slave as static method to resolve TypeError --- efb_telegram_master/db.py | 1 + 1 file changed, 1 insertion(+) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index e27bb5f2..2931ae1d 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -425,6 +425,7 @@ def get_topic_thread_id(topic_chat_id: int, except DoesNotExist: return None + @staticmethod def get_topic_slave(topic_chat_id: int, message_thread_id: int ) -> Optional[EFBChannelChatIDStr]: From 1b7c21b43ed5ef9b9126d807f019b5a7524f8fae Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 17:32:01 +0800 Subject: [PATCH 07/68] fix: adjust quote behavior in topic threads When replying in topic threads, only quote the message if it's not the topic starter message (where message_id equals message_thread_id). This prevents unnecessary quoting of topic headers in thread conversations. --- efb_telegram_master/master_message.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index fca7cf4e..a42b992d 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -158,7 +158,6 @@ def msg(self, update: Update, context: CallbackContext): if destination is None: destination = self.get_singly_linked_chat_id_str(update.effective_chat) if destination: - # if the chat is singly-linked quote = message.reply_to_message is not None self.logger.debug("[%s] Chat %s is singly-linked to %s", mid, message.chat, destination) @@ -167,7 +166,9 @@ def msg(self, update: Update, context: CallbackContext): if thread_id and TelegramChatID(update.effective_chat.id) == self.channel.topic_group: destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=self.channel.topic_group) if destination: - quote = message.reply_to_message is not None + quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id + if not quote: + message.reply_to_message = None if destination is None: # not singly linked quote = False From 93f30c92e85762241abbb83b9ca8909d867eca19 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 18:00:15 +0800 Subject: [PATCH 08/68] fix: handle None message_thread_id in chat actions Change message_thread_id check in send_chat_action to explicitly compare with None instead of truthy check, fixing cases where thread ID could be 0 or other falsy values. --- efb_telegram_master/bot_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/bot_manager.py b/efb_telegram_master/bot_manager.py index c07f8aa9..2fe70248 100644 --- a/efb_telegram_master/bot_manager.py +++ b/efb_telegram_master/bot_manager.py @@ -445,7 +445,7 @@ def send_photo(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration def send_chat_action(self, *args, **kwargs): - if kwargs.get('message_thread_id'): + if kwargs.get('message_thread_id', None) != None: kwargs['api_kwargs'] = { "message_thread_id": kwargs.pop('message_thread_id') } return self.updater.bot.send_chat_action(*args, **kwargs) From 0914da257a50c0bd0cb20e574434538fd10b1d2f Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 19:53:00 +0800 Subject: [PATCH 09/68] feat: improve forum topic handling and persistence - Add reopen_forum_topic method to handle closed topics - Fix topic association lookup to use latest thread ID - Add topic association cleanup before creating new topics - Handle BadRequest errors in topic operations - Fix message_thread_id handling in chat actions --- efb_telegram_master/bot_manager.py | 10 +++++-- efb_telegram_master/db.py | 22 +++++++++++++- efb_telegram_master/slave_message.py | 45 ++++++++++++++++++++-------- 3 files changed, 62 insertions(+), 15 deletions(-) diff --git a/efb_telegram_master/bot_manager.py b/efb_telegram_master/bot_manager.py index 2fe70248..ea13e546 100644 --- a/efb_telegram_master/bot_manager.py +++ b/efb_telegram_master/bot_manager.py @@ -445,8 +445,9 @@ def send_photo(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration def send_chat_action(self, *args, **kwargs): - if kwargs.get('message_thread_id', None) != None: - kwargs['api_kwargs'] = { "message_thread_id": kwargs.pop('message_thread_id') } + message_thread_id = kwargs.pop('message_thread_id', None) + if message_thread_id != None: + kwargs['api_kwargs'] = { "message_thread_id": message_thread_id} return self.updater.bot.send_chat_action(*args, **kwargs) @Decorators.retry_on_timeout @@ -550,6 +551,11 @@ def answer_callback_query(self, *args, prefix="", suffix="", text=None, def create_forum_topic(self, *args, **kwargs): return self.updater.bot.create_forum_topic(*args, **kwargs) + @Decorators.retry_on_timeout + @Decorators.retry_on_chat_migration + def reopen_forum_topic(self, *args, **kwargs) -> bool: + return self.updater.bot.reopen_forum_topic(*args, **kwargs) + @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration def set_chat_title(self, *args, **kwargs): diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 2931ae1d..77904681 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -419,7 +419,8 @@ def get_topic_thread_id(topic_chat_id: int, """ try: assoc = TopicAssoc.select(TopicAssoc.message_thread_id)\ - .where(TopicAssoc.slave_uid == slave_uid, TopicAssoc.topic_chat_id == topic_chat_id).first() + .where(TopicAssoc.slave_uid == slave_uid, TopicAssoc.topic_chat_id == topic_chat_id)\ + .order_by(TopicAssoc.id.desc()).first() if assoc: return int(assoc.message_thread_id) except DoesNotExist: @@ -445,6 +446,25 @@ def get_topic_slave(topic_chat_id: int, .where(TopicAssoc.message_thread_id == message_thread_id, TopicAssoc.topic_chat_id == topic_chat_id).first().slave_uid except DoesNotExist: return None + except AttributeError: # Handle case where .slave_uid doesn't exist on the result + return None + + @staticmethod + def remove_topic_assoc(topic_chat_id: int, slave_uid: Optional[EFBChannelChatIDStr] = None): + """ + Remove topic association (topic link). + + Args: + topic_chat_id (int): The topic group chat ID + slave_uid (str): Slave channel UID ("%(channel_id)s.%(chat_id)s") + """ + try: + return TopicAssoc.delete().where( + (TopicAssoc.topic_chat_id == str(topic_chat_id)) & + (TopicAssoc.slave_uid == str(slave_uid)) + ).execute() + except DoesNotExist: + return 0 def add_or_update_message_log(self, msg: ETMMsg, diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 128be7b8..2f9a2a21 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -266,20 +266,41 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram if tg_chat: # if this chat is linked tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1])) elif self.channel.topic_group: - tg_dest = self.channel.topic_group thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) + if thread_id: + try: + self.bot.reopen_forum_topic( + chat_id=self.channel.topic_group, + message_thread_id=thread_id + ) + tg_dest = self.channel.topic_group + except telegram.error.BadRequest as e: + # expected behavior + if e.message == "Topic_not_modified": + tg_dest = self.channel.topic_group + pass + else: + self.logger.error('Failed to reopen topic, Reason: %s', e) + thread_id = None if not thread_id: - topic: ForumTopic = self.bot.create_forum_topic( - chat_id=self.channel.topic_group, - name=author.name - ) - thread_id = topic.message_thread_id - self.db.add_topic_assoc( - topic_chat_id=self.channel.topic_group, - message_thread_id=thread_id, - slave_uid=chat_uid, - ) - return "", (tg_dest, thread_id) + try: + topic: ForumTopic = self.bot.create_forum_topic( + chat_id=self.channel.topic_group, + name=author.name + ) + tg_dest = self.channel.topic_group + thread_id = topic.message_thread_id + self.db.remove_topic_assoc( + topic_chat_id=self.channel.topic_group, + slave_uid=chat_uid, + ) + self.db.add_topic_assoc( + topic_chat_id=self.channel.topic_group, + message_thread_id=thread_id, + slave_uid=chat_uid, + ) + except telegram.error.BadRequest as e: + self.logger.error('Failed to create topic, Reason: %s', e) else: singly_linked = False From cc2568c4022e597c2fdafdc2583a2f7acd961354 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 20:19:29 +0800 Subject: [PATCH 10/68] fix: use chat name instead --- efb_telegram_master/slave_message.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 2f9a2a21..7335fc3f 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -238,9 +238,9 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram (Optional[TelegramChatID], Optional[TelegramTopicID]): Telegram destination chat ID and thread ID, None if muted. """ xid = msg.uid - msg.chat = self.chat_manager.update_chat_obj(msg.chat) - author = self.chat_manager.get_or_enrol_member(msg.chat, msg.author) - msg.author = author + chat = self.chat_manager.update_chat_obj(msg.chat) + msg.chat = chat + msg.author = self.chat_manager.get_or_enrol_member(msg.chat, msg.author) chat_uid = utils.chat_id_to_str(chat=msg.chat) tg_chats = self.db.get_chat_assoc(slave_uid=chat_uid) @@ -286,7 +286,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram try: topic: ForumTopic = self.bot.create_forum_topic( chat_id=self.channel.topic_group, - name=author.name + name=chat.name ) tg_dest = self.channel.topic_group thread_id = topic.message_thread_id From 1a8a276a8946d3367aedf0d90936906dfb58b1e0 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 20:32:23 +0800 Subject: [PATCH 11/68] fix: cleanup topic assoc when linking chats Remove topic association from database when linking chats to prevent orphaned topic entries. Add error handling around the cleanup to ensure linking process continues even if topic removal fails. --- efb_telegram_master/chat_binding.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index 18075010..900e7d03 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -546,6 +546,7 @@ def link_chat(self, update: Update, args: Optional[List[str]]): chat: ETMChatType = data.chats[0] chat_display_name = chat.full_name slave_channel, slave_chat_uid = chat.module_id, chat.uid + chat_uid = utils.chat_id_to_str(slave_channel, slave_chat_uid) try: coordinator.get_module_by_id(slave_channel) except NameError: @@ -566,6 +567,15 @@ def link_chat(self, update: Update, args: Optional[List[str]]): msg = self.bot.send_message(tg_chat_to_link, text=txt) chat.link(self.channel.channel_id, ChatID(str(tg_chat_to_link)), self.channel.flag("multiple_slave_chats")) + try: + self.db.remove_topic_assoc( + topic_chat_id=self.channel.topic_group, + slave_uid=chat_uid, + ) + #TODO delete or close the topic after link? + except Exception as e: + self.logger.warn("Error occurred while remove topic assoc.\nError: %s\n%s", + repr(e), traceback.format_exc()) txt = self._("Chat {0} is now linked.").format(chat_display_name) self.bot.edit_message_text(text=txt, chat_id=msg.chat.id, message_id=msg.message_id) From 4eb4ae30102652366c63acd5de6ec6ce1d56dedc Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sat, 12 Apr 2025 23:01:13 +0800 Subject: [PATCH 12/68] fix: improve telegram topic message handling Add additional validation for topic messages and proper debug logging when messages are ignored. This helps troubleshoot issues with topics that aren't created by the bot or messages from invalid topic groups. --- efb_telegram_master/master_message.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index a42b992d..155fc3c0 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -163,12 +163,19 @@ def msg(self, update: Update, context: CallbackContext): if destination is None: thread_id = message.message_thread_id - if thread_id and TelegramChatID(update.effective_chat.id) == self.channel.topic_group: - destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=self.channel.topic_group) - if destination: - quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id - if not quote: - message.reply_to_message = None + if thread_id: + if TelegramChatID(update.effective_chat.id) == self.channel.topic_group: + destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=self.channel.topic_group) + if destination: + quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id + if not quote: + message.reply_to_message = None + else: + self.logger.debug("[%s] Ignored message as it's a topic which wasn't created by this bot", mid) + return + else: + self.logger.debug("[%s] Ignored message as it's a invalid topic group.", mid) + return if destination is None: # not singly linked quote = False From 69ec8200eb200d6ab16a6e030e9125406da309e2 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sun, 13 Apr 2025 11:03:32 +0800 Subject: [PATCH 13/68] feat: use chat title property for forum topic Updates the forum topic creation to use chat.chat_title instead of chat.name --- efb_telegram_master/slave_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 7335fc3f..de2496d8 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -286,7 +286,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram try: topic: ForumTopic = self.bot.create_forum_topic( chat_id=self.channel.topic_group, - name=chat.name + name=chat.chat_title ) tg_dest = self.channel.topic_group thread_id = topic.message_thread_id From beb0939553aa0216b7e7f22242623843234242bc Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Sun, 13 Apr 2025 15:41:34 +0800 Subject: [PATCH 14/68] feat: add topic_group experimental flag --- README.rst | 4 ++++ efb_telegram_master/__init__.py | 4 +--- efb_telegram_master/utils.py | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index d42c88dc..0b0cdc5b 100644 --- a/README.rst +++ b/README.rst @@ -565,6 +565,10 @@ e.g.: Enable this option if the bot API is running in ``--local`` mode and is using the same file system with ETM. +- ``topic_group`` *(str)* [Default: ``null``] + + Send message to this topic group, per chat per topic + Network configuration: timeout tweaks ------------------------------------- diff --git a/efb_telegram_master/__init__.py b/efb_telegram_master/__init__.py index 15d06cfa..b47ec7d7 100644 --- a/efb_telegram_master/__init__.py +++ b/efb_telegram_master/__init__.py @@ -128,9 +128,7 @@ def __init__(self, instance_id: InstanceID = None): self.commands: CommandsManager = CommandsManager(self) self.chat_binding: ChatBindingManager = ChatBindingManager(self) self.slave_messages: SlaveMessageProcessor = SlaveMessageProcessor(self) - self.topic_group: Optional[TelegramChatID] = None - if self.config.get('topic_group', None): - self.topic_group: Optional[TelegramChatID] = TelegramChatID(self.config['topic_group']) + self.topic_group: Optional[TelegramChatID] = TelegramChatID(self.flag('topic_group')) if not self.flag('auto_locale'): self.translator = translation("efb_telegram_master", diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index 526dd2b7..ec66a6e1 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -56,6 +56,7 @@ class ExperimentalFlagsManager(LocaleMixin): "api_base_url": None, "api_base_file_url": None, "local_tdlib_api": False, + "topic_group": None, } def __init__(self, channel: 'TelegramChannel'): From cf16c7033d71abb64820f926f389bf81d1ef298c Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Mon, 14 Apr 2025 10:42:07 +0800 Subject: [PATCH 15/68] fix: exclude system chats from topic group processing Prevents the channel from attempting to process topic groups for system chats, which could lead to unexpected behavior since system chats are not meant to be linked to topics. --- efb_telegram_master/slave_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index de2496d8..5a6f378a 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -265,7 +265,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram if tg_chat: # if this chat is linked tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1])) - elif self.channel.topic_group: + elif not isinstance(chat, SystemChat) and self.channel.topic_group: thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) if thread_id: try: From 71e6a592599f078fb13e443e36279aedbf918f23 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 03:57:49 -0400 Subject: [PATCH 16/68] fix: add ForumTopic import to slave_message.py --- efb_telegram_master/slave_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 5a6f378a..619ba88c 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -18,7 +18,7 @@ import telegram.ext from PIL import Image from telegram import InputFile, ChatAction, InputMediaPhoto, InputMediaDocument, InputMediaVideo, InputMediaAnimation, \ - InlineKeyboardMarkup, InlineKeyboardButton, ReplyMarkup, TelegramError, InputMedia + InlineKeyboardMarkup, InlineKeyboardButton, ReplyMarkup, TelegramError, InputMedia, ForumTopic from ehforwarderbot import Message, Status, coordinator from ehforwarderbot.chat import ChatNotificationState, SelfChatMember, GroupChat, PrivateChat, SystemChat, Chat From 2c6e3b9f0dbf23cd4992b40adb27a78ba640d468 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 03:57:49 -0400 Subject: [PATCH 17/68] fix: add ForumTopic import to slave_message.py --- efb_telegram_master/slave_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 5a6f378a..619ba88c 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -18,7 +18,7 @@ import telegram.ext from PIL import Image from telegram import InputFile, ChatAction, InputMediaPhoto, InputMediaDocument, InputMediaVideo, InputMediaAnimation, \ - InlineKeyboardMarkup, InlineKeyboardButton, ReplyMarkup, TelegramError, InputMedia + InlineKeyboardMarkup, InlineKeyboardButton, ReplyMarkup, TelegramError, InputMedia, ForumTopic from ehforwarderbot import Message, Status, coordinator from ehforwarderbot.chat import ChatNotificationState, SelfChatMember, GroupChat, PrivateChat, SystemChat, Chat From 44ba6ba26ab7b8c77a35f6f79377095d4a747637 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 05:04:26 -0400 Subject: [PATCH 18/68] refactor: improve topic association handling in SlaveMessageProcessor and fix types --- efb_telegram_master/db.py | 17 +++++++++-------- efb_telegram_master/slave_message.py | 17 ++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 77904681..07135ce1 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -389,16 +389,17 @@ def get_chat_assoc(master_uid: Optional[EFBChannelChatIDStr] = None, except DoesNotExist: return [] - def add_topic_assoc(self, message_thread_id: EFBChannelChatIDStr, - slave_uid: EFBChannelChatIDStr, - topic_chat_id: int): + def add_topic_assoc(self, topic_chat_id: TelegramChatID, + message_thread_id: EFBChannelChatIDStr, + slave_uid: EFBChannelChatIDStr, ): """ Add topic associations (topic links). One Master channel with many Slave channel. Args: - message_thread_id (str): thread UID in topic - slave_uid (str): Slave channel UID ("%(channel_id)s.%(chat_id)s") + topic_chat_id (TelegramChatID): The topic group chat ID + message_thread_id (EFBChannelChatIDStr): The topic thread ID + slave_uid (EFBChannelChatIDStr): Slave channel UID ("%(channel_id)s.%(chat_id)s") """ return TopicAssoc.create(topic_chat_id=topic_chat_id, message_thread_id=message_thread_id, slave_uid=slave_uid) @@ -450,13 +451,13 @@ def get_topic_slave(topic_chat_id: int, return None @staticmethod - def remove_topic_assoc(topic_chat_id: int, slave_uid: Optional[EFBChannelChatIDStr] = None): + def remove_topic_assoc(topic_chat_id: TelegramChatID, slave_uid: Optional[EFBChannelChatIDStr] = None): """ Remove topic association (topic link). Args: - topic_chat_id (int): The topic group chat ID - slave_uid (str): Slave channel UID ("%(channel_id)s.%(chat_id)s") + topic_chat_id (TelegramChatID): The topic group chat ID + slave_uid (EFBChannelChatIDStr): Slave channel UID ("%(channel_id)s.%(chat_id)s") """ try: return TopicAssoc.delete().where( diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 619ba88c..3998d7c9 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -263,21 +263,21 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram # Generate chat text template & Decide type target tg_dest = TelegramChatID(self.channel.config['admins'][0]) - if tg_chat: # if this chat is linked + if tg_chat and singly_linked: tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1])) elif not isinstance(chat, SystemChat) and self.channel.topic_group: - thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) + tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) + thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=tg_dest) if thread_id: + #TODO: Logic to reopen the topic if it was closed. Move to, when send message fails. try: self.bot.reopen_forum_topic( - chat_id=self.channel.topic_group, + chat_id=tg_dest, message_thread_id=thread_id ) - tg_dest = self.channel.topic_group except telegram.error.BadRequest as e: # expected behavior if e.message == "Topic_not_modified": - tg_dest = self.channel.topic_group pass else: self.logger.error('Failed to reopen topic, Reason: %s', e) @@ -285,17 +285,16 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram if not thread_id: try: topic: ForumTopic = self.bot.create_forum_topic( - chat_id=self.channel.topic_group, + chat_id=tg_dest, name=chat.chat_title ) - tg_dest = self.channel.topic_group thread_id = topic.message_thread_id self.db.remove_topic_assoc( - topic_chat_id=self.channel.topic_group, + topic_chat_id=tg_dest, slave_uid=chat_uid, ) self.db.add_topic_assoc( - topic_chat_id=self.channel.topic_group, + topic_chat_id=tg_dest, message_thread_id=thread_id, slave_uid=chat_uid, ) From 92e710bd1234c4b433b72065c836c4121fb36958 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 06:28:06 -0400 Subject: [PATCH 19/68] fix: enhance remove_topic_assoc method to support message_thread_id and improve validation --- efb_telegram_master/db.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 07135ce1..98d909fb 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -451,19 +451,27 @@ def get_topic_slave(topic_chat_id: int, return None @staticmethod - def remove_topic_assoc(topic_chat_id: TelegramChatID, slave_uid: Optional[EFBChannelChatIDStr] = None): + def remove_topic_assoc(topic_chat_id: Optional[TelegramChatID] = None, + message_thread_id: Optional[EFBChannelChatIDStr] = None, + slave_uid: Optional[EFBChannelChatIDStr] = None): """ Remove topic association (topic link). Args: topic_chat_id (TelegramChatID): The topic group chat ID + message_thread_id (EFBChannelChatIDStr): The topic thread ID slave_uid (EFBChannelChatIDStr): Slave channel UID ("%(channel_id)s.%(chat_id)s") """ try: - return TopicAssoc.delete().where( - (TopicAssoc.topic_chat_id == str(topic_chat_id)) & - (TopicAssoc.slave_uid == str(slave_uid)) - ).execute() + if bool(topic_chat_id and message_thread_id) == bool(slave_uid): + raise ValueError("Please provide either topic_chat_id and message_thread_id or slave_uid.") + elif topic_chat_id and message_thread_id: + return TopicAssoc.delete().where( + (TopicAssoc.topic_chat_id == str(topic_chat_id)) & + (TopicAssoc.message_thread_id == str(message_thread_id)) + ).execute() + elif slave_uid: + return TopicAssoc.delete().where(TopicAssoc.slave_uid == slave_uid).execute() except DoesNotExist: return 0 From 6bb98e8b29e3d789860f7e7fb0f6192f413483dd Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 06:36:27 -0400 Subject: [PATCH 20/68] fix: update remove_topic_assoc call --- efb_telegram_master/chat_binding.py | 12 +++--------- efb_telegram_master/slave_message.py | 1 - 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index 900e7d03..0a51a003 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -567,15 +567,9 @@ def link_chat(self, update: Update, args: Optional[List[str]]): msg = self.bot.send_message(tg_chat_to_link, text=txt) chat.link(self.channel.channel_id, ChatID(str(tg_chat_to_link)), self.channel.flag("multiple_slave_chats")) - try: - self.db.remove_topic_assoc( - topic_chat_id=self.channel.topic_group, - slave_uid=chat_uid, - ) - #TODO delete or close the topic after link? - except Exception as e: - self.logger.warn("Error occurred while remove topic assoc.\nError: %s\n%s", - repr(e), traceback.format_exc()) + self.db.remove_topic_assoc( + slave_uid=chat_uid, + ) txt = self._("Chat {0} is now linked.").format(chat_display_name) self.bot.edit_message_text(text=txt, chat_id=msg.chat.id, message_id=msg.message_id) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 3998d7c9..3a04a2d4 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -290,7 +290,6 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram ) thread_id = topic.message_thread_id self.db.remove_topic_assoc( - topic_chat_id=tg_dest, slave_uid=chat_uid, ) self.db.add_topic_assoc( From 90d99084b0741a451e1ccb5b539f136570aafa15 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 06:37:40 -0400 Subject: [PATCH 21/68] fix: fix get_topic_thread_id method by removing topic_chat_id parameter --- efb_telegram_master/db.py | 7 ++----- efb_telegram_master/slave_message.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 98d909fb..3698fac6 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -404,15 +404,12 @@ def add_topic_assoc(self, topic_chat_id: TelegramChatID, return TopicAssoc.create(topic_chat_id=topic_chat_id, message_thread_id=message_thread_id, slave_uid=slave_uid) @staticmethod - def get_topic_thread_id(topic_chat_id: int, - slave_uid: Optional[EFBChannelChatIDStr] - ) -> int: + def get_topic_thread_id(slave_uid: EFBChannelChatIDStr) -> int: """ Get topic association (topic link) information. Only one parameter is to be provided. Args: - topic_chat_id (int): The topic UID slave_uid (str): Slave channel UID ("%(channel_id)s.%(chat_id)s") Returns: @@ -420,7 +417,7 @@ def get_topic_thread_id(topic_chat_id: int, """ try: assoc = TopicAssoc.select(TopicAssoc.message_thread_id)\ - .where(TopicAssoc.slave_uid == slave_uid, TopicAssoc.topic_chat_id == topic_chat_id)\ + .where(TopicAssoc.slave_uid == slave_uid)\ .order_by(TopicAssoc.id.desc()).first() if assoc: return int(assoc.message_thread_id) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 3a04a2d4..c876ae38 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -267,7 +267,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1])) elif not isinstance(chat, SystemChat) and self.channel.topic_group: tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) - thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=tg_dest) + thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) if thread_id: #TODO: Logic to reopen the topic if it was closed. Move to, when send message fails. try: From 945a91bb3c93319608c5477a41cfc504acd71eca Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 06:53:30 -0400 Subject: [PATCH 22/68] fix: remove master_message_thread_id references and adjust message handling in SlaveMessageProcessor --- efb_telegram_master/db.py | 11 ----------- efb_telegram_master/slave_message.py | 9 +-------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 3698fac6..03cc76ad 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -72,8 +72,6 @@ class MsgLog(BaseModel): """Editable message ID from Telegram if ``master_msg_id`` is not editable and a separate one is sent. """ - master_message_thread_id = TextField(null=True) - """Message thread ID from Telegram""" slave_message_id = TextField() """Message from slave channel.""" text = TextField() @@ -204,8 +202,6 @@ def __init__(self, channel: 'TelegramChannel'): self._migrate(2) elif "file_unique_id" not in msg_log_columns: self._migrate(3) - elif "master_message_thread_id" not in msg_log_columns: - self._migrate(4) self.logger.debug("Database migration finished...") def stop_worker(self): @@ -256,12 +252,6 @@ def _migrate(i: int): migrate( migrator.add_column("msglog", "file_unique_id", MsgLog.file_unique_id) ) - if i <= 4: - # Migration 4: Add column for message thread ID to message log table - # 2025APR12 - migrate( - migrator.add_column("msglog", "master_message_thread_id", MsgLog.master_message_thread_id) - ) def add_chat_assoc(self, master_uid: EFBChannelChatIDStr, slave_uid: EFBChannelChatIDStr, @@ -500,7 +490,6 @@ def add_or_update_message_log(self, row.master_msg_id = master_msg_id row.master_msg_id_alt = master_msg_id_alt - row.master_message_thread_id = str(master_message.message_thread_id) if master_message.message_thread_id else None row.text = msg.text row.slave_origin_uid = chat_id_to_str(chat=msg.chat) row.slave_member_uid = chat_id_to_str(chat=msg.author) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index c876ae38..29d93a92 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -1027,13 +1027,9 @@ def send_status(self, status: Status): except TelegramError as e: self.logger.warning("Failed to delete message %s.%s: %s. Sending notification instead.", *old_msg_id, e) pass - thread_id = None - if old_msg.master_message_thread_id: - thread_id = TelegramTopicThreadID(old_msg.master_message_thread_id) self.bot.send_message(chat_id=old_msg_id[0], text=self._("Message is removed in remote chat."), reply_to_message_id=old_msg_id[1], - message_thread_id=thread_id, # Send notification in the correct thread disable_notification=True) # Probably silent notification else: self.logger.info('Was supposed to delete a message, ' @@ -1070,12 +1066,9 @@ def update_reactions(self, status: MessageReactionsUpdate): msg_template, _ = self.get_slave_msg_dest(old_msg) effective_msg = old_msg_db.master_msg_id_alt or old_msg_db.master_msg_id chat_id, msg_id = utils.message_id_str_to_id(effective_msg) - thread_id = None - if old_msg.master_message_thread_id: - thread_id = TelegramTopicThreadID(old_msg.master_message_thread_id) # Go through the ordinary update process - self.dispatch_message(old_msg, msg_template, old_msg_id=(chat_id, msg_id), tg_dest=chat_id, thread_id=thread_id) + self.dispatch_message(old_msg, msg_template, old_msg_id=(chat_id, msg_id), tg_dest=chat_id) def generate_message_template(self, msg: Message, singly_linked: bool) -> str: msg_prefix = "" # For group member name From 6a186b68313dd01f52e885a3967fc96d9b6b8fff Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 06:54:58 -0400 Subject: [PATCH 23/68] chore: update type names --- efb_telegram_master/db.py | 16 +++++++-------- efb_telegram_master/slave_message.py | 30 ++++++++++++++-------------- efb_telegram_master/utils.py | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 03cc76ad..c0992152 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -22,7 +22,7 @@ from .message import ETMMsg from .msg_type import TGMsgType from .utils import TelegramChatID, EFBChannelChatIDStr, TgChatMsgIDStr, message_id_to_str, \ - chat_id_to_str, OldMsgID, chat_id_str_to_id, TelegramMessageID + chat_id_to_str, OldMsgID, chat_id_str_to_id, TelegramMessageID, TelegramTopicID if TYPE_CHECKING: from . import TelegramChannel @@ -394,13 +394,13 @@ def add_topic_assoc(self, topic_chat_id: TelegramChatID, return TopicAssoc.create(topic_chat_id=topic_chat_id, message_thread_id=message_thread_id, slave_uid=slave_uid) @staticmethod - def get_topic_thread_id(slave_uid: EFBChannelChatIDStr) -> int: + def get_topic_thread_id(slave_uid: EFBChannelChatIDStr) -> TelegramTopicID: """ Get topic association (topic link) information. Only one parameter is to be provided. Args: - slave_uid (str): Slave channel UID ("%(channel_id)s.%(chat_id)s") + slave_uid (EFBChannelChatIDStr): Slave channel UID ("%(channel_id)s.%(chat_id)s") Returns: The message thread_id @@ -415,16 +415,16 @@ def get_topic_thread_id(slave_uid: EFBChannelChatIDStr) -> int: return None @staticmethod - def get_topic_slave(topic_chat_id: int, - message_thread_id: int + def get_topic_slave(topic_chat_id: TelegramChatID, + message_thread_id: TelegramTopicID ) -> Optional[EFBChannelChatIDStr]: """ Get topic association (topic link) information. Only one parameter is to be provided. Args: - topic_chat_id (int): The topic UID - message_thread_id (int): The message thread ID + topic_chat_id (TelegramChatID): The topic UID + message_thread_id (TelegramTopicID): The message thread ID Returns: Slave channel UID ("%(channel_id)s.%(chat_id)s") @@ -434,7 +434,7 @@ def get_topic_slave(topic_chat_id: int, .where(TopicAssoc.message_thread_id == message_thread_id, TopicAssoc.topic_chat_id == topic_chat_id).first().slave_uid except DoesNotExist: return None - except AttributeError: # Handle case where .slave_uid doesn't exist on the result + except AttributeError: return None @staticmethod diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 29d93a92..b9a7cbe6 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -34,7 +34,7 @@ from .locale_mixin import LocaleMixin from .message import ETMMsg from .msg_type import get_msg_type -from .utils import TelegramChatID, TelegramTopicThreadID, TelegramMessageID, OldMsgID +from .utils import TelegramChatID, TelegramTopicID, TelegramMessageID, OldMsgID if TYPE_CHECKING: from . import TelegramChannel @@ -126,7 +126,7 @@ def send_message(self, msg: Message) -> Message: def dispatch_message(self, msg: Message, msg_template: str, old_msg_id: Optional[OldMsgID], tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], + thread_id: Optional[TelegramTopicID], silent: bool = False): """Dispatch with header, destination and Telegram message ID and destinations.""" @@ -230,7 +230,7 @@ def dispatch_message(self, msg: Message, msg_template: str, self.db.add_or_update_message_log(etm_msg, tg_msg, old_msg_id) # self.logger.debug("[%s] Message inserted/updated to the database.", xid) - def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[TelegramChatID], Optional[TelegramTopicThreadID]]]: + def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[TelegramChatID], Optional[TelegramTopicID]]]: """Get the Telegram destination of a message with its header. Returns: @@ -246,7 +246,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram tg_chats = self.db.get_chat_assoc(slave_uid=chat_uid) tg_chat = None tg_dest: Optional[TelegramChatID] = None - thread_id: Optional[TelegramTopicThreadID] = None + thread_id: Optional[TelegramTopicID] = None if tg_chats: tg_chat = tg_chats[0] @@ -338,7 +338,7 @@ def html_substitutions(self, msg: Message) -> str: return text def slave_message_text(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -384,7 +384,7 @@ def slave_message_text(self, msg: Message, tg_dest: TelegramChatID, return tg_msg def slave_message_link(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -429,7 +429,7 @@ def slave_message_link(self, msg: Message, tg_dest: TelegramChatID, """Threshold of aspect ratio (longer side to shorter side) to send as file, used alone.""" def slave_message_image(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -558,7 +558,7 @@ def slave_message_image(self, msg: Message, tg_dest: TelegramChatID, msg.file.close() def slave_message_animation(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -617,7 +617,7 @@ def slave_message_animation(self, msg: Message, tg_dest: TelegramChatID, msg.file.close() def slave_message_sticker(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[InlineKeyboardMarkup] = None, @@ -719,7 +719,7 @@ def build_chat_info_inline_keyboard(msg: Message, msg_template: str, reactions: def slave_message_file(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -790,7 +790,7 @@ def slave_message_file(self, msg: Message, tg_dest: TelegramChatID, msg.file.close() def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -858,7 +858,7 @@ def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, msg.file.close() def slave_message_location(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[InlineKeyboardMarkup] = None, @@ -890,7 +890,7 @@ def slave_message_location(self, msg: Message, tg_dest: TelegramChatID, disable_notification=silent) def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -946,7 +946,7 @@ def slave_message_video(self, msg: Message, tg_dest: TelegramChatID, msg.file.close() def slave_message_unsupported(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID], msg_template: str, reactions: str, + thread_id: Optional[TelegramTopicID], msg_template: str, reactions: str, old_msg_id: OldMsgID = None, target_msg_id: Optional[TelegramMessageID] = None, reply_markup: Optional[ReplyMarkup] = None, @@ -979,7 +979,7 @@ def slave_message_unsupported(self, msg: Message, tg_dest: TelegramChatID, return tg_msg def slave_message_status(self, msg: Message, tg_dest: TelegramChatID, - thread_id: Optional[TelegramTopicThreadID]): + thread_id: Optional[TelegramTopicID]): attributes = msg.attributes assert isinstance(attributes, StatusAttribute) if attributes.status_type is StatusAttribute.Types.TYPING: diff --git a/efb_telegram_master/utils.py b/efb_telegram_master/utils.py index ec66a6e1..3438c08b 100644 --- a/efb_telegram_master/utils.py +++ b/efb_telegram_master/utils.py @@ -27,7 +27,7 @@ TelegramChatID = NewType('TelegramChatID', int) -TelegramTopicThreadID = NewType('TelegramTopicThreadID', int) +TelegramTopicID = NewType('TelegramTopicID', int) TelegramMessageID = NewType('TelegramMessageID', int) TgChatMsgIDStr = NewType('TgChatMsgIDStr', str) EFBChannelChatIDStr = NewType('EFBChannelChatIDStr', str) From 6827520d603957dd5f3bfb70c60f13a02d73deac Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 07:15:22 -0400 Subject: [PATCH 24/68] fix: add forum topic creation and association handling in ChatBindingManager --- efb_telegram_master/chat_binding.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index 0a51a003..8c0729d7 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -11,7 +11,7 @@ import telegram # lgtm [py/import-and-import-from] from PIL import Image from telegram import Update, Message, TelegramError, InlineKeyboardButton, ChatAction, InlineKeyboardMarkup, \ - ParseMode + ParseMode, ForumTopic from telegram.error import BadRequest from telegram.ext import ConversationHandler, CommandHandler, CallbackQueryHandler, CallbackContext, Filters, \ MessageHandler @@ -571,6 +571,28 @@ def link_chat(self, update: Update, args: Optional[List[str]]): slave_uid=chat_uid, ) + chat_id = utils.chat_id_to_str(self.channel.channel_id, ChatID(str(tg_chat_to_link))) + links = self.db.get_chat_assoc(master_uid=chat_id) + if len(links) > 1 and self.channel.topic_group: + try: + topic: ForumTopic = self.bot.create_forum_topic( + chat_id=chat_id, + name=chat.chat_title + ) + thread_id = topic.message_thread_id + self.db.remove_topic_assoc( + slave_uid=chat_uid, + ) + self.db.add_topic_assoc( + topic_chat_id=chat_id, + message_thread_id=thread_id, + slave_uid=chat_uid, + ) + return thread_id + except telegram.error.BadRequest as e: + self.logger.error('Failed to create topic, Reason: %s', e) + return None + txt = self._("Chat {0} is now linked.").format(chat_display_name) self.bot.edit_message_text(text=txt, chat_id=msg.chat.id, message_id=msg.message_id) From 89b6e0498f6e6fe43331b1756cb6b321ab73edd3 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 07:17:25 -0400 Subject: [PATCH 25/68] fix: remove redundant topic association removal in ChatBindingManager and SlaveMessageProcessor --- efb_telegram_master/chat_binding.py | 3 --- efb_telegram_master/slave_message.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index 8c0729d7..5e865363 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -580,9 +580,6 @@ def link_chat(self, update: Update, args: Optional[List[str]]): name=chat.chat_title ) thread_id = topic.message_thread_id - self.db.remove_topic_assoc( - slave_uid=chat_uid, - ) self.db.add_topic_assoc( topic_chat_id=chat_id, message_thread_id=thread_id, diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index b9a7cbe6..4c70959f 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -289,9 +289,6 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram name=chat.chat_title ) thread_id = topic.message_thread_id - self.db.remove_topic_assoc( - slave_uid=chat_uid, - ) self.db.add_topic_assoc( topic_chat_id=tg_dest, message_thread_id=thread_id, From 0d47517a830b5558f294199940d80dfe31facb73 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 07:48:25 -0400 Subject: [PATCH 26/68] fix: change error logging to info level and handle TelegramChatID assignment in SlaveMessageProcessor --- efb_telegram_master/slave_message.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 4c70959f..f7e7a72a 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -295,7 +295,9 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram slave_uid=chat_uid, ) except telegram.error.BadRequest as e: - self.logger.error('Failed to create topic, Reason: %s', e) + self.logger.info('Failed to create topic, Reason: %s', e) + tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.group_chat) + thread_id = None else: singly_linked = False From 69c640ccadd43fb549fbba3b9b5184e3fc912854 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 08:10:34 -0400 Subject: [PATCH 27/68] fix: remove commented-out forum topic creation logic in ChatBindingManager --- efb_telegram_master/chat_binding.py | 36 ++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index 5e865363..b9056fa7 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -571,24 +571,24 @@ def link_chat(self, update: Update, args: Optional[List[str]]): slave_uid=chat_uid, ) - chat_id = utils.chat_id_to_str(self.channel.channel_id, ChatID(str(tg_chat_to_link))) - links = self.db.get_chat_assoc(master_uid=chat_id) - if len(links) > 1 and self.channel.topic_group: - try: - topic: ForumTopic = self.bot.create_forum_topic( - chat_id=chat_id, - name=chat.chat_title - ) - thread_id = topic.message_thread_id - self.db.add_topic_assoc( - topic_chat_id=chat_id, - message_thread_id=thread_id, - slave_uid=chat_uid, - ) - return thread_id - except telegram.error.BadRequest as e: - self.logger.error('Failed to create topic, Reason: %s', e) - return None + # chat_id = utils.chat_id_to_str(self.channel.channel_id, ChatID(str(tg_chat_to_link))) + # links = self.db.get_chat_assoc(master_uid=chat_id) + # if len(links) > 1 and self.channel.topic_group: + # try: + # topic: ForumTopic = self.bot.create_forum_topic( + # chat_id=chat_id, + # name=chat.chat_title + # ) + # thread_id = topic.message_thread_id + # self.db.add_topic_assoc( + # topic_chat_id=chat_id, + # message_thread_id=thread_id, + # slave_uid=chat_uid, + # ) + # return thread_id + # except telegram.error.BadRequest as e: + # self.logger.error('Failed to create topic, Reason: %s', e) + # return None txt = self._("Chat {0} is now linked.").format(chat_display_name) self.bot.edit_message_text(text=txt, chat_id=msg.chat.id, message_id=msg.message_id) From 1623139d72f61a2a60919bf4e3922ea53bf22f38 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 08:40:03 -0400 Subject: [PATCH 28/68] fix: comment out logic for reopening forum topics in SlaveMessageProcessor reopen_forum_topic have strict rate limit --- efb_telegram_master/slave_message.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index f7e7a72a..828e7864 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -268,20 +268,20 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram elif not isinstance(chat, SystemChat) and self.channel.topic_group: tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) - if thread_id: + # if thread_id: #TODO: Logic to reopen the topic if it was closed. Move to, when send message fails. - try: - self.bot.reopen_forum_topic( - chat_id=tg_dest, - message_thread_id=thread_id - ) - except telegram.error.BadRequest as e: - # expected behavior - if e.message == "Topic_not_modified": - pass - else: - self.logger.error('Failed to reopen topic, Reason: %s', e) - thread_id = None + # try: + # self.bot.reopen_forum_topic( + # chat_id=tg_dest, + # message_thread_id=thread_id + # ) + # except telegram.error.BadRequest as e: + # # expected behavior + # if e.message == "Topic_not_modified": + # pass + # else: + # self.logger.error('Failed to reopen topic, Reason: %s', e) + # thread_id = None if not thread_id: try: topic: ForumTopic = self.bot.create_forum_topic( From 95c2a62ca1a94116c500d4f9c16f26d707d15d80 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 09:07:04 -0400 Subject: [PATCH 29/68] fix: handle reopening forum topics on BadRequest error in SlaveMessageProcessor --- efb_telegram_master/slave_message.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 828e7864..299180bc 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -119,7 +119,18 @@ def send_message(self, msg: Message) -> Message: self.dispatch_message(msg, msg_template, old_msg_id, tg_dest, thread_id, silent) except Exception as e: - self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", + if isinstance(e, telegram.error.BadRequest) and e.message: + if "Topic" in e.message: + try: + self.bot.reopen_forum_topic( + chat_id=tg_dest, + message_thread_id=thread_id + ) + except telegram.error.BadRequest as e: + self.logger.error('Failed to reopen topic, Reason: %s', e) + thread_id = None + else: + self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", repr(msg), repr(e), traceback.format_exc()) return msg @@ -268,20 +279,6 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram elif not isinstance(chat, SystemChat) and self.channel.topic_group: tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) - # if thread_id: - #TODO: Logic to reopen the topic if it was closed. Move to, when send message fails. - # try: - # self.bot.reopen_forum_topic( - # chat_id=tg_dest, - # message_thread_id=thread_id - # ) - # except telegram.error.BadRequest as e: - # # expected behavior - # if e.message == "Topic_not_modified": - # pass - # else: - # self.logger.error('Failed to reopen topic, Reason: %s', e) - # thread_id = None if not thread_id: try: topic: ForumTopic = self.bot.create_forum_topic( From 498b333ee5cca9583942a8a8eb3df7d57ae569a3 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 09:14:31 -0400 Subject: [PATCH 30/68] fix: deal with all topics --- efb_telegram_master/master_message.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index 155fc3c0..bf2c8132 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -164,17 +164,13 @@ def msg(self, update: Update, context: CallbackContext): if destination is None: thread_id = message.message_thread_id if thread_id: - if TelegramChatID(update.effective_chat.id) == self.channel.topic_group: - destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=self.channel.topic_group) - if destination: - quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id - if not quote: - message.reply_to_message = None - else: - self.logger.debug("[%s] Ignored message as it's a topic which wasn't created by this bot", mid) - return + destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=self.channel.topic_group) + if destination: + quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id + if not quote: + message.reply_to_message = None else: - self.logger.debug("[%s] Ignored message as it's a invalid topic group.", mid) + self.logger.debug("[%s] Ignored message as it's a topic which wasn't created by this bot", mid) return if destination is None: # not singly linked From a74dd35af582a1c596bb618f11bcd3c7eab13f44 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 09:41:23 -0400 Subject: [PATCH 31/68] fix: update topic chat ID retrieval in MasterMessageProcessor --- efb_telegram_master/master_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index bf2c8132..d040db84 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -164,7 +164,7 @@ def msg(self, update: Update, context: CallbackContext): if destination is None: thread_id = message.message_thread_id if thread_id: - destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=self.channel.topic_group) + destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=message.chat.id) if destination: quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id if not quote: From ef733d656b7725d896a8f93c14456ac85a4d0b56 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 09:54:31 -0400 Subject: [PATCH 32/68] fix: update get_topic_slave method to handle optional message_thread_id --- efb_telegram_master/db.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index c0992152..9cbdaf06 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -416,7 +416,7 @@ def get_topic_thread_id(slave_uid: EFBChannelChatIDStr) -> TelegramTopicID: @staticmethod def get_topic_slave(topic_chat_id: TelegramChatID, - message_thread_id: TelegramTopicID + message_thread_id: Optional[EFBChannelChatIDStr] = None, ) -> Optional[EFBChannelChatIDStr]: """ Get topic association (topic link) information. @@ -430,8 +430,12 @@ def get_topic_slave(topic_chat_id: TelegramChatID, Slave channel UID ("%(channel_id)s.%(chat_id)s") """ try: - return TopicAssoc.select(TopicAssoc.slave_uid)\ - .where(TopicAssoc.message_thread_id == message_thread_id, TopicAssoc.topic_chat_id == topic_chat_id).first().slave_uid + if message_thread_id: + return TopicAssoc.select(TopicAssoc.slave_uid)\ + .where(TopicAssoc.message_thread_id == message_thread_id, TopicAssoc.topic_chat_id == topic_chat_id).first().slave_uid + else: + return TopicAssoc.select(TopicAssoc.slave_uid)\ + .where(TopicAssoc.topic_chat_id == topic_chat_id).first().slave_uid except DoesNotExist: return None except AttributeError: From 4ae52bb5f20b7ef9122f90b380b56e1772c019d6 Mon Sep 17 00:00:00 2001 From: Ovler Date: Wed, 16 Apr 2025 13:19:33 -0400 Subject: [PATCH 33/68] feat: add get_chat_info method to retrieve chat details and update topic creation logic in SlaveMessageProcessor --- efb_telegram_master/bot_manager.py | 5 +++++ efb_telegram_master/slave_message.py | 32 +++++++++++++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/efb_telegram_master/bot_manager.py b/efb_telegram_master/bot_manager.py index ea13e546..b9fe9ae8 100644 --- a/efb_telegram_master/bot_manager.py +++ b/efb_telegram_master/bot_manager.py @@ -546,6 +546,11 @@ def answer_callback_query(self, *args, prefix="", suffix="", text=None, *args, text=prefix + text + suffix, **kwargs ) + @Decorators.retry_on_timeout + @Decorators.retry_on_chat_migration + def get_chat_info(self, *args, **kwargs): + return self.updater.bot.get_chat(*args, **kwargs) + @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration def create_forum_topic(self, *args, **kwargs): diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 299180bc..346ec3a8 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -280,21 +280,23 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) if not thread_id: - try: - topic: ForumTopic = self.bot.create_forum_topic( - chat_id=tg_dest, - name=chat.chat_title - ) - thread_id = topic.message_thread_id - self.db.add_topic_assoc( - topic_chat_id=tg_dest, - message_thread_id=thread_id, - slave_uid=chat_uid, - ) - except telegram.error.BadRequest as e: - self.logger.info('Failed to create topic, Reason: %s', e) - tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.group_chat) - thread_id = None + master_chat_info = self.bot.get_chat_info(tg_dest) + if master_chat_info.is_forum: + try: + topic: ForumTopic = self.bot.create_forum_topic( + chat_id=tg_dest, + name=chat.chat_title + ) + thread_id = topic.message_thread_id + self.db.add_topic_assoc( + topic_chat_id=tg_dest, + message_thread_id=thread_id, + slave_uid=chat_uid, + ) + except telegram.error.BadRequest as e: + self.logger.info('Failed to create topic, Reason: %s', e) + tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.group_chat) + thread_id = None else: singly_linked = False From 69ba07555b5fb5d071dc93c3543d0a6581ecb864 Mon Sep 17 00:00:00 2001 From: Ovler Date: Thu, 17 Apr 2025 02:34:53 -0400 Subject: [PATCH 34/68] fix: update topic group chat id --- efb_telegram_master/slave_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 346ec3a8..7d7f6d3d 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -295,7 +295,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram ) except telegram.error.BadRequest as e: self.logger.info('Failed to create topic, Reason: %s', e) - tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.group_chat) + tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) thread_id = None else: singly_linked = False From a7740b6ff243929ecafd820ab4f7b8ce472268e1 Mon Sep 17 00:00:00 2001 From: Ovler Date: Thu, 17 Apr 2025 03:17:22 -0400 Subject: [PATCH 35/68] fix: remove topic association on BadRequest error during topic reopening --- efb_telegram_master/slave_message.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 7d7f6d3d..28d91894 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -128,7 +128,10 @@ def send_message(self, msg: Message) -> Message: ) except telegram.error.BadRequest as e: self.logger.error('Failed to reopen topic, Reason: %s', e) - thread_id = None + self.db.remove_topic_assoc( + topic_chat_id=tg_dest, + message_thread_id=thread_id, + ) else: self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", repr(msg), repr(e), traceback.format_exc()) From a7e295b8456ec92951d8a841456236926f92df41 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Thu, 17 Apr 2025 15:33:36 +0800 Subject: [PATCH 36/68] fix: locking before create new topic --- efb_telegram_master/slave_message.py | 44 ++++++++++++++++------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 619ba88c..ac479cc5 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -5,8 +5,10 @@ import logging import os import tempfile +import threading import traceback import urllib.parse +from collections import defaultdict from pathlib import Path from typing import Tuple, Optional, TYPE_CHECKING, List, IO, Union @@ -53,6 +55,7 @@ def __init__(self, channel: 'TelegramChannel'): self.db: 'DatabaseManager' = channel.db self.chat_dest_cache: ChatDestinationCache = channel.chat_dest_cache self.chat_manager: ChatObjectCacheManager = channel.chat_manager + self._topic_creation_locks = defaultdict(threading.Lock) def is_silent(self, msg: Message) -> Optional[bool]: """Determine if a message shall be sent silently. @@ -267,6 +270,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1])) elif not isinstance(chat, SystemChat) and self.channel.topic_group: thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) + topic_is_deleted = False if thread_id: try: self.bot.reopen_forum_topic( @@ -281,26 +285,30 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram pass else: self.logger.error('Failed to reopen topic, Reason: %s', e) + topic_is_deleted = True thread_id = None if not thread_id: - try: - topic: ForumTopic = self.bot.create_forum_topic( - chat_id=self.channel.topic_group, - name=chat.chat_title - ) - tg_dest = self.channel.topic_group - thread_id = topic.message_thread_id - self.db.remove_topic_assoc( - topic_chat_id=self.channel.topic_group, - slave_uid=chat_uid, - ) - self.db.add_topic_assoc( - topic_chat_id=self.channel.topic_group, - message_thread_id=thread_id, - slave_uid=chat_uid, - ) - except telegram.error.BadRequest as e: - self.logger.error('Failed to create topic, Reason: %s', e) + with self._topic_creation_locks[self.channel.topic_group]: + thread_id = not topic_is_deleted and self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) + if not thread_id: + try: + topic: ForumTopic = self.bot.create_forum_topic( + chat_id=self.channel.topic_group, + name=chat.chat_title + ) + tg_dest = self.channel.topic_group + thread_id = topic.message_thread_id + self.db.remove_topic_assoc( + topic_chat_id=self.channel.topic_group, + slave_uid=chat_uid, + ) + self.db.add_topic_assoc( + topic_chat_id=self.channel.topic_group, + message_thread_id=thread_id, + slave_uid=chat_uid, + ) + except telegram.error.BadRequest as e: + self.logger.error('Failed to create topic, Reason: %s', e) else: singly_linked = False From 5f9892ecc95d2e91b05f3a5de84ac78930c53ab6 Mon Sep 17 00:00:00 2001 From: Ovler Date: Thu, 17 Apr 2025 03:47:51 -0400 Subject: [PATCH 37/68] fix: ensure topic creation only occurs if thread_id is not already set --- efb_telegram_master/slave_message.py | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 2e6cee93..72767583 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -290,21 +290,22 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram if master_chat_info.is_forum: with self._topic_creation_locks[tg_dest]: thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) - try: - topic: ForumTopic = self.bot.create_forum_topic( - chat_id=tg_dest, - name=chat.chat_title - ) - thread_id = topic.message_thread_id - self.db.add_topic_assoc( - topic_chat_id=tg_dest, - message_thread_id=thread_id, - slave_uid=chat_uid, - ) - except telegram.error.BadRequest as e: - self.logger.info('Failed to create topic, Reason: %s', e) - tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) - thread_id = None + if not thread_id: + try: + topic: ForumTopic = self.bot.create_forum_topic( + chat_id=tg_dest, + name=chat.chat_title + ) + thread_id = topic.message_thread_id + self.db.add_topic_assoc( + topic_chat_id=tg_dest, + message_thread_id=thread_id, + slave_uid=chat_uid, + ) + except telegram.error.BadRequest as e: + self.logger.info('Failed to create topic, Reason: %s', e) + tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) + thread_id = None else: singly_linked = False From 117e2cb2c10a88b0aa8c445702b465ebc46659d0 Mon Sep 17 00:00:00 2001 From: Ovler Date: Thu, 17 Apr 2025 10:23:55 -0400 Subject: [PATCH 38/68] fix: retrieve topic thread ID without specifying topic chat ID --- efb_telegram_master/slave_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 72767583..518c3c36 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -289,7 +289,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram master_chat_info = self.bot.get_chat_info(tg_dest) if master_chat_info.is_forum: with self._topic_creation_locks[tg_dest]: - thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid, topic_chat_id=self.channel.topic_group) + thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) if not thread_id: try: topic: ForumTopic = self.bot.create_forum_topic( From 6a989c831ebd32de2960275349c1ac117ec3304a Mon Sep 17 00:00:00 2001 From: Ovler Date: Thu, 17 Apr 2025 17:01:24 -0400 Subject: [PATCH 39/68] chore: remove unused import of ForumTopic from telegram module --- efb_telegram_master/chat_binding.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index b9056fa7..01b48f17 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -11,7 +11,7 @@ import telegram # lgtm [py/import-and-import-from] from PIL import Image from telegram import Update, Message, TelegramError, InlineKeyboardButton, ChatAction, InlineKeyboardMarkup, \ - ParseMode, ForumTopic + ParseMode from telegram.error import BadRequest from telegram.ext import ConversationHandler, CommandHandler, CallbackQueryHandler, CallbackContext, Filters, \ MessageHandler From b71b9decbe52814c1a4fc82f6371cf98a7d4f5bf Mon Sep 17 00:00:00 2001 From: Ovler Date: Thu, 17 Apr 2025 17:28:56 -0400 Subject: [PATCH 40/68] feat: handle forum topics in chat binding manager --- efb_telegram_master/chat_binding.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index 01b48f17..e4cdb624 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -223,6 +223,22 @@ def link_chat_show_list(self, update: Update, context: CallbackContext): self.link_handler.conversations[storage_id] = Flags.LINK_EXEC self.msg_storage[storage_id] = ChatListStorage([chat]) return self.build_link_action_message(chat, tg_chat_id, tg_msg_id) + if rtm.chat.is_forum: + topic = rtm.message_thread_id + if topic: + slave_origin_uid = self.db.get_topic_slave( + topic_chat_id=utils.chat_id_to_str(self.channel.channel_id, ChatID(str(rtm.chat.id))), + message_thread_id=topic + ) + if slave_origin_uid: + channel_id, chat_id, _ = utils.chat_id_str_to_id(slave_origin_uid) + chat: ETMChatType = self.chat_manager.get_chat(channel_id, chat_id, build_dummy=True) + tg_chat_id = TelegramChatID(message.chat_id) + tg_msg_id = TelegramMessageID(message.reply_text(self._("Processing...")).message_id) + storage_id: Tuple[TelegramChatID, TelegramMessageID] = (tg_chat_id, tg_msg_id) + self.link_handler.conversations[storage_id] = Flags.LINK_EXEC + self.msg_storage[storage_id] = ChatListStorage([chat]) + return self.build_link_action_message(chat, tg_chat_id, tg_msg_id) if message.chat.type != telegram.Chat.PRIVATE: links = self.db.get_chat_assoc( From ed7b17466472fb098a11d77a6cb5a1921af8a97f Mon Sep 17 00:00:00 2001 From: Ovler Date: Thu, 17 Apr 2025 17:47:01 -0400 Subject: [PATCH 41/68] fix: update topic retrieval logic to use message_thread_id directly --- efb_telegram_master/chat_binding.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index e4cdb624..fc3dd352 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -223,11 +223,11 @@ def link_chat_show_list(self, update: Update, context: CallbackContext): self.link_handler.conversations[storage_id] = Flags.LINK_EXEC self.msg_storage[storage_id] = ChatListStorage([chat]) return self.build_link_action_message(chat, tg_chat_id, tg_msg_id) - if rtm.chat.is_forum: - topic = rtm.message_thread_id + if message.message_thread_id: + topic = message.message_thread_id if topic: slave_origin_uid = self.db.get_topic_slave( - topic_chat_id=utils.chat_id_to_str(self.channel.channel_id, ChatID(str(rtm.chat.id))), + topic_chat_id=TelegramChatID(message.chat_id), message_thread_id=topic ) if slave_origin_uid: From 77a75afe08109239b56234146cb67dbf6f790c28 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 03:50:41 -0400 Subject: [PATCH 42/68] fix: retrieve topic thread ID from database in message destination logic --- efb_telegram_master/slave_message.py | 1 + 1 file changed, 1 insertion(+) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 518c3c36..38890b9b 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -282,6 +282,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram if tg_chat and singly_linked: tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1])) + thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) elif not isinstance(chat, SystemChat) and self.channel.topic_group: tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) From 6c4bb216243c2f230c5ea8e289580ead1ba6e687 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 04:09:14 -0400 Subject: [PATCH 43/68] fix: simplify message destination logic by removing redundant thread ID retrieval --- efb_telegram_master/slave_message.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 38890b9b..4e5d5e43 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -279,14 +279,12 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram # Generate chat text template & Decide type target tg_dest = TelegramChatID(self.channel.config['admins'][0]) - - if tg_chat and singly_linked: + + if tg_chat: tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1])) - thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) - elif not isinstance(chat, SystemChat) and self.channel.topic_group: - tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) - thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) - if not thread_id: + if self.channel.topic_group: + if not isinstance(chat, SystemChat): + tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) master_chat_info = self.bot.get_chat_info(tg_dest) if master_chat_info.is_forum: with self._topic_creation_locks[tg_dest]: From b8366acf1bf865392aa647fbb9251c707e73a64b Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 04:17:16 -0400 Subject: [PATCH 44/68] fix: only create lock if thread_id not found --- efb_telegram_master/slave_message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 4e5d5e43..55909c66 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -287,9 +287,9 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) master_chat_info = self.bot.get_chat_info(tg_dest) if master_chat_info.is_forum: - with self._topic_creation_locks[tg_dest]: - thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) - if not thread_id: + thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) + if not thread_id: + with self._topic_creation_locks[tg_dest]: try: topic: ForumTopic = self.bot.create_forum_topic( chat_id=tg_dest, From a7d480822cc6c8e3715cf6ac2fa33f54be678d22 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 04:34:36 -0400 Subject: [PATCH 45/68] fix: update singly linked logic based on thread_id presence in message dispatch --- efb_telegram_master/slave_message.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 55909c66..1479ea12 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -305,8 +305,11 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram self.logger.info('Failed to create topic, Reason: %s', e) tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) thread_id = None - else: + + if not tg_chat: singly_linked = False + if thread_id: + singly_linked = True msg_template = self.generate_message_template(msg, singly_linked) self.logger.debug("[%s] Message is sent to Telegram chat %s, with header \"%s\".", From fbc9244f9982521953a41dc7347c55e9d0d2d193 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 04:38:35 -0400 Subject: [PATCH 46/68] fix: make sure only one thread_id is created --- efb_telegram_master/slave_message.py | 32 +++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 1479ea12..0385ee34 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -290,21 +290,23 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) if not thread_id: with self._topic_creation_locks[tg_dest]: - try: - topic: ForumTopic = self.bot.create_forum_topic( - chat_id=tg_dest, - name=chat.chat_title - ) - thread_id = topic.message_thread_id - self.db.add_topic_assoc( - topic_chat_id=tg_dest, - message_thread_id=thread_id, - slave_uid=chat_uid, - ) - except telegram.error.BadRequest as e: - self.logger.info('Failed to create topic, Reason: %s', e) - tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) - thread_id = None + thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) + if not thread_id: + try: + topic: ForumTopic = self.bot.create_forum_topic( + chat_id=tg_dest, + name=chat.chat_title + ) + thread_id = topic.message_thread_id + self.db.add_topic_assoc( + topic_chat_id=tg_dest, + message_thread_id=thread_id, + slave_uid=chat_uid, + ) + except telegram.error.BadRequest as e: + self.logger.info('Failed to create topic, Reason: %s', e) + tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) + thread_id = None if not tg_chat: singly_linked = False From 24730a27f6c1498d1412eeb427426e9f372d0247 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 04:50:28 -0400 Subject: [PATCH 47/68] fix: validate thread ID for forum messages in singly-linked logic --- efb_telegram_master/master_message.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index d040db84..eb55e951 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -160,6 +160,12 @@ def msg(self, update: Update, context: CallbackContext): if destination: quote = message.reply_to_message is not None self.logger.debug("[%s] Chat %s is singly-linked to %s", mid, message.chat, destination) + if message.is_forum: + ideal_thread_id = self.db.get_topic_thread_id(slave_uid=destination) + if ideal_thread_id and ideal_thread_id != message.message_thread_id: + self.logger.debug("[%s] Chat %s is singly-linked to %s, but the thread ID is not matching.", mid, message.chat, destination) + destination = None + quote = False if destination is None: thread_id = message.message_thread_id From 1b184f2b0dd0640db13c9fab17015a9e8cbbdbff Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 04:56:10 -0400 Subject: [PATCH 48/68] fix: correct typo --- efb_telegram_master/master_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index eb55e951..9d621fff 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -160,7 +160,7 @@ def msg(self, update: Update, context: CallbackContext): if destination: quote = message.reply_to_message is not None self.logger.debug("[%s] Chat %s is singly-linked to %s", mid, message.chat, destination) - if message.is_forum: + if message.chat.is_forum: ideal_thread_id = self.db.get_topic_thread_id(slave_uid=destination) if ideal_thread_id and ideal_thread_id != message.message_thread_id: self.logger.debug("[%s] Chat %s is singly-linked to %s, but the thread ID is not matching.", mid, message.chat, destination) From 89c99b18df3f005f83a72b429a82b37e4fb3c8d7 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 05:20:24 -0400 Subject: [PATCH 49/68] fix: update type hint for topic_chat_id parameter in get_topic_slave method --- efb_telegram_master/db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 9cbdaf06..ca9e5cf1 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -415,7 +415,7 @@ def get_topic_thread_id(slave_uid: EFBChannelChatIDStr) -> TelegramTopicID: return None @staticmethod - def get_topic_slave(topic_chat_id: TelegramChatID, + def get_topic_slave(topic_chat_id: TelegramTopicID, message_thread_id: Optional[EFBChannelChatIDStr] = None, ) -> Optional[EFBChannelChatIDStr]: """ @@ -423,7 +423,7 @@ def get_topic_slave(topic_chat_id: TelegramChatID, Only one parameter is to be provided. Args: - topic_chat_id (TelegramChatID): The topic UID + topic_chat_id (TelegramTopicID): The topic UID message_thread_id (TelegramTopicID): The message thread ID Returns: From 28897f423af7268dca8544038248cd73d12f26bd Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 05:54:19 -0400 Subject: [PATCH 50/68] fix: add get_topic_slaves method to retrieve topic association information --- efb_telegram_master/db.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index ca9e5cf1..ceed0778 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -441,6 +441,26 @@ def get_topic_slave(topic_chat_id: TelegramTopicID, except AttributeError: return None + @staticmethod + def get_topic_slaves(topic_chat_id: TelegramChatID) -> Optional[List[Tuple[EFBChannelChatIDStr, TelegramTopicID]]]: + """ + Get topic association (topic link) information. + Only one parameter is to be provided. + + Args: + topic_chat_id (TelegramChatID): The topic UID + + Returns: + List[Tuple[EFBChannelChatIDStr, TelegramTopicID]]: A list of tuples containing slave channel UID and message thread ID + """ + try: + return TopicAssoc.select(TopicAssoc.slave_uid, TopicAssoc.message_thread_id)\ + .where(TopicAssoc.topic_chat_id == topic_chat_id).order_by(TopicAssoc.id.desc()).tuples() + except DoesNotExist: + return None + except AttributeError: + return None + @staticmethod def remove_topic_assoc(topic_chat_id: Optional[TelegramChatID] = None, message_thread_id: Optional[EFBChannelChatIDStr] = None, From 8c2e2fd3170ee7590db7ac42cf3cb3879fd27ef2 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 05:54:29 -0400 Subject: [PATCH 51/68] fix: enhance topic association handling for forum messages in MasterMessageProcessor --- efb_telegram_master/master_message.py | 29 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index 9d621fff..ca32cb4c 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -168,16 +168,27 @@ def msg(self, update: Update, context: CallbackContext): quote = False if destination is None: - thread_id = message.message_thread_id - if thread_id: - destination = self.db.get_topic_slave(message_thread_id=thread_id, topic_chat_id=message.chat.id) - if destination: - quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id - if not quote: - message.reply_to_message = None + if message.chat.is_forum: + topic_destinations = self.db.get_topic_slaves(topic_chat_id=message.chat.id) + thread_id = message.message_thread_id + if thread_id: + for (destination, topic_id) in topic_destinations: + if topic_id == thread_id: + self.logger.debug("[%s] Chat %s is singly-linked to %s in topic %s", mid, message.chat, destination, topic_id) + destination = destination + quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id + if quote: + message.reply_to_message = None + break + if destination is None: + self.logger.debug("[%s] Ignored message as it's a topic which wasn't created by this bot", mid) + return else: - self.logger.debug("[%s] Ignored message as it's a topic which wasn't created by this bot", mid) - return + self.logger.debug("[%s] Chat %s is a forum, but no thread ID is found.", mid, message.chat) + destinations = self.db.get_chat_assoc(master_uid=utils.chat_id_to_str(self.channel_id, ChatID(str(message.chat.id)))) + if len(destinations) == len(topic_destinations): + self.logger.debug("[%s] Chat %s is a forum, and all destinations are in topics. The new message is not in any topic, so ignore it.", mid, message.chat) + return if destination is None: # not singly linked quote = False From 5459e7f66c222cd353a3c8f95097ce59e861d5b5 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 06:50:39 -0400 Subject: [PATCH 52/68] test: more candidates --- efb_telegram_master/master_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index ca32cb4c..d39165d8 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -217,7 +217,7 @@ def msg(self, update: Update, context: CallbackContext): if destination is None: self.logger.debug("[%s] Destination is not found for this message", mid) candidates = ( - self.db.get_recent_slave_chats(TelegramChatID(message.chat.id), limit=5) or + self.db.get_recent_slave_chats(TelegramChatID(message.chat.id), limit=5) and self.db.get_chat_assoc(master_uid=utils.chat_id_to_str(self.channel_id, ChatID(str(message.chat.id))))[:5] ) if candidates: From f734c0044cd828a75ce74ccb28357cafb86d4864 Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 07:34:53 -0400 Subject: [PATCH 53/68] fix: add return statement to prevent further processing when destination is None --- efb_telegram_master/master_message.py | 1 + 1 file changed, 1 insertion(+) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index d39165d8..adb1b771 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -166,6 +166,7 @@ def msg(self, update: Update, context: CallbackContext): self.logger.debug("[%s] Chat %s is singly-linked to %s, but the thread ID is not matching.", mid, message.chat, destination) destination = None quote = False + return if destination is None: if message.chat.is_forum: From d9c03418d76c155349e7b178dd974aaaa43100dd Mon Sep 17 00:00:00 2001 From: Ovler Date: Fri, 18 Apr 2025 08:29:55 -0400 Subject: [PATCH 54/68] fix: fix get_topic_slaves --- efb_telegram_master/db.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index ceed0778..8ae76c0f 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -454,8 +454,9 @@ def get_topic_slaves(topic_chat_id: TelegramChatID) -> Optional[List[Tuple[EFBCh List[Tuple[EFBChannelChatIDStr, TelegramTopicID]]: A list of tuples containing slave channel UID and message thread ID """ try: - return TopicAssoc.select(TopicAssoc.slave_uid, TopicAssoc.message_thread_id)\ - .where(TopicAssoc.topic_chat_id == topic_chat_id).order_by(TopicAssoc.id.desc()).tuples() + query = TopicAssoc.select(TopicAssoc.slave_uid, TopicAssoc.message_thread_id)\ + .where(TopicAssoc.topic_chat_id == topic_chat_id).order_by(TopicAssoc.id.desc()) + return [(row.slave_uid, int(row.message_thread_id)) for row in query] except DoesNotExist: return None except AttributeError: From f83d4918f44ccd0a3882717b24b00a47c0ff83ce Mon Sep 17 00:00:00 2001 From: Ovler Date: Mon, 12 May 2025 05:51:21 -0400 Subject: [PATCH 55/68] revert proposed by @jiz4oh --- efb_telegram_master/master_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index adb1b771..af1aabca 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -218,7 +218,7 @@ def msg(self, update: Update, context: CallbackContext): if destination is None: self.logger.debug("[%s] Destination is not found for this message", mid) candidates = ( - self.db.get_recent_slave_chats(TelegramChatID(message.chat.id), limit=5) and + self.db.get_recent_slave_chats(TelegramChatID(message.chat.id), limit=5) or self.db.get_chat_assoc(master_uid=utils.chat_id_to_str(self.channel_id, ChatID(str(message.chat.id))))[:5] ) if candidates: From 0816b5888aa1fa1ba17042927ade47c06f0bacf0 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Tue, 13 May 2025 09:31:52 +0800 Subject: [PATCH 56/68] fix: fix overlapped variable name --- efb_telegram_master/master_message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index af1aabca..79253283 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -173,10 +173,10 @@ def msg(self, update: Update, context: CallbackContext): topic_destinations = self.db.get_topic_slaves(topic_chat_id=message.chat.id) thread_id = message.message_thread_id if thread_id: - for (destination, topic_id) in topic_destinations: + for (dest, topic_id) in topic_destinations: if topic_id == thread_id: - self.logger.debug("[%s] Chat %s is singly-linked to %s in topic %s", mid, message.chat, destination, topic_id) - destination = destination + self.logger.debug("[%s] Chat %s is singly-linked to %s in topic %s", mid, message.chat, dest, topic_id) + destination = dest quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id if quote: message.reply_to_message = None From 618dee29b668a087f3f8f4e2d34f5871040dcad2 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Tue, 13 May 2025 09:53:46 +0800 Subject: [PATCH 57/68] fix: fix quote message failed --- efb_telegram_master/master_message.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index 79253283..9b96bd81 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -178,7 +178,7 @@ def msg(self, update: Update, context: CallbackContext): self.logger.debug("[%s] Chat %s is singly-linked to %s in topic %s", mid, message.chat, dest, topic_id) destination = dest quote = message.reply_to_message.message_id != message.reply_to_message.message_thread_id - if quote: + if not quote: message.reply_to_message = None break if destination is None: From fb88e75907ef51a5a57084dfc6f30d6cf1338b3f Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Wed, 14 May 2025 09:35:10 +0800 Subject: [PATCH 58/68] feat: add /update_info support for thread --- efb_telegram_master/bot_manager.py | 5 +++ efb_telegram_master/chat_binding.py | 61 ++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/efb_telegram_master/bot_manager.py b/efb_telegram_master/bot_manager.py index b9fe9ae8..0ba3b4e9 100644 --- a/efb_telegram_master/bot_manager.py +++ b/efb_telegram_master/bot_manager.py @@ -556,6 +556,11 @@ def get_chat_info(self, *args, **kwargs): def create_forum_topic(self, *args, **kwargs): return self.updater.bot.create_forum_topic(*args, **kwargs) + @Decorators.retry_on_timeout + @Decorators.retry_on_chat_migration + def edit_forum_topic(self, *args, **kwargs): + return self.updater.bot.edit_forum_topic(*args, **kwargs) + @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration def reopen_forum_topic(self, *args, **kwargs) -> bool: diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index fc3dd352..3e18717c 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -905,13 +905,18 @@ def update_group_info(self, update: Update, context: CallbackContext): if update.effective_chat.type == telegram.Chat.PRIVATE: return self.bot.reply_error(update, self._('Send /update_info to a group where this bot is a group admin ' 'to update group title, description and profile picture.')) + + if update.effective_chat.is_forum: + return self.update_thread_info(update, context) + forwarded_from_chat = update.effective_message.forward_from_chat if forwarded_from_chat and forwarded_from_chat.type == telegram.Chat.CHANNEL: - tg_chat = forwarded_from_chat.id + tg_chat = forwarded_from_chat else: - tg_chat = update.effective_chat.id + tg_chat = update.effective_chat + chats = self.db.get_chat_assoc(master_uid=utils.chat_id_to_str(channel=self.channel, - chat_uid=ChatID(str(tg_chat)))) + chat_uid=ChatID(str(tg_chat.id)))) if len(chats) != 1: return self.bot.reply_error(update, self.ngettext('This only works in a group linked with one chat. ' 'Currently {0} chat linked to this group.', @@ -929,7 +934,7 @@ def update_group_info(self, update: Update, context: CallbackContext): try: chat = self.chat_manager.update_chat_obj(channel.get_chat(chat_uid), full_update=True) - self.bot.set_chat_title(tg_chat, self.truncate_ellipsis(chat.chat_title, self.MAX_LEN_CHAT_TITLE)) + self.bot.set_chat_title(tg_chat.id, self.truncate_ellipsis(chat.chat_title, self.MAX_LEN_CHAT_TITLE)) # Update remote group members list to Telegram group description if available desc = chat.description @@ -944,7 +949,7 @@ def update_group_info(self, update: Update, context: CallbackContext): if desc: try: self.bot.set_chat_description( - tg_chat, self.truncate_ellipsis(desc, self.MAX_LEN_CHAT_DESC)) + tg_chat.id, self.truncate_ellipsis(desc, self.MAX_LEN_CHAT_DESC)) except BadRequest as e: if "Chat description is not modified" in e.message: pass @@ -969,7 +974,7 @@ def update_group_info(self, update: Update, context: CallbackContext): picture.seek(0) - self.bot.set_chat_photo(tg_chat, pic_resized or picture) + self.bot.set_chat_photo(tg_chat.id, pic_resized or picture) update.effective_message.reply_text(self._('Chat details updated.')) except EFBChatNotFound: self.logger.exception("Chat linked (%s) is not found in the slave channel " @@ -994,6 +999,50 @@ def update_group_info(self, update: Update, context: CallbackContext): if pic_resized and getattr(pic_resized, 'close', None): pic_resized.close() + def update_thread_info(self, update: Update, context: CallbackContext): + assert isinstance(update, Update) + assert update.effective_message + assert update.effective_chat + + try: + thread_id = update.effective_message.message_thread_id + if thread_id: + slave_origin_uid = self.db.get_topic_slave( + topic_chat_id=TelegramChatID(update.effective_message.chat_id), + message_thread_id=thread_id + ) + if not slave_origin_uid: + return self.bot.reply_error(update, self._("This chat is not managed by this bot. Update failed")) + channel_id, chat_id, _ = utils.chat_id_str_to_id(slave_origin_uid) + etm_chat: ETMChatType = self.chat_manager.get_chat(channel_id, chat_id, build_dummy=True) + self.bot.edit_forum_topic( + chat_id=update.effective_chat.id, + message_thread_id=thread_id, + name=self.truncate_ellipsis(etm_chat.chat_title, self.MAX_LEN_CHAT_TITLE), + icon_custom_emoji_id="" # param required by telegram + ) + update.effective_message.reply_text(self._('Chat details updated.')) + except EFBChatNotFound: + self.logger.exception("Chat linked (%s) is not found in the slave channel " + "(%s).", channel_id, chat_uid) + return self.bot.reply_error(update, self._("Chat linked ({chat_uid}) is not found in the slave channel " + "({channel_name}, {channel_id}).") + .format(channel_name=channel.channel_name, channel_id=channel_id, + chat_uid=chat_uid)) + except TelegramError as e: + if e.message == "Topic_not_modified": + update.effective_message.reply_text(self._('Chat details updated.')) + else: + self.logger.exception("Error occurred while update chat details.") + return self.bot.reply_error(update, self._('Error occurred while update chat details.\n' + '{0}'.format(e.message))) + except EFBOperationNotSupported: + return self.bot.reply_error(update, self._('No profile picture provided from this chat.')) + except Exception as e: + self.logger.exception("Unknown error caught when querying chat.") + return self.bot.reply_error(update, self._('Error occurred while update chat details. \n' + '{0}'.format(e))) + def chat_migration(self, update: Update, context: CallbackContext): """Triggered by any message update with either ``migrate_from_chat_id`` or ``migrate_to_chat_id`` From e3d59df618b9e77a0e8072bb827cdda6687d30f7 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Wed, 14 May 2025 10:25:16 +0800 Subject: [PATCH 59/68] feat: add /info support for topic --- efb_telegram_master/__init__.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/efb_telegram_master/__init__.py b/efb_telegram_master/__init__.py index b47ec7d7..f642e411 100644 --- a/efb_telegram_master/__init__.py +++ b/efb_telegram_master/__init__.py @@ -205,7 +205,10 @@ def info(self, update: Update, context: CallbackContext): assert isinstance(update, Update) assert isinstance(update.effective_message, Message) if update.effective_message.chat.type != telegram.Chat.PRIVATE: # Group message - msg = self.info_group(update) + if update.effective_chat.is_forum: + msg = self.info_topic(update) + else: + msg = self.info_group(update) elif update.effective_message.forward_from_chat and \ update.effective_message.forward_from_chat.type == 'channel': # Forwarded channel command. msg = self.info_channel(update) @@ -214,6 +217,32 @@ def info(self, update: Update, context: CallbackContext): update.effective_message.reply_text(msg) + def info_topic(self, update: Update): + """Generate string for chat linking info of a topic.""" + assert isinstance(update, Update) + assert isinstance(update.effective_message, Message) + + links = self.db.get_topic_slaves(topic_chat_id=update.effective_message.chat_id) + thread_id = update.effective_message.message_thread_id + if thread_id: + chat = None + for (dest, topic_id) in links: + if topic_id == thread_id: + chat = dest + break + if chat is None: + return "This chat is not managed by this bot" + else: + links = [chat] + else: + links = [c for c, t in links] + + msg = self._("The topic {topic_name} ({topic_id}) is linked to:").format( + topic_name=update.effective_message.chat.title, + topic_id=update.effective_message.chat_id) + msg += self.build_link_chats_info_str(links) + return msg + def info_general(self): """Generate string for information of the current running EFB instance.""" if self.instance_id: From 01de10312c5e37707230006e6e282f9d0691c7f9 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Wed, 14 May 2025 10:41:41 +0800 Subject: [PATCH 60/68] fix: telegram bot api limit message as 4096 characters --- efb_telegram_master/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/efb_telegram_master/__init__.py b/efb_telegram_master/__init__.py index f642e411..6c3f3a18 100644 --- a/efb_telegram_master/__init__.py +++ b/efb_telegram_master/__init__.py @@ -215,7 +215,11 @@ def info(self, update: Update, context: CallbackContext): else: # Talking to the bot. msg = self.info_general() - update.effective_message.reply_text(msg) + if len(msg) > 4095: + for x in range(0, len(msg), 4095): + update.effective_message.reply_text(msg[x:x+4095]) + else: + update.effective_message.reply_text(msg) def info_topic(self, update: Update): """Generate string for chat linking info of a topic.""" From 0ee294ae0a3e978e41f0be229150328326d0fdef Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Wed, 14 May 2025 17:06:37 +0800 Subject: [PATCH 61/68] feat: create topic after link --- efb_telegram_master/bot_manager.py | 4 +- efb_telegram_master/chat_binding.py | 55 ++++++++++++++++----------- efb_telegram_master/db.py | 22 +++++++---- efb_telegram_master/master_message.py | 2 +- efb_telegram_master/slave_message.py | 24 +----------- 5 files changed, 51 insertions(+), 56 deletions(-) diff --git a/efb_telegram_master/bot_manager.py b/efb_telegram_master/bot_manager.py index 0ba3b4e9..ebb04b9b 100644 --- a/efb_telegram_master/bot_manager.py +++ b/efb_telegram_master/bot_manager.py @@ -10,7 +10,7 @@ import telegram.constants import telegram.error from retrying import retry -from telegram import Update, InputFile, User, File +from telegram import Update, InputFile, User, File, ForumTopic from telegram.ext import CallbackContext, Filters, MessageHandler, Updater, Dispatcher from .locale_handler import LocaleHandler @@ -553,7 +553,7 @@ def get_chat_info(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration - def create_forum_topic(self, *args, **kwargs): + def create_forum_topic(self, *args, **kwargs) -> ForumTopic: return self.updater.bot.create_forum_topic(*args, **kwargs) @Decorators.retry_on_timeout diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index 3e18717c..cb5f9cce 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -5,6 +5,7 @@ import logging import re import urllib.parse +import threading from contextlib import suppress from typing import Tuple, Dict, Optional, List, TYPE_CHECKING, IO, Union, Pattern @@ -27,7 +28,7 @@ from .locale_mixin import LocaleMixin from .message import ETMMsg from .msg_type import TGMsgType -from .utils import EFBChannelChatIDStr, TelegramChatID, TelegramMessageID, TgChatMsgIDStr +from .utils import EFBChannelChatIDStr, TelegramChatID, TelegramMessageID, TgChatMsgIDStr, TelegramTopicID if TYPE_CHECKING: from . import TelegramChannel @@ -97,6 +98,7 @@ def __init__(self, channel: 'TelegramChannel'): self.bot: 'TelegramBotManager' = channel.bot_manager self.db: 'DatabaseManager' = channel.db self.chat_manager: 'ChatObjectCacheManager' = channel.chat_manager + self._topic_mutex = threading.Lock() # Link handler non_edit_filter = Filters.update.message | Filters.update.channel_post @@ -575,36 +577,20 @@ def link_chat(self, update: Update, args: Optional[List[str]]): # Use channel ID if command is forwarded from a channel. forwarded_chat = update.effective_message.forward_from_chat if forwarded_chat and forwarded_chat.type == telegram.Chat.CHANNEL: - tg_chat_to_link = forwarded_chat.id + tg_chat_to_link = forwarded_chat else: - tg_chat_to_link = update.effective_chat.id + tg_chat_to_link = update.effective_chat txt = self._('Trying to link chat {0}...').format(chat_display_name) - msg = self.bot.send_message(tg_chat_to_link, text=txt) + msg = self.bot.send_message(tg_chat_to_link.id, text=txt) - chat.link(self.channel.channel_id, ChatID(str(tg_chat_to_link)), self.channel.flag("multiple_slave_chats")) + chat.link(self.channel.channel_id, ChatID(str(tg_chat_to_link.id)), self.channel.flag("multiple_slave_chats")) self.db.remove_topic_assoc( slave_uid=chat_uid, ) - # chat_id = utils.chat_id_to_str(self.channel.channel_id, ChatID(str(tg_chat_to_link))) - # links = self.db.get_chat_assoc(master_uid=chat_id) - # if len(links) > 1 and self.channel.topic_group: - # try: - # topic: ForumTopic = self.bot.create_forum_topic( - # chat_id=chat_id, - # name=chat.chat_title - # ) - # thread_id = topic.message_thread_id - # self.db.add_topic_assoc( - # topic_chat_id=chat_id, - # message_thread_id=thread_id, - # slave_uid=chat_uid, - # ) - # return thread_id - # except telegram.error.BadRequest as e: - # self.logger.error('Failed to create topic, Reason: %s', e) - # return None + if tg_chat_to_link.is_forum: + self.create_topic(slave_uid=chat_uid, telegram_chat_id=TelegramChatID(tg_chat_to_link.id)) txt = self._("Chat {0} is now linked.").format(chat_display_name) self.bot.edit_message_text(text=txt, chat_id=msg.chat.id, message_id=msg.message_id) @@ -1043,6 +1029,29 @@ def update_thread_info(self, update: Update, context: CallbackContext): return self.bot.reply_error(update, self._('Error occurred while update chat details. \n' '{0}'.format(e))) + def create_topic(self, slave_uid: EFBChannelChatIDStr, telegram_chat_id: TelegramChatID) -> TelegramTopicID: + thread_id = self.db.get_topic_thread_id(slave_uid=slave_uid, topic_chat_id=telegram_chat_id) + if not thread_id: + with self._topic_mutex: + thread_id = self.db.get_topic_thread_id(slave_uid=slave_uid, topic_chat_id=telegram_chat_id) + if not thread_id: + channel_id, chat_id, _ = utils.chat_id_str_to_id(slave_uid) + chat: ETMChatType = self.chat_manager.get_chat(channel_id, chat_id, build_dummy=True) + try: + topic = self.bot.create_forum_topic( + chat_id=telegram_chat_id, + name=chat.chat_title + ) + thread_id = topic.message_thread_id + self.db.add_topic_assoc( + topic_chat_id=telegram_chat_id, + message_thread_id=topic.message_thread_id, + slave_uid=slave_uid, + ) + except Exception as e: + self.logger.info('Failed to create topic, Reason: %s', e) + return thread_id + def chat_migration(self, update: Update, context: CallbackContext): """Triggered by any message update with either ``migrate_from_chat_id`` or ``migrate_to_chat_id`` diff --git a/efb_telegram_master/db.py b/efb_telegram_master/db.py index 8ae76c0f..c3f719a0 100644 --- a/efb_telegram_master/db.py +++ b/efb_telegram_master/db.py @@ -394,36 +394,42 @@ def add_topic_assoc(self, topic_chat_id: TelegramChatID, return TopicAssoc.create(topic_chat_id=topic_chat_id, message_thread_id=message_thread_id, slave_uid=slave_uid) @staticmethod - def get_topic_thread_id(slave_uid: EFBChannelChatIDStr) -> TelegramTopicID: + def get_topic_thread_id(slave_uid: EFBChannelChatIDStr, topic_chat_id: TelegramChatID=None) -> Optional[TelegramTopicID]: """ Get topic association (topic link) information. Only one parameter is to be provided. Args: + topic_chat_id (TelegramChatID): The topic UID slave_uid (EFBChannelChatIDStr): Slave channel UID ("%(channel_id)s.%(chat_id)s") Returns: The message thread_id """ try: - assoc = TopicAssoc.select(TopicAssoc.message_thread_id)\ - .where(TopicAssoc.slave_uid == slave_uid)\ - .order_by(TopicAssoc.id.desc()).first() + if topic_chat_id: + assoc = TopicAssoc.select(TopicAssoc.message_thread_id)\ + .where(TopicAssoc.slave_uid == slave_uid, TopicAssoc.topic_chat_id == topic_chat_id)\ + .order_by(TopicAssoc.id.desc()).first() + else: + assoc = TopicAssoc.select(TopicAssoc.message_thread_id)\ + .where(TopicAssoc.slave_uid == slave_uid)\ + .order_by(TopicAssoc.id.desc()).first() if assoc: - return int(assoc.message_thread_id) + return TelegramTopicID(int(assoc.message_thread_id)) except DoesNotExist: return None @staticmethod - def get_topic_slave(topic_chat_id: TelegramTopicID, - message_thread_id: Optional[EFBChannelChatIDStr] = None, + def get_topic_slave(topic_chat_id: TelegramChatID, + message_thread_id: Optional[TelegramTopicID] = None, ) -> Optional[EFBChannelChatIDStr]: """ Get topic association (topic link) information. Only one parameter is to be provided. Args: - topic_chat_id (TelegramTopicID): The topic UID + topic_chat_id (TelegramChatID): The topic chat UID message_thread_id (TelegramTopicID): The message thread ID Returns: diff --git a/efb_telegram_master/master_message.py b/efb_telegram_master/master_message.py index 9b96bd81..c2e22630 100644 --- a/efb_telegram_master/master_message.py +++ b/efb_telegram_master/master_message.py @@ -161,7 +161,7 @@ def msg(self, update: Update, context: CallbackContext): quote = message.reply_to_message is not None self.logger.debug("[%s] Chat %s is singly-linked to %s", mid, message.chat, destination) if message.chat.is_forum: - ideal_thread_id = self.db.get_topic_thread_id(slave_uid=destination) + ideal_thread_id = self.db.get_topic_thread_id(slave_uid=destination, topic_chat_id=update.effective_chat.id) if ideal_thread_id and ideal_thread_id != message.message_thread_id: self.logger.debug("[%s] Chat %s is singly-linked to %s, but the thread ID is not matching.", mid, message.chat, destination) destination = None diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 0385ee34..300878dd 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -20,7 +20,7 @@ import telegram.ext from PIL import Image from telegram import InputFile, ChatAction, InputMediaPhoto, InputMediaDocument, InputMediaVideo, InputMediaAnimation, \ - InlineKeyboardMarkup, InlineKeyboardButton, ReplyMarkup, TelegramError, InputMedia, ForumTopic + InlineKeyboardMarkup, InlineKeyboardButton, ReplyMarkup, TelegramError, InputMedia from ehforwarderbot import Message, Status, coordinator from ehforwarderbot.chat import ChatNotificationState, SelfChatMember, GroupChat, PrivateChat, SystemChat, Chat @@ -55,7 +55,6 @@ def __init__(self, channel: 'TelegramChannel'): self.db: 'DatabaseManager' = channel.db self.chat_dest_cache: ChatDestinationCache = channel.chat_dest_cache self.chat_manager: ChatObjectCacheManager = channel.chat_manager - self._topic_creation_locks = defaultdict(threading.Lock) def is_silent(self, msg: Message) -> Optional[bool]: """Determine if a message shall be sent silently. @@ -287,26 +286,7 @@ def get_slave_msg_dest(self, msg: Message) -> Tuple[str, Tuple[Optional[Telegram tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) master_chat_info = self.bot.get_chat_info(tg_dest) if master_chat_info.is_forum: - thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) - if not thread_id: - with self._topic_creation_locks[tg_dest]: - thread_id = self.db.get_topic_thread_id(slave_uid=chat_uid) - if not thread_id: - try: - topic: ForumTopic = self.bot.create_forum_topic( - chat_id=tg_dest, - name=chat.chat_title - ) - thread_id = topic.message_thread_id - self.db.add_topic_assoc( - topic_chat_id=tg_dest, - message_thread_id=thread_id, - slave_uid=chat_uid, - ) - except telegram.error.BadRequest as e: - self.logger.info('Failed to create topic, Reason: %s', e) - tg_dest = TelegramChatID(int(utils.chat_id_str_to_id(tg_chat)[1]) if tg_chat else self.channel.topic_group) - thread_id = None + thread_id = self.channel.chat_binding.create_topic(slave_uid=chat_uid, telegram_chat_id=tg_dest) if not tg_chat: singly_linked = False From b957edb3a2a6adf0f3fe383b5bab2fd1220f221a Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Wed, 14 May 2025 19:52:03 +0800 Subject: [PATCH 62/68] feat: create topics automatically on migrate --- efb_telegram_master/chat_binding.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index cb5f9cce..ecc65183 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -1029,6 +1029,15 @@ def update_thread_info(self, update: Update, context: CallbackContext): return self.bot.reply_error(update, self._('Error occurred while update chat details. \n' '{0}'.format(e))) + def topic_migration(self, update: Update, context: CallbackContext): + assert isinstance(update, Update) + assert update.effective_message + + message = update.effective_message + chats = self.db.get_chat_assoc(master_uid=utils.chat_id_to_str(self.channel.channel_id, ChatID(str(message.chat.id)))) + for i in chats: + self.create_topic(slave_uid=i, telegram_chat_id=TelegramChatID(message.chat.id)) + def create_topic(self, slave_uid: EFBChannelChatIDStr, telegram_chat_id: TelegramChatID) -> TelegramTopicID: thread_id = self.db.get_topic_thread_id(slave_uid=slave_uid, topic_chat_id=telegram_chat_id) if not thread_id: @@ -1067,6 +1076,8 @@ def chat_migration(self, update: Update, context: CallbackContext): elif message.migrate_to_chat_id is not None: from_id = ChatID(str(message.chat.id)) to_id = ChatID(str(message.migrate_to_chat_id)) + if str(message.migrate_to_chat_id).startswith('-100') and self.bot.get_chat_info(message.migrate_to_chat_id).is_forum: + self.topic_migration(update, context) else: # Per ptb filter specs, this part of code should not be reached. return From 585d3432f55d026086a86fbfa9c729a74c04d2d5 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Wed, 14 May 2025 20:15:44 +0800 Subject: [PATCH 63/68] feat: add /init_topics command to batch create topics --- efb_telegram_master/chat_binding.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/efb_telegram_master/chat_binding.py b/efb_telegram_master/chat_binding.py index ecc65183..60ec2851 100644 --- a/efb_telegram_master/chat_binding.py +++ b/efb_telegram_master/chat_binding.py @@ -150,6 +150,7 @@ def __init__(self, channel: 'TelegramChannel'): # Update group title and profile picture self.bot.dispatcher.add_handler(CommandHandler('update_info', self.update_group_info)) + self.bot.dispatcher.add_handler(CommandHandler('init_topics', self.topic_migration)) self.bot.dispatcher.add_handler( MessageHandler(Filters.status_update.migrate, self.chat_migration)) @@ -590,7 +591,15 @@ def link_chat(self, update: Update, args: Optional[List[str]]): ) if tg_chat_to_link.is_forum: - self.create_topic(slave_uid=chat_uid, telegram_chat_id=TelegramChatID(tg_chat_to_link.id)) + thread_id = self.create_topic(slave_uid=chat_uid, telegram_chat_id=TelegramChatID(tg_chat_to_link.id)) + if not thread_id: + msg.reply_text( + self._( + "Failed to create topic for {name} in the group.\n" + "Please make sure the bot has the right.\n" + "You can send /init_topics to create again." + ).format(name=chat_display_name), + reply_to_message_id=msg.message_id) txt = self._("Chat {0} is now linked.").format(chat_display_name) self.bot.edit_message_text(text=txt, chat_id=msg.chat.id, message_id=msg.message_id) From feffd2f607a7e6bbf78189f1bcbbc39182050444 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Mon, 2 Jun 2025 20:47:36 +0800 Subject: [PATCH 64/68] build(deps): bump python-telegram-bot to 13.15 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 05a0f81f..30cd61ec 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ tests_require=tests_require, install_requires=[ "ehforwarderbot>=2.0.0", - "python-telegram-bot~=13.11", + "python-telegram-bot~=13.15", "python-magic", "ffmpeg-python", "peewee", From 03b8c10afaad7b5169de7b4970f801edfe5a2637 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Fri, 6 Jun 2025 10:52:49 +0800 Subject: [PATCH 65/68] refactor: revert unrelated changes --- efb_telegram_master/slave_message.py | 34 +++++++++------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 300878dd..b6142d14 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -820,29 +820,17 @@ def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, return self.bot.edit_message_caption(chat_id=old_msg_id[0], message_id=old_msg_id[1], reply_markup=reply_markup, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML") - # Sending new message (initial or fallback) - if not old_msg_id: # Ensure we are in the 'send new' path - assert msg.file is not None - with tempfile.NamedTemporaryFile(suffix=".ogg") as f: # Ensure correct suffix for pydub - try: - pydub.AudioSegment.from_file(msg.file).export(f.name, format="ogg", codec="libopus", - parameters=['-vbr', 'on']) - # process_file_obj might return URI or file object. send_voice expects content or path. - processed_path = self.process_file_obj(f, f.name) # Get path/URI - # Send using the path/URI - tg_msg = self.bot.send_voice(tg_dest, processed_path, prefix=msg_template, suffix=reactions, - caption=text, parse_mode="HTML", - reply_to_message_id=target_msg_id, - message_thread_id=thread_id, reply_markup=reply_markup, - disable_notification=silent) - return tg_msg - except pydub.exceptions.CouldntDecodeError as e: - self.logger.error("[%s] Failed to decode audio file for conversion: %s. Sending as file.", msg.uid, e) - msg.file.seek(0) - # Fallback to sending as a generic file - return self.slave_message_file(msg, tg_dest, thread_id, msg_template, reactions, - old_msg_id=None, # Ensure it sends as new - target_msg_id=target_msg_id, reply_markup=reply_markup, silent=silent) + + assert msg.file is not None + with tempfile.NamedTemporaryFile() as f: + pydub.AudioSegment.from_file(msg.file).export(f, format="ogg", codec="libopus", + parameters=['-vbr', 'on']) + file = self.process_file_obj(f, f.name) + tg_msg = self.bot.send_voice(tg_dest, file, prefix=msg_template, suffix=reactions, + caption=text, parse_mode="HTML", + reply_to_message_id=target_msg_id, reply_markup=reply_markup, + message_thread_id=thread_id, + disable_notification=silent) return tg_msg finally: if msg.file is not None: From dbfc156b38a9922151de940a9c013f79b867efc1 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Fri, 6 Jun 2025 10:55:16 +0800 Subject: [PATCH 66/68] refactor: revert unrelated changes --- efb_telegram_master/slave_message.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index b6142d14..81ad65af 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -811,7 +811,7 @@ def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, if old_msg_id: if edit_media: - self.logger.warning("[%s] Cannot edit voice message media. Sending new message instead.", msg.uid) + # Cannot edit voice message content, send a new one instead msg_template += " " + self._("[Edited]") if str(tg_dest) == old_msg_id[0]: target_msg_id = target_msg_id or old_msg_id[1] @@ -820,7 +820,6 @@ def slave_message_voice(self, msg: Message, tg_dest: TelegramChatID, return self.bot.edit_message_caption(chat_id=old_msg_id[0], message_id=old_msg_id[1], reply_markup=reply_markup, prefix=msg_template, suffix=reactions, caption=text, parse_mode="HTML") - assert msg.file is not None with tempfile.NamedTemporaryFile() as f: pydub.AudioSegment.from_file(msg.file).export(f, format="ogg", codec="libopus", From fa5b9058458c7f7010f78c88aed15153bce88b3a Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Thu, 31 Jul 2025 13:30:09 +0800 Subject: [PATCH 67/68] fix: add missing log --- efb_telegram_master/slave_message.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 81ad65af..1ecef004 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -134,6 +134,9 @@ def send_message(self, msg: Message) -> Message: topic_chat_id=tg_dest, message_thread_id=thread_id, ) + else: + self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", + repr(msg), repr(e), traceback.format_exc()) else: self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", repr(msg), repr(e), traceback.format_exc()) From d821c516090ec24fde42783c58e511d7511743e2 Mon Sep 17 00:00:00 2001 From: jiz4oh Date: Tue, 19 Aug 2025 18:03:59 +0800 Subject: [PATCH 68/68] refactor: use retry_on_topic_closed decorator --- efb_telegram_master/bot_manager.py | 46 ++++++++++++++++++++++++++++ efb_telegram_master/slave_message.py | 19 +----------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/efb_telegram_master/bot_manager.py b/efb_telegram_master/bot_manager.py index ebb04b9b..97558d16 100644 --- a/efb_telegram_master/bot_manager.py +++ b/efb_telegram_master/bot_manager.py @@ -122,6 +122,28 @@ def caption_affix(self, *args, **kwargs): return caption_affix + @classmethod + def retry_on_topic_closed(cls, fn: Callable): + @wraps(fn) + def wrap(self: 'TelegramBotManager', *args, **kwargs): + try: + return fn(self, *args, **kwargs) + except telegram.error.BadRequest as e: + if "Topic_closed" in e.message: + if 'chat_id' in kwargs: + chat_id = kwargs['chat_id'] + else: + chat_id = args[0] + message_thread_id = kwargs.get('message_thread_id', None) + self.reopen_forum_topic( + chat_id=chat_id, + message_thread_id=message_thread_id + ) + return fn(self, *args, **kwargs) + else: + raise e + return wrap + @classmethod def retry_on_chat_migration(cls, fn: Callable): @wraps(fn) @@ -182,6 +204,7 @@ def __init__(self, channel: 'TelegramChannel'): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_message(self, *args, prefix: str = '', suffix: str = '', **kwargs): """ Send text message. @@ -232,6 +255,7 @@ def send_message(self, *args, prefix: str = '', suffix: str = '', **kwargs): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def edit_message_text(self, prefix='', suffix='', **kwargs): """ Edit text message. @@ -313,6 +337,7 @@ def _bot_edit_message_text_fallback(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.caption_affix_decorator @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_audio(self, *args, **kwargs): """ Send an audio file. @@ -337,6 +362,7 @@ def send_audio(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.caption_affix_decorator @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_voice(self, *args, **kwargs): """ Send an voice message. @@ -361,6 +387,7 @@ def send_voice(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.caption_affix_decorator @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_video(self, *args, **kwargs): """ Send an voice message. @@ -385,6 +412,7 @@ def send_video(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.caption_affix_decorator @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_document(self, *args, **kwargs): """ Send a document. @@ -404,6 +432,7 @@ def send_document(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.caption_affix_decorator @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_animation(self, *args, **kwargs): """ Send a document. @@ -423,6 +452,7 @@ def send_animation(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.caption_affix_decorator @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_photo(self, *args, **kwargs): """ Send a document. @@ -444,6 +474,7 @@ def send_photo(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_chat_action(self, *args, **kwargs): message_thread_id = kwargs.pop('message_thread_id', None) if message_thread_id != None: @@ -452,26 +483,31 @@ def send_chat_action(self, *args, **kwargs): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def edit_message_reply_markup(self, *args, **kwargs): return self.updater.bot.edit_message_reply_markup(*args, **kwargs) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_location(self, *args, **kwargs): return self.updater.bot.send_location(*args, **kwargs) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_venue(self, *args, **kwargs): return self.updater.bot.send_venue(*args, **kwargs) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def send_sticker(self, *args, **kwargs): return self.updater.bot.send_sticker(*args, **kwargs) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def get_me(self, *args, **kwargs): return self.updater.bot.get_me(*args, **kwargs) @@ -488,11 +524,13 @@ def session_expired(self, update: Update, context: CallbackContext): @Decorators.retry_on_timeout @Decorators.caption_affix_decorator @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def edit_message_caption(self, *args, **kwargs): return self.updater.bot.edit_message_caption(*args, **kwargs) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def edit_message_media(self, *args, **kwargs): return self.updater.bot.edit_message_media(*args, **kwargs) @@ -508,16 +546,19 @@ def reply_error(self, update, errmsg): @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def get_file(self, file_id: str) -> File: return self.updater.bot.get_file(file_id) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def delete_message(self, chat_id, message_id): return self.updater.bot.delete_message(chat_id, message_id) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def answer_callback_query(self, *args, prefix="", suffix="", text=None, message_id=None, **kwargs): if text is None: @@ -548,6 +589,7 @@ def answer_callback_query(self, *args, prefix="", suffix="", text=None, @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def get_chat_info(self, *args, **kwargs): return self.updater.bot.get_chat(*args, **kwargs) @@ -558,6 +600,7 @@ def create_forum_topic(self, *args, **kwargs) -> ForumTopic: @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def edit_forum_topic(self, *args, **kwargs): return self.updater.bot.edit_forum_topic(*args, **kwargs) @@ -568,16 +611,19 @@ def reopen_forum_topic(self, *args, **kwargs) -> bool: @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def set_chat_title(self, *args, **kwargs): return self.updater.bot.set_chat_title(*args, **kwargs) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def set_chat_photo(self, *args, **kwargs): return self.updater.bot.set_chat_photo(*args, **kwargs) @Decorators.retry_on_timeout @Decorators.retry_on_chat_migration + @Decorators.retry_on_topic_closed def set_chat_description(self, *args, **kwargs): return self.updater.bot.set_chat_description(*args, **kwargs) diff --git a/efb_telegram_master/slave_message.py b/efb_telegram_master/slave_message.py index 1ecef004..f8d45004 100644 --- a/efb_telegram_master/slave_message.py +++ b/efb_telegram_master/slave_message.py @@ -121,24 +121,7 @@ def send_message(self, msg: Message) -> Message: self.dispatch_message(msg, msg_template, old_msg_id, tg_dest, thread_id, silent) except Exception as e: - if isinstance(e, telegram.error.BadRequest) and e.message: - if "Topic" in e.message: - try: - self.bot.reopen_forum_topic( - chat_id=tg_dest, - message_thread_id=thread_id - ) - except telegram.error.BadRequest as e: - self.logger.error('Failed to reopen topic, Reason: %s', e) - self.db.remove_topic_assoc( - topic_chat_id=tg_dest, - message_thread_id=thread_id, - ) - else: - self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", - repr(msg), repr(e), traceback.format_exc()) - else: - self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", + self.logger.error("Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", repr(msg), repr(e), traceback.format_exc()) return msg