class KeywordPromptMiddleware(Middleware):
    """
    """
    middleware_id: ModuleID = ModuleID("blueness.KeywordPromptMiddleware")
    middleware_name: str = "Keyword Prompt Middleware"
    __version__: str = '0.0.1'

    keywords = ['收到红包,请在手机上查看']

    def __init__(self, instance_id: Optional[InstanceID] = None):
        super().__init__(instance_id)

    def process_message(self, message: Message) -> Optional[Message]:
        if message.type == MsgType.Text and \
            message.author.module_id.startswith("blueset.wechat") and \
            type(message.chat) == GroupChat and \
            self.match_list(message.text):

            x: int = 0
            message.text = "@ME " + message.text
            message.substitutions = Substitutions({})
            message.substitutions[(x, x + 3)] = message.chat

        return message

    def match_list(self, text) -> bool:
        """
        关键字的匹配,主要匹配keywords的列表
        """
        for i in self.keywords:
            if text.find(i) != -1:
                return True
        return False
Esempio n. 2
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)
Esempio n. 3
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])
class CaiYunWeatherSlave(SlaveChannel):
    """
    This slave channel crawls weather data from CaiYun API regularly and optionally opens a web
    server to serve the data.
    """
    channel_name: str = 'Echo channel'
    channel_emoji: str = '🔁'
    channel_id: ModuleID = ModuleID('hawthorn.echo')
    __version__: str = __version__
    supported_message_types = {MsgType.Text}

    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 send_message(self, msg: Message) -> Message:
        if msg.type == MsgType.Text:
            coordinator.send_message(Message(
                uid=MessageID(f'echo_{uuid4()}'),
                type=MsgType.Text,
                chat=self.chat,
                author=self.chat.other,
                deliver_to=coordinator.master,
                text=msg.text,
            ))
        return msg

    def send_status(self, status: Status):
        raise EFBOperationNotSupported()

    def get_chat(self, chat_uid: ChatID) -> Chat:
        if chat_uid == self.chat.uid:
            return self.chat
        raise EFBChannelNotFound()

    def get_chat_picture(self, chat: Chat) -> BinaryIO:
        raise EFBOperationNotSupported()

    def get_chats(self) -> Collection[Chat]:
        return [self.chat]

    def get_message_by_id(self, chat: Chat, msg_id: MessageID) -> Optional[Message]:
        return None

    def poll(self):
        pass

    def stop_polling(self):
        pass
Esempio n. 5
0
 def __init__(self, profile: str, instance_id: str):
     coordinator.profile = profile
     self.profile = profile
     self.instance_id = instance_id
     self.channel_id = WeChatChannel.channel_id
     if instance_id:
         self.channel_id = ModuleID(self.channel_id + "#" + instance_id)
     self.config_path = utils.get_config_path(self.channel_id)
     self.yaml = YAML()
     if not self.config_path.exists():
         self.build_default_config()
     else:
         self.data = self.yaml.load(self.config_path.open())
Esempio n. 6
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 __init__(self, profile: str, instance_id: str):
        print("==== etm_wizard, data mod, init", profile)
        coordinator.profile = profile
        self.profile = profile
        self.instance_id = instance_id
        self.channel_id = TelegramChannel.channel_id

        if instance_id:
            self.channel_id = ModuleID(self.channel_id + "#" + instance_id)
        self.config_path = utils.get_config_path(self.channel_id)
        self.yaml = YAML()
        if not self.config_path.exists():
            self.build_default_config()
        else:
            self.data = self.yaml.load(self.config_path.open())
Esempio n. 8
0
class MockMiddleware(EFBMiddleware):
    """
    Attributes:
        mode:
            * Default "log": Logging only
            * "append_text": Append to the text attribute of the message
            * "interrupt": Interrupt and stop all messages from delivering.
            * "interrupt_non_text": Interrupt only non-text messages.
    """

    middleware_id: ModuleID = ModuleID("tests.mocks.middleware.MockMiddleware")
    middleware_name: str = "Mock Middleware"
    __version__: str = '0.0.1'

    logger = logging.getLogger(middleware_id)

    def __init__(self,
                 instance_id: Optional[InstanceID] = None,
                 mode: str = "log"):
        super().__init__(instance_id=instance_id)
        self.mode: str = mode

    def process_message(self, message: EFBMsg) -> Optional[EFBMsg]:
        self.logger.debug("Processing Message %s", message)
        if self.mode == "append_text":
            message.text += " (Processed by " + self.middleware_id + ")"
        elif self.mode == "interrupt":
            return None
        elif self.mode == "interrupt_non_text" and message.type != MsgType.Text:
            return None
        return message

    def process_status(self, status: EFBStatus) -> Optional[EFBStatus]:
        if self.mode == "interrupt":
            return None
        return status

    @extra(name="Echo",
           desc="Echo back the input.\n"
           "Usage:\n"
           "    {function_name} text")
    def echo(self, args):
        return args
class TelegramChannel(MasterChannel):
    """
    EFB Channel - Telegram (Master)
    Based on python-telegram-bot, Telegram Bot API

    Author: Eana Hufwe <https://github.com/blueset>

    Configuration file example:
        .. code-block:: yaml

            token: "12345678:1a2b3c4d5e6g7h8i9j"
            admins:
            - 102938475
            - 91827364
            flags:
                join_msg_threshold_secs: 10
                multiple_slave_chats: false
    """

    # Meta Info
    channel_name = "Telegram Master"
    channel_emoji = "✈"
    channel_id = ModuleID("blueset.telegram")
    supported_message_types = {
        MsgType.Text, MsgType.File, MsgType.Voice, MsgType.Image, MsgType.Link,
        MsgType.Location, MsgType.Sticker, MsgType.Video, MsgType.Animation,
        MsgType.Status
    }
    __version__ = __version__

    # Data
    _stop_polling = False
    timeout_count = 0

    # Constants
    config: dict

    # Translator
    translator: NullTranslations = translation("efb_telegram_master",
                                               resource_filename(
                                                   'efb_telegram_master',
                                                   'locale'),
                                               fallback=True)
    locale: Optional[str] = None

    # RPC server
    rpc_server: SimpleXMLRPCServer = None

    def __init__(self, instance_id: InstanceID = None):
        """
        Initialization.
        """
        super().__init__(instance_id)

        # Check PIL support for WebP
        Image.init()
        if 'WEBP' not in Image.ID or not WebPImagePlugin.SUPPORTED:
            raise EFBException(
                self._(
                    "WebP support of Pillow is required.\n"
                    "Please refer to Pillow Documentation for instructions.\n"
                    "https://pillow.readthedocs.io/"))

        # Suppress debug logs from dependencies
        logging.getLogger('requests').setLevel(logging.CRITICAL)
        logging.getLogger('urllib3').setLevel(logging.CRITICAL)
        logging.getLogger('telegram.bot').setLevel(logging.CRITICAL)
        logging.getLogger(
            'telegram.vendor.ptb_urllib3.urllib3.connectionpool').setLevel(
                logging.CRITICAL)

        # Set up logger
        self.logger: logging.Logger = logging.getLogger(__name__)

        # Load configs
        self.load_config()

        # Load predefined MIME types
        mimetypes.init(files=["mimetypes"])

        # Initialize managers
        self.flag: ExperimentalFlagsManager = ExperimentalFlagsManager(self)
        self.db: DatabaseManager = DatabaseManager(self)
        self.chat_manager: ChatObjectCacheManager = ChatObjectCacheManager(
            self)
        self.chat_dest_cache: ChatDestinationCache = ChatDestinationCache(
            self.flag("send_to_last_chat"))
        self.bot_manager: TelegramBotManager = TelegramBotManager(self)
        self.commands: CommandsManager = CommandsManager(self)
        self.chat_binding: ChatBindingManager = ChatBindingManager(self)
        self.slave_messages: SlaveMessageProcessor = SlaveMessageProcessor(
            self)

        if not self.flag('auto_locale'):
            self.translator = translation("efb_telegram_master",
                                          resource_filename(
                                              'efb_telegram_master', 'locale'),
                                          fallback=True)

        # Basic message handlers
        non_edit_filter = Filters.update.message | Filters.update.channel_post
        self.bot_manager.dispatcher.add_handler(
            CommandHandler("start", self.start, filters=non_edit_filter))
        self.bot_manager.dispatcher.add_handler(
            CommandHandler("help", self.help, filters=non_edit_filter))
        self.bot_manager.dispatcher.add_handler(
            CommandHandler("info", self.info, filters=non_edit_filter))
        self.bot_manager.dispatcher.add_handler(
            CallbackQueryHandler(self.void_callback_handler, pattern="void"))
        self.bot_manager.dispatcher.add_handler(
            CallbackQueryHandler(self.bot_manager.session_expired))
        self.bot_manager.dispatcher.add_handler(
            CommandHandler("react", self.react, filters=non_edit_filter))

        # Register master message handlers after commands to prevent commands
        # commands to be delivered as messages
        self.master_messages: MasterMessageProcessor = MasterMessageProcessor(
            self)

        self.bot_manager.dispatcher.add_error_handler(self.error)

        self.rpc_utilities = RPCUtilities(self)

    @property
    def _(self):
        return self.translator.gettext

    @property
    def ngettext(self):
        return self.translator.ngettext

    def load_config(self):
        """
        Load configuration from path specified by the framework.

        Configuration file is in YAML format.
        """
        config_path = efb_utils.get_config_path(self.channel_id)
        if not config_path.exists():
            raise FileNotFoundError(
                self._("Config File does not exist. ({path})").format(
                    path=config_path))
        with config_path.open() as f:
            data = YAML().load(f)

            # Verify configuration
            if not isinstance(data.get('token', None), str):
                raise ValueError(self._('Telegram bot token must be a string'))
            if isinstance(data.get('admins', None), int):
                data['admins'] = [data['admins']]
            if isinstance(data.get('admins', None),
                          str) and data['admins'].isdigit():
                data['admins'] = [int(data['admins'])]
            if not isinstance(data.get('admins', None),
                              list) or not data['admins']:
                raise ValueError(
                    self.
                    _("Admins' user IDs must be a list of one number or more."
                      ))
            for i in range(len(data['admins'])):
                if isinstance(data['admins'][i],
                              str) and data['admins'][i].isdigit():
                    data['admins'][i] = int(data['admins'][i])
                if not isinstance(data['admins'][i], int):
                    raise ValueError(
                        self.
                        _('Admin ID is expected to be an int, but {data} is found.'
                          ).format(data=data['admins'][i]))

            self.config = data.copy()

    def info(self, update: Update, context: CallbackContext):
        """
        Show info of the current telegram conversation.
        Triggered by `/info`.
        """
        if update.message.chat.type != telegram.Chat.PRIVATE:  # Group message
            msg = self.info_group(update)
        elif update.effective_message.forward_from_chat and \
                update.effective_message.forward_from_chat.type == 'channel':  # Forwarded channel command.
            msg = self.info_channel(update)
        else:  # Talking to the bot.
            msg = self.info_general()

        update.message.reply_text(msg)

    def info_general(self):
        """Generate string for information of the current running EFB instance."""
        if self.instance_id:
            if coordinator.profile != "default":
                msg = self._(
                    "This is EFB Telegram Master Channel {version}, running on profile “{profile}”, "
                    "instance “{instance}”, on EFB {fw_version}.")
            else:  # Default profile
                msg = self._(
                    "This is EFB Telegram Master Channel {version}, running on default profile, "
                    "instance “{instance}”, on EFB {fw_version}.")
        else:  # Default instance
            if coordinator.profile != "default":
                msg = self._(
                    "This is EFB Telegram Master Channel {version}, running on profile “{profile}”, "
                    "default instance, on EFB {fw_version}.")
            else:  # Default profile
                msg = self._(
                    "This is EFB Telegram Master Channel {version}, running on default profile and instance, "
                    "on EFB {fw_version}.")
        msg = msg.format(version=self.__version__,
                         fw_version=ehforwarderbot.__version__,
                         profile=coordinator.profile,
                         instance=self.instance_id)
        msg += "\n" + self.ngettext(
            "{count} slave channel activated:",
            "{count} slave channels activated:", len(
                coordinator.slaves)).format(count=len(coordinator.slaves))
        for i in coordinator.slaves:
            msg += "\n- %s %s (%s, %s)" % (coordinator.slaves[i].channel_emoji,
                                           coordinator.slaves[i].channel_name,
                                           i,
                                           coordinator.slaves[i].__version__)
        if coordinator.middlewares:
            msg += self.ngettext("\n\n{count} middleware activated:",
                                 "\n\n{count} middlewares activated:",
                                 len(coordinator.middlewares)).format(
                                     count=len(coordinator.middlewares))
            for i in coordinator.middlewares:
                msg += "\n- %s (%s, %s)" % (i.middleware_name, i.middleware_id,
                                            i.__version__)
        return msg

    def info_channel(self, update):
        """Generate string for chat linking info of a channel."""
        chat = update.effective_message.forward_from_chat
        links = self.db.get_chat_assoc(
            master_uid=etm_utils.chat_id_to_str(self.channel_id, chat.id))
        if links:  # Linked chat
            # TRANSLATORS: ‘channel’ here refers to a Telegram channel.
            msg = self._("The channel {group_name} ({group_id}) is linked to:") \
                .format(group_name=chat.title,
                        group_id=chat.id)
            msg += self.build_link_chats_info_str(links)
        else:
            # TRANSLATORS: ‘channel’ here means an EFB channel.
            msg = self._("The channel {group_name} ({group_id}) is "
                         "not linked to any remote chat. "
                         "To link one, use /link.").format(
                             group_name=chat.title, group_id=chat.id)
        return msg

    def info_group(self, update):
        """Generate string for chat linking info of a group."""
        links = self.db.get_chat_assoc(master_uid=etm_utils.chat_id_to_str(
            self.channel_id, update.message.chat_id))
        if links:  # Linked chat
            msg = self._(
                "The group {group_name} ({group_id}) is linked to:").format(
                    group_name=update.message.chat.title,
                    group_id=update.message.chat_id)
            msg += self.build_link_chats_info_str(links)
        else:
            msg = self._(
                "The group {group_name} ({group_id}) is not linked to any remote chat. "
                "To link one, use /link.").format(
                    group_name=update.message.chat.title,
                    group_id=update.message.chat_id)
        return msg

    def build_link_chats_info_str(self,
                                  links: List[EFBChannelChatIDStr]) -> str:
        """Build a string indicating all linked chats in argument.

        Returns:
            String that starts with a line break.
        """
        msg = ""
        for i in links:
            channel_id, chat_id, _ = etm_utils.chat_id_str_to_id(i)
            chat_object = self.chat_manager.get_chat(channel_id, chat_id)
            if chat_object:
                msg += "\n- %s (%s:%s)" % (chat_object.full_name, channel_id,
                                           chat_id)
            else:
                try:
                    module = coordinator.get_module_by_id(channel_id)
                    if isinstance(module, Channel):
                        channel_name = f"{module.channel_emoji} {module.channel_name}"
                    else:  # module is Middleware
                        channel_name = module.middleware_name
                    msg += self._(
                        "\n- {channel_name}: Unknown chat ({channel_id}:{chat_id})"
                    ).format(channel_name=channel_name,
                             channel_id=channel_id,
                             chat_id=chat_id)
                except NameError:
                    # TRANSLATORS: ‘channel’ here means an EFB channel.
                    msg += self._(
                        "\n- Unknown channel {channel_id}: ({chat_id})"
                    ).format(channel_id=channel_id, chat_id=chat_id)
        return msg

    def start(self, update: Update, context: CallbackContext):
        """
        Process bot command `/start`.
        """
        if context.args:  # Group binding command
            if update.effective_message.chat.type != telegram.Chat.PRIVATE or \
                    (update.effective_message.forward_from_chat and
                     update.effective_message.forward_from_chat.type == telegram.Chat.CHANNEL):
                self.chat_binding.link_chat(update, context.args)
            else:
                self.bot_manager.send_message(
                    update.effective_chat.id,
                    self.
                    _('You cannot link remote chats to here. Please try again.'
                      ))
        else:
            txt = self._(
                "This is EFB Telegram Master Channel.\n\n"
                "To learn more, please visit https://etm.1a23.studio .")
            self.bot_manager.send_message(update.effective_chat.id, txt)

    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

    def help(self, update: Update, context: CallbackContext):
        txt = self._(
            "EFB Telegram Master Channel\n"
            "/link\n"
            "    Link a remote chat to an empty Telegram group.\n"
            "    Followed by a regular expression to filter results.\n"
            "/chat\n"
            "    Generate a chat head to start a conversation.\n"
            "    Followed by a regular expression to filter results.\n"
            "/extra\n"
            "    List all additional features from slave channels.\n"
            "/unlink_all\n"
            "    Unlink all remote chats in this chat.\n"
            "/info\n"
            "    Show information of the current Telegram chat.\n"
            "/react [emoji]\n"
            "    React to a message with an emoji, or show a list of members reacted.\n"
            "/update_info\n"
            "    Update info of linked Telegram group.\n"
            "    Only works in singly linked group where the bot is an admin.\n"
            "/rm\n"
            "    Remove the quoted message from its remote chat.\n"
            "/help\n"
            "    Print this command list.")
        self.bot_manager.send_message(update.message.from_user.id, txt)

    def poll(self):
        """
        Message polling process.
        """
        self.bot_manager.polling()

    def error(self, update: Update, context: CallbackContext):
        """
        Print error to console, and send error message to first admin.
        Triggered by python-telegram-bot error callback.
        """
        error = context.error
        if "make sure that only one bot instance is running" in str(error):
            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)
            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:
            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 update is not None and isinstance(
                    getattr(update, "message", None), telegram.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:
            new_id = e.new_chat_id
            old_id = 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, 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)

    def send_message(self, msg: Message) -> Message:
        return self.slave_messages.send_message(msg)

    def send_status(self, status: Status):
        return self.slave_messages.send_status(status)

    def get_message_by_id(self, chat: Chat,
                          msg_id: MessageID) -> Optional['Message']:
        origin_uid = etm_utils.chat_id_to_str(chat=chat)
        msg_log = self.db.get_msg_log(slave_origin_uid=origin_uid,
                                      slave_msg_id=msg_id)
        if msg_log is not None:
            return msg_log.build_etm_msg(self.chat_manager)
        else:
            # Message is not found.
            return None

    def void_callback_handler(self, update: Update, context: CallbackContext):
        self.bot_manager.answer_callback_query(
            update.callback_query.id,
            text=self._("This button does nothing."),
            message_id=update.effective_message.message_id,
            cache_time=180)

    def stop_polling(self):
        self.logger.debug("Gracefully stopping %s (%s).", self.channel_name,
                          self.channel_id)
        self.rpc_utilities.shutdown()
        self.bot_manager.graceful_stop()
        self.master_messages.stop_worker()
        self.db.stop_worker()
        self.logger.debug("%s (%s) gracefully stopped.", self.channel_name,
                          self.channel_id)

    def get_chats(self) -> List[Chat]:
        raise EFBOperationNotSupported()
Esempio n. 10
0
class MockSlaveChannel(EFBChannel):

    channel_name: str = "Mock Slave"
    channel_emoji: str = "➖"
    channel_id: ModuleID = ModuleID("tests.mocks.slave.MockSlaveChannel")
    channel_type: ChannelType = ChannelType.Slave
    supported_message_types: Set[MsgType] = {MsgType.Text, MsgType.Link}
    __version__: str = '0.0.1'

    logger = getLogger(channel_id)

    polling = threading.Event()

    __picture_dict = {
        "alice": "A.png",
        "bob": "B.png",
        "carol": "C.png",
        "dave": "D.png",
        "wonderland001": "W.png"
    }

    def __init__(self, instance_id=None):
        super().__init__(instance_id)
        alice = EFBChat(self)
        alice.chat_name = "Alice"
        alice.chat_uid = "alice"
        alice.chat_type = ChatType.User
        self.alice = alice
        bob = EFBChat(self)
        bob.chat_name = "Bob"
        bob.chat_alias = "Little bobby"
        bob.chat_uid = "bob"
        bob.chat_type = ChatType.User
        self.bob = bob
        carol = EFBChat(self)
        carol.chat_name = "Carol"
        carol.chat_uid = "carol"
        carol.chat_type = ChatType.User
        self.carol = carol
        dave = EFBChat(self)
        dave.chat_name = "デブ"  # Nah, that's a joke
        dave.chat_uid = "dave"
        dave.chat_type = ChatType.User
        self.dave = dave
        wonderland = EFBChat(self)
        wonderland.chat_name = "Wonderland"
        wonderland.chat_uid = "wonderland001"
        wonderland.chat_type = ChatType.Group
        wonderland.members = [bob.copy(), carol.copy(), dave.copy()]
        for i in wonderland.members:
            i.group = wonderland
        self.wonderland = wonderland
        self.chats: List[EFBChat] = [alice, bob, wonderland]

    def poll(self):
        self.polling.wait()

    def send_status(self, status: EFBStatus):
        self.logger.debug("Received status: %r", status)

    def send_message(self, msg: EFBMsg) -> EFBMsg:
        self.logger.debug("Received message: %r", msg)
        return msg

    def stop_polling(self):
        self.polling.set()

    def get_chat(self,
                 chat_uid: str,
                 member_uid: Optional[str] = None) -> EFBChat:
        for i in self.chats:
            if chat_uid == i.chat_uid:
                if member_uid:
                    if i.chat_type == ChatType.Group:
                        for j in i.members:
                            if j.chat_uid == member_uid:
                                return j
                        raise EFBChatNotFound()
                    else:
                        raise EFBChatNotFound()
                return i
        raise EFBChatNotFound()

    def get_chats(self) -> List[EFBChat]:
        return self.chats.copy()

    def get_chat_picture(self, chat: EFBChat):
        if chat.chat_uid in self.__picture_dict:
            return open('tests/mocks/' + self.__picture_dict[chat.chat_uid],
                        'rb')

    def get_message_by_id(self, chat: EFBChat,
                          msg_id: MessageID) -> Optional['EFBMsg']:
        pass

    @extra(name="Echo",
           desc="Echo back the input.\n"
           "Usage:\n"
           "    {function_name} text")
    def echo(self, args):
        return args

    @extra(name="Extra function A",
           desc="Do something A.\nUsage: {function_name}")
    def function_a(self):
        return f"Value of function A from {self.channel_id}."

    @extra(name="Extra function B",
           desc="Do something B.\nUsage: {function_name}")
    def function_b(self):
        return f"Value of function B from {self.channel_name}."
Esempio n. 11
0
class WeChatChannel(SlaveChannel):
    """
    EFB Channel - WeChat Slave Channel
    Based on wxpy (itchat), WeChat Web Client

    Author: Eana Hufwe <https://github.com/blueset>
    """

    channel_name = "WeChat Slave"
    channel_emoji = "𝙒𝙚𝙘𝙝𝙖𝙩"
    channel_id = ModuleID('blueset.wechat')

    __version__ = __version__

    supported_message_types = {MsgType.Text, MsgType.Sticker, MsgType.Image,
                               MsgType.File, MsgType.Video, MsgType.Link, MsgType.Voice,
                               MsgType.Animation}
    logger: logging.Logger = logging.getLogger(
        "plugins.%s.WeChatChannel" % channel_id)
    done_reauth: threading.Event = threading.Event()
    _stop_polling_event: threading.Event = threading.Event()

    config: Dict[str, Any] = dict()

    bot: wxpy.Bot

    #third part wwechat_sender:listenser
    #listen(bot)

    # GNU Gettext Translator

    translator = translation("efb_wechat_slave",
                             resource_filename('efb_wechat_slave', 'locale'),
                             fallback=True)

    _: Callable = translator.gettext
    ngettext: Callable = translator.ngettext

    # Constants
    MAX_FILE_SIZE: int = 5 * 2 ** 20
    SYSTEM_ACCOUNTS: Final = {
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'filehelper': _('filehelper'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'newsapp': _('newsapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'fmessage': _('fmessage'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'weibo': _('weibo'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'qqmail': _('qqmail'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'tmessage': _('tmessage'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'qmessage': _('qmessage'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'qqsync': _('qqsync'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'floatbottle': _('floatbottle'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'lbsapp': _('lbsapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'shakeapp': _('shakeapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'medianote': _('medianote'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'qqfriend': _('qqfriend'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'readerapp': _('readerapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'blogapp': _('blogapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'facebookapp': _('facebookapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'masssendapp': _('masssendapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'meishiapp': _('meishiapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'feedsapp': _('feedsapp'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'voip': _('voip'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'blogappweixin': _('blogappweixin'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'weixin': _('weixin'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'brandsessionholder': _('brandsessionholder'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'weixinreminder': _('weixinreminder'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'officialaccounts': _('officialaccounts'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'notification_messages': _('notification_messages'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'wxitil': _('wxitil'),
        # TRANSLATORS: Translate this to the corresponding display name of the WeChat system account. Guessed names are not accepted.
        'userexperience_alarm': _('userexperience_alarm'),
    }
    MEDIA_MSG_TYPES: Final = {MsgType.Voice, MsgType.Video, MsgType.Animation,
                              MsgType.Image, MsgType.Sticker, MsgType.File}

    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__"))

    def load_config(self):
        """
        Load configuration from path specified by the framework.

        Configuration file is in YAML format.
        """
        config_path = efb_utils.get_config_path(self.channel_id)
        if not config_path.exists():
            return
        with config_path.open() as f:
            d = yaml.full_load(f)
            if not d:
                return
            self.config: Dict[str, Any] = d

    #
    # Utilities
    #

    def console_qr_code(self, uuid, status, qrcode=None):
        status = int(status)
        if self.qr_uuid == (uuid, status):
            return
        self.qr_uuid = (uuid, status)
        if status == 201:
            qr = self._('Confirm on your phone.')
            return self.logger.log(99, qr)
        elif status == 200:
            qr = self._("Successfully logged in.")
            return self.logger.log(99, qr)
        else:
            # 0: First QR code
            # 408: Updated QR code
            qr = self._("EWS: Please scan the QR code with your camera, screenshots will not work. ({0}, {1})") \
                     .format(uuid, status) + "\n"
            if status == 408:
                qr += self._("QR code expired, please scan the new one.") + "\n"
            qr += "\n"
            qr_url = "https://login.weixin.qq.com/l/" + uuid
            qr_obj = QRCode(qr_url)
            if self.flag("imgcat_qr"):
                qr_file = io.BytesIO()
                qr_obj.png(qr_file, scale=10)
                qr_file.seek(0)
                qr += ews_utils.imgcat(qr_file,
                                       f"{self.channel_id}_QR_{uuid}.png")
            else:
                qr += qr_obj.terminal()
            qr += "\n" + self._("If the QR code was not shown correctly, please visit:\n"
                                "https://login.weixin.qq.com/qrcode/{0}").format(uuid)
            return self.logger.log(99, qr)

    def master_qr_code(self, uuid, status, qrcode=None):
        status = int(status)
        if self.qr_uuid == (uuid, status):
            return
        self.qr_uuid = (uuid, status)

        msg = Message(
            uid=f"ews_auth_{uuid}_{status}_{uuid4()}",
            type=MsgType.Text,
            chat=self.user_auth_chat,
            author=self.user_auth_chat.other,
            deliver_to=coordinator.master,
        )

        if status == 201:
            msg.type = MsgType.Text
            msg.text = self._('Confirm on your phone.')
            self.master_qr_picture_id = None
        elif status == 200:
            msg.type = MsgType.Text
            msg.text = self._("Successfully logged in.")
            self.master_qr_picture_id = None
        elif uuid != self.qr_uuid:
            msg.type = MsgType.Image
            file = NamedTemporaryFile(suffix=".png")
            qr_url = "https://login.weixin.qq.com/l/" + uuid
            QRCode(qr_url).png(file, scale=10)
            msg.text = self._("QR code expired, please scan the new one.")
            msg.path = Path(file.name)
            msg.file = file
            msg.mime = 'image/png'
            if self.master_qr_picture_id is not None:
                msg.edit = True
                msg.edit_media = True
                msg.uid = self.master_qr_picture_id
            else:
                self.master_qr_picture_id = msg.uid
        if status in (200, 201) or uuid != self.qr_uuid:
            coordinator.send_message(msg)

    def exit_callback(self):
        # Don't send prompt if there's nowhere to send.
        if not getattr(coordinator, 'master', None):
            raise Exception(
                self._("Web WeChat logged your account out before master channel is ready."))
        self.logger.debug('Calling exit callback...')
        if self._stop_polling_event.is_set():
            return
        msg = Message(
            chat=self.user_auth_chat,
            author=self.user_auth_chat.other,
            deliver_to=coordinator.master,
            text=self._(
                "WeChat server has logged you out. Please log in again when you are ready."),
            uid=f"__reauth__.{uuid4()}",
            type=MsgType.Text,
        )
        on_log_out = self.flag("on_log_out")
        on_log_out = on_log_out if on_log_out in (
            "command", "idle", "reauth") else "command"
        if on_log_out == "command":
            msg.type = MsgType.Text
            msg.commands = MessageCommands(
                [MessageCommand(name=self._("Log in again"), callable_name="reauth", kwargs={"command": True})])
        elif on_log_out == "reauth":
            if self.flag("qr_reload") == "console_qr_code":
                msg.text += "\n" + self._("Please check your log to continue.")
            self.reauth()

        coordinator.send_message(msg)

    def poll(self):
        self.bot.start()
        self._stop_polling_event.wait()
        # while not self.stop_polling:
        #     if not self.bot.alive:
        #         self.done_reauth.wait()
        #         self.done_reauth.clear()
        self.logger.debug("%s (%s) gracefully stopped.",
                          self.channel_name, self.channel_id)

    def send_message(self, msg: Message) -> Message:
        """Send a message to WeChat.
        Supports text, image, sticker, and file.

        Args:
            msg (channel.Message): Message Object to be sent.

        Returns:
            This method returns nothing.

        Raises:
            EFBChatNotFound:
                Raised when a chat required is not found.

            EFBMessageTypeNotSupported:
                Raised when the message type sent is not supported by the
                channel.

            EFBOperationNotSupported:
                Raised when an message edit request is sent, but not
                supported by the channel.

            EFBMessageNotFound:
                Raised when an existing message indicated is not found.
                E.g.: The message to be edited, the message referred
                in the :attr:`msg.target <.Message.target>`
                attribute.

            EFBMessageError:
                Raised when other error occurred while sending or editing the
                message.
        """
        if msg.chat == self.user_auth_chat:
            raise EFBChatNotFound

        chat: wxpy.Chat = self.chats.get_wxpy_chat_by_uid(msg.chat.uid)

        # List of "SentMessage" response for all messages sent
        r: List[wxpy.SentMessage] = []
        self.logger.info("[%s] Sending message to WeChat:\n"
                         "uid: %s\n"
                         "UserName: %s\n"
                         "NickName: %s\n"
                         "Type: %s\n"
                         "Text: %s",
                         msg.uid,
                         msg.chat.uid, chat.user_name, chat.name, msg.type, msg.text)

        try:
            chat.mark_as_read()
        except wxpy.ResponseError as e:
            self.logger.exception(
                "[%s] Error occurred while marking chat as read. (%s)", msg.uid, e)

        send_text_only = False
        self.logger.debug('[%s] Is edited: %s', msg.uid, msg.edit)
        if msg.edit and msg.uid:
            if self.flag('delete_on_edit'):
                msg_ids = json.loads(msg.uid)
                if msg.type in self.MEDIA_MSG_TYPES and not msg.edit_media:
                    # Treat message as text message to prevent resend of media
                    msg_ids = msg_ids[1:]
                    send_text_only = True
                failed = 0
                for i in msg_ids:
                    try:
                        ews_utils.message_id_to_dummy_message(i, self).recall()
                    except wxpy.ResponseError as e:
                        self.logger.error(
                            "[%s] Trying to recall message but failed: %s", msg.uid, e)
                        failed += 1
                if failed:
                    raise EFBMessageError(
                        self.ngettext('Failed to recall {failed} out of {total} message, edited message was not sent.',
                                      'Failed to recall {failed} out of {total} messages, edited message was not sent.',
                                      len(msg_ids)).format(
                            failed=failed,
                            total=len(msg_ids)
                        ))
                # Not caching message ID as message recall feedback is not needed in edit mode
            else:
                raise EFBOperationNotSupported()
        if send_text_only or msg.type in [MsgType.Text, MsgType.Link]:
            if isinstance(msg.target, Message):
                max_length = self.flag("max_quote_length")
                qt_txt = msg.target.text or msg.target.type.name
                if max_length > 0:
                    if len(qt_txt) >= max_length:
                        tgt_text = qt_txt[:max_length]
                        tgt_text += "…"
                    else:
                        tgt_text = qt_txt
                elif max_length < 0:
                    tgt_text = qt_txt
                else:
                    tgt_text = ""
                if isinstance(chat, wxpy.Group) and not isinstance(msg.target.author, SelfChatMember):
                    tgt_alias = "@%s\u2005:" % msg.target.author.display_name
                else:
                    tgt_alias = ""
                msg.text = f"「{tgt_alias}{tgt_text}」\n- - - - - - - - - - - - - - -\n{msg.text}"
            r.append(self._bot_send_msg(chat, msg.text))
            self.logger.debug(
                '[%s] Sent as a text message. %s', msg.uid, msg.text)
        elif msg.type in (MsgType.Image, MsgType.Sticker, MsgType.Animation):
            self.logger.info("[%s] Image/GIF/Sticker %s", msg.uid, msg.type)

            convert_to = None
            file = msg.file
            assert file is not None

            if self.flag('send_stickers_and_gif_as_jpeg'):
                if msg.type == MsgType.Sticker or msg.mime == "image/gif":
                    convert_to = "image/jpeg"
            else:
                if msg.type == MsgType.Sticker:
                    convert_to = "image/gif"

            if convert_to == "image/gif":
                with NamedTemporaryFile(suffix=".gif") as f:
                    try:
                        img = Image.open(file)
                        try:
                            alpha = img.split()[3]
                            mask = Image.eval(
                                alpha, lambda a: 255 if a <= 128 else 0)
                        except IndexError:
                            mask = Image.eval(img.split()[0], lambda a: 0)
                        img = img.convert('RGB').convert(
                            'P', palette=Image.ADAPTIVE, colors=255)
                        img.paste(255, mask)
                        img.save(f, transparency=255)
                        msg.path = Path(f.name)
                        self.logger.debug(
                            '[%s] Image converted from %s to GIF', msg.uid, msg.mime)
                        file.close()
                        if f.seek(0, 2) > self.MAX_FILE_SIZE:
                            raise EFBMessageError(
                                self._("Image size is too large. (IS02)"))
                        f.seek(0)
                        r.append(self._bot_send_image(chat, f.name, f))
                    finally:
                        if not file.closed:
                            file.close()
            elif convert_to == "image/jpeg":
                with NamedTemporaryFile(suffix=".jpg") as f:
                    try:
                        img = Image.open(file).convert('RGBA')
                        out = Image.new("RGBA", img.size, (255, 255, 255, 255))
                        out.paste(img, img)
                        out.convert('RGB').save(f)
                        msg.path = Path(f.name)
                        self.logger.debug(
                            '[%s] Image converted from %s to JPEG', msg.uid, msg.mime)
                        file.close()
                        if f.seek(0, 2) > self.MAX_FILE_SIZE:
                            raise EFBMessageError(
                                self._("Image size is too large. (IS02)"))
                        f.seek(0)
                        r.append(self._bot_send_image(chat, f.name, f))
                    finally:
                        if not file.closed:
                            file.close()
            else:
                try:
                    if file.seek(0, 2) > self.MAX_FILE_SIZE:
                        raise EFBMessageError(
                            self._("Image size is too large. (IS01)"))
                    file.seek(0)
                    self.logger.debug(
                        "[%s] Sending %s (image) to WeChat.", msg.uid, msg.path)
                    filename = msg.filename or (msg.path and msg.path.name)
                    assert filename
                    r.append(self._bot_send_image(chat, filename, file))
                finally:
                    if not file.closed:
                        file.close()
            if msg.text:
                r.append(self._bot_send_msg(chat, msg.text))
        elif msg.type in (MsgType.File, MsgType.Audio):
            self.logger.info("[%s] Sending %s to WeChat\nFileName: %s\nPath: %s\nFilename: %s",
                             msg.uid, msg.type, msg.text, msg.path, msg.filename)
            filename = msg.filename or (msg.path and msg.path.name)
            assert filename and msg.file
            r.append(self._bot_send_file(chat, filename, file=msg.file))
            if msg.text:
                self._bot_send_msg(chat, msg.text)
            if not msg.file.closed:
                msg.file.close()
        elif msg.type == MsgType.Video:
            self.logger.info(
                "[%s] Sending video to WeChat\nFileName: %s\nPath: %s", msg.uid, msg.text, msg.path)
            filename = msg.filename or (msg.path and msg.path.name)
            assert filename and msg.file
            r.append(self._bot_send_video(chat, filename, file=msg.file))
            if msg.text:
                r.append(self._bot_send_msg(chat, msg.text))
            if not msg.file.closed:
                msg.file.close()
        else:
            raise EFBMessageTypeNotSupported()

        msg.uid = ews_utils.generate_message_uid(r)
        self.logger.debug(
            'WeChat message is assigned with unique ID: %s', msg.uid)
        return msg

    def send_status(self, status: Status):
        if isinstance(status, MessageRemoval):
            if not isinstance(status.message.author, SelfChatMember):
                raise EFBOperationNotSupported(
                    self._('You can only recall your own messages.'))
            if status.message.uid:
                try:
                    msg_ids = json.loads(status.message.uid)
                except JSONDecodeError:
                    raise EFBMessageError(
                        self._("ID of the message to recall is invalid."))
            else:
                raise EFBMessageError(
                    self._("ID of the message to recall is not found."))
            failed = 0
            if any(len(i) == 1 for i in msg_ids):  # Message is not sent through EWS
                raise EFBOperationNotSupported(
                    self._("You may only recall messages sent via EWS.")
                )
            for i in msg_ids:
                try:
                    ews_utils.message_id_to_dummy_message(i, self).recall()
                except wxpy.ResponseError:
                    failed += 1
            if failed:
                raise EFBMessageError(
                    self.ngettext(
                        'Failed to recall {failed} of {total} message.',
                        'Failed to recall {failed} of {total} messages.',
                        len(msg_ids)
                    ).format(failed=failed, total=len(msg_ids)))
            else:
                val = (status.message.uid, len(msg_ids))
                for i in msg_ids:
                    self.slave_message.recall_msg_id_conversion[str(
                        i[1])] = val
        else:
            raise EFBOperationNotSupported()

    def get_chat_picture(self, chat: Chat) -> BinaryIO:
        uid = chat.uid
        if uid in wxpy.Chat.SYSTEM_ACCOUNTS:
            wxpy_chat: wxpy.Chat = wxpy.Chat(
                wxpy.utils.wrap_user_name(uid), self.bot)
        else:
            wxpy_chat = wxpy.utils.ensure_one(self.bot.search(puid=uid))
        f: BinaryIO = None  # type: ignore
        try:
            f = tempfile.NamedTemporaryFile(suffix='.jpg')  # type: ignore
            data = wxpy_chat.get_avatar(None)
            if not data:
                raise EFBOperationNotSupported()
            f.write(data)
            f.seek(0)
            return f
        except (TypeError, ResponseError):
            if f is not None:
                f.close()
            raise EFBOperationNotSupported()

    # Additional features

    @extra(name=_("Show chat list"),
           desc=_("Show a list of chats from WeChat.\n"
                  "Usage:\n    {function_name} [-r]\n"
                  "    -r: Refresh list"))
    def get_chat_list(self, param: str = "") -> str:
        refresh = False
        if param:
            if param == "-r":
                refresh = True
            else:
                return self._("Unknown parameter: {}.").format(param)
        l: List[wxpy.Chat] = self.bot.chats(refresh)

        msg = self._("Chat list:") + "\n"
        for i in l:
            alias = ews_utils.wechat_string_unescape(getattr(i, 'remark_name', '') or
                                                     getattr(i, 'display_name', ''))
            name = ews_utils.wechat_string_unescape(i.nick_name)
            display_name = "%s (%s)" % (
                alias, name) if alias and alias != name else name
            chat_type = "?"
            if isinstance(i, wxpy.MP):
                # TRANSLATORS: Acronym for MP accounts
                chat_type = self._('MP')
            elif isinstance(i, wxpy.Group):
                # TRANSLATORS: Acronym for groups
                chat_type = self._('Gr')
            elif isinstance(i, wxpy.User):
                # TRANSLATORS: Acronym for users/friends
                chat_type = self._('Fr')
            msg += "\n%s: [%s] %s" % (i.puid, chat_type, display_name)

        return msg

    @extra(name=_("Set alias"),
           desc=_("Set an alias (remark name) for friends. Not applicable to "
                  "groups and MPs.\n"
                  "Usage:\n"
                  "    {function_name} id [alias]\n"
                  "    id: Chat ID, available from \"Show chat list\".\n"
                  "    alias: Alias. Leave empty to delete alias."))
    def set_alias(self, r_param: str = "") -> str:
        if r_param:
            param = r_param.split(maxsplit=1)
            if len(param) == 1:
                cid = param[0]
                alias = ""
            else:
                cid, alias = param
        else:
            return self.set_alias.desc  # type: ignore

        chat = self.bot.search(cid)

        if not chat:
            return self._("Chat {0} is not found.").format(cid)

        if not isinstance(chat, wxpy.User):
            return self._("Remark name is only applicable to friends.")

        chat.set_remark_name(alias)

        if alias:
            return self._("\"{0}\" now has remark name \"{1}\".").format(chat.nick_name, alias)
        else:
            return self._("Remark name of \"{0}\" has been removed.").format(chat.nick_name)

    @extra(name=_("Log out"),
           desc=_("Log out from WeChat and try to log in again.\n"
                  "Usage: {function_name}"))
    def force_log_out(self, _: str = "") -> str:
        self.bot.logout()
        self.exit_callback()
        return self._("Done.")

    # region [Command functions]

    def reauth(self, command=False):
        msg = self._("Preparing to log in...")
        qr_reload = self.flag("qr_reload")
        if command and qr_reload == "console_qr_code":
            msg += "\n" + self._("Please check your log to continue.")

        threading.Thread(target=self.authenticate, args=(
            qr_reload,), name="EWS reauth thread").start()
        return msg

    # endregion [Command functions]

    def authenticate(self, qr_reload, first_start=False):
        self.master_qr_picture_id = None
        qr_callback = getattr(self, qr_reload, self.master_qr_code)
        if getattr(self, 'bot', None):  # if a bot exists
            self.bot.cleanup()
        with coordinator.mutex:
            self.bot: wxpy.Bot = wxpy.Bot(cache_path=str(efb_utils.get_data_path(self.channel_id) / "wxpy.pkl"),
                                          qr_callback=qr_callback,
                                          logout_callback=self.exit_callback,
                                          user_agent=self.flag('user_agent'),
                                          start_immediately=not first_start)
            self.bot.enable_puid(
                efb_utils.get_data_path(self.channel_id) / "wxpy_puid.pkl",
                self.flag('puid_logs')
            )
            self.done_reauth.set()
            if hasattr(self, "slave_message"):
                self.slave_message.bot = self.bot
                self.slave_message.wechat_msg_register()

    def add_friend(self, username: str = None, verify_information: str = "") -> str:
        if not username:
            return self._("Empty username (UE02).")
        try:
            self.bot.add_friend(
                user=username, verify_content=verify_information)
        except wxpy.ResponseError as r:
            return self._("Error occurred while processing (AF01).") + "\n\n{}: {!r}".format(r.err_code, r.err_msg)
        return self._("Request sent.")

    def accept_friend(self, username: str = None, verify_information: str = "") -> str:
        if not username:
            return self._("Empty username (UE03).")
        try:
            self.bot.accept_friend(
                user=username, verify_content=verify_information)
        except wxpy.ResponseError as r:
            return self._("Error occurred while processing (AF02).") + "n\n{}: {!r}".format(r.err_code, r.err_msg)
        return self._("Request accepted.")

    def get_chats(self) -> List[Chat]:
        """
        Get all chats available from WeChat
        """
        return self.chats.get_chats()

    def get_chat(self, chat_uid: str) -> Chat:
        chat = self.chats.search_chat(uid=chat_uid)
        if not chat:
            raise EFBChatNotFound()
        else:
            return chat

    def stop_polling(self):
        self.bot.cleanup()
        if not self._stop_polling_event.is_set():
            self._stop_polling_event.set()
        else:
            self.done_reauth.set()

    def _bot_send_msg(self, chat: wxpy.Chat, message: str) -> wxpy.SentMessage:
        try:
            return chat.send_msg(message)
        except wxpy.ResponseError as e:
            e = self.substitute_known_error_reason(e)
            raise EFBMessageError(self._("Error from Web WeChat while sending message: [{code}] {message}")
                                  .format(code=e.err_code, message=e.err_msg))

    def _bot_send_file(self, chat: wxpy.Chat, filename: str, file: IO[bytes]) -> wxpy.SentMessage:
        try:
            return chat.send_file(filename, file=file)
        except wxpy.ResponseError as e:
            e = self.substitute_known_error_reason(e)
            raise EFBMessageError(self._("Error from Web WeChat while sending file: [{code}] {message}")
                                  .format(code=e.err_code, message=e.err_msg))

    def _bot_send_image(self, chat: wxpy.Chat, filename: str, file: IO[bytes]) -> wxpy.SentMessage:
        try:
            return chat.send_image(filename, file=file)
        except wxpy.ResponseError as e:
            e = self.substitute_known_error_reason(e)
            raise EFBMessageError(self._("Error from Web WeChat while sending image: [{code}] {message}")
                                  .format(code=e.err_code, message=e.err_msg))

    def _bot_send_video(self, chat: wxpy.Chat, filename: str, file: IO[bytes]) -> wxpy.SentMessage:
        try:
            return chat.send_video(filename, file=file)
        except wxpy.ResponseError as e:
            e = self.substitute_known_error_reason(e)
            raise EFBMessageError(self._("Error from Web WeChat while sending video: [{code}] {message}")
                                  .format(code=e.err_code, message=e.err_msg))

    def substitute_known_error_reason(self, err: wxpy.ResponseError) -> wxpy.ResponseError:
        if not err.err_msg:
            issue_url = "https://ews.1a23.studio/issues/55"
            if err.err_code in (1101, 1102, 1103):
                err.err_msg = self._("Your Web WeChat session might be expired. "
                                     "Please try to log out with the “force_log_out” command, and log in again. "
                                     "If you believe that is not the case, please leave a comment at {issue_url} .").format(
                    issue_url=issue_url
                )
            elif err.err_code == 1204:
                err.err_msg = self._(
                    "You don’t have access to the chat that you are trying to send message to.")
            elif err.err_code == 1205:
                err.err_msg = self._("You might have sent your messages too fast. Please try to slow down "
                                     "and retry after a while.")
            else:
                err.err_msg = self._("This is an unknown error from Web WeChat which we know nothing about why this "
                                     "is happening. If you have seen a pattern or if you happen to know the reason "
                                     "for this error code, please leave a comment at {issue_url} .").format(
                    issue_url=issue_url
                )
        return err

    def get_message_by_id(self, chat: Chat, msg_id: MessageID) -> Optional['Message']:
        raise EFBOperationNotSupported()
Esempio n. 12
0
class MockSlaveChannel(SlaveChannel):

    channel_name: str = "Mock Slave"
    channel_emoji: str = "➖"
    channel_id: ModuleID = ModuleID("tests.mocks.slave")
    supported_message_types: Set[MsgType] = {
        MsgType.Text, MsgType.Image, MsgType.Voice, MsgType.Animation,
        MsgType.Video, MsgType.File, MsgType.Location, MsgType.Link,
        MsgType.Sticker, MsgType.Status, MsgType.Unsupported
    }
    __version__: str = '0.0.2'

    logger = getLogger(channel_id)

    CHAT_ID_FORMAT = "__chat_{hash}__"

    polling = threading.Event()

    __picture_dict: Dict[str, str] = {}

    suggested_reactions: List[ReactionName] = [
        ReactionName("R0"),
        ReactionName("R1"),
        ReactionName("R2"),
        ReactionName("R3"),
        ReactionName("R4")
    ]

    # region [Chat data]
    # fields: name, type, notification, avatar, alias
    __chat_templates = [
        ("A", PrivateChat, ChatNotificationState.NONE, "A.png", "Alice"),
        ("B", PrivateChat, ChatNotificationState.MENTIONS, "B.png", "Bob"),
        ("C", PrivateChat, ChatNotificationState.ALL, "C.png", "Carol"),
        ("D", SystemChat, ChatNotificationState.NONE, "D.png", "Dave"),
        ("E", SystemChat, ChatNotificationState.MENTIONS, "E.png", "Eve"),
        ("F", SystemChat, ChatNotificationState.ALL, "F.png", "Frank"),
        ("G", PrivateChat, ChatNotificationState.NONE, "G.png", None),
        ("H", PrivateChat, ChatNotificationState.MENTIONS, "H.png", None),
        ("I", PrivateChat, ChatNotificationState.ALL, "I.png", None),
        ("J", SystemChat, ChatNotificationState.NONE, "J.png", None),
        ("K", SystemChat, ChatNotificationState.MENTIONS, "K.png", None),
        ("L", SystemChat, ChatNotificationState.ALL, "L.png", None),
        ("Ur", GroupChat, ChatNotificationState.NONE, "U.png", "Uranus"),
        ("Ve", GroupChat, ChatNotificationState.MENTIONS, "V.png", "Venus"),
        ("Wo", GroupChat, ChatNotificationState.ALL, "W.png", "Wonderland"),
        ("Xe", GroupChat, ChatNotificationState.NONE, "X.png", None),
        ("Yb", GroupChat, ChatNotificationState.MENTIONS, "Y.png", None),
        ("Zn", GroupChat, ChatNotificationState.ALL, "Z.png", None),
        ("あ", PrivateChat, ChatNotificationState.NONE, None, "あべ"),
        ("い", PrivateChat, ChatNotificationState.MENTIONS, None, "いとう"),
        ("う", PrivateChat, ChatNotificationState.ALL, None, "うえだ"),
        ("え", SystemChat, ChatNotificationState.NONE, None, "えのもと"),
        ("お", SystemChat, ChatNotificationState.MENTIONS, None, "おがわ"),
        ("か", SystemChat, ChatNotificationState.ALL, None, "かとう"),
        ("き", PrivateChat, ChatNotificationState.NONE, None, None),
        ("く", PrivateChat, ChatNotificationState.MENTIONS, None, None),
        ("け", PrivateChat, ChatNotificationState.ALL, None, None),
        ("こ", SystemChat, ChatNotificationState.NONE, None, None),
        ("さ", SystemChat, ChatNotificationState.MENTIONS, None, None),
        ("し", SystemChat, ChatNotificationState.ALL, None, None),
        ("らん", GroupChat, ChatNotificationState.NONE, None, "ランド"),
        ("りぞ", GroupChat, ChatNotificationState.MENTIONS, None, "リゾート"),
        ("るう", GroupChat, ChatNotificationState.ALL, None, "ルートディレクトリ"),
        ("れつ", GroupChat, ChatNotificationState.NONE, None, None),
        ("ろく", GroupChat, ChatNotificationState.MENTIONS, None, None),
        ("われ", GroupChat, ChatNotificationState.ALL, None, None),
    ]

    __group_member_templates = [
        ("A", ChatNotificationState.NONE, "A.png", "安"),
        ("B & S", ChatNotificationState.MENTIONS, "B.png", "柏"),
        ("C", ChatNotificationState.ALL, "C.png", "陈"),
        ("D", ChatNotificationState.NONE, "D.png", None),
        ("E", ChatNotificationState.MENTIONS, "E.png", None),
        ("F", ChatNotificationState.ALL, "F.png", None),
        ("Ал", ChatNotificationState.NONE, None, "Александра"),
        ("Бэ", ChatNotificationState.MENTIONS, None, "Борис"),
        ("Вэ", ChatNotificationState.ALL, None, "Владислав"),
        ("Э", ChatNotificationState.NONE, None, None),
        ("Ю", ChatNotificationState.MENTIONS, None, None),
        ("Я", ChatNotificationState.ALL, None, None),
    ]

    # endregion [Chat data]

    def __init__(self, instance_id=None):
        super().__init__(instance_id)
        self.generate_chats()

        self.chat_with_alias: PrivateChat = self.chats_by_alias[True][0]
        self.chat_without_alias: PrivateChat = self.chats_by_alias[False][0]
        self.group: GroupChat = self.chats_by_chat_type['GroupChat'][0]

        self.messages: "Queue[Message]" = Queue()
        self.statuses: "Queue[Status]" = Queue()
        self.messages_sent: Dict[str, Message] = dict()

        # flags
        self.message_removal_possible: bool = True
        self.accept_message_reactions: str = "accept"

        # chat/member changes
        self.chat_to_toggle: PrivateChat = self.get_chat(
            self.CHAT_ID_FORMAT.format(hash=hash("I")))
        self.chat_to_edit: PrivateChat = self.get_chat(
            self.CHAT_ID_FORMAT.format(hash=hash("われ")))
        self.member_to_toggle: ChatMember = self.get_chat(
            self.group.uid).get_member(
                self.CHAT_ID_FORMAT.format(hash=hash("Ю")))
        self.member_to_edit: ChatMember = self.get_chat(
            self.group.uid).get_member(
                self.CHAT_ID_FORMAT.format(hash=hash("Я")))

    # region [Clear queues]

    def clear_messages(self):
        self._clear_queue(self.messages)

    def clear_statuses(self):
        self._clear_queue(self.statuses)

    @staticmethod
    def _clear_queue(q: Queue):
        """Safely clear all items in a queue.
        Written by Niklas R on StackOverflow
        https://stackoverflow.com/a/31892187/1989455
        """
        with q.mutex:
            unfinished = q.unfinished_tasks - len(q.queue)
            if unfinished <= 0:
                if unfinished < 0:
                    raise ValueError('task_done() called too many times')
                q.all_tasks_done.notify_all()
            q.unfinished_tasks = unfinished
            q.queue.clear()
            q.not_full.notify_all()

    # endregion [Clear queues]
    # region [Populate chats]

    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))))

    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))),
            )

    # endregion [Populate chats]
    # region [Necessities]

    def poll(self):
        self.polling.wait()

    def send_status(self, status: Status):
        self.logger.debug("Received status: %r", status)
        if isinstance(status, MessageRemoval):
            self.message_removal_status(status)
        elif isinstance(status, ReactToMessage):
            self.react_to_message_status(status)
        self.statuses.put(status)

    def send_message(self, msg: Message) -> Message:
        self.logger.debug("Received message: %r", msg)
        self.messages.put(msg)
        msg.uid = MessageID(str(uuid4()))
        self.messages_sent[msg.uid] = msg
        return msg

    def stop_polling(self):
        self.polling.set()

    def get_chat(self, chat_uid: str) -> Chat:
        for i in self.chats:
            if chat_uid == i.uid:
                return i
        raise EFBChatNotFound()

    def get_chats(self) -> List[Chat]:
        return self.chats.copy()

    def get_chat_picture(self, chat: Chat) -> Optional[BinaryIO]:
        if self.__picture_dict.get(chat.uid):
            return open(f'tests/mocks/{self.__picture_dict[chat.uid]}', 'rb')

    # endregion [Necessities]

    def get_chats_by_criteria(
            self,
            chat_type: Optional[ChatTypeName] = None,
            notification: Optional[ChatNotificationState] = None,
            avatar: Optional[bool] = None,
            alias: Optional[bool] = None) -> List[Chat]:
        """Find a list of chats that satisfy a criteria. Leave a value
        unset (None) to pick all possible values of the criteria.
        """
        s = self.chats.copy()
        if chat_type is not None:
            s = [i for i in s if i in self.chats_by_chat_type[chat_type]]
        if notification is not None:
            s = [
                i for i in s
                if i in self.chats_by_notification_state[notification]
            ]
        if avatar is not None:
            s = [i for i in s if i in self.chats_by_profile_picture[avatar]]
        if alias is not None:
            s = [i for i in s if i in self.chats_by_alias[alias]]
        return s

    def get_chat_by_criteria(
            self,
            chat_type: Optional[ChatTypeName] = None,
            notification: Optional[ChatNotificationState] = None,
            avatar: Optional[bool] = None,
            alias: Optional[bool] = None) -> Chat:
        """Alias of ``get_chats_by_criteria(*args)[0]``."""
        return self.get_chats_by_criteria(chat_type=chat_type,
                                          notification=notification,
                                          avatar=avatar,
                                          alias=alias)[0]

    @extra(name="Echo",
           desc="Echo back the input.\n"
           "Usage:\n"
           "    {function_name} text")
    def echo(self, args):
        return args

    # region [Reactions]

    def build_reactions(self, group: Chat) -> Reactions:
        possible_reactions = self.suggested_reactions[:-1] + [None]
        chats = group.members
        reactions: Dict[ReactionName, List[Chat]] = {}
        for i in chats:
            reaction = random.choice(possible_reactions)
            if reaction is None:
                continue
            elif reaction not in reactions:
                reactions[reaction] = [i]
            else:
                reactions[reaction].append(i)
        return reactions

    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

    # endregion [Reactions]
    # region [Commands]

    @staticmethod
    def build_message_commands() -> MessageCommands:
        return MessageCommands([
            MessageCommand("Ping!", "command_ping"),
            MessageCommand("Bam", "command_bam"),
        ])

    @staticmethod
    def command_ping() -> Optional[str]:
        return "Pong!"

    @staticmethod
    def command_bam():
        return None

    # endregion [Commands]

    @staticmethod
    def build_substitutions(text: str, chat: Chat) -> Substitutions:
        size = len(text)
        a_0, a_1, b_0, b_1 = sorted(random.sample(range(size + 1), k=4))
        a = chat.self
        b = getattr(chat, 'other', random.choice(chat.members))
        if random.randrange(2) == 1:  # randomly swap a and b
            a, b = b, a
        return Substitutions({
            (a_0, a_1): a,
            (b_0, b_1): b,
        })

    def attach_message_properties(self, message: Message, reactions: bool,
                                  commands: bool,
                                  substitutions: bool) -> Message:
        reactions_val = self.build_reactions(message.chat) if reactions else {}
        commands_val = self.build_message_commands() if commands else None
        substitutions_val = self.build_substitutions(
            message.text, message.chat) if substitutions else None
        message.reactions = reactions_val
        message.commands = commands_val
        message.substitutions = substitutions_val
        return message

    def send_text_message(self,
                          chat: Chat,
                          author: Optional[ChatMember] = None,
                          target: Optional[Message] = None,
                          reactions: bool = False,
                          commands: bool = False,
                          substitution: bool = False,
                          unsupported: bool = False) -> Message:
        """Send a text message to master channel.
        Leave author blank to use “self” of the chat.

        Returns the message sent.
        """
        author = author or chat.self
        uid = f"__msg_id_{uuid4()}__"
        msg_type = MsgType.Unsupported if unsupported else MsgType.Text
        message = Message(
            chat=chat,
            author=author,
            type=msg_type,
            target=target,
            uid=uid,
            text=f"Content of {msg_type.name} message with ID {uid}",
            deliver_to=coordinator.master)
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)

        coordinator.send_message(message)
        self.messages_sent[uid] = message

        return message

    def edit_text_message(self,
                          message: Message,
                          reactions: bool = False,
                          commands: bool = False,
                          substitution: bool = False) -> Message:
        message.edit = True
        message.text = f"Edited {message.type.name} message {message.uid} @ {time.time_ns()}"
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)
        self.messages_sent[message.uid] = message
        coordinator.send_message(message)
        return message

    def send_link_message(self,
                          chat: Chat,
                          author: Optional[ChatMember] = None,
                          target: Optional[Message] = None,
                          reactions: bool = False,
                          commands: bool = False,
                          substitution: bool = False) -> Message:
        author = author or chat.self
        uid = f"__msg_id_{uuid4()}__"
        message = Message(chat=chat,
                          author=author,
                          type=MsgType.Link,
                          target=target,
                          uid=uid,
                          text=f"Content of link message with ID {uid}",
                          attributes=LinkAttribute(
                              title="EH Forwarder Bot",
                              description="EH Forwarder Bot project site.",
                              url="https://efb.1a23.studio"),
                          deliver_to=coordinator.master)
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)
        self.messages_sent[uid] = message
        coordinator.send_message(message)
        return message

    def edit_link_message(self,
                          message: Message,
                          reactions: bool = False,
                          commands: bool = False,
                          substitution: bool = False) -> Message:

        message.text = f"Content of edited link message with ID {message.uid}"
        message.edit = True
        message.attributes = LinkAttribute(
            title="EH Forwarder Bot (edited)",
            description="EH Forwarder Bot project site. (edited)",
            url="https://efb.1a23.studio/#edited")
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)
        self.messages_sent[message.uid] = message
        coordinator.send_message(message)
        return message

    def send_location_message(self,
                              chat: Chat,
                              author: Optional[ChatMember] = None,
                              target: Optional[Message] = None,
                              reactions: bool = False,
                              commands: bool = False,
                              substitution: bool = False) -> Message:
        author = author or chat.self
        uid = f"__msg_id_{uuid4()}__"
        message = Message(chat=chat,
                          author=author,
                          type=MsgType.Location,
                          target=target,
                          uid=uid,
                          text=f"Content of location message with ID {uid}",
                          attributes=LocationAttribute(
                              latitude=random.uniform(0.0, 90.0),
                              longitude=random.uniform(0.0, 90.0)),
                          deliver_to=coordinator.master)
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)
        self.messages_sent[uid] = message
        coordinator.send_message(message)
        return message

    def edit_location_message(self,
                              message: Message,
                              reactions: bool = False,
                              commands: bool = False,
                              substitution: bool = False) -> Message:
        message.text = f"Content of edited location message with ID {message.uid}"
        message.edit = True
        message.attributes = LocationAttribute(
            latitude=random.uniform(0.0, 90.0),
            longitude=random.uniform(0.0, 90.0))
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)
        self.messages_sent[message.uid] = message
        coordinator.send_message(message)
        return message

    def send_file_like_message(self,
                               msg_type: MsgType,
                               file_path: Path,
                               mime: str,
                               chat: Chat,
                               author: Optional[ChatMember] = None,
                               target: Optional[Message] = None,
                               reactions: bool = False,
                               commands: bool = False,
                               substitution: bool = False) -> Message:
        author = author or chat.self
        uid = f"__msg_id_{uuid4()}__"
        message = Message(
            chat=chat,
            author=author,
            type=msg_type,
            target=target,
            uid=uid,
            file=file_path.open('rb'),
            filename=file_path.name,
            path=file_path,
            mime=mime,
            text=f"Content of {msg_type.name} message with ID {uid}",
            deliver_to=coordinator.master)
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)
        self.messages_sent[uid] = message
        coordinator.send_message(message)
        return message

    def edit_file_like_message_text(self,
                                    message: Message,
                                    reactions: bool = False,
                                    commands: bool = False,
                                    substitution: bool = False) -> Message:
        message.text = f"Content of edited {message.type.name} message with ID {message.uid}"
        message.edit = True
        message.edit_media = False
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)
        self.messages_sent[message.uid] = message
        coordinator.send_message(message)
        return message

    def edit_file_like_message(self,
                               message: Message,
                               file_path: Path,
                               mime: str,
                               reactions: bool = False,
                               commands: bool = False,
                               substitution: bool = False) -> Message:
        message.text = f"Content of edited {message.type.name} media with ID {message.uid}"
        message.edit = True
        message.edit_media = True
        message.file = file_path.open('rb')
        message.filename = file_path.name
        message.path = file_path
        message.mime = mime
        message = self.attach_message_properties(message, reactions, commands,
                                                 substitution)
        self.messages_sent[message.uid] = message
        coordinator.send_message(message)
        return message

    def send_status_message(self,
                            status: StatusAttribute,
                            chat: Chat,
                            author: Optional[ChatMember] = None,
                            target: Optional[Message] = None) -> Message:
        """Send a status message to master channel.
        Leave author blank to use “self” of the chat.

        Returns the message sent.
        """
        author = author or chat.self
        uid = f"__msg_id_{uuid4()}__"
        message = Message(chat=chat,
                          author=author,
                          type=MsgType.Status,
                          target=target,
                          uid=uid,
                          text="",
                          attributes=status,
                          deliver_to=coordinator.master)

        coordinator.send_message(message)
        self.messages_sent[uid] = message

        return message

    # region [Message removal]

    def message_removal_status(self, status: MessageRemoval):
        if not self.message_removal_possible:
            raise EFBOperationNotSupported(
                "Message removal is not possible by flag.")

    @contextmanager
    def set_message_removal(self, value: bool):
        backup = self.message_removal_possible
        self.message_removal_possible = value
        try:
            yield
        finally:
            self.message_removal_possible = backup

    # endregion [Message removal]
    # region [Message reactions]

    # noinspection PyUnresolvedReferences
    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))

    @contextmanager
    def set_react_to_message(self, value: bool):
        """
        Set reaction response status.

        Args:
            value:
                "reject_one": Reject with EFBMessageReactionNotPossible.
                "reject_all": Reject with EFBOperationNotSupported.
        """
        backup = self.accept_message_reactions
        self.accept_message_reactions = value
        try:
            yield
        finally:
            self.accept_message_reactions = backup

    # endregion [Message reactions]
    # region [Chat/Member updates]

    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

    # noinspection PyUnresolvedReferences
    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

    # endregion [Chat/Member updates]

    def get_message_by_id(self, chat: 'Chat',
                          msg_id: MessageID) -> Optional['Message']:
        raise NotImplementedError
Esempio n. 13
0
class FilterMiddleware(Middleware):
    """
    Filter middleware. 
    A demo of advanced user interaction with master channel.
    """
    middleware_id: ModuleID = ModuleID("filter.FilterMiddleware")
    middleware_name: str = "Filter Middleware"
    __version__: str = '1.1.1'

    message_cache: Dict[MessageID, Message] = {}

    def __init__(self, instance_id: Optional[InstanceID] = None):
        super().__init__(instance_id)

        # load config
        self.yaml = YAML()
        conf_path = utils.get_config_path(self.middleware_id)
        if not conf_path.exists():
            conf_path.touch()
        self.config = self.yaml.load(conf_path)

        self.filters = [self.chat_id_based_filter]

        # Mapping
        self.FILTER_MAPPING = {
            "chat_name_contains": self.chat_name_contains_filter,
            "chat_name_matches": self.chat_name_matches_filter,
            "message_contains": self.message_contains_filter,
            "ews_mp": self.ews_mp_filter
        }

        # Chat ID based filter init
        shelve_path = str(
            utils.get_data_path(self.middleware_id) / "chat_id_filter.db")
        self.chat_id_filter_db = shelve.open(shelve_path)
        atexit.register(self.atexit)

        # load other filters
        if isinstance(self.config, Mapping):
            for i in self.config.keys():
                f = self.FILTER_MAPPING.get(i)
                if f:
                    self.filters.append(f)

    def atexit(self):
        self.chat_id_filter_db.close()

    def process_message(self, message: Message) -> Optional[Message]:
        # Only collect the message when it's a text message match the
        # hotword "filter`"
        if message.type == MsgType.Text and message.text == "filter`" and \
                message.deliver_to != coordinator.master:
            reply = self.make_status_message(message)
            self.message_cache[reply.uid] = message
            coordinator.master.send_message(reply)

            return None

        # Do not filter messages from master channel
        if message.deliver_to != coordinator.master:
            return message

        # Try to filter all other messages.
        return self.filter(message)

    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

    def toggle_filter_by_chat_id(self, mid: str, module_id: str, chat_id: str,
                                 value: bool):
        self.chat_id_filter_db[str((module_id, chat_id))] = value
        reply = self.make_status_message(mid=mid)
        reply.edit = True
        # Timer(0.5, coordinator.master.send_message, args=(reply,)).start()
        coordinator.master.send_message(reply)

        return None

    @staticmethod
    def get_chat_key(chat: Chat) -> str:
        return str((chat.module_id, chat.id))

    def process_status(self, status: Status) -> Optional[Status]:
        for i in self.filters:
            if i(status, False):
                return None
        return status

    def filter_reason(self, message: Message):
        for i in self.filters:
            reason = i(message, True)
            if reason is not False:
                return reason
        return False

    def filter(self, message: Message):
        for i in self.filters:
            if i(message, False):
                return None
        return message

    @staticmethod
    def get_chat_from_entity(entity: Union[Message, Status]) -> Optional[Chat]:
        if isinstance(entity, Message):
            return entity.chat
        elif isinstance(entity, MessageRemoval):
            return entity.message.chat
        elif isinstance(entity, ReactToMessage):
            return entity.chat
        elif isinstance(entity, MessageReactionsUpdate):
            return entity.chat
        else:
            return None

    # region [Filters]
    """
    Filters
    
    Filter must take only two argument apart from self
    - ``entity`` (``Union[Message, Status]``)
        The message entity to filter
    - ``reason`` (``bool``)
        Determine whether or not to return the reason to block a message
    
    To allow a message to be delivered, return ``False``.
    
    Otherwise, return ``True`` or a string to explain the reason of filtering
    if ``reason`` is ``True``.
    """

    @overload
    def chat_id_based_filter(self, entity: Union[Message, Status],
                             reason: Literal[True]) -> Union[bool, str]:
        ...

    @overload
    def chat_id_based_filter(self, entity: Union[Message, Status],
                             reason: Literal[False]) -> bool:
        ...

    def chat_id_based_filter(self, entity: Union[Message, Status],
                             reason: bool) -> Union[bool, str]:
        chat = self.get_chat_from_entity(entity)
        if not chat:
            return False
        if self.is_chat_filtered_by_id(chat):
            if reason:
                return "Chat is manually filtered."
            else:
                return True
        else:
            return False

    def is_chat_filtered_by_id(self, chat: Chat) -> bool:
        key = str((chat.module_id, chat.id))
        if key in self.chat_id_filter_db:
            return self.chat_id_filter_db[key]
        return False

    def chat_name_contains_filter(self, entity, reason):
        chat = self.get_chat_from_entity(entity)
        if not chat:
            return False
        for i in self.config['chat_name_contains']:
            if i in chat.display_name:
                if reason:
                    return "Chat is filtered because its name contains \"{}\".".format(
                        i)
                else:
                    return True
        return False

    def chat_name_matches_filter(self, entity, reason):
        chat = self.get_chat_from_entity(entity)
        if not chat:
            return False
        for i in self.config['chat_name_matches']:
            if i == chat.display_name:
                if reason:
                    return "Chat is filtered because its name matches \"{}\".".format(
                        i)
                else:
                    return True
        return False

    def message_contains_filter(self, entity, reason):
        if not isinstance(entity, Message):
            return False
        for i in self.config['message_contains']:
            if i in entity.text:
                if reason:
                    return "Message is filtered because its contains \"{}\".".format(
                        i)
                else:
                    return True
        return False

    def ews_mp_filter(self, entity, reason):
        chat = self.get_chat_from_entity(entity)
        if not chat:
            return False
        if chat.vendor_specific.get('is_mp'):
            if reason:
                return "Chat is filtered as it's a EWS \"WeChat Official Account\" chat."
            else:
                return True
        return False
Esempio n. 14
0
class MockMasterChannel(EFBChannel):

    channel_name: str = "Mock Master"
    channel_emoji: str = "➕"
    channel_id: ModuleID = ModuleID("tests.mocks.master.MockMasterChannel")
    channel_type: ChannelType = ChannelType.Master
    supported_message_types: Set[MsgType] = {MsgType.Text, MsgType.Link}
    __version__: str = '0.0.1'

    logger = getLogger(channel_id)

    polling = threading.Event()

    def poll(self):
        self.polling.wait()

    def send_status(self, status: EFBStatus):
        self.logger.debug("Received status: %r", status)

    def send_message(self, msg: EFBMsg) -> EFBMsg:
        self.logger.debug("Received message: %r", msg)
        return msg

    def stop_polling(self):
        self.polling.set()

    def send_text_msg(self):
        slave = next(iter(coordinator.slaves.values()))
        wonderland = slave.get_chat('wonderland001')
        msg = EFBMsg()
        msg.deliver_to = slave
        msg.chat = wonderland
        msg.author = EFBChat(self).self()
        msg.type = MsgType.Text
        msg.text = "Hello, world."
        return coordinator.send_message(msg)

    def send_link_msg(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.type = MsgType.Link
        msg.text = "Check it out."
        msg.attributes = EFBMsgLinkAttribute(
            title="Example",
            url="https://example.com"
        )
        return coordinator.send_message(msg)

    def send_location_msg(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.type = MsgType.Location
        msg.text = "I'm not here."
        msg.attributes = EFBMsgLocationAttribute(latitude=0.1, longitude=1.0)
        return coordinator.send_message(msg)

    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)

    def get_message_by_id(self, chat: EFBChat, msg_id: MessageID) -> Optional['EFBMsg']:
        pass

    def get_chats(self) -> List['EFBChat']:
        raise NotImplementedError()

    def get_chat(self, chat_uid: ChatID, member_uid: Optional[ChatID] = None) -> 'EFBChat':
        raise NotImplementedError()

    def get_chat_picture(self, chat: 'EFBChat') -> IO[bytes]:
        raise NotImplementedError()
Esempio n. 15
0
class MockSlaveChannel(SlaveChannel):
    channel_name: str = "Mock Slave"
    channel_emoji: str = "➖"
    channel_id: ModuleID = ModuleID("tests.mocks.slave.MockSlaveChannel")
    supported_message_types: Set[MsgType] = {MsgType.Text, MsgType.Link}
    __version__: str = '0.0.1'

    logger = getLogger(channel_id)

    polling = threading.Event()

    __picture_dict = {
        "alice": "A.png",
        "bob": "B.png",
        "carol": "C.png",
        "dave": "D.png",
        "wonderland001": "W.png"
    }

    def __init__(self, instance_id=None):
        super().__init__(instance_id)
        self.alice = PrivateChat(channel=self, name="Alice", uid="alice")
        self.bob = PrivateChat(channel=self,
                               name="Bob",
                               alias="Little bobby",
                               uid="bob")

        self.wonderland = GroupChat(channel=self,
                                    name="Wonderland",
                                    uid="wonderland001")
        self.wonderland.add_member(name="bob", alias="Bob James", uid="bob")
        self.carol = self.wonderland.add_member(name="Carol", uid="carol")
        self.dave = self.wonderland.add_member(
            name="デブ", uid="dave")  # Nah, that's a joke

        self.chats: List[Chat] = [self.alice, self.bob, self.wonderland]

    def poll(self):
        self.polling.wait()

    def send_status(self, status: Status):
        self.logger.debug("Received status: %r", status)

    def send_message(self, msg: Message) -> Message:
        self.logger.debug("Received message: %r", msg)
        return msg

    def stop_polling(self):
        self.polling.set()

    def get_chat(self, chat_uid: str) -> Chat:
        for i in self.chats:
            if chat_uid == i.uid:
                return i
        raise EFBChatNotFound()

    def get_chats(self) -> List[Chat]:
        return self.chats.copy()

    def get_chat_picture(self, chat: Chat):
        if chat.uid in self.__picture_dict:
            return open('tests/mocks/' + self.__picture_dict[chat.uid], 'rb')

    def get_message_by_id(self, chat: Chat,
                          msg_id: MessageID) -> Optional['Message']:
        pass

    @extra(name="Echo",
           desc="Echo back the input.\n"
           "Usage:\n"
           "    {function_name} text")
    def echo(self, args):
        return args

    @extra(name="Extra function A",
           desc="Do something A.\nUsage: {function_name}")
    def function_a(self):
        return f"Value of function A from {self.channel_id}."

    @extra(name="Extra function B",
           desc="Do something B.\nUsage: {function_name}")
    def function_b(self):
        return f"Value of function B from {self.channel_name}."
Esempio n. 16
0
class MockMasterChannel(MasterChannel):
    channel_name: str = "Mock Master"
    channel_emoji: str = "➕"
    channel_id: ModuleID = ModuleID("tests.mocks.master.MockMasterChannel")
    supported_message_types: Set[MsgType] = {MsgType.Text, MsgType.Link}
    __version__: str = '0.0.1'

    logger = getLogger(channel_id)

    polling = threading.Event()

    def poll(self):
        self.polling.wait()

    def send_status(self, status: Status):
        self.logger.debug("Received status: %r", status)

    def send_message(self, msg: Message) -> Message:
        self.logger.debug("Received message: %r", msg)
        return msg

    def stop_polling(self):
        self.polling.set()

    def send_text_msg(self):
        slave = next(iter(coordinator.slaves.values()))
        wonderland = slave.get_chat('wonderland001')
        msg = Message(
            deliver_to=slave,
            chat=wonderland,
            author=wonderland.self,
            type=MsgType.Text,
            text="Hello, world.",
        )
        return coordinator.send_message(msg)

    def send_link_msg(self):
        slave = next(iter(coordinator.slaves.values()))
        alice = slave.get_chat('alice')
        msg = Message(
            deliver_to=slave,
            chat=alice,
            author=alice.self,
            type=MsgType.Link,
            text="Check it out.",
            attributes=LinkAttribute(
                title="Example",
                url="https://example.com"
            )
        )
        return coordinator.send_message(msg)

    def send_location_msg(self):
        slave = next(iter(coordinator.slaves.values()))
        alice = slave.get_chat('alice')
        msg = Message(
            deliver_to=slave,
            chat=alice,
            author=alice.self,
            type=MsgType.Location,
            text="I'm not here.",
            attributes=LocationAttribute(latitude=0.1, longitude=1.0),
        )
        return coordinator.send_message(msg)

    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)

    def get_message_by_id(self, chat: Chat, msg_id: MessageID) -> Optional['Message']:
        pass
class TelegramChannel(SlaveChannel):
    channel_name = 'Telegram Slave'
    channel_emoji = '✈️'
    channel_id = ModuleID('sharzy.telegram')

    __version__ = __version__

    supported_message_types = {
        MsgType.Text, MsgType.Image, MsgType.File, MsgType.Sticker,
        MsgType.Video, MsgType.Audio
    }

    logger: logging.Logger = logging.getLogger(
        f'plugins.{channel_id}.TelegramChannel')

    config: Dict[str, Any] = dict()

    def __init__(self, instance_id: InstanceID = None):
        super().__init__(instance_id)
        self.load_config()
        self.loop = asyncio.get_event_loop()
        data_path = efb_utils.get_data_path(self.channel_id)
        proxy = (self.config['proxy']['protocol'],
                 self.config['proxy']['host'], self.config['proxy']['port'])
        self.client: TelegramClient = TelegramClient(
            f'{data_path}/{instance_id}',
            self.config['api_id'],
            self.config['api_hash'],
            loop=self.loop,
            proxy=proxy).start()
        self.get_chat_cache: Dict[str, ChatID] = {}
        self.task = None
        self._main_thread_name = threading.current_thread().name

    def load_config(self):
        config_path = efb_utils.get_config_path(self.channel_id)
        if not config_path.exists():
            return
        with config_path.open() as f:
            d = yaml.full_load(f)
            if not d:
                return
            self.config: Dict[str, Any] = d

    def make_efb_chat_obj(self, diag) -> Chat:
        if isinstance(diag.entity, TgUser):
            return PrivateChat(channel=self,
                               name=diag.name,
                               uid=str(diag.entity.id),
                               other_is_self=diag.entity.is_self)
        if isinstance(diag.entity, TgChat) or isinstance(
                diag.entity, TgChannel):
            return GroupChat(channel=self,
                             name=diag.name,
                             uid=str(diag.entity.id))

    async def async_get_chat(self, chat_uid: ChatID) -> Chat:
        cache = self.get_chat_cache.get(chat_uid, None)
        if cache:
            return cache
        else:
            chat = None
            async for diag in self.client.iter_dialogs():
                if int(chat_uid) == diag.entity.id:
                    chat = self.make_efb_chat_obj(diag)
            if chat is None:
                raise EFBChatNotFound()
            self.get_chat_cache[chat_uid] = chat
            return chat

    async def async_get_chats(self) -> Collection['Chat']:
        chats = []
        async for diag in self.client.iter_dialogs():
            chats.append(self.make_efb_chat_obj(diag))
        return chats

    def get_chats(self) -> Collection['Chat']:
        return self._async_run(self.async_get_chats())

    def get_chat(self, chat_uid: ChatID) -> 'Chat':
        return self._async_run(self.async_get_chat(chat_uid))

    def send_message(self, msg: 'EfbMsg') -> 'EfbMsg':
        if msg.file:
            print_color(
                f'{msg.text=}, {msg.uid=}, {msg.file.name}, {msg.file.fileno}')
        file = msg.file
        if hasattr(file, 'name'):
            # for file in file system, take its path as input
            # otherwise mimetype may be mistakenly guessed by telethon
            file = file.name
        self._async_run(
            self.client.send_message(int(msg.chat.uid), msg.text, file=file))
        return msg

    def poll(self):
        @self.client.on(events.NewMessage())
        async def handleMsg(event: NewMessage):
            # because telethon swallows exceptions in handlers,
            # we need to add extra exception handler
            try:
                await self.handle_new_telegram_message(event)
            except Exception as e:
                self.logger.error(e)
                raise e

        self.loop.run_forever()

    def send_status(self, status: 'Status'):
        pass

    def stop_polling(self):
        self.loop.call_soon_threadsafe(self.loop.stop)

    def get_message_by_id(self, chat: 'Chat',
                          msg_id: MessageID) -> Optional['Message']:
        raise EFBOperationNotSupported()

    def get_chat_picture(self, chat: 'Chat') -> BinaryIO:
        picture = io.BytesIO()
        chat_id = int(chat.uid)
        entity = self._async_run(self.client.get_entity(chat_id))
        self._async_run(self.client.download_profile_photo(entity, picture))
        return picture

    def _async_run(self, promise):
        # Warning: do not use this in async function
        if not self.loop.is_running():
            return self.loop.run_until_complete(promise)
        else:
            return asyncio.run_coroutine_threadsafe(promise,
                                                    self.loop).result()

    async def handle_new_telegram_message(self, event: NewMessage):
        msg: TgMsg = event.message
        chat_id = get_chat_id(msg.peer_id)
        chat = await self.async_get_chat(chat_id)
        self.logger.debug(msg)

        file = None
        path = None
        filename = None
        mime = None
        msg_type = MsgType.Text
        tempfile_suffix = ''
        if getattr(msg, 'media', None):
            media = msg.media
            tempfile_suffix = get_extension(media)
            if isinstance(media, MessageMediaPhoto):
                msg_type = MsgType.Image
            if isinstance(media, MessageMediaDocument):
                document = media.document
                mime = document.mime_type
                msg_type = MsgType.File
                for attr in document.attributes:
                    if isinstance(attr, DocumentAttributeFilename):
                        tempfile_suffix = attr.file_name
                        filename = attr.file_name
                    if isinstance(attr, DocumentAttributeSticker):
                        msg_type = MsgType.Sticker
                    if isinstance(attr, DocumentAttributeVideo):
                        msg_type = MsgType.Video
                    if isinstance(attr, DocumentAttributeAudio):
                        msg_type = MsgType.Audio
        if msg_type != MsgType.Text:
            file = tempfile.NamedTemporaryFile(suffix=tempfile_suffix)
            path = file.name
            await self.client.download_media(msg, file)

        msg_peer = await self.client.get_entity(
            get_chat_id(msg.from_id or msg.peer_id))
        self.logger.debug(msg_peer)

        chat_member = ChatMember(
            chat=chat,
            name=format_entity_name(msg_peer),
            uid=str(chat_id),
        )
        efb_msg = Message(
            deliver_to=coordinator.master,
            author=chat_member,
            chat=chat,
            text=msg.message,
            type=msg_type,
            uid=f'{chat_id}_{msg.id}',
            file=file,
            filename=filename,
            mime=mime,
            path=path,
        )
        coordinator.send_message(efb_msg)
class FBMessengerChannel(SlaveChannel):
    channel_name: str = "Facebook Messenger Slave"
    channel_emoji: str = "⚡️"
    channel_id: ModuleID = ModuleID("blueset.fbmessenger")
    __version__: str = __version__

    client: EFMSClient
    config: Dict[str, Any] = {}

    logger = logging.getLogger(channel_id)

    suggested_reactions = [
        MessageReaction.LOVE.value, MessageReaction.SMILE.value,
        MessageReaction.WOW.value, MessageReaction.SAD.value,
        MessageReaction.ANGRY.value, MessageReaction.YES.value,
        MessageReaction.NO.value
    ]
    supported_message_types = MasterMessageManager.supported_message_types

    # Translator
    translator = translation("efb_fb_messenger_slave",
                             resource_filename("efb_fb_messenger_slave",
                                               'locale'),
                             fallback=True)

    _: Callable = translator.gettext
    ngettext: Callable = translator.ngettext

    def __init__(self, instance_id: InstanceID = None):
        super().__init__(instance_id)
        session_path = efb_utils.get_data_path(
            self.channel_id) / "session.pickle"
        try:
            data = pickle.load(session_path.open('rb'))
            self.client = EFMSClient(self, None, None, session_cookies=data)
        except FileNotFoundError:
            raise EFBException(
                self._("Session not found, please authorize your account.\n"
                       "To do so, run: efms-auth"))
        except FBchatUserError as e:
            message = str(e) + "\n" + \
                      self._("You may need to re-authorize your account.\n" +
                             "To do so, run: efms-auth")
            raise EFBException(message)

        self.load_config()
        self.chat_manager: EFMSChatManager = EFMSChatManager(self)
        self.flag: ExperimentalFlagsManager = ExperimentalFlagsManager(self)
        self.master_message: MasterMessageManager = MasterMessageManager(self)
        self.extra_functions: ExtraFunctionsManager = ExtraFunctionsManager(
            self)

        # Initialize list of chat from server
        self.get_chats()

        # Monkey patching
        Thread.__eq__ = lambda a, b: a.uid == b.uid

    def load_config(self):
        """
        Load configuration from path specified by the framework.

        Configuration file is in YAML format.
        """
        config_path = efb_utils.get_config_path(self.channel_id)
        if not config_path.exists():
            self.config: Dict[str, Any] = dict()
            return
        with config_path.open() as f:
            self.config: Dict[str, Any] = yaml.full_load(f) or dict()

    def get_chats(self) -> List[Chat]:
        locations: Tuple[ThreadLocation, ...] = (ThreadLocation.INBOX, )
        if self.flag('show_pending_threads'):
            locations += (ThreadLocation.PENDING, ThreadLocation.OTHER)
        if self.flag('show_archived_threads'):
            locations += (ThreadLocation.ARCHIVED, )
        chats: List[Chat] = []
        for i in self.client.fetchThreadList(thread_location=locations):
            chats.append(self.chat_manager.build_and_cache_thread(i))
        loaded_chats = set(i.uid for i in chats)
        for i in self.client.fetchAllUsers():
            if i.uid not in loaded_chats:
                chats.append(self.chat_manager.build_and_cache_thread(i))
        return chats

    def get_chat(self, chat_uid: str) -> Chat:
        try:
            return self.chat_manager.get_thread(thread_id=chat_uid)
        except FBchatException:
            raise EFBChatNotFound

    def send_message(self, msg: Message) -> Message:
        return self.master_message.send_message(msg)

    def send_status(self, status: Status):
        if isinstance(status, MessageRemoval):
            uid: str = cast(str, status.message.uid)
            rfind = uid.rfind('.')
            if rfind > 0 and uid[rfind + 1:].isdecimal():
                uid = uid[:rfind]
            return self.client.unsend(uid)
        elif isinstance(status, ReactToMessage):
            try:
                self.client.reactToMessage(
                    status.msg_id, status.reaction
                    and MessageReaction(status.reaction))
            except (FBchatException, ValueError) as e:
                self.logger.error(f"Error occurred while sending status: {e}")
                raise EFBMessageReactionNotPossible(*e.args)
            return
        # Other status types go here
        raise EFBOperationNotSupported()

    def poll(self):
        self.client.listen(False)

    def stop_polling(self):
        self.client.listening = False

    def get_chat_picture(self, chat: Chat) -> BinaryIO:
        self.logger.debug("Getting picture of chat %s", chat)
        photo_url = chat.vendor_specific.get('profile_picture_url')
        self.logger.debug("[%s] has photo_url from cache: %s", chat.uid,
                          photo_url)
        if not photo_url:
            thread = self.client.get_thread_info(chat.uid)
            photo_url = efms_utils.get_value(
                thread, ('messaging_actor', 'big_image_src', 'uri'))
        self.logger.debug("[%s] has photo_url from GraphQL: %s", chat.uid,
                          photo_url)
        if not photo_url:
            thread = self.client.fetchThreadInfo(chat.uid)[chat.uid]
            photo_url = getattr(thread, 'photo', None)
        self.logger.debug("[%s] has photo_url from legacy API: %s", chat.uid,
                          photo_url)
        if not photo_url:
            raise EFBOperationNotSupported('This chat has no picture.')
        photo = BytesIO(requests.get(photo_url).content)
        photo.seek(0)
        return photo

    def get_message_by_id(self, chat: Chat,
                          msg_id: MessageID) -> Optional['Message']:
        index = None
        if msg_id.split('.')[-1].isdecimal():
            # is sub-message
            index = int(msg_id.split('.')[-1])
            msg_id = MessageID('.'.join(msg_id.split('.')[:-1]))

        thread_id, thread_type = self.client._getThread(chat.uid, None)
        message_info = self.client._forcedFetch(thread_id,
                                                msg_id).get("message")
        message = Message._from_graphql(message_info)

        efb_msg = self.client.build_efb_msg(msg_id, chat.uid, message.author,
                                            message)

        attachments = message_info.get('delta', {}).get('attachments', [])

        if attachments:
            attachment = attachments[index]
            self.client.attach_media(efb_msg, attachment)

        efb_msg.uid = msg_id

        return efb_msg

    # Additional features

    @extra(name=_("Show threads list"),
           desc=_("Usage:\n"
                  "    {function_name}"))
    def threads_list(self, args: str) -> str:
        return self.extra_functions.threads_list(args)

    @extra(name=_("Search for users"),
           desc=_("Show the first 10 results.\n"
                  "Usage:\n"
                  "    {function_name} keyword"))
    def search_users(self, args: str) -> str:
        return self.extra_functions.search_users(args)

    @extra(name=_("Search for groups"),
           desc=_("Show the first 10 results.\n"
                  "Usage:\n"
                  "    {function_name} keyword"))
    def search_groups(self, args: str) -> str:
        return self.extra_functions.search_groups(args)

    @extra(name=_("Search for pages"),
           desc=_("Show the first 10 results.\n"
                  "Usage:\n"
                  "    {function_name} keyword"))
    def search_pages(self, args: str) -> str:
        return self.extra_functions.search_pages(args)

    @extra(name=_("Search for threads"),
           desc=_("Show the first 10 results.\n"
                  "Usage:\n"
                  "    {function_name} keyword"))
    def search_threads(self, args: str) -> str:
        return self.extra_functions.search_threads(args)

    @extra(name=_("Add to group"),
           desc=_("Add members to a group.\n"
                  "Usage:\n"
                  "    {function_name} GroupID UserID [UserID ...]"))
    def add_to_group(self, args: str) -> str:
        return self.extra_functions.add_to_group(args)

    @extra(name=_("Remove from group"),
           desc=_("Remove members from a group.\n"
                  "Usage:\n"
                  "    {function_name} GroupID UserID [UserID ...]"))
    def remove_from_group(self, args: str) -> str:
        return self.extra_functions.remove_from_group(args)

    @extra(name=_("Change nickname"),
           desc=_("Change nickname of a user.\n"
                  "Usage:\n"
                  "    {function_name} UserID nickname"))
    def set_nickname(self, args: str) -> str:
        return self.extra_functions.set_nickname(args)

    @extra(name=_("Change group title"),
           desc=_("Change the title of a group.\n"
                  "Usage:\n"
                  "    {function_name} GroupID title"))
    def set_group_title(self, args: str) -> str:
        return self.extra_functions.set_group_title(args)

    @extra(name=_("Change chat emoji"),
           desc=_("Change the emoji of a chat.\n"
                  "Usage:\n"
                  "    {function_name} ChatID emoji"))
    def set_chat_emoji(self, args: str) -> str:
        return self.extra_functions.set_chat_emoji(args)

    @extra(name=_("Change member nickname"),
           desc=_("Change the nickname of a group member.\n"
                  "Usage:\n"
                  "    {function_name} GroupID MemberID nickname"))
    def set_member_nickname(self, args: str) -> str:
        return self.extra_functions.set_member_nickname(args)