Exemplo n.º 1
0
 def _register_plugin_actions(self, plugin_class, metadata, cls_instance,
                              fn_name, fn, class_help):
     fq_fn_name = "{}.{}".format(plugin_class, fn_name)
     if fn.__doc__:
         self._help['human'][class_help][
             fq_fn_name] = self._parse_human_help(fn.__doc__)
     for action, config in metadata['plugin_actions'].items():
         if action == 'process':
             event_type = config['event_type']
             RTMClient.on(event=event_type,
                          callback=callable_with_sanitized_event(fn))
         if action == 'respond_to' or action == 'listen_to':
             for regex in config['regex']:
                 event_handler = {
                     'class': cls_instance,
                     'class_name': plugin_class,
                     'function': fn,
                     'regex': regex
                 }
                 key = "{}-{}".format(fq_fn_name, regex.pattern)
                 self._plugin_actions[action][key] = event_handler
                 self._help['robot'][class_help].append(
                     self._parse_robot_help(regex, action))
         if action == 'schedule':
             Scheduler.get_instance().add_job(fq_fn_name,
                                              trigger='cron',
                                              args=[cls_instance],
                                              id=fq_fn_name,
                                              replace_existing=True,
                                              **config)
         if action == 'route':
             for route_config in config:
                 bottle.route(**route_config)(fn)
    async def test_issue_611(self):
        channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID]
        text = "This message was sent by <https://slack.dev/python-slackclient/|python-slackclient>! (test_issue_611)"

        self.message_count, self.reaction_count = 0, 0

        async def process_messages(**payload):
            self.logger.info(payload)
            if "subtype" in payload["data"] and payload["data"][
                    "subtype"] == "message_replied":
                return  # skip

            self.message_count += 1
            raise Exception("something is wrong!"
                            )  # This causes the termination of the process

        async def process_reactions(**payload):
            self.logger.info(payload)
            self.reaction_count += 1

        rtm = RTMClient(token=self.bot_token, run_async=True)
        RTMClient.on(event='message', callback=process_messages)
        RTMClient.on(event='reaction_added', callback=process_reactions)

        web_client = WebClient(token=self.bot_token, run_async=True)
        message = await web_client.chat_postMessage(channel=channel_id,
                                                    text=text)
        ts = message["ts"]

        await asyncio.sleep(3)

        # intentionally not waiting here
        rtm.start()

        try:
            await asyncio.sleep(3)

            first_reaction = await web_client.reactions_add(channel=channel_id,
                                                            timestamp=ts,
                                                            name="eyes")
            self.assertFalse("error" in first_reaction)
            await asyncio.sleep(2)

            should_be_ignored = await web_client.chat_postMessage(
                channel=channel_id, text="Hello?", thread_ts=ts)
            self.assertFalse("error" in should_be_ignored)
            await asyncio.sleep(2)

            second_reaction = await web_client.reactions_add(
                channel=channel_id, timestamp=ts, name="tada")
            self.assertFalse("error" in second_reaction)
            await asyncio.sleep(2)

            self.assertEqual(self.message_count, 1)
            self.assertEqual(self.reaction_count, 2)
        finally:
            if not rtm._stopped:
                rtm.stop()
Exemplo n.º 3
0
    async def test_issue_558(self):
        channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID]
        text = "This message was sent by <https://slack.dev/python-slackclient/|python-slackclient>! (test_issue_558)"

        self.message_count, self.reaction_count = 0, 0

        async def process_messages(**payload):
            self.logger.debug(payload)
            self.message_count += 1
            await asyncio.sleep(10)  # this used to block all other handlers

        async def process_reactions(**payload):
            self.logger.debug(payload)
            self.reaction_count += 1

        rtm = RTMClient(token=self.bot_token, run_async=True)
        RTMClient.on(event='message', callback=process_messages)
        RTMClient.on(event='reaction_added', callback=process_reactions)

        web_client = WebClient(token=self.bot_token, run_async=True)
        message = await web_client.chat_postMessage(channel=channel_id,
                                                    text=text)
        self.assertFalse("error" in message)
        ts = message["ts"]
        await asyncio.sleep(3)

        # intentionally not waiting here
        rtm.start()
        await asyncio.sleep(3)

        try:
            first_reaction = await web_client.reactions_add(channel=channel_id,
                                                            timestamp=ts,
                                                            name="eyes")
            self.assertFalse("error" in first_reaction)
            await asyncio.sleep(2)

            message = await web_client.chat_postMessage(channel=channel_id,
                                                        text=text)
            self.assertFalse("error" in message)
            # used to start blocking here

            # This reaction_add event won't be handled due to a bug
            second_reaction = await web_client.reactions_add(
                channel=channel_id, timestamp=ts, name="tada")
            self.assertFalse("error" in second_reaction)
            await asyncio.sleep(2)

            self.assertEqual(self.message_count, 1)
            self.assertEqual(self.reaction_count, 2)  # used to fail
        finally:
            if not rtm._stopped:
                rtm.stop()
Exemplo n.º 4
0
class BddRTMClient:

    event_trail = []

    def __init__(self):
        slack_token = os.environ["SLACK_BDD_API_TOKEN"]
        self.rtm_client = RTMClient(token=slack_token)

        self.rtm_client.on(event="message", callback=self.listen)
        self.rtm_thread = threading.Thread(name="rtm_client",
                                           target=self.rtm_client.start)
        self.rtm_thread.start()

    def listen(self, **payload):
        self.event_trail.append(payload["data"])

    def stop(self):
        self.rtm_client.stop()
Exemplo n.º 5
0
    def test_05(self):
        def on_file_shared(**kwargs):
            j_event = kwargs.get("data")
            j_file_list = FileSharedEvent.j_event2j_file_list(j_event)
            self.assertEqual(len(j_file_list), 1)

            j_file = l_singleton2obj(j_file_list)
            filename = SlackFile.j_file2filename(j_file)
            self.assertEqual(filename, "test_01.txt")

            raise Exception()

        RTMClient.on(event="file_shared", callback=on_file_shared)

        rtm_client = FoxylibSlack.rtm_client()
        rtm_client.start()

        # p = Process(target=rtm_client.start)
        # p.start()

        web_client = FoxylibSlack.web_client()
        channel = FoxylibChannel.V.FOXYLIB
        filepath = os.path.join(FILE_DIR, "test_01.txt")
        response = FilesUploadMethod.invoke(web_client, channel, filepath)
Exemplo n.º 6
0
 def start(self):
     RTMClient.on(event='pong', callback=self.pong)
     RTMClient.on(event='message', callback=self.handle_message)
     self._client.start()
Exemplo n.º 7
0
 def add_callback(self, event_type: str, callback: Callable):
     self.callbacks[event_type].append(callback)
     RTMClient.on(event=event_type, callback=callback)
Exemplo n.º 8
0
class MessageDispatcher:
    """ Message entry point from slack chat. Reply to direct command send to the
   bot
  """
    def __init__(self, token, channel, cache):
        """
    Constructor
    :param token: Authentification token for bot
    :param channel: Name of the channel where the bot is hosted
    :param cache:   Location where to cache data
    """
        #  Bot mention detection
        self._self_mention = None
        self._channel = channel
        self._bot_id = None

        # Commands
        self._keywords = []
        self._authors = []
        self._known_cmd = {
            'help': (self._help_callback, ''),
            'list_keywords': (self._list_keyords_callback, ''),
            'add_keywords':
            (self._add_keyords_callback, 'List of space separated keywords '
             'to add'),
            'run_daily_arxiv_search': (self._run_daily_arxiv_search, '')
        }

        # Arxiv wrapper
        self._cache_folder = cache
        self._arxiv_cfg = _join(self._cache_folder, 'arxiv.cfg')
        if not _exists(self._arxiv_cfg):
            # cs.CV: Compute Vision
            # cs.AI: Artificial Inteligence
            # cs.LG: Machine learning
            # stat.ML: Machine learning
            # cs.GR: Graphics
            self._arxiv = ArxivParser(
                category=['cs.CV', 'cs.AI', 'cs.LG', 'stat.ML', 'cs.GR'])
            self._arxiv.save_config(self._arxiv_cfg)
        else:
            self._arxiv = ArxivParser.from_config(self._arxiv_cfg)
        # Reload authors/keywords
        self._load_config(self._cache_folder)
        #  Create client, define message callback + start service
        # run aynchronously
        # https://github.com/slackapi/python-slackclient/blob/master/tutorial/PythOnBoardingBot/async_app.py
        # https://stackoverflow.com/questions/56539228
        # Start Slack client + scheduler for daily research
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        self.client = RTMClient(token=token, run_async=True, loop=loop)
        self.client.on(event='open', callback=self.open_callback)
        self.client.on(event='message', callback=self.message_callback)
        loop.run_until_complete(
            asyncio.gather(self._daily_scheduler(token), self.client.start()))
        loop.close()

    def __del__(self):
        self._save_config(self._cache_folder)

    def _save_config(self, filename):
        """
    Save configuration
    :param filename:  Path where to dump data
    """
        with open(_join(filename, 'bot.cfg'), 'w') as f:
            cfg = {'keywords': self._keywords, 'authors': self._authors}
            dump(cfg, f)

    def _load_config(self, filename):
        """
    Reload configuration
    :param filename:  Path where configuration is stored
    """
        fname = _join(filename, 'bot.cfg')
        if _exists(fname):
            with open(fname, 'r') as f:
                cfg = load(f)
                self._keywords = cfg['keywords']
                self._authors = cfg['authors']

    async def _daily_scheduler(self, token):
        """
    Post request to trigger daily search
    :param token: Slack bot's token
    """

        # Create scheduled function
        def _start_daily_search():
            """
      Trigger daily search by posting message to Slack Bot
      :param client: Slack webclient to post messages
      """
            cmd = '<@{}> run_daily_arxiv_search'.format(self._bot_id)
            self.client._web_client.chat_postMessage(channel=self._channel,
                                                     text=cmd)

        # Add callback to scheduler
        schedule.every().day.at('09:00').do(_start_daily_search)
        # Start loop
        while True:
            schedule.run_pending()  # Run pending job
            await asyncio.sleep(5)  # Sleep for 5'

    def open_callback(self, **payload):
        """
    Call back invoked when bot is started
    :param payload: Payload
    """
        web_client = payload.get('web_client', None)
        data = payload.get('data', None)
        if self._bot_id is None:
            self._bot_id = data['self']['id']
            self._initialize_self_mention(client=web_client)
            # Retrive channels info
            res = web_client.conversations_list()
            channels = res['channels']
            host_chan = self._channel[1:] if self._channel[
                0] == '#' else self._channel
            for c in channels:
                if c['name'] == host_chan:
                    web_client.chat_postMessage(channel=c['id'],
                                                text='PaperBot is now online.')
                    break

    def message_callback(self, **payload):
        """
    Callback invoked when message is send to the channel
    :param payload: Message payload
    """
        cmd = BotCommand.FromText(search=self._self_mention, payload=payload)
        if cmd.cmd:
            cb = self._known_cmd.get(cmd.cmd, None)
            if cb is not None:
                cb[0](cmd=cmd)
            else:
                self._boilerplate_callback(cmd=cmd)

    def _initialize_self_mention(self, client):
        """
    Initialize self mention detection
    :param client:  Web client passed to through the message
    """

        #  Build regex to detect when bot is mentionned in the message and reach to
        #  the command.
        #  Retrieve bot ID first
        r = client.auth_test()
        if r['ok']:
            bot_id = r['user_id']
        else:
            #  Something went wrong
            raise RuntimeError('Could not retrive bot ID: {}'.format(
                r['error']))

        # Bot should react at: @<BotID> <command> but not to <...> @<BotID> <...>
        # self._self_mention = _re_compile(r"^<@{}>".format(bot_id))
        self._self_mention = _re_compile(
            r"^(<@\w*>) (\w*) ?(.*)".format(bot_id))

    def _boilerplate_callback(self, cmd):
        """
    End point when unknown commands are entered
    :param cmd: Command
    """

        # Format reply
        if cmd.user is None:
            msg = 'Unrecognized command, '
        else:
            msg = 'Sorry <@{}>, the command is *unrecognized*, '.format(
                cmd.user)
        msg += 'here is a list of all _known_ commands:\n'
        for k in self._known_cmd.keys():
            msg += '• {}\n'.format(k)

        # Insert into blocks in order to have markdown formatting
        blocks = {'type': 'section', 'text': {'type': 'mrkdwn', 'text': msg}}
        # Post on chat
        client = cmd.client
        client.chat_postMessage(channel=cmd.channel, blocks=[blocks])

    def _help_callback(self, cmd):
        """
    Help callback, list all known commands
    :param cmd: Command
    """
        if cmd.user is None:
            msg = 'Here is a list of all recognized commands:\n'
        else:
            msg = 'Hi <@{}>, here is a list of all recognized '\
                  'commands\n'.format(cmd.user)
        for k, v in self._known_cmd.items():
            line = ('• {}\n'.format(k)
                    if v[1] == '' else '• {}    {}\n'.format(k, v[1]))
            msg += line
        # Insert into blocks in order to have markdown formatting
        blocks = {'type': 'section', 'text': {'type': 'mrkdwn', 'text': msg}}
        cmd.client.chat_postMessage(channel=cmd.channel, blocks=[blocks])

    def _add_keyords_callback(self, cmd):
        """
    Add new keyword
    :param cmd: Command
    """
        new_kw = cmd.args.split(' ')
        for kw in new_kw:
            kw = kw.lower()
            if kw not in self._keywords:
                self._keywords.append(kw)
        # Save
        self._save_config(self._cache_folder)
        # User feedback
        msg = 'Added following keywords: {}'.format(new_kw)
        cmd.client.chat_postMessage(channel=cmd.channel, text=msg)

    def _list_keyords_callback(self, cmd):
        """
    List all keywords
    :param cmd: Command
    """
        msg = 'List of _keywords_ of interest:\n'
        for kw in self._keywords:
            msg += '• {}\n'.format(kw)
        # Insert into blocks in order to have markdown formatting
        blocks = {'type': 'section', 'text': {'type': 'mrkdwn', 'text': msg}}
        cmd.client.chat_postMessage(channel=cmd.channel, blocks=[blocks])

    def _run_daily_arxiv_search(self, cmd):
        """
    Run daily arxiv search for new papers
    :param cmd: Command
    """
        articles = self._arxiv.run_daily_search(self._keywords)
        # Format output similar to slack's block kit template
        # Header
        msg = 'Found *{} papers* on Arxiv, {}'.format(
            len(articles),
            datetime.today().strftime('%Y-%m-%d'))
        blocks = [
            {
                'type': 'section',
                'text': {
                    'type': 'mrkdwn',
                    'text': msg
                }
            },
        ]
        # Body
        for k, art in enumerate(articles):
            # Create body content
            authors = ', '.join(art.authors)
            msg = '[{}/{}] *<{}|{}>*\n_*Author(s)*:_ {}\n_{}_'.format(
                k + 1, len(articles), art.link, art.title, authors,
                art.summary)
            bloc = [{
                'type': 'divider'
            }, {
                'type': 'section',
                'text': {
                    'type': 'mrkdwn',
                    'text': msg
                }
            }]
            blocks.extend(bloc)

        for k in range(0, len(blocks), 50):
            # k=0, 50, 100, ...
            start = k
            stop = k + 50
            bck = blocks[start:stop]
            # Post batch of blocks since there is a limit of 50 blocks per layout
            # See: https://api.slack.com/reference/block-kit/blocks
            cmd.client.chat_postMessage(channel=cmd.channel, blocks=bck)