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,
     )
Exemple #2
0
 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)
Exemple #3
0
    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())
Exemple #5
0
    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)
Exemple #6
0
 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']
Exemple #7
0
 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)
Exemple #8
0
 def channels(self) -> Dict[str, Channel]:
     return LowLevelSlackClient.get_instance().channels
Exemple #9
0
 def users(self) -> Dict[str, User]:
     return LowLevelSlackClient.get_instance().users
Exemple #10
0
 def bot_info(self) -> Dict[str, str]:
     return LowLevelSlackClient.get_instance().bot_info
Exemple #11
0
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()