def authorize(): if "error" in request.args: return redirect(f"https://slack.com/app_redirect?app={slack_app_id}", code=302) auth_code = request.args['code'] client = WebClient(token="") response = client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=auth_code) slack_info = SlackInfo( access_token=response["authed_user"]["access_token"], bot_access_token=response["access_token"], team_name=response["team"]["name"], team_id=response["team"]["id"], user_id=response["authed_user"]["id"]) db.session.add(slack_info) db.session.commit() return redirect( f"https://slack.com/app_redirect?team={response['team']['id']}&app={slack_app_id}", code=302)
def send_message(): slack_info = SlackInfo.query.filter_by( team_name=request.get_json()["team"]).first() if slack_info is None: return jsonify({"type": "not_found", "text": "Team not found"}), 400 slack = WebClient(token=slack_info.bot_access_token) try: slack.chat_postMessage(channel=request.get_json()['channel'], text=request.get_json()['text']) except SlackApiError as error: if error.response.data["error"] == "channel_not_found": return jsonify({ "type": "not_found", "text": "Channel not found" }), 400 elif error.response.data["error"] == "not_in_channel": return jsonify({ "type": "not_in_channel", "text": "The bot is not a member of the channel" }), 400 return make_response("", 200)
def handler(event, context): body = event['body'] params = parse_qs(body) channel_id = params['channel_id'][0] user_id = params['user_id'][0] if 'text' not in params: return cmd_help() res = token_table.get_item(Key={'user_id': user_id}) if 'Item' not in res: return please_install() encrypt_token = res['Item']['token'] blob_token = base64.b64decode(encrypt_token) kms_res = kms_client.decrypt(CiphertextBlob=blob_token) decrypted_token = kms_res['Plaintext'].decode('utf-8') input_text = params['text'][0].split() TargetLanguageCode = input_text[0] main_text = input_text[1] response = translate.translate_text(Text=main_text, SourceLanguageCode='auto', TargetLanguageCode=TargetLanguageCode) client = WebClient(token=decrypted_token) client.chat_postMessage(channel=channel_id, text=response['TranslatedText']) return { 'statusCode': HTTPStatus.OK, 'body': json.dumps({'text': 'Input: ' + input_text[1]}), 'headers': { 'Content-Type': 'application/json' } }
def get_redirect_url(self, *args, **kwargs): if not SLACK_ID: return reverse("project:edit", kwargs={'project': self.kwargs['project']}) slack = WebClient("") code = self.request.GET['code'] resp = slack.oauth_access( code=code, client_id=SLACK_ID, client_secret=SLACK_SECRET, redirect_uri="https://" + HOST + reverse("integration:slack:auth", kwargs={'project': self.kwargs['project']}), ) if resp['ok']: si = SlackIntegration() si.api_token = resp['access_token'] si.project = Project.objects.get(name_short=self.kwargs['project']) si.save() return reverse("integration:slack:update", kwargs={ 'project': self.kwargs['project'], 'pk': si.pk }) return reverse("project:edit", kwargs={'project': self.kwargs['project']})
def __init__( self, app, # SlackApp response_url: Optional[str] = None, channel: Optional[str] = None, thread_ts: Optional[str] = None): """ Creates an instance of a Messenger based on the provided SlackAPp. Parameters ---------- app: SlackApp The app context response_url: Optional[str] If provided, this becomes the default response URL in use with the send() method. channel: Optional[str] If provided, this becomes the default channel value in use with the send_channel() method. thread_ts: Optional[str] If provided, this becomes the default thread timestamp to use, and messages will be threaded. """ super(Messenger, self).__init__() self.app = app self.response_url = response_url self.channel = channel if thread_ts: self['thread_ts'] = thread_ts self.client = WebClient(self.app.config.token)
def __init__(self): _settings, _ = import_settings() slack_api_token = _settings.get('SLACK_API_TOKEN', None) http_proxy = _settings.get('HTTP_PROXY', None) self.rtm_client = RTMClient(token=slack_api_token, proxy=http_proxy) self.web_client = WebClient(token=slack_api_token, proxy=http_proxy) self._bot_info = {} self._users = {} self._channels = {}
async def _retrieve_websocket_info(self): """Retrieves the WebSocket info from Slack. Returns: A tuple of websocket information. e.g. ( "wss://...", { "self": {"id": "U01234ABC","name": "robotoverlord"}, "team": { "domain": "exampledomain", "id": "T123450FP", "name": "ExampleName" } } ) Raises: SlackApiError: Unable to retrieve RTM URL from Slack. """ if self._web_client is None: self._web_client = WebClient( token=self.token, base_url=self.base_url, timeout=self.timeout, ssl=self.ssl, proxy=self.proxy, run_async=True, loop=self._event_loop, session=self._session, headers=self.headers, ) self._logger.debug("Retrieving websocket info.") use_rtm_start = self.connect_method in ["rtm.start", "rtm_start"] if self.run_async: if use_rtm_start: resp = await self._web_client.rtm_start() else: resp = await self._web_client.rtm_connect() else: if use_rtm_start: resp = self._web_client.rtm_start() else: resp = self._web_client.rtm_connect() url = resp.get("url") if url is None: msg = "Unable to retrieve RTM URL from Slack." raise client_err.SlackApiError(message=msg, response=resp) return url, resp.data
def main(): logging.debug("Authorizing Pixie client.") px_client = pxapi.Client(token=pixie_api_key) cluster_conn = px_client.connect_to_cluster(pixie_cluster_id) logging.debug("Pixie client connected to %s cluster.", cluster_conn.name()) logging.debug("Authorizing Slack client.") slack_client = WebClient(slack_bot_token) # Slack channel for Slackbot to post in. # Slack App must be a member of this channel. SLACK_CHANNEL = "#pixie-alerts" # Schedule sending a Slack channel message every 5 minutes. schedule.every(5).minutes.do(lambda: send_slack_message(slack_client, SLACK_CHANNEL, cluster_conn)) logging.info("Message scheduled for %s Slack channel.", SLACK_CHANNEL) while True: schedule.run_pending() time.sleep(5)
class SlackClient: def __init__(self): self.client = WebClient(token=SLACK_BOT_TOKEN) def _raw_send_slack_message(self, channel: str, message: str) -> Union[Future, SlackResponse]: return self.client.chat_postMessage( channel=channel, text=message, ) def send_slack_message(self, channel: str, message: str) -> Union[Future, SlackResponse]: while True: try: return self._raw_send_slack_message(channel, message) except SlackApiError as e: if e.response["error"] == "ratelimited": delay = int(e.response.headers['Retry-After']) print(f"Rate limited. Retrying in {delay} seconds") time.sleep(delay) return self._raw_send_slack_message(channel, message) else: # other errors raise e
def GetSlackWebClient(): global SlackWebClient if SlackWebClient is None: slack_token = os.environ['SLACK_BOT_TOKEN'] SlackWebClient = WebClient(token=slack_token) return SlackWebClient
def __init__(self, app, rqst_type: str, rqst_data: Dict, user_id: str): """ An instance that represents any one of the many inbound requests from api.slack.com Parameters ---------- app: SlackApp rqst_type: str The request type, as originated from the request message rqst_data: Dict The data portion of the request message. For some message types this is the request form payload, and for other message types there is a single 'payload' within the form that contains the actual request data. user_id: str The Slack User-ID value originating the request. This value is stored in different places depending on the message type. """ self.app = app self.rqst_data = rqst_data self.rqst_type = rqst_type self.user_id = user_id # default places to look for values in payload self.response_url = self.rqst_data.get('response_url') self.trigger_id = self.rqst_data.get('trigger_id') self.channel = self.rqst_data.get('channel') self.surface = self.rqst_data.get('container') self.client = WebClient(token=self.app.config.token)
def verify_slack_request() -> None: """ This verification function is designed to be placed in a flask before_request handler """ request_data = request.get_data().decode() current_app.logger.debug(request.headers) current_app.logger.debug(request_data) timestamp = request.headers.get("X-Slack-Request-Timestamp", "") slack_sig = request.headers.get("X-Slack-Signature", "") request_verified = False verify_message = "" if any([timestamp == "", slack_sig == ""]): verify_message += "Slack request missing timestamp or signature headers" else: request_verified = WebClient.validate_slack_signature( signing_secret=current_app.config["SLACK_SIGNING_SECRET"], timestamp=timestamp, signature=slack_sig, data=request_data, ) if request_verified is False: if current_app.config["DEBUG"] is True: current_app.logger.info( f"Request verification: {request_verified}, msg: {verify_message}" ) else: verify_message = "Request signature verification failed" abort(403, verify_message)
def execute(self, dag, task, execution_date, run_time, url): client = WebClient(token=self._slack_token) slack_msg = f""" :red_circle: {' '.join(self._mentions)} Task Failed. *Task*: {task} *Dag*: {dag} *Execution Time*: {execution_date} *Running For*: {run_time} secs *Log Url*: {url} """ response = client.chat_postMessage(link_names=1, channel=self._channel, text=slack_msg) print(response)
def sendMessage(msg, test): SLACK_BOT_TOKEN = os.environ['TAT_alerts_slack_bot_token'] slack_client = WebClient(SLACK_BOT_TOKEN) logging.debug("authorized slack client") # make the POST request through the python slack client channelname = "#tat-alerts" if test: channelname = channelname + '-test' # check if the request was a success try: slack_client.chat_postMessage( channel=channelname, text=msg ) except SlackApiError as e: logging.error('Request to Slack API Failed: {}.'.format(e.response.status_code)) logging.error(e.response)
def __init__( self, *, token: str, run_async: Optional[bool] = False, auto_reconnect: Optional[bool] = True, ssl: Optional[SSLContext] = None, proxy: Optional[str] = None, timeout: Optional[int] = 30, base_url: Optional[str] = WebClient.BASE_URL, connect_method: Optional[str] = None, ping_interval: Optional[int] = 30, loop: Optional[asyncio.AbstractEventLoop] = None, headers: Optional[dict] = {}, ): self.token = token.strip() self.run_async = run_async self.auto_reconnect = auto_reconnect self.ssl = ssl self.proxy = proxy self.timeout = timeout self.base_url = base_url self.connect_method = connect_method self.ping_interval = ping_interval self.headers = headers self._event_loop = loop or asyncio.get_event_loop() self._web_client = None self._websocket = None self._session = None self._logger = logging.getLogger(__name__) self._last_message_id = 0 self._connection_attempts = 0 self._stopped = False self._web_client = WebClient( token=self.token, base_url=self.base_url, timeout=self.timeout, ssl=self.ssl, proxy=self.proxy, run_async=self.run_async, loop=self._event_loop, session=self._session, headers=self.headers, )
def on_echo_command(): slack_info = SlackInfo.query.filter_by( team_id=request.values["team_id"]).first() slack = WebClient(slack_info.bot_access_token) pprint(request.values.to_dict()) try: slack.chat_postMessage( channel=request.values['channel_id'], text=f"{request.values['user_name']} said: {request.values['text']}" ) except SlackApiError: post(url=request.values["response_url"], json={ "response_type": "ephemeral", "text": "The bot *is not* a member of the channel" }) return make_response('', 200)
def get_channel_info(channel1, verbose=False): try: botToken = os.environ.get('BotUserOAuthAccessToken') slack_client = SlackClient(botToken) info = slack_client.conversations_info(channel=channel1) if info['ok']: if info['channel']['is_private']: if verbose: return 'this is a *private channel*' else: return '' else: return '\nWARNING: this is a *public channel*' else: return '\nWARNING: I can not check if this channel is private' except Exception as e: if verbose: return '\nWARNING. I can not check if this channel is private! ' + str( e) else: return '\nWARNING. I can not check if this channel is private! '
def main(): CONFIG = get_config_from_file() airtable_volunteers = get_volunteers_from_airtable(CONFIG['AIRTABLE_BASE'], CONFIG['AIRTABLE_KEY']) slack_client = WebClient(token=CONFIG['SLACK_BOT_TOKEN']) email2slackid = get_slack_email2id_dict(slack_client) do_channels(slack_client, airtable_volunteers, email2slackid)
def _execute_callback(self, callback, data): """Execute the callback in another thread. Wait for and return the results.""" web_client = WebClient( token=self.token, base_url=self.base_url, ssl=self.ssl, proxy=self.proxy ) with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: # Execute the callback on a separate thread, future = executor.submit( callback, rtm_client=self, web_client=web_client, data=data ) while future.running(): pass future.result()
def command(): SLACK_BOT_TOKEN = os.environ['SLACK_BOT_TOKEN'] SLACK_SIGNATURE = os.environ['SLACK_SIGNATURE'] verifier = SignatureVerifier(SLACK_SIGNATURE) commander = Slash(verifier) if not commander.verify(request): return make_response("invalid request", 403) slack_client = WebClient(SLACK_BOT_TOKEN) info = request.form try: commander.processCommand(slack_client, info) return make_response("", 200) except SlackApiError as e: return make_response("", e.response.status_code)
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}") 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.at_bot = f'<@{self.bot_id}>' logger.info("Created new SlackClient Instance")
# An example of one of your Flask app's routes @app.route("/") def event_hook(request): json_dict = json.loads(request.body.decode("utf-8")) if json_dict["token"] != VERIFICATION_TOKEN: return {"status": 403} if "type" in json_dict: if json_dict["type"] == "url_verification": response_dict = {"challenge": json_dict["challenge"]} return response_dict return {"status": 500} # Start the Flask server if __name__ == "__main__": """ You need python 3.6+ to run. Run this command "python server.py" You'll also need to use ngrok with the port 5000 and copy/paste the URL here: https://api.slack.com/apps/A01C8SP1E30/event-subscriptions """ SLACK_BOT_TOKEN = os.environ['SLACK_BOT_TOKEN'] SLACK_SIGNATURE = os.environ['SLACK_SIGNATURE'] VERIFICATION_TOKEN = os.environ['VERIFICATION_TOKEN'] slack_client = WebClient(SLACK_BOT_TOKEN) verifier = SignatureVerifier(SLACK_SIGNATURE) app.run()
class Messenger(UserDict): """ The Messenger class is used to create an object that can respond back to the User with the context of a received Request message. This use is suitable in contexts such as code running in a background thread. """ def __init__( self, app, # SlackApp response_url: Optional[str] = None, channel: Optional[str] = None, thread_ts: Optional[str] = None): """ Creates an instance of a Messenger based on the provided SlackAPp. Parameters ---------- app: SlackApp The app context response_url: Optional[str] If provided, this becomes the default response URL in use with the send() method. channel: Optional[str] If provided, this becomes the default channel value in use with the send_channel() method. thread_ts: Optional[str] If provided, this becomes the default thread timestamp to use, and messages will be threaded. """ super(Messenger, self).__init__() self.app = app self.response_url = response_url self.channel = channel if thread_ts: self['thread_ts'] = thread_ts self.client = WebClient(self.app.config.token) # noinspection PyProtectedMember def send_response(self, response_url: Optional[str] = None, **kwargs: Optional[Any]): """ This method is used to send a message via the response_url rathern than using the api.slack.com endpoints. Parameters ---------- response_url: str The message will be POST to this URL; originates from a message received from api.slack.com Other Parameters ---------------- Any other kwargs are passed as content into the message. Raises ------ SlackApiError upon error sending; HTTP status code other than 200. Returns ------- True if the message was sent without error (HTTP code 200). Notes ----- Ideally this method should be a part of the `slackclient` BaseClient class to avoid using the internals of the client instance. TODO: open issue with that repo. """ req_args = dict( # contents of messenger[UserDict] **self, # any other API fields **kwargs) if self.client._event_loop is None: self.client._event_loop = _get_event_loop() api_url = response_url or self.response_url future = asyncio.ensure_future(self.client._request( http_verb='POST', api_url=api_url, req_args=dict(json=req_args)), loop=_get_event_loop()) res = self.client._event_loop.run_until_complete(future) status = res['status_code'] if status != 200: raise SlackApiError( message='Failed to send response_url: {}: status={}'.format( api_url, status), response=res) return True def send(self, channel=None, **kwargs): """ Send a message to the User. Parameters ---------- channel: str Direct the message to channel, rather than original channel value from instance initialization. Other Parameters ---------------- user: str send a private message (via postEphemeral) to user """ if 'user' in kwargs: api_call = self.client.chat_postEphemeral else: api_call = self.client.chat_postMessage return api_call( channel=channel or self.channel, # contents of messenger[UserDict] **self, # any other API fields provided by Caller **kwargs)
import os import json from collections import namedtuple from dataclasses import dataclass from flask.blueprints import Blueprint from flask import request from slack.web.client import WebClient from typing import Optional from tasks import async_task, async_func bp = Blueprint('interactive', __name__) OviBot = WebClient(os.getenv('BOT_TOKEN')) @dataclass class PlanConfig: '''Class for keeping track of an item in inventory.''' console: str sensor: str install_source: str post_install: str branch_source: Optional[str] def to_dict(self): base = { 'console': self.console, 'sensors': self.sensor, 'environment': self.install_source,
class RTMClient(object): """An RTMClient allows apps to communicate with the Slack Platform's RTM API. The event-driven architecture of this client allows you to simply link callbacks to their corresponding events. When an event occurs this client executes your callback while passing along any information it receives. Attributes: token (str): A string specifying an xoxp or xoxb token. run_async (bool): A boolean specifying if the client should be run in async mode. Default is False. auto_reconnect (bool): When true the client will automatically reconnect when (not manually) disconnected. Default is True. ssl (SSLContext): To use SSL support, pass an SSLContext object here. Default is None. proxy (str): To use proxy support, pass the string of the proxy server. e.g. "http://proxy.com" Authentication credentials can be passed in proxy URL. e.g. "http://*****:*****@some.proxy.com" Default is None. timeout (int): The amount of seconds the session should wait before timing out. Default is 30. base_url (str): The base url for all HTTP requests. Note: This is only used in the WebClient. Default is "https://www.slack.com/api/". connect_method (str): An string specifying if the client will connect with `rtm.connect` or `rtm.start`. Default is `rtm.connect`. ping_interval (int): automatically send "ping" command every specified period of seconds. If set to 0, do not send automatically. Default is 30. loop (AbstractEventLoop): An event loop provided by asyncio. If None is specified we attempt to use the current loop with `get_event_loop`. Default is None. Methods: ping: Sends a ping message over the websocket to Slack. typing: Sends a typing indicator to the specified channel. on: Stores and links callbacks to websocket and Slack events. run_on: Decorator that stores and links callbacks to websocket and Slack events. start: Starts an RTM Session with Slack. stop: Closes the websocket connection and ensures it won't reconnect. Example: ```python import os from slack import RTMClient @RTMClient.run_on(event="message") def say_hello(**payload): data = payload['data'] web_client = payload['web_client'] if 'Hello' in data['text']: channel_id = data['channel'] thread_ts = data['ts'] user = data['user'] web_client.chat_postMessage( channel=channel_id, text=f"Hi <@{user}>!", thread_ts=thread_ts ) slack_token = os.environ["SLACK_API_TOKEN"] rtm_client = RTMClient(token=slack_token) rtm_client.start() ``` Note: The initial state returned when establishing an RTM connection will be available as the data in payload for the 'open' event. This data is not and will not be stored on the RTM Client. Any attributes or methods prefixed with _underscores are intended to be "private" internal use only. They may be changed or removed at anytime. """ _callbacks: DefaultDict = collections.defaultdict(list) def __init__( self, *, token: str, run_async: Optional[bool] = False, auto_reconnect: Optional[bool] = True, ssl: Optional[SSLContext] = None, proxy: Optional[str] = None, timeout: Optional[int] = 30, base_url: Optional[str] = WebClient.BASE_URL, connect_method: Optional[str] = None, ping_interval: Optional[int] = 30, loop: Optional[asyncio.AbstractEventLoop] = None, headers: Optional[dict] = {}, ): self.token = token.strip() self.run_async = run_async self.auto_reconnect = auto_reconnect self.ssl = ssl self.proxy = proxy self.timeout = timeout self.base_url = base_url self.connect_method = connect_method self.ping_interval = ping_interval self.headers = headers self._event_loop = loop or asyncio.get_event_loop() self._web_client = None self._websocket = None self._session = None self._logger = logging.getLogger(__name__) self._last_message_id = 0 self._connection_attempts = 0 self._stopped = False self._web_client = WebClient( token=self.token, base_url=self.base_url, ssl=self.ssl, proxy=self.proxy, run_async=self.run_async, loop=self._event_loop, session=self._session, headers=self.headers, ) @staticmethod def run_on(*, event: str): """A decorator to store and link a callback to an event.""" def decorator(callback): RTMClient.on(event=event, callback=callback) return callback return decorator @classmethod def on(cls, *, event: str, callback: Callable): """Stores and links the callback(s) to the event. Args: event (str): A string that specifies a Slack or websocket event. e.g. 'channel_joined' or 'open' callback (Callable): Any object or a list of objects that can be called. e.g. <function say_hello at 0x101234567> or [<function say_hello at 0x10123>,<function say_bye at 0x10456>] Raises: SlackClientError: The specified callback is not callable. SlackClientError: The callback must accept keyword arguments (**kwargs). """ if isinstance(callback, list): for cb in callback: cls._validate_callback(cb) previous_callbacks = cls._callbacks[event] cls._callbacks[event] = list(set(previous_callbacks + callback)) else: cls._validate_callback(callback) cls._callbacks[event].append(callback) def start(self) -> asyncio.Future: """Starts an RTM Session with Slack. Makes an authenticated call to Slack's RTM API to retrieve a websocket URL and then connects to the message server. As events stream-in we run any associated callbacks stored on the client. If 'auto_reconnect' is specified we retrieve a new url and reconnect any time the connection is lost unintentionally or an exception is thrown. Raises: SlackApiError: Unable to retrieve RTM URL from Slack. """ # TODO: Add Windows support for graceful shutdowns. if os.name != "nt" and current_thread() == main_thread(): signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) for s in signals: self._event_loop.add_signal_handler(s, self.stop) future = asyncio.ensure_future(self._connect_and_read(), loop=self._event_loop) if self.run_async: return future return self._event_loop.run_until_complete(future) def stop(self): """Closes the websocket connection and ensures it won't reconnect. If your application outputs the following errors, call #async_stop() instead and await for the completion on your application side. asyncio/base_events.py:641: RuntimeWarning: coroutine 'ClientWebSocketResponse.close' was never awaited self._ready.clear() """ self._logger.debug("The Slack RTMClient is shutting down.") self._stopped = True self._close_websocket() async def async_stop(self): """Closes the websocket connection and ensures it won't reconnect.""" self._logger.debug("The Slack RTMClient is shutting down.") remaining_futures = self._close_websocket() for future in remaining_futures: await future self._stopped = True def send_over_websocket(self, *, payload: dict): """Sends a message to Slack over the WebSocket connection. Note: The RTM API only supports posting simple messages formatted using our default message formatting mode. It does not support attachments or other message formatting modes. For this reason we recommend users send messages via the Web API methods. e.g. web_client.chat_postMessage() If the message "id" is not specified in the payload, it'll be added. Args: payload (dict): The message to send over the wesocket. e.g. { "id": 1, "type": "typing", "channel": "C024BE91L" } Raises: SlackClientNotConnectedError: Websocket connection is closed. """ return asyncio.ensure_future(self._send_json(payload), loop=self._event_loop) async def _send_json(self, payload): if self._websocket is None or self._event_loop is None: raise client_err.SlackClientNotConnectedError( "Websocket connection is closed.") if "id" not in payload: payload["id"] = self._next_msg_id() return await self._websocket.send_json(payload) async def ping(self): """Sends a ping message over the websocket to Slack. Not all web browsers support the WebSocket ping spec, so the RTM protocol also supports ping/pong messages. Raises: SlackClientNotConnectedError: Websocket connection is closed. """ payload = {"id": self._next_msg_id(), "type": "ping"} await self._send_json(payload=payload) async def typing(self, *, channel: str): """Sends a typing indicator to the specified channel. This indicates that this app is currently writing a message to send to a channel. Args: channel (str): The channel id. e.g. 'C024BE91L' Raises: SlackClientNotConnectedError: Websocket connection is closed. """ payload = { "id": self._next_msg_id(), "type": "typing", "channel": channel } await self._send_json(payload=payload) @staticmethod def _validate_callback(callback): """Checks if the specified callback is callable and accepts a kwargs param. Args: callback (obj): Any object or a list of objects that can be called. e.g. <function say_hello at 0x101234567> Raises: SlackClientError: The specified callback is not callable. SlackClientError: The callback must accept keyword arguments (**kwargs). """ cb_name = callback.__name__ if hasattr(callback, "__name__") else callback if not callable(callback): msg = "The specified callback '{}' is not callable.".format( cb_name) raise client_err.SlackClientError(msg) callback_params = inspect.signature(callback).parameters.values() if not any(param for param in callback_params if param.kind == param.VAR_KEYWORD): msg = "The callback '{}' must accept keyword arguments (**kwargs).".format( cb_name) raise client_err.SlackClientError(msg) def _next_msg_id(self): """Retrieves the next message id. When sending messages to Slack every event should have a unique (for that connection) positive integer ID. Returns: An integer representing the message id. e.g. 98 """ self._last_message_id += 1 return self._last_message_id async def _connect_and_read(self): """Retrieves the WS url and connects to Slack's RTM API. Makes an authenticated call to Slack's Web API to retrieve a websocket URL. Then connects to the message server and reads event messages as they come in. If 'auto_reconnect' is specified we retrieve a new url and reconnect any time the connection is lost unintentionally or an exception is thrown. Raises: SlackApiError: Unable to retrieve RTM URL from Slack. websockets.exceptions: Errors thrown by the 'websockets' library. """ while not self._stopped: try: self._connection_attempts += 1 async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout( total=self.timeout)) as session: self._session = session url, data = await self._retrieve_websocket_info() async with session.ws_connect( url, heartbeat=self.ping_interval, ssl=self.ssl, proxy=self.proxy, ) as websocket: self._logger.debug( "The Websocket connection has been opened.") self._websocket = websocket await self._dispatch_event(event="open", data=data) await self._read_messages() # The websocket has been disconnected, or self._stopped is True if not self._stopped and not self.auto_reconnect: self._logger.warning( "Not reconnecting the Websocket because auto_reconnect is False" ) return # No need to wait exponentially here, since the connection was # established OK, but timed out, or was closed remotely except ( client_err.SlackClientNotConnectedError, client_err.SlackApiError, # TODO: Catch websocket exceptions thrown by aiohttp. ) as exception: await self._dispatch_event(event="error", data=exception) error_code = (exception.response.get("error", None) if hasattr( exception, "response") else None) if (self.auto_reconnect and not self._stopped and error_code != "invalid_auth" # "invalid_auth" is unrecoverable ): await self._wait_exponentially(exception) continue self._logger.exception( "The Websocket encountered an error. Closing the connection..." ) self._close_websocket() raise async def _read_messages(self): """Process messages received on the WebSocket connection.""" text_message_callback_executions: List[Future] = [] while not self._stopped and self._websocket is not None: for future in text_message_callback_executions: if future.done(): text_message_callback_executions.remove(future) try: # Wait for a message to be received, but timeout after a second so that # we can check if the socket has been closed, or if self._stopped is # True message = await self._websocket.receive(timeout=1) except asyncio.TimeoutError: if not self._websocket.closed: # We didn't receive a message within the timeout interval, but # aiohttp hasn't closed the socket, so ping responses must still be # returning continue self._logger.warning("Websocket was closed (%s).", self._websocket.close_code) await self._dispatch_event(event="error", data=self._websocket.exception()) self._websocket = None await self._dispatch_event(event="close") num_of_running_callbacks = len( text_message_callback_executions) if num_of_running_callbacks > 0: self._logger.info( "WebSocket connection has been closed " f"though {num_of_running_callbacks} callback executions were still in progress" ) return if message.type == aiohttp.WSMsgType.TEXT: payload = message.json() event = payload.pop("type", "Unknown") async def run_dispatch_event(): try: await self._dispatch_event(event, data=payload) except Exception as err: data = message.data if message else message self._logger.info( f"Caught a raised exception ({err}) while dispatching a TEXT message ({data})" ) # Raised exceptions here happen in users' code and were just unhandled. # As they're not intended for closing current WebSocket connection, # this exception should not be propagated to higher level (#_connect_and_read()). return # Asynchronously run callbacks to handle simultaneous incoming messages from Slack f = asyncio.ensure_future(run_dispatch_event()) text_message_callback_executions.append(f) elif message.type == aiohttp.WSMsgType.ERROR: self._logger.error("Received an error on the websocket: %r", message) await self._dispatch_event(event="error", data=message) elif message.type in ( aiohttp.WSMsgType.CLOSE, aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSED, ): self._logger.warning("Websocket was closed.") self._websocket = None await self._dispatch_event(event="close") else: self._logger.debug("Received unhandled message type: %r", message) async def _dispatch_event(self, event, data=None): """Dispatches the event and executes any associated callbacks. Note: To prevent the app from crashing due to callback errors. We catch all exceptions and send all data to the logger. Args: event (str): The type of event. e.g. 'bot_added' data (dict): The data Slack sent. e.g. { "type": "bot_added", "bot": { "id": "B024BE7LH", "app_id": "A4H1JB4AZ", "name": "hugbot" } } """ for callback in self._callbacks[event]: self._logger.debug( "Running %s callbacks for event: '%s'", len(self._callbacks[event]), event, ) try: if self._stopped and event not in ["close", "error"]: # Don't run callbacks if client was stopped unless they're # close/error callbacks. break if inspect.iscoroutinefunction(callback): await callback(rtm_client=self, web_client=self._web_client, data=data) else: if self.run_async is True: raise client_err.SlackRequestError( f'The callback "{callback.__name__}" is NOT a coroutine. ' "Running such with run_async=True is unsupported. " "Consider adding async/await to the method " "or going with run_async=False if your app is not really non-blocking." ) payload = { "rtm_client": self, "web_client": self._web_client, "data": data, } callback(**payload) except Exception as err: name = callback.__name__ module = callback.__module__ msg = f"When calling '#{name}()' in the '{module}' module the following error was raised: {err}" self._logger.error(msg) raise async def _retrieve_websocket_info(self): """Retrieves the WebSocket info from Slack. Returns: A tuple of websocket information. e.g. ( "wss://...", { "self": {"id": "U01234ABC","name": "robotoverlord"}, "team": { "domain": "exampledomain", "id": "T123450FP", "name": "ExampleName" } } ) Raises: SlackApiError: Unable to retrieve RTM URL from Slack. """ if self._web_client is None: self._web_client = WebClient( token=self.token, base_url=self.base_url, ssl=self.ssl, proxy=self.proxy, run_async=True, loop=self._event_loop, session=self._session, headers=self.headers, ) self._logger.debug("Retrieving websocket info.") use_rtm_start = self.connect_method in ["rtm.start", "rtm_start"] if self.run_async: if use_rtm_start: resp = await self._web_client.rtm_start() else: resp = await self._web_client.rtm_connect() else: if use_rtm_start: resp = self._web_client.rtm_start() else: resp = self._web_client.rtm_connect() url = resp.get("url") if url is None: msg = "Unable to retrieve RTM URL from Slack." raise client_err.SlackApiError(message=msg, response=resp) return url, resp.data async def _wait_exponentially(self, exception, max_wait_time=300): """Wait exponentially longer for each connection attempt. Calculate the number of seconds to wait and then add a random number of milliseconds to avoid coincidental synchronized client retries. Wait up to the maximum amount of wait time specified via 'max_wait_time'. However, if Slack returned how long to wait use that. """ wait_time = exception.response.get("headers", {}).get( "Retry-After", min((2**self._connection_attempts) + random.random(), max_wait_time), ) self._logger.debug("Waiting %s seconds before reconnecting.", wait_time) await asyncio.sleep(float(wait_time)) def _close_websocket(self) -> List[Future]: """Closes the websocket connection.""" futures = [] close_method = getattr(self._websocket, "close", None) if callable(close_method): future = asyncio.ensure_future(close_method(), loop=self._event_loop) futures.append(future) self._websocket = None event_f = asyncio.ensure_future(self._dispatch_event(event="close"), loop=self._event_loop) futures.append(event_f) return futures
def get_messages(): team = request.args.get("team", type=str) channel_name = request.args.get("channel", type=str) start_time = request.args.get("from", type=str) end_time = request.args.get("to", type=str) slack_info = SlackInfo.query.filter_by(team_name=team).first() if slack_info is None: return jsonify({"type": "not_found", "text": "Team not found"}), 400 slack = WebClient(slack_info.bot_access_token) channel_list = slack.conversations_list( token=slack_info.bot_access_token, types="public_channel, private_channel") try: channel = next( filter(lambda channel: channel["name"] == channel_name, channel_list["channels"])) message_history = slack.conversations_history( channel=channel["id"], token=slack_info.bot_access_token, oldest=start_time, latest=end_time) except StopIteration: return jsonify({"type": "not_found", "text": "Channel not found"}), 400 except SlackApiError: return jsonify({ "type": "not_in_channel", "text": "The bot is not a member of the channel" }), 400 response_keys = ["text", "sender", "time"] request_keys = ["text", "user", "ts"] response = [{ response_key: message[request_key] for response_key, request_key in zip(response_keys, request_keys) } for message in message_history["messages"]] for message, item in zip(message_history["messages"], response): if "thread_ts" in message: # Get thread messages and delete parent message thread_messages = slack.conversations_replies( channel=channel["id"], ts=message["thread_ts"]).data["messages"][1::] item["thread"] = [{ response_key: thread_message[request_key] for response_key, request_key in zip(response_keys, request_keys) } for thread_message in thread_messages] else: item["thread"] = [] # Replace user ID with username item["sender"] = slack.users_info(token=slack_info.bot_access_token, user=item["sender"])["user"]["name"] for thread in item["thread"]: # Replace user ID with username in thread's message thread["sender"] = slack.users_info( token=slack_info.bot_access_token, user=thread["sender"])["user"]["name"] return jsonify(response)
def slack_on(self): if not self.slack: self.slack = WebClient(token=self.api_token)
class LowLevelSlackClient(metaclass=Singleton): def __init__(self): _settings, _ = import_settings() slack_api_token = _settings.get('SLACK_API_TOKEN', None) http_proxy = _settings.get('HTTP_PROXY', None) self.rtm_client = RTMClient(token=slack_api_token, proxy=http_proxy) self.web_client = WebClient(token=slack_api_token, proxy=http_proxy) self._bot_info = {} self._users = {} self._channels = {} @staticmethod def get_instance() -> 'LowLevelSlackClient': return LowLevelSlackClient() def _register_user(self, user_response): user = User.from_api_response(user_response) self._users[user.id] = user return user def _register_channel(self, channel_response): channel = Channel.from_api_response(channel_response) self._channels[channel.id] = channel return channel def ping(self): # Ugly hack because some parts of slackclient > 2.0 are async-only (like the ping function) # and Slack Machine isn't async yet loop = asyncio.new_event_loop() result = self.rtm_client.ping() loop.run_until_complete(result) def _on_open(self, **payload): # Set bot info self._bot_info = payload['data']['self'] # Build user cache all_users = call_paginated_endpoint(self.web_client.users_list, 'members') for u in all_users: self._register_user(u) logger.debug("Number of users found: %s" % len(self._users)) logger.debug("Users: %s" % ", ".join([ f"{u.profile.display_name}|{u.profile.real_name}" for u in self._users.values() ])) # Build channel cache all_channels = call_paginated_endpoint( self.web_client.conversations_list, 'channels', types='public_channel,private_channel') for c in all_channels: self._register_channel(c) logger.debug("Number of channels found: %s" % len(self._channels)) logger.debug("Channels: %s" % ", ".join([c.name for c in self._channels.values()])) def _on_team_join(self, **payload): user = self._register_user(payload['data']['user']) logger.debug("User joined team: %s" % user) def _on_user_change(self, **payload): user = self._register_user(payload['data']['user']) logger.debug("User changed: %s" % user) def _on_channel_created(self, **payload): channel_resp = self.web_client.channels_info( channel=payload['data']['channel']['id']) channel = self._register_channel(channel_resp['channel']) logger.debug("Channel created: %s" % channel) def _on_channel_updated(self, **payload): data = payload['data'] if isinstance(data['channel'], dict): channel_id = data['channel']['id'] else: channel_id = data['channel'] channel_resp = self.web_client.channels_info(channel=channel_id) channel = self._register_channel(channel_resp['channel']) logger.debug("Channel updated: %s" % channel) def _on_channel_deleted(self, **payload): channel = self._channels[payload['data']['channel']] del self._channels[payload['data']['channel']] logger.debug("Channel %s deleted" % channel.name) @property def bot_info(self) -> Dict[str, str]: return self._bot_info def start(self): RTMClient.on(event='open', callback=self._on_open) RTMClient.on(event='team_join', callback=self._on_team_join) RTMClient.on(event='channel_created', callback=self._on_channel_created) RTMClient.on(event='channel_deleted', callback=self._on_channel_deleted) RTMClient.on(event='channel_rename', callback=self._on_channel_updated) RTMClient.on(event='channel_archive', callback=self._on_channel_updated) RTMClient.on(event='channel_unarchive', callback=self._on_channel_updated) RTMClient.on(event='user_change', callback=self._on_user_change) self.rtm_client.start() @property def users(self) -> Dict[str, User]: return self._users @property def channels(self) -> Dict[str, Channel]: return self._channels
class SlackIntegration(Integration): api_token = models.CharField(_("API token"), max_length=100) channel = models.CharField(_("channel"), max_length=100) notify_issue_create = models.BooleanField(_("notify on issue creation"), default=True) notify_issue_modify = models.BooleanField(_("notify on issue modification"), default=True) notify_comment_create = models.BooleanField(_("notify when there is a new comment"), default=True) notify_sprint_start = models.BooleanField(_("notify when a sprint is started"), default=True) notify_sprint_stop = models.BooleanField(_("notify when when a sprint is stopped"), default=True) slack = None class Meta: verbose_name = _("slackintegration") verbose_name_plural = _("slackintegrations") def __str__(self): return self.channel def connect_signals(self): if self.notify_issue_create: signals.create.connect(self.on_issue_signal, weak=False, sender=Issue) if self.notify_issue_modify: signals.modify.connect(self.on_issue_signal, weak=False, sender=Issue) if self.notify_comment_create: signals.create.connect(self.on_comment_signal, weak=False, sender=Comment) if self.notify_sprint_start: signals.start.connect(self.on_sprint_signal, weak=False, sender=Sprint) if self.notify_sprint_stop: signals.stop.connect(self.on_sprint_signal, weak=False, sender=Sprint) def disconnect_signals(self): if not self.notify_issue_create: signals.create.disconnect(self.on_issue_signal, sender=Issue) if not self.notify_issue_modify: signals.modify.disconnect(self.on_issue_signal, sender=Issue) if not self.notify_comment_create: signals.create.disconnect(self.on_comment_signal, sender=Comment) if not self.notify_sprint_start: signals.start.disconnect(self.on_sprint_signal, sender=Sprint) if not self.notify_sprint_stop: signals.stop.disconnect(self.on_sprint_signal, sender=Sprint) def save(self, *args, **kwargs): if not self.pk: self.connect_signals() else: self.disconnect_signals() super(SlackIntegration, self).save(*args, **kwargs) def slack_on(self): if not self.slack: self.slack = WebClient(token=self.api_token) def on_issue_signal(self, sender, signal, **kwargs): if "instance" not in kwargs or \ kwargs['instance'].project != self.project: return self.slack_on() issue = kwargs['instance'] user = kwargs['user'] protocol = 'https://' if DEBUG: protocol = 'http://' title_link = protocol + HOST + issue.get_absolute_url() issue_title = issue.get_ticket_identifier() + ' ' + issue.title user_name = str(user) user_link = protocol + HOST + user.get_absolute_url() user_avatar = protocol + HOST + user.avatar.url if signal == signals.modify: text = '{} changed issue {}.'.format(user_name, issue_title) fields = [] for field in kwargs['changed_data']: old_str = kwargs['changed_data'][field] new_field = issue.__getattribute__(field) new_str = str(new_field) # Ducktyping for RelatedManager if hasattr(new_field, "add") and \ hasattr(new_field, "create") and \ hasattr(new_field, "remove") and \ hasattr(new_field, "clear") and \ hasattr(new_field, "set"): new_str = ", ".join([str(e) for e in new_field.all()]) fields.append({ 'title': Issue._meta.get_field(field).verbose_name.title(), 'value': '{} → {}'.format(old_str, new_str), 'short': True, }) if field == 'description': fields[-1]['short'] = False resp = self.slack.chat_postMessage( channel=self.channel, attachments=[{ 'fallback': text, 'pretext': 'Issue changed:', 'title': issue_title, 'title_link': title_link, 'author_name': user_name, 'author_link': user_link, 'author_icon': user_avatar, 'fields': fields, 'color': 'good', }] ) elif signal == signals.create: text = '{} created issue {}.'.format(user_name, issue_title) resp = self.slack.chat_postMessage( channel=self.channel, attachments=[{ 'fallback': text, 'pretext': 'New Issue:', 'text': issue.description, 'title': issue_title, 'title_link': title_link, 'author_name': user_name, 'author_link': user_link, 'author_icon': user_avatar, 'color': 'good', }] ) def on_comment_signal(self, sender, signal, **kwargs): if "instance" not in kwargs or \ kwargs['instance'].issue.project != self.project: return self.slack_on() comment = kwargs['instance'] user = kwargs['user'] protocol = 'https://' if DEBUG: protocol = 'http://' title_link = protocol + HOST + comment.issue.get_absolute_url() issue_title = comment.issue.get_ticket_identifier() + ' ' + comment.issue.title user_link = protocol + HOST + user.get_absolute_url() user_avatar = protocol + HOST + user.avatar.url if signal == signals.create: text = '{} commented on "{}".'.format(str(user), issue_title) resp = self.slack.chat_postMessage( channel=self.channel, attachments=[{ 'fallback': text, 'pretext': 'New comment:', 'text': comment.text, 'title': issue_title, 'title_link': title_link, 'author_name': str(user), 'author_link': user_link, 'author_icon': user_avatar, 'color': 'good', }] ) def on_sprint_signal(self, sender, signal, **kwargs): if "instance" not in kwargs or \ kwargs['instance'].project != self.project: return self.slack_on() sprint = kwargs['instance'] user = kwargs['user'] protocol = 'https://' if DEBUG: protocol = 'http://' title_link = protocol + HOST + reverse("backlog:backlog", kwargs={'project': self.project.name_short}) title = "sprint {}".format(sprint.seqnum) user_link = protocol + HOST + user.get_absolute_url() user_avatar = protocol + HOST + user.avatar.url action = "" text = "" if signal == signals.start: action = "started" text = '{} started {}.'.format(str(user), title) elif signal == signals.stop: action = "stopped" text = '{} stopped {}.'.format(str(user), title) title = title.capitalize() date_format = "%D" fields = [] fields.append({ 'title': _("Started"), 'value': sprint.startdate.strftime(date_format), 'short': True, }) if action == "stopped": fields.append({ 'title': _("Stopped"), 'value': sprint.enddate.strftime(date_format), 'short': True, }) if sprint.plandate: fields.append({ 'title': _("Planned end"), 'value': sprint.plandate.strftime(date_format), 'short': True, }) resp = self.slack.chat_postMessage( channel=self.channel, attachments=[{ 'fallback': text, 'pretext': 'Sprint {}:'.format(action), 'text': '', 'title': title, 'title_link': title_link, 'author_name': str(user), 'author_link': user_link, 'author_icon': user_avatar, 'fields': fields, 'color': 'good', }] ) def user_has_write_permissions(self, user): return self.project.is_manager(user) def user_has_read_permissions(self, user): return self.project.user_has_read_permission(user)
def __init__(self, db_uri=None, config=None, preload_path=None, **kwargs): Flask.__init__(self, __name__) # Preload default configuration self.config.from_object(config) self.config.from_mapping(kwargs) # Set the secret key for this instance (creating one if one does not exist already) self.config["SECRET_KEY"] = self.config["SECRET_KEY"] or str( uuid.uuid4()) # Configure database if db_uri: self.config["SQLALCHEMY_DATABASE_URI"] = db_uri if self.config["SQLALCHEMY_DATABASE_URI"] == "sqlite:///:memory:": self.logger.warning( "Using Sqlite in-memory database, all data will be lost when server shuts down!" ) # DB dialect logic - used for lookup operations db_dialect = self.config["SQLALCHEMY_DATABASE_URI"].split(":")[0] self.logger.info(f"Attempting to use db dialect {db_dialect}") if self.config.get("DEBUG") is not True: self.logger.warning( "It is strongly recommended that you do not use Sqlite for production deployments!" ) if not any([i == db_dialect for i in ["postgres", "sqlite"]]): raise RuntimeError( f"Dialect {db_dialect} not supported - please use sqlite or postgres" ) self.config["DB_DIALECT"] = db_dialect # Register database schema with flask app sqlalchemy_db.init_app(self) # Set up database migration information # Registers Migrate plugin in self.extensions['migrate'] Migrate(self, self.db) # Try to create the database if it does not already exist # Existence is determined by whether there is an existing alembic migration revision db_auto_create = self.config.get("DB_AUTO_CREATE", True) db_auto_upgrade = self.config.get("DB_AUTO_UPGRADE", True) if db_auto_create and self.db_revision is None: self.db_init() elif db_auto_upgrade: self.db_upgrade() self.logger.setLevel(logging.DEBUG) # Install postgres fuzzystrmatch extension if db_dialect == "postgres": self.logger.info("Enabling Postgres fuzzy string matching") with self.app_context(), self.db.engine.connect() as conn: conn.execute("CREATE EXTENSION IF NOT EXISTS fuzzystrmatch") # Handle preloading an existing Terminology set self.handle_whatis_preload(preload_path) # Register Slack client on the current application instance if all([ self.config.get(i) is None for i in ["SLACK_SIGNING_SECRET", "SLACK_TOKEN"] ]): raise RuntimeError( "Whatis must have both a slack signing secret and slack bot token set" ) self.sc = WebClient(self.config.get("SLACK_TOKEN"), ssl=False) from whatis.routes.slack_route import slack_blueprint self.register_blueprint(slack_blueprint, url_prefix="/slack") if not all([ type(self.config[i]) == list for i in ["ADMIN_USER_IDS", "ADMIN_CHANNEL_IDS"] ]): raise RuntimeError( "ADMIN_USER_IDS and ADMIN_CHANNEL_IDS must be lists of Admin user IDs or channel IDs" ) try: au = self.admin_users self.logger.info(f"Initial Admin users set as {au}") except SlackApiError as s: raise RuntimeError( f"Failed to get Admin users from specified Admin channels - are you sure the whatis bot " f"is invited and has the necessary scopes {s}") # Register a basic route for healthchecking @self.route("/ping") def healthcheck(): return "pong"