def __init__(self, bot_user_token, bot_id=None):
        self.name = BOT_NAME
        self.bot_id = bot_id
        if not self.bot_id:
            # Read the bot's id by calling auth.test
            response = WebClient(token=bot_user_token).api_call('auth.test')
            self.bot_id = response['user_id']
            logger.info(f'My bot_id is {self.bot_id}')

        # Create an instance of the RTM client
        self.sc = RTMClient(token=bot_user_token, run_async=True)

        # Connect our callback events to the RTM client
        RTMClient.run_on(event="hello")(self.on_hello)
        RTMClient.run_on(event="message")(self.on_message)
        RTMClient.run_on(event="goodbye")(self.on_goodbye)

        # startup our client event loop
        self.future = self.sc.start()
        self.bot_start = dt.now()
        self.msg_lock = Lock()
        self.at_bot = f'<@{self.bot_id}>'
        self.comic_history = []
        logger.info("Created new SlackClient Instance")
        self.xkcd = XkcdApi()
示例#2
0
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)
示例#3
0
class Moonbeam:
    def __init__(self):
        logging.basicConfig(
            level=os.environ.get("LOGLEVEL", "DEBUG"),
            format=
            '%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s',
        )
        self.__log = logging.getLogger(type(self).__name__)
        self.__log.info("Starting Moonbeam")
        self.__config = self.__load_config(prefix="Moonbeam")
        if 'BOT_TOKEN' not in self.__config:
            raise RuntimeError("BOT_TOKEN not found in config")
        self.__web_client = WebClient(self.__config['BOT_TOKEN'])
        self.__rtm_client = RTMClient(
            token=self.__config.get("BOT_TOKEN"),
            ping_interval=30,
            auto_reconnect=True,
        )
        self.__trigger_words = []
        self.__plugins = []
        self.__load_plugins()
        self.__rtm_client.run_on(event='message')(self.__process_message)
        self.__rtm_client.run_on(event='user_typing')(self.__process_typing)
        self.__rtm_client.run_on(event='reaction_added')(
            self.__process_reaction_added)
        self.__rtm_client.run_on(event='reaction_removed')(
            self.__process_reaction_removed)
        self.__rtm_client.start()

    def __post_message(self, response, conditionals=None):
        channel = response.get('channel')
        as_user = response.get('as_user')
        self.__log.info(
            f"Posting message in channel {channel} (as_user={as_user}):")
        try:
            if 'text' in response.keys() or 'blocks' in response.keys():
                text = response.get('text')
                if response.get('emojify') or \
                        ('emojify' not in response.keys() and \
                        round(time()) % 50 == 0): # 1/50th of the time randomly
                    text = moonbeam_utils.emojify(text)
                mappings = response.get('mappings')
                if mappings:
                    if '@CHANNEL@' in text and '@CHANNEL@' in mappings.keys():
                        channel_name = mappings.get('@CHANNEL@')
                        channel_id = 'UNKNOWN'
                        channels = self.__web_client.conversations_list()
                        for ch in channels.get('channels'):
                            if ch.get('name') == channel_name:
                                channel_id = ch.get('id')
                                break
                        mappings['@CHANNEL@'] = channel_id
                    text = self.__replace_vars(response.get('text'), mappings)
                slack_response = self.__web_client.chat_postMessage(
                    channel=channel,
                    text=text,
                    attachments=response.get('attachments'),
                    blocks=response.get('blocks'),
                    as_user=as_user,
                )
                if conditionals and conditionals.get(True):
                    self.__post_message(conditionals.get(True))
            elif 'name' in response.keys():
                slack_response = self.__web_client.reactions_add(
                    channel=channel,
                    name=response.get('name'),
                    timestamp=response.get('timestamp'),
                )
            else:
                raise RuntimeError(
                    f"Unable to process response: {json.dumps(response, indent=2)}"
                )
        except SlackApiError as e:
            if conditionals and conditionals.get(False):
                if not mappings:
                    mappings = response.get('mappings', {})
                mappings['@ERROR@'] = traceback.format_exc()
                response = conditionals.get(False)
                response['text'] = self.__replace_vars(response.get('text'),
                                                       mappings)
                self.__post_message(response)
            else:
                self.__log.exception(
                    f"Encountered a Slack API Error posting message: {e.response['error']}"
                )

    def __replace_vars(self, message, mappings):
        for key in mappings.keys():
            message = message.replace(key, mappings[key])
        return message

    def __process_message(self, **payload):
        message = payload['data']
        self.__web_client = payload['web_client']
        self.__log.info(json.dumps(message, indent=2))
        for plugin in self.__plugins:
            try:
                responses = plugin.receive(message)
                if isinstance(responses, dict):
                    self.__post_message(responses)
                elif isinstance(responses, list):
                    if len(responses) == 2 and isinstance(
                            responses[1], dict) and isinstance(
                                responses[1].get(True), dict):
                        [responses, conditionals] = responses
                        for response in responses:
                            self.__post_message(response, conditionals)
                    else:
                        for response in responses:
                            if isinstance(response, dict) and response:
                                self.__post_message(response)
                else:
                    self.__log.debug(
                        f"{plugin.__class__.__name__} didn't need to do anything with that message"
                    )
            except Exception as e:
                self.__log.exception(
                    f"Encountered an exception with {plugin.__class__.__name__} responding to message: {e}"
                )

    def __process_typing(self, **payload):
        data = payload['data']
        self.__log.debug(json.dumps(data, indent=2))
        for plugin in self.__plugins:
            try:
                responses = plugin.typing(data)
                if responses:
                    for response in responses:
                        self.__post_message(response)
            except Exception as e:
                self.__log.exception(
                    f"Encountered an exception with {plugin.__class__.__name__} responding to typing: {e}"
                )

    def __process_reaction_added(self, **payload):
        data = payload['data']
        self.__log.debug(json.dumps(data, indent=2))
        for plugin in self.__plugins:
            try:
                responses = plugin.reaction(data, added=True)
                if responses:
                    for response in responses:
                        self.__post_message(response)
            except Exception as e:
                self.__log.exception(
                    f"Encountered an exception with {plugin.__class__.__name__} responding to reaction added: {e}"
                )

    def __process_reaction_removed(self, **payload):
        data = payload['data']
        self.__log.debug(json.dumps(data, indent=2))
        for plugin in self.__plugins:
            try:
                responses = plugin.reaction(data, added=False)
                if responses:
                    for response in responses:
                        self.__post_message(response)
            except Exception as e:
                self.__log.exception(
                    f"Encountered an exception with {plugin.__class__.__name__} responding to reaction removed: {e}"
                )

    def __load_config(self, prefix):
        config = {}
        config_file = os.path.join(os.path.dirname(__file__), 'config.json')
        # Process config file first, if present
        if os.path.isfile(config_file):
            try:
                with open(
                        os.path.join(os.path.dirname(__file__), 'config.json'),
                        'r') as f:
                    config = json.load(f)
            except Exception as e:
                self.__log.warning(e)
        # Env vars override config file settings, if present
        for key, value in os.environ.items():
            if key.startswith(f"{prefix}_"):
                subkey = "_".join(key.split('_')[1:])
                try:
                    config[subkey] = json.loads(value)
                except json.decoder.JSONDecodeError:
                    config[subkey] = value
        return config

    def __load_plugins(self):
        active_plugins = self.__config.get('PLUGINS')
        if not active_plugins:
            self.__log.debug(
                "No plugins specified in config file/environment variables")
            return
        elif not isinstance(active_plugins, list) and not isinstance(
                active_plugins, str):
            self.__log.debug(
                "Plugins specified in config file/environment variables must be a (JSON) list or a comma-delimited string"
            )
            return

        if isinstance(active_plugins, str):
            active_plugins = active_plugins.split(",")

        for plugin_path in active_plugins:
            try:
                module_path, class_name = plugin_path.rsplit('.', 1)
                module = import_module(module_path)
                cls = getattr(module, class_name)
            except AttributeError:
                raise ImportError(
                    f"Module {module_path} does not define a \"{class_name}\" attribute/class"
                )
            except ValueError:
                raise ImportError(
                    f"{plugin_path} doesn't look like a module path")
            except ImportError as error:
                raise ImportError(f"Problem importing {plugin_path} - {error}")
            plugin_config = self.__config.get(cls.__name__, {})
            if not plugin_config:
                plugin_config = self.__load_config(prefix=cls.__name__)
            plugin_config['BOT_ID'] = self.__config['BOT_ID']
            self.__log.debug(f"Loading class {cls.__name__}")
            plugin = cls(web_client=self.__web_client,
                         plugin_config=plugin_config)
            self.__trigger_words.extend(plugin.get_trigger_words())
            self.__plugins.append(plugin)
            self.__log.debug(f"Plugin registered: {plugin}")
        for plugin in self.__plugins:
            plugin.store_global_trigger_words(self.__trigger_words)
示例#4
0
class Slack2rtmInterface(Interface):

    def name(self):
        """Return the name of this interface."""
        return "Slack2rtm"

    def setup(self, config):
        self.log.info("Set up Slack2 rtm interface: {}".format(config.id))
        self.last_ping = 0
        self.token = config.api_token
        self.username = config.username
        self.client = None
        self.webClient = None
        # Internal mappings.
        self.channel_by_uid = dict()  # User ID => channel ID
        self.thread_by_uid = dict()

    def connect(self):
        ssl_context = ssl.create_default_context()
        ssl_context.check_hostname = False
        ssl_context.verify_mode = ssl.CERT_NONE
        self.client  = RTMClient(token=self.token, ssl=ssl_context)
        self.client.run_on(event='message')(self.GetMessage)
        self.client.start()
        
    def do_one_loop(self):
        """Look for new messages."""
        time.sleep(1) #Loop does nothing in V2.since it doesnt currently get hit, but saw large IO / CPU on older version

    def GetMessage(self, **payload):
        print(payload)
        message = payload['data']
        self.webClient = payload['web_client']
        print(message)
        self.handle_api_message(message)


    def handle_api_message(self, message):
        """Handle an incoming message from Slack."""
        #if "type" in message:
        if 'text' in message and 'subtype' not in message:
            self.handle_message(message)

    def handle_message(self, message, in_channel=False):
        """Handle a message that will actually elicit a response."""
        # Store their channel ID for the response.
        print("handle Message")
        self.channel_by_uid[message["user"]] = message["channel"]
        self.thread_by_uid[message["user"]] = message['ts']
        # User name no longer returned on new platform
        username = message["user"]

        # Format the message.
        message_data = message
        message = re.sub(
            r'^{username}\s*|<?@{username}>?'.format(username=self.username),
            '',
            message["text"]
        )
        if len(message.strip()) > 0:
            # Handle commands.
            if not self.slack_commands(username, message, message_data, in_channel):
                self.on_message(
                    username=username,
                    message=message,
                )

    def send_message(self, username, message):
        """Send a message to the user."""
        self.log.debug("Send Slack message to [{}] {}".format(
            username, message,
        ))
        print(message)
        try:
            self.webClient.chat_postMessage(channel=self.channel_by_uid[username],text= message,thread_ts=self.thread_by_uid[username])
        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']}")


    def slack_commands(self, username, message, data, in_channel):
        if in_channel:
            if message.startswith("!leave"):
                # Leaving a channel.
                channel = data["channel"]
                self.log.info("Asked to leave channel {} by {}".format(channel, username)) 
                self.log.debug("Slack API:{}".format(self.webClient.api_call("channels.leave", channel=channel)))
                return True
        return False
示例#5
0
class Ops_Bot:
    # TARGET_CHANNEL = '#ops-robot-test'
    TARGET_CHANNEL = '#operations'
    TRIGGER_STRINGS = [
    "funder", "funders", "funding"
    ]
    BOT_INTRO = f"""
    *OPSBOT* has been activated...

    I can update the <https://docs.google.com/spreadsheets/d/{TARGET_SHEET}#gid=0|NPR Funding Credit Schedule> for you. 

    *Just type:* `@opsbot update NPR Funding Credit Schedule` to activate.
    (or `@opsbot funding`. That's much easier.)
    """
    SHUTDOWN_MESSAGE = """*OPSBOT* deactivated..."""
    DEFAULT_EXT_FUNCS = (download_input_files, write)

    def __init__(self, token, external_functions=None):
        self.token = token
        self.external_functions = external_functions or self.DEFAULT_EXT_FUNCS

        self.web_client = WebClient(token=self.token)
        self.rtm_client = RTMClient(token=self.token)

        self.bot_id = self._connect()
        self.bot_id_text = f'<@{self.bot_id}>'

        # Decorate the message handler
        self.rtm_client.run_on(event='message')(self._message_handler)

    # @RTMClient.run_on(event='message')
    def _message_handler(self, **payload):

        data = payload.get('data')
        raw_message_text = data.get('text')

        if self.bot_id_text not in raw_message_text:
            return

        message_word_set = self._get_message_set(raw_message_text)

        if message_word_set.intersection(set(self.TRIGGER_STRINGS)):
            self._send_message("I'm on it!")
            try:
                self.run_external()
            except Exception as e:
                self._send_message(f"This is not going very well...\nERROR: {e}")
            self._send_task_confirmation(data)

    def start_bot(self):
        print('Starting Ops Bot...')
        self._introduce_yourself()
        try:
            self.rtm_client.start()
        except KeyboardInterrupt:
            print('Ops Bot Shutting Down...')
            self._send_message(self.SHUTDOWN_MESSAGE, self.TARGET_CHANNEL)
    
    def run_external(self):
        for func in self.external_functions:
            func()

    def _connect(self):
        response = self.web_client.rtm_connect(with_team_state=False)
        return response.get('self').get('id')

    def _introduce_yourself(self):
        self.web_client.chat_postMessage(
            channel=self.TARGET_CHANNEL,
            text=self.BOT_INTRO
        )

    def _send_task_confirmation(self, data):
        user = data.get('user')
        channel = data.get('channel')
        confirmation_string = f"OK <@{user}>!\nNPR Funding Credit Schedule is updated."
        self._send_message(confirmation_string, channel)

    def _send_message(self, message_text, channel=None):
        return self.web_client.chat_postMessage(channel=channel or self.TARGET_CHANNEL, text=message_text)

    def _process_message(self, input_text: str) -> str:
        return input_text.replace('\\xa0', '').lower()

    def _get_message_set(self, input_text: str) -> set:
        return set(self._process_message(input_text).split())
示例#6
0
class Botovkov:
    def __init__(self):
        """

        """
        print("Starting Botovkov ...")
        self.reddit = praw.Reddit(client_id=os.environ["REDDIT_ID"],
                                  client_secret=os.environ["REDDIT_SECRET"],
                                  password=os.environ["REDDIT_PASSWORD"],
                                  user_agent="Maxim",
                                  username=os.environ["REDDIT_USERNAME"])
        self.slack_token = os.environ["SLACK_API_TOKEN"]
        self.rtm_client = RTMClient(token=self.slack_token)
        self.rtm_client.run_on(event='message')(self.process_incoming_msg)
        self.joke_count = 0
        self.fun_fact_count = 0
        self.jokes = None
        self.facts = None
        self.corona_data = None

    def run(self):
        """

        :return:
        """
        print("Running command listener ...")
        self.rtm_client.start()

    def process_incoming_msg(self, **kwargs):
        """

        :param kwargs:
        :return:
        """
        print("Received a message. Processing.")
        if "data" in kwargs:
            data = kwargs["data"]
            if "text" in data:
                msg = data["text"]
                if msg.startswith("$"):
                    command, param = self.extract_command(msg)
                    if hasattr(self, command):
                        msg = getattr(self, command)(param.lower())
                    else:
                        msg = self.default_msg(command)
                    web_client = kwargs['web_client']
                    channel_id = data["channel"]
                    for line in msg:
                        self.respond(web_client, channel_id, line)
                        time.sleep(3)

    @staticmethod
    def extract_command(full_msg):
        user_input = full_msg.split(" ")
        command = user_input[0][1:]
        if len(user_input) > 1:
            param = " ".join(full_msg.split(" ")[1:])
        else:
            param = ""
        return command, param

    @staticmethod
    def respond(web_client, channel, msg):
        """

        :param web_client:
        :param channel:
        :param msg:
        :return:
        """
        web_client.chat_postMessage(channel=channel, text=msg)

    @staticmethod
    def default_msg(command):
        """

        :return:
        """
        return [
            "Command ${} does not exist. Ask the BIG BOI to implement it for you."
            .format(command)
        ]

    def joke(self, param):
        """

        :return:
        """
        if self.jokes is None:
            hot = self.reddit.subreddit("DirtyJokes").hot()
            self.jokes = []
            for post in hot:
                if post.selftext != "":
                    self.jokes.append([post.title, post.selftext])
                else:
                    self.jokes.append([post.title, "Ha ha"])
        self.joke_count += 1
        if self.joke_count <= len(self.jokes):
            return self.jokes[self.joke_count - 1]
        else:
            self.jokes = None
            return self.joke()

    def funfact(self, param):
        """

        :return:
        """
        if self.facts is None:
            hot = self.reddit.subreddit("todayilearned").hot()
            self.facts = [[post.title, post.url] for post in hot]
        self.fun_fact_count += 1
        if self.fun_fact_count <= len(self.facts):
            return self.facts[self.fun_fact_count - 1]
        else:
            self.facts = None
            return self.funfact()

    def curse(self, language):
        """

        :param language:
        :return:
        """
        if language is not None:
            if language in curses.curses:
                return [
                    curses.curses[language][randint(
                        0,
                        len(curses.curses[language]) - 1)]
                ]
        else:
            key = randint(0, len(curses.curses) - 1)
            lst = curses.curses[list(curses.curses)[key]]
            return [lst[randint(0, len(lst) - 1)]]
            # return [curses.curses[lang][randint(0, len(curses.curses[lang]))]]

    def corona(self, country=""):
        """

        :param country:
        :return:
        """
        if self.corona_data is None:
            user_agent = "Mozilla / 5.0(Windows NT 10.0; Win64; x64; rv: 73.0) Gecko / 20100101 Firefox / 73.0"
            headers = {"User-Agent": user_agent}
            url = "https://coronavirus-6728.gserveri.workers.dev/"

            req = ur.Request(url=url, headers=headers)
            html = ur.urlopen(req)

            data = json.loads(html.read().decode())
            self.corona_data = corona.World(world=True, *data)

        if country == "":
            return [str(self.corona_data)]
        else:
            if country in self.corona_data.countries:
                return [str(self.corona_data.countries[country])]
            else:
                return ["No data on country: {}".format(country)]