Пример #1
0
 def send_member_update_status(
         self) -> Tuple[ChatMember, ChatMember, ChatMember]:
     """
     Returns:
         member added, member edited, member removed
     """
     keyword = " (Edited)"
     if self.backup_member not in self.group.members:
         to_add, to_remove = self.backup_member, self.member_to_toggle
         self.member_to_edit.name += keyword
     else:
         to_add, to_remove = self.member_to_toggle, self.backup_member
         self.member_to_edit.name = self.member_to_edit.name.replace(
             keyword, '')
     self.group.members.append(to_add)
     self.group.members.remove(to_remove)
     coordinator.send_status(
         MemberUpdates(
             self,
             self.group.uid,
             new_members=[to_add.uid],
             modified_members=[self.member_to_edit.uid],
             removed_members=[to_remove.uid],
         ))
     return to_add, self.member_to_edit, to_remove
Пример #2
0
    def react_to_message_status(self, status: ReactToMessage):
        if self.accept_message_reactions == "reject_one":
            raise EFBMessageReactionNotPossible(
                "Message reaction is rejected by flag.")
        if self.accept_message_reactions == "reject_all":
            raise EFBOperationNotSupported(
                "All message reactions are rejected by flag.")
        message = self.messages_sent.get(status.msg_id)
        if message is None:
            raise EFBOperationNotSupported("Message is not found.")

        if status.reaction is None:
            for idx, i in message.reactions.items():
                message.reactions[idx] = [
                    j for j in i if not isinstance(j, SelfChatMember)
                ]
        else:
            if status.reaction not in message.reactions:
                message.reactions[status.reaction] = []
            message.reactions[status.reaction].append(message.chat.self)

        coordinator.send_status(
            MessageReactionsUpdate(chat=message.chat,
                                   msg_id=message.uid,
                                   reactions=message.reactions))
Пример #3
0
 def send_reactions_update(self,
                           message: Message) -> MessageReactionsUpdate:
     reactions = self.build_reactions(message.chat)
     message.reactions = reactions
     status = MessageReactionsUpdate(chat=message.chat,
                                     msg_id=message.uid,
                                     reactions=reactions)
     coordinator.send_status(status)
     return status
 def onUnknownMesssageType(self, msg=None):
     if msg.get("class") == 'ForcedFetch':
         # New chat appears.
         # {'forceInsert': False, 'irisSeqId': '538', 'isLazy': False, 'threadKey': {'threadFbId': '1234567890'}, 'class': 'ForcedFetch'}
         coordinator.send_status(ChatUpdates(
             channel=self.channel,
             new_chats=[msg['threadKey']['threadFbId']]
         ))
     super().onUnknownMesssageType(msg)
Пример #5
0
 def wechat_system_msg(self, msg: wxpy.Message) -> Optional[EFBMsg]:
     if msg.recalled_message_id:
         efb_msg = EFBMsg()
         efb_msg.chat = self.channel.chats.wxpy_chat_to_efb_chat(msg.chat)
         efb_msg.author = self.channel.chats.wxpy_chat_to_efb_chat(msg.sender)
         efb_msg.uid = str(msg.recalled_message_id)
         coordinator.send_status(EFBMessageRemoval(source_channel=self.channel,
                                                   destination_channel=coordinator.master,
                                                   message=efb_msg))
         return None
     efb_msg = EFBMsg()
     efb_msg.text = msg.text
     efb_msg.type = MsgType.Text
     efb_msg.author = EFBChat(self.channel).system()
     return efb_msg
 def onMessageUnsent(self, mid=None, author_id=None, thread_id=None, thread_type=None, ts=None, msg=None):
     chat = self.chat_manager.get_thread(thread_id)
     author = chat.get_member(author_id)
     if mid in self.message_mappings:
         for i in range(self.message_mappings[mid]):
             coordinator.send_status(
                 MessageRemoval(source_channel=self.channel,
                                destination_channel=coordinator.master,
                                message=EFBMessage(chat=chat, author=author, uid=f"{mid}.{i}"))
             )
     else:
         coordinator.send_status(
             MessageRemoval(source_channel=self.channel,
                            destination_channel=coordinator.master,
                            message=EFBMessage(chat=chat, author=author, uid=mid))
         )
    def on_message_reaction(self, thread_id, message_id):
        thread_id, thread_type = self._getThread(thread_id, None)
        msg_data = self._forcedFetch(thread_id, message_id).get("message")
        msg: Message = Message._from_graphql(msg_data)

        chat = self.chat_manager.get_thread(thread_id)

        reactions = {}
        if msg.reactions:
            reactions = defaultdict(list)
            for user_id, reaction in msg.reactions.items():
                reactions[reaction.value].append(chat.get_member(user_id))

        update = MessageReactionsUpdate(chat=chat, msg_id=message_id, reactions=reactions)

        coordinator.send_status(update)
Пример #8
0
 def send_message_recall_status(self):
     slave = next(iter(coordinator.slaves.values()))
     alice = slave.get_chat('alice')
     msg = EFBMsg()
     msg.deliver_to = slave
     msg.chat = alice
     msg.author = EFBChat(self).self()
     msg.uid = "1"
     status = EFBMessageRemoval(self, slave, msg)
     return coordinator.send_status(status)
Пример #9
0
 def send_message_recall_status(self):
     slave = coordinator.slaves['tests.mocks.slave']
     alice = slave.get_chat('alice')
     msg = EFBMsg()
     msg.deliver_to = slave
     msg.chat = alice
     msg.author = EFBChat(self).self()
     msg.uid = "1"
     status = EFBMessageRemoval(self, slave, msg)
     return coordinator.send_status(status)
Пример #10
0
 def send_message_recall_status(self):
     slave = next(iter(coordinator.slaves.values()))
     alice = slave.get_chat('alice')
     msg = Message(
         deliver_to=slave,
         chat=alice,
         author=alice.self,
         uid="1"
     )
     status = MessageRemoval(self, slave, msg)
     return coordinator.send_status(status)
Пример #11
0
 def send_chat_update_status(self) -> Tuple[Chat, Chat, Chat]:
     """
     Returns:
         chat added, chat edited, chat removed
     """
     keyword = " (Edited)"
     if self.backup_chat not in self.chats:
         to_add, to_remove = self.backup_chat, self.chat_to_toggle
         self.chat_to_edit.name += keyword
     else:
         to_add, to_remove = self.chat_to_toggle, self.backup_chat
         self.chat_to_edit.name = self.chat_to_edit.name.replace(
             keyword, '')
     self.chats.append(to_add)
     self.chats.remove(to_remove)
     coordinator.send_status(
         ChatUpdates(
             self,
             new_chats=[to_add.uid],
             modified_chats=[self.chat_to_edit.uid],
             removed_chats=[to_remove.uid],
         ))
     return to_add, self.chat_to_edit, to_remove
Пример #12
0
 def wechat_system_msg(self, msg: wxpy.Message) -> Optional[Message]:
     if msg.recalled_message_id:
         recall_id = str(msg.recalled_message_id)
         # check conversion table first
         if recall_id in self.recall_msg_id_conversion:
             # prevent feedback of messages deleted by master channel.
             del self.recall_msg_id_conversion[recall_id]
             return None
             # val = self.recall_msg_id_conversion.pop(recall_id)
             # val[1] -= 1
             # if val[1] > 0:  # not all associated messages are recalled.
             #     return None
             # else:
             #     efb_msg.uid = val[0]
         else:
             # Format message IDs as JSON of List[List[str]].
             chat, author = self.get_chat_and_author(msg)
             efb_msg = Message(
                 chat=chat, author=author,
                 uid=MessageID(json.dumps([[recall_id]]))
             )
         coordinator.send_status(MessageRemoval(source_channel=self.channel,
                                                destination_channel=coordinator.master,
                                                message=efb_msg))
         return None
     chat, _ = self.get_chat_and_author(msg)
     try:
         author = chat.get_member(SystemChatMember.SYSTEM_ID)
     except KeyError:
         author = chat.add_system_member()
     if any(i in msg.text for i in self.NEW_CHAT_PATTERNS):
         coordinator.send_status(ChatUpdates(
             channel=self.channel,
             new_chats=(chat.uid,)
         ))
     elif any(i in msg.text for i in self.CHAT_AND_MEMBER_UPDATE_PATTERNS):
         # TODO: detect actual member changes from message text
         coordinator.send_status(ChatUpdates(
             channel=self.channel,
             modified_chats=(chat.uid,)
         ))
     return Message(
         text=msg.text,
         type=MsgType.Text,
         chat=chat,
         author=author,
     )
Пример #13
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()
Пример #14
0
 def delete_message(self, update: Update, context: CallbackContext):
     """Remove an arbitrary message from its remote chat.
     Triggered by command ``/rm``.
     """
     message: Message = update.message
     if message.reply_to_message is None:
         return self.bot.reply_error(
             update,
             self.
             _("Reply /rm to a message to remove it from its remote chat."))
     reply: Message = message.reply_to_message
     msg_log = self.db.get_msg_log(master_msg_id=utils.message_id_to_str(
         chat_id=reply.chat_id, message_id=reply.message_id))
     if not msg_log or msg_log.slave_member_uid == self.db.FAIL_FLAG:
         return self.bot.reply_error(
             update,
             self.
             _("This message is not found in ETM database. You cannot remove it from its remote chat."
               ))
     try:
         etm_msg: ETMMsg = msg_log.build_etm_msg(self.chat_manager)
     except UnpicklingError:
         return self.bot.reply_error(
             update,
             self.
             _("This message is not found in ETM database. You cannot remove it from its remote chat."
               ))
     dest_channel = coordinator.slaves.get(etm_msg.chat.module_id, None)
     if dest_channel is None:
         return self.bot.reply_error(
             update,
             self.
             _("Module of this message ({module_id}) could not be found, or is not a slave channel."
               ).format(module_id=etm_msg.chat.module_id))
     # noinspection PyBroadException
     try:
         coordinator.send_status(
             MessageRemoval(source_channel=self.channel,
                            destination_channel=dest_channel,
                            message=etm_msg))
     except EFBException as e:
         self.logger.exception(
             "Failed to remove message from remote chat. Message: %s; Error: %s",
             etm_msg, e)
         return reply.reply_text(
             self.
             _("Failed to remove this message from remote chat.\n\n{error!s}"
               ).format(error=e))
     except Exception as e:
         self.logger.exception(
             "Failed to remove message from remote chat. Message: %s; Error: %s",
             etm_msg, e)
         return reply.reply_text(
             self.
             _("Failed to remove this message from remote chat.\n\n{error!r}"
               ).format(error=e))
     if not self.channel.flag('prevent_message_removal'):
         try:
             reply.delete()
         except telegram.TelegramError:
             reply.reply_text(self._("Message is removed in remote chat."))
     else:
         reply.reply_text(self._("Message is removed in remote chat."))
Пример #15
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()
Пример #16
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()
Пример #17
0
    def react(self, update: Update, context: CallbackContext):
        """React to a message."""
        message: Message = update.effective_message

        reaction = None
        args = message.text and message.text.split(' ', 1)
        if args and len(args) > 1:
            reaction = args[1]

        if not message.reply_to_message:
            message.reply_html(
                self._("Reply to a message with this command and an emoji "
                       "to send a reaction. "
                       "Ex.: <code>/react 👍</code>.\n"
                       "Send <code>/react -</code> to remove your reaction "
                       "from a message."))
            return

        target: Message = update.message.reply_to_message
        msg_log = self.db.get_msg_log(
            master_msg_id=etm_utils.message_id_to_str(
                chat_id=target.chat_id, message_id=target.message_id))
        if msg_log is None:
            message.reply_text(
                self.
                _("The message you replied to is not recorded in ETM database. "
                  "You cannot react to this message."))
            return

        if not reaction:
            msg_log_obj: ETMMsg = msg_log.build_etm_msg(self.chat_manager)
            reactors = msg_log_obj.reactions
            if not reactors:
                message.reply_html(
                    self._("This message has no reactions yet. "
                           "Reply to a message with this command and "
                           "an emoji to send a reaction. "
                           "Ex.: <code>/react 👍</code>."))
                return
            else:
                text = ""
                for key, values in reactors.items():
                    if not values:
                        continue
                    text += f"{key}:\n"
                    for j in values:
                        text += f"    {j.display_name}\n"
                text = text.strip()
                message.reply_text(text)
                return

        message_id = msg_log.slave_message_id
        channel_id, chat_uid, _ = etm_utils.chat_id_str_to_id(
            msg_log.slave_origin_uid)

        if channel_id not in coordinator.slaves:
            message.reply_text(
                self.
                _("The slave channel involved in this message ({}) is not available. "
                  "You cannot react to this message.").format(channel_id))
            return

        channel = coordinator.slaves[channel_id]

        if channel.suggested_reactions is None:
            message.reply_text(
                self.
                _("The channel involved in this message ({}) does not accept reactions. "
                  "You cannot react to this message.").format(channel_id))
            return

        try:
            chat_obj = channel.get_chat(chat_uid)
        except EFBChatNotFound:
            message.reply_text(
                self._("The chat involved in this message ({}) is not found. "
                       "You cannot react to this message.").format(chat_uid))
            return

        if reaction == "-":
            reaction = None

        try:
            coordinator.send_status(
                ReactToMessage(chat=chat_obj,
                               msg_id=message_id,
                               reaction=reaction))
        except EFBOperationNotSupported:
            message.reply_text(
                self._("You cannot react anything to this message."))
            return
        except EFBMessageReactionNotPossible:
            prompt = self._("{} is not accepted as a reaction to this message."
                            ).format(reaction)
            if channel.suggested_reactions:
                # TRANSLATORS: {} is a list of names of possible reactions, separated with comma.
                prompt += "\n" + self._("You may want to try: {}").format(
                    ", ".join(channel.suggested_reactions[:10]))
            message.reply_text(prompt)
            return