From 95bda42cb8f81c03540e9a2d42da4cec4210dd13 Mon Sep 17 00:00:00 2001 From: Nicolas Barra <47286193+nicolasbarra@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:08:02 -0500 Subject: [PATCH 1/6] [INTPROD-8358] Bump setuptools from 56.2.0 to 59.6.0 (#59) [INTPROD-8358](https://jira.lyft.net/browse/INTPROD-8358) --- piptools_requirements3.txt | 2 +- requirements3.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/piptools_requirements3.txt b/piptools_requirements3.txt index aa51f1a..d174f97 100644 --- a/piptools_requirements3.txt +++ b/piptools_requirements3.txt @@ -3,4 +3,4 @@ # pip==19.2 -setuptools==56.2.0 +setuptools==59.6.0 diff --git a/requirements3.txt b/requirements3.txt index 49111cc..5cbf794 100644 --- a/requirements3.txt +++ b/requirements3.txt @@ -54,4 +54,4 @@ zope.event==4.5.0 # via gevent zope.interface==5.4.0 # via gevent pip==19.2 # via -r piptools_requirements3.txt -setuptools==56.2.0 # via -r piptools_requirements3.txt, gevent, zope.event, zope.interface +setuptools==59.6.0 # via -r piptools_requirements3.txt, gevent, zope.event, zope.interface From 9e2947792e8b5645cf52052ec2abe8229da938a4 Mon Sep 17 00:00:00 2001 From: Nicolas Barra <47286193+nicolasbarra@users.noreply.github.com> Date: Mon, 13 Feb 2023 14:32:49 -0500 Subject: [PATCH 2/6] Create dependency-review.yml (#62) --- .github/workflows/dependency-review.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..fe461b4 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v3 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v2 From 5915a1baaac7355791335470ea827717c534cde9 Mon Sep 17 00:00:00 2001 From: Nicolas Barra <47286193+nicolasbarra@users.noreply.github.com> Date: Mon, 13 Feb 2023 15:42:24 -0500 Subject: [PATCH 3/6] [INTPROD-8358] Create codeql.yml (#61) [INTPROD-8358](https://jira.lyft.net/browse/INTPROD-8358) --- .github/workflows/codeql.yml | 76 ++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..84ecf85 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,76 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ "master" ] + schedule: + - cron: '39 14 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Use only 'java' to analyze code written in Java, Kotlin or both + # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{matrix.language}}" From 49f02e83a47ba2404d89ba4bd13f5f81c504eab6 Mon Sep 17 00:00:00 2001 From: Nicolas Barra <47286193+nicolasbarra@users.noreply.github.com> Date: Mon, 13 Feb 2023 15:57:14 -0500 Subject: [PATCH 4/6] Update Github Actions to use Python 3.8 for building (#64) --- .github/workflows/pull_request.yml | 30 +++++++++++++++--------------- .github/workflows/push.yml | 24 ++++++++++++------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index c639b5d..bff696c 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,12 +3,12 @@ jobs: pre-commit: runs-on: ubuntu-18.04 steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Setup python 3.6 - uses: actions/setup-python@v1 + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup python 3.8 + uses: actions/setup-python@v4 with: - python-version: 3.6 + python-version: 3.8 - name: Install pre-commit run: pip install pre-commit - name: Run pre-commit @@ -16,8 +16,8 @@ jobs: license-check: runs-on: ubuntu-18.04 steps: - - name: Checkout - uses: actions/checkout@v1 + - name: Checkout repository + uses: actions/checkout@v3 - name: Setup Ruby 2.x uses: actions/setup-ruby@v1 with: @@ -26,10 +26,10 @@ jobs: run: gem install license_finder - name: Allow gevent Zope license run: license_finder permitted_licenses add "Zope Public License" - - name: Setup python 3.6 - uses: actions/setup-python@v1 + - name: Setup python 3.8 + uses: actions/setup-python@v4 with: - python-version: 3.6 + python-version: 3.8 - name: Install apt dependencies run: sudo apt-get update -y && sudo apt-get install -y python3-dev openssl libssl-dev gcc pkg-config libffi-dev libxml2-dev libxmlsec1-dev - name: Install dependencies @@ -39,12 +39,12 @@ jobs: test: runs-on: ubuntu-18.04 steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Setup python 3.6 - uses: actions/setup-python@v1 + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup python 3.8 + uses: actions/setup-python@v4 with: - python-version: 3.6 + python-version: 3.8 - name: Install apt dependencies run: sudo apt-get update -y && sudo apt-get install -y python3-dev openssl libssl-dev gcc pkg-config libffi-dev libxml2-dev libxmlsec1-dev - name: Install dependencies diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index cd4f2d9..667410b 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -10,12 +10,12 @@ jobs: name: Build and publish docs runs-on: ubuntu-18.04 steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Setup python 3.6 - uses: actions/setup-python@v1 + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup python 3.8 + uses: actions/setup-python@v4 with: - python-version: 3.6 + python-version: 3.8 - name: Install virtualenv run: pip install virtualenv - name: Build docs @@ -33,12 +33,12 @@ jobs: name: Build and publish python module to pypi runs-on: ubuntu-18.04 steps: - - name: Checkout - uses: actions/checkout@v1 - - name: Setup python 3.6 - uses: actions/setup-python@v1 + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup python 3.8 + uses: actions/setup-python@v4 with: - python-version: 3.6 + python-version: 3.8 - name: Add wheel dependency run: pip install wheel - name: Generate dist @@ -53,8 +53,8 @@ jobs: name: Build and publish docker image runs-on: ubuntu-18.04 steps: - - name: Checkout - uses: actions/checkout@v1 + - name: Checkout repository + uses: actions/checkout@v3 - name: Publish to Registry uses: elgohr/Publish-Docker-Github-Action@2.8 with: From 54e2d76cf81511ee10ddd2c4059724c1eb667e96 Mon Sep 17 00:00:00 2001 From: Nicolas Barra <47286193+nicolasbarra@users.noreply.github.com> Date: Mon, 13 Feb 2023 16:04:22 -0500 Subject: [PATCH 5/6] Update .pre-commit-config.yaml (#67) --- .pre-commit-config.yaml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a94479a..99effa3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,9 +1,25 @@ exclude: '^docs/.*$' +default_language_version: + python: python3.8 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v4.3.0 + hooks: + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace +- repo: https://github.com/pycqa/flake8 + rev: 5.0.4 hooks: - id: flake8 additional_dependencies: - - flake8==3.3.0 - - flake8-tidy-imports==1.0.6 + - flake8-bugbear==22.10.27 + - flake8-builtins==2.0.1 + - flake8-comprehensions==3.10.1 + - flake8-tidy-imports==4.8.0 + From 67baa6b79b0c98f085a975efae23d191db889852 Mon Sep 17 00:00:00 2001 From: Nicolas Barra <47286193+nicolasbarra@users.noreply.github.com> Date: Mon, 13 Feb 2023 16:18:45 -0500 Subject: [PATCH 6/6] Add black to pre-commit (#68) --- .pre-commit-config.yaml | 7 +- .../view_submission_synchronous_test.json | 2 +- omnibot/__init__.py | 2 +- omnibot/authnz/__init__.py | 17 +- omnibot/authnz/envoy_checks.py | 36 +- .../interactive_component_callbacks.py | 12 +- omnibot/callbacks/message_callbacks.py | 285 ++++---- omnibot/callbacks/network_callbacks.py | 33 +- omnibot/callbacks/slash_command_callbacks.py | 90 +-- omnibot/processor.py | 351 ++++------ omnibot/routes/api.py | 622 +++++++++--------- omnibot/scripts/omniredis.py | 20 +- omnibot/scripts/utils.py | 7 +- omnibot/services/__init__.py | 42 +- omnibot/services/omniredis.py | 2 +- omnibot/services/slack/__init__.py | 297 ++++----- omnibot/services/slack/bot.py | 36 +- .../services/slack/interactive_component.py | 177 +++-- omnibot/services/slack/message.py | 196 +++--- omnibot/services/slack/parser.py | 125 ++-- omnibot/services/slack/slash_command.py | 157 ++--- omnibot/services/slack/team.py | 8 +- omnibot/services/sqs.py | 11 +- omnibot/services/stats.py | 4 +- omnibot/settings.py | 101 ++- omnibot/setup_logging.py | 6 +- omnibot/utils/__init__.py | 22 +- omnibot/utils/settings.py | 4 +- omnibot/watcher.py | 117 ++-- omnibot/webhook_worker.py | 115 ++-- omnibot/wsgi.py | 11 +- setup.py | 4 +- tests/data/__init__.py | 2 +- tests/integration/routes/test_interactive.py | 6 +- tests/unit/omnibot/authnz/authnz_test.py | 26 +- .../unit/omnibot/authnz/envoy_checks_test.py | 82 +-- tests/unit/omnibot/services/slack/bot_test.py | 46 +- .../slack/interactive_component_test.py | 204 +++--- .../omnibot/services/slack/message_test.py | 154 ++--- .../omnibot/services/slack/parser_test.py | 16 +- .../unit/omnibot/services/slack/team_test.py | 16 +- tests/unit/omnibot/settings_test.py | 46 +- 42 files changed, 1562 insertions(+), 1955 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99effa3..c7ac204 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ default_language_version: python: python3.8 repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-docstring-first - id: check-executables-have-shebangs @@ -13,6 +13,10 @@ repos: - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace +- repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black - repo: https://github.com/pycqa/flake8 rev: 5.0.4 hooks: @@ -22,4 +26,3 @@ repos: - flake8-builtins==2.0.1 - flake8-comprehensions==3.10.1 - flake8-tidy-imports==4.8.0 - diff --git a/data/mock/interactive/view_submission_synchronous_test.json b/data/mock/interactive/view_submission_synchronous_test.json index 7c8e208..1594438 100644 --- a/data/mock/interactive/view_submission_synchronous_test.json +++ b/data/mock/interactive/view_submission_synchronous_test.json @@ -1,3 +1,3 @@ { "payload": "{\"type\":\"view_submission\",\"token\":\"ABCDEFGHIJKLMNOPQRSTUVWX\",\"team\":{\"id\":\"TEST_TEAM_ID\",\"domain\":\"test-team-name\"},\"user\":{\"id\":\"TEST_USER_ID\",\"name\":\"testusername\"},\"view\":{\"id\":\"VNHU13V36\",\"type\":\"modal\",\"title\":{ \"a\":\"b\" },\"submit\":{ \"a\":\"b\" },\"blocks\":[],\"private_metadata\":\"shhh-its-secret\",\"callback_id\":\"modal-with-inputs\",\"state\":{\"values\":{\"multiline\":{\"mlvalue\":{\"type\":\"plain_text_input\",\"value\":\"This is my example inputted value\"}},\"target_channel\":{\"target_select\":{\"type\":\"conversations_select\",\"selected_conversation\":\"C123B12DE\"}}}},\"hash\":\"156663117.cd33ad1f\",\"response_urls\":[{\"block_id\":\"target_channel\",\"action_id\":\"target_select\",\"channel_id\":\"C123B12DE\",\"response_url\":\"https:\\/\\/hooks.slack.com\\/app\\/ABC12312\\/1234567890\\/A100B100C100d100\"}]}}" -} \ No newline at end of file +} diff --git a/omnibot/__init__.py b/omnibot/__init__.py index 24a7414..5f3a65d 100644 --- a/omnibot/__init__.py +++ b/omnibot/__init__.py @@ -1,4 +1,4 @@ from os import getenv import importlib -logging = importlib.import_module(getenv('LOG_MODULE', 'logging')) +logging = importlib.import_module(getenv("LOG_MODULE", "logging")) diff --git a/omnibot/authnz/__init__.py b/omnibot/authnz/__init__.py index bf42acf..4ce5d61 100644 --- a/omnibot/authnz/__init__.py +++ b/omnibot/authnz/__init__.py @@ -14,10 +14,7 @@ import re from functools import wraps -from flask import ( - abort, - request -) +from flask import abort, request from omnibot import logging from omnibot import settings @@ -42,24 +39,26 @@ def enforce_checks(f): Checks will be executed in the order defined by the list. All checks must pass for a request to be accepted. """ + @wraps(f) def decorated(*args, **kwargs): - checks = settings.AUTHORIZATION.get('checks', []) + checks = settings.AUTHORIZATION.get("checks", []) if not checks: logger.warning( - 'No checks set in the authorization section of the configuration;' - ' denying access to API calls for sanity sake' + "No checks set in the authorization section of the configuration;" + " denying access to API calls for sanity sake" ) return abort(403) for check in checks: - module_name, function_name = check['module'].split(':') + module_name, function_name = check["module"].split(":") module = importlib.import_module(module_name) function = getattr(module, function_name) - func_kwargs = check.get('kwargs', {}) + func_kwargs = check.get("kwargs", {}) response = function(**func_kwargs) if not response: return abort(403) return f(*args, **kwargs) + return decorated diff --git a/omnibot/authnz/envoy_checks.py b/omnibot/authnz/envoy_checks.py index d45002f..e81fcb0 100644 --- a/omnibot/authnz/envoy_checks.py +++ b/omnibot/authnz/envoy_checks.py @@ -23,7 +23,7 @@ def _match_subject(subject_to_match, subject): return False -def envoy_internal_check(header='x-envoy-internal'): +def envoy_internal_check(header="x-envoy-internal"): """ Perform a check to ensure that the ``x-envoy-internal`` is set to 'true'. By default this check will apply to all routes, if enabled. It's possible @@ -45,44 +45,44 @@ def envoy_internal_check(header='x-envoy-internal'): """ # Flask provides all headers as strings. The only acceptable string here # is 'true' - envoy_internal = request.headers.get(header) == 'true' + envoy_internal = request.headers.get(header) == "true" # Easy case. The header says the request is internal. if envoy_internal: return True # If the request isn't internal, let's see if we have a permission that # matches, which has internal_only set to False - permissions = settings.AUTHORIZATION.get('permissions', {}) + permissions = settings.AUTHORIZATION.get("permissions", {}) for policy_name, policy in permissions.items(): - method_match = request.method in policy['methods'] - path_match = _match_path(request.path, policy['paths']) - internal_only = policy.get('internal_only', True) + method_match = request.method in policy["methods"] + path_match = _match_path(request.path, policy["paths"]) + internal_only = policy.get("internal_only", True) if (method_match and path_match) and not internal_only: return True logger.warning( - 'Received an external request to internal endpoint', + "Received an external request to internal endpoint", extra={ - 'endpoint': request.path, - 'method': request.method, - 'header_value': envoy_internal, + "endpoint": request.path, + "method": request.method, + "header_value": envoy_internal, }, ) return False def _check_permission(permission): - permissions = settings.AUTHORIZATION.get('permissions', {}) + permissions = settings.AUTHORIZATION.get("permissions", {}) policy = permissions.get(permission, {}) # TODO: envoy RBAC spec allows for matching methods and paths as # individual checks. So for instance, a permission may allow for all GETs # without a particular path, or may allow all methods on particular paths. - method_match = request.method in policy.get('methods', []) - path_match = _match_path(request.path, policy.get('paths', [])) + method_match = request.method in policy.get("methods", []) + path_match = _match_path(request.path, policy.get("paths", [])) if method_match and path_match: return True return False -def envoy_permissions_check(header='x-envoy-downstream-service-cluster'): +def envoy_permissions_check(header="x-envoy-downstream-service-cluster"): """ Perform a check against the defined permissions and bindings in the authorization configuration to ensure the service defined in the @@ -122,17 +122,17 @@ def envoy_permissions_check(header='x-envoy-downstream-service-cluster'): envoy_identity = request.headers.get(header) if envoy_identity is None: return False - bindings = settings.AUTHORIZATION.get('bindings', {}) + bindings = settings.AUTHORIZATION.get("bindings", {}) for subject, permissions in bindings.items(): if _match_subject(envoy_identity, subject): for permission in permissions: if _check_permission(permission): return True logger.warning( - 'Received an unauthorized request', + "Received an unauthorized request", extra={ - 'from': envoy_identity, - 'endpoint': request.path, + "from": envoy_identity, + "endpoint": request.path, }, ) return False diff --git a/omnibot/callbacks/interactive_component_callbacks.py b/omnibot/callbacks/interactive_component_callbacks.py index 08ab222..eeb3358 100644 --- a/omnibot/callbacks/interactive_component_callbacks.py +++ b/omnibot/callbacks/interactive_component_callbacks.py @@ -14,17 +14,13 @@ def echo_dialog_submission_callback(container): Just repond back with whatever is sent in. """ payload = container.payload - logger.debug('echo callback payload: {}'.format( - json.dumps(payload, indent=2)) - ) + logger.debug("echo callback payload: {}".format(json.dumps(payload, indent=2))) return { # Respond back to the slash command with the same text - 'responses': [ + "responses": [ { - 'text': payload['submission']['echo_element'], - 'omnibot_parse': { - 'text': ['users', 'specials', 'channels'] - } + "text": payload["submission"]["echo_element"], + "omnibot_parse": {"text": ["users", "specials", "channels"]}, } ] } diff --git a/omnibot/callbacks/message_callbacks.py b/omnibot/callbacks/message_callbacks.py index 403ad5e..b8516fb 100644 --- a/omnibot/callbacks/message_callbacks.py +++ b/omnibot/callbacks/message_callbacks.py @@ -17,42 +17,39 @@ def help_callback(container): Callback for omnibot help info. """ payload = container.payload - logger.debug('Help callback text: {}'.format(payload['text'])) - logger.debug('Help callback payload: {}'.format( - json.dumps(payload, indent=2)) - ) - ret_action = { - 'action': 'chat.postMessage', - 'kwargs': {'attachments': []} - } - ret = {'actions': [ret_action]} + logger.debug("Help callback text: {}".format(payload["text"])) + logger.debug("Help callback payload: {}".format(json.dumps(payload, indent=2))) + ret_action = {"action": "chat.postMessage", "kwargs": {"attachments": []}} + ret = {"actions": [ret_action]} command_fields = [] regex_fields = [] - team = Team.get_team_by_name(payload['team']['name']) - bot = Bot.get_bot_by_name(team, payload['bot']['name']) + team = Team.get_team_by_name(payload["team"]["name"]) + bot = Bot.get_bot_by_name(team, payload["bot"]["name"]) for handler in bot.message_handlers: - if handler['match_type'] == 'command': - command_fields.append({ - 'title': handler['match'], - 'value': handler.get('description', ''), - 'short': False - }) - if handler['match_type'] == 'regex': - regex_fields.append({ - 'title': handler['match'], - 'value': handler.get('description', ''), - 'short': False - }) + if handler["match_type"] == "command": + command_fields.append( + { + "title": handler["match"], + "value": handler.get("description", ""), + "short": False, + } + ) + if handler["match_type"] == "regex": + regex_fields.append( + { + "title": handler["match"], + "value": handler.get("description", ""), + "short": False, + } + ) if command_fields: - ret_action['kwargs']['attachments'].append({ - 'title': 'Commands:', - 'fields': command_fields - }) + ret_action["kwargs"]["attachments"].append( + {"title": "Commands:", "fields": command_fields} + ) if regex_fields: - ret_action['kwargs']['attachments'].append({ - 'title': 'Regex matches:', - 'fields': regex_fields - }) + ret_action["kwargs"]["attachments"].append( + {"title": "Regex matches:", "fields": regex_fields} + ) return ret @@ -62,99 +59,92 @@ def specials_callback(container, channels): and asks the sender to not use the specials. """ payload = container.payload - logger.debug('Specials callback text: {}'.format(payload['text'])) - logger.debug('Specials callback payload: {}'.format( - json.dumps(payload, indent=2)) - ) + logger.debug("Specials callback text: {}".format(payload["text"])) + logger.debug("Specials callback payload: {}".format(json.dumps(payload, indent=2))) fallback_text = "Please don't use {special}" - config_for_channel = channels.get(payload['channel']["name_normalized"]) + config_for_channel = channels.get(payload["channel"]["name_normalized"]) if not config_for_channel: return {} - for special in ['@here', '@channel']: - if special in payload['specials'].values(): - text = config_for_channel.get('message', fallback_text) + for special in ["@here", "@channel"]: + if special in payload["specials"].values(): + text = config_for_channel.get("message", fallback_text) try: text = text.format( special=special, - member_count=payload['channel'].get('num_members', '`?`') + member_count=payload["channel"].get("num_members", "`?`"), ) except KeyError: logger.error( "Misconfigured message string for {}".format( - payload['channel']["name_normalized"] + payload["channel"]["name_normalized"] ), extra=container.event_trace, ) text = fallback_text.format(special=special) - actions = [ - { - 'action': 'chat.postMessage', - 'kwargs': {'text': text} - } - ] - - if config_for_channel.get('reaction'): - actions.append({ - 'action': 'reactions.add', - 'kwargs': { - 'name': config_for_channel.get('reaction'), - 'timestamp': payload['ts'], - 'channel': payload['channel_id'] + actions = [{"action": "chat.postMessage", "kwargs": {"text": text}}] + + if config_for_channel.get("reaction"): + actions.append( + { + "action": "reactions.add", + "kwargs": { + "name": config_for_channel.get("reaction"), + "timestamp": payload["ts"], + "channel": payload["channel_id"], + }, } - }) + ) - return {'actions': actions} + return {"actions": actions} return {} def channel_channel_callback(container): payload = container.payload - logger.debug('Channel channel callback text: {}'.format(payload['text'])) + logger.debug("Channel channel callback text: {}".format(payload["text"])) logger.debug( - 'Channel channel callback payload: {}'.format( - json.dumps(payload, indent=2) - ) + "Channel channel callback payload: {}".format(json.dumps(payload, indent=2)) ) - if payload['channel'].get('name_normalized') != 'channel-channel': + if payload["channel"].get("name_normalized") != "channel-channel": return {} return { - 'actions': [ + "actions": [ { - 'action': 'reactions.add', - 'kwargs': { - 'name': 'please_make_sure_to_use_at_here_or_at_channel_in_your_message_next_time', # noqa: E501 - 'timestamp': payload['ts'], - 'channel': payload['channel_id'] - } + "action": "reactions.add", + "kwargs": { + "name": "please_make_sure_to_use_at_here_or_at_channel_in_your_message_next_time", # noqa: E501 + "timestamp": payload["ts"], + "channel": payload["channel_id"], + }, }, { - 'action': 'reactions.add', - 'kwargs': { - 'name': 'mega', - 'timestamp': payload['ts'], - 'channel': payload['channel_id'] - } + "action": "reactions.add", + "kwargs": { + "name": "mega", + "timestamp": payload["ts"], + "channel": payload["channel_id"], + }, }, { - 'action': 'reactions.add', - 'kwargs': { - 'name': 'bangbang', - 'timestamp': payload['ts'], - 'channel': payload['channel_id'] - } + "action": "reactions.add", + "kwargs": { + "name": "bangbang", + "timestamp": payload["ts"], + "channel": payload["channel_id"], + }, }, { - 'action': 'chat.postMessage', - 'kwargs': { - 'thread_ts': None, - 'text': 'Please make sure to use or in your message next time.', # noqa: E501 + "action": "chat.postMessage", + "kwargs": { + "thread_ts": None, + "text": "Please make sure to use or in your message next time.", # noqa: E501 }, }, ], @@ -163,31 +153,31 @@ def channel_channel_callback(container): def congratulations_bot_callback(container, channels, emojis, messages): payload = container.payload - logger.debug('Congratulations bot\'s callback text: {}'.format(payload['text'])) + logger.debug("Congratulations bot's callback text: {}".format(payload["text"])) logger.debug( - 'Congratulations bot\'s callback payload: {}'.format( + "Congratulations bot's callback payload: {}".format( json.dumps(payload, indent=2) ) ) - if payload['channel']['name'] not in channels: + if payload["channel"]["name"] not in channels: return {} return { - 'actions': [ + "actions": [ { - 'action': 'reactions.add', - 'kwargs': { - 'name': random.choice(emojis), - 'timestamp': payload['ts'], - 'channel': payload['channel_id'] - } + "action": "reactions.add", + "kwargs": { + "name": random.choice(emojis), + "timestamp": payload["ts"], + "channel": payload["channel_id"], + }, }, { - 'action': 'chat.postMessage', - 'kwargs': { - 'thread_ts': payload['ts'], - 'text': random.choice(messages), + "action": "chat.postMessage", + "kwargs": { + "thread_ts": payload["ts"], + "text": random.choice(messages), }, }, ], @@ -199,25 +189,23 @@ def channel_response_callback(container, channels): A callback to give back a canned response for regex matches in channels """ payload = container.payload - logger.debug('channel response callback text: {}'.format(payload['text'])) - logger.debug('channel response payload: {}'.format( - json.dumps(payload, indent=2)) - ) + logger.debug("channel response callback text: {}".format(payload["text"])) + logger.debug("channel response payload: {}".format(json.dumps(payload, indent=2))) - if not payload['channel'].get('name_normalized'): + if not payload["channel"].get("name_normalized"): return {} - config_for_channel = channels.get(payload['channel']['name_normalized']) + config_for_channel = channels.get(payload["channel"]["name_normalized"]) if not config_for_channel: return {} try: - find_list = config_for_channel['find'] + find_list = config_for_channel["find"] except KeyError: logger.error( - 'Missing find in channel_response_callback for {}'.format( - payload['channel']['name_normalized'] + "Missing find in channel_response_callback for {}".format( + payload["channel"]["name_normalized"] ), extra=container.event_trace, ) @@ -226,34 +214,29 @@ def channel_response_callback(container, channels): matched = False # Match entire words if this is a single word, to be less greedy for find in find_list: - if ' ' not in find: - matched = find in payload['parsed_text'].split() + if " " not in find: + matched = find in payload["parsed_text"].split() else: - matched = find in payload['parsed_text'] + matched = find in payload["parsed_text"] if matched: break if not matched: return {} try: - text = config_for_channel['message'] + text = config_for_channel["message"] except KeyError: logger.error( - 'Missing message in channel_response_callback for {}'.format( - payload['channel']['name_normalized'] + "Missing message in channel_response_callback for {}".format( + payload["channel"]["name_normalized"] ), extra=container.event_trace, ) return {} - actions = [ - { - 'action': 'chat.postMessage', - 'kwargs': {'text': text} - } - ] + actions = [{"action": "chat.postMessage", "kwargs": {"text": text}}] - return {'actions': actions} + return {"actions": actions} def example_topic_callback(container): @@ -261,19 +244,15 @@ def example_topic_callback(container): A callback for setting the topic """ payload = container.payload - logger.debug('Example topic callback text: {}'.format(payload['text'])) - logger.debug('Example topic callback payload: {}'.format( - json.dumps(payload, indent=2)) + logger.debug("Example topic callback text: {}".format(payload["text"])) + logger.debug( + "Example topic callback payload: {}".format(json.dumps(payload, indent=2)) ) return { - 'actions': - [ + "actions": [ { - 'action': 'channels.setTopic', - 'kwargs': { - 'topic': payload['args'], - 'channel': payload['channel_id'] - } + "action": "channels.setTopic", + "kwargs": {"topic": payload["args"], "channel": payload["channel_id"]}, } ] } @@ -284,43 +263,35 @@ def example_attachment_callback(container): A callback for responding with an attachment """ payload = container.payload - logger.debug('Example attachment callback text: {}'.format(payload['text'])) - logger.debug('Example attachment callback payload: {}'.format( - json.dumps(payload, indent=2)) + logger.debug("Example attachment callback text: {}".format(payload["text"])) + logger.debug( + "Example attachment callback payload: {}".format(json.dumps(payload, indent=2)) ) return { - 'actions': - [ + "actions": [ { - 'action': 'chat.postMessage', - 'kwargs': { - 'thread': False, - 'attachments': [{ - 'fallback': 'Example attachment!', - 'color': '#36a64f', - 'fields': [{ - 'title': 'Hello', - 'value': 'World' - }] - }] - } + "action": "chat.postMessage", + "kwargs": { + "thread": False, + "attachments": [ + { + "fallback": "Example attachment!", + "color": "#36a64f", + "fields": [{"title": "Hello", "value": "World"}], + } + ], + }, } ] } -def test_callback(container, text=''): +def test_callback(container, text=""): """ A callback used for basic testing. """ return { - 'actions': - [ - { - 'action': 'chat.postMessage', - 'kwargs': { - 'text': '{}'.format(text) - } - } + "actions": [ + {"action": "chat.postMessage", "kwargs": {"text": "{}".format(text)}} ] } diff --git a/omnibot/callbacks/network_callbacks.py b/omnibot/callbacks/network_callbacks.py index 2326aec..44f566c 100644 --- a/omnibot/callbacks/network_callbacks.py +++ b/omnibot/callbacks/network_callbacks.py @@ -20,11 +20,11 @@ def _get_requests_session(): read=5, connect=5, backoff_factor=1, - status_forcelist=(429, 500, 502, 503, 504) + status_forcelist=(429, 500, 502, 503, 504), ) adapter = HTTPAdapter(max_retries=retry) - SESSION.mount('http://', adapter) - SESSION.mount('https://', adapter) + SESSION.mount("http://", adapter) + SESSION.mount("https://", adapter) return SESSION @@ -34,42 +34,31 @@ def http_callback(container, request_kwargs=None, client_kwargs=None): client_kwargs = {} if request_kwargs is None: logger.error( - 'http_callback called without request_kwargs', + "http_callback called without request_kwargs", extra=container.event_trace, ) return {} client = _get_requests_session() - url = request_kwargs['url'] + url = request_kwargs["url"] # request_kwargs is a reference to the global settings, # so changing it changes the global settings, which we don't # want to do. Instead of changing request_kwargs, we make a # copy of it, and change the copy by removing the 'url' field. - kwargs = {k: v for k, v in request_kwargs.items() if k != 'url'} + kwargs = {k: v for k, v in request_kwargs.items() if k != "url"} try: - response = client.post( - url, - json=container.payload, - **kwargs - ) + response = client.post(url, json=container.payload, **kwargs) except RequestException as e: logger.error( - 'Failed to make request to {} with error: {}'.format( - url, - str(e) - ), + "Failed to make request to {} with error: {}".format(url, str(e)), extra=container.event_trace, ) return {} if response.status_code != requests.codes.ok: - msg = 'Got status code {0} for {1}, with response: {2}' + msg = "Got status code {0} for {1}, with response: {2}" logger.error( - msg.format( - response.status_code, - url, - response.text - ), - extra=container.event_trace + msg.format(response.status_code, url, response.text), + extra=container.event_trace, ) return response.json() diff --git a/omnibot/callbacks/slash_command_callbacks.py b/omnibot/callbacks/slash_command_callbacks.py index 93c67ed..1fce034 100644 --- a/omnibot/callbacks/slash_command_callbacks.py +++ b/omnibot/callbacks/slash_command_callbacks.py @@ -15,33 +15,29 @@ def echo_callback(container): Just respond back with whatever is sent in. """ payload = container.payload - logger.debug('echo callback payload: {}'.format( - json.dumps(payload, indent=2)) - ) + logger.debug("echo callback payload: {}".format(json.dumps(payload, indent=2))) return { # Respond back to the slash command with the same text - 'responses': [ + "responses": [ { - 'text': payload['parsed_text'], - 'omnibot_parse': { - 'text': ['users', 'specials', 'channels'] - } + "text": payload["parsed_text"], + "omnibot_parse": {"text": ["users", "specials", "channels"]}, } ], # Post into the #echo channel, letting everyone @here know what's up - 'actions': [ + "actions": [ { - 'action': 'chat.postMessage', - 'kwargs': { - 'channel': 'echo', - 'text': '@here {}'.format(payload['parsed_text']), - 'omnibot_parse': { - 'channel': ['channels'], - 'text': ['users', 'specials', 'channels'] - } - } + "action": "chat.postMessage", + "kwargs": { + "channel": "echo", + "text": "@here {}".format(payload["parsed_text"]), + "omnibot_parse": { + "channel": ["channels"], + "text": ["users", "specials", "channels"], + }, + }, } - ] + ], } @@ -50,17 +46,10 @@ def tableflip_callback(container): Respond back with a tableflip """ payload = container.payload - logger.debug('tableflip callback payload: {}'.format( - json.dumps(payload, indent=2)) - ) + logger.debug("tableflip callback payload: {}".format(json.dumps(payload, indent=2))) return { # Respond back to the slash command with the same text - 'responses': [ - { - 'response_type': 'in_channel', - 'text': '(╯°□°)╯︵ ┻━┻' - } - ] + "responses": [{"response_type": "in_channel", "text": "(╯°□°)╯︵ ┻━┻"}] } @@ -69,54 +58,47 @@ def unfliptable_callback(container): Respond back with an unfliptable """ payload = container.payload - logger.debug('unfliptable callback payload: {}'.format( - json.dumps(payload, indent=2)) + logger.debug( + "unfliptable callback payload: {}".format(json.dumps(payload, indent=2)) ) return { # Respond back to the slash command with the same text - 'responses': [ - { - 'response_type': 'in_channel', - 'text': '┬─┬ノ( º _ ºノ)' - } - ] + "responses": [{"response_type": "in_channel", "text": "┬─┬ノ( º _ ºノ)"}] } def bigemoji_callback(container): payload = container.payload - logger.debug('bigemoji callback payload: {}'.format( - json.dumps(payload, indent=2)) - ) + logger.debug("bigemoji callback payload: {}".format(json.dumps(payload, indent=2))) - s = container.payload['text'].strip() + s = container.payload["text"].strip() args = s.split() if len(args) != 1: return { - 'responses': [ + "responses": [ { - 'response_type': 'ephemeral', - 'text': 'usage: /bigemoji :emoji:', + "response_type": "ephemeral", + "text": "usage: /bigemoji :emoji:", } ] } else: - emoji_name, = args - emoji_name = emoji_name.strip(':') - result = get_emoji(container.bot, emoji_name) or f':{emoji_name}:' - resp = '<@{}> `/bigemoji :{}:` => {}'.format( - container.payload["user_id"], emoji_name, result, + (emoji_name,) = args + emoji_name = emoji_name.strip(":") + result = get_emoji(container.bot, emoji_name) or f":{emoji_name}:" + resp = "<@{}> `/bigemoji :{}:` => {}".format( + container.payload["user_id"], + emoji_name, + result, ) return { - 'responses': [ + "responses": [ { - 'response_type': 'in_channel', - 'text': resp, - 'omnibot_parse': { - 'text': ['users', 'specials', 'channels'] - } + "response_type": "in_channel", + "text": resp, + "omnibot_parse": {"text": ["users", "specials", "channels"]}, } ] } diff --git a/omnibot/processor.py b/omnibot/processor.py index 4209b04..9a76275 100644 --- a/omnibot/processor.py +++ b/omnibot/processor.py @@ -27,26 +27,24 @@ def process_event(event): Dispatcher for slack api events. """ statsd = stats.get_statsd_client() - team = Team.get_team_by_id(event['team_id']) - bot = Bot.get_bot_by_bot_id(team, event['api_app_id']) - event_info = event['event'] - event_type = event_info['type'] + team = Team.get_team_by_id(event["team_id"]) + bot = Bot.get_bot_by_bot_id(team, event["api_app_id"]) + event_info = event["event"] + event_type = event_info["type"] event_trace = merge_logging_context( { - 'event_ts': event_info['event_ts'], - 'event_type': event_type, + "event_ts": event_info["event_ts"], + "event_type": event_type, }, bot.logging_context, ) - statsd.incr('event.process.attempt.{}'.format(event_type)) - if event_type == 'message' or event_type == 'app_mention': + statsd.incr("event.process.attempt.{}".format(event_type)) + if event_type == "message" or event_type == "app_mention": try: - with statsd.timer('process_event'): + with statsd.timer("process_event"): logger.debug( - 'Processing message: {}'.format( - json.dumps(event, indent=2) - ), - extra=event_trace + "Processing message: {}".format(json.dumps(event, indent=2)), + extra=event_trace, ) try: message = Message(bot, event_info, event_trace) @@ -54,17 +52,12 @@ def process_event(event): except MessageUnsupportedError: pass except Exception: - statsd.incr('event.process.failed.{}'.format(event_type)) + statsd.incr("event.process.failed.{}".format(event_type)) logger.exception( - 'Could not process message.', - exc_info=True, - extra=event_trace + "Could not process message.", exc_info=True, extra=event_trace ) else: - logger.debug( - 'Event is not a message type.', - extra=event_trace - ) + logger.debug("Event is not a message type.", extra=event_trace) logger.debug(event) @@ -75,31 +68,31 @@ def _process_message_handlers(message): handler_called = False for handler in bot.message_handlers: # We only match commands against directed messages - if handler['match_type'] == 'command': + if handler["match_type"] == "command": if not _should_handle_command(handler, message): continue # We only match against a single command if command_matched: continue - if message.command_text.startswith(handler['match']): + if message.command_text.startswith(handler["match"]): command_matched = True - message.set_match('command', handler['match']) - for callback in handler['callbacks']: + message.set_match("command", handler["match"]) + for callback in handler["callbacks"]: _handle_message_callback(message, callback) handler_called = True - if handler['match_type'] == 'regex': - match = bool(re.search(handler['match'], message.parsed_text)) - regex_should_not_match = handler.get('regex_type') == 'absence' + if handler["match_type"] == "regex": + match = bool(re.search(handler["match"], message.parsed_text)) + regex_should_not_match = handler.get("regex_type") == "absence" # A matched regex should callback only if the regex is supposed to # match. An unmatched regex should callback only if the regex is # not supposed to match. if match != regex_should_not_match: - message.set_match('regex', handler['match']) - for callback in handler['callbacks']: + message.set_match("regex", handler["match"]) + for callback in handler["callbacks"]: _handle_message_callback(message, callback) handler_called = True if handler_called: - statsd.incr('event.handled') + statsd.incr("event.handled") elif not handler_called: _handle_help(message) @@ -109,36 +102,32 @@ def process_slash_command(command): Dispatcher for slack slash commands. """ statsd = stats.get_statsd_client() - team = Team.get_team_by_id(command['team_id']) - bot = Bot.get_bot_by_bot_id(team, command['omnibot_bot_id']) - if command['command'].startswith('/'): - command_name = command['command'][1:] + team = Team.get_team_by_id(command["team_id"]) + bot = Bot.get_bot_by_bot_id(team, command["omnibot_bot_id"]) + if command["command"].startswith("/"): + command_name = command["command"][1:] else: - command_name = command['command'] + command_name = command["command"] event_trace = merge_logging_context( { - 'trigger_id': command['trigger_id'], - 'command': command_name, + "trigger_id": command["trigger_id"], + "command": command_name, }, bot.logging_context, ) - statsd.incr('slash_command.process.attempt.{}'.format(command_name)) + statsd.incr("slash_command.process.attempt.{}".format(command_name)) try: - with statsd.timer('process_slash_command'): + with statsd.timer("process_slash_command"): logger.debug( - 'Processing slash_command: {}'.format( - json.dumps(command, indent=2) - ), - extra=event_trace + "Processing slash_command: {}".format(json.dumps(command, indent=2)), + extra=event_trace, ) slash_command = SlashCommand(bot, command, event_trace) _process_slash_command_handlers(slash_command) except Exception: - statsd.incr('slash_command.process.failed.{}'.format(command_name)) + statsd.incr("slash_command.process.failed.{}".format(command_name)) logger.exception( - 'Could not process slash command.', - exc_info=True, - extra=event_trace + "Could not process slash command.", exc_info=True, extra=event_trace ) @@ -147,57 +136,45 @@ def process_interactive_component(component): Dispatcher for slack interactive components """ statsd = stats.get_statsd_client() - team = Team.get_team_by_id(component['team']['id']) - bot = Bot.get_bot_by_bot_id(team, component['omnibot_bot_id']) + team = Team.get_team_by_id(component["team"]["id"]) + bot = Bot.get_bot_by_bot_id(team, component["omnibot_bot_id"]) event_trace = merge_logging_context( { - 'callback_id': get_callback_id(component), - 'component_type': component['type'], + "callback_id": get_callback_id(component), + "component_type": component["type"], }, bot.logging_context, ) statsd.incr( - 'interactive_component.process.attempt.{}'.format( - get_callback_id(component) - ) + "interactive_component.process.attempt.{}".format(get_callback_id(component)) ) try: - with statsd.timer('process_interactive_component'): + with statsd.timer("process_interactive_component"): logger.debug( - 'Processing interactive component: {}'.format( + "Processing interactive component: {}".format( json.dumps(component, indent=2) ), - extra=event_trace - ) - interactive_component = InteractiveComponent( - bot, - component, - event_trace + extra=event_trace, ) + interactive_component = InteractiveComponent(bot, component, event_trace) _process_interactive_component(interactive_component) except Exception: statsd.incr( - 'interactive_component.process.failed.{}'.format( - get_callback_id(component) - ) + "interactive_component.process.failed.{}".format(get_callback_id(component)) ) logger.exception( - 'Could not process interactive component.', - exc_info=True, - extra=event_trace + "Could not process interactive component.", exc_info=True, extra=event_trace ) def _process_slash_command_handlers(command): handler_called = False for handler in command.bot.slash_command_handlers: - if command.command != handler.get('command'): + if command.command != handler.get("command"): continue - for callback in handler['callbacks']: + for callback in handler["callbacks"]: _handle_slash_command_callback( - command, - callback, - handler.get('response_type', 'ephemeral') + command, callback, handler.get("response_type", "ephemeral") ) handler_called = True if not handler_called: @@ -208,13 +185,11 @@ def _process_slash_command_handlers(command): def _process_interactive_component(component): handler_called = False for handler in component.bot.interactive_component_handlers: - if component.callback_id != handler.get('callback_id'): + if component.callback_id != handler.get("callback_id"): continue - for callback in handler.get('callbacks', []): + for callback in handler.get("callbacks", []): _handle_interactive_component_callback( - component, - callback, - handler.get('response_type', 'ephemeral') + component, callback, handler.get("response_type", "ephemeral") ) handler_called = True if not handler_called: @@ -225,30 +200,22 @@ def _process_interactive_component(component): def _handle_help(message): statsd = stats.get_statsd_client() if message.directed: - statsd.incr('event.defaulted') + statsd.incr("event.defaulted") if settings.HELP_CALLBACK: - _handle_message_callback( - message, - settings.HELP_CALLBACK['callback'] - ) + _handle_message_callback(message, settings.HELP_CALLBACK["callback"]) elif settings.DEFAULT_TO_HELP: _handle_message_callback( - message, - { - 'module': 'omnibot.callbacks.message_callbacks:help_callback' - } + message, {"module": "omnibot.callbacks.message_callbacks:help_callback"} ) else: # TODO: respond with error message here pass else: - statsd.incr('event.ignored') + statsd.incr("event.ignored") def _should_handle_command(handler, message): - handle_mention = ( - handler.get('match_mention', False) and message.mentioned - ) + handle_mention = handler.get("match_mention", False) and message.mentioned if message.directed or handle_mention: return True else: @@ -259,59 +226,49 @@ def parse_kwargs(kwargs, bot, event_trace=None): if event_trace is None: event_trace = {} statsd = stats.get_statsd_client() - omnibot_parse = kwargs.pop('omnibot_parse', {}) + omnibot_parse = kwargs.pop("omnibot_parse", {}) for attr, to_parse in omnibot_parse.items(): if attr not in kwargs: logger.warning( - '{} not found in kwargs when parsing post response.'.format(attr), - extra=event_trace + "{} not found in kwargs when parsing post response.".format(attr), + extra=event_trace, ) - with statsd.timer('unexpand_metadata'): - if 'specials' in to_parse: + with statsd.timer("unexpand_metadata"): + if "specials" in to_parse: kwargs[attr] = parser.unextract_specials(kwargs[attr]) - if 'channels' in to_parse: - kwargs[attr] = parser.unextract_channels( - kwargs[attr], - bot - ) - if 'users' in to_parse: - kwargs[attr] = parser.unextract_users( - kwargs[attr], - bot - ) + if "channels" in to_parse: + kwargs[attr] = parser.unextract_channels(kwargs[attr], bot) + if "users" in to_parse: + kwargs[attr] = parser.unextract_users(kwargs[attr], bot) def _handle_post_message(message, kwargs): try: - channel = kwargs.pop('channel') + channel = kwargs.pop("channel") except KeyError: channel = message.channel_id try: - thread_ts = kwargs.pop('thread_ts') + thread_ts = kwargs.pop("thread_ts") except KeyError: - if message.channel.get('is_im'): + if message.channel.get("is_im"): thread_ts = None else: thread_ts = message.ts if thread_ts: - kwargs['thread_ts'] = thread_ts + kwargs["thread_ts"] = thread_ts parse_kwargs(kwargs, message.bot, message.event_trace) try: ret = slack.client( message.bot, - ).api_call( - 'chat.postMessage', - channel=channel, - **kwargs - ) + ).api_call("chat.postMessage", channel=channel, **kwargs) except json.decoder.JSONDecodeError: logger.exception( - 'JSON decode failure when parsing {}'.format(kwargs), + "JSON decode failure when parsing {}".format(kwargs), extra=message.event_trace, ) return logger.debug(ret, extra=message.event_trace) - if not ret['ok']: + if not ret["ok"]: logger.error(ret, extra=message.event_trace) @@ -319,47 +276,40 @@ def _handle_action(action, container, kwargs): parse_kwargs(kwargs, container.bot, container.event_trace) ret = slack.client( container.bot, - ).api_call( - action, - **kwargs - ) + ).api_call(action, **kwargs) logger.debug( - 'return from action {}: {}'.format(action, ret), + "return from action {}: {}".format(action, ret), extra=container.event_trace, ) - if not ret['ok']: - if ret.get('error') == 'missing_scope': + if not ret["ok"]: + if ret.get("error") == "missing_scope": logger.warning( - 'action {} failed, attempting as user.'.format(action), - extra=container.event_trace + "action {} failed, attempting as user.".format(action), + extra=container.event_trace, ) try: - ret = slack.client( - container.bot, - client_type='user' - ).api_call( - action, - **kwargs + ret = slack.client(container.bot, client_type="user").api_call( + action, **kwargs ) except json.decoder.JSONDecodeError: logger.exception( - 'JSON decode failure when parsing {}'.format(kwargs), + "JSON decode failure when parsing {}".format(kwargs), extra=container.event_trace, ) return logger.debug( - 'return from action {}: {}'.format(action, ret), + "return from action {}: {}".format(action, ret), extra=container.event_trace, ) - if not ret['ok']: + if not ret["ok"]: logger.debug( - 'return from failed action {}: {}'.format(action, ret), + "return from failed action {}: {}".format(action, ret), extra=container.event_trace, ) else: - if not ret['ok']: + if not ret["ok"]: logger.debug( - 'return from failed action {}: {}'.format(action, ret), + "return from failed action {}: {}".format(action, ret), extra=container.event_trace, ) @@ -367,148 +317,125 @@ def _handle_action(action, container, kwargs): def _handle_message_callback(message, callback): logger.info( 'Handling callback for message: match_type="{}" match="{}"'.format( - message.match_type, - message.match + message.match_type, message.match ), extra={ **message.event_trace, - 'module': callback['module'], - 'request_kwargs': callback.get('kwargs', {}).get('request_kwargs', {}), - 'client_kwargs': {'service': callback.get('kwargs', {}).get('client_kwargs', {}).get('service', "")}, + "module": callback["module"], + "request_kwargs": callback.get("kwargs", {}).get("request_kwargs", {}), + "client_kwargs": { + "service": callback.get("kwargs", {}) + .get("client_kwargs", {}) + .get("service", "") + }, }, ) response = _handle_callback(message, callback) - for action in response.get('actions', []): + for action in response.get("actions", []): if not isinstance(action, dict): - logger.error( - 'Action in response is not a dict.', - extra=message.event_trace - ) + logger.error("Action in response is not a dict.", extra=message.event_trace) continue logger.debug( - 'action for callback: {}'.format(action), + "action for callback: {}".format(action), extra=message.event_trace, ) - if action['action'] == 'chat.postMessage': - _handle_post_message(message, action['kwargs']) + if action["action"] == "chat.postMessage": + _handle_post_message(message, action["kwargs"]) else: - _handle_action(action['action'], message, action['kwargs']) + _handle_action(action["action"], message, action["kwargs"]) def _handle_slash_command_callback(command, callback, response_type): logger.info( - 'Handling callback for slash_command: command="{}"'.format( - command.command - ), - extra={**command.event_trace, 'callback': callback}, + 'Handling callback for slash_command: command="{}"'.format(command.command), + extra={**command.event_trace, "callback": callback}, ) response = _handle_callback(command, callback) - for command_response in response.get('responses', []): + for command_response in response.get("responses", []): logger.debug( - 'Handling response for callback (pre-parse): {}'.format( + "Handling response for callback (pre-parse): {}".format( json.dumps(command_response) ), extra=command.event_trace, ) - if 'response_type' not in command_response: - command_response['response_type'] = response_type + if "response_type" not in command_response: + command_response["response_type"] = response_type parse_kwargs(command_response, command.bot, command.event_trace) logger.debug( - 'Handling response for callback (post-parse): {}'.format( + "Handling response for callback (post-parse): {}".format( json.dumps(command_response) ), extra=command.event_trace, ) - r = requests.post( - command.response_url, - json=command_response - ) + r = requests.post(command.response_url, json=command_response) if r.status_code != requests.codes.ok: - msg = 'Got status code {0} for {1}, with response: {2}' + msg = "Got status code {0} for {1}, with response: {2}" logger.error( - msg.format( - r.status_code, - command.response_url, - r.text - ), - extra=command.event_trace + msg.format(r.status_code, command.response_url, r.text), + extra=command.event_trace, ) - for action in response.get('actions', []): + for action in response.get("actions", []): if not isinstance(action, dict): - logger.error( - 'Action in response is not a dict.', - extra=command.event_trace - ) + logger.error("Action in response is not a dict.", extra=command.event_trace) continue logger.debug( - 'Action in response: {}'.format(action), + "Action in response: {}".format(action), extra=command.event_trace, ) - _handle_action(action['action'], command, action['kwargs']) + _handle_action(action["action"], command, action["kwargs"]) def _handle_interactive_component_callback(component, callback, response_type): logger.info( - 'Handling callback for interactive component', - extra={**component.event_trace, 'callback': callback}, + "Handling callback for interactive component", + extra={**component.event_trace, "callback": callback}, ) response = _handle_callback(component, callback) - if response_type == 'raw': + if response_type == "raw": return response - for component_response in response.get('responses', []): + for component_response in response.get("responses", []): logger.debug( - 'Handling response for callback (pre-parse): {}'.format( + "Handling response for callback (pre-parse): {}".format( json.dumps(component_response) ), extra=component.event_trace, ) - if 'response_type' not in component_response: - component_response['response_type'] = response_type + if "response_type" not in component_response: + component_response["response_type"] = response_type parse_kwargs(component_response, component.bot, component.event_trace) logger.debug( - 'Handling response for callback (post-parse): {}'.format( + "Handling response for callback (post-parse): {}".format( json.dumps(component_response) ), extra=component.event_trace, ) - r = requests.post( - component.response_url, - json=component_response - ) + r = requests.post(component.response_url, json=component_response) if r.status_code != requests.codes.ok: - msg = 'Got status code {0} for {1}, with response: {2}' + msg = "Got status code {0} for {1}, with response: {2}" logger.error( - msg.format( - r.status_code, - component.response_url, - r.text - ), - extra=component.event_trace + msg.format(r.status_code, component.response_url, r.text), + extra=component.event_trace, ) - for action in response.get('actions', []): + for action in response.get("actions", []): if not isinstance(action, dict): logger.error( - 'Action in response is not a dict.', - extra=component.event_trace + "Action in response is not a dict.", extra=component.event_trace ) continue logger.debug( - 'Action in response: {}'.format(action), + "Action in response: {}".format(action), extra=component.event_trace, ) - if action['action'] == 'chat.postMessage': - _handle_post_message(component, action['kwargs']) + if action["action"] == "chat.postMessage": + _handle_post_message(component, action["kwargs"]) else: - _handle_action(action['action'], component, action['kwargs']) + _handle_action(action["action"], component, action["kwargs"]) def _handle_callback(container, callback): - module_name, function_name = callback['module'].split(':') + module_name, function_name = callback["module"].split(":") module = importlib.import_module(module_name) function = getattr(module, function_name) - kwargs = callback.get('kwargs', {}) - response = function( - container=container, - **kwargs - ) + kwargs = callback.get("kwargs", {}) + response = function(container=container, **kwargs) return response diff --git a/omnibot/routes/api.py b/omnibot/routes/api.py index 8d01e0b..b45a167 100644 --- a/omnibot/routes/api.py +++ b/omnibot/routes/api.py @@ -12,12 +12,7 @@ import time from functools import wraps -from flask import ( - Blueprint, - jsonify, - request, - abort -) +from flask import Blueprint, jsonify, request, abort from omnibot import authnz from omnibot import logging @@ -35,401 +30,396 @@ logger = logging.getLogger(__name__) -blueprint = Blueprint('api', __name__) +blueprint = Blueprint("api", __name__) def verify_bot(f): @wraps(f) def decorated(*args, **kwargs): - bot_name = request.view_args.get('bot_name') - team_name = request.view_args.get('team_name') + bot_name = request.view_args.get("bot_name") + team_name = request.view_args.get("team_name") try: team = Team.get_team_by_name(team_name) except TeamInitializationError: logger.warning( - 'Failed to validate bot', + "Failed to validate bot", extra={ - 'bot': bot_name, - 'team': team_name, - } + "bot": bot_name, + "team": team_name, + }, ) return abort(404) try: Bot.get_bot_by_name(team, bot_name) except BotInitializationError: logger.warning( - 'Failed to validate bot', + "Failed to validate bot", extra={ - 'bot': bot_name, - 'team': team_name, - } + "bot": bot_name, + "team": team_name, + }, ) return abort(404) return f(*args, **kwargs) + return decorated -@blueprint.route('/healthcheck') +@blueprint.route("/healthcheck") def healthcheck(): # The healthcheck returns status code 200 - return 'OK' + return "OK" -@blueprint.route('/api/v1/slack/event', methods=['POST']) +@blueprint.route("/api/v1/slack/event", methods=["POST"]) @authnz.enforce_checks def slack_event(): """ Handle event subscription API webhooks from slack. """ event = request.json - logger.debug('Event received in API slack_event: {}'.format(event)) + logger.debug("Event received in API slack_event: {}".format(event)) # Every event should have a validation token - if 'token' not in event: - msg = 'No verification token in event.' + if "token" not in event: + msg = "No verification token in event." logger.error(msg) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 # url_verification events don't contain info about team_id or api_app_id, # annoyingly, so we need to special case this to validate the token # against all configured apps. - if event.get('type') == 'url_verification': + if event.get("type") == "url_verification": try: - Bot.get_bot_by_verification_token(event['token']) + Bot.get_bot_by_verification_token(event["token"]) except BotInitializationError: - msg = 'url_verification failed.' + msg = "url_verification failed." logger.error(msg) - return jsonify({'status': 'failure', 'error': msg}), 403 - return jsonify({'challenge': event['challenge']}) - api_app_id = event.get('api_app_id') + return jsonify({"status": "failure", "error": msg}), 403 + return jsonify({"challenge": event["challenge"]}) + api_app_id = event.get("api_app_id") if api_app_id is None: - msg = 'No api_app_id in event.' + msg = "No api_app_id in event." logger.error(msg) - return jsonify({'status': 'failure', 'error': msg}), 403 - team_id = event.get('team_id') + return jsonify({"status": "failure", "error": msg}), 403 + team_id = event.get("team_id") if team_id is None: - msg = 'No team_id in event.' + msg = "No team_id in event." logger.error( msg, - extra={'bot_id': api_app_id}, + extra={"bot_id": api_app_id}, ) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 try: team = Team.get_team_by_id(team_id) except TeamInitializationError: - msg = 'Unsupported team' - logger.warning(msg, extra={'team_id': team_id, 'bot_id': api_app_id}) - return jsonify({'status': 'failure', 'error': msg}), 403 + msg = "Unsupported team" + logger.warning(msg, extra={"team_id": team_id, "bot_id": api_app_id}) + return jsonify({"status": "failure", "error": msg}), 403 try: bot = Bot.get_bot_by_bot_id(team, api_app_id) except BotInitializationError: - msg = 'Unsupported bot' - logger.info(msg, extra={'team_id': team_id, 'bot_id': api_app_id}) - return jsonify({'status': 'ignored', 'warning': msg}), 200 - if event['token'] != bot.verification_token: - msg = 'Incorrect verification token in event for bot' + msg = "Unsupported bot" + logger.info(msg, extra={"team_id": team_id, "bot_id": api_app_id}) + return jsonify({"status": "ignored", "warning": msg}), 200 + if event["token"] != bot.verification_token: + msg = "Incorrect verification token in event for bot" logger.error( msg, extra=bot.logging_context, ) - return jsonify({'status': 'failure', 'error': msg}), 403 - if 'event' not in event: - msg = 'Request does not have an event. Processing will not proceed!' + return jsonify({"status": "failure", "error": msg}), 403 + if "event" not in event: + msg = "Request does not have an event. Processing will not proceed!" logger.error( msg, extra=bot.logging_context, ) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 try: instrument_event(bot, event) except Exception: logger.exception( - 'Could not instrument request', + "Could not instrument request", extra=bot.logging_context, ) try: - queue_event(bot, event, 'event') + queue_event(bot, event, "event") except Exception: logger.exception( - 'Could not queue request.', + "Could not queue request.", extra=bot.logging_context, ) - return jsonify({'status': 'failure'}), 500 - return jsonify({'status': 'success'}), 200 + return jsonify({"status": "failure"}), 500 + return jsonify({"status": "success"}), 200 -@blueprint.route('/api/v1/slack/slash_command', methods=['POST']) +@blueprint.route("/api/v1/slack/slash_command", methods=["POST"]) @authnz.enforce_checks def slack_slash_command(): # Slack sends slash commands as application/x-www-form-urlencoded command = request.form.to_dict() - logger.debug( - 'command received in API slack_slash_command: {}'.format(command) - ) + logger.debug("command received in API slack_slash_command: {}".format(command)) # Every event should have a validation token - if 'token' not in command: - msg = 'No verification token in slash command.' + if "token" not in command: + msg = "No verification token in slash command." logger.error(msg) - return jsonify({'status': 'failure', 'error': msg}), 403 - if 'team_id' not in command: - msg = 'No team_id in slash command.' + return jsonify({"status": "failure", "error": msg}), 403 + if "team_id" not in command: + msg = "No team_id in slash command." logger.error(msg) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 try: - team = Team.get_team_by_id(command['team_id']) + team = Team.get_team_by_id(command["team_id"]) except TeamInitializationError: - msg = 'Unsupported team' + msg = "Unsupported team" logger.warning( msg, - extra={'team_id': command['team_id']}, + extra={"team_id": command["team_id"]}, ) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 # Slash commands annoyingly don't send an app id, so we need to verify try: - bot = Bot.get_bot_by_verification_token(command['token']) + bot = Bot.get_bot_by_verification_token(command["token"]) except BotInitializationError: - msg = ( - 'Token sent with slash command does not match any configured app.' - ) + msg = "Token sent with slash command does not match any configured app." logger.error( msg, extra=team.logging_context, ) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 if team.team_id != bot.team.team_id: # This should never happen, but let's be paranoid. - msg = ( - 'Token sent with slash command does not match team in event.' - ) + msg = "Token sent with slash command does not match team in event." logger.error( msg, extra=merge_logging_context( - {'expected_team_id': team.team_id}, + {"expected_team_id": team.team_id}, bot.logging_context, - ) + ), ) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 handler_found = None for slash_handler in bot.slash_command_handlers: - if command['command'] == slash_handler.get('command'): + if command["command"] == slash_handler.get("command"): handler_found = slash_handler break if not handler_found: - msg = ('This slash command does not have any omnibot handler' - ' associated with it.') + msg = ( + "This slash command does not have any omnibot handler" + " associated with it." + ) logger.error( msg, extra=bot.logging_context, ) - return jsonify({'response_type': 'ephemeral', 'text': msg}), 200 + return jsonify({"response_type": "ephemeral", "text": msg}), 200 # To avoid needing to look the bot up from its token when the dequeue this # command,:let's extend the payload with the bot id - command['omnibot_bot_id'] = bot.bot_id + command["omnibot_bot_id"] = bot.bot_id # We can't instrument slash commands, because they don't have ts info. # TODO: investigate if we can parse the trigger ID; it's possible part # of that is a timestamp try: # If there's no callbacks defined for this slash command, we # can skip enqueuing it, since the workers will just discard it. - if handler_found.get('callbacks'): - queue_event(bot, command, 'slash_command') + if handler_found.get("callbacks"): + queue_event(bot, command, "slash_command") except Exception: - msg = 'Could not queue slash command.' + msg = "Could not queue slash command." logger.exception( msg, - extra={'team': team.team_id, 'app': bot.bot_id, 'bot': bot.name}, + extra={"team": team.team_id, "app": bot.bot_id, "bot": bot.name}, + ) + return jsonify({"status": "failure", "error": msg}), 500 + if handler_found.get("dialog"): + _perform_action( + bot, + { + "action": "dialog.open", + "kwargs": { + "dialog": handler_found["dialog"], + "trigger_id": command["trigger_id"], + }, + }, ) - return jsonify({'status': 'failure', 'error': msg}), 500 - if handler_found.get('dialog'): - _perform_action(bot, { - 'action': 'dialog.open', - 'kwargs': { - 'dialog': handler_found['dialog'], - 'trigger_id': command['trigger_id'] - } - }) return _get_write_message_response(handler_found), 200 -@blueprint.route('/api/v1/slack/interactive', methods=['POST']) +@blueprint.route("/api/v1/slack/interactive", methods=["POST"]) @authnz.enforce_checks def slack_interactive_component(): # Slack sends interactive components as application/x-www-form-urlencoded, # json encoded inside of the payload field. What a whacky API. - component = json.loads(request.form.to_dict().get('payload', {})) - logger.debug( - 'component received in API slack_slash_command: {}'.format(component) - ) - if ( - component.get('type') not in [ - 'interactive_message', - 'message_action', - 'dialog_submission', - 'block_actions', - 'view_submission', - ] - ): - msg = ('Unsupported type={} in interactive' - ' component.'.format(component.get('type'))) + component = json.loads(request.form.to_dict().get("payload", {})) + logger.debug("component received in API slack_slash_command: {}".format(component)) + if component.get("type") not in [ + "interactive_message", + "message_action", + "dialog_submission", + "block_actions", + "view_submission", + ]: + msg = "Unsupported type={} in interactive" " component.".format( + component.get("type") + ) logger.warning(msg) - return jsonify({'status': 'failure', 'error': msg}), 400 + return jsonify({"status": "failure", "error": msg}), 400 # Every event should have a validation token - if 'token' not in component: - msg = 'No verification token in interactive component.' + if "token" not in component: + msg = "No verification token in interactive component." logger.warning(msg) - return jsonify({'status': 'failure', 'error': msg}), 403 - if not component.get('team', {}).get('id'): - msg = 'No team id in interactive component.' + return jsonify({"status": "failure", "error": msg}), 403 + if not component.get("team", {}).get("id"): + msg = "No team id in interactive component." logger.warning(msg) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 try: - team = Team.get_team_by_id(component['team']['id']) + team = Team.get_team_by_id(component["team"]["id"]) except TeamInitializationError: - msg = 'Unsupported team' + msg = "Unsupported team" logger.warning( msg, - extra={'team_id': component['team']['id']}, + extra={"team_id": component["team"]["id"]}, ) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 # interactive components annoyingly don't send an app id, so we need # to verify try: - bot = Bot.get_bot_by_verification_token(component['token']) + bot = Bot.get_bot_by_verification_token(component["token"]) except BotInitializationError: - msg = ('Token sent with interactive component does not match any' - ' configured app.') + msg = ( + "Token sent with interactive component does not match any" + " configured app." + ) logger.error( msg, extra=team.logging_context, ) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 if team.team_id != bot.team.team_id: # This should never happen, but let's be paranoid. - msg = ( - 'Token sent with slash command does not match team in event.' - ) + msg = "Token sent with slash command does not match team in event." logger.error( msg, extra=merge_logging_context( - {'expected_team_id': team.team_id}, + {"expected_team_id": team.team_id}, bot.logging_context, ), ) - return jsonify({'status': 'failure', 'error': msg}), 403 + return jsonify({"status": "failure", "error": msg}), 403 handler_found = None for handler in bot.interactive_component_handlers: - if get_callback_id(component) == handler.get('callback_id'): + if get_callback_id(component) == handler.get("callback_id"): handler_found = handler break if not handler_found: - msg = ('This interactive component does not have any omnibot handler' - ' associated with it.') + msg = ( + "This interactive component does not have any omnibot handler" + " associated with it." + ) logger.error( msg, extra=merge_logging_context( - {'callback_id': get_callback_id(component)}, + {"callback_id": get_callback_id(component)}, bot.logging_context, - ) + ), ) - return jsonify({'response_type': 'ephemeral', 'text': msg}), 200 + return jsonify({"response_type": "ephemeral", "text": msg}), 200 # To avoid needing to look the bot up from its token when the dequeue this # command,:let's extend the payload with the bot id - component['omnibot_bot_id'] = bot.bot_id + component["omnibot_bot_id"] = bot.bot_id # TODO: Use action_ts to instrument event try: # If there's no callbacks defined for this interactive component, we # can skip enqueuing it, since the workers will just discard it. - if handler_found.get('callbacks'): - callbacks = handler_found.get('callbacks') + if handler_found.get("callbacks"): + callbacks = handler_found.get("callbacks") for c in callbacks: - if c.get('synchronous'): + if c.get("synchronous"): interactive_component = InteractiveComponent(bot, component, {}) - resp = _handle_interactive_component_callback(interactive_component, c, 'raw') + resp = _handle_interactive_component_callback( + interactive_component, c, "raw" + ) logger.info( - 'Synchronous callback response', + "Synchronous callback response", extra=merge_logging_context( - {'response': resp}, + {"response": resp}, bot.logging_context, - ) + ), ) return resp, 200 - queue_event(bot, component, 'interactive_component') + queue_event(bot, component, "interactive_component") except Exception: - msg = 'Could not queue interactive component.' + msg = "Could not queue interactive component." logger.exception( msg, extra=bot.logging_context, ) - return jsonify({'status': 'failure', 'error': msg}), 500 + return jsonify({"status": "failure", "error": msg}), 500 # Open a dialog, if we have a trigger ID, and a dialog is defined for this # handler. Not all interactive components have a trigger ID. - if component.get('trigger_id') and handler_found.get('dialog'): - _perform_action(bot, { - 'action': 'dialog.open', - 'kwargs': { - 'dialog': handler_found['dialog'], - 'trigger_id': component['trigger_id'] - } - }) - if component['type'] in ['dialog_submission']: - return '', 200 - elif handler_found.get('no_message_response'): - return '', 200 + if component.get("trigger_id") and handler_found.get("dialog"): + _perform_action( + bot, + { + "action": "dialog.open", + "kwargs": { + "dialog": handler_found["dialog"], + "trigger_id": component["trigger_id"], + }, + }, + ) + if component["type"] in ["dialog_submission"]: + return "", 200 + elif handler_found.get("no_message_response"): + return "", 200 else: return _get_write_message_response(handler_found), 200 def _get_write_message_response(handler): - canned_response = handler.get('canned_response') - response_type = handler.get('response_type', 'ephemeral') + canned_response = handler.get("canned_response") + response_type = handler.get("response_type", "ephemeral") response = {} if canned_response: - response['text'] = canned_response - response['response_type'] = response_type + response["text"] = canned_response + response["response_type"] = response_type else: # If we aren't sending back text, we can only send back a response # type if it's in_channel, or the slash command will respond with an # error every time. - if response_type == 'in_channel': - response['response_type'] = response_type + if response_type == "in_channel": + response["response_type"] = response_type # We can only send back a json payload if we have items in it, or slack # sends errors in the slash command. if response: return jsonify(response) else: - return '' + return "" def instrument_event(bot, event): statsd = stats.get_statsd_client() - retry = request.headers.get( - 'X-Slack-Retry-Num', - default=0, - type=int - ) - retry_reason = request.headers.get( - 'X-Slack-Retry-Reason', - default='', - type=str - ) - event_info = event['event'] - event_sent_time_ms = int(float(event_info['event_ts']) * 1000) + retry = request.headers.get("X-Slack-Retry-Num", default=0, type=int) + retry_reason = request.headers.get("X-Slack-Retry-Reason", default="", type=str) + event_info = event["event"] + event_sent_time_ms = int(float(event_info["event_ts"]) * 1000) now = int(time.time() * 1000) latency = now - event_sent_time_ms if retry > 0: - statsd.timing('pre_sqs_delivery_retry_latency', latency) + statsd.timing("pre_sqs_delivery_retry_latency", latency) else: - statsd.timing('pre_sqs_delivery_latency', latency) + statsd.timing("pre_sqs_delivery_latency", latency) if latency > 20000: logger.warning( - 'Event is greater than 20s delayed in' - ' delivery ({} ms)'.format(latency), + "Event is greater than 20s delayed in" " delivery ({} ms)".format(latency), extra=merge_logging_context( { - 'event_ts': event_info['event_ts'], - 'event_type': event_info['type'], - 'retry': retry + "event_ts": event_info["event_ts"], + "event_type": event_info["type"], + "retry": retry, }, bot.logging_context, - ) + ), ) if retry_reason: logger.warning( @@ -443,34 +433,26 @@ def queue_event(bot, event, event_type): sqs_client = sqs.get_client() sqs_client.send_message( QueueUrl=sqs.get_queue_url(), - MessageBody=json.dumps({ - 'event': event - }), + MessageBody=json.dumps({"event": event}), MessageAttributes={ # Add a version, so we know how to parse this in the receiver when # we make message schema changes. - 'version': { - 'DataType': 'Number', + "version": { + "DataType": "Number", # Seems SQS uses StringValue for Number type... We'll cast # this on the receiver end. - 'StringValue': '2' + "StringValue": "2", }, # Specify the type of SQS message, so we can handle more than just # the event subscription API. - 'type': { - 'DataType': 'String', - 'StringValue': event_type - } - } + "type": {"DataType": "String", "StringValue": event_type}, + }, ) - statsd.incr('sqs.sent') - statsd.incr('sqs.{}.sent'.format(bot.name)) + statsd.incr("sqs.sent") + statsd.incr("sqs.{}.sent".format(bot.name)) -@blueprint.route( - '/api/v1/slack/get_team/', - methods=['GET'] -) +@blueprint.route("/api/v1/slack/get_team/", methods=["GET"]) @authnz.enforce_checks def get_team_id_by_name(team_name): """ @@ -504,19 +486,16 @@ def get_team_id_by_name(team_name): :statuscode 200: success :statuscode 404: team is not configured """ - logger.debug('Getting team id', extra={'team': team_name}) + logger.debug("Getting team id", extra={"team": team_name}) try: team = Team.get_team_by_name(team_name) - return jsonify({'team_id': team.team_id}) + return jsonify({"team_id": team.team_id}) except TeamInitializationError: - return jsonify( - {'error': 'provided team_name is not configured.'} - ), 404 + return jsonify({"error": "provided team_name is not configured."}), 404 @blueprint.route( - '/api/v1/slack/get_user///', - methods=['GET'] + "/api/v1/slack/get_user///", methods=["GET"] ) @authnz.enforce_checks @verify_bot @@ -560,40 +539,44 @@ def get_user_v2(team_name, bot_name, email): specified bot. """ logger.debug( - 'Getting user team={} bot={} email={}.', + "Getting user team={} bot={} email={}.", extra={ - 'team': team_name, - 'bot': bot_name, - 'email': email, - } + "team": team_name, + "bot": bot_name, + "email": email, + }, ) try: team = Team.get_team_by_name(team_name) except TeamInitializationError: - return jsonify({'error': 'provided team name was not found.'}), 404 + return jsonify({"error": "provided team name was not found."}), 404 try: bot = Bot.get_bot_by_name(team, bot_name) except BotInitializationError: - return jsonify({'error': 'provided bot name was not found.'}), 404 + return jsonify({"error": "provided bot name was not found."}), 404 user = slack.get_user_by_email(bot, email) if not user: - return jsonify( - {'error': 'user not found'}, - ), 404 + return ( + jsonify( + {"error": "user not found"}, + ), + 404, + ) name = slack.get_name_from_user(user) - return jsonify({ - 'user': { - 'email': email, - 'name': name, - 'team_id': team.team_id, - 'user_id': user['id'] + return jsonify( + { + "user": { + "email": email, + "name": name, + "team_id": team.team_id, + "user_id": user["id"], + } } - }) + ) @blueprint.route( - '/api/v1/slack/get_channel///', - methods=['GET'] + "/api/v1/slack/get_channel///", methods=["GET"] ) @authnz.enforce_checks @verify_bot @@ -679,114 +662,96 @@ def get_channel_by_name(team_name, bot_name, channel_name): in the specified team using the specified bot. """ logger.debug( - 'Getting channel for team={} bot={} channel={}.', + "Getting channel for team={} bot={} channel={}.", extra={ - 'team': team_name, - 'bot': bot_name, - 'channel': channel_name, + "team": team_name, + "bot": bot_name, + "channel": channel_name, }, ) try: team = Team.get_team_by_name(team_name) except TeamInitializationError: - return jsonify({'error': 'provided team name was not found.'}), 404 + return jsonify({"error": "provided team name was not found."}), 404 try: bot = Bot.get_bot_by_name(team, bot_name) except BotInitializationError: - return jsonify({'error': 'provided bot name was not found.'}), 404 + return jsonify({"error": "provided bot name was not found."}), 404 channel = slack.get_channel_by_name(bot, channel_name) if channel is None: logger.debug( - 'Failed to get channel', + "Failed to get channel", extra=merge_logging_context( - {'channel': channel_name}, + {"channel": channel_name}, bot.logging_context, ), ) - return jsonify({'error': 'provided channel_name was not found.'}), 404 + return jsonify({"error": "provided channel_name was not found."}), 404 return jsonify(channel) def _perform_action(bot, data): - for arg in ['action', 'kwargs']: + for arg in ["action", "kwargs"]: if arg not in data: - return { - 'ok': False, - 'error': '{} not provided in payload'.format(arg) - } - action = data['action'] - kwargs = data['kwargs'] + return {"ok": False, "error": "{} not provided in payload".format(arg)} + action = data["action"] + kwargs = data["kwargs"] logger.debug( - 'Performing action', + "Performing action", extra=merge_logging_context( - {'action': action}, + {"action": action}, bot.logging_context, ), ) parse_kwargs(kwargs, bot) - ret = slack.client(bot).api_call( - action, - **kwargs - ) + ret = slack.client(bot).api_call(action, **kwargs) logger.debug(ret) - if not ret['ok']: - if ret.get('error') in { - 'missing_scope', - 'not_allowed_token_type', - 'channel_not_found', - 'not_in_channel', + if not ret["ok"]: + if ret.get("error") in { + "missing_scope", + "not_allowed_token_type", + "channel_not_found", + "not_in_channel", }: logger.warning( - 'action failed in post_slack, attempting as user.', + "action failed in post_slack, attempting as user.", extra=merge_logging_context( - {'action': action}, + {"action": action}, bot.logging_context, ), ) try: - ret = slack.client(bot, client_type='user').api_call( - action, - **kwargs - ) + ret = slack.client(bot, client_type="user").api_call(action, **kwargs) except json.decoder.JSONDecodeError: logger.exception( - 'JSON decode failure when parsing kwargs={}'.format( - kwargs - ), + "JSON decode failure when parsing kwargs={}".format(kwargs), extra=merge_logging_context( - {'action': action}, + {"action": action}, bot.logging_context, ), ) - return {'ok': False} + return {"ok": False} logger.debug(ret) - if not ret['ok']: + if not ret["ok"]: logger.error( - 'action failed in post_slack: ret={}'.format( - ret - ), + "action failed in post_slack: ret={}".format(ret), extra=merge_logging_context( - {'action': action, 'kwargs': kwargs}, + {"action": action, "kwargs": kwargs}, bot.logging_context, ), ) else: logger.error( - 'action failed in post_slack: ret={}'.format( - ret - ), + "action failed in post_slack: ret={}".format(ret), extra=merge_logging_context( - {'action': action, 'kwargs': kwargs}, + {"action": action, "kwargs": kwargs}, bot.logging_context, ), ) return ret -@blueprint.route( - '/api/v2/slack/action//', - methods=['POST'] -) +@blueprint.route("/api/v2/slack/action//", methods=["POST"]) @authnz.enforce_checks @verify_bot def slack_action_v2(team_name, bot_name): @@ -863,22 +828,19 @@ def slack_action_v2(team_name, bot_name): try: team = Team.get_team_by_name(team_name) except TeamInitializationError: - return jsonify({'error': 'provided team name was not found.'}), 404 + return jsonify({"error": "provided team name was not found."}), 404 try: bot = Bot.get_bot_by_name(team, bot_name) except BotInitializationError: - return jsonify({'error': 'provided bot name was not found.'}), 404 + return jsonify({"error": "provided bot name was not found."}), 404 ret = _perform_action(bot, data) - if ret['ok']: + if ret["ok"]: return jsonify(ret), 200 else: return jsonify(ret), 400 -@blueprint.route( - '/api/v1/slack/get_ims//', - methods=['GET'] -) +@blueprint.route("/api/v1/slack/get_ims//", methods=["GET"]) @authnz.enforce_checks @verify_bot def get_bot_ims(team_name, bot_name): @@ -924,23 +886,21 @@ def get_bot_ims(team_name, bot_name): try: team = Team.get_team_by_name(team_name) except TeamInitializationError: - return jsonify({'error': 'provided team name was not found.'}), 404 + return jsonify({"error": "provided team name was not found."}), 404 try: bot = Bot.get_bot_by_name(team, bot_name) except BotInitializationError: - return jsonify({'error': 'provided bot name was not found.'}), 404 + return jsonify({"error": "provided bot name was not found."}), 404 raw_ims = slack.get_ims(bot) ims = [] for im in raw_ims: # each im is a tuple where im[0] is the channel id and im[1] is the im object ims.append(json.loads(im[1])) - return jsonify( - {'ims': ims} - ) + return jsonify({"ims": ims}) @blueprint.route( - '/api/v1/slack/send_im///', methods=['POST'] + "/api/v1/slack/send_im///", methods=["POST"] ) @authnz.enforce_checks @verify_bot @@ -999,34 +959,58 @@ def send_bot_im(team_name, bot_name, email): team = Team.get_team_by_name(team_name) bot = Bot.get_bot_by_name(team, bot_name) except TeamInitializationError: - return jsonify({'error': 'provided team name was not found.', - 'team_name': team_name, - 'bot_name': bot_name, - 'email': email - }), 404 + return ( + jsonify( + { + "error": "provided team name was not found.", + "team_name": team_name, + "bot_name": bot_name, + "email": email, + } + ), + 404, + ) except BotInitializationError: - return jsonify({'error': 'provided bot name was not found.', - 'team_name': team_name, - 'bot_name': bot_name, - 'email': email - }), 404 + return ( + jsonify( + { + "error": "provided bot name was not found.", + "team_name": team_name, + "bot_name": bot_name, + "email": email, + } + ), + 404, + ) user = slack.get_user_by_email(bot, email) if not user: - return jsonify({'error': 'unable to find slack user for given email.', - 'team_name': team_name, - 'bot_name': bot_name, - 'email': email - }), 404 - im_id = slack.get_im_channel_id(bot, user['id']) + return ( + jsonify( + { + "error": "unable to find slack user for given email.", + "team_name": team_name, + "bot_name": bot_name, + "email": email, + } + ), + 404, + ) + im_id = slack.get_im_channel_id(bot, user["id"]) if im_id is None: - return jsonify({'error': 'unable to find IM channel.', - 'team_name': team_name, - 'bot_name': bot_name, - 'email': email - }), 404 - data['kwargs']['channel'] = im_id + return ( + jsonify( + { + "error": "unable to find IM channel.", + "team_name": team_name, + "bot_name": bot_name, + "email": email, + } + ), + 404, + ) + data["kwargs"]["channel"] = im_id ret = _perform_action(bot, data) - if ret['ok']: + if ret["ok"]: return jsonify(ret), 200 else: return jsonify(ret), 400 diff --git a/omnibot/scripts/omniredis.py b/omnibot/scripts/omniredis.py index 45bd01e..f8e125d 100644 --- a/omnibot/scripts/omniredis.py +++ b/omnibot/scripts/omniredis.py @@ -8,13 +8,13 @@ class PurgeRedis(Command): def run(self): redis_client = omniredis.get_redis_client() for team in settings.SLACK_TEAMS: - redis_client.delete('channels:{}'.format(team)) - redis_client.delete('channelsmap:{}'.format(team)) - redis_client.delete('groups:{}'.format(team)) - redis_client.delete('groupsmap:{}'.format(team)) - redis_client.delete('ims:{}'.format(team)) - redis_client.delete('imsmap:{}'.format(team)) - redis_client.delete('mpims:{}'.format(team)) - redis_client.delete('mpimsmap:{}'.format(team)) - redis_client.delete('users:{}'.format(team)) - redis_client.delete('teams') + redis_client.delete("channels:{}".format(team)) + redis_client.delete("channelsmap:{}".format(team)) + redis_client.delete("groups:{}".format(team)) + redis_client.delete("groupsmap:{}".format(team)) + redis_client.delete("ims:{}".format(team)) + redis_client.delete("imsmap:{}".format(team)) + redis_client.delete("mpims:{}".format(team)) + redis_client.delete("mpimsmap:{}".format(team)) + redis_client.delete("users:{}".format(team)) + redis_client.delete("teams") diff --git a/omnibot/scripts/utils.py b/omnibot/scripts/utils.py index 603505d..be34571 100644 --- a/omnibot/scripts/utils.py +++ b/omnibot/scripts/utils.py @@ -7,11 +7,8 @@ class CreateSQSQueue(Command): def run(self): try: - sqs = omnibot.services.get_boto_client( - 'sqs', - endpoint_url=settings.SQS_URL - ) + sqs = omnibot.services.get_boto_client("sqs", endpoint_url=settings.SQS_URL) print(sqs.create_queue(QueueName=settings.SQS_QUEUE_NAME)) except Exception: - print('Failed to create queue') + print("Failed to create queue") return 1 diff --git a/omnibot/services/__init__.py b/omnibot/services/__init__.py index b47d918..2025c11 100644 --- a/omnibot/services/__init__.py +++ b/omnibot/services/__init__.py @@ -9,55 +9,43 @@ def get_boto_client( - client, - region=None, - aws_access_key_id=None, - aws_secret_access_key=None, - aws_session_token=None, - config=None, - endpoint_url=None - ): + client, + region=None, + aws_access_key_id=None, + aws_secret_access_key=None, + aws_session_token=None, + config=None, + endpoint_url=None, +): """Get a boto3 client connection.""" if config is None: config = {} - cache_key = '{0}:{1}:{2}:{3}:{4}'.format( - client, - region, - aws_access_key_id, - config.get('name'), - endpoint_url + cache_key = "{0}:{1}:{2}:{3}:{4}".format( + client, region, aws_access_key_id, config.get("name"), endpoint_url ) if not aws_session_token: if cache_key in CLIENT_CACHE: return CLIENT_CACHE[cache_key] session = get_boto_session( - region, - aws_access_key_id, - aws_secret_access_key, - aws_session_token + region, aws_access_key_id, aws_secret_access_key, aws_session_token ) if not session: logger.error("Failed to get {0} client.".format(client)) return None CLIENT_CACHE[cache_key] = session.client( - client, - config=config.get('config'), - endpoint_url=endpoint_url + client, config=config.get("config"), endpoint_url=endpoint_url ) return CLIENT_CACHE[cache_key] def get_boto_session( - region, - aws_access_key_id=None, - aws_secret_access_key=None, - aws_session_token=None - ): + region, aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None +): """Get a boto3 session.""" return boto3.session.Session( region_name=region, aws_secret_access_key=aws_secret_access_key, aws_access_key_id=aws_access_key_id, - aws_session_token=aws_session_token + aws_session_token=aws_session_token, ) diff --git a/omnibot/services/omniredis.py b/omnibot/services/omniredis.py index b934013..7b550c7 100644 --- a/omnibot/services/omniredis.py +++ b/omnibot/services/omniredis.py @@ -10,5 +10,5 @@ def get_redis_client(decode_responses=True): host=settings.REDIS_HOST, port=settings.REDIS_PORT, charset="utf-8", - decode_responses=decode_responses + decode_responses=decode_responses, ) diff --git a/omnibot/services/slack/__init__.py b/omnibot/services/slack/__init__.py index 5fd9145..560d348 100644 --- a/omnibot/services/slack/__init__.py +++ b/omnibot/services/slack/__init__.py @@ -19,7 +19,7 @@ _client = {} -def client(bot, client_type='bot'): +def client(bot, client_type="bot"): """ Global Slack client. """ @@ -30,20 +30,20 @@ def client(bot, client_type='bot'): if bot.name not in _client[team_name]: _client[team_name][bot.name] = {} if bot.oauth_bot_token: - _client[team_name][bot.name]['bot'] = slackclient.SlackClient( + _client[team_name][bot.name]["bot"] = slackclient.SlackClient( bot.oauth_bot_token ) if bot.oauth_user_token: - _client[team_name][bot.name]['user'] = slackclient.SlackClient( + _client[team_name][bot.name]["user"] = slackclient.SlackClient( bot.oauth_user_token ) - if client_type == 'bot': + if client_type == "bot": try: - return _client[team_name][bot.name]['bot'] + return _client[team_name][bot.name]["bot"] except KeyError: pass - elif client_type == 'user': - return _client[team_name][bot.name]['user'] + elif client_type == "user": + return _client[team_name][bot.name]["user"] def _get_failure_context(result): @@ -51,7 +51,7 @@ def _get_failure_context(result): Get the logging context from a failed slack call. """ ret = {} - for attr in ['error', 'needed', 'provided']: + for attr in ["error", "needed", "provided"]: if attr in result: ret[attr] = result[attr] return ret @@ -63,41 +63,38 @@ def _get_conversations(bot, team): """ conversations = [] retry = 0 - next_cursor = '' + next_cursor = "" while True: conversations_data = client(bot).api_call( - 'conversations.list', + "conversations.list", exclude_archived=True, exclude_members=True, limit=1000, cursor=next_cursor, - team_id=team.team_id + team_id=team.team_id, ) - if conversations_data['ok']: - conversations.extend(conversations_data['channels']) + if conversations_data["ok"]: + conversations.extend(conversations_data["channels"]) else: # TODO: split this retry logic into a generic retry function retry = retry + 1 if retry >= MAX_RETRIES: logger.error( - 'Exceeded max retries when calling conversations.list.', + "Exceeded max retries when calling conversations.list.", extra=bot.logging_context, ) break logger.warning( - 'Call to channels.list failed, attempting retry', + "Call to channels.list failed, attempting retry", extra=merge_logging_context( - {'retry': retry}, + {"retry": retry}, _get_failure_context(conversations_data), bot.logging_context, ), ) gevent.sleep(GEVENT_SLEEP_TIME) continue - next_cursor = conversations_data.get( - 'response_metadata', - {} - ).get('next_cursor') + next_cursor = conversations_data.get("response_metadata", {}).get("next_cursor") if not next_cursor: break gevent.sleep(GEVENT_SLEEP_TIME) @@ -106,20 +103,20 @@ def _get_conversations(bot, team): def update_conversations(bot, team): for conversation in _get_conversations(bot, team): - if conversation.get('is_channel', False): + if conversation.get("is_channel", False): update_channel(bot, conversation) - elif conversation.get('is_group', False): + elif conversation.get("is_group", False): update_group(bot, conversation) - elif conversation.get('is_im', False): + elif conversation.get("is_im", False): update_im(bot, conversation) - elif conversation.get('is_mpim', False): + elif conversation.get("is_mpim", False): update_mpim(bot, conversation) else: logger.info( - 'Not updating unsupported conversation.', + "Not updating unsupported conversation.", extra=merge_logging_context( bot.logging_context, - {'channel': conversation['id']}, + {"channel": conversation["id"]}, ), ) @@ -127,92 +124,79 @@ def update_conversations(bot, team): def update_channel(bot, channel): redis_client = omniredis.get_redis_client() redis_client.hset( - 'channels:{}'.format(bot.team.name), - channel['id'], - json.dumps(channel) + "channels:{}".format(bot.team.name), channel["id"], json.dumps(channel) ) redis_client.hset( - 'channelsmap:{}'.format(bot.team.name), - channel['name'], - channel['id'], + "channelsmap:{}".format(bot.team.name), + channel["name"], + channel["id"], ) def get_channels(bot): redis_client = omniredis.get_redis_client() - return redis_client.hscan_iter('channels:{}'.format(bot.team.name)) + return redis_client.hscan_iter("channels:{}".format(bot.team.name)) def update_group(bot, group): redis_client = omniredis.get_redis_client() + redis_client.hset("groups:{}".format(bot.team.name), group["id"], json.dumps(group)) redis_client.hset( - 'groups:{}'.format(bot.team.name), - group['id'], - json.dumps(group) - ) - redis_client.hset( - 'groupsmap:{}'.format(bot.team.name), - group['name'], - group['id'], + "groupsmap:{}".format(bot.team.name), + group["name"], + group["id"], ) def get_groups(bot): redis_client = omniredis.get_redis_client() - return redis_client.hscan_iter('groups:{}'.format(bot.team.name)) + return redis_client.hscan_iter("groups:{}".format(bot.team.name)) def update_im(bot, im): redis_client = omniredis.get_redis_client() + redis_client.hset("ims:{}".format(bot.team.name), im["id"], json.dumps(im)) redis_client.hset( - 'ims:{}'.format(bot.team.name), - im['id'], - json.dumps(im) - ) - redis_client.hset( - 'imsmap:{}'.format(bot.team.name), - im['user'], - im['id'], + "imsmap:{}".format(bot.team.name), + im["user"], + im["id"], ) def get_ims(bot): redis_client = omniredis.get_redis_client() - return redis_client.hscan_iter('ims:{}'.format(bot.team.name)) + return redis_client.hscan_iter("ims:{}".format(bot.team.name)) def get_im_channel_id(bot, user_id): redis_client = omniredis.get_redis_client() - imsmap_id = redis_client.hget('imsmap:{}'.format(bot.team.name), user_id) + imsmap_id = redis_client.hget("imsmap:{}".format(bot.team.name), user_id) if imsmap_id: - raw_im = redis_client.hget('ims:{}'.format(bot.team.name), imsmap_id) + raw_im = redis_client.hget("ims:{}".format(bot.team.name), imsmap_id) if raw_im: im = json.loads(raw_im) - if not im.get('is_user_deleted', False): - return im['id'] + if not im.get("is_user_deleted", False): + return im["id"] retry = 0 while True: users = user_id - conversation_data = client(bot).api_call( - 'conversations.open', - users=users - ) - if conversation_data['ok']: - return conversation_data['channel']['id'] + conversation_data = client(bot).api_call("conversations.open", users=users) + if conversation_data["ok"]: + return conversation_data["channel"]["id"] else: # TODO: split this retry logic into a generic retry function retry = retry + 1 if retry >= MAX_RETRIES: logger.error( - 'Exceeded max retries when calling conversations.open.', + "Exceeded max retries when calling conversations.open.", extra=bot.logging_context, ) break logger.warning( - 'Call to conversations.open failed, attempting retry', + "Call to conversations.open failed, attempting retry", extra=merge_logging_context( - {'retry': retry}, + {"retry": retry}, _get_failure_context(conversation_data), bot.logging_context, ), @@ -224,33 +208,29 @@ def get_im_channel_id(bot, user_id): def update_mpim(bot, mpim): redis_client = omniredis.get_redis_client() + redis_client.hset("mpims:{}".format(bot.team.name), mpim["id"], json.dumps(mpim)) redis_client.hset( - 'mpims:{}'.format(bot.team.name), - mpim['id'], - json.dumps(mpim) - ) - redis_client.hset( - 'mpimsmap:{}'.format(bot.team.name), - mpim['name'], - mpim['id'], + "mpimsmap:{}".format(bot.team.name), + mpim["name"], + mpim["id"], ) def get_mpims(bot): redis_client = omniredis.get_redis_client() - return redis_client.hscan_iter('mpims:{}'.format(bot.team.name)) + return redis_client.hscan_iter("mpims:{}".format(bot.team.name)) def _get_emoji(bot): # TODO: split this retry logic into a generic retry function for retry in range(MAX_RETRIES): - resp = client(bot).api_call('emoji.list') - if resp['ok']: + resp = client(bot).api_call("emoji.list") + if resp["ok"]: break logger.warning( - 'Call to emoji.list failed, attempting retry', + "Call to emoji.list failed, attempting retry", extra=merge_logging_context( - {'retry': retry}, + {"retry": retry}, _get_failure_context(resp), bot.logging_context, ), @@ -258,16 +238,16 @@ def _get_emoji(bot): gevent.sleep(GEVENT_SLEEP_TIME) else: logger.error( - 'Exceeded max retries when calling emoji.list.', + "Exceeded max retries when calling emoji.list.", extra=bot.logging_context, ) return {} emoji = {} - for k, v in resp['emoji'].items(): - while v.startswith('alias:'): - _, _, alias = v.partition(':') - v = resp['emoji'].get(alias, '') + for k, v in resp["emoji"].items(): + while v.startswith("alias:"): + _, _, alias = v.partition(":") + v = resp["emoji"].get(alias, "") if v: emoji[k] = v return emoji @@ -276,29 +256,26 @@ def _get_emoji(bot): def update_emoji(bot): redis_client = omniredis.get_redis_client() for k, v in _get_emoji(bot).items(): - redis_client.hset('emoji:{}'.format(bot.team.name), k, v) + redis_client.hset("emoji:{}".format(bot.team.name), k, v) def get_emoji(bot, name): redis_client = omniredis.get_redis_client() - return redis_client.hget('emoji:{}'.format(bot.team.name), name) + return redis_client.hget("emoji:{}".format(bot.team.name), name) def _get_channel_from_cache(bot, channel): redis_client = omniredis.get_redis_client() - channel_data = redis_client.hget( - 'channels:{}'.format(bot.team.name), - channel - ) + channel_data = redis_client.hget("channels:{}".format(bot.team.name), channel) if channel_data: return json.loads(channel_data) - group_data = redis_client.hget('groups:{}'.format(bot.team.name), channel) + group_data = redis_client.hget("groups:{}".format(bot.team.name), channel) if group_data: return json.loads(group_data) - im_data = redis_client.hget('ims:{}'.format(bot.team.name), channel) + im_data = redis_client.hget("ims:{}".format(bot.team.name), channel) if im_data: return json.loads(im_data) - mpim_data = redis_client.hget('mpims:{}'.format(bot.team.name), channel) + mpim_data = redis_client.hget("mpims:{}".format(bot.team.name), channel) if mpim_data: return json.loads(mpim_data) return None @@ -309,35 +286,32 @@ def get_channel(bot, channel): Get a channel, from its channel id """ logger.debug( - 'Fetching channel', + "Fetching channel", extra=merge_logging_context( - {'channel': channel}, + {"channel": channel}, bot.logging_context, - ) + ), ) cached_channel = _get_channel_from_cache(bot, channel) if cached_channel: return cached_channel logger.debug( - 'Channel not in cache.', + "Channel not in cache.", extra=merge_logging_context( - {'channel': channel}, + {"channel": channel}, bot.logging_context, - ) + ), ) - channel_data = client(bot).api_call( - 'conversations.info', - channel=channel - ) - if channel_data['ok']: - update_channel(bot, channel_data['channel']) - return channel_data['channel'] + channel_data = client(bot).api_call("conversations.info", channel=channel) + if channel_data["ok"]: + update_channel(bot, channel_data["channel"]) + return channel_data["channel"] return {} def _get_channel_name_from_cache(key, bot_name, value): redis_client = omniredis.get_redis_client() - ret = redis_client.hget('{}:{}'.format(key, bot_name), value) + ret = redis_client.hget("{}:{}".format(key, bot_name), value) if ret is None: return None else: @@ -349,68 +323,58 @@ def get_channel_by_name(bot, channel): Get a channel, from its channel name. This function will only fetch from cache. If the channel isn't in cache, it will return None. """ - if channel.startswith('#'): + if channel.startswith("#"): channel = channel[1:] redis_client = omniredis.get_redis_client() - channel_id = redis_client.hget( - 'channelsmap:{}'.format(bot.team.name), - channel - ) + channel_id = redis_client.hget("channelsmap:{}".format(bot.team.name), channel) if channel_id: - return _get_channel_name_from_cache( - 'channels', - bot.team.name, - channel_id - ) - group_id = redis_client.hget('groupsmap:{}'.format(bot.team.name), channel) + return _get_channel_name_from_cache("channels", bot.team.name, channel_id) + group_id = redis_client.hget("groupsmap:{}".format(bot.team.name), channel) if group_id: - return _get_channel_name_from_cache('groups', bot.team.name, group_id) - im_id = redis_client.hget('imsmap:{}'.format(bot.team.name), channel) + return _get_channel_name_from_cache("groups", bot.team.name, group_id) + im_id = redis_client.hget("imsmap:{}".format(bot.team.name), channel) if im_id: - return _get_channel_name_from_cache('ims', bot.team.name, im_id) - mpim_id = redis_client.hget('mpimsmap:{}'.format(bot.team.name), channel) + return _get_channel_name_from_cache("ims", bot.team.name, im_id) + mpim_id = redis_client.hget("mpimsmap:{}".format(bot.team.name), channel) if mpim_id: - return _get_channel_name_from_cache('mpims', bot.team.name, mpim_id) + return _get_channel_name_from_cache("mpims", bot.team.name, mpim_id) return None def _get_users(bot, team, max_retries=MAX_RETRIES, sleep=GEVENT_SLEEP_TIME): users = [] retry = 0 - next_cursor = '' + next_cursor = "" while True: users_data = client(bot).api_call( - 'users.list', + "users.list", presence=False, limit=1000, cursor=next_cursor, - team_id=team.team_id + team_id=team.team_id, ) - if users_data['ok']: - users.extend(users_data['members']) + if users_data["ok"]: + users.extend(users_data["members"]) else: # TODO: split this retry logic into a generic retry function retry = retry + 1 if retry >= max_retries: logger.error( - 'Exceeded max retries when calling users.list.', + "Exceeded max retries when calling users.list.", extra=bot.logging_context, ) break logger.warning( - 'Call to users.list failed, attempting retry', + "Call to users.list failed, attempting retry", extra=merge_logging_context( - {'retry': retry}, + {"retry": retry}, _get_failure_context(users_data), bot.logging_context, ), ) gevent.sleep(sleep * retry) continue - next_cursor = users_data.get( - 'response_metadata', - {} - ).get('next_cursor') + next_cursor = users_data.get("response_metadata", {}).get("next_cursor") if not next_cursor: break gevent.sleep(sleep) @@ -423,36 +387,34 @@ def update_users(bot, team): def update_user(bot, user): - if user['is_bot']: + if user["is_bot"]: return - if user.get('deleted', False): + if user.get("deleted", False): return - profile = user.get('profile') + profile = user.get("profile") if not profile: return - email = profile.get('email') + email = profile.get("email") if not email: return name = get_name_from_user(user) redis_client = omniredis.get_redis_client() + redis_client.hset("users:{}".format(bot.team.name), user["id"], json.dumps(user)) redis_client.hset( - 'users:{}'.format(bot.team.name), user['id'], json.dumps(user) - ) - redis_client.hset( - 'usersmap:name:{}'.format(bot.team.name), + "usersmap:name:{}".format(bot.team.name), name, - user['id'], + user["id"], ) redis_client.hset( - 'usersmap:email:{}'.format(bot.team.name), + "usersmap:email:{}".format(bot.team.name), email, - user['id'], + user["id"], ) def get_users(bot): redis_client = omniredis.get_redis_client() - return redis_client.hscan_iter('users:{}'.format(bot.team.name)) + return redis_client.hscan_iter("users:{}".format(bot.team.name)) def get_user(bot, user_id): @@ -460,21 +422,18 @@ def get_user(bot, user_id): Get a user, from its user id """ redis_client = omniredis.get_redis_client() - user = redis_client.hget('users:{}'.format(bot.team.name), user_id) + user = redis_client.hget("users:{}".format(bot.team.name), user_id) if user: return json.loads(user) - user = client(bot).api_call( - 'users.info', - user=user_id - ) - if user['ok']: - update_user(bot, user['user']) - return user['user'] + user = client(bot).api_call("users.info", user=user_id) + if user["ok"]: + update_user(bot, user["user"]) + return user["user"] else: logger.warning( - 'Failed to find user', + "Failed to find user", extra=merge_logging_context( - {'user': user_id}, + {"user": user_id}, bot.logging_context, ), ) @@ -482,27 +441,21 @@ def get_user(bot, user_id): def get_name_from_user(user): - profile = user.get('profile', {}) - name = profile.get('display_name') + profile = user.get("profile", {}) + name = profile.get("display_name") if name: return name else: - return user.get('name') + return user.get("name") def get_user_by_name(bot, username): - if username.startswith('@'): + if username.startswith("@"): username = username[1:] redis_client = omniredis.get_redis_client() - user_id = redis_client.hget( - 'usersmap:name:{}'.format(bot.team.name), - username - ) + user_id = redis_client.hget("usersmap:name:{}".format(bot.team.name), username) if user_id: - user_data = redis_client.hget( - 'users:{}'.format(bot.team.name), - user_id - ) + user_data = redis_client.hget("users:{}".format(bot.team.name), user_id) if user_data: return json.loads(user_data) return {} @@ -510,15 +463,9 @@ def get_user_by_name(bot, username): def get_user_by_email(bot, email): redis_client = omniredis.get_redis_client() - user_id = redis_client.hget( - 'usersmap:email:{}'.format(bot.team.name), - email - ) + user_id = redis_client.hget("usersmap:email:{}".format(bot.team.name), email) if user_id: - user_data = redis_client.hget( - 'users:{}'.format(bot.team.name), - user_id - ) + user_data = redis_client.hget("users:{}".format(bot.team.name), user_id) if user_data: return json.loads(user_data) return {} diff --git a/omnibot/services/slack/bot.py b/omnibot/services/slack/bot.py index 8bc0de8..a7b35fc 100644 --- a/omnibot/services/slack/bot.py +++ b/omnibot/services/slack/bot.py @@ -19,16 +19,16 @@ def __init__(self, team, name, data): def _configure_handlers(self): handlers = settings.HANDLERS - for handler in handlers.get('interactive_component_handlers', []): - bots = handler.get('bots', {}).get(self.team.name, {}) + for handler in handlers.get("interactive_component_handlers", []): + bots = handler.get("bots", {}).get(self.team.name, {}) if self.name in bots: self._interactive_component_handlers.append(handler) - for handler in handlers.get('slash_command_handlers', []): - bots = handler.get('bots', {}).get(self.team.name, {}) + for handler in handlers.get("slash_command_handlers", []): + bots = handler.get("bots", {}).get(self.team.name, {}) if self.name in bots: self._slash_command_handlers.append(handler) - for handler in handlers.get('message_handlers', []): - bots = handler.get('bots', {}).get(self.team.name, {}) + for handler in handlers.get("message_handlers", []): + bots = handler.get("bots", {}).get(self.team.name, {}) if self.name in bots: self._message_handlers.append(handler) @@ -37,7 +37,7 @@ def get_bot_by_name(cls, team, name): bots = settings.SLACK_BOT_TOKENS.get(team.name, {}) _bot_data = bots.get(name, {}) if not _bot_data: - raise BotInitializationError('Invalid bot') + raise BotInitializationError("Invalid bot") return cls(team, name, _bot_data) @classmethod @@ -46,12 +46,12 @@ def get_bot_by_bot_id(cls, team, bot_id): _bot_data = {} bots = settings.SLACK_BOT_TOKENS.get(team.name, {}) for bot_name, bot_data in bots.items(): - if bot_id == bot_data.get('app_id'): + if bot_id == bot_data.get("app_id"): name = bot_name _bot_data = bot_data break if not _bot_data: - raise BotInitializationError('Invalid bot') + raise BotInitializationError("Invalid bot") return cls(team, name, _bot_data) @classmethod @@ -60,14 +60,14 @@ def get_bot_by_verification_token(cls, verification_token): _bot_data = {} for team_name, bots in settings.SLACK_BOT_TOKENS.items(): for bot_name, bot_data in bots.items(): - if verification_token == bot_data['verification_token']: + if verification_token == bot_data["verification_token"]: name = bot_name _bot_data = bot_data break if name is not None: break if not _bot_data: - raise BotInitializationError('Invalid bot') + raise BotInitializationError("Invalid bot") team = Team.get_team_by_name(team_name) return cls(team, name, _bot_data) @@ -81,26 +81,26 @@ def name(self): @property def bot_id(self): - return self._bot_data.get('app_id') + return self._bot_data.get("app_id") @property def verification_token(self): try: - return self._bot_data.get('verification_token') + return self._bot_data.get("verification_token") except KeyError: return None @property def oauth_user_token(self): try: - return self._bot_data.get('oauth_user_token') + return self._bot_data.get("oauth_user_token") except KeyError: return None @property def oauth_bot_token(self): try: - return self._bot_data.get('oauth_bot_token') + return self._bot_data.get("oauth_bot_token") except KeyError: return None @@ -120,10 +120,10 @@ def message_handlers(self): def logging_context(self): return merge_logging_context( { - 'bot': self.name, - 'bot_id': self.bot_id, + "bot": self.name, + "bot_id": self.bot_id, }, - self.team.logging_context + self.team.logging_context, ) diff --git a/omnibot/services/slack/interactive_component.py b/omnibot/services/slack/interactive_component.py index 8c25959..00751e5 100644 --- a/omnibot/services/slack/interactive_component.py +++ b/omnibot/services/slack/interactive_component.py @@ -14,207 +14,186 @@ class InteractiveComponent(object): def __init__(self, bot, component, event_trace): self._event_trace = event_trace self._payload = {} - self._payload['omnibot_payload_type'] = 'interactive_component' + self._payload["omnibot_payload_type"] = "interactive_component" self._bot = bot # The bot object has data we don't want to pass to downstreams, so # in the payload, we just store specific bot data. - self._payload['bot'] = { - 'name': bot.name, - 'bot_id': bot.bot_id - } + self._payload["bot"] = {"name": bot.name, "bot_id": bot.bot_id} # For future safety sake, we'll do the same for the team. - self._payload['team'] = { - 'name': bot.team.name, - 'team_id': bot.team.team_id - } - self._payload['type'] = component['type'] - self._payload['callback_id'] = get_callback_id(component) - self._payload['action_ts'] = component.get('action_ts') - self._payload['message_ts'] = component.get('message_ts') - self._payload['trigger_id'] = component.get('trigger_id') - self._payload['response_url'] = component.get('response_url') - self._payload['original_message'] = component.get('original_message') - self._payload['view'] = component.get('view') - self._payload['state'] = component.get('state') - self._payload['user'] = component.get('user') + self._payload["team"] = {"name": bot.team.name, "team_id": bot.team.team_id} + self._payload["type"] = component["type"] + self._payload["callback_id"] = get_callback_id(component) + self._payload["action_ts"] = component.get("action_ts") + self._payload["message_ts"] = component.get("message_ts") + self._payload["trigger_id"] = component.get("trigger_id") + self._payload["response_url"] = component.get("response_url") + self._payload["original_message"] = component.get("original_message") + self._payload["view"] = component.get("view") + self._payload["state"] = component.get("state") + self._payload["user"] = component.get("user") if self.user: - self._payload['parsed_user'] = slack.get_user( - self.bot, - self.user['id'] - ) - self._payload['channel'] = component.get('channel') + self._payload["parsed_user"] = slack.get_user(self.bot, self.user["id"]) + self._payload["channel"] = component.get("channel") if self.channel: - self._event_trace['channel_id'] = self.channel['id'] - self._payload['parsed_channel'] = slack.get_channel( - self.bot, - self.channel['id'] + self._event_trace["channel_id"] = self.channel["id"] + self._payload["parsed_channel"] = slack.get_channel( + self.bot, self.channel["id"] ) - self._payload['message'] = component.get('message') + self._payload["message"] = component.get("message") if self.message: self._parse_message() - self._payload['submission'] = component.get('submission') - self._payload['actions'] = component.get('actions') + self._payload["submission"] = component.get("submission") + self._payload["actions"] = component.get("actions") def _parse_message(self): - if self.message.get('user'): - self._payload['message']['parsed_user'] = slack.get_user( - self.bot, - self.message['user'] + if self.message.get("user"): + self._payload["message"]["parsed_user"] = slack.get_user( + self.bot, self.message["user"] ) - elif self.message.get('bot_id'): + elif self.message.get("bot_id"): # TODO: call get_bot - self._payload['message']['parsed_user'] = None + self._payload["message"]["parsed_user"] = None else: - self._payload['message']['parsed_user'] = None + self._payload["message"]["parsed_user"] = None try: - self._payload['message']['users'] = parser.extract_users( - self.message['text'], - self.bot + self._payload["message"]["users"] = parser.extract_users( + self.message["text"], self.bot ) - self._payload['message']['parsed_text'] = parser.replace_users( - self.message['text'], - self.message['users'] + self._payload["message"]["parsed_text"] = parser.replace_users( + self.message["text"], self.message["users"] ) except Exception: logger.exception( - 'Failed to extract user info from text.', + "Failed to extract user info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['message']['channels'] = parser.extract_channels( - self.message['parsed_text'], - self.bot + self._payload["message"]["channels"] = parser.extract_channels( + self.message["parsed_text"], self.bot ) - self._payload['message']['parsed_text'] = parser.replace_channels( - self.message['parsed_text'], - self.message['channels'] + self._payload["message"]["parsed_text"] = parser.replace_channels( + self.message["parsed_text"], self.message["channels"] ) except Exception: logger.exception( - 'Failed to extract channel info from text.', + "Failed to extract channel info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['message']['subteams'] = parser.extract_subteams( - self.message['text'], - self.bot + self._payload["message"]["subteams"] = parser.extract_subteams( + self.message["text"], self.bot ) except Exception: logger.exception( - 'Failed to extract subteam info from text.', + "Failed to extract subteam info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['message']['specials'] = parser.extract_specials( - self.message['text'] + self._payload["message"]["specials"] = parser.extract_specials( + self.message["text"] ) - self._payload['message']['parsed_text'] = parser.replace_specials( - self.message['parsed_text'], - self.message['specials'] + self._payload["message"]["parsed_text"] = parser.replace_specials( + self.message["parsed_text"], self.message["specials"] ) except Exception: logger.exception( - 'Failed to extract special info from text.', + "Failed to extract special info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['message']['emojis'] = parser.extract_emojis( - self.message['text'] + self._payload["message"]["emojis"] = parser.extract_emojis( + self.message["text"] ) except Exception: logger.exception( - 'Failed to extract emoji info from text.', + "Failed to extract emoji info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['message']['emails'] = parser.extract_emails( - self.message['text'] + self._payload["message"]["emails"] = parser.extract_emails( + self.message["text"] ) - self._payload['message']['parsed_text'] = parser.replace_emails( - self.message['parsed_text'], - self.message['emails'] + self._payload["message"]["parsed_text"] = parser.replace_emails( + self.message["parsed_text"], self.message["emails"] ) except Exception: logger.exception( - 'Failed to extract email info from text.', + "Failed to extract email info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['message']['urls'] = parser.extract_urls( - self.message['text'] - ) - self._payload['message']['parsed_text'] = parser.replace_urls( - self.message['parsed_text'], - self.message['urls'] + self._payload["message"]["urls"] = parser.extract_urls(self.message["text"]) + self._payload["message"]["parsed_text"] = parser.replace_urls( + self.message["parsed_text"], self.message["urls"] ) except Exception: logger.exception( - 'Failed to extract url info from text.', + "Failed to extract url info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) @property def component_type(self): - return self._payload['type'] + return self._payload["type"] @property def callback_id(self): - return self._payload['callback_id'] + return self._payload["callback_id"] @property def action_ts(self): - return self._payload['action_ts'] + return self._payload["action_ts"] @property def trigger_id(self): - return self._payload['trigger_id'] + return self._payload["trigger_id"] @property def response_url(self): - return self._payload['response_url'] + return self._payload["response_url"] @property def message(self): - return self._payload['message'] + return self._payload["message"] @property def submission(self): - return self._payload['submission'] + return self._payload["submission"] @property def actions(self): - return self._payload['actions'] + return self._payload["actions"] @property def channel(self): - return self._payload['channel'] + return self._payload["channel"] @property def channel_id(self): - return self._payload['channel']['id'] + return self._payload["channel"]["id"] @property def parsed_channel(self): - return self._payload.get('parsed_channel') + return self._payload.get("parsed_channel") @property def user(self): - return self._payload['user'] + return self._payload["user"] @property def parsed_user(self): - return self._payload.get('parsed_user') + return self._payload.get("parsed_user") @property def team(self): - return self._payload['team'] + return self._payload["team"] @property def bot(self): diff --git a/omnibot/services/slack/message.py b/omnibot/services/slack/message.py index 0d61278..a659a2f 100644 --- a/omnibot/services/slack/message.py +++ b/omnibot/services/slack/message.py @@ -16,49 +16,38 @@ def __init__(self, bot, event, event_trace): self.event = event self._match = None self._payload = {} - self._payload['omnibot_payload_type'] = 'message' + self._payload["omnibot_payload_type"] = "message" self._bot = bot # The bot object has data we don't want to pass to downstreams, so # in the payload, we just store specific bot data. - self._payload['bot'] = { - 'name': bot.name, - 'bot_id': bot.bot_id - } + self._payload["bot"] = {"name": bot.name, "bot_id": bot.bot_id} # For future safety sake, we'll do the same for the team. - self._payload['team'] = { - 'name': bot.team.name, - 'team_id': bot.team.team_id - } - self._payload['ts'] = event['ts'] - self._payload['thread_ts'] = event.get('thread_ts') + self._payload["team"] = {"name": bot.team.name, "team_id": bot.team.team_id} + self._payload["ts"] = event["ts"] + self._payload["thread_ts"] = event.get("thread_ts") self._check_unsupported() - self._payload['user'] = event.get('user') + self._payload["user"] = event.get("user") if self.user: - self._payload['parsed_user'] = slack.get_user(self.bot, self.user) + self._payload["parsed_user"] = slack.get_user(self.bot, self.user) elif self.bot_id: # TODO: call get_bot - self._payload['parsed_user'] = None + self._payload["parsed_user"] = None else: - self._payload['parsed_user'] = None + self._payload["parsed_user"] = None try: - self._payload['text'] = event['text'] + self._payload["text"] = event["text"] except Exception: logger.error( - 'Message event is missing text attribute.', - extra=self.event_trace + "Message event is missing text attribute.", extra=self.event_trace ) raise - self._payload['parsed_text'] = self.text - self._payload['channel_id'] = event['channel'] - self._event_trace['channel_id'] = self.channel_id - self._payload['channel'] = slack.get_channel( - self.bot, - self.channel_id - ) + self._payload["parsed_text"] = self.text + self._payload["channel_id"] = event["channel"] + self._event_trace["channel_id"] = self.channel_id + self._payload["channel"] = slack.get_channel(self.bot, self.channel_id) if not self.channel: logger.error( - 'Failed to fetch channel from channel_id.', - extra=self.event_trace + "Failed to fetch channel from channel_id.", extra=self.event_trace ) self._parse_payload() @@ -69,188 +58,177 @@ def _check_unsupported(self): # Ignore bots unsupported = False if self.bot_id: - logger.debug('ignoring message from bot', extra=self.event_trace) + logger.debug("ignoring message from bot", extra=self.event_trace) unsupported = True # Ignore threads elif self.thread_ts: - logger.debug('ignoring thread message', extra=self.event_trace) + logger.debug("ignoring thread message", extra=self.event_trace) unsupported = True # For now, ignore all event subtypes elif self.subtype: - extra = {'subtype': self.subtype} + extra = {"subtype": self.subtype} extra.update(self.event_trace) logger.debug( - 'ignoring message with unsupported subtype', + "ignoring message with unsupported subtype", extra=extra, ) unsupported = True if unsupported: statsd = stats.get_statsd_client() - statsd.incr('event.unsupported') + statsd.incr("event.unsupported") raise MessageUnsupportedError() def _parse_payload(self): try: - self._payload['users'] = parser.extract_users(self.text, self.bot) - self._payload['parsed_text'] = parser.replace_users( - self.parsed_text, - self.users + self._payload["users"] = parser.extract_users(self.text, self.bot) + self._payload["parsed_text"] = parser.replace_users( + self.parsed_text, self.users ) except Exception: logger.exception( - 'Failed to extract user info from text.', + "Failed to extract user info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['channels'] = parser.extract_channels( - self.text, - self.bot - ) - self._payload['parsed_text'] = parser.replace_channels( - self.parsed_text, - self.channels + self._payload["channels"] = parser.extract_channels(self.text, self.bot) + self._payload["parsed_text"] = parser.replace_channels( + self.parsed_text, self.channels ) except Exception: logger.exception( - 'Failed to extract channel info from text.', + "Failed to extract channel info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['subteams'] = parser.extract_subteams( - self.text, - self.bot - ) + self._payload["subteams"] = parser.extract_subteams(self.text, self.bot) except Exception: logger.exception( - 'Failed to extract subteam info from text.', + "Failed to extract subteam info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['specials'] = parser.extract_specials(self.text) - self._payload['parsed_text'] = parser.replace_specials( - self.parsed_text, - self.specials + self._payload["specials"] = parser.extract_specials(self.text) + self._payload["parsed_text"] = parser.replace_specials( + self.parsed_text, self.specials ) except Exception: logger.exception( - 'Failed to extract special info from text.', + "Failed to extract special info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['emojis'] = parser.extract_emojis(self.text) + self._payload["emojis"] = parser.extract_emojis(self.text) except Exception: logger.exception( - 'Failed to extract emoji info from text.', + "Failed to extract emoji info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['emails'] = parser.extract_emails(self.text) - self._payload['parsed_text'] = parser.replace_emails( - self.parsed_text, - self.emails + self._payload["emails"] = parser.extract_emails(self.text) + self._payload["parsed_text"] = parser.replace_emails( + self.parsed_text, self.emails ) except Exception: logger.exception( - 'Failed to extract email info from text.', + "Failed to extract email info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['urls'] = parser.extract_urls(self.text) - self._payload['parsed_text'] = parser.replace_urls( - self.parsed_text, - self.urls + self._payload["urls"] = parser.extract_urls(self.text) + self._payload["parsed_text"] = parser.replace_urls( + self.parsed_text, self.urls ) except Exception: logger.exception( - 'Failed to extract url info from text.', + "Failed to extract url info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['directed'] = parser.extract_mentions( + self._payload["directed"] = parser.extract_mentions( # We match mentioned and directed against parsed users, not # against raw users. self.parsed_text, self.bot, - self.channel + self.channel, ) except Exception: logger.exception( - 'Failed to extract mentions from text.', + "Failed to extract mentions from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) - self._payload['mentioned'] = False + self._payload["mentioned"] = False for user_id, user_name in self.users.items(): if self.bot.name == user_name: - self._payload['mentioned'] = True + self._payload["mentioned"] = True try: - self._payload['command_text'] = parser.extract_command( + self._payload["command_text"] = parser.extract_command( # Similar to mentions above, we find the command text # from pre-parsed text for users, not against raw users. self.parsed_text, - self.bot + self.bot, ) except Exception: logger.exception( - 'Failed to extract command_text from text.', + "Failed to extract command_text from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) @property def subtype(self): - return self.event.get('subtype') + return self.event.get("subtype") @property def text(self): - return self._payload['text'] + return self._payload["text"] @property def parsed_text(self): - return self._payload['parsed_text'] + return self._payload["parsed_text"] @property def command_text(self): - return self._payload.get('command_text') + return self._payload.get("command_text") @property def directed(self): - return self._payload.get('directed', False) + return self._payload.get("directed", False) @property def mentioned(self): - return self._payload.get('mentioned', False) + return self._payload.get("mentioned", False) @property def channel_id(self): - return self._payload.get('channel_id') + return self._payload.get("channel_id") @property def channel(self): - return self._payload.get('channel', {}) + return self._payload.get("channel", {}) @property def user(self): - return self._payload['user'] + return self._payload["user"] @property def ts(self): - return self._payload['ts'] + return self._payload["ts"] @property def thread_ts(self): - return self._payload['thread_ts'] + return self._payload["thread_ts"] @property def team(self): - return self._payload['team'] + return self._payload["team"] @property def bot(self): @@ -267,31 +245,31 @@ def bot_id(self): The bot_id associated with the message, if the message if from a bot. If this message isn't from a bot, this will return None. """ - return self.event.get('bot_id') + return self.event.get("bot_id") @property def channels(self): - return self._payload.get('channels', {}) + return self._payload.get("channels", {}) @property def users(self): - return self._payload.get('users', {}) + return self._payload.get("users", {}) @property def specials(self): - return self._payload.get('specials', {}) + return self._payload.get("specials", {}) @property def emails(self): - return self._payload.get('emails', {}) + return self._payload.get("emails", {}) @property def urls(self): - return self._payload.get('urls', {}) + return self._payload.get("urls", {}) @property def match_type(self): - return self._payload.get('match_type') + return self._payload.get("match_type") @property def match(self): @@ -306,13 +284,13 @@ def event_trace(self): return self._event_trace def set_match(self, match_type, match): - self._payload['match_type'] = match_type + self._payload["match_type"] = match_type self._match = match - if match_type == 'command': - self._payload['command'] = match - self._payload['args'] = self.command_text[len(match):].strip() - elif match_type == 'regex': - self._payload['regex'] = match + if match_type == "command": + self._payload["command"] = match + self._payload["args"] = self.command_text[len(match) :].strip() + elif match_type == "regex": + self._payload["regex"] = match class MessageUnsupportedError(Exception): diff --git a/omnibot/services/slack/parser.py b/omnibot/services/slack/parser.py index 5bf7974..c656573 100644 --- a/omnibot/services/slack/parser.py +++ b/omnibot/services/slack/parser.py @@ -3,17 +3,17 @@ from omnibot.services import slack from omnibot.services import stats -SPACE_REGEX = re.compile(r'[\s\u00A0]') +SPACE_REGEX = re.compile(r"[\s\u00A0]") def extract_users(text, bot): statsd = stats.get_statsd_client() - with statsd.timer('parser.extract_users'): + with statsd.timer("parser.extract_users"): # Example: <@U024BE7LH> or <@U024BE7LH|bob-marley> or <@W024BE7LH|bob-marley> user_arr = {} - users = re.findall('<@[UW]\w+(?:\|[\w-]+)?>', text) + users = re.findall("<@[UW]\w+(?:\|[\w-]+)?>", text) for user in users: - match = re.match('<@([UW]\w+)(\|[\w-]+)?>', user) + match = re.match("<@([UW]\w+)(\|[\w-]+)?>", user) user_name = None if match.group(2) is not None: # user name is embedded; use the second match and strip | @@ -22,7 +22,7 @@ def extract_users(text, bot): user_id = match.group(1) user_data = slack.get_user(bot, user_id) if user_data: - user_name = user_data['name'] + user_name = user_data["name"] user_arr[user] = user_name return user_arr @@ -30,21 +30,18 @@ def extract_users(text, bot): def replace_users(text, users): for user, user_name in users.items(): if user_name is not None: - text = text.replace( - user, - '@{}'.format(user_name) - ) + text = text.replace(user, "@{}".format(user_name)) return text def extract_channels(text, bot): statsd = stats.get_statsd_client() - with statsd.timer('parser.extract_channels'): + with statsd.timer("parser.extract_channels"): # Example: <#C024BE7LR> or <#C024BE7LR|general-room> channel_arr = {} - channels = re.findall('<#C\w+(?:\|[\w-]+)?>', text) + channels = re.findall("<#C\w+(?:\|[\w-]+)?>", text) for channel in channels: - match = re.match('<#(C\w+)(\|[\w-]+)?>', channel) + match = re.match("<#(C\w+)(\|[\w-]+)?>", channel) channel_name = None if match.group(2) is not None: # channel name is embedded; use the second match and strip | @@ -54,7 +51,7 @@ def extract_channels(text, bot): channel_data = slack.get_channel(bot, channel_id) if not channel_data: continue - channel_name = channel_data['name'] + channel_name = channel_data["name"] channel_arr[channel] = channel_name return channel_arr @@ -62,17 +59,14 @@ def extract_channels(text, bot): def replace_channels(text, channels): for channel, channel_name in channels.items(): if channel_name is not None: - text = text.replace( - channel, - '#{}'.format(channel_name) - ) + text = text.replace(channel, "#{}".format(channel_name)) return text def extract_subteams(text, bot): statsd = stats.get_statsd_client() # TODO: parse this - with statsd.timer('parser.extract_subteams'): + with statsd.timer("parser.extract_subteams"): # Example: # subteams = re.findall( # '', @@ -86,15 +80,15 @@ def extract_subteams(text, bot): def extract_specials(text): statsd = stats.get_statsd_client() - with statsd.timer('parser.extract_specials'): + with statsd.timer("parser.extract_specials"): # Example: - specials = re.findall('', text) + specials = re.findall("", text) special_arr = {} for special in specials: - match = re.match('', special) + match = re.match("", special) special_label = None if match.group(1) is not None: - special_label = '@{}'.format(match.group(1)) + special_label = "@{}".format(match.group(1)) special_arr[special] = special_label return special_arr @@ -102,21 +96,18 @@ def extract_specials(text): def replace_specials(text, specials): for special, special_label in specials.items(): if special_label is not None: - text = text.replace( - special, - special_label - ) + text = text.replace(special, special_label) return text def extract_emojis(text): statsd = stats.get_statsd_client() - with statsd.timer('parser.extract_emojis'): + with statsd.timer("parser.extract_emojis"): # Example: :test_me: or :test-me: - emojis = re.findall(':[a-z0-9_\+\-]+:', text) + emojis = re.findall(":[a-z0-9_\+\-]+:", text) emoji_arr = {} for emoji in emojis: - match = re.match(':([a-z0-9_\+\-]+):', emoji) + match = re.match(":([a-z0-9_\+\-]+):", emoji) emoji_name = None if match.group(1) is not None: emoji_name = match.group(1) @@ -126,17 +117,17 @@ def extract_emojis(text): def extract_emails(text): statsd = stats.get_statsd_client() - with statsd.timer('parser.extract_emails'): + with statsd.timer("parser.extract_emails"): # Example: emails = re.findall( # [^>]* is non-greedy .* - ']*)(?:\|[^>]*)?>', - text + "]*)(?:\|[^>]*)?>", + text, ) email_arr = {} for email in emails: - unparsed_email = ''.format(email) - email_label = email.split('|')[0] + unparsed_email = "".format(email) + email_label = email.split("|")[0] email_arr[unparsed_email] = email_label return email_arr @@ -144,23 +135,20 @@ def extract_emails(text): def replace_emails(text, emails): for email, email_label in emails.items(): if email_label is not None: - text = text.replace( - email, - email_label - ) + text = text.replace(email, email_label) return text def extract_urls(text): statsd = stats.get_statsd_client() - with statsd.timer('parser.extract_urls'): + with statsd.timer("parser.extract_urls"): # Example: or # [^>]* is non-greedy .* - urls = re.findall('<(http[s]?://[^>]*)(?:\|[^>]*)?>', text) + urls = re.findall("<(http[s]?://[^>]*)(?:\|[^>]*)?>", text) url_arr = {} for url in urls: - unparsed_url = '<{0}>'.format(url) - url_label = url.split('|')[0] + unparsed_url = "<{0}>".format(url) + url_label = url.split("|")[0] url_arr[unparsed_url] = url_label return url_arr @@ -168,36 +156,29 @@ def extract_urls(text): def replace_urls(text, urls): for url, url_label in urls.items(): if url_label is not None: - text = text.replace( - url, - url_label - ) + text = text.replace(url, url_label) return text def extract_mentions(text, bot, channel): statsd = stats.get_statsd_client() - with statsd.timer('parser.extract_mentions'): + with statsd.timer("parser.extract_mentions"): to_me = False - at_me = '@{}'.format(bot.name) + at_me = "@{}".format(bot.name) if SPACE_REGEX.split(text)[0] == at_me: to_me = True - directed = channel.get('is_im') or to_me + directed = channel.get("is_im") or to_me return directed def extract_command(text, bot): statsd = stats.get_statsd_client() - with statsd.timer('parser.extract_command'): - at_me = '@{}'.format(bot.name) + with statsd.timer("parser.extract_command"): + at_me = "@{}".format(bot.name) if text.startswith(at_me): - command_text = text[len(at_me):].strip() + command_text = text[len(at_me) :].strip() elif at_me in text: - command_text = re.sub( - r'.*{}'.format(at_me), - '', - text - ).strip() + command_text = re.sub(r".*{}".format(at_me), "", text).strip() else: command_text = text return command_text @@ -205,41 +186,35 @@ def extract_command(text, bot): def unextract_specials(text): statsd = stats.get_statsd_client() - with statsd.timer('parser.unextract_specials'): + with statsd.timer("parser.unextract_specials"): # Example: @here - specials = re.findall('(@here|@channel)', text) + specials = re.findall("(@here|@channel)", text) for special in specials: - text = text.replace( - special, - ''.format(special[1:]) - ) + text = text.replace(special, "".format(special[1:])) return text def unextract_channels(text, bot): statsd = stats.get_statsd_client() - with statsd.timer('parser.unextract_channels'): + with statsd.timer("parser.unextract_channels"): # Example: #my-channel - _channel_labels = re.findall('(^#[\w\-_]+| #[\w\-_]+)', text) + _channel_labels = re.findall("(^#[\w\-_]+| #[\w\-_]+)", text) for label in _channel_labels: channel = slack.get_channel_by_name(bot, label.strip()) if not channel: continue text = text.replace( - '#{}'.format(channel['name']), - '<#{0}|{1}>'.format( - channel['id'], - channel['name'] - ) + "#{}".format(channel["name"]), + "<#{0}|{1}>".format(channel["id"], channel["name"]), ) return text def unextract_users(text, bot): statsd = stats.get_statsd_client() - with statsd.timer('parser.unextract_users'): + with statsd.timer("parser.unextract_users"): # Example: @my-user - _user_labels = re.findall('(^@[\w\-_]+| @[\w\-_]+)', text) + _user_labels = re.findall("(^@[\w\-_]+| @[\w\-_]+)", text) user_labels = [] for label in _user_labels: user_labels.append(label.strip()) @@ -248,10 +223,6 @@ def unextract_users(text, bot): if not user: continue text = text.replace( - label, - '<@{0}|{1}>'.format( - user['id'], - slack.get_name_from_user(user) - ) + label, "<@{0}|{1}>".format(user["id"], slack.get_name_from_user(user)) ) return text diff --git a/omnibot/services/slack/slash_command.py b/omnibot/services/slack/slash_command.py index 656997f..6ca42e3 100644 --- a/omnibot/services/slack/slash_command.py +++ b/omnibot/services/slack/slash_command.py @@ -14,174 +14,149 @@ def __init__(self, bot, command, event_trace): self._event_trace = event_trace self._command = command self._payload = {} - self._payload['omnibot_payload_type'] = 'slash_command' + self._payload["omnibot_payload_type"] = "slash_command" self._bot = bot # The bot object has data we don't want to pass to downstreams, so # in the payload, we just store specific bot data. - self._payload['bot'] = { - 'name': bot.name, - 'bot_id': bot.bot_id - } + self._payload["bot"] = {"name": bot.name, "bot_id": bot.bot_id} # For future safety sake, we'll do the same for the team. - self._payload['team'] = { - 'name': bot.team.name, - 'team_id': bot.team.team_id - } - self._payload['enterprise_id'] = self._command.get('enterprise_id') - self._payload['enterprise_name'] = self._command.get('enterprise_name') - self._payload['command'] = self._command['command'] - self._payload['response_url'] = self._command['response_url'] - self._payload['trigger_id'] = self._command['trigger_id'] - self._payload['user_id'] = self._command.get('user_id') + self._payload["team"] = {"name": bot.team.name, "team_id": bot.team.team_id} + self._payload["enterprise_id"] = self._command.get("enterprise_id") + self._payload["enterprise_name"] = self._command.get("enterprise_name") + self._payload["command"] = self._command["command"] + self._payload["response_url"] = self._command["response_url"] + self._payload["trigger_id"] = self._command["trigger_id"] + self._payload["user_id"] = self._command.get("user_id") if self.user_id: - self._payload['parsed_user'] = slack.get_user( - self.bot, - self.user_id - ) + self._payload["parsed_user"] = slack.get_user(self.bot, self.user_id) else: - self._payload['parsed_user'] = None + self._payload["parsed_user"] = None try: - self._payload['text'] = command['text'] + self._payload["text"] = command["text"] except Exception: logger.error( - 'Slash command is missing text attribute.', - extra=self.event_trace + "Slash command is missing text attribute.", extra=self.event_trace ) raise - self._payload['parsed_text'] = self.text - self._payload['channel_id'] = command['channel_id'] - self._event_trace['channel_id'] = self.channel_id - self._payload['channel'] = slack.get_channel( - self.bot, - self.channel_id - ) + self._payload["parsed_text"] = self.text + self._payload["channel_id"] = command["channel_id"] + self._event_trace["channel_id"] = self.channel_id + self._payload["channel"] = slack.get_channel(self.bot, self.channel_id) if not self.channel: logger.error( - 'Failed to fetch channel from channel_id.', - extra=self.event_trace + "Failed to fetch channel from channel_id.", extra=self.event_trace ) self._parse_payload() def _parse_payload(self): try: - self._payload['users'] = parser.extract_users(self.text, self.bot) - self._payload['parsed_text'] = parser.replace_users( - self.parsed_text, - self.users + self._payload["users"] = parser.extract_users(self.text, self.bot) + self._payload["parsed_text"] = parser.replace_users( + self.parsed_text, self.users ) except Exception: logger.exception( - 'Failed to extract user info from text.', + "Failed to extract user info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['channels'] = parser.extract_channels( - self.text, - self.bot - ) - self._payload['parsed_text'] = parser.replace_channels( - self.parsed_text, - self.channels + self._payload["channels"] = parser.extract_channels(self.text, self.bot) + self._payload["parsed_text"] = parser.replace_channels( + self.parsed_text, self.channels ) except Exception: logger.exception( - 'Failed to extract channel info from text.', + "Failed to extract channel info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['subteams'] = parser.extract_subteams( - self.text, - self.bot - ) + self._payload["subteams"] = parser.extract_subteams(self.text, self.bot) except Exception: logger.exception( - 'Failed to extract subteam info from text.', + "Failed to extract subteam info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['specials'] = parser.extract_specials(self.text) - self._payload['parsed_text'] = parser.replace_specials( - self.parsed_text, - self.specials + self._payload["specials"] = parser.extract_specials(self.text) + self._payload["parsed_text"] = parser.replace_specials( + self.parsed_text, self.specials ) except Exception: logger.exception( - 'Failed to extract special info from text.', + "Failed to extract special info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['emojis'] = parser.extract_emojis(self.text) + self._payload["emojis"] = parser.extract_emojis(self.text) except Exception: logger.exception( - 'Failed to extract emoji info from text.', + "Failed to extract emoji info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['emails'] = parser.extract_emails(self.text) - self._payload['parsed_text'] = parser.replace_emails( - self.parsed_text, - self.emails + self._payload["emails"] = parser.extract_emails(self.text) + self._payload["parsed_text"] = parser.replace_emails( + self.parsed_text, self.emails ) except Exception: logger.exception( - 'Failed to extract email info from text.', + "Failed to extract email info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) try: - self._payload['urls'] = parser.extract_urls(self.text) - self._payload['parsed_text'] = parser.replace_urls( - self.parsed_text, - self.urls + self._payload["urls"] = parser.extract_urls(self.text) + self._payload["parsed_text"] = parser.replace_urls( + self.parsed_text, self.urls ) except Exception: logger.exception( - 'Failed to extract url info from text.', + "Failed to extract url info from text.", exc_info=True, - extra=self.event_trace + extra=self.event_trace, ) @property def command(self): - return self._payload['command'] + return self._payload["command"] @property def response_url(self): - return self._payload['response_url'] + return self._payload["response_url"] @property def text(self): - return self._payload['text'] + return self._payload["text"] @property def parsed_text(self): - return self._payload['parsed_text'] + return self._payload["parsed_text"] @property def channel_id(self): - return self._payload.get('channel_id') + return self._payload.get("channel_id") @property def channel(self): - return self._payload.get('channel', {}) + return self._payload.get("channel", {}) @property def user_id(self): - return self._payload['user_id'] + return self._payload["user_id"] @property def trigger_id(self): - return self._payload['trigger_id'] + return self._payload["trigger_id"] @property def team(self): - return self._payload['team'] + return self._payload["team"] @property def bot(self): @@ -198,35 +173,35 @@ def bot_id(self): The bot_id associated with the message, if the message if from a bot. If this message isn't from a bot, this will return None. """ - return self._command.get('bot_id') + return self._command.get("bot_id") @property def enterprise_id(self): - return self._payload['enterprise_id'] + return self._payload["enterprise_id"] @property def enterprise_name(self): - return self._payload['enterprise_name'] + return self._payload["enterprise_name"] @property def channels(self): - return self._payload.get('channels', {}) + return self._payload.get("channels", {}) @property def users(self): - return self._payload.get('users', {}) + return self._payload.get("users", {}) @property def specials(self): - return self._payload.get('specials', {}) + return self._payload.get("specials", {}) @property def emails(self): - return self._payload.get('emails', {}) + return self._payload.get("emails", {}) @property def urls(self): - return self._payload.get('urls', {}) + return self._payload.get("urls", {}) @property def payload(self): diff --git a/omnibot/services/slack/team.py b/omnibot/services/slack/team.py index fd025c6..3bf28b5 100644 --- a/omnibot/services/slack/team.py +++ b/omnibot/services/slack/team.py @@ -22,7 +22,7 @@ def __hash__(self): def get_team_by_name(cls, name): team_id = settings.SLACK_TEAMS.get(name) if not team_id: - raise TeamInitializationError('Invalid team') + raise TeamInitializationError("Invalid team") return cls(name, team_id) @classmethod @@ -33,7 +33,7 @@ def get_team_by_id(cls, team_id): name = team_name break if not name: - raise TeamInitializationError('Invalid team') + raise TeamInitializationError("Invalid team") return cls(name, team_id) @property @@ -47,8 +47,8 @@ def team_id(self): @property def logging_context(self): return { - 'team': self.name, - 'team_id': self.team_id, + "team": self.name, + "team_id": self.team_id, } diff --git a/omnibot/services/sqs.py b/omnibot/services/sqs.py index 4275fb9..eaf9e22 100644 --- a/omnibot/services/sqs.py +++ b/omnibot/services/sqs.py @@ -12,14 +12,13 @@ def get_client(): if settings.SQS_URL: return omnibot.services.get_boto_client( - 'sqs', + "sqs", endpoint_url=settings.SQS_URL, - config={'name': 'keymanager', 'config': BOTOCORE_CONFIG} + config={"name": "keymanager", "config": BOTOCORE_CONFIG}, ) else: return omnibot.services.get_boto_client( - 'sqs', - config={'name': 'keymanager', 'config': BOTOCORE_CONFIG} + "sqs", config={"name": "keymanager", "config": BOTOCORE_CONFIG} ) @@ -28,8 +27,6 @@ def get_queue_url(): if QUEUE_URL is None: client = get_client() - QUEUE_URL = client.get_queue_url( - QueueName=settings.SQS_QUEUE_NAME - )['QueueUrl'] + QUEUE_URL = client.get_queue_url(QueueName=settings.SQS_QUEUE_NAME)["QueueUrl"] return QUEUE_URL diff --git a/omnibot/services/stats.py b/omnibot/services/stats.py index a134657..66acad0 100644 --- a/omnibot/services/stats.py +++ b/omnibot/services/stats.py @@ -9,8 +9,6 @@ def get_statsd_client(): global STATS_CLIENT if STATS_CLIENT is None: STATS_CLIENT = statsd.StatsClient( - settings.STATSD_HOST, - settings.STATSD_PORT, - prefix=settings.STATSD_PREFIX + settings.STATSD_HOST, settings.STATSD_PORT, prefix=settings.STATSD_PREFIX ) return STATS_CLIENT diff --git a/omnibot/settings.py b/omnibot/settings.py index b4e63d2..5512bcd 100644 --- a/omnibot/settings.py +++ b/omnibot/settings.py @@ -9,119 +9,106 @@ # Log config file used from any direct main entrypoint. You should # use gunicorn config for the web worker. -LOG_CONFIG_FILE = str_env('LOG_CONFIG_FILE', '/etc/omnibot/logging.conf') -LOG_MODULE = str_env('LOG_MODULE', 'logging') +LOG_CONFIG_FILE = str_env("LOG_CONFIG_FILE", "/etc/omnibot/logging.conf") +LOG_MODULE = str_env("LOG_MODULE", "logging") # Flask-related settings -DEBUG = bool_env('DEBUG', False) +DEBUG = bool_env("DEBUG", False) # Config used for running wsgi directly, without gunicorn -HOST = str_env('HOST', '127.0.0.1') -PORT = int_env('PORT', 80) +HOST = str_env("HOST", "127.0.0.1") +PORT = int_env("PORT", 80) # A statsd host -STATSD_HOST = str_env('STATSD_HOST', 'localhost') +STATSD_HOST = str_env("STATSD_HOST", "localhost") # A statsd port -STATSD_PORT = int_env('STATSD_PORT', 8125) +STATSD_PORT = int_env("STATSD_PORT", 8125) # A statsd prefix for metrics -STATSD_PREFIX = str_env('STATSD_PREFIX', 'omnibot') +STATSD_PREFIX = str_env("STATSD_PREFIX", "omnibot") -EXIT_ON_BAD_CONFIG = bool_env('EXIT_ON_BAD_CONFIG', True) -CONFIG_FILE = str_env('CONFIG_FILE', '/etc/omnibot/omnibot.conf') +EXIT_ON_BAD_CONFIG = bool_env("EXIT_ON_BAD_CONFIG", True) +CONFIG_FILE = str_env("CONFIG_FILE", "/etc/omnibot/omnibot.conf") try: with open(CONFIG_FILE) as _config_file: _config = yaml.safe_load(_config_file) except Exception: if EXIT_ON_BAD_CONFIG: raise - logger.error(f'Failed to load configuration file: {CONFIG_FILE}') + logger.error(f"Failed to load configuration file: {CONFIG_FILE}") _config = {} # authnz checks, permissions and bindings -AUTHORIZATION = _config.get('authorization', {}) +AUTHORIZATION = _config.get("authorization", {}) # Slack bot data -PRIMARY_SLACK_BOT = _config.get('primary_bot', {}) +PRIMARY_SLACK_BOT = _config.get("primary_bot", {}) if not PRIMARY_SLACK_BOT: - logger.warning('primary_bot not set in configuration; watcher will not be able to refresh caches') -SLACK_TEAMS = _config.get('teams', {}) + logger.warning( + "primary_bot not set in configuration; watcher will not be able to refresh caches" + ) +SLACK_TEAMS = _config.get("teams", {}) if not SLACK_TEAMS: - message = 'teams not set in configuration; omnibot has no functionality without teams defined' + message = "teams not set in configuration; omnibot has no functionality without teams defined" if EXIT_ON_BAD_CONFIG: raise Exception(message) logger.warning(message) SLACK_BOT_TOKENS = {} -for team, bots in _config.get('bots', {}).items(): +for team, bots in _config.get("bots", {}).items(): SLACK_BOT_TOKENS[team] = {} for bot_name, bot_id in bots.items(): _v_token = str_env( - 'CREDENTIALS_SLACK_VERIFICATION_TOKEN_{}'.format(bot_id.upper()) - ) - _o_token = str_env( - 'CREDENTIALS_SLACK_OAUTH_TOKEN_{}'.format(bot_id.upper()) + "CREDENTIALS_SLACK_VERIFICATION_TOKEN_{}".format(bot_id.upper()) ) + _o_token = str_env("CREDENTIALS_SLACK_OAUTH_TOKEN_{}".format(bot_id.upper())) _o_bot_token = str_env( - 'CREDENTIALS_SLACK_OAUTH_BOT_TOKEN_{}'.format(bot_id.upper()) + "CREDENTIALS_SLACK_OAUTH_BOT_TOKEN_{}".format(bot_id.upper()) ) # We require a verification token, and require some type of # oauth token. if not _v_token: - logger.warning( - '{} bot missing verification token.'.format(bot_name) - ) + logger.warning("{} bot missing verification token.".format(bot_name)) continue if not _o_token and not _o_bot_token: - logger.warning( - '{} bot missing oauth tokens.'.format(bot_name) - ) + logger.warning("{} bot missing oauth tokens.".format(bot_name)) continue SLACK_BOT_TOKENS[team][bot_name] = {} - SLACK_BOT_TOKENS[team][bot_name]['verification_token'] = _v_token - SLACK_BOT_TOKENS[team][bot_name]['oauth_user_token'] = _o_token - SLACK_BOT_TOKENS[team][bot_name]['oauth_bot_token'] = _o_bot_token - SLACK_BOT_TOKENS[team][bot_name]['app_id'] = bot_id - -HANDLERS = _config.get('handlers') -HELP_CALLBACK = _config.get('help_callback') -DEFAULT_TO_HELP = _config.get('default_to_help', True) -LIST_PROVIDER_UPDATE_FREQUENCY = _config.get( - 'list_provider_update_frequency', - 120 -) -WATCHER_SPAWN_WAIT_TIME_IN_SEC = _config.get( - 'watcher_spawn_wait_time_in_sec', - 5 -) + SLACK_BOT_TOKENS[team][bot_name]["verification_token"] = _v_token + SLACK_BOT_TOKENS[team][bot_name]["oauth_user_token"] = _o_token + SLACK_BOT_TOKENS[team][bot_name]["oauth_bot_token"] = _o_bot_token + SLACK_BOT_TOKENS[team][bot_name]["app_id"] = bot_id + +HANDLERS = _config.get("handlers") +HELP_CALLBACK = _config.get("help_callback") +DEFAULT_TO_HELP = _config.get("default_to_help", True) +LIST_PROVIDER_UPDATE_FREQUENCY = _config.get("list_provider_update_frequency", 120) +WATCHER_SPAWN_WAIT_TIME_IN_SEC = _config.get("watcher_spawn_wait_time_in_sec", 5) # The SQS URL # Example: http://localhost -SQS_URL = str_env('SQS_URL') +SQS_URL = str_env("SQS_URL") # SQS queue name for enqueuing webhooks -SQS_QUEUE_NAME = str_env('SQS_QUEUE_NAME') +SQS_QUEUE_NAME = str_env("SQS_QUEUE_NAME") # gevent pool concurrency for handling enqueued webhooks -WEBHOOK_WORKER_CONCURRENCY = int_env('WEBHOOK_WORKER_CONCURRENCY', 10) +WEBHOOK_WORKER_CONCURRENCY = int_env("WEBHOOK_WORKER_CONCURRENCY", 10) # Match the sqs batch size to the webhook concurrency size, up to the sqs # maximum of 10. if WEBHOOK_WORKER_CONCURRENCY <= 10: _SQS_BATCH_SIZE_DEFAULT = WEBHOOK_WORKER_CONCURRENCY else: _SQS_BATCH_SIZE_DEFAULT = 10 -SQS_BATCH_SIZE = int_env( - 'SQS_BATCH_SIZE', - _SQS_BATCH_SIZE_DEFAULT -) +SQS_BATCH_SIZE = int_env("SQS_BATCH_SIZE", _SQS_BATCH_SIZE_DEFAULT) SQS_MAX_POOL_CONNECTIONS = int_env( - 'SQS_MAX_POOL_CONNECTIONS', + "SQS_MAX_POOL_CONNECTIONS", # Default to the webhook worker size, or 10, if that's lower. - max(10, WEBHOOK_WORKER_CONCURRENCY) + max(10, WEBHOOK_WORKER_CONCURRENCY), ) -SQS_VISIBILITY_TIMEOUT = int_env('SQS_VISIBILITY_TIMEOUT', 60) -SQS_WAIT_TIME_SECONDS = int_env('SQS_WAIT_TIME_SECONDS', 1) +SQS_VISIBILITY_TIMEOUT = int_env("SQS_VISIBILITY_TIMEOUT", 60) +SQS_WAIT_TIME_SECONDS = int_env("SQS_WAIT_TIME_SECONDS", 1) # Redis settings -REDIS_PORT = int_env('REDIS_PORT', 6379) -REDIS_HOST = str_env('REDIS_HOST', 'localhost') +REDIS_PORT = int_env("REDIS_PORT", 6379) +REDIS_HOST = str_env("REDIS_HOST", "localhost") def get(name, default=None): diff --git a/omnibot/setup_logging.py b/omnibot/setup_logging.py index 0e8eded..b548ff3 100644 --- a/omnibot/setup_logging.py +++ b/omnibot/setup_logging.py @@ -11,15 +11,15 @@ try: with open(settings.LOG_CONFIG_FILE, "r") as fd: - logger.info('Configuring logger from file') + logger.info("Configuring logger from file") logconfig = yaml.safe_load(os.path.expandvars(fd.read())) logging.config.dictConfig(logconfig) logger = logging.getLogger(__name__) except FileNotFoundError: logger.warning( - f'{settings.LOG_CONFIG_FILE} not found; skipping logging configuration' + f"{settings.LOG_CONFIG_FILE} not found; skipping logging configuration" ) except Exception: logger.exception( - f'Failed to load {settings.LOG_CONFIG_FILE}; skipping logging configuration' + f"Failed to load {settings.LOG_CONFIG_FILE}; skipping logging configuration" ) diff --git a/omnibot/utils/__init__.py b/omnibot/utils/__init__.py index dbbea04..bdbd2a6 100644 --- a/omnibot/utils/__init__.py +++ b/omnibot/utils/__init__.py @@ -1,26 +1,26 @@ def get_callback_id(component): - ''' + """ Retrieves the callback ID of the given interactive component. In the case the component corresponds to Slack's newer block components, return its block ID instead, but treat it as though it were the callback ID to minimize code change as the two function similarly. - ''' - if component.get('type') == 'block_actions': - actions = component.get('actions', []) + """ + if component.get("type") == "block_actions": + actions = component.get("actions", []) action = next(iter(actions)) - return action.get('block_id') - elif component.get('type') == 'view_submission': - view = component.get('view', {}) - return view.get('callback_id') + return action.get("block_id") + elif component.get("type") == "view_submission": + view = component.get("view", {}) + return view.get("callback_id") else: - return component.get('callback_id') + return component.get("callback_id") def merge_logging_context(*args): - ''' + """ Merge return a merged dict of the logging context dicts passed in. - ''' + """ ret = {} for arg in args: ret.update(arg) diff --git a/omnibot/utils/settings.py b/omnibot/utils/settings.py index 477b4ff..dc9bbb2 100644 --- a/omnibot/utils/settings.py +++ b/omnibot/utils/settings.py @@ -27,7 +27,7 @@ def bool_env(var_name, default=False): test_val = getenv(var_name, default) # Explicitly check for 'False', 'false', and '0' since all non-empty # string are normally coerced to True. - if test_val in ('False', 'false', '0'): + if test_val in ("False", "false", "0"): return False return bool(test_val) @@ -50,7 +50,7 @@ def int_env(var_name, default=0): return int(getenv(var_name, default)) -def str_env(var_name, default=''): +def str_env(var_name, default=""): """ Get an environment variable as a string. This has the same arguments as bool_env. diff --git a/omnibot/watcher.py b/omnibot/watcher.py index 323a6c9..7faf4f6 100644 --- a/omnibot/watcher.py +++ b/omnibot/watcher.py @@ -1,29 +1,25 @@ import gevent import gevent.monkey + gevent.monkey.patch_all() -import gevent.pool # noqa:E402 -import signal # noqa:E402 -import json # noqa:E402 -import time # noqa:E402 -import dateutil.parser # noqa:E402 -from datetime import ( - datetime, - timedelta -) # noqa:E402 - -import redis_lock # noqa:E402 - -from omnibot import logging # noqa:E402 -from omnibot import settings # noqa:E402 -from omnibot.services import omniredis # noqa:E402 -from omnibot.services import stats # noqa:E402 -from omnibot.services import slack # noqa:E402 -from omnibot.services.slack.team import Team # noqa:E402 -from omnibot.services.slack.bot import Bot # noqa:E402 - -STATE = { - 'shutdown': False -} +import gevent.pool # noqa:E402 +import signal # noqa:E402 +import json # noqa:E402 +import time # noqa:E402 +import dateutil.parser # noqa:E402 +from datetime import datetime, timedelta # noqa:E402 + +import redis_lock # noqa:E402 + +from omnibot import logging # noqa:E402 +from omnibot import settings # noqa:E402 +from omnibot.services import omniredis # noqa:E402 +from omnibot.services import stats # noqa:E402 +from omnibot.services import slack # noqa:E402 +from omnibot.services.slack.team import Team # noqa:E402 +from omnibot.services.slack.bot import Bot # noqa:E402 + +STATE = {"shutdown": False} LOCK_EXPIRATION = 120 @@ -36,7 +32,8 @@ def bootstrap(): def finalizer(signal, frame): logger.info("SIGTERM caught, shutting down") - STATE['shutdown'] = True + STATE["shutdown"] = True + signal.signal(signal.SIGTERM, finalizer) @@ -47,7 +44,9 @@ def _is_allowed_to_run(redis_client, key): last_run_datetime = dateutil.parser.parse(last_run_datetime_str) current_datetime = datetime.now() - return current_datetime > last_run_datetime + timedelta(seconds=settings.LIST_PROVIDER_UPDATE_FREQUENCY) + return current_datetime > last_run_datetime + timedelta( + seconds=settings.LIST_PROVIDER_UPDATE_FREQUENCY + ) def watch_users(): @@ -59,33 +58,28 @@ def watch_users(): statsd = stats.get_statsd_client() with redis_lock.Lock( - redis_client, - # we prepend an elasticache {hash_key} - # to this and other redis locks - # so that they can function in clustered mode - '{watch_users}:watch_users', - expire=LOCK_EXPIRATION, - auto_renewal=True): - with statsd.timer('watch.users'): + redis_client, + # we prepend an elasticache {hash_key} + # to this and other redis locks + # so that they can function in clustered mode + "{watch_users}:watch_users", + expire=LOCK_EXPIRATION, + auto_renewal=True, + ): + with statsd.timer("watch.users"): for team_name, bot_name in settings.PRIMARY_SLACK_BOT.items(): logger.info( - 'Updating slack user list.', - extra={'team': team_name, 'bot': bot_name}, + "Updating slack user list.", + extra={"team": team_name, "bot": bot_name}, ) team = Team.get_team_by_name(team_name) bot = Bot.get_bot_by_name(team, bot_name) slack.update_users(bot, team) redis_client.set(last_run_key, datetime.now().isoformat()) except Exception: - logger.exception( - 'Failed to update slack user list.', - exc_info=True - ) + logger.exception("Failed to update slack user list.", exc_info=True) finally: - return gevent.spawn_later( - settings.WATCHER_SPAWN_WAIT_TIME_IN_SEC, - watch_users - ) + return gevent.spawn_later(settings.WATCHER_SPAWN_WAIT_TIME_IN_SEC, watch_users) def watch_conversations(): @@ -97,15 +91,16 @@ def watch_conversations(): statsd = stats.get_statsd_client() with redis_lock.Lock( - redis_client, - '{watch_conversation}:watch_conversation', - expire=LOCK_EXPIRATION, - auto_renewal=True): - with statsd.timer('watch.conversation'): + redis_client, + "{watch_conversation}:watch_conversation", + expire=LOCK_EXPIRATION, + auto_renewal=True, + ): + with statsd.timer("watch.conversation"): for team_name, bot_name in settings.PRIMARY_SLACK_BOT.items(): logger.info( - 'Updating slack conversations list.', - extra={'team': team_name, 'bot': bot_name}, + "Updating slack conversations list.", + extra={"team": team_name, "bot": bot_name}, ) team = Team.get_team_by_name(team_name) bot = Bot.get_bot_by_name(team, bot_name) @@ -113,7 +108,7 @@ def watch_conversations(): redis_client.set(last_run_key, datetime.now().isoformat()) except Exception: logger.exception( - 'Failed to update slack conversations list.', + "Failed to update slack conversations list.", exc_info=True, ) finally: @@ -132,15 +127,16 @@ def watch_emoji(): statsd = stats.get_statsd_client() with redis_lock.Lock( - redis_client, - '{watch_emoji}:watch_emoji', - expire=LOCK_EXPIRATION, - auto_renewal=True): - with statsd.timer('watch.emoji'): + redis_client, + "{watch_emoji}:watch_emoji", + expire=LOCK_EXPIRATION, + auto_renewal=True, + ): + with statsd.timer("watch.emoji"): for team_name, bot_name in settings.PRIMARY_SLACK_BOT.items(): logger.info( - 'Updating slack emoji map.', - extra={'team': team_name, 'bot': bot_name}, + "Updating slack emoji map.", + extra={"team": team_name, "bot": bot_name}, ) team = Team.get_team_by_name(team_name) bot = Bot.get_bot_by_name(team, bot_name) @@ -148,7 +144,7 @@ def watch_emoji(): redis_client.set(last_run_key, datetime.now().isoformat()) except Exception: logger.exception( - 'Failed to update slack emoji list.', + "Failed to update slack emoji list.", exc_info=True, ) finally: @@ -160,11 +156,12 @@ def watch_emoji(): def main(): bootstrap() - while not STATE['shutdown']: + while not STATE["shutdown"]: gevent.sleep(1) if __name__ == "__main__": from omnibot import setup_logging # noqa:F401 + logger = logging.getLogger(__name__) main() diff --git a/omnibot/webhook_worker.py b/omnibot/webhook_worker.py index 9ffdca3..acfe628 100644 --- a/omnibot/webhook_worker.py +++ b/omnibot/webhook_worker.py @@ -1,87 +1,76 @@ import gevent import gevent.monkey + gevent.monkey.patch_all(thread=False) -import gevent.pool # noqa:E402 -import signal # noqa:E402 -import json # noqa:E402 -import time # noqa:E402 +import gevent.pool # noqa:E402 +import signal # noqa:E402 +import json # noqa:E402 +import time # noqa:E402 -import botocore # noqa:E402 +import botocore # noqa:E402 -from omnibot import logging # noqa:E402 -from omnibot import settings # noqa:E402 -from omnibot import processor # noqa:E402 -from omnibot.services import stats # noqa:E402 -from omnibot.services import slack # noqa:E402 -from omnibot.services import sqs # noqa:E402 -from omnibot.services.slack.team import Team # noqa:E402 -from omnibot.services.slack.bot import Bot # noqa:E402 +from omnibot import logging # noqa:E402 +from omnibot import settings # noqa:E402 +from omnibot import processor # noqa:E402 +from omnibot.services import stats # noqa:E402 +from omnibot.services import slack # noqa:E402 +from omnibot.services import sqs # noqa:E402 +from omnibot.services.slack.team import Team # noqa:E402 +from omnibot.services.slack.bot import Bot # noqa:E402 -STATE = { - 'shutdown': False -} +STATE = {"shutdown": False} def wait_available(pool, pool_name): statsd = stats.get_statsd_client() if pool.full(): - statsd.incr('%s.pool.full' % pool_name) + statsd.incr("%s.pool.full" % pool_name) pool.wait_available() def delete_message(client, queue_url, message): - client.delete_message( - QueueUrl=queue_url, - ReceiptHandle=message['ReceiptHandle'] - ) + client.delete_message(QueueUrl=queue_url, ReceiptHandle=message["ReceiptHandle"]) def _instrument_message_latency(event): statsd = stats.get_statsd_client() - event_sent_time_ms = int(float(event['event_ts']) * 1000) + event_sent_time_ms = int(float(event["event_ts"]) * 1000) now = int(time.time() * 1000) - statsd.timing('delivery_latency', now - event_sent_time_ms) + statsd.timing("delivery_latency", now - event_sent_time_ms) def handle_message(client, queue_url, message): statsd = stats.get_statsd_client() - with statsd.timer('handle_message'): - attrs = message['MessageAttributes'] - if 'type' not in attrs: - logger.error('SQS message does not have a type attribute.') + with statsd.timer("handle_message"): + attrs = message["MessageAttributes"] + if "type" not in attrs: + logger.error("SQS message does not have a type attribute.") delete_message(client, queue_url, message) return - m_type = attrs['type']['StringValue'] - if m_type not in ['event', 'slash_command', 'interactive_component']: + m_type = attrs["type"]["StringValue"] + if m_type not in ["event", "slash_command", "interactive_component"]: delete_message(client, queue_url, message) - logger.error( - '{} is an unsupported message type.'.format(m_type) - ) + logger.error("{} is an unsupported message type.".format(m_type)) return - if 'version' not in attrs: + if "version" not in attrs: version = 1 else: - version = int(attrs['version']['StringValue']) - logger.debug('Received SQS message of type {}'.format(m_type)) + version = int(attrs["version"]["StringValue"]) + logger.debug("Received SQS message of type {}".format(m_type)) try: if version == 2: - event = json.loads(message['Body'])['event'] - if m_type == 'event': - _instrument_message_latency(event['event']) + event = json.loads(message["Body"])["event"] + if m_type == "event": + _instrument_message_latency(event["event"]) processor.process_event(event) - elif m_type == 'slash_command': + elif m_type == "slash_command": processor.process_slash_command(event) - elif m_type == 'interactive_component': + elif m_type == "interactive_component": processor.process_interactive_component(event) else: - logger.error( - '{} is an unsupported message version.'.format(version) - ) + logger.error("{} is an unsupported message version.".format(version)) except Exception: - logger.exception( - 'Failed to handle webhook SQS message', - exc_info=True - ) + logger.exception("Failed to handle webhook SQS message", exc_info=True) return delete_message(client, queue_url, message) @@ -90,33 +79,28 @@ def handle_messages(client, queue_url, queue_pool): global STATE statsd = stats.get_statsd_client() - while not STATE['shutdown']: + while not STATE["shutdown"]: try: response = client.receive_message( QueueUrl=queue_url, - AttributeNames=['SentTimestamp'], + AttributeNames=["SentTimestamp"], MaxNumberOfMessages=settings.SQS_BATCH_SIZE, - MessageAttributeNames=['All'], + MessageAttributeNames=["All"], VisibilityTimeout=settings.SQS_VISIBILITY_TIMEOUT, - WaitTimeSeconds=settings.SQS_WAIT_TIME_SECONDS + WaitTimeSeconds=settings.SQS_WAIT_TIME_SECONDS, ) - if 'Messages' in response: - statsd.incr('sqs.received', len(response['Messages'])) - for message in response['Messages']: - with statsd.timer('webhookpool.spawn'): - wait_available(queue_pool, 'webhookpool') - queue_pool.spawn( - handle_message, - client, - queue_url, - message - ) + if "Messages" in response: + statsd.incr("sqs.received", len(response["Messages"])) + for message in response["Messages"]: + with statsd.timer("webhookpool.spawn"): + wait_available(queue_pool, "webhookpool") + queue_pool.spawn(handle_message, client, queue_url, message) else: - logger.debug('No messages, continuing') + logger.debug("No messages, continuing") except botocore.parsers.ResponseParserError: - logger.warning('Got a bad response from SQS, continuing.') + logger.warning("Got a bad response from SQS, continuing.") except Exception: - logger.exception('General error', exc_info=True) + logger.exception("General error", exc_info=True) def main(): @@ -128,5 +112,6 @@ def main(): if __name__ == "__main__": from omnibot import setup_logging # noqa:F401 + logger = logging.getLogger(__name__) main() diff --git a/omnibot/wsgi.py b/omnibot/wsgi.py index 10a84af..e9b3af3 100644 --- a/omnibot/wsgi.py +++ b/omnibot/wsgi.py @@ -4,12 +4,15 @@ from omnibot import settings from omnibot.routes import api + app.register_blueprint(api.blueprint) -if __name__ == '__main__': +if __name__ == "__main__": from omnibot import setup_logging # noqa:F401 + app.run( - host=settings.get('HOST', '0.0.0.0'), - port=settings.get('PORT', 5000), - debug=settings.get('DEBUG', True)) + host=settings.get("HOST", "0.0.0.0"), + port=settings.get("PORT", 5000), + debug=settings.get("DEBUG", True), + ) diff --git a/setup.py b/setup.py index 5a101ab..68d9282 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ from setuptools import setup, find_packages -with open('requirements.in') as f: +with open("requirements.in") as f: REQUIREMENTS = f.read().splitlines() -with open('VERSION') as f: +with open("VERSION") as f: VERSION = f.read() setup( diff --git a/tests/data/__init__.py b/tests/data/__init__.py index 8b8770b..cbc1b81 100644 --- a/tests/data/__init__.py +++ b/tests/data/__init__.py @@ -12,4 +12,4 @@ def get_mock_data(filepath: str): def test_handler(container): - return "{\"foo\": \"bar\"}" + return '{"foo": "bar"}' diff --git a/tests/integration/routes/test_interactive.py b/tests/integration/routes/test_interactive.py index a748f2b..dbe0155 100644 --- a/tests/integration/routes/test_interactive.py +++ b/tests/integration/routes/test_interactive.py @@ -176,7 +176,9 @@ def test_view_submission_synchronous( client: Client, queue: MagicMock, slack_api_call: MagicMock, mocker ): mocker.patch("omnibot.services.slack.get_user", return_value={"id": "TEST_USER_ID"}) - with get_mock_data("interactive/view_submission_synchronous_test.json") as json_data: + with get_mock_data( + "interactive/view_submission_synchronous_test.json" + ) as json_data: event: Dict[str, Any] = json.loads(json_data.read()) resp: Response = client.post( _ENDPOINT, @@ -187,4 +189,4 @@ def test_view_submission_synchronous( component["omnibot_bot_id"] = "TEST_OMNIBOT_ID" queue.assert_not_called() assert resp.status_code == 200 - assert resp.data == b"{\"foo\": \"bar\"}" + assert resp.data == b'{"foo": "bar"}' diff --git a/tests/unit/omnibot/authnz/authnz_test.py b/tests/unit/omnibot/authnz/authnz_test.py index c064218..deda37b 100644 --- a/tests/unit/omnibot/authnz/authnz_test.py +++ b/tests/unit/omnibot/authnz/authnz_test.py @@ -10,16 +10,14 @@ def test_enforce_checks(mocker): def some_test_function(): return True - allowed_paths_check = mocker.patch( - 'omnibot.authnz.allowed_paths' - ) + allowed_paths_check = mocker.patch("omnibot.authnz.allowed_paths") allowed_paths_check.return_value = True envoy_internal_check = mocker.patch( - 'omnibot.authnz.envoy_checks.envoy_internal_check' + "omnibot.authnz.envoy_checks.envoy_internal_check" ) envoy_internal_check.return_value = True envoy_permissions_check = mocker.patch( - 'omnibot.authnz.envoy_checks.envoy_permissions_check' + "omnibot.authnz.envoy_checks.envoy_permissions_check" ) envoy_permissions_check.return_value = True @@ -49,27 +47,23 @@ def some_test_function(): def test_allowed_paths(mocker): paths = [ - '/api/v1/slack/event', - '/api/v1/slack/get_team/testteam', - '/api/v2/slack/action/ateam/abot' + "/api/v1/slack/event", + "/api/v1/slack/get_team/testteam", + "/api/v2/slack/action/ateam/abot", ] # Test a basic post route - with app.test_request_context( - path='/api/v1/slack/event', - method='POST'): + with app.test_request_context(path="/api/v1/slack/event", method="POST"): result = authnz.allowed_paths(paths) assert result is True # Test a route that uses regex - with app.test_request_context( - path='/api/v1/slack/get_team/testteam', - method='GET'): + with app.test_request_context(path="/api/v1/slack/get_team/testteam", method="GET"): result = authnz.allowed_paths(paths) assert result is True # Test a route that's not allowed with app.test_request_context( - path='/api/v2/slack/action/notateam/notabot', - method='GET'): + path="/api/v2/slack/action/notateam/notabot", method="GET" + ): result = authnz.allowed_paths(paths) assert result is False diff --git a/tests/unit/omnibot/authnz/envoy_checks_test.py b/tests/unit/omnibot/authnz/envoy_checks_test.py index 27e683d..62633fa 100644 --- a/tests/unit/omnibot/authnz/envoy_checks_test.py +++ b/tests/unit/omnibot/authnz/envoy_checks_test.py @@ -5,94 +5,98 @@ def test_envoy_internal_check(mocker): # Test an internal route with the header set with app.test_request_context( - path='/api/v1/slack/get_team/testteam', - method='GET', - headers={'x-envoy-internal': 'true'}): + path="/api/v1/slack/get_team/testteam", + method="GET", + headers={"x-envoy-internal": "true"}, + ): result = envoy_checks.envoy_internal_check() assert result is True # Test an internal route with the header set to False with app.test_request_context( - path='/api/v1/slack/get_team/testteam', - method='GET', - headers={'x-envoy-internal': 'false'}): + path="/api/v1/slack/get_team/testteam", + method="GET", + headers={"x-envoy-internal": "false"}, + ): result = envoy_checks.envoy_internal_check() assert result is False # Test an internal route with the header not set - with app.test_request_context( - path='/api/v1/slack/get_team/testteam', - method='GET'): + with app.test_request_context(path="/api/v1/slack/get_team/testteam", method="GET"): result = envoy_checks.envoy_internal_check() assert result is False # Test a route that isn't set as internal only - with app.test_request_context( - path='/api/v1/slack/event', - method='POST'): + with app.test_request_context(path="/api/v1/slack/event", method="POST"): result = envoy_checks.envoy_internal_check() assert result is True def test_envoy_permissions_check(mocker): with app.test_request_context( - path='/api/v1/slack/event', - method='POST', - headers={'x-envoy-downstream-service-cluster': 'envoy'}): + path="/api/v1/slack/event", + method="POST", + headers={"x-envoy-downstream-service-cluster": "envoy"}, + ): result = envoy_checks.envoy_permissions_check() assert result is True with app.test_request_context( - path='/api/v1/slack/event', - method='POST', - headers={'x-envoy-downstream-service-cluster': 'notasubject'}): + path="/api/v1/slack/event", + method="POST", + headers={"x-envoy-downstream-service-cluster": "notasubject"}, + ): result = envoy_checks.envoy_permissions_check() assert result is False - with app.test_request_context( - path='/api/v1/slack/event', - method='POST'): + with app.test_request_context(path="/api/v1/slack/event", method="POST"): result = envoy_checks.envoy_permissions_check() assert result is False with app.test_request_context( - path='/api/v1/slack/get_user/testteam/echobot/test@example.com', - method='GET', - headers={'x-envoy-downstream-service-cluster': 'someservice'}): + path="/api/v1/slack/get_user/testteam/echobot/test@example.com", + method="GET", + headers={"x-envoy-downstream-service-cluster": "someservice"}, + ): result = envoy_checks.envoy_permissions_check() assert result is True with app.test_request_context( - path='/api/v1/slack/get_user/testteam/unauthbot/test@example.com', - method='GET', - headers={'x-envoy-downstream-service-cluster': 'someservice'}): + path="/api/v1/slack/get_user/testteam/unauthbot/test@example.com", + method="GET", + headers={"x-envoy-downstream-service-cluster": "someservice"}, + ): result = envoy_checks.envoy_permissions_check() assert result is False with app.test_request_context( - path='/api/v1/slack/get_user/testteam/echobot/test@example.com', - method='GET', - headers={'x-envoy-downstream-service-cluster': 'unauthservice'}): + path="/api/v1/slack/get_user/testteam/echobot/test@example.com", + method="GET", + headers={"x-envoy-downstream-service-cluster": "unauthservice"}, + ): result = envoy_checks.envoy_permissions_check() assert result is False with app.test_request_context( - path='/api/v2/slack/action/test2ndteam/pingbot', - method='POST', - headers={'x-envoy-downstream-service-cluster': 'service-test'}): + path="/api/v2/slack/action/test2ndteam/pingbot", + method="POST", + headers={"x-envoy-downstream-service-cluster": "service-test"}, + ): result = envoy_checks.envoy_permissions_check() assert result is True with app.test_request_context( - path='/api/v2/slack/action/test2ndteam/pingbot', - method='POST', - headers={'x-envoy-downstream-service-cluster': 'service'}): + path="/api/v2/slack/action/test2ndteam/pingbot", + method="POST", + headers={"x-envoy-downstream-service-cluster": "service"}, + ): result = envoy_checks.envoy_permissions_check() assert result is True with app.test_request_context( - path='/api/v2/slack/action/test2ndteam/pingbot', - method='POST', - headers={'x-envoy-downstream-service-cluster': 'unauthservice'}): + path="/api/v2/slack/action/test2ndteam/pingbot", + method="POST", + headers={"x-envoy-downstream-service-cluster": "unauthservice"}, + ): result = envoy_checks.envoy_permissions_check() assert result is False diff --git a/tests/unit/omnibot/services/slack/bot_test.py b/tests/unit/omnibot/services/slack/bot_test.py index 874d498..6d1ec26 100644 --- a/tests/unit/omnibot/services/slack/bot_test.py +++ b/tests/unit/omnibot/services/slack/bot_test.py @@ -5,35 +5,35 @@ def test_team(): - _team = Team.get_team_by_name('testteam') - _bot = Bot.get_bot_by_name(_team, 'echobot') - assert _bot.name == 'echobot' - assert _bot.bot_id == 'A12345678' + _team = Team.get_team_by_name("testteam") + _bot = Bot.get_bot_by_name(_team, "echobot") + assert _bot.name == "echobot" + assert _bot.bot_id == "A12345678" assert _bot.team == _team - assert _bot.oauth_user_token == '1234' - assert _bot.oauth_bot_token == '1234' - assert _bot.verification_token == '1234' + assert _bot.oauth_user_token == "1234" + assert _bot.oauth_bot_token == "1234" + assert _bot.verification_token == "1234" - _team = Team.get_team_by_id(team_id='TABCDEF12') - _bot = Bot.get_bot_by_bot_id(_team, 'A98765432') - assert _bot.name == 'echobot' - assert _bot.bot_id == 'A98765432' + _team = Team.get_team_by_id(team_id="TABCDEF12") + _bot = Bot.get_bot_by_bot_id(_team, "A98765432") + assert _bot.name == "echobot" + assert _bot.bot_id == "A98765432" assert _bot.team == _team - assert _bot.oauth_user_token == '1234' - assert _bot.oauth_bot_token == '' - assert _bot.verification_token == '1234' + assert _bot.oauth_user_token == "1234" + assert _bot.oauth_bot_token == "" + assert _bot.verification_token == "1234" - _team = Team.get_team_by_name('testteam') - _bot = Bot.get_bot_by_verification_token('5555') - assert _bot.name == 'pingbot' - assert _bot.bot_id == 'AABCDEF12' + _team = Team.get_team_by_name("testteam") + _bot = Bot.get_bot_by_verification_token("5555") + assert _bot.name == "pingbot" + assert _bot.bot_id == "AABCDEF12" assert _bot.team == _team - assert _bot.oauth_user_token == '5555' - assert _bot.oauth_bot_token == '5555' - assert _bot.verification_token == '5555' + assert _bot.oauth_user_token == "5555" + assert _bot.oauth_bot_token == "5555" + assert _bot.verification_token == "5555" with pytest.raises(BotInitializationError): - _bot = Bot.get_bot_by_name(_team, 'fakebot') + _bot = Bot.get_bot_by_name(_team, "fakebot") with pytest.raises(BotInitializationError): - _bot = Bot.get_bot_by_bot_id(_team, 'BADBOTID') + _bot = Bot.get_bot_by_bot_id(_team, "BADBOTID") diff --git a/tests/unit/omnibot/services/slack/interactive_component_test.py b/tests/unit/omnibot/services/slack/interactive_component_test.py index 24197ba..c728e4f 100644 --- a/tests/unit/omnibot/services/slack/interactive_component_test.py +++ b/tests/unit/omnibot/services/slack/interactive_component_test.py @@ -4,160 +4,130 @@ def test_interactive_component(mocker): - _team = Team.get_team_by_name('testteam') - _bot = Bot.get_bot_by_name(_team, 'echobot') + _team = Team.get_team_by_name("testteam") + _bot = Bot.get_bot_by_name(_team, "echobot") component = { - 'type': 'message_action', - 'callback_id': 'echobot_action_test', - 'action_ts': '1234567.12', - 'trigger_id': '376604117319.165116859648.515402022613c2893a80d6268c463e54', # noqa:E501 - 'response_url': 'https://hooks.slack.com/app/T999999/375455994771/iMW9hNKFI739hGOw9FCXMlf4', # noqa:E501 - 'user': { - 'id': 'A12345678', - 'name': 'echobot' + "type": "message_action", + "callback_id": "echobot_action_test", + "action_ts": "1234567.12", + "trigger_id": "376604117319.165116859648.515402022613c2893a80d6268c463e54", # noqa:E501 + "response_url": "https://hooks.slack.com/app/T999999/375455994771/iMW9hNKFI739hGOw9FCXMlf4", # noqa:E501 + "user": {"id": "A12345678", "name": "echobot"}, + "team": {"id": "T999999", "domain": "omnibot-test-domain"}, + "channel": {"id": "C123456AB", "name": "channel-channel"}, + "message": { + "type": "message", + "user": "A12345678", + "text": "<@A12345678> echo I am in <#C123456AB|channel-channel>. See: :simple_smile:", # noqa:E501 + "ts": "1230000.00", }, - 'team': { - 'id': 'T999999', - 'domain': 'omnibot-test-domain' - }, - 'channel': { - 'id': 'C123456AB', - 'name': 'channel-channel' - }, - 'message': { - 'type': 'message', - 'user': 'A12345678', - 'text': '<@A12345678> echo I am in <#C123456AB|channel-channel>. See: :simple_smile:', # noqa:E501 - 'ts': '1230000.00' - } } event_trace = { - 'callback_id': 'echobot_action_test', - 'app_id': _bot.bot_id, - 'team_id': _bot.team.team_id, - 'bot_receiver': _bot.name, - 'component_type': 'message_action' + "callback_id": "echobot_action_test", + "app_id": _bot.bot_id, + "team_id": _bot.team.team_id, + "bot_receiver": _bot.name, + "component_type": "message_action", } - get_user_mock = mocker.patch( - 'omnibot.services.slack.get_user' - ) - user_ret = {'A12345678': {}} + get_user_mock = mocker.patch("omnibot.services.slack.get_user") + user_ret = {"A12345678": {}} get_user_mock.return_value = user_ret - get_channel_mock = mocker.patch( - 'omnibot.services.slack.get_channel' - ) - channel_ret = {'C123456AB': {}} + get_channel_mock = mocker.patch("omnibot.services.slack.get_channel") + channel_ret = {"C123456AB": {}} get_channel_mock.return_value = channel_ret - extract_users_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_users' - ) - users_ret = {'<@A12345678>': 'echobot'} + extract_users_mock = mocker.patch("omnibot.services.slack.parser.extract_users") + users_ret = {"<@A12345678>": "echobot"} extract_users_mock.return_value = users_ret - replace_users_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_users' - ) - replace_users_mock.return_value = '@echobot echo I am in <#C123456AB|channel-channel>. See: :simple_smile:' # noqa:E501 + replace_users_mock = mocker.patch("omnibot.services.slack.parser.replace_users") + replace_users_mock.return_value = "@echobot echo I am in <#C123456AB|channel-channel>. See: :simple_smile:" # noqa:E501 extract_channels_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_channels' + "omnibot.services.slack.parser.extract_channels" ) - channels_ret = { - '<#C123456AB|channel-channel>': 'channel-channel' - } + channels_ret = {"<#C123456AB|channel-channel>": "channel-channel"} extract_channels_mock.return_value = channels_ret replace_channels_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_channels' + "omnibot.services.slack.parser.replace_channels" ) - replace_channels_mock.return_value = '@echobot echo I am in #channel-channel. See: :simple_smile:' # noqa:E501 + replace_channels_mock.return_value = "@echobot echo I am in #channel-channel. See: :simple_smile:" # noqa:E501 extract_subteams_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_subteams' + "omnibot.services.slack.parser.extract_subteams" ) extract_subteams_mock.return_value = {} extract_specials_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_specials' + "omnibot.services.slack.parser.extract_specials" ) - special_ret = {'': '@here'} + special_ret = {"": "@here"} extract_specials_mock.return_value = special_ret replace_specials_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_specials' - ) - replace_specials_mock.return_value = '@echobot echo I am @here in #channel-channel. See: :simple_smile:' # noqa:E501 - extract_emojis_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_emojis' + "omnibot.services.slack.parser.replace_specials" ) - emoji_ret = {':simple-smile': 'simple_smile'} + replace_specials_mock.return_value = "@echobot echo I am @here in #channel-channel. See: :simple_smile:" # noqa:E501 + extract_emojis_mock = mocker.patch("omnibot.services.slack.parser.extract_emojis") + emoji_ret = {":simple-smile": "simple_smile"} extract_emojis_mock.return_value = emoji_ret - extract_emails_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_emails' - ) + extract_emails_mock = mocker.patch("omnibot.services.slack.parser.extract_emails") extract_emails_mock.return_value = {} - replace_emails_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_emails' - ) - replace_emails_mock.return_value = '@echobot echo I am @here in #channel-channel. See: :simple_smile:' # noqa:E501 - extract_urls_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_urls' - ) - url_ret = { - '': 'http://example.com' - } + replace_emails_mock = mocker.patch("omnibot.services.slack.parser.replace_emails") + replace_emails_mock.return_value = "@echobot echo I am @here in #channel-channel. See: :simple_smile:" # noqa:E501 + extract_urls_mock = mocker.patch("omnibot.services.slack.parser.extract_urls") + url_ret = {"": "http://example.com"} extract_urls_mock.return_value = url_ret - replace_urls_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_urls' - ) - replace_urls_mock.return_value = '@echobot echo I am @here in #channel-channel. See: http://example.com :simple_smile:' # noqa:E501 + replace_urls_mock = mocker.patch("omnibot.services.slack.parser.replace_urls") + replace_urls_mock.return_value = "@echobot echo I am @here in #channel-channel. See: http://example.com :simple_smile:" # noqa:E501 extract_mentions_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_mentions' + "omnibot.services.slack.parser.extract_mentions" ) extract_mentions_mock.return_value = True - extract_command_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_command' - ) - extract_command_mock.return_value = 'echo I am @here in #channel-channel. See: http://example.com :simple_smile:' # noqa:E501 + extract_command_mock = mocker.patch("omnibot.services.slack.parser.extract_command") + extract_command_mock.return_value = "echo I am @here in #channel-channel. See: http://example.com :simple_smile:" # noqa:E501 _component = InteractiveComponent(_bot, component, event_trace) assert _component.event_trace == event_trace assert _component.bot == _bot - assert _component.component_type == 'message_action' - assert _component.callback_id == 'echobot_action_test' - assert _component.action_ts == '1234567.12' - assert _component.trigger_id == '376604117319.165116859648.515402022613c2893a80d6268c463e54' # noqa:E501 - assert _component.response_url == 'https://hooks.slack.com/app/T999999/375455994771/iMW9hNKFI739hGOw9FCXMlf4' # noqa:E501 + assert _component.component_type == "message_action" + assert _component.callback_id == "echobot_action_test" + assert _component.action_ts == "1234567.12" + assert ( + _component.trigger_id + == "376604117319.165116859648.515402022613c2893a80d6268c463e54" + ) # noqa:E501 + assert ( + _component.response_url + == "https://hooks.slack.com/app/T999999/375455994771/iMW9hNKFI739hGOw9FCXMlf4" + ) # noqa:E501 assert _component.submission is None - assert _component.channel['id'] == 'C123456AB' + assert _component.channel["id"] == "C123456AB" assert _component.parsed_channel == channel_ret - assert _component.user == component['user'] - assert _component.team == { - 'name': _bot.team.name, - 'team_id': _bot.team.team_id - } - assert _component.message['parsed_user'] == user_ret - assert _component.message['users'] == users_ret - assert _component.message['parsed_text'] == '@echobot echo I am @here in #channel-channel. See: http://example.com :simple_smile:' # noqa:E501 - assert _component.message.get('bot_id') is None - assert _component.message['channels'] == channels_ret - assert _component.message['specials'] == special_ret - assert _component.message['emails'] == {} - assert _component.message['urls'] == url_ret + assert _component.user == component["user"] + assert _component.team == {"name": _bot.team.name, "team_id": _bot.team.team_id} + assert _component.message["parsed_user"] == user_ret + assert _component.message["users"] == users_ret + assert ( + _component.message["parsed_text"] + == "@echobot echo I am @here in #channel-channel. See: http://example.com :simple_smile:" + ) # noqa:E501 + assert _component.message.get("bot_id") is None + assert _component.message["channels"] == channels_ret + assert _component.message["specials"] == special_ret + assert _component.message["emails"] == {} + assert _component.message["urls"] == url_ret def test_interactive_block_component(mocker): - _team = Team.get_team_by_name('testteam') - _bot = Bot.get_bot_by_name(_team, 'echobot') + _team = Team.get_team_by_name("testteam") + _bot = Bot.get_bot_by_name(_team, "echobot") component = { - 'type': 'block_actions', - 'response_url': 'https://hooks.slack.com/app/T999999/375455994771/iMW9hNKFI739hGOw9FCXMlf4', # noqa:E501 - 'actions': [ - { - 'block_id': 'echobot_action_test', - 'action_ts': '1561559117.130541' - } - ] + "type": "block_actions", + "response_url": "https://hooks.slack.com/app/T999999/375455994771/iMW9hNKFI739hGOw9FCXMlf4", # noqa:E501 + "actions": [ + {"block_id": "echobot_action_test", "action_ts": "1561559117.130541"} + ], } event_trace = { - 'callback_id': 'echobot_action_test', - 'app_id': _bot.bot_id, - 'team_id': _bot.team.team_id, - 'bot_receiver': _bot.name, - 'component_type': 'message_action' + "callback_id": "echobot_action_test", + "app_id": _bot.bot_id, + "team_id": _bot.team.team_id, + "bot_receiver": _bot.name, + "component_type": "message_action", } _component = InteractiveComponent(_bot, component, event_trace) - assert _component.callback_id == 'echobot_action_test' + assert _component.callback_id == "echobot_action_test" diff --git a/tests/unit/omnibot/services/slack/message_test.py b/tests/unit/omnibot/services/slack/message_test.py index 70fa987..b475435 100644 --- a/tests/unit/omnibot/services/slack/message_test.py +++ b/tests/unit/omnibot/services/slack/message_test.py @@ -8,116 +8,95 @@ def test_message(mocker): - _team = Team.get_team_by_name('testteam') - _bot = Bot.get_bot_by_name(_team, 'echobot') + _team = Team.get_team_by_name("testteam") + _bot = Bot.get_bot_by_name(_team, "echobot") event = { - 'ts': '1234567.12', - 'thread_ts': None, - 'user': 'A12345678', - 'text': '<@A12345678> echo I am in <#C123456AB|channel-channel>. See: :simple_smile:', # noqa:E501 - 'channel': 'C123456AB', + "ts": "1234567.12", + "thread_ts": None, + "user": "A12345678", + "text": "<@A12345678> echo I am in <#C123456AB|channel-channel>. See: :simple_smile:", # noqa:E501 + "channel": "C123456AB", } event_trace = { - 'event_ts': '1234567.12', - 'event_type': 'message', - 'app_id': _bot.bot_id, - 'team_id': _bot.team.team_id, - 'bot_receiver': _bot.name + "event_ts": "1234567.12", + "event_type": "message", + "app_id": _bot.bot_id, + "team_id": _bot.team.team_id, + "bot_receiver": _bot.name, } - get_user_mock = mocker.patch( - 'omnibot.services.slack.get_user' - ) - user_ret = {'A12345678': {}} + get_user_mock = mocker.patch("omnibot.services.slack.get_user") + user_ret = {"A12345678": {}} get_user_mock.return_value = user_ret - get_channel_mock = mocker.patch( - 'omnibot.services.slack.get_channel' - ) - channel_ret = {'C123456AB': {}} + get_channel_mock = mocker.patch("omnibot.services.slack.get_channel") + channel_ret = {"C123456AB": {}} get_channel_mock.return_value = channel_ret - extract_users_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_users' - ) - user_ret = {'<@A12345678>': 'echobot'} + extract_users_mock = mocker.patch("omnibot.services.slack.parser.extract_users") + user_ret = {"<@A12345678>": "echobot"} extract_users_mock.return_value = user_ret - replace_users_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_users' - ) - replace_users_mock.return_value = '@echobot echo I am in <#C123456AB|channel-channel>. See: :simple_smile:' # noqa:E501 + replace_users_mock = mocker.patch("omnibot.services.slack.parser.replace_users") + replace_users_mock.return_value = "@echobot echo I am in <#C123456AB|channel-channel>. See: :simple_smile:" # noqa:E501 extract_channels_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_channels' + "omnibot.services.slack.parser.extract_channels" ) - channels_ret = { - '<#C123456AB|channel-channel>': 'channel-channel' - } + channels_ret = {"<#C123456AB|channel-channel>": "channel-channel"} extract_channels_mock.return_value = channels_ret replace_channels_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_channels' + "omnibot.services.slack.parser.replace_channels" ) - replace_channels_mock.return_value = '@echobot echo I am in #channel-channel. See: :simple_smile:' # noqa:E501 + replace_channels_mock.return_value = "@echobot echo I am in #channel-channel. See: :simple_smile:" # noqa:E501 extract_subteams_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_subteams' + "omnibot.services.slack.parser.extract_subteams" ) extract_subteams_mock.return_value = {} extract_specials_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_specials' + "omnibot.services.slack.parser.extract_specials" ) - special_ret = {'': '@here'} + special_ret = {"": "@here"} extract_specials_mock.return_value = special_ret replace_specials_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_specials' - ) - replace_specials_mock.return_value = '@echobot echo I am @here in #channel-channel. See: :simple_smile:' # noqa:E501 - extract_emojis_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_emojis' + "omnibot.services.slack.parser.replace_specials" ) - emoji_ret = {':simple-smile': 'simple_smile'} + replace_specials_mock.return_value = "@echobot echo I am @here in #channel-channel. See: :simple_smile:" # noqa:E501 + extract_emojis_mock = mocker.patch("omnibot.services.slack.parser.extract_emojis") + emoji_ret = {":simple-smile": "simple_smile"} extract_emojis_mock.return_value = emoji_ret - extract_emails_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_emails' - ) + extract_emails_mock = mocker.patch("omnibot.services.slack.parser.extract_emails") extract_emails_mock.return_value = {} - replace_emails_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_emails' - ) - replace_emails_mock.return_value = '@echobot echo I am @here in #channel-channel. See: :simple_smile:' # noqa:E501 - extract_urls_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_urls' - ) - url_ret = { - '': 'http://example.com' - } + replace_emails_mock = mocker.patch("omnibot.services.slack.parser.replace_emails") + replace_emails_mock.return_value = "@echobot echo I am @here in #channel-channel. See: :simple_smile:" # noqa:E501 + extract_urls_mock = mocker.patch("omnibot.services.slack.parser.extract_urls") + url_ret = {"": "http://example.com"} extract_urls_mock.return_value = url_ret - replace_urls_mock = mocker.patch( - 'omnibot.services.slack.parser.replace_urls' - ) - replace_urls_mock.return_value = '@echobot echo I am @here in #channel-channel. See: http://example.com :simple_smile:' # noqa:E501 + replace_urls_mock = mocker.patch("omnibot.services.slack.parser.replace_urls") + replace_urls_mock.return_value = "@echobot echo I am @here in #channel-channel. See: http://example.com :simple_smile:" # noqa:E501 extract_mentions_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_mentions' + "omnibot.services.slack.parser.extract_mentions" ) extract_mentions_mock.return_value = True - extract_command_mock = mocker.patch( - 'omnibot.services.slack.parser.extract_command' - ) - extract_command_mock.return_value = 'echo I am @here in #channel-channel. See: http://example.com :simple_smile:' # noqa:E501 + extract_command_mock = mocker.patch("omnibot.services.slack.parser.extract_command") + extract_command_mock.return_value = "echo I am @here in #channel-channel. See: http://example.com :simple_smile:" # noqa:E501 _message = Message(_bot, event, event_trace) assert _message.event == event assert _message.event_trace == event_trace assert _message.bot == _bot assert _message.subtype is None - assert _message.text == event['text'] - assert _message.parsed_text == '@echobot echo I am @here in #channel-channel. See: http://example.com :simple_smile:' # noqa:E501 - assert _message.command_text == 'echo I am @here in #channel-channel. See: http://example.com :simple_smile:' # noqa:E501 + assert _message.text == event["text"] + assert ( + _message.parsed_text + == "@echobot echo I am @here in #channel-channel. See: http://example.com :simple_smile:" + ) # noqa:E501 + assert ( + _message.command_text + == "echo I am @here in #channel-channel. See: http://example.com :simple_smile:" + ) # noqa:E501 assert _message.directed is True assert _message.mentioned is True - assert _message.channel_id == 'C123456AB' + assert _message.channel_id == "C123456AB" assert _message.channel == channel_ret - assert _message.user == event['user'] - assert _message.ts == event['ts'] - assert _message.thread_ts == event['thread_ts'] - assert _message.team == { - 'name': _bot.team.name, - 'team_id': _bot.team.team_id - } + assert _message.user == event["user"] + assert _message.ts == event["ts"] + assert _message.thread_ts == event["thread_ts"] + assert _message.team == {"name": _bot.team.name, "team_id": _bot.team.team_id} assert _message.bot_id is None assert _message.channels == channels_ret assert _message.users == user_ret @@ -127,25 +106,28 @@ def test_message(mocker): assert _message.match_type is None assert _message.match is None assert _message.event_trace == event_trace - _message.set_match('command', 'echo') - assert _message.match_type == 'command' - assert _message.match == 'echo' - assert _message.payload['command'] == 'echo' - assert _message.payload['args'] == 'I am @here in #channel-channel. See: http://example.com :simple_smile:' # noqa:E501 - _message.set_match('regex', None) - assert _message.match_type == 'regex' + _message.set_match("command", "echo") + assert _message.match_type == "command" + assert _message.match == "echo" + assert _message.payload["command"] == "echo" + assert ( + _message.payload["args"] + == "I am @here in #channel-channel. See: http://example.com :simple_smile:" + ) # noqa:E501 + _message.set_match("regex", None) + assert _message.match_type == "regex" event_copy = copy.deepcopy(event) - event_copy['bot_id'] = 'A12345678' + event_copy["bot_id"] = "A12345678" with pytest.raises(MessageUnsupportedError): _message = Message(_bot, event_copy, event_trace) event_copy = copy.deepcopy(event) - event_copy['thread_ts'] = '1234568.00' + event_copy["thread_ts"] = "1234568.00" with pytest.raises(MessageUnsupportedError): _message = Message(_bot, event_copy, event_trace) event_copy = copy.deepcopy(event) - event_copy['subtype'] = 'some_subtype' + event_copy["subtype"] = "some_subtype" with pytest.raises(MessageUnsupportedError): _message = Message(_bot, event_copy, event_trace) diff --git a/tests/unit/omnibot/services/slack/parser_test.py b/tests/unit/omnibot/services/slack/parser_test.py index ded4ac9..5f1ea34 100644 --- a/tests/unit/omnibot/services/slack/parser_test.py +++ b/tests/unit/omnibot/services/slack/parser_test.py @@ -3,9 +3,11 @@ def test_extract_user(): assert extract_users("<@W024BE7LH|logan-smith>", "ball") == { - '<@W024BE7LH|logan-smith>': 'logan-smith'} + "<@W024BE7LH|logan-smith>": "logan-smith" + } assert extract_users("<@U024BE7LH|logan-smith>", "ball") == { - '<@U024BE7LH|logan-smith>': 'logan-smith'} + "<@U024BE7LH|logan-smith>": "logan-smith" + } # we do not support G so nothing should be returned @@ -14,8 +16,8 @@ def test_not_supported_extract(): def test_extract_mentions(mocker): - mock_bot = mocker.patch('omnibot.services.slack.bot.Bot') - mock_bot.return_value.name = 'omnibot' - assert extract_mentions('@omnibot testcommand', mock_bot.return_value, {}) - assert extract_mentions('@omnibot\u00A0testcommand', mock_bot.return_value, {}) - assert not extract_mentions('@omnibottestcommand', mock_bot.return_value, {}) + mock_bot = mocker.patch("omnibot.services.slack.bot.Bot") + mock_bot.return_value.name = "omnibot" + assert extract_mentions("@omnibot testcommand", mock_bot.return_value, {}) + assert extract_mentions("@omnibot\u00A0testcommand", mock_bot.return_value, {}) + assert not extract_mentions("@omnibottestcommand", mock_bot.return_value, {}) diff --git a/tests/unit/omnibot/services/slack/team_test.py b/tests/unit/omnibot/services/slack/team_test.py index 2e47af5..6167f43 100644 --- a/tests/unit/omnibot/services/slack/team_test.py +++ b/tests/unit/omnibot/services/slack/team_test.py @@ -4,16 +4,16 @@ def test_team(): - _team = Team.get_team_by_name('testteam') - assert _team.name == 'testteam' - assert _team.team_id == 'T12345678' + _team = Team.get_team_by_name("testteam") + assert _team.name == "testteam" + assert _team.team_id == "T12345678" - _team = Team.get_team_by_id('TABCDEF12') - assert _team.name == 'test2ndteam' - assert _team.team_id == 'TABCDEF12' + _team = Team.get_team_by_id("TABCDEF12") + assert _team.name == "test2ndteam" + assert _team.team_id == "TABCDEF12" with pytest.raises(TeamInitializationError): - _team = Team.get_team_by_name('faketeam') + _team = Team.get_team_by_name("faketeam") with pytest.raises(TeamInitializationError): - _team = Team.get_team_by_id(team_id='BADTEAMID') + _team = Team.get_team_by_id(team_id="BADTEAMID") diff --git a/tests/unit/omnibot/settings_test.py b/tests/unit/omnibot/settings_test.py index 8d957d9..a30d695 100644 --- a/tests/unit/omnibot/settings_test.py +++ b/tests/unit/omnibot/settings_test.py @@ -2,42 +2,42 @@ def test_primary_slack_bot(): - assert settings.PRIMARY_SLACK_BOT['testteam'] == 'testteambot' - assert settings.PRIMARY_SLACK_BOT['test2ndteam'] == 'test2ndteambot' + assert settings.PRIMARY_SLACK_BOT["testteam"] == "testteambot" + assert settings.PRIMARY_SLACK_BOT["test2ndteam"] == "test2ndteambot" def test_slack_teams(): - assert settings.SLACK_TEAMS['testteam'] == 'T12345678' - assert settings.SLACK_TEAMS['test2ndteam'] == 'TABCDEF12' + assert settings.SLACK_TEAMS["testteam"] == "T12345678" + assert settings.SLACK_TEAMS["test2ndteam"] == "TABCDEF12" def test_slack_bot_tokens(): - assert 'testteam' in settings.SLACK_BOT_TOKENS - assert 'test2ndteam' in settings.SLACK_BOT_TOKENS + assert "testteam" in settings.SLACK_BOT_TOKENS + assert "test2ndteam" in settings.SLACK_BOT_TOKENS required_keys = [ - 'verification_token', - 'oauth_user_token', - 'oauth_bot_token', - 'app_id' + "verification_token", + "oauth_user_token", + "oauth_bot_token", + "app_id", ] - assert 'echobot' in settings.SLACK_BOT_TOKENS['testteam'] + assert "echobot" in settings.SLACK_BOT_TOKENS["testteam"] for key in required_keys: - assert key in settings.SLACK_BOT_TOKENS['testteam']['echobot'] - assert 'pingbot' in settings.SLACK_BOT_TOKENS['testteam'] - assert 'commandbot' in settings.SLACK_BOT_TOKENS['testteam'] - assert 'mentionbot' in settings.SLACK_BOT_TOKENS['testteam'] + assert key in settings.SLACK_BOT_TOKENS["testteam"]["echobot"] + assert "pingbot" in settings.SLACK_BOT_TOKENS["testteam"] + assert "commandbot" in settings.SLACK_BOT_TOKENS["testteam"] + assert "mentionbot" in settings.SLACK_BOT_TOKENS["testteam"] # missing oauth bot token, but has oauth token - assert 'echobot' in settings.SLACK_BOT_TOKENS['test2ndteam'] + assert "echobot" in settings.SLACK_BOT_TOKENS["test2ndteam"] for key in required_keys: - assert key in settings.SLACK_BOT_TOKENS['test2ndteam']['echobot'] - assert settings.SLACK_BOT_TOKENS['test2ndteam']['echobot']['oauth_user_token'] != '' - assert settings.SLACK_BOT_TOKENS['test2ndteam']['echobot']['oauth_bot_token'] == '' + assert key in settings.SLACK_BOT_TOKENS["test2ndteam"]["echobot"] + assert settings.SLACK_BOT_TOKENS["test2ndteam"]["echobot"]["oauth_user_token"] != "" + assert settings.SLACK_BOT_TOKENS["test2ndteam"]["echobot"]["oauth_bot_token"] == "" # missing verification token - assert 'pingbot' not in settings.SLACK_BOT_TOKENS['test2ndteam'] + assert "pingbot" not in settings.SLACK_BOT_TOKENS["test2ndteam"] # missing oauth tokens - assert 'channelchannelbot' not in settings.SLACK_BOT_TOKENS['test2ndteam'] + assert "channelchannelbot" not in settings.SLACK_BOT_TOKENS["test2ndteam"] def test_handlers(): - assert 'slash_command_handlers' in settings.HANDLERS - assert 'message_handlers' in settings.HANDLERS + assert "slash_command_handlers" in settings.HANDLERS + assert "message_handlers" in settings.HANDLERS