Exemple #1
0
    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()
Exemple #2
0
    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)
Exemple #4
0
    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()
Exemple #5
0
    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