def send(channel, text, thread_ts=None): Slack.get_instance().rtm_send_message(channel, text, thread_ts)
def users(self): return Slack.get_instance().server.users
def channels(self): return Slack.get_instance().server.channels
def retrieve_bot_info(): return Slack.get_instance().server.login_data['self']
def read_group_topic_webapi(self, channel): method = 'groups.info' kwargs = {'channel': channel, 'as_user': True} return Slack.get_instance().api_call(method, **kwargs)
def scheduled_messages(self): response = Slack.get_instance().api_call('chat.scheduledMessages.list') return response
def open_im(self, user): response = Slack.get_instance().api_call('im.open', user=user) return response['channel']['id']
def set_group_topic_webapi(self, channel, text): method = 'groups.setTopic' kwargs = {'channel': channel, 'topic': text, 'as_user': True} return Slack.get_instance().api_call(method, **kwargs)
def react(self, channel, ts, emoji): return Slack.get_instance().api_call('reactions.add', name=emoji, channel=channel, timestamp=ts)
def __init__(self, plugin_actions): self._client = Slack() self._plugin_actions = plugin_actions self._pool = ThreadPool()
class EventDispatcher: RESPOND_MATCHER = re.compile( r'^(?:\<@(?P<atuser>\w+)\>:?|(?P<username>\w+):) ?(?P<text>.*)$') def __init__(self, plugin_actions): self._client = Slack() self._plugin_actions = plugin_actions self._pool = ThreadPool() def start(self): while True: for event in self._client.rtm_read(): self._pool.add_task(self.handle_event, event) time.sleep(.1) def handle_event(self, event): # Gotta catch 'em all! for action in self._plugin_actions['catch_all'].values(): action['function'](event) # Basic dispatch based on event type if 'type' in event: if event['type'] in self._plugin_actions['process']: for action in self._plugin_actions['process'][ event['type']].values(): action['function'](event) # Handle message listeners if 'type' in event and event['type'] == 'message': respond_to_msg = self._check_bot_mention(event) if respond_to_msg: listeners = self._find_listeners('respond_to') self._dispatch_listeners(listeners, respond_to_msg) else: listeners = self._find_listeners('listen_to') self._dispatch_listeners(listeners, event) def _find_listeners(self, type): return [action for action in self._plugin_actions[type].values()] def _gen_message(self, event, plugin_class_name): return Message(MessagingClient(), event, plugin_class_name) def _get_bot_id(self): return self._client.server.login_data['self']['id'] def _get_bot_name(self): return self._client.server.login_data['self']['name'] def _check_bot_mention(self, event): full_text = event.get('text', '') channel = event['channel'] bot_name = self._get_bot_name() bot_id = self._get_bot_id() m = self.RESPOND_MATCHER.match(full_text) if channel[0] == 'C' or channel[0] == 'G': if not m: return None matches = m.groupdict() atuser = matches.get('atuser') username = matches.get('username') text = matches.get('text') if atuser != bot_id and username != bot_name: # a channel message at other user return None event['text'] = text else: if m: event['text'] = m.groupdict().get('text', None) return event def _dispatch_listeners(self, listeners, event): for l in listeners: matcher = l['regex'] match = matcher.search(event.get('text', '')) if match: message = self._gen_message(event, l['class_name']) l['function'](message, **match.groupdict())
class Machine: def __init__(self, settings=None): announce("Initializing Slack Machine:") with indent(4): puts("Loading settings...") if settings: self._settings = settings found_local_settings = True else: self._settings, found_local_settings = import_settings() fmt = '[%(asctime)s][%(levelname)s] %(name)s %(filename)s:%(funcName)s:%(lineno)d |' \ ' %(message)s' date_fmt = '%Y-%m-%d %H:%M:%S' log_level = self._settings.get('LOGLEVEL', logging.ERROR) logging.basicConfig( level=log_level, format=fmt, datefmt=date_fmt, ) if not found_local_settings: warn( "No local_settings found! Are you sure this is what you want?" ) if 'SLACK_API_TOKEN' not in self._settings: error( "No SLACK_API_TOKEN found in settings! I need that to work..." ) sys.exit(1) self._client = Slack() puts("Initializing storage using backend: {}".format( self._settings['STORAGE_BACKEND'])) self._storage = Storage.get_instance() logger.debug("Storage initialized!") self._plugin_actions = { 'process': {}, 'listen_to': {}, 'respond_to': {}, 'catch_all': {} } self._help = {'human': {}, 'robot': {}} puts("Loading plugins...") self.load_plugins() logger.debug("The following plugin actions were registered: %s", self._plugin_actions) self._dispatcher = EventDispatcher(self._plugin_actions, self._settings) def load_plugins(self): with indent(4): logger.debug("PLUGINS: %s", self._settings['PLUGINS']) for plugin in self._settings['PLUGINS']: for class_name, cls in import_string(plugin): if issubclass(cls, MachineBasePlugin ) and cls is not MachineBasePlugin: logger.debug( "Found a Machine plugin: {}".format(plugin)) storage = PluginStorage(class_name) instance = cls(self._settings, MessagingClient(), storage) missing_settings = self._register_plugin( class_name, instance) if (missing_settings): show_invalid(class_name) with indent(4): error_msg = "The following settings are missing: {}".format( ", ".join(missing_settings)) puts(colored.red(error_msg)) puts( colored.red( "This plugin will not be loaded!")) del instance else: instance.init() show_valid(class_name) self._storage.set('manual', dill.dumps(self._help)) def _register_plugin(self, plugin_class, cls_instance): missing_settings = [] missing_settings.extend( self._check_missing_settings(cls_instance.__class__)) methods = inspect.getmembers(cls_instance, predicate=inspect.ismethod) for _, fn in methods: missing_settings.extend(self._check_missing_settings(fn)) if missing_settings: return missing_settings if hasattr(cls_instance, 'catch_all'): self._plugin_actions['catch_all'][plugin_class] = { 'class': cls_instance, 'class_name': plugin_class, 'function': getattr(cls_instance, 'catch_all') } if cls_instance.__doc__: class_help = cls_instance.__doc__.splitlines()[0] else: class_help = plugin_class self._help['human'][class_help] = self._help['human'].get( class_help, {}) self._help['robot'][class_help] = self._help['robot'].get( class_help, []) for name, fn in methods: if hasattr(fn, 'metadata'): self._register_plugin_actions(plugin_class, fn.metadata, cls_instance, name, fn, class_help) def _check_missing_settings(self, fn_or_class): missing_settings = [] if hasattr(fn_or_class, 'metadata') and 'required_settings' in fn_or_class.metadata: for setting in fn_or_class.metadata['required_settings']: if setting not in self._settings: missing_settings.append(setting.upper()) return missing_settings def _register_plugin_actions(self, plugin_class, metadata, cls_instance, fn_name, fn, class_help): fq_fn_name = "{}.{}".format(plugin_class, fn_name) if fn.__doc__: self._help['human'][class_help][ fq_fn_name] = self._parse_human_help(fn.__doc__) for action, config in metadata['plugin_actions'].items(): if action == 'process': event_type = config['event_type'] event_handlers = self._plugin_actions['process'].get( event_type, {}) event_handlers[fq_fn_name] = { 'class': cls_instance, 'class_name': plugin_class, 'function': fn } self._plugin_actions['process'][event_type] = event_handlers if action == 'respond_to' or action == 'listen_to': for regex in config['regex']: event_handler = { 'class': cls_instance, 'class_name': plugin_class, 'function': fn, 'regex': regex } key = "{}-{}".format(fq_fn_name, regex.pattern) self._plugin_actions[action][key] = event_handler self._help['robot'][class_help].append( self._parse_robot_help(regex, action)) if action == 'schedule': Scheduler.get_instance().add_job(fq_fn_name, trigger='cron', args=[cls_instance], id=fq_fn_name, replace_existing=True, **config) if action == 'route': for route_config in config: bottle.route(**route_config)(fn) @staticmethod def _parse_human_help(doc): summary = doc.splitlines()[0].split(':') if len(summary) > 1: command = summary[0].strip() cmd_help = summary[1].strip() else: command = "??" cmd_help = summary[0].strip() return {'command': command, 'help': cmd_help} @staticmethod def _parse_robot_help(regex, action): if action == 'respond_to': return "@botname {}".format(regex.pattern) else: return regex.pattern def _keepalive(self): while True: time.sleep(self._settings['KEEP_ALIVE']) self._client.server.send_to_websocket({'type': 'ping'}) logger.debug("Client Ping!") def run(self): announce("\nStarting Slack Machine:") with indent(4): connected = self._client.rtm_connect() if not connected: logger.error("Could not connect to Slack! Aborting...") sys.exit(1) show_valid("Connected to Slack") Scheduler.get_instance().start() show_valid("Scheduler started") if not self._settings['DISABLE_HTTP']: self._bottle_thread = Thread( target=bottle.run, kwargs=dict( host=self._settings['HTTP_SERVER_HOST'], port=self._settings['HTTP_SERVER_PORT'], server=self._settings['HTTP_SERVER_BACKEND'], )) self._bottle_thread.daemon = True self._bottle_thread.start() show_valid("Web server started") if self._settings['KEEP_ALIVE']: self._keep_alive_thread = Thread(target=self._keepalive) self._keep_alive_thread.daemon = True self._keep_alive_thread.start() show_valid("Keepalive thread started [Interval: %ss]" % self._settings['KEEP_ALIVE']) show_valid("Dispatcher started") self._dispatcher.start()
class EventDispatcher: def __init__(self, plugin_actions, settings=None): self._client = Slack() self._plugin_actions = plugin_actions self._pool = ThreadPool() alias_regex = '' if settings and "ALIASES" in settings: logger.info("Setting aliases to {}".format(settings['ALIASES'])) alias_regex = '|(?P<alias>{})'.format( '|'.join([re.escape(s) for s in settings['ALIASES'].split(',')])) self.RESPOND_MATCHER = re.compile( r"^(?:<@(?P<atuser>\w+)>:?|(?P<username>\w+):{}) ?(?P<text>.*)$".format( alias_regex ), re.DOTALL, ) def start(self): while True: for event in self._client.rtm_read(): self._pool.add_task(self.handle_event, event) time.sleep(.1) def handle_event(self, event): # Gotta catch 'em all! for action in self._plugin_actions['catch_all'].values(): action['function'](event) # Basic dispatch based on event type if 'type' in event: if event['type'] in self._plugin_actions['process']: for action in self._plugin_actions['process'][event['type']].values(): action['function'](event) # Handle message listeners if 'type' in event and event['type'] == 'message': respond_to_msg = self._check_bot_mention(event) if respond_to_msg: listeners = self._find_listeners('respond_to') self._dispatch_listeners(listeners, respond_to_msg) else: listeners = self._find_listeners('listen_to') self._dispatch_listeners(listeners, event) if 'type' in event and event['type'] == 'pong': logger.debug("Server Pong!") def _find_listeners(self, type): return [action for action in self._plugin_actions[type].values()] @staticmethod def _gen_message(event, plugin_class_name): return Message(MessagingClient(), event, plugin_class_name) def _get_bot_id(self): return self._client.server.login_data['self']['id'] def _get_bot_name(self): return self._client.server.login_data['self']['name'] def _check_bot_mention(self, event): full_text = event.get('text', '') channel = event['channel'] bot_name = self._get_bot_name() bot_id = self._get_bot_id() m = self.RESPOND_MATCHER.match(full_text) if channel[0] == 'C' or channel[0] == 'G': if not m: return None matches = m.groupdict() atuser = matches.get('atuser') username = matches.get('username') text = matches.get('text') alias = matches.get('alias') if alias: atuser = bot_id if atuser != bot_id and username != bot_name: # a channel message at other user return None event['text'] = text else: if m: event['text'] = m.groupdict().get('text', None) return event def _dispatch_listeners(self, listeners, event): for l in listeners: matcher = l['regex'] match = matcher.search(event.get('text', '')) if match: message = self._gen_message(event, l['class_name']) l['function'](message, **match.groupdict())