diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2b708b3..9167918 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -15,32 +15,37 @@ } }, + "runArgs": ["--env-file", "${localWorkspaceFolder}/.env"], + // Set *default* container specific settings.json values on container create. - "settings": { - "terminal.integrated.shell.linux": "/bin/bash", - "python.pythonPath": "/usr/local/bin/python", - "python.languageServer": "Pylance", - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", - "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", - "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", - "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", - "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", - "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", - "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", - "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": "/usr/local/bin/python", + "python.languageServer": "Pylance", + "python.linting.enabled": true, + "python.linting.pylintEnabled": true, + "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", + "python.formatting.blackPath": "/usr/local/py-utils/bin/black", + "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", + "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", + "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", + "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", + "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", + "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" + }, + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "ms-azuretools.vscode-docker", + "eamodio.gitlens", + "kuscamara.yamllint-fix" + ] + } }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "ms-azuretools.vscode-docker", - "eamodio.gitlens", - ], - // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 096df21..02da1b8 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -38,10 +38,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python 3.11.4 uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: 3.11.4 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -59,6 +59,7 @@ jobs: bottomly_google_api_key: ${{ secrets.GOOGLE_API_KEY }} bottomly_google_cse_id: ${{ secrets.GOOGLE_CSE_ID }} bottomly_slack_bot_token: ${{ secrets.SLACK_TOKEN }} + bottomly_slack_app_token: ${{ secrets.SLACK_APP_TOKEN }} bottomly_prefix: _ bottomly_env: test bottomly_mongo_conn_str: ${{ secrets.MONGO_CONN_STR }} diff --git a/.github/workflows/push_to_live.yml b/.github/workflows/push_to_live.yml index 92a6956..7a122ab 100644 --- a/.github/workflows/push_to_live.yml +++ b/.github/workflows/push_to_live.yml @@ -1,45 +1,47 @@ +--- on: release: - types: - - created + types: [created] name: Push to Live - jobs: - build-and-deploy: - runs-on: ubuntu-latest - - environment: - name: live - - steps: + build-and-deploy: + runs-on: ubuntu-latest + environment: + name: live + steps: # checkout the repo - - name: 'Checkout GitHub Action' - uses: actions/checkout@main - - - name: 'Login via Azure CLI' - uses: azure/login@v1.4.5 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: 'Build and push image' - uses: azure/docker-login@v1 - with: - login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_PASSWORD }} - - run: | - docker build . -f run.dockerfile -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} - docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} - - - name: 'Deploy to Azure Container Instances' - uses: 'azure/aci-deploy@v1' - with: - resource-group: ${{ secrets.RESOURCE_GROUP }} - dns-name-label: ${{ secrets.RESOURCE_GROUP }}live${{ github.run_number }} - image: ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} - registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} - registry-username: ${{ secrets.REGISTRY_USERNAME }} - registry-password: ${{ secrets.REGISTRY_PASSWORD }} - name: bottomly-live - location: 'uk south' - environment-variables: bottomly_giphy_api_key=${{ secrets.GIPHY_API_KEY }} bottomly_google_api_key=${{ secrets.GOOGLE_API_KEY }} bottomly_google_cse_id=${{ secrets.GOOGLE_CSE_ID }} bottomly_slack_bot_token=${{ secrets.SLACK_TOKEN }} bottomly_prefix=${{ secrets.PREFIX }} bottomly_env=${{ secrets.ENV }} bottomly_mongo_conn_str=${{ secrets.MONGO_CONN_STR }} bottomly_github_token=${{ secrets.RELEASE_HISTORY_TOKEN }} + - name: Checkout GitHub Action + uses: actions/checkout@main + - name: Login via Azure CLI + uses: azure/login@v1.4.5 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + - name: Build and push image + uses: azure/docker-login@v1 + with: + login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + - run: | + docker build . -f run.dockerfile -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} + docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} + - name: Deploy to Azure Container Instances + uses: azure/aci-deploy@v1 + with: + resource-group: ${{ secrets.RESOURCE_GROUP }} + dns-name-label: ${{ secrets.RESOURCE_GROUP }}live${{ github.run_number }} + image: ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} + registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} + registry-username: ${{ secrets.REGISTRY_USERNAME }} + registry-password: ${{ secrets.REGISTRY_PASSWORD }} + name: bottomly-live + location: uk south + environment-variables: bottomly_giphy_api_key=${{ secrets.GIPHY_API_KEY }} + bottomly_google_api_key=${{ secrets.GOOGLE_API_KEY }} + bottomly_google_cse_id=${{ secrets.GOOGLE_CSE_ID }} + bottomly_slack_bot_token=${{ secrets.SLACK_TOKEN }} + bottomly_slack_app_token=${{ secrets.SLACK_APP_TOKEN }} + bottomly_prefix=${{ secrets.PREFIX }} + bottomly_env=${{ secrets.ENV }} + bottomly_mongo_conn_str=${{ secrets.MONGO_CONN_STR }} + bottomly_github_token=${{ secrets.RELEASE_HISTORY_TOKEN }} diff --git a/.github/workflows/push_to_test.yml b/.github/workflows/push_to_test.yml index d64fa93..fc00429 100644 --- a/.github/workflows/push_to_test.yml +++ b/.github/workflows/push_to_test.yml @@ -1,46 +1,48 @@ +--- on: push - # release: # types: # - created name: Push to Test - jobs: - build-and-deploy: - runs-on: ubuntu-latest - - environment: - name: test - - steps: + build-and-deploy: + runs-on: ubuntu-latest + environment: + name: test + steps: # checkout the repo - - name: 'Checkout GitHub Action' - uses: actions/checkout@main - - - name: 'Login via Azure CLI' - uses: azure/login@v1.4.5 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: 'Build and push image' - uses: azure/docker-login@v1 - with: - login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_PASSWORD }} - - run: | - docker build . -f run.dockerfile -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} - docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} - - - name: 'Deploy to Azure Container Instances' - uses: 'azure/aci-deploy@v1' - with: - resource-group: ${{ secrets.RESOURCE_GROUP }} - dns-name-label: ${{ secrets.RESOURCE_GROUP }}${{ github.run_number }} - image: ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} - registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} - registry-username: ${{ secrets.REGISTRY_USERNAME }} - registry-password: ${{ secrets.REGISTRY_PASSWORD }} - name: bottomly-test - location: 'uk south' - environment-variables: bottomly_giphy_api_key=${{ secrets.GIPHY_API_KEY }} bottomly_google_api_key=${{ secrets.GOOGLE_API_KEY }} bottomly_google_cse_id=${{ secrets.GOOGLE_CSE_ID }} bottomly_slack_bot_token=${{ secrets.SLACK_TOKEN }} bottomly_prefix=${{ secrets.PREFIX }} bottomly_env=${{ secrets.ENV }} bottomly_mongo_conn_str=${{ secrets.MONGO_CONN_STR }} bottomly_github_token=${{ secrets.RELEASE_HISTORY_TOKEN }} + - name: Checkout GitHub Action + uses: actions/checkout@main + - name: Login via Azure CLI + uses: azure/login@v1.4.5 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + - name: Build and push image + uses: azure/docker-login@v1 + with: + login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + - run: | + docker build . -f run.dockerfile -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} + docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} + - name: Deploy to Azure Container Instances + uses: azure/aci-deploy@v1 + with: + resource-group: ${{ secrets.RESOURCE_GROUP }} + dns-name-label: ${{ secrets.RESOURCE_GROUP }}${{ github.run_number }} + image: ${{ secrets.REGISTRY_LOGIN_SERVER }}/bottomly:${{ github.sha }} + registry-login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} + registry-username: ${{ secrets.REGISTRY_USERNAME }} + registry-password: ${{ secrets.REGISTRY_PASSWORD }} + name: bottomly-test + location: uk south + environment-variables: bottomly_giphy_api_key=${{ secrets.GIPHY_API_KEY }} + bottomly_google_api_key=${{ secrets.GOOGLE_API_KEY }} + bottomly_google_cse_id=${{ secrets.GOOGLE_CSE_ID }} + bottomly_slack_bot_token=${{ secrets.SLACK_TOKEN }} + bottomly_slack_app_token=${{ secrets.SLACK_APP_TOKEN }} + bottomly_prefix=${{ secrets.PREFIX }} + bottomly_env=${{ secrets.ENV }} + bottomly_mongo_conn_str=${{ secrets.MONGO_CONN_STR }} + bottomly_github_token=${{ secrets.RELEASE_HISTORY_TOKEN }} diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..7e7ffe5 --- /dev/null +++ b/.yamllint @@ -0,0 +1,6 @@ +extends: default + +rules: + line-length: disable + truthy: + allowed-values: ['true', 'false', 'yes', 'no'] \ No newline at end of file diff --git a/config.py b/config.py index 5678740..90e6643 100644 --- a/config.py +++ b/config.py @@ -8,6 +8,7 @@ class ConfigKeys(Enum): google_cse_id = "bottomly_google_cse_id" mongo_conn_st = "bottomly_mongo_conn_str" slack_bot_token = "bottomly_slack_bot_token" + slack_app_token = "bottomly_slack_app_token" prefix = "bottomly_prefix" giphy_api_key = "bottomly_giphy_api_key" env_key = "bottomly_env" @@ -23,6 +24,7 @@ class Config(object): ConfigKeys.google_cse_id: 'Google custom search engine ID is not configured', ConfigKeys.mongo_conn_st: 'MongoDB connection string is not configured', ConfigKeys.slack_bot_token: 'Slack bot auth token is not configured', + ConfigKeys.slack_app_token: 'Slack app auth token is not configured', ConfigKeys.prefix: "Standard command prefix is not configured", ConfigKeys.giphy_api_key: "Giphy API key is not configured", ConfigKeys.env_key: "Environment mode is not configured", diff --git a/main.py b/main.py index 575ac66..d02bf95 100644 --- a/main.py +++ b/main.py @@ -2,8 +2,5 @@ from config import Config, ConfigKeys import slack_channel.slack_event_handler -live_mode = "live" - if __name__ == '__main__': - is_debug = Config().get_config_value(ConfigKeys.env_key) != live_mode - slack_channel.slack_event_handler.handle_slack_context() + slack_channel.slack_event_handler.run_slack_socket_mode_client() diff --git a/requirements.txt b/requirements.txt index c65abef..a0f2faf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,52 @@ -aiohttp==3.7.4 -pymongo==3.12.3 -pymodm==0.4.3 -google-api-python-client==1.6.5 -slackclient==1.1.3 -requests>=2.20.0 -websocket-client==0.48.0 -slacksocket==1.0.0 -slack-sdk==3.4.0 -beautifulsoup4==4.7.1 -soupsieve==1.9 -dnspython==1.16.0 -dnslib==0.9.14 -PyGithub==1.57 \ No newline at end of file +aiohappyeyeballs==2.6.1 +aiohttp==3.13.3 +aiosignal==1.4.0 +async-timeout==5.0.1 +attrs==25.4.0 +beautifulsoup4==4.14.3 +certifi==2026.2.25 +cffi==2.0.0 +chardet==6.0.0.post1 +charset-normalizer==3.4.4 +cryptography==46.0.5 +dnslib==0.9.26 +dnspython==2.8.0 +frozenlist==1.8.0 +google-api-core==2.30.0 +google-api-python-client==2.190.0 +google-auth==2.48.0 +google-auth-httplib2==0.3.0 +googleapis-common-protos==1.72.0 +httplib2==0.31.2 +idna==3.11 +multidict==6.7.1 +oauth2client==4.1.3 +packaging==26.0 +pip-review==1.3.0 +propcache==0.4.1 +proto-plus==1.27.1 +protobuf==6.33.5 +pyasn1==0.6.2 +pyasn1_modules==0.4.2 +pycparser==3.0 +PyGithub==2.8.1 +PyJWT==2.11.0 +pymodm==0.4.3 +pymongo==3.13.0 +PyNaCl==1.6.2 +pyparsing==3.3.2 +requests==2.32.5 +rsa==4.9.1 +six==1.17.0 +slack_sdk==3.40.1 +slackclient==2.9.4 +slacksocket==1.0.1 +soupsieve==2.8.3 +typing_extensions==4.15.0 +uritemplate==4.2.0 +urllib3==2.6.3 +websocket-client==0.56.0 +wrapt==2.1.1 +yamllint==1.38.0 +yamlfix==1.19.1 +yarl==1.23.0 \ No newline at end of file diff --git a/slack_channel/slack_event_handler.py b/slack_channel/slack_event_handler.py index 20beb8d..c967d41 100644 --- a/slack_channel/slack_event_handler.py +++ b/slack_channel/slack_event_handler.py @@ -3,10 +3,16 @@ import logging import logging.config +from threading import Event + from slack_sdk import WebClient from slack_sdk.rtm import RTMClient +from slack_sdk.socket_mode import SocketModeClient from slack_sdk.errors import SlackApiError +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest + from config import Config, ConfigKeys from model.member import Member from slack_channel import * @@ -15,6 +21,7 @@ config = Config() token = config.get_config_value(ConfigKeys.slack_bot_token) +app_token = config.get_config_value(ConfigKeys.slack_app_token) debug = Config().get_config_value(ConfigKeys.env_key) != live_mode logging.config.fileConfig('logging.conf') @@ -54,6 +61,7 @@ def _is_subscribed_message(slack_event): try: subscribed = True + subscribed = subscribed and slack_event["type"] == "message" subscribed = subscribed and "text" in slack_event subscribed = subscribed and "bot_id" not in slack_event return subscribed @@ -66,7 +74,7 @@ def _is_subscribed_message(slack_event): def _is_subscribed_reaction(slack_event): try: subscribed = True - # subscribed = subscribed and "bot_id" not in slack_event + subscribed = subscribed and slack_event["type"] == "reaction_added" return subscribed except Exception as ex: logger.warning("Error determining if reaction is subscribed: " + str(ex)) @@ -74,14 +82,25 @@ def _is_subscribed_reaction(slack_event): return False -def handle_slack_context(): +def run_slack_rtm_client(): try: + logger.info("Opening RTM connection to slack.") rtm_client = RTMClient(token=token) - logger.info("Opening connection to slack.") rtm_client.start() except SlackApiError: - logger.exception("Error establishing connection to slack.") + logger.exception("Error establishing RTM connection to slack.") +def run_slack_socket_mode_client(): + try: + logger.info("Opening Socket Mode connection to slack.") + socket_mode_client = SocketModeClient(app_token=app_token, web_client=WebClient(token=token)) + socket_mode_client.socket_mode_request_listeners.append(_process_slack_socket_message) + socket_mode_client.socket_mode_request_listeners.append(_process_slack_socket_reaction) + socket_mode_client.connect() + logger.info("Successfully connected to slack via Socket Mode.") + Event().wait() + except SlackApiError: + logger.exception("Error establishing Socket Mode connection to slack.") @RTMClient.run_on(event="message") def _process_slack_message(**e): @@ -114,6 +133,44 @@ def _process_slack_message(**e): except SlackApiError: logger.exception("Error processing slack message") +def _process_slack_socket_message(client: SocketModeClient, req: SocketModeRequest): + try: + slack_event = req.payload["event"] + if not _is_subscribed_message(slack_event): + _acknowledge_message(client, req) + return + + logger.info("Subscribing to: " + str(slack_event)) + + _insert_channel_id_to_message(slack_event) + _insert_username(slack_event) + + handled = False + + help_handler = HelpEventHandler(debug, _command_handlers) + if help_handler.can_handle(slack_event): + handled = help_handler.handle(slack_event) + + if handled: + _acknowledge_message(client, req) + return + + handled = _execute_command_handlers(slack_event) + + if handled: + _acknowledge_message(client, req) + return + + # TODO: Dynamic response handler + + except SlackApiError: + logger.exception("Error processing slack message") + _acknowledge_message(client, req) + + +def _acknowledge_message(client: SocketModeClient, req: SocketModeRequest): + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) @RTMClient.run_on(event="reaction_added") def _process_slack_reaction(**e): @@ -136,6 +193,28 @@ def _process_slack_reaction(**e): except Exception as ex: logger.exception("General error processing slack reaction: " + str(ex)) +def _process_slack_socket_reaction(client: SocketModeClient, req: SocketModeRequest): + try: + slack_event = req.payload["event"] + if not _is_subscribed_reaction(slack_event): + _acknowledge_message(client, req) + return + slack_event["channel"] = slack_event["item"]["channel"] + + slack_event["reactor"] = Member.get_member_by_slack_id(slack_event["user"]).username + slack_event["reactee"] = Member.get_member_by_slack_id(slack_event["item_user"]).username + + logger.info(str(slack_event)) + + _reaction_handlers[0].handle(slack_event) + _acknowledge_message(client, req) + + except SlackApiError: + logger.exception("Slack API Error processing slack reaction") + + except Exception as ex: + logger.exception("General error processing slack reaction: " + str(ex)) + def _execute_command_handlers(slack_event): handled = False