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()
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()
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()
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)
def start(self): RTMClient.on(event='pong', callback=self.pong) RTMClient.on(event='message', callback=self.handle_message) self._client.start()
def add_callback(self, event_type: str, callback: Callable): self.callbacks[event_type].append(callback) RTMClient.on(event=event_type, callback=callback)
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)