class Bot: def __init__(self, config): self.s = SlackSocket(config['slack'].get('token')) # Is this a good way to handle mongo connection? Seems weird. # TODO check for connection timeout self.mongo = mongoengine self.mongo.connect(config['mongo'].get('db'), host=config['mongo'].get('host'), port=config['mongo'].getint('port')) def start(self): for event in self.s.events(): if event.type == 'message': # TODO Pass when message has subtype, eg. message_changed. # Maybe do this in a nicer way. Use event.event (dict)? if 'subtype' in event.json: pass else: event_json = json.loads(event.json) message = Message(user=event_json['user'], channel=event_json['channel'], text=event_json['text'].strip(), timestamp=datetime.datetime.utcnow()) message.save() def quit(self): self.s.close() self.mongo.connection.disconnect() sys.exit(0)
def start(self): socket = SlackSocket(config_provider.token, translate=True) for event in socket.events(): message = MessageWrapper(json.loads(event.json)) if message.type == "message": pykka.ActorRegistry.broadcast(message.__dict__)
class SlackBot(ChatBot): """ params: - slack_token(str): """ def __init__(self, slack_token, redis_host, redis_port): print('Starting Slackbot') self.slacksocket = SlackSocket(slack_token, event_filters=['message']) self.me = self.slacksocket.user print('Connected to Slack as %s' % self.me) super().__init__(redis_host, redis_port) @property def messages(self): for event in self.slacksocket.events(): log.debug('saw event %s' % event.json) if self.me in event.mentions: yield self._parse(event) def reply(self, msg, channel): # skip any empty messages if not msg or msg == 'EOF': return # make codeblock if message is multiline if isinstance(msg, list): msg = '```' + '\n'.join(msg) + '```' else: msg = '`' + msg + '`' self.slacksocket.send_msg(msg, channel_name=channel, confirm=False) log.debug('sent "%s" to "%s"' % (msg, channel)) @staticmethod def _parse(event): """ Parse slack event, removing @ mention """ words = event.event['text'].split(' ') words.pop(0) return (' '.join(words), event.event['user'], event.event['channel'])
class SlackInterface: MESSAGES = { 'hello_world': ( "Hello, world. I'm posting this to @channel in response to {user}."), } # Get any sequence of word-characters, possibly including the @ before them to indicate a username, # OR, # Get ANYTHING that starts and ends with ``. TOKENIZER_RE = re.compile(r'(?:@?\w+|`[^`]*`)') FAKE_PM_CHANNEL_NAME = '___private_message___' def __init__(self, responder=None): assert isinstance(responder, Responder) self.responder = responder self.slack = slacker.Slacker(SLACK_TOKEN) self.socket = SlackSocket(SLACK_TOKEN, translate=False) self.user_id_to_user_name = {} self.chan_id_to_chan_name = {} self.user_id_to_im_chan_id = {} self._update_cache() def _update_cache(self): self.user_id_to_user_name = { u['id']:u['name'] for u in self.slack.users.list().body['members']} self.chan_id_to_chan_name = { c['id']:c['name'] for c in self.slack.channels.list().body['channels']} self.chan_id_to_chan_name.update({ g['id']:g['name'] for g in self.slack.groups.list().body['groups']}) self.user_id_to_im_chan_id = { i['user']:i['id'] for i in self.slack.im.list().body['ims']} def _parse_message(self, e): if e.type not in ('message',) or e.event['user'] == BOT_USER_ID: return None # Extract and preprocess text. text = e.event['text'] for u_id, u_name in self.user_id_to_user_name.iteritems(): text = text.replace("<@{}>".format(u_id), "@{}".format(u_name)) tokenized = self.TOKENIZER_RE.findall(text.lower()) # Extract user and channel names from cache. u_name = self._get_user_name(e.event['user']) chan_name, is_im = self._get_channel_name_and_type(e.event['channel']) # if we aren't called out by name and this isn't a direct message to us, # we absolutely do not care if '@{}'.format(BOT_USER_NAME) not in tokenized and not is_im: return print (text, u_name, chan_name, is_im) return Message( text=text, tokenized=tokenized, user_id=e.event['user'], user_name=u_name, chan_id=e.event['channel'], chan_name=chan_name, im=is_im) def _get_channel_name_and_type(self, chan_id): # Check channel_id and im_channel caches. for loop in (True, False): if chan_id in self.chan_id_to_chan_name: return self.chan_id_to_chan_name[chan_id], False elif chan_id in self.user_id_to_im_chan_id.values(): return self.FAKE_PM_CHANNEL_NAME, True if loop: self._update_cache() raise ValueError, "Could not find channel_id." def _get_user_name(self, user_id): for loop in (True, False): if user_id in self.user_id_to_user_name: return self.user_id_to_user_name[user_id] if loop: self._update_cache() raise ValueError, "Could not find user id." def _send_response(self, response): assert isinstance(response, Response) message = response.text.replace("@channel", "<!channel|@channel>") if not message: return channel = response.chan_id if not channel and response.im: if not response.im in self.user_id_to_im_chan_id: self._update_cache() channel = self.user_id_to_im_chan_id[response.im] if not channel: raise StandardError, "No channel, explicit or actual, in response {}".format(response) self.slack.chat.post_message(channel, message, as_user=True) def listen(self): print "Happy birthday!" for e in self.socket.events(): # Convert socket event to Message object, if possible. try: msg = self._parse_message(e) except StandardError, err: print err continue if not msg: continue responses = self.responder.respond_to_message(msg) if not responses: continue if not isinstance(responses, (set, list, tuple)): responses = [responses] for response in responses: try: self._send_response(response) except StandardError, err: print err continue
class SimpleSlackBot: """Simplifies interacting with the Slack API. Allows users to register functions to specific events, get those functions called when those specific events are triggered and run their business code """ def __init__(self, debug=False): """Initializes our Slack bot and slack bot token. Will exit if the required environment variable is not set. """ self._SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN") if self._SLACK_BOT_TOKEN is None: sys.exit("ERROR: environment variable SLACK_BOT_TOKEN is not set") self._slacker = Slacker(self._SLACK_BOT_TOKEN) self._slackSocket = SlackSocket(self._SLACK_BOT_TOKEN, translate=False) self._BOT_ID = self._slacker.auth.test().body["user_id"] self._registrations = { } # our dictionary of event_types to a list of callbacks if debug: print("DEBUG!") logger.removeHandler(null_handler) logger.addHandler(StreamHandler()) logger.setLevel(logging.DEBUG) logger.info( f"set bot id to {self._BOT_ID} with name {self.helper_user_id_to_user_name(self._BOT_ID)}" ) logger.info("initialized") def register(self, event_type): """Registers a callback function to a a event type. All supported even types are defined here https://api.slack.com/events-api """ def function_wrapper(callback): logger.info( f"registering callback {callback.__name__} to event type {event_type}" ) if event_type not in self._registrations: self._registrations[event_type] = [] # create an empty list self._registrations[event_type].append(callback) return function_wrapper def route_request_to_callbacks(self, request): """Routes the request to the correct notify """ logger.info( f"received an event of type {request.type} and slack event {request._slack_event.event}" ) if request.type in self._registrations: for callback in self._registrations[request.type]: callback(request) def listen(self): """Listens forever for Slack events, triggering appropriately callbacks when respective events are received """ READ_WEBSOCKET_DELAY = 1 # 1 second delay between reading from firehose logger.info("began listening!") for slack_event in self._slackSocket.events(): if slack_event: if slack_event.event and "bot_id" not in slack_event.event: # We don't reply to bots request = SlackRequest(self._slacker, slack_event) self.route_request_to_callbacks(request) time.sleep(READ_WEBSOCKET_DELAY) logger.info("Keyboard interrupt received. Gracefully shutting down") sys.exit(0) def start(self): """Connect the Slack bot to the chatroom and begin listening """ ok = self._slacker.rtm.start().body["ok"] if ok: logger.info("started!") self.listen() else: logger.error( "Connection failed. Are you connected to the internet? Potentially invalid Slack token? " "Check environment variable and \"SLACK_BOT_TOKEN\"") def get_slacker(self): """Returns SimpleSlackBot's SlackClient. This is useful if you are writing a more advanced bot and want complete access to all SlackClient has to offer. """ return self._slacker def get_slack_socket(self): """Returns SimpleSlackBot's SlackSocket. This is useful if you are writing a more advanced bot and want complete access to all SlackSocket has to offer. """ return self._slackSocket def helper_get_public_channel_ids(self): """Helper function that gets all public channel ids """ public_channel_ids = [] public_channels = self._slacker.channels.list().body["channels"] for channel in public_channels: public_channel_ids.append(channel["id"]) if len(public_channel_ids) == 0: logger.warning("got no public channel ids") else: logger.debug(f"got public channel ids {public_channel_ids}") return public_channel_ids def helper_get_private_channel_ids(self): """Helper function that gets all private channel ids """ private_channel_ids = [] private_channels = self._slacker.groups.list().body["groups"] for private_channel in private_channels: private_channels.append(private_channel["id"]) if len(private_channel_ids) == 0: logger.warning("got no private channel ids") else: logger.debug(f"got private channel ids {private_channel_ids}") return private_channel_ids def helper_get_user_ids(self): """Helper function that gets all user ids """ user_ids = [] users = self._slacker.users.list().body["members"] for user in users: user_ids.append(user["id"]) if len(user_ids) == 0: logger.warning("got no user ids") else: logger.debug(f"got user ids {user_ids}") return user_ids def helper_get_user_names(self): """Helper function that gets all user names """ user_names = [] users = self._slacker.users.list().body["members"] for user in users: user_names.append(user["name"]) if len(user_names) == 0: logger.warning("got no user names") else: logger.debug(f"got user names {user_names}") return user_names def helper_get_users_in_channel(self, channel_id): """Helper function that gets all users in a given channel id """ user_ids = [] channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list["channels"]: if channel["id"] == channel_id: for user_id in channel["members"]: user_ids.append(user_id) if len(user_ids) == 0: logger.warning(f"got no user ids for channel {channel_id}") else: logger.debug(f"got user ids {user_ids}") return user_ids def helper_public_channel_name_to_channel_id(self, name): """Helper function that converts a channel name to its respected channel id """ channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list["channels"]: if channel["name"] == name: logger.debug(f"converted {channel['name']} to {channel['id']}") return channel["id"] logger.warning(f"could not convert channel name {name} to an id") def helper_private_channel_name_to_channel_id(self, name): """Helper function that converts a channel name to its respected channel id """ channels_list = self._slacker.groups.list().body["groups"] logger.info(str(channels_list)) for channel in channels_list["groups"]: if channel["name"] == name: logger.debug(f"converted {channel['name']} to {channel['id']}") return channel["id"] logger.warning(f"could not convert channel name {name} to an id") def helper_user_name_to_user_id(self, name): """Helper function that converts a user name to its respected user id """ users = self._slacker.users.list().body["members"] for user in users: if user["name"] == name: logger.debug(f"converted {name} to {user['id']}") return user["id"] logger.warning(f"could not convert user name {name} to a user id") def helper_channel_id_to_channel_name(self, channel_id): """Helper function that converts a channel id to its respected channel name """ channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list["channels"]: if channel["id"] == channel_id: logger.debug("converted {} to {}".format( channel_id, channel["name"])) return channel["name"] logger.warning(f"could not convert channel id {channel_id} to a name") def helper_user_id_to_user_name(self, user_id): """Helper function that converts a user id to its respected user name """ users_list = self._slacker.users.list() for user in users_list.body["members"]: if user["id"] == user_id: logger.debug(f"converted {user_id} to {user['name']}") return user["name"] logger.warning(f"could not convert user id {user_id} to a name") def helper_user_id_to_user_real_name(self, user_id): """Helper function that converts a user id to its respected user name """ users_list = self._slacker.users.list() for user in users_list.body["members"]: if user["id"] == user_id: logger.debug(f"converted {user_id} to {user['real_name']}") return user["real_name"] logger.warning(f"could not convert user id {user_id} to a name") def helper_user_id_to_tz_offset(self, user_id): """Helper function that converts a user id to its respected time zone offset """ users_list = self._slacker.users.list() for user in users_list.body["members"]: if user["id"] == user_id: logger.debug(f"converted {user_id} to {user['tz_offset']}") return int(user["tz_offset"]) logger.warning( f"could not get time zone offset from user id {user_id}") return 0 def helper_user_id_to_tz_label(self, user_id): """Helper function that converts a user id to its respected time zone """ users_list = self._slacker.users.list() for user in users_list.body["members"]: if user["id"] == user_id: logger.debug(f"converted {user_id} to {user['tz']}") return user["tz"] logger.warning(f"could not get time zone from user id {user_id}") return "Unknown"
class SimpleSlackBot: """Simplifies interacting with the Slack API. Allows users to register functions to specific events, get those functions called when those specific events are triggered and run their business code """ KEYBOARD_INTERRUPT_EXCEPTION_LOG_MESSAGE = "KeyboardInterrupt exception caught." SYSTEM_INTERRUPT_EXCEPTION_LOG_MESSAGE = "SystemExit exception caught." @staticmethod def peek(iterable): """Allows us to look at the next yield in an Iterable. From: https://stackoverflow.com/a/664239/1983957 :param iterable: some Iterable to peek at :return: the first and rest of the yielded items """ try: first = next(iterable) except StopIteration: return None return first, itertools.chain([first], iterable) @staticmethod def log_gracefully_shutdown(prefix_str): """Just a convenient way to log multiple messages in a similar way :param prefix_str: String to log in the begging :return: None """ logger.info(f"{prefix_str} Gracefully shutting down") def __init__(self, debug=False): """Initializes our Slack bot and slack bot token. Will exit if the required environment variable is not set. :param debug: Whether or not to use default a Logging config """ self._SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN") if self._SLACK_BOT_TOKEN is None: sys.exit("ERROR: environment variable SLACK_BOT_TOKEN is not set") self._slacker = Slacker(self._SLACK_BOT_TOKEN) self._slackSocket = SlackSocket(self._SLACK_BOT_TOKEN, translate=False) self._BOT_ID = self._slacker.auth.test().body["user_id"] self._registrations = { } # our dictionary of event_types to a list of callbacks if debug: # Enable logging for our users logger.addHandler(StreamHandler()) logger.setLevel(logging.DEBUG) logger.info( f"set bot id to {self._BOT_ID} with name {self.helper_user_id_to_user_name(self._BOT_ID)}" ) logger.info("initialized") def register(self, event_type): """Registers a callback function to a a event type. All supported even types are defined here https://api.slack.com/events-api :param event_type: the type of the event to register :return: reference to wrapped function """ def function_wrapper(callback): """Registers event before executing wrapped function, referred to as callback :param callback: function to execute after runnign wrapped code :return: None """ logger.info( f"registering callback {callback.__name__} to event type {event_type}" ) if event_type not in self._registrations: self._registrations[event_type] = [] # create an empty list self._registrations[event_type].append(callback) return function_wrapper def route_request_to_callbacks(self, request): """Routes the request to the correct notify :param request: request to be routed :return: None """ logger.info( f"received an event of type {request.type} and slack event {request._slack_event.event}" ) if request.type in self._registrations: for callback in self._registrations[request.type]: try: callback(request) except Exception as ex: logger.exception( f'exception processing event {request.type}') def listen(self): """Listens forever for Slack events, triggering appropriately callbacks when respective events are received. Catches and logs all Exceptions except for KeyboardInterrupt or SystemExit, which gracefully shuts down program. The following function is crucial to Simple Slack Bot and looks a little messy. This is do partly to the way that our dependency SlackSocket is written. They do not re-raise any caught KeyboardInterrupt exceptions and instead we have to infer one was caught based on what their generator returns. This is incredibly unfortunate, but this currently works. Since most of Simple Slack Bot's time is spent blocked on waiting for events from SlackSocket, a solution was needed to deal with this. Otherwise our application would not respond to a request from the user to stop the program with a CTRL + C. """ READ_WEBSOCKET_DELAY = 1 # 1 second delay between reading from fire hose running = True logger.info("began listening!") while running: # required to continue to run after experiencing an unexpected exception res = self.peek(self._slackSocket.events()) if res is None: self.log_gracefully_shutdown( self.KEYBOARD_INTERRUPT_EXCEPTION_LOG_MESSAGE) running = False break else: slack_event, mysequence = res if slack_event.event and "bot_id" not in slack_event.event: # We don't reply to bots try: request = SlackRequest(self._slacker, slack_event) self.route_request_to_callbacks(request) time.sleep(READ_WEBSOCKET_DELAY) except KeyboardInterrupt: self.log_gracefully_shutdown( self.KEYBOARD_INTERRUPT_EXCEPTION_LOG_MESSAGE) running = False break except SystemExit: self.log_gracefully_shutdown( self.SYSTEM_INTERRUPT_EXCEPTION_LOG_MESSAGE) running = False break except Exception as e: logging.warning( f"Unexpected exception caught, but we will keep listening. Exception: {e}" ) logging.warning(traceback.format_stack()) continue # ensuring the loop continues logger.info("stopped listening!") def start(self): """Connect the Slack bot to the chatroom and begin listening """ ok = self._slacker.rtm.start().body["ok"] if ok: logger.info("started!") self.listen() else: logger.error( "Connection failed. Are you connected to the internet? Potentially invalid Slack token? " "Check environment variable and \"SLACK_BOT_TOKEN\"") logger.info("stopped!") def get_slacker(self): """Returns SimpleSlackBot's SlackClient. This is useful if you are writing a more advanced bot and want complete access to all SlackClient has to offer. """ return self._slacker def get_slack_socket(self): """Returns SimpleSlackBot's SlackSocket. This is useful if you are writing a more advanced bot and want complete access to all SlackSocket has to offer. """ return self._slackSocket def helper_get_public_channel_ids(self): """Helper function that gets all public channel ids """ public_channel_ids = [] public_channels = self._slacker.channels.list().body["channels"] for channel in public_channels: public_channel_ids.append(channel["id"]) if len(public_channel_ids) == 0: logger.warning("got no public channel ids") else: logger.debug(f"got public channel ids {public_channel_ids}") return public_channel_ids def helper_get_private_channel_ids(self): """Helper function that gets all private channel ids """ private_channel_ids = [] private_channels = self._slacker.groups.list().body["groups"] for private_channel in private_channels: private_channels.append(private_channel["id"]) if len(private_channel_ids) == 0: logger.warning("got no private channel ids") else: logger.debug(f"got private channel ids {private_channel_ids}") return private_channel_ids def helper_get_user_ids(self): """Helper function that gets all user ids """ user_ids = [] users = self._slacker.users.list().body["members"] for user in users: user_ids.append(user["id"]) if len(user_ids) == 0: logger.warning("got no user ids") else: logger.debug(f"got user ids {user_ids}") return user_ids def helper_get_user_names(self): """Helper function that gets all user names """ user_names = [] users = self._slacker.users.list().body["members"] for user in users: user_names.append(user["name"]) if len(user_names) == 0: logger.warning("got no user names") else: logger.debug(f"got user names {user_names}") return user_names def helper_get_users_in_channel(self, channel_id): """Helper function that gets all users in a given channel id :param channel_id: channel id to get all user ids in it :return: list of user ids """ user_ids = [] channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list: if channel["id"] == channel_id: for user_id in channel["members"]: user_ids.append(user_id) if len(user_ids) == 0: logger.warning(f"got no user ids for channel {channel_id}") else: logger.debug(f"got user ids {user_ids}") return user_ids def helper_channel_name_to_channel_id(self, name): """Helper function that converts a channel name to its respected channel id :param name: name of channel to convert to id :return: id representation of original channel name """ channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list: if channel["name"] == name: logger.debug(f"converted {channel['name']} to {channel['id']}") return channel["id"] logger.warning(f"could not convert channel name {name} to an id") def helper_user_name_to_user_id(self, name): """Helper function that converts a user name to its respected user id :param name: name of user to convert to id :return: id representation of original user name """ users = self._slacker.users.list().body["members"] for user in users: if user["name"] == name: logger.debug(f"converted {name} to {user['id']}") return user["id"] logger.warning(f"could not convert user name {name} to a user id") def helper_channel_id_to_channel_name(self, channel_id): """Helper function that converts a channel id to its respected channel name :param channel_id: id of channel to convert to name :return: name representation of original channel id """ channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list: if channel["id"] == channel_id: logger.debug("converted {} to {}".format( channel_id, channel["name"])) return channel["name"] logger.warning(f"could not convert channel id {channel_id} to a name") def helper_user_id_to_user_name(self, user_id): """Helper function that converts a user id to its respected user name :param user_id: id of user to convert to name :return: name representation of original user id """ users_list = self._slacker.users.list() for user in users_list.body["members"]: if user["id"] == user_id: logger.debug(f"converted {user_id} to {user['name']}") return user["name"] logger.warning(f"could not convert user id {user_id} to a name")
s = SlackSocket(slackToken) except: import sys import tools.shenanigans if shenanigansState: tools.shenanigans.enable() tools.shenanigans.reboot() else: print "That token was invalid!" sys.exit(2) setPresence('auto') s.send_msg("I'm combat ready!", debugChannel) for event in s.events(): eventString = str(event.json) if (eventString == '{}'): reboot() if (debugMode): s.send_msg(eventString, debugChannel) print(eventString) if shenanigansState and('_left' in eventString): import json import tools.shenanigans eventParsed = json.loads(eventString) channel = eventParsed['channel'].strip()
class GameMaster(object): """ Manages user sessions. """ def __init__(self, config): # Slack config slack_api_token = config.get('slack', 'api_token') self._slack_username = config.get('slack', 'bot_username') self._slack = SlackSocket(slack_api_token, translate=True) self._slack_events = self._slack.events() self._slack_sessions = {} self._slack_event_handler = Thread(target=self._handle_slack_events, name='event_handler') # Frotz config self._frotz_binary = config.get('frotz', 'path') self._frotz_story_file = config.get('frotz', 'story') # Logging config self._logs_dir = config.get('frotzlack', 'logs_dir') error_log_path = os.path.join(self._logs_dir, 'frotzlack.log') self._global_handler = RotatingFileHandler(error_log_path) self._global_handler.setLevel(logging.WARNING) # Other config self._admins = config.get('frotzlack', 'admins').split(',') self._stop_requested = False self._slack_event_handler.start() def _event_is_game_input(self, event): event_attrs = event.event is_game_input = 'type' in event_attrs.keys() and \ event_attrs['type'] == 'message' and \ event_attrs['user'] in self._slack_sessions and \ event_attrs['user'] != self._slack_username and \ self._slack_username not in event.mentions and \ event_attrs['channel'] == event_attrs['user'] return is_game_input def _event_is_command(self, event): event_attrs = event.event return 'type' in event_attrs.keys() and \ event_attrs['type'] == 'message' and \ self._slack_username in event.mentions def _handle_game_input(self, user, game_input): session = self._slack_sessions[user] if game_input.strip() == 'save' or game_input.strip() == 'load': session.send("Sorry, I can't save or load games yet.") elif game_input.strip() == 'quit': session.send("Sorry, I can't quit the game yet.") else: session.put(game_input) def _start_session(self, username): def send_msg(msg): self._slack.send_msg(msg, channel_id=channel_id, confirm=False) channel_id = self._slack.get_im_channel(username)['id'] session_logger = logging.getLogger(username) session_logger.setLevel(logging.INFO) session_log_path = os.path.join(self._logs_dir, username + '.log') session_handler = RotatingFileHandler(session_log_path) session_handler.setLevel(logging.INFO) session_logger.addHandler(session_handler) session_logger.addHandler(self._global_handler) slack_session = SlackSession(send_msg, channel_id) frotz_session = FrotzSession(self._frotz_binary, self._frotz_story_file, session_logger) self._slack_sessions[username] = slack_session Session(slack_session, frotz_session) def _reject_command(self, username, command): channel = self._slack.get_im_channel(username) message = "Sorry, I don't recognize the command `{0}`" self._slack.send_msg(message.format(command), channel_id=channel['id']) def _stop_server(self): pool = ThreadPool() for session in self._slack_sessions.values(): def polite_stop(): session.send("Sorry, the server is shutting down now.") session.kill() pool.apply_async(polite_stop) pool.close() pool.join() self._stop_requested = True def _handle_slack_events(self): while not self._stop_requested: event = self._slack_events.next() event_attrs = event.event if self._event_is_game_input(event): msg = event_attrs['text'] self._handle_game_input(event_attrs['user'], msg) elif self._event_is_command(event): command = event_attrs['text'] user = event_attrs['user'] if 'stop' in command and user in self._admins: self._stop_server() elif 'play' in command: self._start_session(user) else: self._reject_command(event_attrs['user'], command)
class SimpleSlackBot: """Simplifie interacting with the Slack API. Allows users to register functions to specific events, get those functions called when those specific events are triggered and run their business code """ KEYBOARD_INTERRUPT_EXCEPTION_LOG_MESSAGE = "KeyboardInterrupt exception caught." SYSTEM_INTERRUPT_EXCEPTION_LOG_MESSAGE = "SystemExit exception caught." @staticmethod def peek( iterator: typing.Iterator, ) -> typing.Union[None, typing.Tuple[typing.Any, typing.Iterator]]: """Allow us to look at the next yield in an Iterator. From: https://stackoverflow.com/a/664239/1983957 :param iterator: some Iterator to peek at :return: the first and rest of the yielded items """ try: first = next(iterator) except StopIteration: return None return first, itertools.chain([first], iterator) def __init__(self, slack_bot_token: str = None, debug: bool = False): """Initialize our Slack bot and slack bot token. Will exit if the required environment variable is not set. :param slack_bot_token: The token given by Slack for API authentication :param debug: Whether or not to use default a Logging config """ # fetch a slack_bot_token first checking params, then environment variable otherwise # raising a SystemExit exception as this is required for execution if slack_bot_token is None: self._slack_bot_token = os.environ.get("SLACK_BOT_TOKEN") else: self._slack_bot_token = slack_bot_token if self._slack_bot_token is None or self._slack_bot_token == "": sys.exit( "ERROR: SLACK_BOT_TOKEN not passed to constructor or set as environment variable" ) if debug: # enable logging additional debug logging logger.addHandler(StreamHandler()) logger.setLevel(logging.DEBUG) logger.info("initialized. Ready to connect") def connect(self): """Connect to underlying SlackSocket. Additionally stores conections for future usage. """ # Disable all the attribute-defined-out-init in this function # pylint: disable=attribute-defined-outside-init logger.info("Connecting...") self._python_slackclient = WebClient(self._slack_bot_token) self._slack_socket = SlackSocket(self._slack_bot_token) self._bot_id = self._python_slackclient.auth_test()["bot_id"] logger.info( "Connected. Set bot id to %s with name %s", self._bot_id, self.helper_user_id_to_user_name(self._bot_id), ) def register(self, event_type: str) -> typing.Callable[..., typing.Any]: """Register a callback function to a a event type. All supported even types are defined here https://api.slack.com/events-api :param event_type: the type of the event to register :return: reference to wrapped function """ def function_wrapper(callback: typing.Callable): """Register event before executing wrapped function, referred to as callback. :param callback: function to execute after runnign wrapped code """ # Disable all the attribute-defined-out-init in this function # pylint: disable=attribute-defined-outside-init # first time initialization if not hasattr(self, "_registrations"): self._registrations: typing.Dict[ str, typing.List[typing.Callable]] = {} if event_type not in self._registrations: # first registration of this type self._registrations[event_type] = [] self._registrations[event_type].append(callback) return function_wrapper def route_request_to_callbacks(self, request: SlackRequest): """Route the request to the correct notify. :param request: request to be routed """ logger.info( "received an event of type %s and slack event type of %s with content %s", request.type, request.slack_event.type, request, ) # we ignore subtypes to ensure thread messages don't go to the channel as well, as two events are created # i'm totally confident this will have unexpected consequences but have not discovered any at the time of # writing this if request.type in self._registrations and request.subtype is None: for callback in self._registrations[request.type]: try: callback(request) except Exception: # pylint: disable=broad-except logger.exception( "exception processing event %s . Exception %s", request.type, traceback.format_exc(), ) def extract_slack_socket_response(self) -> typing.Union[SlackEvent, None]: """Extract a useable response from the underlying _slack_socket. Catch all SlackSocket exceptions except forExitError, treating those as warnings. """ try: return self.peek(self._slack_socket.events()) except ( slacksocket.errors.APIError, slacksocket.errors.ConfigError, slacksocket.errors.APINameError, slacksocket.errors.ConnectionError, slacksocket.errors.TimeoutError, ): logging.warning( "Unexpected exception caught, but we will keep listening. Exception: %s", traceback.format_exc(), ) return None # ensuring the loop continues and execution ends def listen(self): """Listen forever for Slack events, triggering appropriately callbacks when respective events are received. Catches and logs all Exceptions except for KeyboardInterrupt or SystemExit, which gracefully shuts down program. The following function is crucial to Simple Slack Bot and looks a little messy. Since most of Simple Slack Bot's time is spent blocked on waiting for events from SlackSocket, a solution was needed to deal with this. Otherwise our application would not respond to a request from the user to stop the program with a CTRL + C. """ read_websocket_delay = 1 # 1 second delay between reading from fire hose running = True logger.info("began listening!") # required to continue to run after experiencing an unexpected exception while running: response = None try: while response is None: response = self.extract_slack_socket_response() except slacksocket.errors.ExitError: logging.info(self.KEYBOARD_INTERRUPT_EXCEPTION_LOG_MESSAGE) running = False break # ensuring the loop stops and execution ceases slack_event, _ = response try: self.route_request_to_callbacks( SlackRequest(self._python_slackclient, slack_event)) time.sleep(read_websocket_delay) except Exception: # pylint: disable=broad-except logging.warning( "Unexpected exception caught, but we will keep listening. Exception: %s", traceback.format_exc(), ) continue # ensuring the loop continues logger.info("stopped listening!") def start(self): """Connect the Slack bot to the chatroom and begin listening.""" self.connect() ok_reponse = self._python_slackclient.rtm_start() if ok_reponse: logger.info("started!") self.listen() else: logger.error( "Connection failed. Are you connected to the internet? Potentially invalid Slack token? " 'Check environment variable and "SLACK_BOT_TOKEN"') logger.info("stopped!") def helper_get_public_channel_ids(self) -> typing.List[str]: """Get all public channel ids. :return: list of public channel ids """ public_channel_ids = [] if self._python_slackclient and self._python_slackclient.channels_list( ): public_channels = self._python_slackclient.channels_list( )["channels"] for channel in public_channels: public_channel_ids.append(channel["id"]) if len(public_channel_ids) == 0: logger.warning("got no public channel ids") else: logger.debug("got public channel ids %s", public_channel_ids) return public_channel_ids def helper_get_private_channel_ids(self) -> typing.List[str]: """Get all private channel ids. :return: list of private channel ids """ private_channel_ids: typing.List[str] = [] for private_channel in self._python_slackclient.groups_list( )["groups"]: private_channel_ids.append(private_channel["id"]) if len(private_channel_ids) == 0: logger.warning("got no private channel ids") else: logger.debug("got private channel ids %s", private_channel_ids) return private_channel_ids def helper_get_user_ids(self) -> typing.List[str]: """Get all user ids. :return: list of user ids """ user_ids = [] for user in self._python_slackclient.users_list()["members"]: user_ids.append(user["id"]) if len(user_ids) == 0: logger.warning("got no user ids") else: logger.debug("got user ids %s", user_ids) return user_ids def helper_get_user_names(self) -> typing.List[str]: """Get all user names. :return: list of user names """ user_names = [] for user in self._python_slackclient.users_list()["members"]: user_names.append(user["name"]) if len(user_names) == 0: logger.warning("got no user names") else: logger.debug("got user names %s", user_names) return user_names def helper_get_users_in_channel(self, channel_id: str) -> typing.List[str]: """Get all users in a given channel id. :param channel_id: channel id to get all user ids in it :return: list of user ids """ user_ids = [] for channel in self._python_slackclient.channels_list()["channels"]: if channel["id"] == channel_id: for user_id in channel["members"]: user_ids.append(user_id) if len(user_ids) == 0: logger.warning("got no user ids for channel %s", channel_id) else: logger.debug("got user ids %s", user_ids) return user_ids def helper_channel_name_to_channel_id( self, name: str) -> typing.Union[str, None]: """Convert a channel name to its respected channel id. :param name: name of channel to convert to id :return: id representation of original channel name """ for channel in self._python_slackclient.channels_list()["channels"]: if channel["name"] == name: logger.debug("converted %s to %s", channel["name"], channel["id"]) return channel["id"] logger.warning("could not convert channel name %s to an id", name) return None def helper_user_name_to_user_id(self, name: str) -> typing.Union[str, None]: """Convert a user name to its respected user id. :param name: name of user to convert to id :return: id representation of original user name """ for user in self._python_slackclient.users_list()["members"]: if user["name"] == name: logger.debug("converted %s to %s", name, user["id"]) return user["id"] logger.warning("could not convert user name %s to a user id", name) return None def helper_channel_id_to_channel_name( self, channel_id: str) -> typing.Union[str, None]: """Convert a channel id to its respected channel name. :param channel_id: id of channel to convert to name :return: name representation of original channel id """ for channel in self._python_slackclient.channels_list()["channels"]: if channel["id"] == channel_id: logger.debug("converted %s to %s", channel_id, channel["name"]) return channel["name"] logger.warning("could not convert channel id %s to a name", channel_id) return None def helper_user_id_to_user_name(self, user_id: str) -> typing.Union[str, None]: """Convert a user id to its respected user name. :param user_id: id of user to convert to name :return: name representation of original user id """ for user in self._python_slackclient.users_list()["members"]: if user["id"] == user_id: logger.debug("converted %s to %s", user_id, user["name"]) return user["name"] logger.warning("could not convert user id %s to a name", user_id) return None
class SimpleSlackBot: """Simplifies interacting with the Slack API. Allows users to register functions to specific events, get those functions called when those specific events are triggered and run their business code """ def __init__(self, debug=False): """Initializes our Slack bot and slack bot token. Will exit if the required environment variable is not set. """ self._SLACK_BOT_TOKEN = os.environ.get("SLACK_BOT_TOKEN") if self._SLACK_BOT_TOKEN is None: sys.exit("ERROR: environment variable SLACK_BOT_TOKEN is not set") self._slacker = Slacker(self._SLACK_BOT_TOKEN) self._slackSocket = SlackSocket(self._SLACK_BOT_TOKEN, translate=False) self._BOT_ID = self._slacker.auth.test().body["user_id"] self._registrations = {} # our dictionary of event_types to a list of callbacks if debug: print("DEBUG!") logger.removeHandler(null_handler) logger.addHandler(StreamHandler()) logger.setLevel(logging.DEBUG) logger.info(f"set bot id to {self._BOT_ID} with name {self.helper_user_id_to_user_name(self._BOT_ID)}") logger.info("initialized") def register(self, event_type): """Registers a callback function to a a event type. All supported even types are defined here https://api.slack.com/events-api """ def function_wrapper(callback): logger.info(f"registering callback {callback.__name__} to event type {event_type}") if event_type not in self._registrations: self._registrations[event_type] = [] # create an empty list self._registrations[event_type].append(callback) return function_wrapper def route_request_to_callbacks(self, request): """Routes the request to the correct notify """ logger.info(f"received an event of type {request.type} and slack event {request._slack_event.event}") if request.type in self._registrations: for callback in self._registrations[request.type]: callback(request) def listen(self): """Listens forever for Slack events, triggering appropriately callbacks when respective events are received """ READ_WEBSOCKET_DELAY = 1 # 1 second delay between reading from firehose logger.info("began listening!") for slack_event in self._slackSocket.events(): if slack_event: if slack_event.event and "bot_id" not in slack_event.event: # We don't reply to bots request = SlackRequest(self._slacker, slack_event) self.route_request_to_callbacks(request) time.sleep(READ_WEBSOCKET_DELAY) logger.info("Keyboard interrupt received. Gracefully shutting down") sys.exit(0) def start(self): """Connect the Slack bot to the chatroom and begin listening """ ok = self._slacker.rtm.start().body["ok"] if ok: logger.info("started!") self.listen() else: logger.error("Connection failed. Are you connected to the internet? Potentially invalid Slack token? " "Check environment variable and \"SLACK_BOT_TOKEN\"") def get_slacker(self): """Returns SimpleSlackBot's SlackClient. This is useful if you are writing a more advanced bot and want complete access to all SlackClient has to offer. """ return self._slacker def get_slack_socket(self): """Returns SimpleSlackBot's SlackSocket. This is useful if you are writing a more advanced bot and want complete access to all SlackSocket has to offer. """ return self._slackSocket def helper_get_public_channel_ids(self): """Helper function that gets all public channel ids """ public_channel_ids = [] public_channels = self._slacker.channels.list().body["channels"] for channel in public_channels: public_channel_ids.append(channel["id"]) if len(public_channel_ids) == 0: logger.warning("got no public channel ids") else: logger.debug(f"got public channel ids {public_channel_ids}") return public_channel_ids def helper_get_private_channel_ids(self): """Helper function that gets all private channel ids """ private_channel_ids = [] private_channels = self._slacker.groups.list().body["groups"] for private_channel in private_channels: private_channels.append(private_channel["id"]) if len(private_channel_ids) == 0: logger.warning("got no private channel ids") else: logger.debug(f"got private channel ids {private_channel_ids}") return private_channel_ids def helper_get_user_ids(self): """Helper function that gets all user ids """ user_ids = [] users = self._slacker.users.list().body["members"] for user in users: user_ids.append(user["id"]) if len(user_ids) == 0: logger.warning("got no user ids") else: logger.debug(f"got user ids {user_ids}") return user_ids def helper_get_user_names(self): """Helper function that gets all user names """ user_names = [] users = self._slacker.users.list().body["members"] for user in users: user_names.append(user["name"]) if len(user_names) == 0: logger.warning("got no user names") else: logger.debug(f"got user names {user_names}") return user_names def helper_get_users_in_channel(self, channel_id): """Helper function that gets all users in a given channel id """ user_ids = [] channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list["channels"]: if channel["id"] == channel_id: for user_id in channel["members"]: user_ids.append(user_id) if len(user_ids) == 0: logger.warning(f"got no user ids for channel {channel_id}") else: logger.debug(f"got user ids {user_ids}") return user_ids def helper_channel_name_to_channel_id(self, name): """Helpfer function that converts a channel name to its respected channel id """ channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list["channels"]: if channel["name"] == name: logger.debug(f"converted {channel['name']} to {channel['id']}") return channel["id"] logger.warning(f"could not convert channel name {name} to an id") def helper_user_name_to_user_id(self, name): """Helper function that converts a user name to its respected user id """ users = self._slacker.users.list().body["members"] for user in users: if user["name"] == name: logger.debug(f"converted {name} to {user['id']}") return user["id"] logger.warning(f"could not convert user name {name} to a user id") def helper_channel_id_to_channel_name(self, channel_id): """Helper function that converts a channel id to its respected channel name """ channels_list = self._slacker.channels.list().body["channels"] for channel in channels_list["channels"]: if channel["id"] == channel_id: logger.debug("converted {} to {}".format(channel_id, channel["name"])) return channel["name"] logger.warning(f"could not convert channel id {channel_id} to a name") def helper_user_id_to_user_name(self, user_id): """Helper function that converts a user id to its respected user name """ users_list = self._slacker.users.list() for user in users_list.body["members"]: if user["id"] == user_id: logger.debug(f"converted {user_id} to {user['name']}") return user["name"] logger.warning(f"could not convert user id {user_id} to a name")
def start_joy(team_id, bot_id): SLACK_TOKEN = '' with open('tokens.pickle', 'rb') as f: tokens = pickle.load(f) SLACK_TOKEN = tokens[team_id][0] slack_socket = SlackSocket(SLACK_TOKEN, translate=True) slack = Slacker(SLACK_TOKEN) response = slack.channels.list() channels = {} for c in [u for u in response.body['channels']]: name = c['name'] user_id = c['id'] channels[name] = Channel(name, user_id) response = slack.users.list() people = {} for p in [u for u in response.body['members']]: name = p['name'] user_id = p['id'] person = User(name, user_id) if 'is_admin' in p: person.manager = p['is_admin'] else: person.manager = False people[name] = person print('starting joy on ' + team_id) for event in slack_socket.events(): res = json.loads(event.json) if 'team' in res and res['team'] == team_id and res['type'] == 'message' and 'user' in res and res['user'] != 'joy': print(res) team_id = res['team'] message = res['text'] user = res['user'] channel = res['channel'] timestamp = res['ts'] if bot_id in message: if 'get morale' in message.lower(): t = message.lower().split('get morale ') person = '' if len(t) > 1: person = t[1] # print(person) slack_socket.send_msg(str(compute_person_channel_morale(slack, people, channels, person)), channel_name=channel) else: slack_socket.send_msg(str(compute_team_morale(people)), channel_name=channel) continue res = tone_analyzer.tone(text=message) emotional_tone = res['children'][0] writing_tone = res['children'][1] social_tone = res['children'][2] cheerfulness = float(emotional_tone['children'][0]['normalized_score']) negative = float(emotional_tone['children'][1]['normalized_score']) anger = float(emotional_tone['children'][2]['normalized_score']) analytical = float(writing_tone['children'][0]['normalized_score']) confident = float(writing_tone['children'][1]['normalized_score']) tentative = float(writing_tone['children'][2]['normalized_score']) openness = float(social_tone['children'][0]['normalized_score']) agreeableness = float(social_tone['children'][1]['normalized_score']) conscientiousness = float(social_tone['children'][2]['normalized_score']) sentiment = { 'cheerfulness' : [cheerfulness], 'negative' : [negative], 'anger' : [anger], 'analytical' : [analytical], 'confident' : [confident], 'tentative' : [tentative], 'openness' : [openness], 'agreeableness' : [agreeableness], 'conscientiousness' : [conscientiousness] } teams = {} try: with open('teams.pickle', 'rb') as f: teams = pickle.load(f) # print('load current teams: ' + str(teams.keys())) except: pass if team_id in teams: if channel in channels: teams[team_id]['channels'][channel].add_sentiment(sentiment) channels = teams[team_id]['channels'] if user in people: teams[team_id]['people'][user].add_sentiment(sentiment) people = teams[team_id]['people'] else: d = {'channels' : channels, 'people' : people} teams[team_id] = d # print('saving ' + team_id) # print('current teams: ' + str(teams.keys())) with open('teams.pickle', 'wb') as f: pickle.dump(teams, f)