예제 #1
0
    def __init__(self, channel: 'WeChatChannel'):
        self.channel: 'WeChatChannel' = channel
        self.logger: logging.Logger = logging.getLogger(__name__)

        # noinspection PyProtectedMember
        self._ = self.channel._

        self.MISSING_GROUP: GroupChat = GroupChat(
            channel=self.channel,
            uid=ChatID("__error_group__"),
            name=self._("Group Missing")
        )

        self.MISSING_CHAT: PrivateChat = PrivateChat(
            channel=self.channel,
            uid=ChatID("__error_chat__"),
            name=self._("Chat Missing")
        )

        self.efb_chat_objs: Dict[str, Chat] = {}
        # Cached Chat objects. Key: tuple(chat PUID, group PUID or None)

        # Load system chats
        self.system_chats: List[Chat] = []
        for i in channel.flag('system_chats_to_include'):
            self.system_chats.append(
                self.wxpy_chat_to_efb_chat(
                    wxpy.Chat(
                        wxpy.utils.wrap_user_name(i),
                        self.bot
                    )
                )
            )
예제 #2
0
 def chat_migration(self, update: Update, context: CallbackContext):
     message = update.effective_message
     from_id = ChatID(message.migrate_from_chat_id)
     to_id = ChatID(message.migrate_to_chat_id)
     from_str = utils.chat_id_to_str(self.channel.channel_id, from_id)
     to_str = utils.chat_id_to_str(self.channel.channel_id, to_id)
     for i in self.db.get_chat_assoc(master_uid=from_str):
         self.db.add_chat_assoc(master_uid=from_str, slave_uid=to_str)
     self.db.remove_chat_assoc(master_uid=from_str)
예제 #3
0
    def __init__(self, channel: 'QQMessengerChannel'):
        self.channel: 'QQMessengerChannel' = channel
        self.logger: logging.Logger = logging.getLogger(__name__)

        self.MISSING_GROUP: GroupChat = GroupChat(
            channel=self.channel,
            uid=ChatID("__error_group__"),
            name="Group Missing")

        self.MISSING_CHAT: PrivateChat = PrivateChat(
            channel=self.channel,
            uid=ChatID("__error_chat__"),
            name="Chat Missing")
예제 #4
0
def chat_id_str_to_id(s: EFBChannelChatIDStr) -> Tuple[ModuleID, ChatID, Optional[ChatID]]:
    """
    Reverse of chat_id_to_str.
    Returns:
        channel_id, chat_uid, group_id
    """
    ids = s.split(" ", 2)
    channel_id = ModuleID(ids[0])
    chat_uid = ChatID(ids[1])
    if len(ids) < 3:
        group_id = None
    else:
        group_id = ChatID(ids[2])
    return channel_id, chat_uid, group_id
 def chat_migration(self, update: Update, context: CallbackContext):
     """Triggered by any message update with either
     ``migrate_from_chat_id`` or ``migrate_to_chat_id``
     or both (which shouldn’t happen).
     """
     message = update.effective_message
     if message.migrate_from_chat_id is not None:
         from_id = ChatID(message.migrate_from_chat_id)
         to_id = ChatID(message.chat.id)
     elif message.migrate_to_chat_id is not None:
         from_id = ChatID(message.chat.id)
         to_id = ChatID(message.migrate_to_chat_id)
     else:
         # Per ptb filter specs, this part of code should not be reached.
         return
     self.chat_migration_by_id(from_id, to_id)
예제 #6
0
 def make_system_member(self, name: str = "", alias: Optional[str] = None, id: ChatID = ChatID(""),
                        uid: ChatID = ChatID(""), vendor_specific: Dict[str, Any] = None, description: str = "",
                        middleware: Optional[Middleware] = None) -> ETMSystemChatMember:
     # TODO: remove deprecated ID
     assert not id, f"id is {id!r}"
     return ETMSystemChatMember(self.db, self, name=name, alias=alias, uid=uid,
                                vendor_specific=vendor_specific, description=description, middleware=middleware)
예제 #7
0
 def __init__(
         self,
         db: 'DatabaseManager',
         *,
         channel: Optional[SlaveChannel] = None,
         middleware: Optional[Middleware] = None,
         module_name: str = "",
         channel_emoji: str = "",
         module_id: ModuleID = ModuleID(""),
         name: str = "",
         alias: Optional[str] = None,
         uid: ChatID = ChatID(""),
         vendor_specific: Dict[str, Any] = None,
         description: str = "",
         notification: ChatNotificationState = ChatNotificationState.ALL,
         with_self: bool = True):
     super().__init__(db,
                      channel=channel,
                      middleware=middleware,
                      module_name=module_name,
                      channel_emoji=channel_emoji,
                      module_id=module_id,
                      name=name,
                      alias=alias,
                      uid=uid,
                      vendor_specific=vendor_specific,
                      description=description,
                      notification=notification,
                      with_self=with_self)
예제 #8
0
 def chat_migration(self, update: Update, context: CallbackContext):
     message = update.effective_message
     if message.migrate_from_chat_id is not None:
         from_id = ChatID(message.migrate_from_chat_id)
         to_id = ChatID(message.chat.id)
     elif message.migrate_to_chat_id is not None:
         from_id = ChatID(message.chat.id)
         to_id = ChatID(message.migrate_to_chat_id)
     else:
         # Per ptb filter specs, this part of code should not be reached.
         return
     from_str = utils.chat_id_to_str(self.channel.channel_id, from_id)
     to_str = utils.chat_id_to_str(self.channel.channel_id, to_id)
     for i in self.db.get_chat_assoc(master_uid=from_str):
         self.db.add_chat_assoc(master_uid=from_str, slave_uid=to_str)
     self.db.remove_chat_assoc(master_uid=from_str)
예제 #9
0
 def get_chat(self, chat_uid: ChatID) -> 'Chat':
     chat_info = chat_uid.split('_')
     chat_type = chat_info[0]
     chat_attr = chat_info[1]
     chat = None
     if chat_type == 'friend':
         chat_uin = int(chat_attr)
         remark_name = self.get_friend_remark(chat_uin)
         chat = ChatMgr.build_efb_chat_as_private(
             EFBPrivateChat(
                 uid=chat_attr,
                 name=remark_name if remark_name else "",
             ))
     elif chat_type == 'group':
         chat_uin = int(chat_attr)
         group_info = self.get_group_info(chat_uin, no_cache=False)
         group_members = self.get_group_member_list(chat_uin,
                                                    no_cache=False)
         chat = ChatMgr.build_efb_chat_as_group(
             EFBGroupChat(uid=f"group_{chat_uin}",
                          name=group_info.get('GroupName', "")),
             group_members)
     elif chat_type == 'private':
         pass  # fixme
     elif chat_type == 'phone':
         pass  # fixme
     return chat
예제 #10
0
    def __init__(self, instance_id: InstanceID = None):
        """
        Initialize the channel

        Args:
            coordinator (:obj:`ehforwarderbot.coordinator.EFBCoordinator`):
                The EFB framework coordinator
        """
        super().__init__(instance_id)
        self.load_config()

        PuidMap.SYSTEM_ACCOUNTS = self.SYSTEM_ACCOUNTS

        self.flag: ExperimentalFlagsManager = ExperimentalFlagsManager(self)

        self.qr_uuid: Tuple[str, int] = ('', 0)
        self.master_qr_picture_id: Optional[str] = None

        self.authenticate('console_qr_code', first_start=True)

        # Managers
        self.slave_message: SlaveMessageManager = SlaveMessageManager(self)
        self.chats: ChatManager = ChatManager(self)
        self.user_auth_chat = SystemChat(channel=self,
                                         name=self._("EWS User Auth"),
                                         uid=ChatID("__ews_user_auth__"))
예제 #11
0
 def get_chat(self, chat_uid: ChatID) -> 'Chat':
     chat_info = chat_uid.split('_')
     chat_type = chat_info[0]
     chat_attr = chat_info[1]
     chat = None
     if chat_type == 'friend':
         chat_uin = int(chat_attr)
         if not self.info_list.get('friend', None) and chat_uin in self.info_list['friend']:
             chat = ChatMgr.build_efb_chat_as_private(EFBPrivateChat(
                 uid=f"friend_{chat_attr}",
                 name=self.info_list['friend'][chat_uin].remark,
                 alias=self.info_list['friend'][chat_uin].nickname
             ))
         else:
             remark_name = self.get_friend_remark(chat_uin)
             chat = ChatMgr.build_efb_chat_as_private(EFBPrivateChat(
                 uid=f"friend_{chat_attr}",
                 name=remark_name if remark_name else "",
             ))
     elif chat_type == 'group':
         chat_uin = int(chat_attr)
         group_info = self.get_group_info(chat_uin, no_cache=False)
         group_members = self.get_group_member_list(chat_uin, no_cache=False)
         chat = ChatMgr.build_efb_chat_as_group(EFBGroupChat(
             uid=f"group_{chat_uin}",
             name=group_info.get('name', "")
         ), group_members)
     elif chat_type == 'private':
         pass  # fixme
     elif chat_type == 'phone':
         pass  # fixme
     return chat
예제 #12
0
def chat_id_str_to_id(s: EFBChannelChatIDStr) -> Tuple[ModuleID, ChatID]:
    """
    Reverse of chat_id_to_str.
    Returns:
        channel_id, chat_uid
    """
    chat_ids = s.split(" ", 1)
    return ModuleID(chat_ids[0]), ChatID(chat_ids[1])
예제 #13
0
    def make_status_message(self,
                            msg_base: Message = None,
                            mid: MessageID = None) -> Message:
        if mid is not None:
            msg = self.message_cache[mid]
        elif msg_base is not None:
            msg = msg_base
        else:
            raise ValueError

        reply = Message(
            type=MsgType.Text,
            chat=msg.chat,
            author=msg.chat.make_system_member(uid=ChatID("filter_info"),
                                               name="Filter middleware",
                                               middleware=self),
            deliver_to=coordinator.master,
        )

        if mid:
            reply.uid = mid
        else:
            reply.uid = str(uuid.uuid4())

        status = self.filter_reason(msg)
        if not status:
            # Blue circle emoji
            status = "\U0001F535 This chat is not filtered."
        else:
            # Red circle emoji
            status = "\U0001F534 " + status

        reply.text = "Filter status for chat {chat_id} from {module_id}:\n" \
                     "\n" \
                     "{status}\n".format(
            module_id=msg.chat.module_id,
            chat_id=msg.chat.id,
            status=status
        )

        command = MessageCommand(name="%COMMAND_NAME%",
                                 callable_name="toggle_filter_by_chat_id",
                                 kwargs={
                                     "mid": reply.uid,
                                     "module_id": msg.chat.module_id,
                                     "chat_id": msg.chat.id
                                 })

        if self.is_chat_filtered_by_id(msg.chat):
            command.name = "Unfilter by chat ID"
            command.kwargs['value'] = False
        else:
            command.name = "Filter by chat ID"
            command.kwargs['value'] = True
        reply.commands = MessageCommands([command])

        return reply
예제 #14
0
 def fill_group(self, group: Chat):
     """Populate members into a group per membership template."""
     for name, notification, avatar, alias in self.__group_member_templates:
         group.add_member(
             name=name,
             alias=f"{alias} @ {group.name[::-1]}"
             if alias is not None else None,
             uid=ChatID(self.CHAT_ID_FORMAT.format(hash=hash(name))),
         )
 def get_singly_linked_chat_id_str(
         self, chat: Chat) -> Optional[EFBChannelChatIDStr]:
     """Return the singly-linked remote chat if available.
     Otherwise return None.
     """
     master_chat_uid = utils.chat_id_to_str(self.channel_id,
                                            ChatID(str(chat.id)))
     chats = self.db.get_chat_assoc(master_uid=master_chat_uid)
     if len(chats) == 1:
         return chats[0]
     return None
예제 #16
0
 def __init__(self,
              db: 'DatabaseManager',
              chat: 'Chat',
              *,
              name: str = "",
              alias: Optional[str] = None,
              uid: ChatID = ChatID(""),
              vendor_specific: Dict[str, Any] = None,
              description: str = "",
              middleware: Optional[Middleware] = None):
     super().__init__(db,
                      chat,
                      name=name,
                      alias=alias,
                      uid=uid,
                      vendor_specific=vendor_specific,
                      description=description,
                      middleware=middleware)
예제 #17
0
 def add_system_member(
         self,
         name: str = "",
         alias: Optional[str] = None,
         uid: ChatID = ChatID(""),  # type: ignore
         vendor_specific: Dict[str, Any] = None,
         description: str = "",
         id='',
         middleware: Optional[Middleware] = None) -> ETMSystemChatMember:
     # TODO: remove deprecated ID
     assert not id, f"id is {id!r}"
     member = self.make_system_member(name=name,
                                      alias=alias,
                                      uid=uid,
                                      vendor_specific=vendor_specific,
                                      description=description,
                                      middleware=middleware)
     self.members.append(member)
     return member
    def build_efb_msg(self, mid: str, thread_id: str, author_id: str, message_object: Message,
                      nested: bool = False) -> EFBMessage:
        efb_msg = EFBMessage(
            uid=MessageID(mid),
            text=message_object.text,
            type=MsgType.Text,
            deliver_to=coordinator.master,
        )

        # Authors
        efb_msg.chat = self.chat_manager.get_thread(thread_id)
        efb_msg.author = efb_msg.chat.get_member(ChatID(author_id))

        if not nested and message_object.replied_to:
            efb_msg.target = self.build_efb_msg(message_object.reply_to_id,
                                                thread_id=thread_id,
                                                author_id=message_object.author,
                                                message_object=message_object.replied_to,
                                                nested=True)

        if message_object.mentions:
            mentions: Dict[Tuple[int, int], ChatMember] = dict()
            for i in message_object.mentions:
                mentions[(i.offset, i.offset + i.length)] = efb_msg.chat.get_member(i.thread_id)
            efb_msg.substitutions = Substitutions(mentions)

        if message_object.emoji_size:
            efb_msg.text += " (%s)" % message_object.emoji_size.name[0]
            # " (S)", " (M)", " (L)"

        if message_object.reactions:
            reactions: DefaultDict[ReactionName, List[ChatMember]] = defaultdict(list)
            for user_id, reaction in message_object.reactions.items():
                reactions[ReactionName(reaction.value)].append(
                    efb_msg.chat.get_member(user_id))
            efb_msg.reactions = reactions
        return efb_msg
예제 #19
0
    def generate_chats(self):
        """Generate a list of chats per the chat templates, and categorise
        them accordingly.
        """
        self.chats: List[Chat] = []

        self.chats_by_chat_type: Dict[ChatTypeName, List[Chat]] = {
            'PrivateChat': [],
            'GroupChat': [],
            'SystemChat': [],
        }
        self.chats_by_notification_state: Dict[
            ChatNotificationState, List[Chat]] = {
                ChatNotificationState.ALL: [],
                ChatNotificationState.MENTIONS: [],
                ChatNotificationState.NONE: [],
            }
        self.chats_by_profile_picture: Dict[bool, List[Chat]] = {
            True: [],
            False: []
        }
        self.chats_by_alias: Dict[bool, List[Chat]] = {True: [], False: []}

        for name, chat_type, notification, avatar, alias in self.__chat_templates:
            chat = chat_type(channel=self,
                             name=name,
                             alias=alias,
                             uid=ChatID(
                                 self.CHAT_ID_FORMAT.format(hash=hash(name))),
                             notification=notification)
            self.__picture_dict[chat.uid] = avatar

            if chat_type == GroupChat:
                self.fill_group(chat)

            self.chats_by_chat_type[chat_type.__name__].append(chat)
            self.chats_by_notification_state[notification].append(chat)
            self.chats_by_profile_picture[avatar is not None].append(chat)
            self.chats_by_alias[alias is not None].append(chat)
            self.chats.append(chat)

        name = "Unknown Chat"
        self.unknown_chat: PrivateChat = PrivateChat(
            channel=self,
            name=name,
            alias="不知道",
            uid=ChatID(self.CHAT_ID_FORMAT.format(hash=hash(name))),
            notification=ChatNotificationState.ALL)

        name = "Unknown Chat @ unknown channel"
        self.unknown_channel: PrivateChat = PrivateChat(
            module_id="__this_is_not_a_channel__",
            module_name="Unknown Channel",
            channel_emoji="‼️",
            name=name,
            alias="知らんでぇ",
            uid=ChatID(self.CHAT_ID_FORMAT.format(hash=hash(name))),
            notification=ChatNotificationState.ALL)

        name = "backup_chat"
        self.backup_chat: PrivateChat = PrivateChat(
            channel=self,
            name=name,
            uid=ChatID(self.CHAT_ID_FORMAT.format(hash=hash(name))),
            notification=ChatNotificationState.ALL)

        name = "backup_member"
        self.backup_member: ChatMember = ChatMember(
            self.chats_by_chat_type['GroupChat'][0],
            name=name,
            uid=ChatID(self.CHAT_ID_FORMAT.format(hash=hash(name))))
예제 #20
0
 def build_efb_chat_as_system_user(self, context):  # System user only!
     return SystemChat(
         channel=self.channel,
         name=str(context['event_description']),
         uid=ChatID("__{context[uid_prefix]}__".format(context=context)))
    def build_chat_by_thread_obj(self, thread: Thread) -> Chat:
        vendor_specific = {
            "chat_type": thread.type.name.capitalize(),
            "profile_picture_url": thread.photo,
        }
        chat: Chat
        if thread.uid == self.client.uid:
            chat = PrivateChat(channel=self.channel,
                               uid=thread.uid,
                               other_is_self=True)
            chat.name = chat.self.name  # type: ignore
        elif isinstance(thread, User):
            chat = PrivateChat(channel=self.channel,
                               name=thread.name,
                               uid=ChatID(thread.uid),
                               alias=thread.own_nickname or thread.nickname,
                               vendor_specific=vendor_specific)
        elif isinstance(thread, Page):
            desc = self._("{subtitle}\n{category} in {city}\n"
                          "Likes: {likes}\n{url}").format(
                              subtitle=thread.sub_title,
                              category=thread.category,
                              city=thread.city,
                              likes=thread.likes,
                              url=thread.url)
            chat = PrivateChat(channel=self.channel,
                               name=thread.name,
                               uid=ChatID(thread.uid),
                               description=desc,
                               vendor_specific=vendor_specific)
        elif isinstance(thread, Group):
            name = thread.name or ""

            group = GroupChat(channel=self.channel,
                              name=name,
                              uid=ChatID(thread.uid),
                              vendor_specific=vendor_specific)
            participant_ids = thread.participants - {self.client.uid}
            try:
                participants: Dict[ThreadID,
                                   User] = self.client.fetchThreadInfo(
                                       *participant_ids)
                for i in participant_ids:
                    member = participants[i]
                    alias = member.own_nickname or member.nickname or None
                    if thread.nicknames and i in thread.nicknames:
                        alias = thread.nicknames[str(i)] or None
                    group.add_member(name=member.name,
                                     alias=alias,
                                     uid=ChatID(i))
            except FBchatException:
                self.logger.exception(
                    "Error occurred while building chat members.")
                for i in participant_ids:
                    group.add_member(name=str(i), uid=ChatID(i))

            if thread.name is None:
                names = sorted(i.name for i in group.members)
                # TRANSLATORS: separation symbol between member names when group name is not provided.
                name = self._(", ").join(names[:3])
                if len(names) > 3:
                    extras = len(names) - 3
                    name += self.ngettext(", and {number} more",
                                          ", and {number} more",
                                          extras).format(number=extras)
                group.name = name

            chat = group
        else:
            chat = SystemChat(channel=self.channel,
                              name=thread.name,
                              uid=ChatID(thread.uid),
                              vendor_specific=vendor_specific)
        if chat.self:
            chat.self.uid = self.client.uid
        return chat
예제 #22
0
    def error(self, update: object, context: CallbackContext):
        """
        Print error to console, and send error message to first admin.
        Triggered by python-telegram-bot error callback.
        """
        assert context.error
        error: Exception = context.error
        if "make sure that only one bot instance is running" in str(error):
            now = time.time()
            # Warn the user only from the second time within ``CONFLICTION_TIMEOUT``
            # seconds to suppress isolated warnings.
            # https://github.com/ehForwarderBot/efb-telegram-master/issues/103
            if now - self.last_poll_confliction_time < self.CONFLICTION_TIMEOUT:
                msg = self._(
                    'Conflicted polling detected. If this error persists, '
                    'please ensure you are running only one instance of this Telegram bot.'
                )
                self.logger.critical(msg)
                self.bot_manager.send_message(self.config['admins'][0], msg)
            self.last_poll_confliction_time = now
            return
        if "Invalid server response" in str(error) and not update:
            self.logger.error(
                "Boom! Telegram API is no good. (Invalid server response.)")
            return
        # noinspection PyBroadException
        try:
            raise error
        except telegram.error.Unauthorized:
            self.logger.error(
                "The bot is not authorised to send update:\n%s\n%s",
                str(update), str(error))
        except telegram.error.BadRequest as e:
            assert isinstance(update, Update)
            if e.message == "Message is not modified" and update.callback_query:
                self.logger.error("Chill bro, don't click that fast.")
            else:
                self.logger.exception("Message request is invalid.\n%s\n%s",
                                      str(update), str(error))
                self.bot_manager.send_message(
                    self.config['admins'][0],
                    self._("Message request is invalid.\n{error}\n"
                           "<code>{update}</code>").format(
                               error=html.escape(str(error)),
                               update=html.escape(str(update))),
                    parse_mode="HTML")
        except (telegram.error.TimedOut, telegram.error.NetworkError):
            self.timeout_count += 1
            self.logger.error(
                "Poor internet connection detected.\n"
                "Number of network error occurred since last startup: %s\n%s\nUpdate: %s",
                self.timeout_count, str(error), str(update))
            if isinstance(update, Update) and isinstance(
                    update.message, Message):
                update.message.reply_text(self._(
                    "This message is not processed due to poor internet environment "
                    "of the server.\n"
                    "<code>{code}</code>").format(
                        code=html.escape(str(error))),
                                          quote=True,
                                          parse_mode="HTML")

            timeout_interval = self.flag('network_error_prompt_interval')
            if timeout_interval > 0 and self.timeout_count % timeout_interval == 0:
                self.bot_manager.send_message(
                    self.config['admins'][0],
                    self.ngettext(
                        "<b>EFB Telegram Master channel</b>\n"
                        "You may have a poor internet connection on your server. "
                        "Currently {count} network error is detected.\n"
                        "For more details, please refer to the log.",
                        "<b>EFB Telegram Master channel</b>\n"
                        "You may have a poor internet connection on your server. "
                        "Currently {count} network errors are detected.\n"
                        "For more details, please refer to the log.",
                        self.timeout_count).format(count=self.timeout_count),
                    parse_mode="HTML")
        except telegram.error.ChatMigrated as e:
            assert isinstance(update, Update)
            new_id = e.new_chat_id
            assert isinstance(update.message, Message)
            old_id = ChatID(str(update.message.chat_id))
            count = 0
            for i in self.db.get_chat_assoc(
                    master_uid=etm_utils.chat_id_to_str(
                        self.channel_id, old_id)):
                self.logger.debug(
                    'Migrating slave chat %s from Telegram chat %s to %s.', i,
                    old_id, new_id)
                self.db.remove_chat_assoc(slave_uid=i)
                self.db.add_chat_assoc(master_uid=etm_utils.chat_id_to_str(
                    self.channel_id, ChatID(str(new_id))),
                                       slave_uid=i)
                count += 1
            self.bot_manager.send_message(
                new_id,
                self.ngettext(
                    "Chat migration detected.\n"
                    "All {count} remote chat are now linked to this new group.",
                    "Chat migration detected.\n"
                    "All {count} remote chats are now linked to this new group.",
                    count).format(count=count))
        except Exception:
            try:
                self.bot_manager.send_message(
                    self.config['admins'][0],
                    self.
                    _("EFB Telegram Master channel encountered error <code>{error}</code> "
                      "caused by update <code>{update}</code>. See log for details."
                      ).format(error=html.escape(str(error)),
                               update=html.escape(str(update))),
                    parse_mode="HTML")
            except Exception as ex:
                self.logger.exception(
                    "Failed to send error message through Telegram: %s", ex)

            finally:
                self.logger.exception(
                    'Unhandled telegram bot error!\n'
                    'Update %s caused error %s. Exception', update, error)
예제 #23
0
    def wxpy_chat_to_efb_chat(self, chat: wxpy.Chat) -> Chat:
        # self.logger.debug("Converting WXPY chat %r, %sin recursive mode", chat, '' if recursive else 'not ')
        # self.logger.debug("WXPY chat with ID: %s, name: %s, alias: %s;", chat.puid, chat.nick_name, chat.alias)
        if chat is None:
            return self.MISSING_USER

        cache_key = chat.puid

        chat_name, chat_alias = self.get_name_alias(chat)

        cached_obj: Optional[Chat] = None
        if cache_key in self.efb_chat_objs:
            cached_obj = self.efb_chat_objs[cache_key]
            if chat_name == cached_obj.name and chat_alias == cached_obj.alias:
                return cached_obj

        # if chat name or alias changes, update cache
        efb_chat: Chat
        chat_id = ChatID(chat.puid or f"__invalid_{uuid4()}__")
        if cached_obj:
            efb_chat = cached_obj
            efb_chat.uid = chat_id
            efb_chat.name = chat_name
            efb_chat.alias = chat_alias
            efb_chat.vendor_specific = {'is_mp': isinstance(chat, wxpy.MP)}

            if isinstance(chat, wxpy.Group):
                # Update members if necessary
                remote_puids = {i.puid for i in chat.members}
                local_ids = {i.uid for i in efb_chat.members if not isinstance(i, SelfChatMember)}
                # Add missing members
                missing_puids = remote_puids - local_ids
                for member in chat.members:
                    if member.puid in missing_puids:
                        member_name, member_alias = self.get_name_alias(member)
                        efb_chat.add_member(name=member_name, alias=member_alias, uid=member.puid,
                                            vendor_specific={'is_mp': False})
        elif chat == chat.bot.self:
            efb_chat = PrivateChat(channel=self.channel, uid=chat_id, name=chat_name,
                                   alias=chat_alias, vendor_specific={'is_mp': True}, other_is_self=True)
        elif isinstance(chat, wxpy.Group):
            efb_chat = GroupChat(channel=self.channel, uid=chat_id, name=chat_name,
                                 alias=chat_alias, vendor_specific={'is_mp': False})
            for i in chat.members:
                if i.user_name == self.bot.self.user_name:
                    continue
                member_name, member_alias = self.get_name_alias(i)
                efb_chat.add_member(name=member_name, alias=member_alias, uid=i.puid, vendor_specific={'is_mp': False})
        elif isinstance(chat, wxpy.MP):
            efb_chat = PrivateChat(channel=self.channel, uid=chat_id, name=chat_name,
                                   alias=chat_alias, vendor_specific={'is_mp': True})
        elif isinstance(chat, wxpy.User):
            efb_chat = PrivateChat(channel=self.channel, uid=chat_id, name=chat_name,
                                   alias=chat_alias, vendor_specific={'is_mp': False})
        else:
            efb_chat = SystemChat(channel=self.channel, uid=chat_id, name=chat_name,
                                  alias=chat_alias, vendor_specific={'is_mp': False})

        efb_chat.vendor_specific.update(self.generate_vendor_specific(chat))
        if efb_chat.vendor_specific.get('is_muted', False):
            efb_chat.notification = ChatNotificationState.MENTIONS

        self.efb_chat_objs[cache_key] = efb_chat

        return efb_chat
 def __init__(self, instance_id: InstanceID = None):
     super().__init__(instance_id)
     # For test
     self.chat: PrivateChat = PrivateChat(
         channel=self, name='Echo Message', uid=ChatID('echo_chat'))
     self.condition: Optional[Condition] = None
    def msg(self, update: Update, context: CallbackContext):
        """
        Process, wrap and dispatch messages from user.
        """
        assert isinstance(update, Update)
        assert update.effective_message
        assert update.effective_chat

        message: Message = update.effective_message
        mid = utils.message_id_to_str(update=update)

        self.logger.debug("[%s] Received message from Telegram: %s", mid,
                          message.to_dict())

        destination = None
        edited = None
        quote = False

        if update.edited_message or update.edited_channel_post:
            self.logger.debug('[%s] Message is edited: %s', mid,
                              message.edit_date)
            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:
                message.reply_text(self._(
                    "Error: This message cannot be edited, and thus is not sent. (ME01)"
                ),
                                   quote=True)
                return
            destination = msg_log.slave_origin_uid
            edited = msg_log
            quote = msg_log.build_etm_msg(self.chat_manager).target is not None

        if destination is None:
            destination = self.get_singly_linked_chat_id_str(
                update.effective_chat)
            if destination:
                # if the chat is singly-linked
                quote = message.reply_to_message is not None
                self.logger.debug("[%s] Chat %s is singly-linked to %s", mid,
                                  message.chat, destination)

        if destination is None:  # not singly linked
            quote = False
            self.logger.debug("[%s] Chat %s is not singly-linked", mid,
                              update.effective_chat)
            reply_to = message.reply_to_message
            cached_dest = self.chat_dest_cache.get(str(message.chat.id))
            if reply_to:
                self.logger.debug("[%s] Message is quote-replying to %s", mid,
                                  reply_to)
                dest_msg = self.db.get_msg_log(
                    master_msg_id=utils.message_id_to_str(
                        TelegramChatID(reply_to.chat.id),
                        TelegramMessageID(reply_to.message_id)))
                if dest_msg:
                    destination = dest_msg.slave_origin_uid
                    self.chat_dest_cache.set(str(message.chat.id), destination)
                    self.logger.debug(
                        "[%s] Quoted message is found in database with destination: %s",
                        mid, destination)
            elif cached_dest:
                self.logger.debug("[%s] Cached destination found: %s", mid,
                                  cached_dest)
                destination = cached_dest
                self._send_cached_chat_warning(update,
                                               TelegramChatID(message.chat.id),
                                               cached_dest)

        self.logger.debug("[%s] Destination chat = %s", mid, destination)

        if destination is None:
            self.logger.debug("[%s] Destination is not found for this message",
                              mid)
            candidates = (
                self.db.get_recent_slave_chats(TelegramChatID(message.chat.id),
                                               limit=5)
                or self.db.get_chat_assoc(master_uid=utils.chat_id_to_str(
                    self.channel_id, ChatID(str(message.chat.id))))[:5])
            if candidates:
                self.logger.debug(
                    "[%s] Candidate suggestions are found for this message: %s",
                    mid, candidates)
                tg_err_msg = message.reply_text(self._(
                    "Error: No recipient specified.\n"
                    "Please reply to a previous message. (MS01)"),
                                                quote=True)
                self.channel.chat_binding.register_suggestions(
                    update, candidates,
                    TelegramChatID(update.effective_chat.id),
                    TelegramMessageID(tg_err_msg.message_id))
            else:
                self.logger.debug(
                    "[%s] Candidate suggestions not found, give up.", mid)
                message.reply_text(self._(
                    "Error: No recipient specified.\n"
                    "Please reply to a previous message. (MS02)"),
                                   quote=True)
        else:
            return self.process_telegram_message(update,
                                                 context,
                                                 destination,
                                                 quote=quote,
                                                 edited=edited)