def __init__(self, plugin_actions, settings=None): self._client = LowLevelSlackClient() self._plugin_actions = plugin_actions 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 send(self, channel: Union[Channel, str], text: str, **kwargs): channel_id = id_for_channel(channel) if 'attachments' in kwargs and kwargs['attachments'] is not None: kwargs['attachments'] = extract_json(kwargs['attachments']) if 'blocks' in kwargs and kwargs['blocks'] is not None: kwargs['blocks'] = extract_json(kwargs['blocks']) if 'ephemeral_user' in kwargs and kwargs['ephemeral_user'] is not None: ephemeral_user_id = id_for_user(kwargs['ephemeral_user']) del kwargs['ephemeral_user'] return LowLevelSlackClient.get_instance( ).web_client.chat_postEphemeral(channel=channel_id, user=ephemeral_user_id, text=text, **kwargs) else: return LowLevelSlackClient.get_instance( ).web_client.chat_postMessage(channel=channel_id, text=text, **kwargs)
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 = LowLevelSlackClient() puts("Initializing storage using backend: {}".format( self._settings['STORAGE_BACKEND'])) self._storage = Storage.get_instance() logger.debug("Storage initialized!") self._plugin_actions = {'listen_to': {}, 'respond_to': {}} self._help = {'human': {}, 'robot': {}} self._dispatcher = EventDispatcher(self._plugin_actions, self._settings) puts("Loading plugins...") self.load_plugins() logger.debug("The following plugin actions were registered: %s", self._plugin_actions)
class EventDispatcher: def __init__(self, plugin_actions, settings=None): self._client = LowLevelSlackClient() self._plugin_actions = plugin_actions 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): RTMClient.on(event='pong', callback=self.pong) RTMClient.on(event='message', callback=self.handle_message) self._client.start() def pong(self, **kwargs): logger.debug("Server Pong!") def handle_message(self, **payload): # Handle message listeners event = payload['data'] # Also account for changed messages if 'message' in event: event['user'] = event['message']['user'] event['text'] = event['message']['text'] if 'user' in event and not event['user'] == self._get_bot_id(): listeners = self._find_listeners('listen_to') 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: self._dispatch_listeners(listeners, event) def _find_listeners(self, _type): return list(self._plugin_actions[_type].values()) @staticmethod def _gen_message(event, plugin_class_name): return Message(SlackClient(), event, plugin_class_name) def _get_bot_id(self) -> str: return self._client.bot_info['id'] def _get_bot_name(self) -> str: return self._client.bot_info['name'] def _check_bot_mention(self, event: Dict[str, Any]) -> Optional[Dict[str, Any]]: 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: List[Dict[str, Any]], event: Dict[str, Any]): for listener in listeners: matcher = listener['regex'] # Check if this is a message subtype, and if so, if the listener should handle it if listener[ 'handle_changed_message'] and 'subtype' in event and event[ 'subtype'] == 'message_changed': # Check the new message text for a match match = matcher.search(event['message'].get('text', '')) else: match = matcher.search(event.get('text', '')) if match: message = self._gen_message(event, listener['class_name']) listener['function'](message, **match.groupdict())
def send_dm(self, user: Union[User, str], text: str, **kwargs): user_id = id_for_user(user) dm_channel_id = self.open_im(user_id) return LowLevelSlackClient.get_instance().web_client.chat_postMessage( channel=dm_channel_id, text=text, as_user=True, **kwargs)
def open_im(self, user: Union[User, str]) -> str: user_id = id_for_user(user) response = LowLevelSlackClient.get_instance( ).web_client.conversations_open(users=user_id) return response['channel']['id']
def react(self, channel: Union[Channel, str], ts: str, emoji: str): channel_id = id_for_channel(channel) return LowLevelSlackClient.get_instance().web_client.reactions_add( name=emoji, channel=channel_id, timestamp=ts)
def channels(self) -> Dict[str, Channel]: return LowLevelSlackClient.get_instance().channels
def users(self) -> Dict[str, User]: return LowLevelSlackClient.get_instance().users
def bot_info(self) -> Dict[str, str]: return LowLevelSlackClient.get_instance().bot_info
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 = LowLevelSlackClient() puts("Initializing storage using backend: {}".format( self._settings['STORAGE_BACKEND'])) self._storage = Storage.get_instance() logger.debug("Storage initialized!") self._plugin_actions = {'listen_to': {}, 'respond_to': {}} self._help = {'human': {}, 'robot': {}} self._dispatcher = EventDispatcher(self._plugin_actions, self._settings) puts("Loading plugins...") self.load_plugins() logger.debug("The following plugin actions were registered: %s", self._plugin_actions) 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(SlackClient(), self._settings, 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 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'] RTMClient.on(event=event_type, callback=callable_with_sanitized_event(fn)) if action in ['respond_to', 'listen_to']: for regex, handle_changed_message in zip( config['regex'], config['handle_changed_message']): event_handler = { 'class': cls_instance, 'class_name': plugin_class, 'function': fn, 'regex': regex, 'handle_changed_message': handle_changed_message } key = "{}-{}-{}".format(fq_fn_name, regex.pattern, handle_changed_message) self._plugin_actions[action][key] = event_handler self._help['robot'][class_help].append( self._parse_robot_help(regex, handle_changed_message, 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, handle_changed_message, action): if action == 'respond_to': return "@botname {}{}".format( regex.pattern, " [includes changed messages]" if handle_changed_message else "") else: return "{}{}".format( regex.pattern, " [includes changed messages]" if handle_changed_message else "") def _keepalive(self): while True: time.sleep(self._settings['KEEP_ALIVE']) self._client.ping() logger.debug("Client Ping!") def run(self): announce("\nStarting Slack Machine:") with indent(4): 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()