Example #1
0
class RTMClient(Singleton):
    @wraps(SlackRTMClient.__init__)
    def __init__(self, token, *args, **kwargs):
        super().__init__()
        self._client = SlackRTMClient(token=token,
                                      run_async=True,
                                      loop=asyncio.get_running_loop(),
                                      *args,
                                      **kwargs)
        old_os_name = os.name
        os.name = 'nt'
        self._client.start()
        os.name = old_os_name

    @classmethod
    @wraps(SlackRTMClient.__init__)
    def start(cls, token, *args, **kwargs):
        return cls.instantiate(token, *args, **kwargs)._client

    @classmethod
    async def stop(cls):
        await cls.instance()._client.async_stop()

    @classmethod
    def on(cls, event):
        def decorator(callback):
            @SlackRTMClient.run_on(event=event)
            @wraps(callback)
            async def wrapper(**payload):
                await callback(**payload)

            return wrapper

        return decorator
    async def test_issue_611(self):
        channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID]
        text = "This message was sent by <https://slack.dev/python-slackclient/|python-slackclient>! (test_issue_611)"

        self.message_count, self.reaction_count = 0, 0

        async def process_messages(**payload):
            self.logger.info(payload)
            if "subtype" in payload["data"] and payload["data"][
                    "subtype"] == "message_replied":
                return  # skip

            self.message_count += 1
            raise Exception("something is wrong!"
                            )  # This causes the termination of the process

        async def process_reactions(**payload):
            self.logger.info(payload)
            self.reaction_count += 1

        rtm = RTMClient(token=self.bot_token, run_async=True)
        RTMClient.on(event='message', callback=process_messages)
        RTMClient.on(event='reaction_added', callback=process_reactions)

        web_client = WebClient(token=self.bot_token, run_async=True)
        message = await web_client.chat_postMessage(channel=channel_id,
                                                    text=text)
        ts = message["ts"]

        await asyncio.sleep(3)

        # intentionally not waiting here
        rtm.start()

        try:
            await asyncio.sleep(3)

            first_reaction = await web_client.reactions_add(channel=channel_id,
                                                            timestamp=ts,
                                                            name="eyes")
            self.assertFalse("error" in first_reaction)
            await asyncio.sleep(2)

            should_be_ignored = await web_client.chat_postMessage(
                channel=channel_id, text="Hello?", thread_ts=ts)
            self.assertFalse("error" in should_be_ignored)
            await asyncio.sleep(2)

            second_reaction = await web_client.reactions_add(
                channel=channel_id, timestamp=ts, name="tada")
            self.assertFalse("error" in second_reaction)
            await asyncio.sleep(2)

            self.assertEqual(self.message_count, 1)
            self.assertEqual(self.reaction_count, 2)
        finally:
            if not rtm._stopped:
                rtm.stop()
def main():
  flags.mark_flag_as_required('customer')

  CUSTOMER_NAME = FLAGS.customer
  customer_codes ,msg_queue = cfg.read_config()

  rtm_client = RTMClient(token=customer_codes['customer1'])
  print(f'logging {FLAGS.customer}:{customer_codes["customer1"]}')  
  rtm_client.start()
Example #4
0
 def slack_bot(self):
     print('\033[0;32m' + "[!] Starting slack bot!\033[0m")
     try:
         self.slack_data['text'] = 'Ready for action!'
         requests.post(url='https://slack.com/api/chat.postMessage',
                       data=self.slack_data)
         rtm_client = RTMClient(token=os.environ.get('SLACK_BOT_TOKEN'))
         rtm_client.start()
     except:
         pass
Example #5
0
    def main():
        @RTMClient.run_on(event='message')
        def handle(**kwargs):
            data = kwargs['data']   
            text = data['text'] 
            if data['user'] != __self_user_id and len(text) is not 0 and text.startswith('!'):
                command.runCommand(kwargs)

        rtm_client = RTMClient(token=__slack_token)
        rtm_client.start()
Example #6
0
def start_work(retries=0):
    try:
        rtm_client = RTMClient(token=os.environ["SLACKBOT_API_TOKEN"],
                               auto_reconnect=True)
        rtm_client.start()
    except Exception as e:
        print(f'main loop crash {e}, try to restart, attempt {retries}')
        if retries == 3:
            raise e
        time.sleep(retries * 15)
        start_work(retries + 1)
Example #7
0
    async def test_issue_558(self):
        channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID]
        text = "This message was sent by <https://slack.dev/python-slackclient/|python-slackclient>! (test_issue_558)"

        self.message_count, self.reaction_count = 0, 0

        async def process_messages(**payload):
            self.logger.debug(payload)
            self.message_count += 1
            await asyncio.sleep(10)  # this used to block all other handlers

        async def process_reactions(**payload):
            self.logger.debug(payload)
            self.reaction_count += 1

        rtm = RTMClient(token=self.bot_token, run_async=True)
        RTMClient.on(event='message', callback=process_messages)
        RTMClient.on(event='reaction_added', callback=process_reactions)

        web_client = WebClient(token=self.bot_token, run_async=True)
        message = await web_client.chat_postMessage(channel=channel_id,
                                                    text=text)
        self.assertFalse("error" in message)
        ts = message["ts"]
        await asyncio.sleep(3)

        # intentionally not waiting here
        rtm.start()
        await asyncio.sleep(3)

        try:
            first_reaction = await web_client.reactions_add(channel=channel_id,
                                                            timestamp=ts,
                                                            name="eyes")
            self.assertFalse("error" in first_reaction)
            await asyncio.sleep(2)

            message = await web_client.chat_postMessage(channel=channel_id,
                                                        text=text)
            self.assertFalse("error" in message)
            # used to start blocking here

            # This reaction_add event won't be handled due to a bug
            second_reaction = await web_client.reactions_add(
                channel=channel_id, timestamp=ts, name="tada")
            self.assertFalse("error" in second_reaction)
            await asyncio.sleep(2)

            self.assertEqual(self.message_count, 1)
            self.assertEqual(self.reaction_count, 2)  # used to fail
        finally:
            if not rtm._stopped:
                rtm.stop()
Example #8
0
 def test_issue_530(self):
     try:
         rtm_client = RTMClient(token="I am not a token", run_async=False, loop=asyncio.new_event_loop())
         rtm_client.start()
         self.fail("Raising an error here was expected")
     except Exception as e:
         self.assertEqual(
             "The request to the Slack API failed.\n"
             "The server responded with: {'ok': False, 'error': 'invalid_auth'}", str(e))
     finally:
         if not rtm_client._stopped:
             rtm_client.stop()
Example #9
0
class TestRTMClient(unittest.TestCase):
    """Runs integration tests with real Slack API

    https://github.com/slackapi/python-slackclient/issues/605
    """
    def setUp(self):
        self.logger = logging.getLogger(__name__)
        self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN]
        self.channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID]
        self.rtm_client = RTMClient(token=self.bot_token, run_async=False)

    def tearDown(self):
        # Reset the decorators by @RTMClient.run_on
        RTMClient._callbacks = collections.defaultdict(list)

    @pytest.mark.skipif(condition=is_not_specified(), reason="still unfixed")
    def test_issue_605(self):
        self.text = "This message was sent to verify issue #605"
        self.called = False

        @RTMClient.run_on(event="message")
        def process_messages(**payload):
            self.logger.info(payload)
            self.called = True

        def connect():
            self.logger.debug("Starting RTM Client...")
            self.rtm_client.start()

        t = threading.Thread(target=connect)
        t.setDaemon(True)
        try:
            t.start()
            self.assertFalse(self.called)

            time.sleep(3)

            self.web_client = WebClient(
                token=self.bot_token,
                run_async=False,
                loop=asyncio.new_event_loop(
                ),  # TODO: this doesn't work without this
            )
            new_message = self.web_client.chat_postMessage(
                channel=self.channel_id, text=self.text)
            self.assertFalse("error" in new_message)

            time.sleep(5)
            self.assertTrue(self.called)
        finally:
            t.join(.3)
Example #10
0
def run_bot():
    token = os.environ.get('SLACKBOT_TOKEN')
    report_url = os.environ.get('SLACKBOT_REPORT_URL')

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

    global metrics
    metrics = None
    global channels
    channels = {
        u['id']: u['name']
        for u in web_client.channels_list()['channels']
    }
    global users
    users = {u['id']: u['name'] for u in web_client.users_list()['members']}

    if report_url:
        # Reports are enabled, so start reporting thread
        global last_run
        last_run = None
        metrics = defaultdict(int)

        class ReportingThread(threading.Thread):
            def run(self):
                while True:
                    curr_min = datetime.utcnow().minute
                    global last_run
                    global metrics
                    if last_run == curr_min:
                        sleep(5)
                        continue
                    last_run = curr_min
                    if not metrics:
                        continue
                    try:
                        metrics = json.dumps(metrics)
                        resp = post(url=report_url, json=dict(text=metrics))
                        resp.raise_for_status()
                    except Exception as exc:
                        print(exc)
                    metrics = defaultdict(int)

        reporting = ReportingThread(name='Reporting Thread')
        reporting.start()

    while True:
        try:
            rtm_client.start()
        except Exception as exc:
            logging.error('Exception during rtm_client.start', exc)
def main():
    """
    Startup logic and the main application loop to monitor Slack events.
    """

    # build the text model
    model = build_text_model()

    # Create the slackclient instance
    sc = WebClient(BOT_TOKEN)

    # check Slack API connection
    if not sc.rtm_connect():
        raise Exception("Couldn't connect to slack.")

    @RTMClient.run_on(event='message')
    def echo_msg(**payload):
        nonlocal model

        # Get WebClient so you can communicate back to Slack
        sc = payload['web_client']

        data = payload['data']
        # use get() to avoid key missing error, esp from json (python dict) parsing
        user_id = data.get('user')
        channel_id = data.get('channel')
        message = data.get('text')

        # Since message event catches all messages sent to slack, including those from bot,
        # user_id and message are checked
        if user_id and message:
            print(
                f"receive message from user {user_id} in channel {channel_id}")

            if "parrot me" in message.lower():
                markov_chain = model.make_sentence(
                ) or "I don't know what to say."
                sc.chat_postMessage(channel=channel_id,
                                    text=format_message(markov_chain))

            if "level up parrot" in message.lower():
                # Fetch new messages.  If new ones are found, rebuild the text model
                if update_corpus(sc, channel_id) > 0:
                    model = build_text_model()

    # Where the magic happens
    rtm_client = RTMClient(token=BOT_TOKEN)
    rtm_client.start()
Example #12
0
    def main():
        @RTMClient.run_on(event='message')
        def handle(web_client=None, data=None, **kwargs):
            print(f'Message data: {data}')

            if __should_handle(user=data.get('user'), text=data.get('text')):
                handle_message(data, web_client)

        global __web_client

        __web_client = WebClient(token=__slack_token)
        __rtm_client = RTMClient(token=__slack_token)
        __rtm_future = None

        while True:
            try:
                __rtm_future = __rtm_client.start()
                print("Learning bot is connected!")
                break
            except Exception:
                print(
                    "Failed to connect to Slack; retrying in 5 seconds\n\n\n\n\n"
                )
                time.sleep(5)

        asyncio.gather(__rtm_future, schedule_monitor())
Example #13
0
def run():
    import asyncio

    rtm_client = RTMClient(token=os.environ["SLACK_API_TOKEN"], run_async=True)
    loop = asyncio.get_event_loop()
    loop.set_debug(True)
    loop.run_until_complete(rtm_client.start())
Example #14
0
    def runforever(self):
        from slack import RTMClient
        @RTMClient.run_on(event='message')
        def handle_message(**payload):
            if 'bot_id' in payload['data']:
                return
            message = SlackMessage(payload)
            for func in self.handlers:
                resp_text = func(message)
                if resp_text:
                    if isinstance(resp_text, str):
                        self.say(message.channel.id, resp_text)
                    break

        rtm_client = RTMClient(token=self.token)
        rtm_client.start()
Example #15
0
    def test_02(self):
        logger = FoxylibLogger.func_level2logger(self.test_02, logging.DEBUG)

        loop = asyncio.get_event_loop()
        rtm_client = RTMClient(token=FoxylibSlack.xoxb_token(),
                               run_async=True,
                               loop=loop)

        async def inf_loop():
            logger = logging.getLogger()
            while 1:
                try:
                    logger.info("Ping Pong! I'm alive")
                    await asyncio.sleep(900)
                except asyncio.CancelledError:
                    break

        tasks = asyncio.gather(rtm_client.start(), inf_loop())

        def callback(signum, frame):
            tasks.cancel()
            logger.warning("Cancelling tasks...")

        # loop.add_signal_handler(signal.SIGINT, callback)
        signal.signal(signal.SIGINT, callback)
        signal.signal(signal.SIGTERM, callback)

        try:
            loop.run_until_complete(tasks)
        except asyncio.CancelledError as e:
            logger.error(e)
        finally:
            logger.info("Quitting... Bye!")
Example #16
0
        async def slack_main():
            loop = asyncio.get_event_loop()
            rtm_client = RTMClient(token=FoxylibSlack.xoxb_token(), run_async=True, loop=loop)

            executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
            await asyncio.gather(
                loop.run_in_executor(executor, partial(sync_loop, rtm_client)),
                rtm_client.start()
            )
Example #17
0
def runMockMission():
  #operator command testing
  #moonRanger.push_waypoint(20,20,2,"southern region")
  #moonRanger.push_waypoint(20,5,2,"out")
  #moonRanger.push_waypoint(0,0,2,"home region")
  #moonRanger.drive_trek()
  #moonRanger.map.printMap(moonRanger.map.driveRecord)
  #moonRanger.get_telemetry_log()
  #moonRanger.get_nss_data()
  #print(moonRanger.checkSolarEff())
  #moonRanger.get_solar_status()
  #moonRanger.get_battery_status()
  #print(moonRanger.get_comm_status())

  # SLACK_BOT_TOKEN must be set from terminal using 'export SLACK_BOT_TOKEN='token'
  # Start slack bot client
  rtm_client = RTMClient(token=os.environ["SLACK_BOT_TOKEN"])
  rtm_client.start()
Example #18
0
    def main():
        @RTMClient.run_on(event='message')
        def handle(**kwargs):
            try:
                text = kwargs['data']['text']

            except KeyError:
                return
            except Exception as e:
                print(e)
                return

            if text:
                slackutils.CLIENT = kwargs['web_client']
                cmd.EVAL(kwargs['data'])

        rtm_client = RTMClient(token=SLACK_TOKEN)
        rtm_client.start()
Example #19
0
class SlackClient(object):
    token = None
    _client = None
    web_client: WebClient = None
    callbacks = defaultdict(list)

    def __init__(self, token: str):
        self.token = token
        self.web_client = WebClient(token=self.token)

    @property
    def client(self) -> RTMClient:
        assert self._client, "Client must be initialized first"
        return self._client

    @client.setter
    def client(self, client):
        self._client = client

    def connect(self):
        self.client = RTMClient(token=self.token)
        self.add_callback("open", self._on_open)
        self.client.start()

    def add_callback(self, event_type: str, callback: Callable):
        self.callbacks[event_type].append(callback)
        RTMClient.on(event=event_type, callback=callback)

    def emails_to_user_ids(self, emails: List[str]):
        for email in emails:
            user = self.web_client.users_lookupByEmail(email=email)
            print(user)

    def subscribe_to_presence(self, user_ids: List[str]):
        res = self.client.send_over_websocket(payload={
            "type": "presence_sub",
            "ids": user_ids,
        })

    def _on_open(self, **payload: dict):
        self.slack_team = payload["data"]["team"]
        self.slack_self = payload["data"]["self"]
        self.web_client = payload["web_client"]
Example #20
0
class SlackBot(object):
    def __init__(self, name: str, pattern: str, remarks_location: str,
                 **kwargs):
        self.name = name
        self.pattern = pattern
        self.remarks_location = remarks_location

        self.slack_id = None
        self._rtm_client = RTMClient(**kwargs)
        self.slack_client = WebClient(**kwargs)

        self.remarks = self.get_file_contents()

    def start(self):
        self._rtm_client.start()

    def process_message(self, **kwargs):
        data = kwargs.get("data")
        text = data.get("text")

        if text:
            match = re.match(self.pattern, text, re.I)
            if match is not None:
                channel_id = data.get("channel")
                thread_ts = data.get("ts")

                self.slack_client.chat_postMessage(
                    channel=channel_id,
                    thread_ts=thread_ts,
                    text=self.get_random_response(),
                )

    def get_file_contents(self) -> list:
        with open(self.remarks_location) as file:
            return file.readlines()

    def get_random_response(self) -> str:
        remarks_length = len(self.remarks)

        return self.remarks[random.randint(0, remarks_length - 1)]
Example #21
0
class Client:
    _web_client: WebClient
    _rtm_client: RTMClient

    def __init__(
        self,
        *,
        token: str,
    ) -> None:
        self._webclient = WebClient(
            token=token,
            run_async=True,
        )
        self._rtm_client = RTMClient(
            token=token,
            run_async=True,
        )

    async def post_message(
        self,
        channel: str,
        text: str,
    ) -> Dict[str, Any]:
        log.info('Sending slack message to channel %s: %s', channel, text)
        return await self._webclient.chat_postMessage(
            channel=channel,
            text=text,
            icon_emoji='robot_face',
        )

    def start_rtm_client(self) -> asyncio.Future:
        return self._rtm_client.start()

    def stop_rtm_client(self) -> None:
        return self._rtm_client.stop()

    def get_message_queue(self) -> asyncio.Queue:
        return _message_queue
professor_repr = ProfessorRepresentative(["language", "general"])


@RTMClient.run_on(event='message')
def say_hello(**payload):
    global counter
    data = payload['data']
    web_client = payload['web_client']
    rtm_client = payload['rtm_client']
    if 'text' in data and 'user' in data:
        channel_id = data['channel']
        thread_ts = data['ts']
        user = data['user']
        text = data['text']

        try:
            response = web_client.chat_postMessage(
                channel=channel_id,
                text=professor_repr.answer_short(text),
                #thread_ts=thread_ts
            )
        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']}")


rtm_client = RTMClient(token=os.environ["SLACK_API_TOKEN"])
rtm_client.start()
Example #23
0
class SlackBot(object):

    def __init__(self):
        logger.info("Starting up Slack Bot", format_opts=["header"])
        basedir = os.path.dirname(os.path.realpath(__file__))
        self.base_path = os.path.join(basedir, '..', '..')
        self.ready_event = threading.Event()
        self.stop_event = threading.Event()
        self.__bootstrap()

    def __bootstrap(self):
        logger.info("Loading configuration...")
        self.__verify_config()
        logger.debug("Initializing kv store session")
        self.db = DatabaseSession()
        logger.debug("Initializing context manager")
        self.contexts = ContextManager(self)
        logger.info("Loading Plugins...")
        self.plugins = PluginManager(self, os.path.join(self.base_path, 'plugins'))

    def __verify_config(self):
        self.name = config.get('bot_name')
        if not self.name:
            raise MissingBotName
        logger.debug(f"Using bot name: {self.name}")
        slack_token = config.get('slack_token')
        if not slack_token:
            raise MissingSlackToken
        logger.debug("Retieved slack token from configuration")
        self.admins = config.get('admins') or []
        logger.debug(f"Configured admins: {self.admins}")

    def _get_users(self):
        api_call = self.client.api_call("users.list")
        if api_call.get('ok'):
            return api_call.get('members')

    def _get_my_id(self):
        for user in self.users:
            if 'name' in user and user.get('name') == self.name:
                return user.get('id')

    def _wait(self, interval):
        self.stop_event.wait(interval)

    def _handle_plugin_response(self, channel, response):
        if isinstance(response, str):
            self.send_channel_message(channel, response)
        elif isinstance(response, list):
            for item in response:
                self.send_channel_message(channel, item)
        else:
            logger.info(f"Invalid response from plugin: {response}")

    def _is_not_message(self, output):
        if output.get('text'):
            if output['text'].strip() != '':
                return False
        return True

    def _is_self_message(self, output):
        return output.get('user') and output.get('user') == self.bot_id

    def is_mention(self, text):
        return self.at_bot in text

    def shutdown(self):
        logger.info("Received shutdown signal...")
        self.stop_event.set()
        self.rtm_client.stop()

    def restart(self):
        self.shutdown()
        time.sleep(5)
        self.__bootstrap()
        self.start()

    def ready(self):
        return self.ready_event.is_set()

    def running(self):
        return not self.stop_event.is_set()

    def start(self):
        try:
            self.rtm_client = RTMClient(token=config.get('slack_token'))
            self.rtm_client.start()
        except KeyboardInterrupt:
            self.shutdown()

    def get_help_page(self, command):
        return self.plugins.get_help_page(command)

    def handle_hello(self, payload):
        logger.debug("Received 'hello' from slack rtm api")
        logger.debug("Setting slack web/rtm clients")
        self.client = payload.get('web_client')
        logger.debug("Retrieving user list")
        self.users = self._get_users()
        logger.debug("Gathering information about myself")
        self.bot_id = self._get_my_id()
        self.display_name = self.get_user_profile(self.bot_id)['name']
        self.at_bot = "<@" + self.bot_id + ">"
        self.ready_event.set()
        logger.info("Initialization Complete!", format_opts=["green"])

    def handle_message(self, payload):
        logger.debug(f"Handling slack message payload: {payload}")
        output = payload.get('data')
        if self._is_not_message(output) or self._is_self_message(output):
            return
        channel = output.get('channel')
        if not channel:
            return
        try:
            logger.debug("Looking up source user of event")
            user = self.get_user_profile(output['user'])
            logger.debug(f"User: {user}")
        except Exception as err:
            log.error(f"Failed to fetch user for event: {output}", err, sys.exc_info())
            return
        text = output['text']
        try:
            logger.info(f"<{channel}/{user['profile']['display_name']}>: {text}")
            cmd, words = self.plugins.get_cmd(text)
            if cmd:
                res = self.plugins.serve_cmd(channel, user, cmd, words)
                if res:
                    self._handle_plugin_response(channel, res)
                return
            words = text.split()
            ctx = self.contexts.get_context(channel, user['id'])
            if ctx:
                if ctx.is_finished() or ctx.is_expired():
                    return
                with self.contexts.lock:
                    res = self.plugins.serve_context(channel, user, ctx, words)
                    if res:
                        self._handle_plugin_response(channel, res)
                    ctx.messages.append(words)
                    return
            trigger = self.plugins.get_trigger(text)
            if trigger:
                res = self.plugins.serve_trigger(channel, user, trigger, words)
                if res:
                    self._handle_plugin_response(channel, res)
                return
            if self.is_mention(text):
                text.replace(self.at_bot, "")
                res = self.plugins.serve_mention(channel, user, text.split())
                if res:
                    self._handle_plugin_response(channel, res)
                    return
            logger.debug("No plugins matched the event")
        except Exception as err:
            logger.error("Failed processing message", err, sys.exc_info())
            self._handle_plugin_response(channel, "An error has occurred and been logged accordingly")

    def sanitize_handle(self, handle):
        fixed = handle
        for char in ['@', '<', '>']:
            fixed = fixed.replace(char, '')
        return fixed

    def get_user_profile(self, user_id):
        found = None
        for user in self.users:
            try:
                if user['id'].upper() == user_id.upper():
                    found = user
                    break
            except KeyError:
                pass
        return found

    def send_channel_message(self, channel, message, attachments=[],
                             action=False):
        logger.debug(
            f"""Sending channel message:
            channel: {channel}
            message: {message}
            action: {action}
            attachments: {attachments}
            """
        )
        if not action:
            response = self.client.chat_postMessage(
                channel=channel,
                text=message,
                attachments=attachments,
                as_user=True
            )
        else:
            response = self.client.chat_meMessage(
                channel=channel,
                text=message
            )
        if not response.get('ok'):
            logger.info(str(response))

    def send_channel_file(self, channel, title, filetype, content):
        logger.debug(
            f"""Uploading file to channel
            channel: {channel}
            filename: {title}
            filetype: {filetype}
            """
        )
        api_call = self.client.files_upload(
            channels=channel,
            title=title,
            filetype=filetype,
            content=content
        )
        if api_call.get('ok'):
            return api_call.get('file')

    def register_loop(self, function, args=[], interval=10):
        logger.debug(
            f"""Registering plugin loop
            Function: {function}
            args: {args}
            interval: {interval}
            """
        )
        threading.Thread(
            target=self.run_plugin_loop,
            args=[function, interval, args],
            daemon=True
        ).start()

    def run_plugin_loop(self, function, interval, args=[]):
        self._wait(interval)
        while self.running():
            try:
                if len(args) > 0:
                    logger.debug(f"Firing {function} with args: {args}")
                    function(*args)
                else:
                    logger.debug(f"Firing {function}")
                    function()
                self._wait(interval)
            except Exception as err:
                logger.error("Exception while running plugin loop", err, sys.exc_info())
                self._wait(interval)
Example #24
0
        args = text.split(' ')
        if args[1] == 'match':
            match_command(payload, args[2:])


def match_command(payload: dict, args: list):
    data = payload['data']
    web_client = payload['web_client']
    if len(args) != 3:
        web_client.chat_postMessage(
            channel=data['channel'],
            text='Usage: <@UQWDVSDNH> [event ID] [match #] [team #]')
        return
    try:
        match_data = firebase.get_match_data(args[0], int(args[1]),
                                             int(args[2]))
    except:
        web_client.chat_postMessage(
            channel=data['channel'],
            text='Usage: <@UQWDVSDNH> [event ID] [match #] [team #]')
        return
    message = message_builder.match_data(match_data)
    web_client.chat_postMessage(channel=data['channel'],
                                text=message,
                                mrkdwn=True)


print('client set')
client.start()
Example #25
0
          CSM:
        "
        """
        try:
            web_client.chat_postMessage(
                channel=channel_id, text=text, thread_ts=thread_ts,
            )
        # You will get a SlackApiError if "ok" is False
        except SlackApiError as exc:
            assert exc.response["ok"] is False
            assert exc.response["error"]
            LOG.exception(f'Got an error: {exc.response["error"]}')


# Watch for messages from users with trigger
@RTMClient.run_on(event="message")
def get_info(**payload):
    """Process channel messages."""
    data = payload["data"]
    web_client = payload["web_client"]

    if "#whois" in data.get("text", []):
        try:
            process_whois(data=data, web_client=web_client)
        except Exception:
            LOG.exception(f"Exception in process_whois!!!")


RTM_CLIENT = RTMClient(token=os.getenv("SLACK_TOKEN"))
RTM_CLIENT.start()
Example #26
0
class TestFoxylibSlackFunction:
    @classmethod
    def setUpClass(cls):
        FoxylibLogger.attach_stderr2loggers(logging.DEBUG)

    async def mock_server(self):
        app = web.Application()
        app["websockets"] = []
        app.router.add_get("/", self.websocket_handler)
        app.on_shutdown.append(self.on_shutdown)
        runner = web.AppRunner(app)
        await runner.setup()
        self.site = web.TCPSite(runner, "localhost", 8765)
        await self.site.start()

    async def websocket_handler(self, request):
        ws = web.WebSocketResponse()
        await ws.prepare(request)

        request.app["websockets"].append(ws)
        try:
            async for msg in ws:
                await ws.send_json({
                    "type": "message",
                    "message_sent": msg.json()
                })
        finally:
            request.app["websockets"].remove(ws)
        return ws

    async def on_shutdown(self, app):
        for ws in set(app["websockets"]):
            await ws.close(code=WSCloseCode.GOING_AWAY,
                           message="Server shutdown")

    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        task = asyncio.ensure_future(self.mock_server(), loop=self.loop)
        self.loop.run_until_complete(asyncio.wait_for(task, 0.1))

        self.client = RTMClient(token=FoxylibSlack.xoxb_token(),
                                loop=self.loop,
                                auto_reconnect=False)

    def test_01(self):
        channel = FoxylibChannel.V.FOXYLIB

        @RTMClient.run_on(event="open")
        async def typing_message(**payload):
            rtm_client = payload["rtm_client"]
            await rtm_client.typing(channel=channel)

        @RTMClient.run_on(event="message")
        def check_message(**payload):
            message = {"id": 1, "type": "typing", "channel": channel}
            rtm_client = payload["rtm_client"]
            self.assertDictEqual(payload["data"]["message_sent"], message)
            rtm_client.stop()

        self.client.start()

    def test_04(self):
        @RTMClient.run_on(event="open")
        async def typing_message(**payload):
            web_client = payload["web_client"]
            channel = FoxylibChannel.V.FOXYLIB
            filepath = os.path.join(FILE_DIR, "test_01.txt")

            await FilesUploadMethod.invoke(web_client, channel, filepath)
            await rtm_client.typing(channel="C01234567")

        @RTMClient.run_on(event=FileSharedEvent.NAME)
        def on_file_shared(**payload):
            message = {"id": 1, "type": "typing", "channel": "C01234567"}
            rtm_client = payload["rtm_client"]
            self.assertDictEqual(payload["data"]["message_sent"], message)
            rtm_client.stop()

        FoxylibSlack.rtm_client().start()
        raise Exception()

    def test_05(self):
        def on_file_shared(**kwargs):
            j_event = kwargs.get("data")
            j_file_list = FileSharedEvent.j_event2j_file_list(j_event)
            self.assertEqual(len(j_file_list), 1)

            j_file = l_singleton2obj(j_file_list)
            filename = SlackFile.j_file2filename(j_file)
            self.assertEqual(filename, "test_01.txt")

            raise Exception()

        RTMClient.on(event="file_shared", callback=on_file_shared)

        rtm_client = FoxylibSlack.rtm_client()
        rtm_client.start()

        # p = Process(target=rtm_client.start)
        # p.start()

        web_client = FoxylibSlack.web_client()
        channel = FoxylibChannel.V.FOXYLIB
        filepath = os.path.join(FILE_DIR, "test_01.txt")
        response = FilesUploadMethod.invoke(web_client, channel, filepath)
Example #27
0
 def slack_setup():
     SlackNotification.update_feature()
     if SlackNotification.SLACK_NOTIFICATION_ENABLED:
         rtm_client = RTMClient(token=os.environ["SLACK_API_TOKEN"])
         rtm_client.start()
Example #28
0
class SlackRTMBackend(ErrBot):
    @staticmethod
    def _unpickle_identifier(identifier_str):
        return SlackRTMBackend.__build_identifier(identifier_str)

    @staticmethod
    def _pickle_identifier(identifier):
        return SlackRTMBackend._unpickle_identifier, (str(identifier), )

    def _register_identifiers_pickling(self):
        """
        Register identifiers pickling.

        As Slack needs live objects in its identifiers, we need to override their pickling behavior.
        But for the unpickling to work we need to use bot.build_identifier, hence the bot parameter here.
        But then we also need bot for the unpickling so we save it here at module level.
        """
        SlackRTMBackend.__build_identifier = self.build_identifier
        for cls in (SlackPerson, SlackRoomOccupant, SlackRoom):
            copyreg.pickle(
                cls,
                SlackRTMBackend._pickle_identifier,
                SlackRTMBackend._unpickle_identifier,
            )

    def __init__(self, config):
        super().__init__(config)
        identity = config.BOT_IDENTITY
        self.token = identity.get("token", None)
        self.proxies = identity.get("proxies", None)
        if not self.token:
            log.fatal(
                'You need to set your token (found under "Bot Integration" on Slack) in '
                "the BOT_IDENTITY setting in your configuration. Without this token I "
                "cannot connect to Slack.")
            sys.exit(1)
        self.sc = None  # Will be initialized in serve_once
        self.webclient = None
        self.bot_identifier = None
        compact = config.COMPACT_OUTPUT if hasattr(config,
                                                   "COMPACT_OUTPUT") else False
        self.md = slack_markdown_converter(compact)
        self._register_identifiers_pickling()

    def update_alternate_prefixes(self):
        """Converts BOT_ALT_PREFIXES to use the slack ID instead of name

        Slack only acknowledges direct callouts `@username` in chat if referred
        by using the ID of that user.
        """
        # convert BOT_ALT_PREFIXES to a list
        try:
            bot_prefixes = self.bot_config.BOT_ALT_PREFIXES.split(",")
        except AttributeError:
            bot_prefixes = list(self.bot_config.BOT_ALT_PREFIXES)

        converted_prefixes = []
        for prefix in bot_prefixes:
            try:
                converted_prefixes.append(
                    f"<@{self.username_to_userid(prefix)}>")
            except Exception as e:
                log.error(
                    'Failed to look up Slack userid for alternate prefix "%s": %s',
                    prefix,
                    e,
                )

        self.bot_alt_prefixes = tuple(
            x.lower() for x in self.bot_config.BOT_ALT_PREFIXES)
        log.debug("Converted bot_alt_prefixes: %s",
                  self.bot_config.BOT_ALT_PREFIXES)

    def _setup_slack_callbacks(self):
        @RTMClient.run_on(event="message")
        def serve_messages(**payload):
            self._message_event_handler(payload["web_client"], payload["data"])

        @RTMClient.run_on(event="member_joined_channel")
        def serve_joins(**payload):
            self._member_joined_channel_event_handler(payload["web_client"],
                                                      payload["data"])

        @RTMClient.run_on(event="hello")
        def serve_hellos(**payload):
            self._hello_event_handler(payload["web_client"], payload["data"])

        @RTMClient.run_on(event="presence_change")
        def serve_presences(**payload):
            self._presence_change_event_handler(payload["web_client"],
                                                payload["data"])

    def serve_forever(self):
        self.sc = RTMClient(token=self.token, proxy=self.proxies)

        @RTMClient.run_on(event="open")
        def get_bot_identity(**payload):
            self.bot_identifier = SlackPerson(payload["web_client"],
                                              payload["data"]["self"]["id"])
            # only hook up the message callback once we have our identity set.
            self._setup_slack_callbacks()

        # log.info('Verifying authentication token')
        # self.auth = self.api_call("auth.test", raise_errors=False)
        # if not self.auth['ok']:
        #     raise SlackAPIResponseError(error=f"Couldn't authenticate with Slack. Server said: {self.auth['error']}")
        # log.debug("Token accepted")

        log.info("Connecting to Slack real-time-messaging API")
        self.sc.start()
        # Inject bot identity to alternative prefixes
        self.update_alternate_prefixes()

        try:
            while True:
                sleep(1)
        except KeyboardInterrupt:
            log.info("Interrupt received, shutting down..")
            return True
        except Exception:
            log.exception("Error reading from RTM stream:")
        finally:
            log.debug("Triggering disconnect callback")
            self.disconnect_callback()

    def _hello_event_handler(self, webclient: WebClient, event):
        """Event handler for the 'hello' event"""
        self.webclient = webclient
        self.connect_callback()
        self.callback_presence(
            Presence(identifier=self.bot_identifier, status=ONLINE))

    def _presence_change_event_handler(self, webclient: WebClient, event):
        """Event handler for the 'presence_change' event"""

        idd = SlackPerson(webclient, event["user"])
        presence = event["presence"]
        # According to https://api.slack.com/docs/presence, presence can
        # only be one of 'active' and 'away'
        if presence == "active":
            status = ONLINE
        elif presence == "away":
            status = AWAY
        else:
            log.error(
                f"It appears the Slack API changed, I received an unknown presence type {presence}."
            )
            status = ONLINE
        self.callback_presence(Presence(identifier=idd, status=status))

    def _message_event_handler(self, webclient: WebClient, event):
        """Event handler for the 'message' event"""
        channel = event["channel"]
        if channel[0] not in "CGD":
            log.warning("Unknown message type! Unable to handle %s", channel)
            return

        subtype = event.get("subtype", None)

        if subtype in ("message_deleted", "channel_topic", "message_replied"):
            log.debug("Message of type %s, ignoring this event", subtype)
            return

        if subtype == "message_changed" and "attachments" in event["message"]:
            # If you paste a link into Slack, it does a call-out to grab details
            # from it so it can display this in the chatroom. These show up as
            # message_changed events with an 'attachments' key in the embedded
            # message. We should completely ignore these events otherwise we
            # could end up processing bot commands twice (user issues a command
            # containing a link, it gets processed, then Slack triggers the
            # message_changed event and we end up processing it again as a new
            # message. This is not what we want).
            log.debug(
                "Ignoring message_changed event with attachments, likely caused "
                "by Slack auto-expanding a link")
            return
        text = event["text"]

        text, mentioned = self.process_mentions(text)

        text = self.sanitize_uris(text)

        log.debug("Saw an event: %s", pprint.pformat(event))
        log.debug("Escaped IDs event text: %s", text)

        msg = Message(
            text,
            extras={
                "attachments": event.get("attachments"),
                "slack_event": event,
            },
        )

        if channel.startswith("D"):
            if subtype == "bot_message":
                msg.frm = SlackBot(
                    webclient,
                    bot_id=event.get("bot_id"),
                    bot_username=event.get("username", ""),
                )
            else:
                msg.frm = SlackPerson(webclient, event["user"],
                                      event["channel"])
            msg.to = SlackPerson(webclient, self.bot_identifier.userid,
                                 event["channel"])
            channel_link_name = event["channel"]
        else:
            if subtype == "bot_message":
                msg.frm = SlackRoomBot(
                    webclient,
                    bot_id=event.get("bot_id"),
                    bot_username=event.get("username", ""),
                    channelid=event["channel"],
                    bot=self,
                )
            else:
                msg.frm = SlackRoomOccupant(webclient,
                                            event["user"],
                                            event["channel"],
                                            bot=self)
            msg.to = SlackRoom(webclient=webclient,
                               channelid=event["channel"],
                               bot=self)
            channel_link_name = msg.to.name

        # TODO: port to slackclient2
        # msg.extras['url'] = f'https://{self.sc.server.domain}.slack.com/archives/' \
        #                     f'{channel_link_name}/p{self._ts_for_message(msg).replace(".", "")}'

        self.callback_message(msg)

        if mentioned:
            self.callback_mention(msg, mentioned)

    def _member_joined_channel_event_handler(self, webclient: WebClient,
                                             event):
        """Event handler for the 'member_joined_channel' event"""
        user = SlackPerson(webclient, event["user"])
        if user == self.bot_identifier:
            user = self.bot_identifier
        self.callback_room_joined(
            SlackRoom(webclient=webclient,
                      channelid=event["channel"],
                      bot=self), user)

    def userid_to_username(self, id_: str):
        """Convert a Slack user ID to their user name"""
        user = self.webclient.users_info(user=id_)["user"]
        if user is None:
            raise UserDoesNotExistError(f"Cannot find user with ID {id_}.")
        return user["name"]

    def username_to_userid(self, name: str):
        """Convert a Slack user name to their user ID"""
        name = name.lstrip("@")
        user = [
            user for user in self.webclient.users_list()["members"]
            if user["name"] == name
        ]
        if user == []:
            raise UserDoesNotExistError(f"Cannot find user {name}.")
        if len(user) > 1:
            log.error(
                "Failed to uniquely identify '{}'.  Errbot found the following users: {}"
                .format(
                    name, " ".join(
                        ["{}={}".format(u["name"], u["id"]) for u in user])))
            raise UserNotUniqueError(f"Failed to uniquely identify {name}.")
        return user[0]["id"]

    def channelid_to_channelname(self, id_: str):
        """Convert a Slack channel ID to its channel name"""
        channel = self.webclient.conversations_info(channel=id_)["channel"]
        if channel is None:
            raise RoomDoesNotExistError(f"No channel with ID {id_} exists.")
        return channel["name"]

    def channelname_to_channelid(self, name: str):
        """Convert a Slack channel name to its channel ID"""
        name = name.lstrip("#")
        channel = [
            channel for channel in self.webclient.channels_list()
            if channel.name == name
        ]
        if not channel:
            raise RoomDoesNotExistError(f"No channel named {name} exists")
        return channel[0].id

    def channels(self, exclude_archived=True, joined_only=False):
        """
        Get all channels and groups and return information about them.

        :param exclude_archived:
            Exclude archived channels/groups
        :param joined_only:
            Filter out channels the bot hasn't joined
        :returns:
            A list of channel (https://api.slack.com/types/channel)
            and group (https://api.slack.com/types/group) types.

        See also:
          * https://api.slack.com/methods/channels.list
          * https://api.slack.com/methods/groups.list
        """
        response = self.webclient.channels_list(
            exclude_archived=exclude_archived)
        channels = [
            channel for channel in response["channels"]
            if channel["is_member"] or not joined_only
        ]

        response = self.webclient.groups_list(
            exclude_archived=exclude_archived)
        # No need to filter for 'is_member' in this next call (it doesn't
        # (even exist) because leaving a group means you have to get invited
        # back again by somebody else.
        groups = [group for group in response["groups"]]

        return channels + groups

    @lru_cache(1024)
    def get_im_channel(self, id_):
        """Open a direct message channel to a user"""
        try:
            response = self.webclient.im_open(user=id_)
            return response["channel"]["id"]
        except SlackAPIResponseError as e:
            if e.error == "cannot_dm_bot":
                log.info("Tried to DM a bot.")
                return None
            else:
                raise e

    def _prepare_message(self, msg):  # or card
        """
        Translates the common part of messaging for Slack.
        :param msg: the message you want to extract the Slack concept from.
        :return: a tuple to user human readable, the channel id
        """
        if msg.is_group:
            to_channel_id = msg.to.id
            to_humanreadable = (msg.to.name if msg.to.name else
                                self.channelid_to_channelname(to_channel_id))
        else:
            to_humanreadable = msg.to.username
            to_channel_id = msg.to.channelid
            if to_channel_id.startswith("C"):
                log.debug(
                    "This is a divert to private message, sending it directly to the user."
                )
                to_channel_id = self.get_im_channel(
                    self.username_to_userid(msg.to.username))
        return to_humanreadable, to_channel_id

    def send_message(self, msg):
        super().send_message(msg)

        if msg.parent is not None:
            # we are asked to reply to a specify thread.
            try:
                msg.extras["thread_ts"] = self._ts_for_message(msg.parent)
            except KeyError:
                # Gives to the user a more interesting explanation if we cannot find a ts from the parent.
                log.exception(
                    "The provided parent message is not a Slack message "
                    "or does not contain a Slack timestamp.")

        to_humanreadable = "<unknown>"
        try:
            if msg.is_group:
                to_channel_id = msg.to.id
                to_humanreadable = (
                    msg.to.name if msg.to.name else
                    self.channelid_to_channelname(to_channel_id))
            else:
                to_humanreadable = msg.to.username
                if isinstance(
                        msg.to, RoomOccupant
                ):  # private to a room occupant -> this is a divert to private !
                    log.debug(
                        "This is a divert to private message, sending it directly to the user."
                    )
                    to_channel_id = self.get_im_channel(
                        self.username_to_userid(msg.to.username))
                else:
                    to_channel_id = msg.to.channelid

            msgtype = "direct" if msg.is_direct else "channel"
            log.debug(
                "Sending %s message to %s (%s).",
                msgtype,
                to_humanreadable,
                to_channel_id,
            )
            body = self.md.convert(msg.body)
            log.debug("Message size: %d.", len(body))

            limit = min(self.bot_config.MESSAGE_SIZE_LIMIT,
                        SLACK_MESSAGE_LIMIT)
            parts = self.prepare_message_body(body, limit)

            timestamps = []
            for part in parts:
                data = {
                    "channel": to_channel_id,
                    "text": part,
                    "unfurl_media": "true",
                    "link_names": "1",
                    "as_user": "******",
                }

                # Keep the thread_ts to answer to the same thread.
                if "thread_ts" in msg.extras:
                    data["thread_ts"] = msg.extras["thread_ts"]

                result = self.webclient.chat_postMessage(**data)
                timestamps.append(result["ts"])

            msg.extras["ts"] = timestamps
        except Exception:
            log.exception(
                f"An exception occurred while trying to send the following message "
                f"to {to_humanreadable}: {msg.body}.")

    def _slack_upload(self, stream: Stream) -> None:
        """
        Performs an upload defined in a stream
        :param stream: Stream object
        :return: None
        """
        try:
            stream.accept()
            resp = self.webclient.files_upload(
                channels=stream.identifier.channelid,
                filename=stream.name,
                file=stream)
            if "ok" in resp and resp["ok"]:
                stream.success()
            else:
                stream.error()
        except Exception:
            log.exception(
                f"Upload of {stream.name} to {stream.identifier.channelname} failed."
            )

    def send_stream_request(
        self,
        user: Identifier,
        fsource: BinaryIO,
        name: str = None,
        size: int = None,
        stream_type: str = None,
    ) -> Stream:
        """
        Starts a file transfer. For Slack, the size and stream_type are unsupported

        :param user: is the identifier of the person you want to send it to.
        :param fsource: is a file object you want to send.
        :param name: is an optional filename for it.
        :param size: not supported in Slack backend
        :param stream_type: not supported in Slack backend

        :return Stream: object on which you can monitor the progress of it.
        """
        stream = Stream(user, fsource, name, size, stream_type)
        log.debug(
            "Requesting upload of %s to %s (size hint: %d, stream type: %s).",
            name,
            user.channelname,
            size,
            stream_type,
        )
        self.thread_pool.apply_async(self._slack_upload, (stream, ))
        return stream

    def send_card(self, card: Card):
        if isinstance(card.to, RoomOccupant):
            card.to = card.to.room
        to_humanreadable, to_channel_id = self._prepare_message(card)
        attachment = {}
        if card.summary:
            attachment["pretext"] = card.summary
        if card.title:
            attachment["title"] = card.title
        if card.link:
            attachment["title_link"] = card.link
        if card.image:
            attachment["image_url"] = card.image
        if card.thumbnail:
            attachment["thumb_url"] = card.thumbnail

        if card.color:
            attachment["color"] = (COLORS[card.color]
                                   if card.color in COLORS else card.color)

        if card.fields:
            attachment["fields"] = [{
                "title": key,
                "value": value,
                "short": True
            } for key, value in card.fields]

        limit = min(self.bot_config.MESSAGE_SIZE_LIMIT, SLACK_MESSAGE_LIMIT)
        parts = self.prepare_message_body(card.body, limit)
        part_count = len(parts)
        footer = attachment.get("footer", "")
        for i in range(part_count):
            if part_count > 1:
                attachment["footer"] = f"{footer} [{i + 1}/{part_count}]"
            attachment["text"] = parts[i]
            data = {
                "channel": to_channel_id,
                "attachments": json.dumps([attachment]),
                "link_names": "1",
                "as_user": "******",
            }
            try:
                log.debug("Sending data:\n%s", data)
                self.webclient.chat_postMessage(**data)
            except Exception:
                log.exception(
                    f"An exception occurred while trying to send a card to {to_humanreadable}.[{card}]"
                )

    def __hash__(self):
        return 0  # this is a singleton anyway

    def change_presence(self, status: str = ONLINE, message: str = "") -> None:
        self.webclient.users_setPresence(
            presence="auto" if status == ONLINE else "away")

    @staticmethod
    def prepare_message_body(body, size_limit):
        """
        Returns the parts of a message chunked and ready for sending.

        This is a staticmethod for easier testing.

        Args:
            body (str)
            size_limit (int): chunk the body into sizes capped at this maximum

        Returns:
            [str]

        """
        fixed_format = body.startswith("```")  # hack to fix the formatting
        parts = list(split_string_after(body, size_limit))

        if len(parts) == 1:
            # If we've got an open fixed block, close it out
            if parts[0].count("```") % 2 != 0:
                parts[0] += "\n```\n"
        else:
            for i, part in enumerate(parts):
                starts_with_code = part.startswith("```")

                # If we're continuing a fixed block from the last part
                if fixed_format and not starts_with_code:
                    parts[i] = "```\n" + part

                # If we've got an open fixed block, close it out
                if part.count("```") % 2 != 0:
                    parts[i] += "\n```\n"

        return parts

    @staticmethod
    def extract_identifiers_from_string(text):
        """
        Parse a string for Slack user/channel IDs.

        Supports strings with the following formats::

            <#C12345>
            <@U12345>
            <@U12345|user>
            @user
            #channel/user
            #channel

        Returns the tuple (username, userid, channelname, channelid).
        Some elements may come back as None.
        """
        exception_message = (
            "Unparseable slack identifier, should be of the format `<#C12345>`, `<@U12345>`, "
            "`<@U12345|user>`, `@user`, `#channel/user` or `#channel`. (Got `%s`)"
        )
        text = text.strip()

        if text == "":
            raise ValueError(exception_message % "")

        channelname = None
        username = None
        channelid = None
        userid = None

        if text[0] == "<" and text[-1] == ">":
            exception_message = (
                "Unparseable slack ID, should start with U, B, C, G, D or W (got `%s`)"
            )
            text = text[2:-1]
            if text == "":
                raise ValueError(exception_message % "")
            if text[0] in ("U", "B", "W"):
                if "|" in text:
                    userid, username = text.split("|")
                else:
                    userid = text
            elif text[0] in ("C", "G", "D"):
                channelid = text
            else:
                raise ValueError(exception_message % text)
        elif text[0] == "@":
            username = text[1:]
        elif text[0] == "#":
            plainrep = text[1:]
            if "/" in text:
                channelname, username = plainrep.split("/", 1)
            else:
                channelname = plainrep
        else:
            raise ValueError(exception_message % text)

        return username, userid, channelname, channelid

    def build_identifier(self, txtrep):
        """
        Build a :class:`SlackIdentifier` from the given string txtrep.

        Supports strings with the formats accepted by
        :func:`~extract_identifiers_from_string`.
        """
        log.debug("building an identifier from %s.", txtrep)
        username, userid, channelname, channelid = self.extract_identifiers_from_string(
            txtrep)

        if userid is None and username is not None:
            userid = self.username_to_userid(username)
        if channelid is None and channelname is not None:
            channelid = self.channelname_to_channelid(channelname)
        if userid is not None and channelid is not None:
            return SlackRoomOccupant(self.webclient,
                                     userid,
                                     channelid,
                                     bot=self)
        if userid is not None:
            return SlackPerson(self.webclient, userid,
                               self.get_im_channel(userid))
        if channelid is not None:
            return SlackRoom(webclient=self.webclient,
                             channelid=channelid,
                             bot=self)

        raise Exception(
            "You found a bug. I expected at least one of userid, channelid, username or channelname "
            "to be resolved but none of them were. This shouldn't happen so, please file a bug."
        )

    def is_from_self(self, msg: Message) -> bool:
        return self.bot_identifier.userid == msg.frm.userid

    def build_reply(self, msg, text=None, private=False, threaded=False):
        response = self.build_message(text)

        if "thread_ts" in msg.extras["slack_event"]:
            # If we reply to a threaded message, keep it in the thread.
            response.extras["thread_ts"] = msg.extras["slack_event"][
                "thread_ts"]
        elif threaded:
            # otherwise check if we should start a new thread
            response.parent = msg

        response.frm = self.bot_identifier
        if private:
            response.to = msg.frm
        else:
            response.to = msg.frm.room if isinstance(msg.frm,
                                                     RoomOccupant) else msg.frm
        return response

    def add_reaction(self, msg: Message, reaction: str) -> None:
        """
        Add the specified reaction to the Message if you haven't already.
        :param msg: A Message.
        :param reaction: A str giving an emoji, without colons before and after.
        :raises: ValueError if the emoji doesn't exist.
        """
        return self._react("reactions.add", msg, reaction)

    def remove_reaction(self, msg: Message, reaction: str) -> None:
        """
        Remove the specified reaction from the Message if it is currently there.
        :param msg: A Message.
        :param reaction: A str giving an emoji, without colons before and after.
        :raises: ValueError if the emoji doesn't exist.
        """
        return self._react("reactions.remove", msg, reaction)

    def _react(self, method: str, msg: Message, reaction: str) -> None:
        try:
            # this logic is from send_message
            if msg.is_group:
                to_channel_id = msg.to.id
            else:
                to_channel_id = msg.to.channelid

            ts = self._ts_for_message(msg)

            self.api_call(
                method,
                data={
                    "channel": to_channel_id,
                    "timestamp": ts,
                    "name": reaction
                },
            )
        except SlackAPIResponseError as e:
            if e.error == "invalid_name":
                raise ValueError(e.error, "No such emoji", reaction)
            elif e.error in ("no_reaction", "already_reacted"):
                # This is common if a message was edited after you reacted to it, and you reacted to it again.
                # Chances are you don't care about this. If you do, call api_call() directly.
                pass
            else:
                raise SlackAPIResponseError(error=e.error)

    def _ts_for_message(self, msg):
        try:
            return msg.extras["slack_event"]["message"]["ts"]
        except KeyError:
            return msg.extras["slack_event"]["ts"]

    def shutdown(self):
        super().shutdown()

    @property
    def mode(self):
        return "slack"

    def query_room(self, room):
        """ Room can either be a name or a channelid """
        if room.startswith("C") or room.startswith("G"):
            return SlackRoom(webclient=self.webclient,
                             channelid=room,
                             bot=self)

        m = SLACK_CLIENT_CHANNEL_HYPERLINK.match(room)
        if m is not None:
            return SlackRoom(webclient=self.webclient,
                             channelid=m.groupdict()["id"],
                             bot=self)

        return SlackRoom(webclient=self.webclient, name=room, bot=self)

    def rooms(self):
        """
        Return a list of rooms the bot is currently in.

        :returns:
            A list of :class:`~SlackRoom` instances.
        """
        channels = self.channels(joined_only=True, exclude_archived=True)
        return [
            SlackRoom(webclient=self.webclient,
                      channelid=channel["id"],
                      bot=self) for channel in channels
        ]

    def prefix_groupchat_reply(self, message, identifier):
        super().prefix_groupchat_reply(message, identifier)
        message.body = f"@{identifier.nick}: {message.body}"

    @staticmethod
    def sanitize_uris(text):
        """
        Sanitizes URI's present within a slack message. e.g.
        <mailto:[email protected]|[email protected]>,
        <http://example.org|example.org>
        <http://example.org>

        :returns:
            string
        """
        text = re.sub(r"<([^|>]+)\|([^|>]+)>", r"\2", text)
        text = re.sub(r"<(http([^>]+))>", r"\1", text)

        return text

    def process_mentions(self, text):
        """
        Process mentions in a given string
        :returns:
            A formatted string of the original message
            and a list of :class:`~SlackPerson` instances.
        """
        mentioned = []

        m = re.findall("<@[^>]*>*", text)

        for word in m:
            try:
                identifier = self.build_identifier(word)
            except Exception as e:
                log.debug(
                    "Tried to build an identifier from '%s' but got exception: %s",
                    word,
                    e,
                )
                continue

            # We only track mentions of persons.
            if isinstance(identifier, SlackPerson):
                log.debug("Someone mentioned")
                mentioned.append(identifier)
                text = text.replace(word, str(identifier))

        return text, mentioned
Example #29
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)
Example #30
0
def init():
    client = RTMClient(token=os.environ.get('SLACK_API_TOKEN'))
    client.start()