Esempio n. 1
0
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)
Esempio n. 2
0
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'
        }
    }
Esempio n. 4
0
 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']})
Esempio n. 5
0
    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)
Esempio n. 6
0
 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 = {}
Esempio n. 7
0
    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
Esempio n. 8
0
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)
Esempio n. 9
0
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
Esempio n. 10
0
def GetSlackWebClient():
    global SlackWebClient
    if SlackWebClient is None:
        slack_token = os.environ['SLACK_BOT_TOKEN']
        SlackWebClient = WebClient(token=slack_token)

    return SlackWebClient
Esempio n. 11
0
    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)
Esempio n. 12
0
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)
Esempio n. 13
0
    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)
Esempio n. 14
0
 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)
Esempio n. 15
0
 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,
     )
Esempio n. 16
0
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)
Esempio n. 17
0
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! '
Esempio n. 18
0
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)
Esempio n. 19
0
    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()
Esempio n. 20
0
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)
Esempio n. 21
0
    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")
Esempio n. 22
0
# 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()
Esempio n. 23
0
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)
Esempio n. 24
0
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
Esempio n. 26
0
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)
Esempio n. 27
0
 def slack_on(self):
     if not self.slack:
         self.slack = WebClient(token=self.api_token)
Esempio n. 28
0
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
Esempio n. 29
0
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)
Esempio n. 30
0
    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"