Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
39687bc
feat: add telegram topics support
jiz4oh Apr 12, 2025
9376bec
feat: Add master_message_thread_id column to msglog table
jiz4oh Apr 12, 2025
1ba102c
fix: Ensure TopicAssoc table is created during database initialization
jiz4oh Apr 12, 2025
2d811e0
fix: improve topic message handling and assoc lookup
jiz4oh Apr 12, 2025
0a23092
fix: add thread support to telegram chat actions
jiz4oh Apr 12, 2025
b6bcd82
fix: Mark get_topic_slave as static method to resolve TypeError
jiz4oh Apr 12, 2025
1b7c21b
fix: adjust quote behavior in topic threads
jiz4oh Apr 12, 2025
93f30c9
fix: handle None message_thread_id in chat actions
jiz4oh Apr 12, 2025
0914da2
feat: improve forum topic handling and persistence
jiz4oh Apr 12, 2025
cc2568c
fix: use chat name instead
jiz4oh Apr 12, 2025
1a8a276
fix: cleanup topic assoc when linking chats
jiz4oh Apr 12, 2025
4eb4ae3
fix: improve telegram topic message handling
jiz4oh Apr 12, 2025
69ec820
feat: use chat title property for forum topic
jiz4oh Apr 13, 2025
beb0939
feat: add topic_group experimental flag
jiz4oh Apr 13, 2025
cf16c70
fix: exclude system chats from topic group processing
jiz4oh Apr 14, 2025
71e6a59
fix: add ForumTopic import to slave_message.py
Ovler-Young Apr 16, 2025
2c6e3b9
fix: add ForumTopic import to slave_message.py
Ovler-Young Apr 16, 2025
44ba6ba
refactor: improve topic association handling in SlaveMessageProcessor…
Ovler-Young Apr 16, 2025
92e710b
fix: enhance remove_topic_assoc method to support message_thread_id a…
Ovler-Young Apr 16, 2025
6bb98e8
fix: update remove_topic_assoc call
Ovler-Young Apr 16, 2025
90d9908
fix: fix get_topic_thread_id method by removing topic_chat_id parameter
Ovler-Young Apr 16, 2025
945a91b
fix: remove master_message_thread_id references and adjust message ha…
Ovler-Young Apr 16, 2025
6a186b6
chore: update type names
Ovler-Young Apr 16, 2025
6827520
fix: add forum topic creation and association handling in ChatBinding…
Ovler-Young Apr 16, 2025
89b6e04
fix: remove redundant topic association removal in ChatBindingManager…
Ovler-Young Apr 16, 2025
0d47517
fix: change error logging to info level and handle TelegramChatID ass…
Ovler-Young Apr 16, 2025
69c640c
fix: remove commented-out forum topic creation logic in ChatBindingMa…
Ovler-Young Apr 16, 2025
1623139
fix: comment out logic for reopening forum topics in SlaveMessageProc…
Ovler-Young Apr 16, 2025
95c2a62
fix: handle reopening forum topics on BadRequest error in SlaveMessag…
Ovler-Young Apr 16, 2025
498b333
fix: deal with all topics
Ovler-Young Apr 16, 2025
a74dd35
fix: update topic chat ID retrieval in MasterMessageProcessor
Ovler-Young Apr 16, 2025
ef733d6
fix: update get_topic_slave method to handle optional message_thread_id
Ovler-Young Apr 16, 2025
4ae52bb
feat: add get_chat_info method to retrieve chat details and update to…
Ovler-Young Apr 16, 2025
69ba075
fix: update topic group chat id
Ovler-Young Apr 17, 2025
a7740b6
fix: remove topic association on BadRequest error during topic reopening
Ovler-Young Apr 17, 2025
a7e295b
fix: locking before create new topic
jiz4oh Apr 17, 2025
ab0d713
Merge branch 'topic-support' into More-Topics
jiz4oh Apr 17, 2025
5f9892e
fix: ensure topic creation only occurs if thread_id is not already set
Ovler-Young Apr 17, 2025
117e2cb
fix: retrieve topic thread ID without specifying topic chat ID
Ovler-Young Apr 17, 2025
6a989c8
chore: remove unused import of ForumTopic from telegram module
Ovler-Young Apr 17, 2025
b71b9de
feat: handle forum topics in chat binding manager
Ovler-Young Apr 17, 2025
ed7b174
fix: update topic retrieval logic to use message_thread_id directly
Ovler-Young Apr 17, 2025
77a75af
fix: retrieve topic thread ID from database in message destination logic
Ovler-Young Apr 18, 2025
6c4bb21
fix: simplify message destination logic by removing redundant thread …
Ovler-Young Apr 18, 2025
b8366ac
fix: only create lock if thread_id not found
Ovler-Young Apr 18, 2025
a7d4808
fix: update singly linked logic based on thread_id presence in messag…
Ovler-Young Apr 18, 2025
fbc9244
fix: make sure only one thread_id is created
Ovler-Young Apr 18, 2025
24730a2
fix: validate thread ID for forum messages in singly-linked logic
Ovler-Young Apr 18, 2025
1b184f2
fix: correct typo
Ovler-Young Apr 18, 2025
89c99b1
fix: update type hint for topic_chat_id parameter in get_topic_slave …
Ovler-Young Apr 18, 2025
28897f4
fix: add get_topic_slaves method to retrieve topic association inform…
Ovler-Young Apr 18, 2025
8c2e2fd
fix: enhance topic association handling for forum messages in MasterM…
Ovler-Young Apr 18, 2025
5459e7f
test: more candidates
Ovler-Young Apr 18, 2025
f734c00
fix: add return statement to prevent further processing when destinat…
Ovler-Young Apr 18, 2025
d9c0341
fix: fix get_topic_slaves
Ovler-Young Apr 18, 2025
f83d491
revert proposed by @jiz4oh
Ovler-Young May 12, 2025
9de5140
Merge pull request #2 from Ovler-Young/More-Topics
Ovler-Young May 12, 2025
0816b58
fix: fix overlapped variable name
jiz4oh May 13, 2025
618dee2
fix: fix quote message failed
jiz4oh May 13, 2025
fb88e75
feat: add /update_info support for thread
jiz4oh May 14, 2025
e3d59df
feat: add /info support for topic
jiz4oh May 14, 2025
01de103
fix: telegram bot api limit message as 4096 characters
jiz4oh May 14, 2025
0ee294a
feat: create topic after link
jiz4oh May 14, 2025
b957edb
feat: create topics automatically on migrate
jiz4oh May 14, 2025
585d343
feat: add /init_topics command to batch create topics
jiz4oh May 14, 2025
feffd2f
build(deps): bump python-telegram-bot to 13.15
jiz4oh Jun 2, 2025
03b8c10
refactor: revert unrelated changes
jiz4oh Jun 6, 2025
dbfc156
refactor: revert unrelated changes
jiz4oh Jun 6, 2025
fa5b905
fix: add missing log
jiz4oh Jul 31, 2025
d821c51
refactor: use retry_on_topic_closed decorator
jiz4oh Aug 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------------------------------

Expand Down
43 changes: 39 additions & 4 deletions efb_telegram_master/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +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] = TelegramChatID(self.flag('topic_group'))

if not self.flag('auto_locale'):
self.translator = translation("efb_telegram_master",
Expand Down Expand Up @@ -204,14 +205,47 @@ 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)
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."""
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."""
Expand Down Expand Up @@ -322,9 +356,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,
Expand Down
71 changes: 70 additions & 1 deletion efb_telegram_master/bot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -444,31 +474,40 @@ 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:
kwargs['api_kwargs'] = { "message_thread_id": message_thread_id}
return self.updater.bot.send_chat_action(*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)

Expand All @@ -485,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)

Expand All @@ -505,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:
Expand Down Expand Up @@ -545,16 +589,41 @@ 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)

@Decorators.retry_on_timeout
@Decorators.retry_on_chat_migration
def create_forum_topic(self, *args, **kwargs) -> ForumTopic:
return self.updater.bot.create_forum_topic(*args, **kwargs)

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

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

Expand Down
Loading
Loading