class RTMClient(Singleton): @wraps(SlackRTMClient.__init__) def __init__(self, token, *args, **kwargs): super().__init__() self._client = SlackRTMClient(token=token, run_async=True, loop=asyncio.get_running_loop(), *args, **kwargs) old_os_name = os.name os.name = 'nt' self._client.start() os.name = old_os_name @classmethod @wraps(SlackRTMClient.__init__) def start(cls, token, *args, **kwargs): return cls.instantiate(token, *args, **kwargs)._client @classmethod async def stop(cls): await cls.instance()._client.async_stop() @classmethod def on(cls, event): def decorator(callback): @SlackRTMClient.run_on(event=event) @wraps(callback) async def wrapper(**payload): await callback(**payload) return wrapper return decorator
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()
def main(): flags.mark_flag_as_required('customer') CUSTOMER_NAME = FLAGS.customer customer_codes ,msg_queue = cfg.read_config() rtm_client = RTMClient(token=customer_codes['customer1']) print(f'logging {FLAGS.customer}:{customer_codes["customer1"]}') rtm_client.start()
def slack_bot(self): print('\033[0;32m' + "[!] Starting slack bot!\033[0m") try: self.slack_data['text'] = 'Ready for action!' requests.post(url='https://slack.com/api/chat.postMessage', data=self.slack_data) rtm_client = RTMClient(token=os.environ.get('SLACK_BOT_TOKEN')) rtm_client.start() except: pass
def main(): @RTMClient.run_on(event='message') def handle(**kwargs): data = kwargs['data'] text = data['text'] if data['user'] != __self_user_id and len(text) is not 0 and text.startswith('!'): command.runCommand(kwargs) rtm_client = RTMClient(token=__slack_token) rtm_client.start()
def start_work(retries=0): try: rtm_client = RTMClient(token=os.environ["SLACKBOT_API_TOKEN"], auto_reconnect=True) rtm_client.start() except Exception as e: print(f'main loop crash {e}, try to restart, attempt {retries}') if retries == 3: raise e time.sleep(retries * 15) start_work(retries + 1)
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()
def test_issue_530(self): try: rtm_client = RTMClient(token="I am not a token", run_async=False, loop=asyncio.new_event_loop()) rtm_client.start() self.fail("Raising an error here was expected") except Exception as e: self.assertEqual( "The request to the Slack API failed.\n" "The server responded with: {'ok': False, 'error': 'invalid_auth'}", str(e)) finally: if not rtm_client._stopped: rtm_client.stop()
class TestRTMClient(unittest.TestCase): """Runs integration tests with real Slack API https://github.com/slackapi/python-slackclient/issues/605 """ def setUp(self): self.logger = logging.getLogger(__name__) self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] self.channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID] self.rtm_client = RTMClient(token=self.bot_token, run_async=False) def tearDown(self): # Reset the decorators by @RTMClient.run_on RTMClient._callbacks = collections.defaultdict(list) @pytest.mark.skipif(condition=is_not_specified(), reason="still unfixed") def test_issue_605(self): self.text = "This message was sent to verify issue #605" self.called = False @RTMClient.run_on(event="message") def process_messages(**payload): self.logger.info(payload) self.called = True def connect(): self.logger.debug("Starting RTM Client...") self.rtm_client.start() t = threading.Thread(target=connect) t.setDaemon(True) try: t.start() self.assertFalse(self.called) time.sleep(3) self.web_client = WebClient( token=self.bot_token, run_async=False, loop=asyncio.new_event_loop( ), # TODO: this doesn't work without this ) new_message = self.web_client.chat_postMessage( channel=self.channel_id, text=self.text) self.assertFalse("error" in new_message) time.sleep(5) self.assertTrue(self.called) finally: t.join(.3)
def run_bot(): token = os.environ.get('SLACKBOT_TOKEN') report_url = os.environ.get('SLACKBOT_REPORT_URL') rtm_client = RTMClient(token=token) web_client = WebClient(token=token) global metrics metrics = None global channels channels = { u['id']: u['name'] for u in web_client.channels_list()['channels'] } global users users = {u['id']: u['name'] for u in web_client.users_list()['members']} if report_url: # Reports are enabled, so start reporting thread global last_run last_run = None metrics = defaultdict(int) class ReportingThread(threading.Thread): def run(self): while True: curr_min = datetime.utcnow().minute global last_run global metrics if last_run == curr_min: sleep(5) continue last_run = curr_min if not metrics: continue try: metrics = json.dumps(metrics) resp = post(url=report_url, json=dict(text=metrics)) resp.raise_for_status() except Exception as exc: print(exc) metrics = defaultdict(int) reporting = ReportingThread(name='Reporting Thread') reporting.start() while True: try: rtm_client.start() except Exception as exc: logging.error('Exception during rtm_client.start', exc)
def main(): """ Startup logic and the main application loop to monitor Slack events. """ # build the text model model = build_text_model() # Create the slackclient instance sc = WebClient(BOT_TOKEN) # check Slack API connection if not sc.rtm_connect(): raise Exception("Couldn't connect to slack.") @RTMClient.run_on(event='message') def echo_msg(**payload): nonlocal model # Get WebClient so you can communicate back to Slack sc = payload['web_client'] data = payload['data'] # use get() to avoid key missing error, esp from json (python dict) parsing user_id = data.get('user') channel_id = data.get('channel') message = data.get('text') # Since message event catches all messages sent to slack, including those from bot, # user_id and message are checked if user_id and message: print( f"receive message from user {user_id} in channel {channel_id}") if "parrot me" in message.lower(): markov_chain = model.make_sentence( ) or "I don't know what to say." sc.chat_postMessage(channel=channel_id, text=format_message(markov_chain)) if "level up parrot" in message.lower(): # Fetch new messages. If new ones are found, rebuild the text model if update_corpus(sc, channel_id) > 0: model = build_text_model() # Where the magic happens rtm_client = RTMClient(token=BOT_TOKEN) rtm_client.start()
def main(): @RTMClient.run_on(event='message') def handle(web_client=None, data=None, **kwargs): print(f'Message data: {data}') if __should_handle(user=data.get('user'), text=data.get('text')): handle_message(data, web_client) global __web_client __web_client = WebClient(token=__slack_token) __rtm_client = RTMClient(token=__slack_token) __rtm_future = None while True: try: __rtm_future = __rtm_client.start() print("Learning bot is connected!") break except Exception: print( "Failed to connect to Slack; retrying in 5 seconds\n\n\n\n\n" ) time.sleep(5) asyncio.gather(__rtm_future, schedule_monitor())
def run(): import asyncio rtm_client = RTMClient(token=os.environ["SLACK_API_TOKEN"], run_async=True) loop = asyncio.get_event_loop() loop.set_debug(True) loop.run_until_complete(rtm_client.start())
def runforever(self): from slack import RTMClient @RTMClient.run_on(event='message') def handle_message(**payload): if 'bot_id' in payload['data']: return message = SlackMessage(payload) for func in self.handlers: resp_text = func(message) if resp_text: if isinstance(resp_text, str): self.say(message.channel.id, resp_text) break rtm_client = RTMClient(token=self.token) rtm_client.start()
def test_02(self): logger = FoxylibLogger.func_level2logger(self.test_02, logging.DEBUG) loop = asyncio.get_event_loop() rtm_client = RTMClient(token=FoxylibSlack.xoxb_token(), run_async=True, loop=loop) async def inf_loop(): logger = logging.getLogger() while 1: try: logger.info("Ping Pong! I'm alive") await asyncio.sleep(900) except asyncio.CancelledError: break tasks = asyncio.gather(rtm_client.start(), inf_loop()) def callback(signum, frame): tasks.cancel() logger.warning("Cancelling tasks...") # loop.add_signal_handler(signal.SIGINT, callback) signal.signal(signal.SIGINT, callback) signal.signal(signal.SIGTERM, callback) try: loop.run_until_complete(tasks) except asyncio.CancelledError as e: logger.error(e) finally: logger.info("Quitting... Bye!")
async def slack_main(): loop = asyncio.get_event_loop() rtm_client = RTMClient(token=FoxylibSlack.xoxb_token(), run_async=True, loop=loop) executor = concurrent.futures.ThreadPoolExecutor(max_workers=1) await asyncio.gather( loop.run_in_executor(executor, partial(sync_loop, rtm_client)), rtm_client.start() )
def runMockMission(): #operator command testing #moonRanger.push_waypoint(20,20,2,"southern region") #moonRanger.push_waypoint(20,5,2,"out") #moonRanger.push_waypoint(0,0,2,"home region") #moonRanger.drive_trek() #moonRanger.map.printMap(moonRanger.map.driveRecord) #moonRanger.get_telemetry_log() #moonRanger.get_nss_data() #print(moonRanger.checkSolarEff()) #moonRanger.get_solar_status() #moonRanger.get_battery_status() #print(moonRanger.get_comm_status()) # SLACK_BOT_TOKEN must be set from terminal using 'export SLACK_BOT_TOKEN='token' # Start slack bot client rtm_client = RTMClient(token=os.environ["SLACK_BOT_TOKEN"]) rtm_client.start()
def main(): @RTMClient.run_on(event='message') def handle(**kwargs): try: text = kwargs['data']['text'] except KeyError: return except Exception as e: print(e) return if text: slackutils.CLIENT = kwargs['web_client'] cmd.EVAL(kwargs['data']) rtm_client = RTMClient(token=SLACK_TOKEN) rtm_client.start()
class SlackClient(object): token = None _client = None web_client: WebClient = None callbacks = defaultdict(list) def __init__(self, token: str): self.token = token self.web_client = WebClient(token=self.token) @property def client(self) -> RTMClient: assert self._client, "Client must be initialized first" return self._client @client.setter def client(self, client): self._client = client def connect(self): self.client = RTMClient(token=self.token) self.add_callback("open", self._on_open) 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) def emails_to_user_ids(self, emails: List[str]): for email in emails: user = self.web_client.users_lookupByEmail(email=email) print(user) def subscribe_to_presence(self, user_ids: List[str]): res = self.client.send_over_websocket(payload={ "type": "presence_sub", "ids": user_ids, }) def _on_open(self, **payload: dict): self.slack_team = payload["data"]["team"] self.slack_self = payload["data"]["self"] self.web_client = payload["web_client"]
class SlackBot(object): def __init__(self, name: str, pattern: str, remarks_location: str, **kwargs): self.name = name self.pattern = pattern self.remarks_location = remarks_location self.slack_id = None self._rtm_client = RTMClient(**kwargs) self.slack_client = WebClient(**kwargs) self.remarks = self.get_file_contents() def start(self): self._rtm_client.start() def process_message(self, **kwargs): data = kwargs.get("data") text = data.get("text") if text: match = re.match(self.pattern, text, re.I) if match is not None: channel_id = data.get("channel") thread_ts = data.get("ts") self.slack_client.chat_postMessage( channel=channel_id, thread_ts=thread_ts, text=self.get_random_response(), ) def get_file_contents(self) -> list: with open(self.remarks_location) as file: return file.readlines() def get_random_response(self) -> str: remarks_length = len(self.remarks) return self.remarks[random.randint(0, remarks_length - 1)]
class Client: _web_client: WebClient _rtm_client: RTMClient def __init__( self, *, token: str, ) -> None: self._webclient = WebClient( token=token, run_async=True, ) self._rtm_client = RTMClient( token=token, run_async=True, ) async def post_message( self, channel: str, text: str, ) -> Dict[str, Any]: log.info('Sending slack message to channel %s: %s', channel, text) return await self._webclient.chat_postMessage( channel=channel, text=text, icon_emoji='robot_face', ) def start_rtm_client(self) -> asyncio.Future: return self._rtm_client.start() def stop_rtm_client(self) -> None: return self._rtm_client.stop() def get_message_queue(self) -> asyncio.Queue: return _message_queue
professor_repr = ProfessorRepresentative(["language", "general"]) @RTMClient.run_on(event='message') def say_hello(**payload): global counter data = payload['data'] web_client = payload['web_client'] rtm_client = payload['rtm_client'] if 'text' in data and 'user' in data: channel_id = data['channel'] thread_ts = data['ts'] user = data['user'] text = data['text'] try: response = web_client.chat_postMessage( channel=channel_id, text=professor_repr.answer_short(text), #thread_ts=thread_ts ) except SlackApiError as e: # You will get a SlackApiError if "ok" is False #assert e.response["ok"] is False #assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' print(f"Got an error: {e.response['error']}") rtm_client = RTMClient(token=os.environ["SLACK_API_TOKEN"]) rtm_client.start()
class SlackBot(object): def __init__(self): logger.info("Starting up Slack Bot", format_opts=["header"]) basedir = os.path.dirname(os.path.realpath(__file__)) self.base_path = os.path.join(basedir, '..', '..') self.ready_event = threading.Event() self.stop_event = threading.Event() self.__bootstrap() def __bootstrap(self): logger.info("Loading configuration...") self.__verify_config() logger.debug("Initializing kv store session") self.db = DatabaseSession() logger.debug("Initializing context manager") self.contexts = ContextManager(self) logger.info("Loading Plugins...") self.plugins = PluginManager(self, os.path.join(self.base_path, 'plugins')) def __verify_config(self): self.name = config.get('bot_name') if not self.name: raise MissingBotName logger.debug(f"Using bot name: {self.name}") slack_token = config.get('slack_token') if not slack_token: raise MissingSlackToken logger.debug("Retieved slack token from configuration") self.admins = config.get('admins') or [] logger.debug(f"Configured admins: {self.admins}") def _get_users(self): api_call = self.client.api_call("users.list") if api_call.get('ok'): return api_call.get('members') def _get_my_id(self): for user in self.users: if 'name' in user and user.get('name') == self.name: return user.get('id') def _wait(self, interval): self.stop_event.wait(interval) def _handle_plugin_response(self, channel, response): if isinstance(response, str): self.send_channel_message(channel, response) elif isinstance(response, list): for item in response: self.send_channel_message(channel, item) else: logger.info(f"Invalid response from plugin: {response}") def _is_not_message(self, output): if output.get('text'): if output['text'].strip() != '': return False return True def _is_self_message(self, output): return output.get('user') and output.get('user') == self.bot_id def is_mention(self, text): return self.at_bot in text def shutdown(self): logger.info("Received shutdown signal...") self.stop_event.set() self.rtm_client.stop() def restart(self): self.shutdown() time.sleep(5) self.__bootstrap() self.start() def ready(self): return self.ready_event.is_set() def running(self): return not self.stop_event.is_set() def start(self): try: self.rtm_client = RTMClient(token=config.get('slack_token')) self.rtm_client.start() except KeyboardInterrupt: self.shutdown() def get_help_page(self, command): return self.plugins.get_help_page(command) def handle_hello(self, payload): logger.debug("Received 'hello' from slack rtm api") logger.debug("Setting slack web/rtm clients") self.client = payload.get('web_client') logger.debug("Retrieving user list") self.users = self._get_users() logger.debug("Gathering information about myself") self.bot_id = self._get_my_id() self.display_name = self.get_user_profile(self.bot_id)['name'] self.at_bot = "<@" + self.bot_id + ">" self.ready_event.set() logger.info("Initialization Complete!", format_opts=["green"]) def handle_message(self, payload): logger.debug(f"Handling slack message payload: {payload}") output = payload.get('data') if self._is_not_message(output) or self._is_self_message(output): return channel = output.get('channel') if not channel: return try: logger.debug("Looking up source user of event") user = self.get_user_profile(output['user']) logger.debug(f"User: {user}") except Exception as err: log.error(f"Failed to fetch user for event: {output}", err, sys.exc_info()) return text = output['text'] try: logger.info(f"<{channel}/{user['profile']['display_name']}>: {text}") cmd, words = self.plugins.get_cmd(text) if cmd: res = self.plugins.serve_cmd(channel, user, cmd, words) if res: self._handle_plugin_response(channel, res) return words = text.split() ctx = self.contexts.get_context(channel, user['id']) if ctx: if ctx.is_finished() or ctx.is_expired(): return with self.contexts.lock: res = self.plugins.serve_context(channel, user, ctx, words) if res: self._handle_plugin_response(channel, res) ctx.messages.append(words) return trigger = self.plugins.get_trigger(text) if trigger: res = self.plugins.serve_trigger(channel, user, trigger, words) if res: self._handle_plugin_response(channel, res) return if self.is_mention(text): text.replace(self.at_bot, "") res = self.plugins.serve_mention(channel, user, text.split()) if res: self._handle_plugin_response(channel, res) return logger.debug("No plugins matched the event") except Exception as err: logger.error("Failed processing message", err, sys.exc_info()) self._handle_plugin_response(channel, "An error has occurred and been logged accordingly") def sanitize_handle(self, handle): fixed = handle for char in ['@', '<', '>']: fixed = fixed.replace(char, '') return fixed def get_user_profile(self, user_id): found = None for user in self.users: try: if user['id'].upper() == user_id.upper(): found = user break except KeyError: pass return found def send_channel_message(self, channel, message, attachments=[], action=False): logger.debug( f"""Sending channel message: channel: {channel} message: {message} action: {action} attachments: {attachments} """ ) if not action: response = self.client.chat_postMessage( channel=channel, text=message, attachments=attachments, as_user=True ) else: response = self.client.chat_meMessage( channel=channel, text=message ) if not response.get('ok'): logger.info(str(response)) def send_channel_file(self, channel, title, filetype, content): logger.debug( f"""Uploading file to channel channel: {channel} filename: {title} filetype: {filetype} """ ) api_call = self.client.files_upload( channels=channel, title=title, filetype=filetype, content=content ) if api_call.get('ok'): return api_call.get('file') def register_loop(self, function, args=[], interval=10): logger.debug( f"""Registering plugin loop Function: {function} args: {args} interval: {interval} """ ) threading.Thread( target=self.run_plugin_loop, args=[function, interval, args], daemon=True ).start() def run_plugin_loop(self, function, interval, args=[]): self._wait(interval) while self.running(): try: if len(args) > 0: logger.debug(f"Firing {function} with args: {args}") function(*args) else: logger.debug(f"Firing {function}") function() self._wait(interval) except Exception as err: logger.error("Exception while running plugin loop", err, sys.exc_info()) self._wait(interval)
args = text.split(' ') if args[1] == 'match': match_command(payload, args[2:]) def match_command(payload: dict, args: list): data = payload['data'] web_client = payload['web_client'] if len(args) != 3: web_client.chat_postMessage( channel=data['channel'], text='Usage: <@UQWDVSDNH> [event ID] [match #] [team #]') return try: match_data = firebase.get_match_data(args[0], int(args[1]), int(args[2])) except: web_client.chat_postMessage( channel=data['channel'], text='Usage: <@UQWDVSDNH> [event ID] [match #] [team #]') return message = message_builder.match_data(match_data) web_client.chat_postMessage(channel=data['channel'], text=message, mrkdwn=True) print('client set') client.start()
CSM: " """ try: web_client.chat_postMessage( channel=channel_id, text=text, thread_ts=thread_ts, ) # You will get a SlackApiError if "ok" is False except SlackApiError as exc: assert exc.response["ok"] is False assert exc.response["error"] LOG.exception(f'Got an error: {exc.response["error"]}') # Watch for messages from users with trigger @RTMClient.run_on(event="message") def get_info(**payload): """Process channel messages.""" data = payload["data"] web_client = payload["web_client"] if "#whois" in data.get("text", []): try: process_whois(data=data, web_client=web_client) except Exception: LOG.exception(f"Exception in process_whois!!!") RTM_CLIENT = RTMClient(token=os.getenv("SLACK_TOKEN")) RTM_CLIENT.start()
class TestFoxylibSlackFunction: @classmethod def setUpClass(cls): FoxylibLogger.attach_stderr2loggers(logging.DEBUG) async def mock_server(self): app = web.Application() app["websockets"] = [] app.router.add_get("/", self.websocket_handler) app.on_shutdown.append(self.on_shutdown) runner = web.AppRunner(app) await runner.setup() self.site = web.TCPSite(runner, "localhost", 8765) await self.site.start() async def websocket_handler(self, request): ws = web.WebSocketResponse() await ws.prepare(request) request.app["websockets"].append(ws) try: async for msg in ws: await ws.send_json({ "type": "message", "message_sent": msg.json() }) finally: request.app["websockets"].remove(ws) return ws async def on_shutdown(self, app): for ws in set(app["websockets"]): await ws.close(code=WSCloseCode.GOING_AWAY, message="Server shutdown") def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) task = asyncio.ensure_future(self.mock_server(), loop=self.loop) self.loop.run_until_complete(asyncio.wait_for(task, 0.1)) self.client = RTMClient(token=FoxylibSlack.xoxb_token(), loop=self.loop, auto_reconnect=False) def test_01(self): channel = FoxylibChannel.V.FOXYLIB @RTMClient.run_on(event="open") async def typing_message(**payload): rtm_client = payload["rtm_client"] await rtm_client.typing(channel=channel) @RTMClient.run_on(event="message") def check_message(**payload): message = {"id": 1, "type": "typing", "channel": channel} rtm_client = payload["rtm_client"] self.assertDictEqual(payload["data"]["message_sent"], message) rtm_client.stop() self.client.start() def test_04(self): @RTMClient.run_on(event="open") async def typing_message(**payload): web_client = payload["web_client"] channel = FoxylibChannel.V.FOXYLIB filepath = os.path.join(FILE_DIR, "test_01.txt") await FilesUploadMethod.invoke(web_client, channel, filepath) await rtm_client.typing(channel="C01234567") @RTMClient.run_on(event=FileSharedEvent.NAME) def on_file_shared(**payload): message = {"id": 1, "type": "typing", "channel": "C01234567"} rtm_client = payload["rtm_client"] self.assertDictEqual(payload["data"]["message_sent"], message) rtm_client.stop() FoxylibSlack.rtm_client().start() raise Exception() 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 slack_setup(): SlackNotification.update_feature() if SlackNotification.SLACK_NOTIFICATION_ENABLED: rtm_client = RTMClient(token=os.environ["SLACK_API_TOKEN"]) rtm_client.start()
class SlackRTMBackend(ErrBot): @staticmethod def _unpickle_identifier(identifier_str): return SlackRTMBackend.__build_identifier(identifier_str) @staticmethod def _pickle_identifier(identifier): return SlackRTMBackend._unpickle_identifier, (str(identifier), ) def _register_identifiers_pickling(self): """ Register identifiers pickling. As Slack needs live objects in its identifiers, we need to override their pickling behavior. But for the unpickling to work we need to use bot.build_identifier, hence the bot parameter here. But then we also need bot for the unpickling so we save it here at module level. """ SlackRTMBackend.__build_identifier = self.build_identifier for cls in (SlackPerson, SlackRoomOccupant, SlackRoom): copyreg.pickle( cls, SlackRTMBackend._pickle_identifier, SlackRTMBackend._unpickle_identifier, ) def __init__(self, config): super().__init__(config) identity = config.BOT_IDENTITY self.token = identity.get("token", None) self.proxies = identity.get("proxies", None) if not self.token: log.fatal( 'You need to set your token (found under "Bot Integration" on Slack) in ' "the BOT_IDENTITY setting in your configuration. Without this token I " "cannot connect to Slack.") sys.exit(1) self.sc = None # Will be initialized in serve_once self.webclient = None self.bot_identifier = None compact = config.COMPACT_OUTPUT if hasattr(config, "COMPACT_OUTPUT") else False self.md = slack_markdown_converter(compact) self._register_identifiers_pickling() def update_alternate_prefixes(self): """Converts BOT_ALT_PREFIXES to use the slack ID instead of name Slack only acknowledges direct callouts `@username` in chat if referred by using the ID of that user. """ # convert BOT_ALT_PREFIXES to a list try: bot_prefixes = self.bot_config.BOT_ALT_PREFIXES.split(",") except AttributeError: bot_prefixes = list(self.bot_config.BOT_ALT_PREFIXES) converted_prefixes = [] for prefix in bot_prefixes: try: converted_prefixes.append( f"<@{self.username_to_userid(prefix)}>") except Exception as e: log.error( 'Failed to look up Slack userid for alternate prefix "%s": %s', prefix, e, ) self.bot_alt_prefixes = tuple( x.lower() for x in self.bot_config.BOT_ALT_PREFIXES) log.debug("Converted bot_alt_prefixes: %s", self.bot_config.BOT_ALT_PREFIXES) def _setup_slack_callbacks(self): @RTMClient.run_on(event="message") def serve_messages(**payload): self._message_event_handler(payload["web_client"], payload["data"]) @RTMClient.run_on(event="member_joined_channel") def serve_joins(**payload): self._member_joined_channel_event_handler(payload["web_client"], payload["data"]) @RTMClient.run_on(event="hello") def serve_hellos(**payload): self._hello_event_handler(payload["web_client"], payload["data"]) @RTMClient.run_on(event="presence_change") def serve_presences(**payload): self._presence_change_event_handler(payload["web_client"], payload["data"]) def serve_forever(self): self.sc = RTMClient(token=self.token, proxy=self.proxies) @RTMClient.run_on(event="open") def get_bot_identity(**payload): self.bot_identifier = SlackPerson(payload["web_client"], payload["data"]["self"]["id"]) # only hook up the message callback once we have our identity set. self._setup_slack_callbacks() # log.info('Verifying authentication token') # self.auth = self.api_call("auth.test", raise_errors=False) # if not self.auth['ok']: # raise SlackAPIResponseError(error=f"Couldn't authenticate with Slack. Server said: {self.auth['error']}") # log.debug("Token accepted") log.info("Connecting to Slack real-time-messaging API") self.sc.start() # Inject bot identity to alternative prefixes self.update_alternate_prefixes() try: while True: sleep(1) except KeyboardInterrupt: log.info("Interrupt received, shutting down..") return True except Exception: log.exception("Error reading from RTM stream:") finally: log.debug("Triggering disconnect callback") self.disconnect_callback() def _hello_event_handler(self, webclient: WebClient, event): """Event handler for the 'hello' event""" self.webclient = webclient self.connect_callback() self.callback_presence( Presence(identifier=self.bot_identifier, status=ONLINE)) def _presence_change_event_handler(self, webclient: WebClient, event): """Event handler for the 'presence_change' event""" idd = SlackPerson(webclient, event["user"]) presence = event["presence"] # According to https://api.slack.com/docs/presence, presence can # only be one of 'active' and 'away' if presence == "active": status = ONLINE elif presence == "away": status = AWAY else: log.error( f"It appears the Slack API changed, I received an unknown presence type {presence}." ) status = ONLINE self.callback_presence(Presence(identifier=idd, status=status)) def _message_event_handler(self, webclient: WebClient, event): """Event handler for the 'message' event""" channel = event["channel"] if channel[0] not in "CGD": log.warning("Unknown message type! Unable to handle %s", channel) return subtype = event.get("subtype", None) if subtype in ("message_deleted", "channel_topic", "message_replied"): log.debug("Message of type %s, ignoring this event", subtype) return if subtype == "message_changed" and "attachments" in event["message"]: # If you paste a link into Slack, it does a call-out to grab details # from it so it can display this in the chatroom. These show up as # message_changed events with an 'attachments' key in the embedded # message. We should completely ignore these events otherwise we # could end up processing bot commands twice (user issues a command # containing a link, it gets processed, then Slack triggers the # message_changed event and we end up processing it again as a new # message. This is not what we want). log.debug( "Ignoring message_changed event with attachments, likely caused " "by Slack auto-expanding a link") return text = event["text"] text, mentioned = self.process_mentions(text) text = self.sanitize_uris(text) log.debug("Saw an event: %s", pprint.pformat(event)) log.debug("Escaped IDs event text: %s", text) msg = Message( text, extras={ "attachments": event.get("attachments"), "slack_event": event, }, ) if channel.startswith("D"): if subtype == "bot_message": msg.frm = SlackBot( webclient, bot_id=event.get("bot_id"), bot_username=event.get("username", ""), ) else: msg.frm = SlackPerson(webclient, event["user"], event["channel"]) msg.to = SlackPerson(webclient, self.bot_identifier.userid, event["channel"]) channel_link_name = event["channel"] else: if subtype == "bot_message": msg.frm = SlackRoomBot( webclient, bot_id=event.get("bot_id"), bot_username=event.get("username", ""), channelid=event["channel"], bot=self, ) else: msg.frm = SlackRoomOccupant(webclient, event["user"], event["channel"], bot=self) msg.to = SlackRoom(webclient=webclient, channelid=event["channel"], bot=self) channel_link_name = msg.to.name # TODO: port to slackclient2 # msg.extras['url'] = f'https://{self.sc.server.domain}.slack.com/archives/' \ # f'{channel_link_name}/p{self._ts_for_message(msg).replace(".", "")}' self.callback_message(msg) if mentioned: self.callback_mention(msg, mentioned) def _member_joined_channel_event_handler(self, webclient: WebClient, event): """Event handler for the 'member_joined_channel' event""" user = SlackPerson(webclient, event["user"]) if user == self.bot_identifier: user = self.bot_identifier self.callback_room_joined( SlackRoom(webclient=webclient, channelid=event["channel"], bot=self), user) def userid_to_username(self, id_: str): """Convert a Slack user ID to their user name""" user = self.webclient.users_info(user=id_)["user"] if user is None: raise UserDoesNotExistError(f"Cannot find user with ID {id_}.") return user["name"] def username_to_userid(self, name: str): """Convert a Slack user name to their user ID""" name = name.lstrip("@") user = [ user for user in self.webclient.users_list()["members"] if user["name"] == name ] if user == []: raise UserDoesNotExistError(f"Cannot find user {name}.") if len(user) > 1: log.error( "Failed to uniquely identify '{}'. Errbot found the following users: {}" .format( name, " ".join( ["{}={}".format(u["name"], u["id"]) for u in user]))) raise UserNotUniqueError(f"Failed to uniquely identify {name}.") return user[0]["id"] def channelid_to_channelname(self, id_: str): """Convert a Slack channel ID to its channel name""" channel = self.webclient.conversations_info(channel=id_)["channel"] if channel is None: raise RoomDoesNotExistError(f"No channel with ID {id_} exists.") return channel["name"] def channelname_to_channelid(self, name: str): """Convert a Slack channel name to its channel ID""" name = name.lstrip("#") channel = [ channel for channel in self.webclient.channels_list() if channel.name == name ] if not channel: raise RoomDoesNotExistError(f"No channel named {name} exists") return channel[0].id def channels(self, exclude_archived=True, joined_only=False): """ Get all channels and groups and return information about them. :param exclude_archived: Exclude archived channels/groups :param joined_only: Filter out channels the bot hasn't joined :returns: A list of channel (https://api.slack.com/types/channel) and group (https://api.slack.com/types/group) types. See also: * https://api.slack.com/methods/channels.list * https://api.slack.com/methods/groups.list """ response = self.webclient.channels_list( exclude_archived=exclude_archived) channels = [ channel for channel in response["channels"] if channel["is_member"] or not joined_only ] response = self.webclient.groups_list( exclude_archived=exclude_archived) # No need to filter for 'is_member' in this next call (it doesn't # (even exist) because leaving a group means you have to get invited # back again by somebody else. groups = [group for group in response["groups"]] return channels + groups @lru_cache(1024) def get_im_channel(self, id_): """Open a direct message channel to a user""" try: response = self.webclient.im_open(user=id_) return response["channel"]["id"] except SlackAPIResponseError as e: if e.error == "cannot_dm_bot": log.info("Tried to DM a bot.") return None else: raise e def _prepare_message(self, msg): # or card """ Translates the common part of messaging for Slack. :param msg: the message you want to extract the Slack concept from. :return: a tuple to user human readable, the channel id """ if msg.is_group: to_channel_id = msg.to.id to_humanreadable = (msg.to.name if msg.to.name else self.channelid_to_channelname(to_channel_id)) else: to_humanreadable = msg.to.username to_channel_id = msg.to.channelid if to_channel_id.startswith("C"): log.debug( "This is a divert to private message, sending it directly to the user." ) to_channel_id = self.get_im_channel( self.username_to_userid(msg.to.username)) return to_humanreadable, to_channel_id def send_message(self, msg): super().send_message(msg) if msg.parent is not None: # we are asked to reply to a specify thread. try: msg.extras["thread_ts"] = self._ts_for_message(msg.parent) except KeyError: # Gives to the user a more interesting explanation if we cannot find a ts from the parent. log.exception( "The provided parent message is not a Slack message " "or does not contain a Slack timestamp.") to_humanreadable = "<unknown>" try: if msg.is_group: to_channel_id = msg.to.id to_humanreadable = ( msg.to.name if msg.to.name else self.channelid_to_channelname(to_channel_id)) else: to_humanreadable = msg.to.username if isinstance( msg.to, RoomOccupant ): # private to a room occupant -> this is a divert to private ! log.debug( "This is a divert to private message, sending it directly to the user." ) to_channel_id = self.get_im_channel( self.username_to_userid(msg.to.username)) else: to_channel_id = msg.to.channelid msgtype = "direct" if msg.is_direct else "channel" log.debug( "Sending %s message to %s (%s).", msgtype, to_humanreadable, to_channel_id, ) body = self.md.convert(msg.body) log.debug("Message size: %d.", len(body)) limit = min(self.bot_config.MESSAGE_SIZE_LIMIT, SLACK_MESSAGE_LIMIT) parts = self.prepare_message_body(body, limit) timestamps = [] for part in parts: data = { "channel": to_channel_id, "text": part, "unfurl_media": "true", "link_names": "1", "as_user": "******", } # Keep the thread_ts to answer to the same thread. if "thread_ts" in msg.extras: data["thread_ts"] = msg.extras["thread_ts"] result = self.webclient.chat_postMessage(**data) timestamps.append(result["ts"]) msg.extras["ts"] = timestamps except Exception: log.exception( f"An exception occurred while trying to send the following message " f"to {to_humanreadable}: {msg.body}.") def _slack_upload(self, stream: Stream) -> None: """ Performs an upload defined in a stream :param stream: Stream object :return: None """ try: stream.accept() resp = self.webclient.files_upload( channels=stream.identifier.channelid, filename=stream.name, file=stream) if "ok" in resp and resp["ok"]: stream.success() else: stream.error() except Exception: log.exception( f"Upload of {stream.name} to {stream.identifier.channelname} failed." ) def send_stream_request( self, user: Identifier, fsource: BinaryIO, name: str = None, size: int = None, stream_type: str = None, ) -> Stream: """ Starts a file transfer. For Slack, the size and stream_type are unsupported :param user: is the identifier of the person you want to send it to. :param fsource: is a file object you want to send. :param name: is an optional filename for it. :param size: not supported in Slack backend :param stream_type: not supported in Slack backend :return Stream: object on which you can monitor the progress of it. """ stream = Stream(user, fsource, name, size, stream_type) log.debug( "Requesting upload of %s to %s (size hint: %d, stream type: %s).", name, user.channelname, size, stream_type, ) self.thread_pool.apply_async(self._slack_upload, (stream, )) return stream def send_card(self, card: Card): if isinstance(card.to, RoomOccupant): card.to = card.to.room to_humanreadable, to_channel_id = self._prepare_message(card) attachment = {} if card.summary: attachment["pretext"] = card.summary if card.title: attachment["title"] = card.title if card.link: attachment["title_link"] = card.link if card.image: attachment["image_url"] = card.image if card.thumbnail: attachment["thumb_url"] = card.thumbnail if card.color: attachment["color"] = (COLORS[card.color] if card.color in COLORS else card.color) if card.fields: attachment["fields"] = [{ "title": key, "value": value, "short": True } for key, value in card.fields] limit = min(self.bot_config.MESSAGE_SIZE_LIMIT, SLACK_MESSAGE_LIMIT) parts = self.prepare_message_body(card.body, limit) part_count = len(parts) footer = attachment.get("footer", "") for i in range(part_count): if part_count > 1: attachment["footer"] = f"{footer} [{i + 1}/{part_count}]" attachment["text"] = parts[i] data = { "channel": to_channel_id, "attachments": json.dumps([attachment]), "link_names": "1", "as_user": "******", } try: log.debug("Sending data:\n%s", data) self.webclient.chat_postMessage(**data) except Exception: log.exception( f"An exception occurred while trying to send a card to {to_humanreadable}.[{card}]" ) def __hash__(self): return 0 # this is a singleton anyway def change_presence(self, status: str = ONLINE, message: str = "") -> None: self.webclient.users_setPresence( presence="auto" if status == ONLINE else "away") @staticmethod def prepare_message_body(body, size_limit): """ Returns the parts of a message chunked and ready for sending. This is a staticmethod for easier testing. Args: body (str) size_limit (int): chunk the body into sizes capped at this maximum Returns: [str] """ fixed_format = body.startswith("```") # hack to fix the formatting parts = list(split_string_after(body, size_limit)) if len(parts) == 1: # If we've got an open fixed block, close it out if parts[0].count("```") % 2 != 0: parts[0] += "\n```\n" else: for i, part in enumerate(parts): starts_with_code = part.startswith("```") # If we're continuing a fixed block from the last part if fixed_format and not starts_with_code: parts[i] = "```\n" + part # If we've got an open fixed block, close it out if part.count("```") % 2 != 0: parts[i] += "\n```\n" return parts @staticmethod def extract_identifiers_from_string(text): """ Parse a string for Slack user/channel IDs. Supports strings with the following formats:: <#C12345> <@U12345> <@U12345|user> @user #channel/user #channel Returns the tuple (username, userid, channelname, channelid). Some elements may come back as None. """ exception_message = ( "Unparseable slack identifier, should be of the format `<#C12345>`, `<@U12345>`, " "`<@U12345|user>`, `@user`, `#channel/user` or `#channel`. (Got `%s`)" ) text = text.strip() if text == "": raise ValueError(exception_message % "") channelname = None username = None channelid = None userid = None if text[0] == "<" and text[-1] == ">": exception_message = ( "Unparseable slack ID, should start with U, B, C, G, D or W (got `%s`)" ) text = text[2:-1] if text == "": raise ValueError(exception_message % "") if text[0] in ("U", "B", "W"): if "|" in text: userid, username = text.split("|") else: userid = text elif text[0] in ("C", "G", "D"): channelid = text else: raise ValueError(exception_message % text) elif text[0] == "@": username = text[1:] elif text[0] == "#": plainrep = text[1:] if "/" in text: channelname, username = plainrep.split("/", 1) else: channelname = plainrep else: raise ValueError(exception_message % text) return username, userid, channelname, channelid def build_identifier(self, txtrep): """ Build a :class:`SlackIdentifier` from the given string txtrep. Supports strings with the formats accepted by :func:`~extract_identifiers_from_string`. """ log.debug("building an identifier from %s.", txtrep) username, userid, channelname, channelid = self.extract_identifiers_from_string( txtrep) if userid is None and username is not None: userid = self.username_to_userid(username) if channelid is None and channelname is not None: channelid = self.channelname_to_channelid(channelname) if userid is not None and channelid is not None: return SlackRoomOccupant(self.webclient, userid, channelid, bot=self) if userid is not None: return SlackPerson(self.webclient, userid, self.get_im_channel(userid)) if channelid is not None: return SlackRoom(webclient=self.webclient, channelid=channelid, bot=self) raise Exception( "You found a bug. I expected at least one of userid, channelid, username or channelname " "to be resolved but none of them were. This shouldn't happen so, please file a bug." ) def is_from_self(self, msg: Message) -> bool: return self.bot_identifier.userid == msg.frm.userid def build_reply(self, msg, text=None, private=False, threaded=False): response = self.build_message(text) if "thread_ts" in msg.extras["slack_event"]: # If we reply to a threaded message, keep it in the thread. response.extras["thread_ts"] = msg.extras["slack_event"][ "thread_ts"] elif threaded: # otherwise check if we should start a new thread response.parent = msg response.frm = self.bot_identifier if private: response.to = msg.frm else: response.to = msg.frm.room if isinstance(msg.frm, RoomOccupant) else msg.frm return response def add_reaction(self, msg: Message, reaction: str) -> None: """ Add the specified reaction to the Message if you haven't already. :param msg: A Message. :param reaction: A str giving an emoji, without colons before and after. :raises: ValueError if the emoji doesn't exist. """ return self._react("reactions.add", msg, reaction) def remove_reaction(self, msg: Message, reaction: str) -> None: """ Remove the specified reaction from the Message if it is currently there. :param msg: A Message. :param reaction: A str giving an emoji, without colons before and after. :raises: ValueError if the emoji doesn't exist. """ return self._react("reactions.remove", msg, reaction) def _react(self, method: str, msg: Message, reaction: str) -> None: try: # this logic is from send_message if msg.is_group: to_channel_id = msg.to.id else: to_channel_id = msg.to.channelid ts = self._ts_for_message(msg) self.api_call( method, data={ "channel": to_channel_id, "timestamp": ts, "name": reaction }, ) except SlackAPIResponseError as e: if e.error == "invalid_name": raise ValueError(e.error, "No such emoji", reaction) elif e.error in ("no_reaction", "already_reacted"): # This is common if a message was edited after you reacted to it, and you reacted to it again. # Chances are you don't care about this. If you do, call api_call() directly. pass else: raise SlackAPIResponseError(error=e.error) def _ts_for_message(self, msg): try: return msg.extras["slack_event"]["message"]["ts"] except KeyError: return msg.extras["slack_event"]["ts"] def shutdown(self): super().shutdown() @property def mode(self): return "slack" def query_room(self, room): """ Room can either be a name or a channelid """ if room.startswith("C") or room.startswith("G"): return SlackRoom(webclient=self.webclient, channelid=room, bot=self) m = SLACK_CLIENT_CHANNEL_HYPERLINK.match(room) if m is not None: return SlackRoom(webclient=self.webclient, channelid=m.groupdict()["id"], bot=self) return SlackRoom(webclient=self.webclient, name=room, bot=self) def rooms(self): """ Return a list of rooms the bot is currently in. :returns: A list of :class:`~SlackRoom` instances. """ channels = self.channels(joined_only=True, exclude_archived=True) return [ SlackRoom(webclient=self.webclient, channelid=channel["id"], bot=self) for channel in channels ] def prefix_groupchat_reply(self, message, identifier): super().prefix_groupchat_reply(message, identifier) message.body = f"@{identifier.nick}: {message.body}" @staticmethod def sanitize_uris(text): """ Sanitizes URI's present within a slack message. e.g. <mailto:[email protected]|[email protected]>, <http://example.org|example.org> <http://example.org> :returns: string """ text = re.sub(r"<([^|>]+)\|([^|>]+)>", r"\2", text) text = re.sub(r"<(http([^>]+))>", r"\1", text) return text def process_mentions(self, text): """ Process mentions in a given string :returns: A formatted string of the original message and a list of :class:`~SlackPerson` instances. """ mentioned = [] m = re.findall("<@[^>]*>*", text) for word in m: try: identifier = self.build_identifier(word) except Exception as e: log.debug( "Tried to build an identifier from '%s' but got exception: %s", word, e, ) continue # We only track mentions of persons. if isinstance(identifier, SlackPerson): log.debug("Someone mentioned") mentioned.append(identifier) text = text.replace(word, str(identifier)) return text, mentioned
class ChatClient(BaseChatClient): ''' A wrapper around the Slack API designed for Securitybot. ''' # username: str, token: str, icon_url: str=None) -> None: def __init__(self, connection_config) -> None: ''' Constructs the Slack API object using the bot's username, a Slack token, and a URL to what the bot's profile pic should be. ''' self._username = connection_config['username'] self._icon_url = connection_config['icon_url'] self.reporting_channel = connection_config['reporting_channel'] self.messages = [] self._token = connection_config['token'] self._slack_web = WebClient(self._token) self._validate() loop = asyncio.new_event_loop() thread = threading.Thread(target=self.connect, args=(loop, )) thread.start() def _validate(self) -> None: '''Validates Slack API connection.''' response = self._slack_web.api_test() if not response['ok']: raise ChatException('Unable to connect to Slack API.') logging.info('Connection to Slack API successful!') logging.debug('Checking reporting channel is valid') try: response = self._slack_web.conversations_info( channel=self.reporting_channel, ) logging.info("Using channel '{}' ({}) for notifications.".format( response['channel']['name'], response['channel']['id'])) except Exception as error: raise ChatException('Configured reporting channel {} invalid.\n' '{}'.format(error, self.reporting_channel)) def connect(self, loop): asyncio.set_event_loop(loop) ssl_context = ssl_lib.create_default_context(cafile=certifi.where()) self._slack_rtm = RTMClient(token=self._token, ssl=ssl_context, run_async=True, loop=loop) self._slack_rtm.run_on(event="message")(self.get_message) loop.run_until_complete(self._slack_rtm.start()) def get_users(self) -> List[Dict[str, Any]]: ''' Returns a list of all users in the chat system. Returns: A list of dictionaries, each dictionary representing a user. The rest of the bot expects the following minimal format: { "name": The username of a user, "id": A user's unique ID in the chat system, "profile": A dictionary representing a user with at least: { "first_name": A user's first name } } ''' return self._slack_web.users_list()['members'] def get_messages(self): messages = self.messages self.messages = [] return messages async def get_message(self, **payload): ''' Gets a list of all new messages received by the bot in direct messaging channels. That is, this function ignores all messages posted in group chats as the bot never interacts with those. Each message should have the following format, minimally: { "user": The unique ID of the user who sent a message. "text": The text of the received message. } ''' data = payload["data"] if 'user' in data and data['channel'].startswith('D'): message = {} message['user'] = data['user'] message['text'] = data['text'] self.messages.append(message) def send_message(self, channel: Any, message: str) -> None: ''' Sends some message to a desired channel. As channels are possibly chat-system specific, this function has a horrible type signature. ''' self._slack_web.chat_postMessage(channel=channel, text=message, username=self._username, as_user=False, icon_url=self._icon_url) def message_user(self, user: User, message: str = None): ''' Sends some message to a desired user, using a User object and a string message. ''' channel = self._slack_web.conversations_open( users=[user['id']])['channel']['id'] self.send_message(channel, message)
def init(): client = RTMClient(token=os.environ.get('SLACK_API_TOKEN')) client.start()