def process_telegram_message(self, bot: telegram.Bot, update: telegram.Update, channel_id: Optional[str] = None, chat_id: Optional[str] = None, target_msg: Optional[str] = None): """ Process messages came from Telegram. Args: bot: Telegram bot update: Telegram message update channel_id: Slave channel ID if specified chat_id: Slave chat ID if specified target_msg: Target slave message if specified Returns: """ target: Optional[str] = None target_channel: Optional[str] = None target_log: Optional['MsgLog'] = None # Message ID for logging message_id = utils.message_id_to_str(update=update) multi_slaves: bool = False destination: Optional[str] = None slave_msg: Optional[EFBMsg] = None message: telegram.Message = update.effective_message edited = bool(update.edited_message or update.edited_channel_post) self.logger.debug('[%s] Message is edited: %s, %s', message_id, edited, message.edit_date) private_chat = update.effective_chat.type == telegram.Chat.PRIVATE if not private_chat: # from group linked_chats = self.db.get_chat_assoc( master_uid=utils.chat_id_to_str(self.channel_id, update.effective_chat.id)) if len(linked_chats) == 1: destination = linked_chats[0] elif len(linked_chats) > 1: multi_slaves = True reply_to = bool(getattr(message, "reply_to_message", None)) # Process predefined target (slave) chat. if channel_id and chat_id: destination = utils.chat_id_to_str(channel_id, chat_id) if target_msg: target_log = self.db.get_msg_log(master_msg_id=target_msg) if target_log: target = target_log.slave_origin_uid target_channel, target_uid = utils.chat_id_str_to_id( target) else: self.logger.info( "[%s], Predefined chat %d.%d with target msg") return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another message. (UC07)")) elif private_chat: if reply_to: dest_msg = self.db.get_msg_log( master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if dest_msg: destination = dest_msg.slave_origin_uid else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another one. (UC03)")) else: return self.bot.reply_error( update, self._("Please reply to an incoming message. (UC04)")) else: # group chat if multi_slaves: if reply_to: dest_msg = self.db.get_msg_log( master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if dest_msg: destination = dest_msg.slave_origin_uid else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another one. (UC05)")) else: return self.bot.reply_error( update, self. _("This group is linked to multiple remote chats. " "Please reply to an incoming message. " "To unlink all remote chats, please send /unlink_all . (UC06)" )) elif destination: if reply_to: target_log = \ self.db.get_msg_log(master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if target_log: target = target_log.slave_origin_uid target_channel, target_uid = utils.chat_id_str_to_id( target) else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another message. (UC07)")) else: return self.bot.reply_error( update, self._("This group is not linked to any chat. (UC06)")) self.logger.debug( "[%s] Telegram received. From private chat: %s; Group has multiple linked chats: %s; " "Message replied to another message: %s", message_id, private_chat, multi_slaves, reply_to) self.logger.debug("[%s] Destination chat = %s", message_id, destination) channel, uid = utils.chat_id_str_to_id(destination) if channel not in coordinator.slaves: return self.bot.reply_error( update, self._("Internal error: Channel \"{0}\" not found.").format( channel)) m = EFBMsg() try: m.uid = message_id mtype = get_msg_type(message) # Chat and author related stuff m.author = EFBChat(self.channel).self() m.chat = EFBChat(coordinator.slaves[channel]) m.chat.chat_uid = uid chat_info = self.db.get_slave_chat_info(channel, uid) if chat_info: m.chat.chat_name = chat_info.slave_chat_name m.chat.chat_alias = chat_info.slave_chat_alias m.chat.chat_type = ChatType(chat_info.slave_chat_type) m.deliver_to = coordinator.slaves[channel] if target and target_channel == channel: trgt_msg = EFBMsg() trgt_msg.type = MsgType.Text trgt_msg.text = target_log.text trgt_msg.uid = target_log.slave_message_id trgt_msg.chat = EFBChat(coordinator.slaves[target_channel]) trgt_msg.chat.chat_name = target_log.slave_origin_display_name trgt_msg.chat.chat_alias = target_log.slave_origin_display_name trgt_msg.chat.chat_uid = utils.chat_id_str_to_id( target_log.slave_origin_uid)[1] if target_log.slave_member_uid: trgt_msg.author = EFBChat( coordinator.slaves[target_channel]) trgt_msg.author.chat_name = target_log.slave_member_display_name trgt_msg.author.chat_alias = target_log.slave_member_display_name trgt_msg.author.chat_uid = target_log.slave_member_uid elif target_log.sent_to == 'master': trgt_msg.author = trgt_msg.chat else: trgt_msg.author = EFBChat(self.channel).self() m.target = trgt_msg self.logger.debug( "[%s] This message replies to another message of the same channel.\n" "Chat ID: %s; Message ID: %s.", message_id, trgt_msg.chat.chat_uid, trgt_msg.uid) # Type specific stuff self.logger.debug("[%s] Message type from Telegram: %s", message_id, mtype) if self.TYPE_DICT.get(mtype, None): m.type = self.TYPE_DICT[mtype] self.logger.debug("[%s] EFB message type: %s", message_id, mtype) else: self.logger.info( "[%s] Message type %s is not supported by ETM", message_id, mtype) raise EFBMessageTypeNotSupported( "Message type %s is not supported by ETM" % mtype) if m.type not in coordinator.slaves[ channel].supported_message_types: self.logger.info( "[%s] Message type %s is not supported by channel %s", message_id, m.type.name, channel) raise EFBMessageTypeNotSupported( "Message type %s is not supported by channel %s" % (m.type, coordinator.slaves[channel].channel_name)) # Parse message text and caption to markdown msg_md_text = message.text and message.text_markdown if msg_md_text and msg_md_text == escape_markdown(message.text): msg_md_text = message.text msg_md_text = msg_md_text or "" msg_md_caption = message.caption and message.caption_markdown if msg_md_caption and msg_md_caption == escape_markdown( message.caption): msg_md_caption = message.caption msg_md_caption = msg_md_caption or "" # Flag for edited message if edited: m.edit = True text = msg_md_text or msg_md_caption msg_log = self.db.get_msg_log( master_msg_id=utils.message_id_to_str(update=update)) if not msg_log or msg_log == self.FAIL_FLAG: raise EFBMessageNotFound() m.uid = msg_log.slave_message_id if text.startswith(self.DELETE_FLAG): coordinator.send_status( EFBMessageRemoval( source_channel=self.channel, destination_channel=coordinator.slaves[channel], message=m)) self.db.delete_msg_log( master_msg_id=utils.message_id_to_str(update=update)) m = None return self.logger.debug('[%s] Message is edited (%s)', m.uid, m.edit) # Enclose message as an EFBMsg object by message type. if mtype == TGMsgType.Text: m.text = msg_md_text elif mtype == TGMsgType.Photo: m.text = msg_md_caption m.file, m.mime, m.filename, m.path = self._download_file( message.photo[-1]) elif mtype == TGMsgType.Sticker: # Convert WebP to the more common PNG m.text = "" m.file, m.mime, m.filename, m.path = self._download_file( message.sticker, 'image/webp') self.logger.debug( "[%s] Trying to convert WebP sticker (%s) to PNG.", message_id, m.path) f = tempfile.NamedTemporaryFile(suffix=".png") Image.open(m.file).convert("RGBA").save(f, 'png') m.file.close() m.file, m.mime, m.filename, m.path = f, 'image/png', os.path.basename( f.name), f.name self.logger.debug( "[%s] WebP sticker is converted to PNG (%s).", message_id, f.name) elif mtype == TGMsgType.Animation: m.text = "" self.logger.debug( "[%s] Telegram message is a \"Telegram GIF\".", message_id) m.filename = getattr(message.document, "file_name", None) or None m.file, m.mime, m.filename, m.path = self._download_gif( message.document, channel) m.mime = message.document.mime_type or m.mime elif mtype == TGMsgType.Document: m.text = msg_md_caption self.logger.debug("[%s] Telegram message type is document.", message_id) m.filename = getattr(message.document, "file_name", None) or None m.file, m.mime, filename, m.path = self._download_file( message.document, message.document.mime_type) m.filename = m.filename or filename m.mime = message.document.mime_type or m.mime elif mtype == TGMsgType.Video: m.text = msg_md_caption m.file, m.mime, m.filename, m.path = self._download_file( message.video, message.video.mime_type) elif mtype == TGMsgType.Audio: m.text = "%s - %s\n%s" % (message.audio.title, message.audio.performer, msg_md_caption) m.file, m.mime, m.filename, m.path = self._download_file( message.audio, message.audio.mime_type) elif mtype == TGMsgType.Voice: m.text = msg_md_caption m.file, m.mime, m.filename, m.path = self._download_file( message.voice, message.voice.mime_type) elif mtype == TGMsgType.Location: # TRANSLATORS: Message body text for location messages. m.text = self._("Location") m.attributes = EFBMsgLocationAttribute( message.location.latitude, message.location.longitude) elif mtype == TGMsgType.Venue: m.text = message.location.title + "\n" + message.location.adderss m.attributes = EFBMsgLocationAttribute( message.venue.location.latitude, message.venue.location.longitude) elif mtype == TGMsgType.Contact: contact: telegram.Contact = message.contact m.text = self._( "Shared a contact: {first_name} {last_name}\n{phone_number}" ).format(first_name=contact.first_name, last_name=contact.last_name, phone_number=contact.phone_number) else: raise EFBMessageTypeNotSupported( self._("Message type {0} is not supported.").format(mtype)) # return self.bot.reply_error(update, "Message type not supported. (MN02)") slave_msg = coordinator.send_message(m) except EFBChatNotFound as e: self.bot.reply_error(update, e.args[0] or self._("Chat is not found.")) except EFBMessageTypeNotSupported as e: self.bot.reply_error( update, e.args[0] or self._("Message type is not supported.")) except EFBOperationNotSupported as e: self.bot.reply_error( update, self._("Message editing is not supported.\n\n{!s}".format(e))) except Exception as e: self.bot.reply_error( update, self._("Message is not sent.\n\n{!r}".format(e))) finally: if m: msg_log_d = { "master_msg_id": utils.message_id_to_str(update=update), "text": m.text or "Sent a %s" % m.type, "slave_origin_uid": utils.chat_id_to_str(chat=m.chat), "slave_origin_display_name": "__chat__", "msg_type": m.type, "sent_to": "slave", "slave_message_id": None if m.edit else "%s.%s" % (self.FAIL_FLAG, int(time.time())), # Overwritten later if slave message ID exists "update": m.edit } # Store media related information to local database for tg_media_type in ('audio', 'animation', 'document', 'video', 'voice', 'video_note'): attachment = getattr(message, tg_media_type, None) if attachment: msg_log_d.update(media_type=tg_media_type, file_id=attachment.file_id, mime=attachment.mime_type) break if not msg_log_d.get('media_type', None): if getattr(message, 'sticker', None): msg_log_d.update(media_type='sticker', file_id=message.sticker.file_id, mime='image/webp') elif getattr(message, 'photo', None): attachment = message.photo[-1] msg_log_d.update(media_type=tg_media_type, file_id=attachment.file_id, mime='image/jpeg') if slave_msg: msg_log_d['slave_message_id'] = slave_msg.uid self.db.add_msg_log(**msg_log_d) if m.file: m.file.close()
def process_telegram_message(self, update: Update, context: CallbackContext, destination: EFBChannelChatIDStr, quote: bool = False): """ Process messages came from Telegram. Args: update: Telegram message update context: PTB update context destination: Destination of the message specified. quote: If the message shall quote another one """ # Message ID for logging message_id = utils.message_id_to_str(update=update) message: Message = update.effective_message edited = bool(update.edited_message or update.edited_channel_post) self.logger.debug('[%s] Message is edited: %s, %s', message_id, edited, message.edit_date) channel, uid, gid = utils.chat_id_str_to_id(destination) if channel not in coordinator.slaves: return self.bot.reply_error( update, self._("Internal error: Slave channel “{0}” not found."). format(channel)) m = ETMMsg() log_message = True try: m.uid = MessageID(message_id) # Store Telegram message type m.type_telegram = mtype = get_msg_type(message) if self.TYPE_DICT.get(mtype, None): m.type = self.TYPE_DICT[mtype] self.logger.debug("[%s] EFB message type: %s", message_id, mtype) else: self.logger.info( "[%s] Message type %s is not supported by ETM", message_id, mtype) raise EFBMessageTypeNotSupported( self. _("{type_name} messages are not supported by EFB Telegram Master channel." ).format(type_name=mtype.name)) m.put_telegram_file(message) # Chat and author related stuff m.chat = self.chat_manager.get_chat(channel, uid, build_dummy=True) m.author = m.chat.self or m.chat.add_self() m.deliver_to = coordinator.slaves[channel] if quote: self.attach_target_message(message, m, channel) # Type specific stuff self.logger.debug("[%s] Message type from Telegram: %s", message_id, mtype) if m.type not in coordinator.slaves[ channel].supported_message_types: self.logger.info( "[%s] Message type %s is not supported by channel %s", message_id, m.type.name, channel) raise EFBMessageTypeNotSupported( self. _("{type_name} messages are not supported by slave channel {channel_name}." ).format(type_name=m.type.name, channel_name=coordinator.slaves[channel]. channel_name)) # Parse message text and caption to markdown msg_md_text = message.text and message.text_markdown if msg_md_text and msg_md_text == escape_markdown(message.text): msg_md_text = message.text msg_md_text = msg_md_text or "" msg_md_caption = message.caption and message.caption_markdown if msg_md_caption and msg_md_caption == escape_markdown( message.caption): msg_md_caption = message.caption msg_md_caption = msg_md_caption or "" # Flag for edited message if edited: m.edit = True text = msg_md_text or msg_md_caption msg_log = self.db.get_msg_log( master_msg_id=utils.message_id_to_str(update=update)) if not msg_log or msg_log.slave_message_id == self.db.FAIL_FLAG: raise EFBMessageNotFound() m.uid = msg_log.slave_message_id if text.startswith(self.DELETE_FLAG): coordinator.send_status( MessageRemoval( source_channel=self.channel, destination_channel=coordinator.slaves[channel], message=m)) if not self.channel.flag('prevent_message_removal'): try: message.delete() except telegram.TelegramError: message.reply_text( self._("Message is removed in remote chat.")) else: message.reply_text( self._("Message is removed in remote chat.")) log_message = False return self.logger.debug('[%s] Message is edited (%s)', m.uid, m.edit) if m.file_unique_id and m.file_unique_id != msg_log.file_unique_id: self.logger.debug( "[%s] Message media is edited (%s -> %s)", m.uid, msg_log.file_unique_id, m.file_unique_id) m.edit_media = True # Enclose message as an Message object by message type. if mtype is TGMsgType.Text: m.text = msg_md_text elif mtype is TGMsgType.Photo: m.text = msg_md_caption m.mime = "image/jpeg" self._check_file_download(message.photo[-1]) elif mtype in (TGMsgType.Sticker, TGMsgType.AnimatedSticker): # Convert WebP to the more common PNG m.text = "" self._check_file_download(message.sticker) elif mtype is TGMsgType.Animation: m.text = msg_md_caption self.logger.debug( "[%s] Telegram message is a \"Telegram GIF\".", message_id) m.filename = getattr(message.document, "file_name", None) or None if m.filename and not m.filename.lower().endswith(".gif"): m.filename += ".gif" m.mime = message.document.mime_type or m.mime elif mtype is TGMsgType.Document: m.text = msg_md_caption self.logger.debug("[%s] Telegram message type is document.", message_id) m.filename = getattr(message.document, "file_name", None) or None m.mime = message.document.mime_type self._check_file_download(message.document) elif mtype is TGMsgType.Video: m.text = msg_md_caption m.mime = message.video.mime_type self._check_file_download(message.video) elif mtype is TGMsgType.VideoNote: m.text = msg_md_caption self._check_file_download(message.video) elif mtype is TGMsgType.Audio: m.text = "%s - %s\n%s" % (message.audio.title, message.audio.performer, msg_md_caption) m.mime = message.audio.mime_type self._check_file_download(message.audio) elif mtype is TGMsgType.Voice: m.text = msg_md_caption m.mime = message.voice.mime_type self._check_file_download(message.voice) elif mtype is TGMsgType.Location: # TRANSLATORS: Message body text for location messages. m.text = self._("Location") m.attributes = LocationAttribute(message.location.latitude, message.location.longitude) elif mtype is TGMsgType.Venue: m.text = f"📍 {message.location.title}\n{message.location.adderss}" m.attributes = LocationAttribute( message.venue.location.latitude, message.venue.location.longitude) elif mtype is TGMsgType.Contact: contact: telegram.Contact = message.contact m.text = self._( "Shared a contact: {first_name} {last_name}\n{phone_number}" ).format(first_name=contact.first_name, last_name=contact.last_name, phone_number=contact.phone_number) elif mtype is TGMsgType.Dice: # Per docs, message.dice must be one of [1, 2, 3, 4, 5, 6], # DICE_CHAR is of length 7, so should be safe. m.text = f"{self.DICE_CHAR[message.dice.value]} ({message.dice.value})" else: raise EFBMessageTypeNotSupported( self._("Message type {0} is not supported.").format( mtype.name)) slave_msg = coordinator.send_message(m) if slave_msg and slave_msg.uid: m.uid = slave_msg.uid else: m.uid = None except EFBChatNotFound as e: self.bot.reply_error(update, e.args[0] or self._("Chat is not found.")) except EFBMessageTypeNotSupported as e: self.bot.reply_error( update, e.args[0] or self._("Message type is not supported.")) except EFBOperationNotSupported as e: self.bot.reply_error( update, self._("Message editing is not supported.\n\n{exception!s}". format(exception=e))) except EFBException as e: self.bot.reply_error( update, self._("Message is not sent.\n\n{exception!s}".format( exception=e))) self.logger.exception( "Message is not sent. (update: %s, exception: %s)", update, e) except Exception as e: self.bot.reply_error( update, self._("Message is not sent.\n\n{exception!r}".format( exception=e))) self.logger.exception( "Message is not sent. (update: %s, exception: %s)", update, e) finally: if log_message: self.db.add_or_update_message_log(m, update.effective_message) if m.file: m.file.close()
def send_message(self, msg: EFBMessage) -> Message: self.logger.debug("Received message from master: %s", msg) try: target_msg_offset = 0 prefix = "" mentions = [] # Send message reaction # if msg.target and msg.text.startswith('r`') and \ # msg.target.uid.startswith("mid.$"): # self.logger.debug("[%s] Message is a reaction to another message: %s", msg.uid, msg.text) # msg_id = ".".join(msg.target.uid.split(".", 2)[:2]) # if getattr(MessageReaction, msg.text[2:], None): # self.client.reactToMessage(msg_id, getattr(MessageReaction, msg.text[2:])) # else: # self.client.reactToMessage(msg_id, self.CustomReaction(msg.text[2:])) # msg.uid = "__reaction__" # return msg # Message substitutions if msg.substitutions: self.logger.debug("[%s] Message has substitutions: %s", msg.uid, msg.substitutions) for i in msg.substitutions: mentions.append(Mention(msg.substitutions[i].id, target_msg_offset + i[0], i[1] - i[0])) self.logger.debug("[%s] Translated to mentions: %s", msg.uid, mentions) fb_msg = Message(text=prefix + msg.text, mentions=mentions) thread: Thread = self.client.fetchThreadInfo(msg.chat.uid)[str(msg.chat.uid)] if msg.target and msg.target.uid: fb_msg.reply_to_id = msg.target.uid if msg.type in (MsgType.Text, MsgType.Unsupported): # Remove variation selector-16 (force colored emoji) for # matching. emoji_compare = msg.text.replace("\uFE0F", "") if emoji_compare == "👍": fb_msg.sticker = Sticker(uid=EmojiSize.SMALL.value) if not prefix: fb_msg.text = None elif emoji_compare[:-1] == "👍" and emoji_compare[-1] in 'SML': if emoji_compare[-1] == 'S': fb_msg.sticker = Sticker(uid=EmojiSize.SMALL.value) elif emoji_compare[-1] == 'M': fb_msg.sticker = Sticker(uid=EmojiSize.MEDIUM.value) elif emoji_compare[-1] == 'L': fb_msg.sticker = Sticker(uid=EmojiSize.LARGE.value) if not prefix: fb_msg.text = None elif emoji_compare[:-1] in emoji.UNICODE_EMOJI and emoji_compare[-1] in 'SML': # type: ignore self.logger.debug("[%s] Message is an Emoji message: %s", msg.uid, emoji_compare) if emoji_compare[-1] == 'S': fb_msg.emoji_size = EmojiSize.SMALL elif emoji_compare[-1] == 'M': fb_msg.emoji_size = EmojiSize.MEDIUM elif emoji_compare[-1] == 'L': fb_msg.emoji_size = EmojiSize.LARGE fb_msg.text = emoji_compare[:-1] msg.uid = self.client.send(fb_msg, thread_id=thread.uid, thread_type=thread.type) elif msg.type in (MsgType.Image, MsgType.Sticker, MsgType.Animation): msg_uid = self.client.send_image_file(msg.filename, msg.file, msg.mime, message=fb_msg, thread_id=thread.uid, thread_type=thread.type) if msg_uid.startswith('mid.$'): self.client.sent_messages.add(msg_uid) self.logger.debug("Sent message with ID %s", msg_uid) msg.uid = msg_uid elif msg.type == MsgType.Voice: files = self.upload_file(msg, voice_clip=True) msg_uid = self.client._sendFiles(files=files, message=fb_msg, thread_id=thread.uid, thread_type=thread.type) if msg_uid.startswith('mid.$'): self.client.sent_messages.add(msg_uid) self.logger.debug("Sent message with ID %s", msg_uid) msg.uid = msg_uid elif msg.type in (MsgType.File, MsgType.Video): files = self.upload_file(msg) msg_uid = self.client._sendFiles(files=files, message=fb_msg, thread_id=thread.uid, thread_type=thread.type) if msg_uid.startswith('mid.$'): self.client.sent_messages.add(msg_uid) self.logger.debug("Sent message with ID %s", msg_uid) msg.uid = msg_uid elif msg.type == MsgType.Status: assert (isinstance(msg.attributes, StatusAttribute)) status: StatusAttribute = msg.attributes if status.status_type in (StatusAttribute.Types.TYPING, StatusAttribute.Types.UPLOADING_VOICE, StatusAttribute.Types.UPLOADING_VIDEO, StatusAttribute.Types.UPLOADING_IMAGE, StatusAttribute.Types.UPLOADING_FILE): self.client.setTypingStatus(TypingStatus.TYPING, thread_id=thread.uid, thread_type=thread.type) threading.Timer(status.timeout / 1000, self.stop_typing, args=(thread.uid, thread.type)).start() elif msg.type == MsgType.Link: assert (isinstance(msg.attributes, LinkAttribute)) link: LinkAttribute = msg.attributes if self.flag('send_link_with_description'): info: Tuple[str, ...] = (link.title,) if link.description: info += (link.description,) info += (link.url,) text = "\n".join(info) else: text = link.url if fb_msg.text: text = fb_msg.text + "\n" + text fb_msg.text = text msg.uid = self.client.send(fb_msg, thread_id=thread.uid, thread_type=thread.type) elif msg.type == MsgType.Location: assert (isinstance(msg.attributes, LocationAttribute)) location_attr: LocationAttribute = msg.attributes location = LocationAttachment(latitude=location_attr.latitude, longitude=location_attr.longitude) self.client.sendPinnedLocation(location, fb_msg, thread_id=thread.uid, thread_type=thread.type) else: raise EFBMessageTypeNotSupported() return msg finally: if msg.file and not msg.file.closed: msg.file.close() self.client.markAsSeen() self.client.markAsRead(msg.chat.uid)
def process_telegram_message( self, update: Update, context: CallbackContext, channel_id: Optional[ModuleID] = None, chat_id: Optional[ChatID] = None, target_msg: Optional[utils.TgChatMsgIDStr] = None): """ Process messages came from Telegram. Args: update: Telegram message update context: PTB update context channel_id: Slave channel ID if specified chat_id: Slave chat ID if specified target_msg: Target slave message if specified """ target: Optional[EFBChannelChatIDStr] = None target_channel: Optional[ModuleID] = None target_log: Optional['MsgLog'] = None # Message ID for logging message_id = utils.message_id_to_str(update=update) multi_slaves: bool = False destination: Optional[EFBChannelChatIDStr] = None slave_msg: Optional[EFBMsg] = None message: telegram.Message = update.effective_message edited = bool(update.edited_message or update.edited_channel_post) self.logger.debug('[%s] Message is edited: %s, %s', message_id, edited, message.edit_date) private_chat = update.effective_chat.type == telegram.Chat.PRIVATE if not private_chat: # from group linked_chats = self.db.get_chat_assoc( master_uid=utils.chat_id_to_str(self.channel_id, update.effective_chat.id)) if len(linked_chats) == 1: destination = linked_chats[0] elif len(linked_chats) > 1: multi_slaves = True reply_to = bool(getattr(message, "reply_to_message", None)) # Process predefined target (slave) chat. cached_dest = self.chat_dest_cache.get(message.chat.id) if channel_id and chat_id: destination = utils.chat_id_to_str(channel_id, chat_id) if target_msg is not None: target_log = self.db.get_msg_log(master_msg_id=target_msg) if target_log: target = target_log.slave_origin_uid if target is not None: target_channel, target_uid = utils.chat_id_str_to_id( target) else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another message. (UC07)")) elif private_chat: if reply_to: dest_msg = self.db.get_msg_log( master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if dest_msg: destination = dest_msg.slave_origin_uid self.chat_dest_cache.set(message.chat.id, dest_msg.slave_origin_uid) else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another one. (UC03)")) elif cached_dest: destination = cached_dest self._send_cached_chat_warning(update, message.chat.id, cached_dest) else: return self.bot.reply_error( update, self._("Please reply to an incoming message. (UC04)")) else: # group chat if multi_slaves: if reply_to: dest_msg = self.db.get_msg_log( master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if dest_msg: destination = dest_msg.slave_origin_uid assert destination is not None self.chat_dest_cache.set(message.chat.id, destination) else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another one. (UC05)")) elif cached_dest: destination = cached_dest self._send_cached_chat_warning(update, message.chat.id, cached_dest) else: return self.bot.reply_error( update, self. _("This group is linked to multiple remote chats. " "Please reply to an incoming message. " "To unlink all remote chats, please send /unlink_all . (UC06)" )) elif destination: if reply_to: target_log = \ self.db.get_msg_log(master_msg_id=utils.message_id_to_str( message.reply_to_message.chat.id, message.reply_to_message.message_id)) if target_log: target = target_log.slave_origin_uid if target is not None: target_channel, target_uid = utils.chat_id_str_to_id( target) else: return self.bot.reply_error( update, self._("Message is not found in database. " "Please try with another message. (UC07)")) else: return self.bot.reply_error( update, self._("This group is not linked to any chat. (UC06)")) self.logger.debug( "[%s] Telegram received. From private chat: %s; Group has multiple linked chats: %s; " "Message replied to another message: %s", message_id, private_chat, multi_slaves, reply_to) self.logger.debug("[%s] Destination chat = %s", message_id, destination) assert destination is not None channel, uid = utils.chat_id_str_to_id(destination) if channel not in coordinator.slaves: return self.bot.reply_error( update, self._("Internal error: Channel \"{0}\" not found.").format( channel)) m = ETMMsg() log_message = True try: m.uid = MessageID(message_id) m.put_telegram_file(message) mtype = m.type_telegram # Chat and author related stuff m.author = ETMChat(db=self.db, channel=self.channel).self() m.chat = ETMChat(db=self.db, channel=coordinator.slaves[channel]) m.chat.chat_uid = m.chat.chat_name = uid # TODO: get chat from ETM local cache when available chat_info = self.db.get_slave_chat_info(channel, uid) if chat_info: m.chat.chat_name = chat_info.slave_chat_name m.chat.chat_alias = chat_info.slave_chat_alias m.chat.chat_type = ChatType(chat_info.slave_chat_type) m.deliver_to = coordinator.slaves[channel] if target and target_log is not None and target_channel == channel: if target_log.pickle: trgt_msg: ETMMsg = ETMMsg.unpickle(target_log.pickle, self.db) trgt_msg.target = None else: trgt_msg = ETMMsg() trgt_msg.type = MsgType.Text trgt_msg.text = target_log.text trgt_msg.uid = target_log.slave_message_id trgt_msg.chat = ETMChat( db=self.db, channel=coordinator.slaves[target_channel]) trgt_msg.chat.chat_name = target_log.slave_origin_display_name trgt_msg.chat.chat_alias = target_log.slave_origin_display_name trgt_msg.chat.chat_uid = utils.chat_id_str_to_id( target_log.slave_origin_uid)[1] if target_log.slave_member_uid: trgt_msg.author = ETMChat( db=self.db, channel=coordinator.slaves[target_channel]) trgt_msg.author.chat_name = target_log.slave_member_display_name trgt_msg.author.chat_alias = target_log.slave_member_display_name trgt_msg.author.chat_uid = target_log.slave_member_uid elif target_log.sent_to == 'master': trgt_msg.author = trgt_msg.chat else: trgt_msg.author = ETMChat(db=self.db, channel=self.channel).self() m.target = trgt_msg self.logger.debug( "[%s] This message replies to another message of the same channel.\n" "Chat ID: %s; Message ID: %s.", message_id, trgt_msg.chat.chat_uid, trgt_msg.uid) # Type specific stuff self.logger.debug("[%s] Message type from Telegram: %s", message_id, mtype) if self.TYPE_DICT.get(mtype, None): m.type = self.TYPE_DICT[mtype] self.logger.debug("[%s] EFB message type: %s", message_id, mtype) else: self.logger.info( "[%s] Message type %s is not supported by ETM", message_id, mtype) raise EFBMessageTypeNotSupported( self._("Message type {} is not supported by ETM.").format( mtype.name)) if m.type not in coordinator.slaves[ channel].supported_message_types: self.logger.info( "[%s] Message type %s is not supported by channel %s", message_id, m.type.name, channel) raise EFBMessageTypeNotSupported( self._("Message type {0} is not supported by channel {1}." ).format(m.type.name, coordinator.slaves[channel].channel_name)) # Parse message text and caption to markdown msg_md_text = message.text and message.text_markdown if msg_md_text and msg_md_text == escape_markdown(message.text): msg_md_text = message.text msg_md_text = msg_md_text or "" msg_md_caption = message.caption and message.caption_markdown if msg_md_caption and msg_md_caption == escape_markdown( message.caption): msg_md_caption = message.caption msg_md_caption = msg_md_caption or "" # Flag for edited message if edited: m.edit = True text = msg_md_text or msg_md_caption msg_log = self.db.get_msg_log( master_msg_id=utils.message_id_to_str(update=update)) if not msg_log or msg_log == self.FAIL_FLAG: raise EFBMessageNotFound() m.uid = msg_log.slave_message_id if text.startswith(self.DELETE_FLAG): coordinator.send_status( EFBMessageRemoval( source_channel=self.channel, destination_channel=coordinator.slaves[channel], message=m)) self.db.delete_msg_log( master_msg_id=utils.message_id_to_str(update=update)) log_message = False return self.logger.debug('[%s] Message is edited (%s)', m.uid, m.edit) # Enclose message as an EFBMsg object by message type. if mtype == TGMsgType.Text: m.text = msg_md_text elif mtype == TGMsgType.Photo: m.text = msg_md_caption m.mime = "image/jpeg" self._check_file_download(message.photo[-1]) elif mtype in (TGMsgType.Sticker, TGMsgType.AnimatedSticker): # Convert WebP to the more common PNG m.text = "" self._check_file_download(message.sticker) elif mtype == TGMsgType.Animation: m.text = "" self.logger.debug( "[%s] Telegram message is a \"Telegram GIF\".", message_id) m.filename = getattr(message.document, "file_name", None) or None m.mime = message.document.mime_type or m.mime elif mtype == TGMsgType.Document: m.text = msg_md_caption self.logger.debug("[%s] Telegram message type is document.", message_id) m.filename = getattr(message.document, "file_name", None) or None m.mime = message.document.mime_type self._check_file_download(message.document) elif mtype == TGMsgType.Video: m.text = msg_md_caption m.mime = message.video.mime_type self._check_file_download(message.video) elif mtype == TGMsgType.Audio: m.text = "%s - %s\n%s" % (message.audio.title, message.audio.performer, msg_md_caption) m.mime = message.audio.mime_type self._check_file_download(message.audio) elif mtype == TGMsgType.Voice: m.text = msg_md_caption m.mime = message.voice.mime_type self._check_file_download(message.voice) elif mtype == TGMsgType.Location: # TRANSLATORS: Message body text for location messages. m.text = self._("Location") m.attributes = EFBMsgLocationAttribute( message.location.latitude, message.location.longitude) elif mtype == TGMsgType.Venue: m.text = message.location.title + "\n" + message.location.adderss m.attributes = EFBMsgLocationAttribute( message.venue.location.latitude, message.venue.location.longitude) elif mtype == TGMsgType.Contact: contact: telegram.Contact = message.contact m.text = self._( "Shared a contact: {first_name} {last_name}\n{phone_number}" ).format(first_name=contact.first_name, last_name=contact.last_name, phone_number=contact.phone_number) else: raise EFBMessageTypeNotSupported( self._("Message type {0} is not supported.").format(mtype)) slave_msg = coordinator.send_message(m) except EFBChatNotFound as e: self.bot.reply_error(update, e.args[0] or self._("Chat is not found.")) except EFBMessageTypeNotSupported as e: self.bot.reply_error( update, e.args[0] or self._("Message type is not supported.")) except EFBOperationNotSupported as e: self.bot.reply_error( update, self._("Message editing is not supported.\n\n{!s}".format(e))) except Exception as e: self.bot.reply_error( update, self._("Message is not sent.\n\n{!r}".format(e))) self.logger.exception("Message is not sent. (update: %s)", update) finally: if log_message: pickled_msg = m.pickle(self.db) self.logger.debug("[%s] Pickle size: %s", message_id, len(pickled_msg)) msg_log_d = { "master_msg_id": utils.message_id_to_str(update=update), "text": m.text or "Sent a %s" % m.type.name, "slave_origin_uid": utils.chat_id_to_str(chat=m.chat), "slave_origin_display_name": "__chat__", "msg_type": m.type.name, "sent_to": "slave", "slave_message_id": None if m.edit else "%s.%s" % (self.FAIL_FLAG, int(time.time())), # Overwritten later if slave message ID exists "update": m.edit, "media_type": m.type_telegram.value, "file_id": m.file_id, "mime": m.mime, "pickle": pickled_msg } if slave_msg: msg_log_d['slave_message_id'] = slave_msg.uid # self.db.add_msg_log(**msg_log_d) self.db.add_task(self.db.add_msg_log, tuple(), msg_log_d) if m.file: m.file.close()
def send_message(self, msg: EFBMsg) -> EFBMsg: """Send a message to WeChat. Supports text, image, sticker, and file. Args: msg (channel.EFBMsg): Message Object to be sent. Returns: This method returns nothing. Raises: EFBMessageTypeNotSupported: Raised when message type is not supported by the channel. """ chat: wxpy.Chat = self.chats.get_wxpy_chat_by_uid(msg.chat.chat_uid) r: wxpy.SentMessage self.logger.info("[%s] Sending message to WeChat:\n" "uid: %s\n" "UserName: %s\n" "NickName: %s\n" "Type: %s\n" "Text: %s", msg.uid, msg.chat.chat_uid, chat.user_name, chat.name, msg.type, msg.text) chat.mark_as_read() self.logger.debug('[%s] Is edited: %s', msg.uid, msg.edit) if msg.edit: if self.flag('delete_on_edit'): try: ews_utils.message_to_dummy_message(msg.uid, self).recall() except wxpy.ResponseError as e: self.logger.error("[%s] Trying to recall message but failed: %s", msg.uid, e) raise EFBMessageError(self._('Failed to recall message, edited message was not sent.')) else: raise EFBOperationNotSupported() if msg.type in [MsgType.Text, MsgType.Link]: if isinstance(msg.target, EFBMsg): max_length = self.flag("max_quote_length") qt_txt = "%s" % msg.target.text if max_length > 0: tgt_text = qt_txt[:max_length] if len(qt_txt) >= max_length: tgt_text += "…" tgt_text = "「%s」" % tgt_text elif max_length < 0: tgt_text = "「%s」" % qt_txt else: tgt_text = "" if isinstance(chat, wxpy.Group) and not msg.target.author.is_self: tgt_alias = "@%s\u2005 " % msg.target.author.display_name else: tgt_alias = "" msg.text = "%s%s\n\n%s" % (tgt_alias, tgt_text, msg.text) r = self._bot_send_msg(chat, msg.text) self.logger.debug('[%s] Sent as a text message. %s', msg.uid, msg.text) elif msg.type in (MsgType.Image, MsgType.Sticker, MsgType.Animation): self.logger.info("[%s] Image/GIF/Sticker %s", msg.uid, msg.type) convert_to = None file = msg.file assert file is not None if self.flag('send_stickers_and_gif_as_jpeg'): if msg.type == MsgType.Sticker or msg.mime == "image/gif": convert_to = "image/jpeg" else: if msg.type == MsgType.Sticker: convert_to = "image/gif" if convert_to == "image/gif": with NamedTemporaryFile(suffix=".gif") as f: try: img = Image.open(file) try: alpha = img.split()[3] mask = Image.eval(alpha, lambda a: 255 if a <= 128 else 0) except IndexError: mask = Image.eval(img.split()[0], lambda a: 0) img = img.convert('RGB').convert('P', palette=Image.ADAPTIVE, colors=255) img.paste(255, mask) img.save(f, transparency=255) msg.path = f.name self.logger.debug('[%s] Image converted from %s to GIF', msg.uid, msg.mime) file.close() f.seek(0) if os.fstat(f.fileno()).st_size > self.MAX_FILE_SIZE: raise EFBMessageError(self._("Image size is too large. (IS02)")) r = self._bot_send_image(chat, f.name, f) finally: if not file.closed: file.close() elif convert_to == "image/jpeg": with NamedTemporaryFile(suffix=".jpg") as f: try: img = Image.open(file).convert('RGBA') out = Image.new("RGBA", img.size, (255,255,255,255)) out.paste(img, img) out.convert('RGB').save(f) msg.path = f.name self.logger.debug('[%s] Image converted from %s to JPEG', msg.uid, msg.mime) file.close() f.seek(0) if os.fstat(f.fileno()).st_size > self.MAX_FILE_SIZE: raise EFBMessageError(self._("Image size is too large. (IS02)")) r = self._bot_send_image(chat, f.name, f) finally: if not file.closed: file.close() else: try: if os.fstat(file.fileno()).st_size > self.MAX_FILE_SIZE: raise EFBMessageError(self._("Image size is too large. (IS01)")) self.logger.debug("[%s] Sending %s (image) to WeChat.", msg.uid, msg.path) r = self._bot_send_image(chat, msg.path, file) finally: if not file.closed: file.close() if msg.text: self._bot_send_msg(chat, msg.text) elif msg.type in (MsgType.File, MsgType.Audio): self.logger.info("[%s] Sending %s to WeChat\nFileName: %s\nPath: %s\nFilename: %s", msg.uid, msg.type, msg.text, msg.path, msg.filename) r = self._bot_send_file(chat, msg.filename, file=msg.file) if msg.text: self._bot_send_msg(chat, msg.text) msg.file.close() elif msg.type == MsgType.Video: self.logger.info("[%s] Sending video to WeChat\nFileName: %s\nPath: %s", msg.uid, msg.text, msg.path) r = self._bot_send_video(chat, msg.path, file=msg.file) if msg.text: self._bot_send_msg(chat, msg.text) msg.file.close() else: raise EFBMessageTypeNotSupported() msg.uid = ews_utils.generate_message_uid(r) self.logger.debug('WeChat message is assigned with unique ID: %s', msg.uid) return msg
def send_message(self, msg: Message) -> Message: """Send a message to WeChat. Supports text, image, sticker, and file. Args: msg (channel.Message): Message Object to be sent. Returns: This method returns nothing. Raises: EFBChatNotFound: Raised when a chat required is not found. EFBMessageTypeNotSupported: Raised when the message type sent is not supported by the channel. EFBOperationNotSupported: Raised when an message edit request is sent, but not supported by the channel. EFBMessageNotFound: Raised when an existing message indicated is not found. E.g.: The message to be edited, the message referred in the :attr:`msg.target <.Message.target>` attribute. EFBMessageError: Raised when other error occurred while sending or editing the message. """ if msg.chat == self.user_auth_chat: raise EFBChatNotFound chat: wxpy.Chat = self.chats.get_wxpy_chat_by_uid(msg.chat.uid) # List of "SentMessage" response for all messages sent r: List[wxpy.SentMessage] = [] self.logger.info("[%s] Sending message to WeChat:\n" "uid: %s\n" "UserName: %s\n" "NickName: %s\n" "Type: %s\n" "Text: %s", msg.uid, msg.chat.uid, chat.user_name, chat.name, msg.type, msg.text) try: chat.mark_as_read() except wxpy.ResponseError as e: self.logger.exception( "[%s] Error occurred while marking chat as read. (%s)", msg.uid, e) send_text_only = False self.logger.debug('[%s] Is edited: %s', msg.uid, msg.edit) if msg.edit and msg.uid: if self.flag('delete_on_edit'): msg_ids = json.loads(msg.uid) if msg.type in self.MEDIA_MSG_TYPES and not msg.edit_media: # Treat message as text message to prevent resend of media msg_ids = msg_ids[1:] send_text_only = True failed = 0 for i in msg_ids: try: ews_utils.message_id_to_dummy_message(i, self).recall() except wxpy.ResponseError as e: self.logger.error( "[%s] Trying to recall message but failed: %s", msg.uid, e) failed += 1 if failed: raise EFBMessageError( self.ngettext('Failed to recall {failed} out of {total} message, edited message was not sent.', 'Failed to recall {failed} out of {total} messages, edited message was not sent.', len(msg_ids)).format( failed=failed, total=len(msg_ids) )) # Not caching message ID as message recall feedback is not needed in edit mode else: raise EFBOperationNotSupported() if send_text_only or msg.type in [MsgType.Text, MsgType.Link]: if isinstance(msg.target, Message): max_length = self.flag("max_quote_length") qt_txt = msg.target.text or msg.target.type.name if max_length > 0: if len(qt_txt) >= max_length: tgt_text = qt_txt[:max_length] tgt_text += "…" else: tgt_text = qt_txt elif max_length < 0: tgt_text = qt_txt else: tgt_text = "" if isinstance(chat, wxpy.Group) and not isinstance(msg.target.author, SelfChatMember): tgt_alias = "@%s\u2005:" % msg.target.author.display_name else: tgt_alias = "" msg.text = f"「{tgt_alias}{tgt_text}」\n- - - - - - - - - - - - - - -\n{msg.text}" r.append(self._bot_send_msg(chat, msg.text)) self.logger.debug( '[%s] Sent as a text message. %s', msg.uid, msg.text) elif msg.type in (MsgType.Image, MsgType.Sticker, MsgType.Animation): self.logger.info("[%s] Image/GIF/Sticker %s", msg.uid, msg.type) convert_to = None file = msg.file assert file is not None if self.flag('send_stickers_and_gif_as_jpeg'): if msg.type == MsgType.Sticker or msg.mime == "image/gif": convert_to = "image/jpeg" else: if msg.type == MsgType.Sticker: convert_to = "image/gif" if convert_to == "image/gif": with NamedTemporaryFile(suffix=".gif") as f: try: img = Image.open(file) try: alpha = img.split()[3] mask = Image.eval( alpha, lambda a: 255 if a <= 128 else 0) except IndexError: mask = Image.eval(img.split()[0], lambda a: 0) img = img.convert('RGB').convert( 'P', palette=Image.ADAPTIVE, colors=255) img.paste(255, mask) img.save(f, transparency=255) msg.path = Path(f.name) self.logger.debug( '[%s] Image converted from %s to GIF', msg.uid, msg.mime) file.close() if f.seek(0, 2) > self.MAX_FILE_SIZE: raise EFBMessageError( self._("Image size is too large. (IS02)")) f.seek(0) r.append(self._bot_send_image(chat, f.name, f)) finally: if not file.closed: file.close() elif convert_to == "image/jpeg": with NamedTemporaryFile(suffix=".jpg") as f: try: img = Image.open(file).convert('RGBA') out = Image.new("RGBA", img.size, (255, 255, 255, 255)) out.paste(img, img) out.convert('RGB').save(f) msg.path = Path(f.name) self.logger.debug( '[%s] Image converted from %s to JPEG', msg.uid, msg.mime) file.close() if f.seek(0, 2) > self.MAX_FILE_SIZE: raise EFBMessageError( self._("Image size is too large. (IS02)")) f.seek(0) r.append(self._bot_send_image(chat, f.name, f)) finally: if not file.closed: file.close() else: try: if file.seek(0, 2) > self.MAX_FILE_SIZE: raise EFBMessageError( self._("Image size is too large. (IS01)")) file.seek(0) self.logger.debug( "[%s] Sending %s (image) to WeChat.", msg.uid, msg.path) filename = msg.filename or (msg.path and msg.path.name) assert filename r.append(self._bot_send_image(chat, filename, file)) finally: if not file.closed: file.close() if msg.text: r.append(self._bot_send_msg(chat, msg.text)) elif msg.type in (MsgType.File, MsgType.Audio): self.logger.info("[%s] Sending %s to WeChat\nFileName: %s\nPath: %s\nFilename: %s", msg.uid, msg.type, msg.text, msg.path, msg.filename) filename = msg.filename or (msg.path and msg.path.name) assert filename and msg.file r.append(self._bot_send_file(chat, filename, file=msg.file)) if msg.text: self._bot_send_msg(chat, msg.text) if not msg.file.closed: msg.file.close() elif msg.type == MsgType.Video: self.logger.info( "[%s] Sending video to WeChat\nFileName: %s\nPath: %s", msg.uid, msg.text, msg.path) filename = msg.filename or (msg.path and msg.path.name) assert filename and msg.file r.append(self._bot_send_video(chat, filename, file=msg.file)) if msg.text: r.append(self._bot_send_msg(chat, msg.text)) if not msg.file.closed: msg.file.close() else: raise EFBMessageTypeNotSupported() msg.uid = ews_utils.generate_message_uid(r) self.logger.debug( 'WeChat message is assigned with unique ID: %s', msg.uid) return msg