def build_link_chats_info_str(self, links: List[EFBChannelChatIDStr]) -> str: """Build a string indicating all linked chats in argument. Returns: String that starts with a line break. """ msg = "" for i in links: channel_id, chat_id, _ = etm_utils.chat_id_str_to_id(i) chat_object = self.chat_manager.get_chat(channel_id, chat_id) if chat_object: msg += "\n- %s (%s:%s)" % (chat_object.full_name, channel_id, chat_id) else: try: module = coordinator.get_module_by_id(channel_id) if isinstance(module, Channel): channel_name = f"{module.channel_emoji} {module.channel_name}" else: # module is Middleware channel_name = module.middleware_name msg += self._( "\n- {channel_name}: Unknown chat ({channel_id}:{chat_id})" ).format(channel_name=channel_name, channel_id=channel_id, chat_id=chat_id) except NameError: # TRANSLATORS: ‘channel’ here means an EFB channel. msg += self._( "\n- Unknown channel {channel_id}: ({chat_id})" ).format(channel_id=channel_id, chat_id=chat_id) return msg
def __setstate__(self, state: Dict[str, Any]): # Try to load channel object try: state['channel'] = coordinator.get_module_by_id(state['channel']) except NameError: del state['channel'] self.__dict__.update(state)
def build_etm_msg(self, chat_manager: ChatObjectCacheManager, recur: bool = True) -> ETMMsg: c_module, c_id, _ = chat_id_str_to_id(self.slave_origin_uid) a_module, a_id, a_grp = chat_id_str_to_id(self.slave_member_uid) chat: 'ETMChatType' = chat_manager.get_chat(c_module, c_id, build_dummy=True) author: 'ETMChatMember' = chat_manager.get_chat_member(a_module, a_grp, a_id, build_dummy=True) # type: ignore msg = ETMMsg( uid=self.slave_message_id, chat=chat, author=author, text=self.text, type=MsgType(self.msg_type), type_telegram=TGMsgType(self.media_type), mime=self.mime or None, file_id=self.file_id or None, ) with suppress(NameError): to_module = coordinator.get_module_by_id(self.sent_to) if isinstance(to_module, Channel): msg.deliver_to = to_module # - ``target``: ``master_msg_id`` of the target message # - ``is_system`` # - ``attributes`` # - ``commands`` # - ``substitutions``: ``Dict[Tuple[int, int], SlaveChatID]`` # - ``reactions``: ``Dict[str, Collection[SlaveChatID]]`` if self.pickle: misc_data: PickledDict = pickle.loads(self.pickle) if 'target' in misc_data and recur: target_row = self.get_or_none(MsgLog.master_msg_id == misc_data['target']) if target_row: msg.target = target_row.build_etm_msg(chat_manager, recur=False) if 'is_system' in misc_data: msg.is_system = misc_data['is_system'] if 'attributes' in misc_data: msg.attributes = misc_data['attributes'] if 'commands' in misc_data: msg.commands = misc_data['commands'] if 'substitutions' in misc_data: subs = Substitutions({}) for sk, sv in misc_data['substitutions'].items(): module_id, chat_id, group_id = chat_id_str_to_id(sv) if group_id: subs[sk] = chat_manager.get_chat_member(module_id, group_id, chat_id, build_dummy=True) else: subs[sk] = chat_manager.get_chat(module_id, chat_id, build_dummy=True) msg.substitutions = subs if 'reactions' in misc_data: reactions: Dict[ReactionName, List[ETMChatMember]] = {} for rk, rv in misc_data['reactions'].items(): reactions[rk] = [] for idx in rv: module_id, chat_id, group_id = chat_id_str_to_id(idx) reactions[rk].append(chat_manager.get_chat_member(module_id, group_id, chat_id, build_dummy=True)) # type: ignore msg.reactions = reactions return msg
def link_chat(self, update, args): """Actual code of linking a chat by manipulating database. Triggered by ``/start BASE64(msg_id_to_str(chat_id, msg_id))``. """ try: msg_id = utils.message_id_str_to_id(utils.b64de(args[0])) storage_key: Tuple[int, int] = (int(msg_id[0]), int(msg_id[1])) data = self.msg_storage[storage_key] except KeyError: return update.message.reply_text( self._("Session expired or unknown parameter. (SE02)")) chat: ETMChatType = data.chats[0] chat_display_name = chat.full_name slave_channel, slave_chat_uid = chat.module_id, chat.uid try: coordinator.get_module_by_id(slave_channel) except NameError: self.bot.edit_message_text(text=self._( "{module_id} is not activated in current profile. " "It cannot be linked.").format(module_id=slave_channel), chat_id=storage_key[0], message_id=storage_key[1]) # Use channel ID if command is forwarded from a channel. forwarded_chat: Chat = update.effective_message.forward_from_chat if forwarded_chat and forwarded_chat.type == telegram.Chat.CHANNEL: tg_chat_to_link = forwarded_chat.id else: tg_chat_to_link = update.effective_chat.id txt = self._('Trying to link chat {0}...').format(chat_display_name) msg = self.bot.send_message(tg_chat_to_link, text=txt) chat.link(self.channel.channel_id, tg_chat_to_link, self.channel.flag("multiple_slave_chats")) 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) self.bot.edit_message_text(chat_id=storage_key[0], message_id=storage_key[1], text=txt) self.msg_storage.pop(storage_key, None)
def chat_head_req_generate(self, chat_id: TelegramChatID, message_id: TelegramMessageID = None, offset: int = 0, pattern: str = "", chats: List[EFBChannelChatIDStr] = None): """ Generate the list for chat head, and update it to a message. Args: chat_id: Chat ID message_id: ID of message to be updated, None to send a new message. offset: Offset for pagination. pattern: Regex String used as a filter. chats: Specified list of chats to start a chat head. """ if message_id is None: message_id = self.bot.send_message( chat_id, text=self._("Processing...")).message_id self.bot.send_chat_action(chat_id, ChatAction.TYPING) if chats and len(chats): if len(chats) == 1: slave_channel_id, slave_chat_id, _ = utils.chat_id_str_to_id( chats[0]) # TODO: Channel might be gone, add a check here. chat = self.chat_manager.get_chat(slave_channel_id, slave_chat_id) if chat: msg_text = self._( 'This group is linked to {0}. ' 'Send a message to this group to deliver it to the chat.\n' 'Do NOT reply to this system message.').format( chat.full_name) else: try: channel = coordinator.get_module_by_id( slave_channel_id) if isinstance(channel, Channel): name = channel.channel_name else: name = channel.middleware_name msg_text = self._( "This group is linked to an unknown chat ({chat_id}) " "on channel {channel_name} ({channel_id}). Possibly you can " "no longer reach this chat. Send /unlink_all to unlink all chats " "from this group.").format( channel_name=name, channel_id=slave_channel_id, chat_id=slave_chat_id) except NameError: msg_text = self._( "This group is linked to a chat from a channel that is not activated " "({channel_id}, {chat_id}). You cannot reach this chat unless the channel is " "enabled. Send /unlink_all to unlink all chats " "from this group.").format( channel_id=slave_channel_id, chat_id=slave_chat_id) self.bot.edit_message_text(text=msg_text, chat_id=chat_id, message_id=message_id) return ConversationHandler.END else: msg_text = self._( "This Telegram group is linked to the following chats, " "choose one to start a conversation with.") else: msg_text = "Choose a chat you want to start a conversation with." legend, chat_btn_list = self.slave_chats_pagination( (chat_id, message_id), offset, pattern=pattern, source_chats=chats) msg_text += self._("\n\nLegend:\n") for i in legend: msg_text += f"{i}\n" self.bot.edit_message_text( text=msg_text, chat_id=chat_id, message_id=message_id, reply_markup=InlineKeyboardMarkup(chat_btn_list)) self.chat_head_handler.conversations[( chat_id, message_id)] = Flags.CHAT_HEAD_CONFIRM
def slave_chats_pagination(self, storage_id: Tuple[TelegramChatID, TelegramMessageID], offset: int = 0, pattern: Optional[str] = "", source_chats: Optional[List[EFBChannelChatIDStr]] = None, filter_availability: bool = True) \ -> Tuple[List[str], List[List[InlineKeyboardButton]]]: """ Generate a list of (list of) `InlineKeyboardButton`s of chats in slave channels, based on the status of message located by `storage_id` and the paging from `offset` value. Args: pattern: Regular expression filter for chat details storage_id (Tuple[int, int]): Message_storage ID for generating the buttons list. offset (int): Offset for pagination source_chats (Optional[List[str]]): A list of chats used to generate the pagination list. Each str is in the format of "{channel_id}.{chat_uid}". filter_availability (bool): Whether to filter chats based on the availabilities. Only works when ``source_chats`` is specified. Returns: Tuple[List[str], List[List[telegram.InlineKeyboardButton]]]: A tuple: legend, chat_btn_list `legend` is the legend of all Emoji headings in the entire list. `chat_btn_list` is a list which can be fit into `telegram.InlineKeyboardMarkup`. """ self.logger.debug( "Generating pagination of chats.\nStorage ID: %s; Offset: %s; Filter: %s; Source chats: %s;", storage_id, offset, pattern, source_chats) legend: List[str] = [ self._("{0}: Linked").format(Emoji.LINK), self._("{0}: User").format(Emoji.USER), self._("{0}: Group").format(Emoji.GROUP), ] chat_list: Optional[ChatListStorage] = self.msg_storage.get( storage_id, None) if chat_list is None or chat_list.length == 0: # Generate the full chat list first re_filter: Union[str, Pattern, None] = None if pattern: self.logger.debug("Filter pattern: %s", pattern) escaped_pattern = re.escape(pattern) # Use simple string match if no regex significance is found. if pattern == escaped_pattern: re_filter = pattern else: # Use simple string match if regex provided is invalid try: re_filter = re.compile(pattern, re.DOTALL | re.IGNORECASE) except re.error: re_filter = pattern chats: List[ETMChatType] = [] if source_chats: for s_chat in source_chats: channel_id, chat_uid, _ = utils.chat_id_str_to_id(s_chat) with suppress(NameError): coordinator.get_module_by_id(channel_id) chat = self.chat_manager.get_chat( channel_id, chat_uid, build_dummy=not filter_availability) if not chat: self.logger.debug( "slave_chats_pagination with chat list: Chat %s not found.", s_chat) continue if chat.match(re_filter): chats.append(chat) else: for etm_chat in self.chat_manager.all_chats: if etm_chat.match(re_filter): chats.append(etm_chat) chats.sort(key=lambda a: a.last_message_time, reverse=True) chat_list = self.msg_storage[storage_id] = ChatListStorage( chats, offset) # self._db_update_slave_chats_cache(chat_list.chats) for ch in chat_list.channels.values(): legend.append(f"{ch.channel_emoji}: {ch.channel_name}") # Build inline button list chat_btn_list: List[List[InlineKeyboardButton]] = [] chats_per_page = self.channel.flag("chats_per_page") for idx in range(offset, min(offset + chats_per_page, chat_list.length)): chat = chat_list.chats[idx] if chat.linked: mode = Emoji.LINK else: mode = "" chat_type = chat.chat_type_emoji chat_name = chat.long_name button_text = f"{chat.channel_emoji}{chat_type}{mode}: {chat_name}" button_callback = f"chat {idx}" chat_btn_list.append([ InlineKeyboardButton(button_text, callback_data=button_callback) ]) # Pagination page_number_row: List[InlineKeyboardButton] = [] if offset - chats_per_page >= 0: page_number_row.append( InlineKeyboardButton( self._("< Prev"), callback_data=f"offset {offset - chats_per_page}")) page_number_row.append( InlineKeyboardButton(self._("Cancel"), callback_data=Flags.CANCEL_PROCESS)) if offset + chats_per_page < chat_list.length: page_number_row.append( InlineKeyboardButton( self._("Next >"), callback_data=f"offset {offset + chats_per_page}")) chat_btn_list.append(page_number_row) return legend, chat_btn_list
def dispatch_message(self, msg: EFBMsg, msg_template: str, old_msg_id: Optional[OldMsgID], tg_dest: TelegramChatID, silent: bool = False): """Dispatch with header, destination and Telegram message ID and destinations.""" xid = msg.uid # When targeting a message (reply to) target_msg_id: Optional[TelegramMessageID] = None if isinstance(msg.target, EFBMsg): self.logger.debug("[%s] Message is replying to %s.", msg.uid, msg.target) log = self.db.get_msg_log( slave_msg_id=msg.target.uid, slave_origin_uid=utils.chat_id_to_str(chat=msg.target.chat) ) if not log: self.logger.debug("[%s] Target message %s is not found in database.", msg.uid, msg.target) 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) if not target_msg or target_msg[0] != str(tg_dest): self.logger.error('[%s] Trying to reply to a message not from this chat. ' 'Message destination: %s. Target message: %s.', msg.uid, tg_dest, target_msg) target_msg_id = None else: target_msg_id = target_msg[1] # Generate basic reply markup commands: Optional[List[EFBMsgCommand]] = None reply_markup: Optional[telegram.InlineKeyboardMarkup] = None if msg.commands: commands = msg.commands.commands buttons = [] for i, ival in enumerate(commands): buttons.append([telegram.InlineKeyboardButton(ival.name, callback_data=str(i))]) reply_markup = telegram.InlineKeyboardMarkup(buttons) reactions = self.build_reactions_footer(msg.reactions) msg.text = msg.text or "" # 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, 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, 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, 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, reply_markup, silent) else: tg_msg = self.slave_message_image(msg, tg_dest, 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, 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, reply_markup, silent) elif msg.type == MsgType.Audio: tg_msg = self.slave_message_audio(msg, tg_dest, 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, 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, reply_markup, silent) elif msg.type == MsgType.Unsupported: tg_msg = self.slave_message_unsupported(msg, tg_dest, msg_template, reactions, old_msg_id, target_msg_id, reply_markup, silent) else: self.bot.send_chat_action(tg_dest, telegram.ChatAction.TYPING) tg_msg = self.bot.send_message(tg_dest, prefix=msg_template, suffix=reactions, disable_notification=silent, text=self._('Unknown type of message "{0}". (UT01)') .format(msg.type.name)) if tg_msg and commands: self.channel.commands.register_command(tg_msg, ETMCommandMsgStorage( commands, coordinator.get_module_by_id(msg.author.module_id), msg_template, msg.text )) self.logger.debug("[%s] Message is sent to the user with telegram message id %s.%s.", xid, tg_msg.chat.id, tg_msg.message_id) etm_msg = ETMMsg.from_efbmsg(msg, self.db) etm_msg.put_telegram_file(tg_msg) pickled_msg = etm_msg.pickle(self.db) self.logger.debug("[%s] Pickle size: %s", xid, len(pickled_msg)) msg_log = {"master_msg_id": utils.message_id_to_str(tg_msg.chat.id, tg_msg.message_id), "text": msg.text or "Sent a %s." % msg.type.name, "msg_type": msg.type.name, "sent_to": "master" if msg.author.is_self else 'slave', "slave_origin_uid": utils.chat_id_to_str(chat=msg.chat), "slave_origin_display_name": msg.chat.chat_alias, "slave_member_uid": msg.author.chat_uid if not msg.author.is_self else None, "slave_member_display_name": msg.author.chat_alias if not msg.author.is_self else None, "slave_message_id": msg.uid, "update": msg.edit, "media_type": etm_msg.type_telegram.value, "file_id": etm_msg.file_id, "mime": etm_msg.mime, "pickle": pickled_msg } if old_msg_id and old_msg_id != tg_msg.message_id: msg_log['master_msg_id'] = utils.message_id_to_str(*old_msg_id) msg_log['master_msg_id_alt'] = utils.message_id_to_str(tg_msg.chat.id, tg_msg.message_id) # self.db.add_msg_log(**msg_log) self.db.add_task(self.db.add_msg_log, tuple(), msg_log)
def send_message(self, msg: EFBMsg) -> EFBMsg: """ Process a message from slave channel and deliver it to the user. Args: msg (EFBMsg): The message. """ try: xid = msg.uid self.logger.debug("[%s] Slave message delivered to ETM.\n%s", xid, msg) chat_uid = utils.chat_id_to_str(chat=msg.chat) tg_chat = self.db.get_chat_assoc(slave_uid=chat_uid) if tg_chat: tg_chat = tg_chat[0] self.logger.debug("[%s] The message should deliver to %s", xid, tg_chat) if tg_chat == ETMChat.MUTE_CHAT_ID: self.logger.debug("[%s] Sender of the message is muted.", xid) return msg multi_slaves = False if tg_chat: slaves = self.db.get_chat_assoc(master_uid=tg_chat) if slaves and len(slaves) > 1: multi_slaves = True self.logger.debug( "[%s] Sender is linked with other chats in a Telegram group.", xid) self.logger.debug("[%s] Message is in chat %s", xid, msg.chat) # Generate chat text template & Decide type target tg_dest = self.channel.config['admins'][0] if tg_chat: # if this chat is linked tg_dest = int(utils.chat_id_str_to_id(tg_chat)[1]) msg_template = self.generate_message_template( msg, tg_chat, multi_slaves) self.logger.debug( "[%s] Message is sent to Telegram chat %s, with header \"%s\".", xid, tg_dest, msg_template) # When editing message old_msg_id: Tuple[str, str] = None if msg.edit: old_msg = self.db.get_msg_log( slave_msg_id=msg.uid, slave_origin_uid=utils.chat_id_to_str(chat=msg.chat)) if old_msg: if old_msg.master_msg_id_alt: old_msg_id = utils.message_id_str_to_id( old_msg.master_msg_id_alt) else: old_msg_id = utils.message_id_str_to_id( old_msg.master_msg_id) else: self.logger.info( '[%s] Was supposed to edit this message, ' 'but it does not exist in database. Sending new message instead.', msg.uid) # When targeting a message (reply to) target_msg_id: str = None if isinstance(msg.target, EFBMsg): self.logger.debug("[%s] Message is replying to %s.", msg.uid, msg.target) log = self.db.get_msg_log( slave_msg_id=msg.target.uid, slave_origin_uid=utils.chat_id_to_str( chat=msg.target.chat)) if not log: self.logger.debug( "[%s] Target message %s is not found in database.", msg.uid, msg.target) else: self.logger.debug( "[%s] Target message has database entry: %s.", msg.uid, log) target_msg_id = utils.message_id_str_to_id( log.master_msg_id) if not target_msg_id or target_msg_id[0] != str(tg_dest): self.logger.error( '[%s] Trying to reply to a message not from this chat. ' 'Message destination: %s. Target message: %s.', msg.uid, tg_dest, target_msg_id) target_msg_id = None else: target_msg_id = target_msg_id[1] commands: Optional[List[EFBMsgCommand]] = None reply_markup: Optional[telegram.InlineKeyboardMarkup] = None if msg.commands: commands = msg.commands.commands buttons = [] for i, ival in enumerate(commands): buttons.append([ telegram.InlineKeyboardButton(ival.name, callback_data=str(i)) ]) reply_markup = telegram.InlineKeyboardMarkup(buttons) msg.text = msg.text or "" # Type dispatching if msg.type == MsgType.Text: tg_msg = self.slave_message_text(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Link: tg_msg = self.slave_message_link(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Sticker: tg_msg = self.slave_message_image(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Image: if self.flag("send_image_as_file"): tg_msg = self.slave_message_file(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) else: tg_msg = self.slave_message_image(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.File: tg_msg = self.slave_message_file(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Audio: tg_msg = self.slave_message_audio(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Location: tg_msg = self.slave_message_location(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Video: tg_msg = self.slave_message_video(msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) elif msg.type == MsgType.Unsupported: tg_msg = self.slave_message_unsupported( msg, tg_dest, msg_template, old_msg_id, target_msg_id, reply_markup) else: self.bot.send_chat_action(tg_dest, telegram.ChatAction.TYPING) tg_msg = self.bot.send_message( tg_dest, prefix=msg_template, text=self._("Unsupported type of message. (UT01)")) if tg_msg and msg.commands: self.channel.commands.register_command( tg_msg, ETMCommandMsgStorage( commands, coordinator.get_module_by_id(msg.author.module_id), msg_template, msg.text)) self.logger.debug( "[%s] Message is sent to the user with telegram message id %s.%s.", xid, tg_msg.chat.id, tg_msg.message_id) msg_log = { "master_msg_id": utils.message_id_to_str(tg_msg.chat.id, tg_msg.message_id), "text": msg.text or "Sent a %s." % msg.type, "msg_type": msg.type, "sent_to": "master" if msg.author.is_self else 'slave', "slave_origin_uid": utils.chat_id_to_str(chat=msg.chat), "slave_origin_display_name": msg.chat.chat_alias, "slave_member_uid": msg.author.chat_uid if not msg.author.is_self else None, "slave_member_display_name": msg.author.chat_alias if not msg.author.is_self else None, "slave_message_id": msg.uid, "update": msg.edit } if old_msg_id and old_msg_id != tg_msg.message_id: msg_log['master_msg_id'] = utils.message_id_to_str(*old_msg_id) msg_log['master_msg_id_alt'] = utils.message_id_to_str( tg_msg.chat.id, tg_msg.message_id) # Store media related information to local database for tg_media_type in ('audio', 'animation', 'document', 'video', 'voice', 'video_note'): attachment = getattr(tg_msg, tg_media_type, None) if attachment: msg_log.update(media_type=tg_media_type, file_id=attachment.file_id, mime=attachment.mime_type) break if not msg_log.get('media_type', None): if getattr(tg_msg, 'sticker', None): msg_log.update(media_type='sticker', file_id=tg_msg.sticker.file_id, mime='image/webp') elif getattr(tg_msg, 'photo', None): attachment = tg_msg.photo[-1] msg_log.update(media_type=tg_media_type, file_id=attachment.file_id, mime='image/jpeg') self.db.add_msg_log(**msg_log) self.logger.debug("[%s] Message inserted/updated to the database.", xid) except Exception as e: self.logger.error( "[%s] Error occurred while processing message from slave channel.\nMessage: %s\n%s\n%s", xid, repr(msg), repr(e), traceback.format_exc())