diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..f3aa1f5 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index df97526..9796a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - Ensure ephemeral messages return a ts attribute. #81 (@TheJokersThief) +## [0.2.1] 2022-10-02 +### Added + - Send cards to threads, when requested. #76 (@TheJokersThief) + - Ability to update slack messages. #75 (@TheJokersThief) + - Allow supplying raw attachments/blocks for messages. #83 (@TheJokersThief) + +### Changed + - refactored repository for setting it up as a pypi package. #82, #89 (@sijis) + +### Fixed + - Ensure ephemeral messages return a ts attribute. #81 (@TheJokersThief) + +## [0.2.1] 2022-10-02 +### Added + - Send cards to threads, when requested. #76 (@TheJokersThief) + - Ability to update slack messages. #75 (@TheJokersThief) + - Allow supplying raw attachments/blocks for messages. #83 (@TheJokersThief) + +### Changed + - refactored repository for setting it up as a pypi package. #82, #89 (@sijis) + +### Fixed + - Ensure ephemeral messages return a ts attribute. #81 (@TheJokersThief) + ## [0.2.0] 2022-09-22 ### Added - Ability to update slack messages. #75 (@TheJokersThief) diff --git a/README.md b/README.md index 3dcd50d..4c25b61 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,18 @@ -# err-backend-slackv3 +# errbot-backend-slackv3 -Slack Events and Real Time Messaging backend for Errbot +[![Documentation Status](https://readthedocs.org/projects/err-backend-slackv3/badge/?version=latest)](https://err-backend-slackv3.readthedocs.io/en/latest/?badge=latest) -## Purpose +Slack Events and Real Time Messaging backend for Errbot. -This backend has been developed to support both the Slack Events and Real Time Messaging APIs using the latest SDK from Slack. +## Documentation -The backend has been made available outside the core errbot project to allow development and user feedback to happen on independent release cycles. +See the (slackv3 documentation)[https://err-backend-slackv3.readthedocs.io/en/latest/] for: + - Installation + - Configuraiton + - User guide + - Developer guide -## Connection Methods +## Support -Slack has been making changes to their OAuth and API architecture that can seem quite confusing. -No matter which OAuth bot token you're using or the API architecture in your environment, `slackv3` has got you covered. +If you need help for an `err-backend-slackv3` problem, open an issue at (github repository)[https://github.com/errbotio/err-backend-slackv3] -The backend will automatically detect which token and architecture you have and start listening for Slack events in the right way: - -- Legacy tokens (OAuthv1) with Real Time Messaging (RTM) API -- Current token (OAuthv2) with Event API using the Event Subscriptions and Request URL. -- Current token (Oauthv2) with Event API using the Socket-mode client. - -## Backend Installation - -These instructions are for errbot running inside a Python virtual environment. You will need to adapt these steps to your own errbot instance setup. -The virtual environment is created in `/opt/errbot/virtualenv` and errbot initialised in `/opt/errbot`. The extra backend directory is in `/opt/erbot/backend`. - -1. Create the errbot virtual environment - - ```bash - mkdir -p /opt/errbot/backend - virtualenv --python=python3 /opt/errbot/virtualenv - ``` - - or - - ```bash - mkdir -p /opt/errbot/backend - python3 -m venv /opt/errbot/virtualenv - ``` - -2. Install and initialise errbot. [See here for details](https://errbot.readthedocs.io/en/latest/user_guide/setup.html) - - ```bash - source /opt/errbot/virtualenv/bin/activate - pip install errbot - cd /opt/errbot - errbot --init - ``` - -3. Configure the slackv3 backend and extra backend directory. Located in `/opt/errbot/config.py` - - ```python - BACKEND="SlackV3" - BOT_EXTRA_BACKEND_DIR="/opt/errbot/backend" - ``` - -4. Clone `err-backend-slackv3` into the backend directory and install module dependencies. - - ```bash - cd /opt/errbot/backend - git clone https://github.com/errbotio/err-backend-slackv3 - # to get a specific release use `--branch `, e.g. `--branch v0.1.0` - git clone --depth 1 https://github.com/errbotio/err-backend-slackv3 - pip install . - ``` - -5. Configure the slack bot token, signing secret (Events API with Request URLs) and/or app token (Events API with Socket-mode). Located in `/opt/errbot/config.py` - - ```python - BOT_IDENTITY = { - 'token': 'xoxb-...', - 'signing_secret': "", - 'app_token': "xapp-..." - } - ``` - -## Setting up Slack application - -### Legacy token with RTM - -This was the original method for connecting a bot to Slack. Create a bot token, configure errbot with it and start using Slack. -Pay attention when reading [this document](https://github.com/slackapi/python-slack-sdk/blob/main/docs-src/real_time_messaging.rst) explaining how to create a "classic slack application". Slack does not allow Legacy bot tokens to use the Events API. - -### Current token with Events Request URLs - -This is by far the most complex method of having errbot communicate with Slack. The architecture involves server to client communication over HTTP. This means the Slack server must be able to reach errbot's `/slack/events` endpoint via the internet using a valid SSL connection. -How to set up such an architecture is outside the scope of this readme and is left as an exercise for the reader. Read [this document](https://github.com/slackapi/python-slack-events-api) for details on how to configure the Slack app and request URL. - -### Current token with Events Socket-mode client - -Create a current bot token, enable socket mode. Configure errbot to use the bot and app tokens and start using Slack. -Read [this document](https://github.com/slackapi/python-slack-sdk/blob/main/docs-src/socket-mode/index.rst) for instructions on setting up Socket-mode. - -Ensure the bot is also subscribed to the following events: - -- `file_created` -- `file_public` -- `message.channels` -- `message.groups` -- `message.im` - -Moving from older slack backends - -### Bot Admins -Slack changed the way users are uniquely identified from display name `@some_name` to user id `Uxxxxxx`. -Errbot configuration will need to be updated before administrators can be correctly identified aginst -the ACL sets. - -The UserID is in plain text format. It can be found in the the Slack full profile page or using the `!whoami` command (`person` field). - -Because BOT_ADMINS is defined as plain text User IDs, they can not be used to send notifications. The mention format -`<@Uxxxxx>` must be used in the BOT_ADMINS_NOTIFICATIONS configuration setting for errbot to initiate message to bot administrators. diff --git a/contrib/manifest-example.yaml b/contrib/manifest-example.yaml new file mode 100644 index 0000000..a81fe9d --- /dev/null +++ b/contrib/manifest-example.yaml @@ -0,0 +1,39 @@ +display_information: + name: Your Bot Name + description: Description + background_color: "#000000" +features: + bot_user: + display_name: Your Bot Name + always_online: true +oauth_config: + scopes: + bot: + - channels:history + - channels:read + - chat:write + - groups:history + - groups:read + - groups:write + - im:history + - im:read + - im:write + - mpim:read + - mpim:write + - reactions:read + - team:read + - users:read + - users:read.email + - channels:manage +settings: + event_subscriptions: + bot_events: + - message.channels + - message.groups + - message.im + - reaction_added + interactivity: + is_enabled: true + org_deploy_enabled: false + socket_mode_enabled: true + token_rotation_enabled: false diff --git a/docs/configuration.rst b/docs/configuration.rst new file mode 100644 index 0000000..f8a2608 --- /dev/null +++ b/docs/configuration.rst @@ -0,0 +1,128 @@ +Configuration +======================================================================== + +It is simple to configure errbot to use the slackv3 backend, however care must be taken when creating your +bot tokens and applying the correct information to errbot's configuration file. + +.. note:: + The use of the Real-time messaging protocol is not recommended by Slack and they urge people to + move to the Event based protocol. https://api.slack.com/changelog/2021-10-rtm-start-to-stop + +To select this backend, set `BACKEND = 'SlackV3'`. + +Connection Methods +------------------------------------------------------------------------ + +Slack's OAuth and API architecture has evolved and caused some confusion. No matter which OAuth bot token you're using or the API architecture in your environment, slackv3 will support it. + +The backend will automatically detect which token and architecture you have and start listening for Slack events in the right way: + + - Legacy tokens (OAuthv1) with Real Time Messaging (RTM) API + - Current token (OAuthv2) with Event API using the Event Subscriptions and Request URL. + - Current token (Oauthv2) with Event API using the Socket-mode client. + +Legacy tokens (OAuthv1) with Real Time Messaging (RTM) API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When the following oauth scopes are detected, the RTM protocol will be used. These scopes are automatically present when using a legacy token. + +.. code:: + + "apps" + "bot" + "bot:basic" + "client" + "files:write:user" + "identify" + "post" + "read" + +- Current token (OAuthv2) with Event API using the Event Subscriptions and Request URL. +- Current token (OAuthv2) with Event API using the Socket-mode client. + +Backend Installation +------------------------------------------------------------------------ + +These instructions are for errbot running inside a Python virtual environment. You will need to adapt these steps to your own errbot instance setup. +The virtual environment is created in `/opt/errbot/virtualenv` and errbot initialised in `/opt/errbot`. The extra backend directory is in `/opt/errbot/backend`. + +1. Create the errbot virtual environment + +.. code:: + + mkdir -p /opt/errbot/backend + python3 -m venv /opt/errbot/virtualenv + +2. Install and initialise errbot. `See here for details `_ + +.. code:: + + source /opt/errbot/virtualenv/bin/activate + pip install errbot + cd /opt/errbot + errbot --init + +3. Configure the slackv3 backend and extra backend directory. Located in `/opt/errbot/config.py` + +.. code:: + + BACKEND="SlackV3" + BOT_EXTRA_BACKEND_DIR=/opt/errbot/backend + +4. Clone `err-backend-slackv3` into the backend directory and install module dependencies. + +.. code:: + + cd /opt/errbot/backend + git clone https://github.com/errbotio/err-backend-slackv3 + # to get a specific release use `--branch `, e.g. `--branch v0.1.0` + git clone --depth 1 https://github.com/errbotio/err-backend-slackv3 + pip install . + +5. Configure the slack bot token, signing secret (Events API with Request URLs) and/or app token (Events API with Socket-mode). Located in `/opt/errbot/config.py` + +.. code:: + + BOT_IDENTITY = { + 'token': 'xoxb-...', + 'signing_secret': "", + 'app_token': "xapp-..." + } + + +Setting up Slack application +------------------------------------------------------------------------ + +Legacy token with RTM +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This was the original method for connecting a bot to Slack. Create a bot token, configure errbot with it and start using Slack. +Pay attention when reading `real time messaging `_ explaining how to create a "classic slack application". Slack does not allow Legacy bot tokens to use the Events API. + +Current token with Events Request URLs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This is by far the most complex method of having errbot communicate with Slack. The architecture involves server to client communication over HTTP. This means the Slack server must be able to reach errbot's `/slack/events` endpoint via the internet using a valid SSL connection. +How to set up such an architecture is outside the scope of this readme and is left as an exercise for the reader. Read `slack events api document `_ for details on how to configure the Slack app and request URL. + +Current token with Events Socket-mode client +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Create a current bot token, enable socket mode. Configure errbot to use the bot and app tokens and start using Slack. +Read `socket-mode `_ for instructions on setting up Socket-mode. + +Ensure the bot is also subscribed to the following events: + +- `file_created` +- `file_public` +- `message.channels` +- `message.groups` +- `message.im` + +Bot Admins +------------------------------------------------------------------------ +Slack changed the way users are uniquely identified from display name ``@some_name`` to user id ``Uxxxxxx``. Errbot configuration will need to be updated before administrators can be correctly identified aginst the ACL sets. + +The UserID is in plain text format. It can be found in the the Slack full profile page or using the ``!whoami`` command (``person`` field). + +Because BOT_ADMINS is defined as plain text User IDs, they can not be used to send notifications. The mention format ``<@Uxxxxx>`` must be used in the BOT_ADMINS_NOTIFICATIONS configuration setting for errbot to initiate message to bot administrators. diff --git a/docs/developers.rst b/docs/developers.rst new file mode 100644 index 0000000..38e868d --- /dev/null +++ b/docs/developers.rst @@ -0,0 +1,157 @@ +Developers Guide +======================================================================== + +Here you will find information related to developing the slackv3 backend. + + + +Errbot uses external libraries for most backends, which may offer additional +functionality not exposed by Errbot in a generic, backend-agnostic fashion. + +It is possible to access the underlying client used by the backend you are +using in order to provide functionality that isn't otherwise available. +Additionally, interacting directly with the bot internals gives you the freedom +to control Errbot in highly specific ways that may not be officially supported. + +.. warning:: + + The following instructions describe how to interface directly with the underlying bot object and clients of backends. + We offer no guarantees that these internal APIs are stable or that a given backend will continue to use a given client in the future. + The following information is provided **as-is** without any official support. + We can give **no** guarantees about API stability on the topics described below. + + +Getting to the bot object +------------------------------------------------------------------------ + +From within a plugin, you may access `self._bot` in order to get to the instance of the currently running bot class. +For example, with the Telegram backend this would be an instance of :class:`~errbot.backends.telegram.TelegramBackend`: + +.. code-block:: python + + >>> type(self._bot) + + +To find out what methods each bot backend has, you can take a look at the documentation of the various backends in the :mod:`errbot.backends` package. + +Plugins may use the `self._bot` object to offer tailored, backend-specific functionality on specific backends. +To determine which backend is being used, a plugin can inspect the `self._bot.mode` property. +The following table lists all the values for `mode` for the official backends: + +============================================ ========== +Backend Mode value +============================================ ========== +:class:`~errbot.backends.irc` irc +:class:`~errbot.backends.slackv3` slackv3 +:class:`~errbot.backends.telegram_messenger` telegram +:class:`~errbot.backends.test` test +:class:`~errbot.backends.text` text +:class:`~errbot.backends.xmpp` xmpp +============================================ ========== + +Here's an example of using a backend-specific feature. In Slack, emoji reactions can be added to messages the bot +receives using the `add_reaction` and `remove_reaction` methods. For example, you could add an hourglass to messages +that will take a long time to reply fully to. + +.. code-block:: python + + from errbot import BotPlugin, botcmd + + class PluginExample(BotPlugin): + @botcmd + def longcompute(self, mess, args): + if self._bot.mode == "slack": + self._bot.add_reaction(mess, "hourglass") + else: + yield "Finding the answer..." + + time.sleep(10) + + yield "The answer is: 42" + if self._bot.mode == "slack": + self._bot.remove_reaction(mess, "hourglass") + + +Getting to the underlying client library +------------------------------------------------------------------------ + +Most of the backends use a third-party library in order to connect to their respective network. +These libraries often support additional features which Errbot doesn't expose in a generic +way so you may wish to make use of these in order to access advanced functionality. + +Backends set their own attribute(s) to point to the underlying libraries' client instance(s). +The following table lists these attributes for the official backends, along with the library used by the backend: + + +============================================ =============================== ==================================================== +Backend Library Attribute(s) +============================================ =============================== ==================================================== +:class:`~errbot.backends.irc` `irc`_ ``self._bot.conn`` ``self._bot.conn.connection`` +:class:`~errbot.backends.slackv3` `slacksdk`_, `_slackeventsapi`_ ``self._bot.slack_sdk`` ``self._bot.slackeventsapi`` +:class:`~errbot.backends.telegram_messenger` `telegram-python-bot`_ ``self._bot.telegram`` +:class:`~errbot.backends.xmpp` `slixmpp`_ ``self._bot.conn`` +============================================ =============================== ==================================================== + +.. _irc: https://pypi.org/project/irc/ +.. _`telegram-python-bot`: https://pypi.org/project/python-telegram-bot +.. _slacksdk: https://slack.dev/python-slack-sdk/ +.. _slackeventsapi: https://github.com/slackapi/python-slack-events-api +.. _slixmpp: https://pypi.org/project/slixmpp + + +Slack v3 Backend +======================================================================== + +.. Note:: + + Slack provides advanced features above and beyond simple text messaging in the form of Slack Applications and Workflows. These features cross into the domain of application development and use + specialised events and data structures. Support for these features is asked for by plugin developers, and for good reasons as their ChatOps requirements grow. It is at this level of sophistication + that errbot's framework becomes a hinderance rather than a help because errbot's design goal is to be backend agnostic to ensure portability between chat service providers. For advanced use cases + as mentioned early, it is strongly recommended to use (Slack's Bolt Application Framework)[https://slack.dev/bolt-python/concepts] to write complex application/workflows in Slack. + +The Slack v3 backend provides some advanced formatting through direct access to the underlying python module functionality. +Below are examples of how to make use of Slack specific features. + +Slack attachments and block +------------------------------------------------------------------------ + +It is possible to pass additional payload data along with the message. When this extra information is present, the slack python module will process it. +The below example shows how to send attachments (deprecated) or blocks for advanced text message formatting. + +.. code-block:: python + + from slack_sdk.models.blocks import SectionBlock, TextObject + from errbot.backends.base import Message + + @botcmd + def hello(self, msg, args): + """Say hello to someone""" + msg.body = "Using the sent message to shorten the code example" + msg.extras['attachments'] = [{ + 'color': '5F4B48', + 'fallback': 'Help text for: Bot plugin', + 'footer': 'For these commands: `help Bot`', + 'text': 'General commands to do with the ChatOps bot', + 'title': 'Bot' + },{ + 'color': 'FAF5F5', + 'fallback': 'Help text for: Example plugin', + 'footer': 'For these commands: `help Example`', + 'text': 'This is a very basic plugin to try out your new installation and get you started.\n Feel free to tweak me to experiment with Errbot.\n You can find me in your init directory in the subdirectory plugins.', + 'title': 'Example' + }] + + self._bot.send_message(msg) + + + # Example with the blocks SDK + msg = Message() + msg.extras['blocks'] = [ + SectionBlock( + text=TextObject( + text="Welcome to Slack! :wave: We're so glad you're here. :blush:\n\n", + type="mrkdwn" + ) + ).to_dict() + ] + self._bot.send_message(msg) diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..64ff0c5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,28 @@ + +.. title:: err-backend-slackv3 documentation + +Errbot Slackv3 Backend Documentation +======================================================================== + +Welcome to the ``err-backend-slackv3`` documentation page. You'll be able to find +installation instructions, configuration information and examples of using some of the backend's features. + +The ``err-backend-slackv3`` backend lets you connect to the `Slack `_ messaging service using the +Real-time Messaging Protocol, Events Request-URL or Events Socket mode. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + installation.rst + configuration.rst + users.rst + developers.rst + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..d17d441 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,134 @@ +Installation +======================================================================== + + + + + +Dependencies +------------------------------------------------------------------------ + +You need to install Slackv3 dependencies before using Errbot with Slack. In the below example, +it is assumed slackv3 has been download to the /opt/errbot/backends directory and errbot has been +installed in a python virtual environment (adjust the command to your errbot's installation):: + + git clone https://github.com/errbotio/err-backend-slackv3.git + source /opt/errbot/bin/activate + /opt/errbot/bin/pip install . + +Connection Methods +------------------------------------------------------------------------ + +Slack's OAuth and API architecture has evolved and caused some confusion. No matter which OAuth bot token you're using or the API architecture in your environment, slackv3 will support it. + +The backend will automatically detect which token and architecture you have and start listening for Slack events in the right way: + + - Legacy tokens (OAuthv1) with Real Time Messaging (RTM) API + - Current token (OAuthv2) with Event API using the Event Subscriptions and Request URL. + - Current token (Oauthv2) with Event API using the Socket-mode client. + +Legacy tokens (OAuthv1) with Real Time Messaging (RTM) API +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When the following oauth scopes are detected, the RTM protocol will be used. These scopes are automatically present when using a legacy token. + +.. code:: + + "apps" + "bot" + "bot:basic" + "client" + "files:write:user" + "identify" + "post" + "read" + +- Current token (OAuthv2) with Event API using the Event Subscriptions and Request URL. +- Current token (OAuthv2) with Event API using the Socket-mode client. + +Backend Installation +------------------------------------------------------------------------ + +These instructions are for errbot running inside a Python virtual environment. You will need to adapt these steps to your own errbot instance setup. +The virtual environment is created in `/opt/errbot/virtualenv` and errbot initialised in `/opt/errbot`. The extra backend directory is in `/opt/errbot/backend`. + +1. Create the errbot virtual environment + +.. code:: + + mkdir -p /opt/errbot/backend + python3 -m venv /opt/errbot/virtualenv + +2. Install and initialise errbot. `See here for details `_ + +.. code:: + + source /opt/errbot/virtualenv/bin/activate + pip install errbot + cd /opt/errbot + errbot --init + +3. Configure the slackv3 backend and extra backend directory. Located in `/opt/errbot/config.py` + +.. code:: + + BACKEND="SlackV3" + BOT_EXTRA_BACKEND_DIR=/opt/errbot/backend + +4. Clone `err-backend-slackv3` into the backend directory and install module dependencies. + +.. code:: + + cd /opt/errbot/backend + git clone https://github.com/errbotio/err-backend-slackv3 + # to get a specific release use `--branch `, e.g. `--branch v0.1.0` + git clone --depth 1 https://github.com/errbotio/err-backend-slackv3 + pip install . + +5. Configure the slack bot token, signing secret (Events API with Request URLs) and/or app token (Events API with Socket-mode). Located in `/opt/errbot/config.py` + +.. code:: + + BOT_IDENTITY = { + 'token': 'xoxb-...', + 'signing_secret': "", + 'app_token': "xapp-..." + } + + +Setting up Slack application +------------------------------------------------------------------------ + +Legacy token with RTM +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This was the original method for connecting a bot to Slack. Create a bot token, configure errbot with it and start using Slack. +Pay attention when reading `real time messaging `_ explaining how to create a "classic slack application". Slack does not allow Legacy bot tokens to use the Events API. + +Current token with Events Request URLs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This is by far the most complex method of having errbot communicate with Slack. The architecture involves server to client communication over HTTP. This means the Slack server must be able to reach errbot's `/slack/events` endpoint via the internet using a valid SSL connection. +How to set up such an architecture is outside the scope of this readme and is left as an exercise for the reader. Read `slack events api document `_ for details on how to configure the Slack app and request URL. + +Current token with Events Socket-mode client +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Create a current bot token, enable socket mode. Configure errbot to use the bot and app tokens and start using Slack. +Read `socket-mode `_ for instructions on setting up Socket-mode. + +Ensure the bot is also subscribed to the following events: + +- `file_created` +- `file_public` +- `message.channels` +- `message.groups` +- `message.im` + +Bot Admins +------------------------------------------------------------------------ +Slack changed the way users are uniquely identified from display name ``@some_name`` to user id ``Uxxxxxx``. Errbot configuration will need to be updated before administrators can be correctly identified aginst the ACL sets. + +The UserID is in plain text format. It can be found in the the Slack full profile page or using the ``!whoami`` command (``person`` field). + +Because BOT_ADMINS is defined as plain text User IDs, they can not be used to send notifications. The mention format ``<@Uxxxxx>`` must be used in the BOT_ADMINS_NOTIFICATIONS configuration setting for errbot to initiate message to bot administrators. diff --git a/docs/users.rst b/docs/users.rst new file mode 100644 index 0000000..d7303be --- /dev/null +++ b/docs/users.rst @@ -0,0 +1,206 @@ +Users Guide +======================================================================== + +Errbot uses external libraries for most backends, which may offer additional +functionality not exposed by Errbot in a generic, backend-agnostic fashion. + +It is possible to access the underlying client used by the backend in order to provide functionality that isn't otherwise available from errbot's framework. Additionally, interacting directly with the bot internals gives you the freedom to control Errbot in highly specific ways that may not be officially supported. + +.. warning:: + + The following instructions describe how to interface directly with the underlying bot object and clients of backends. + We offer no guarantees that these internal APIs are stable or that a given backend will continue to use a given client in the future. + The following information is provided **as-is** without any official support. + We can give **no** guarantees about API stability on the topics described below. + + +Getting to the bot object +------------------------------------------------------------------------ + +From within a plugin, you may access `self._bot` in order to get to the instance of the currently running bot class. +For example, with the Telegram backend this would be an instance of :class:`~errbot.backends.telegram.TelegramBackend`: + +.. code-block:: python + + >>> type(self._bot) + + +To find out what methods each bot backend has, you can take a look at the documentation of the various backends in the :mod:`errbot.backends` package. + +Plugins may use the `self._bot` object to offer tailored, backend-specific functionality on specific backends. +To determine which backend is being used, a plugin can inspect the `self._bot.mode` property. +The following table lists all the values for `mode` for the official backends: + +============================================ ========== +Backend Mode value +============================================ ========== +:class:`~errbot.backends.irc` irc +:class:`~errbot.backends.slackv3` slackv3 +:class:`~errbot.backends.telegram_messenger` telegram +:class:`~errbot.backends.test` test +:class:`~errbot.backends.text` text +:class:`~errbot.backends.xmpp` xmpp +============================================ ========== + +Here's an example of using a backend-specific feature. In Slack, emoji reactions can be added to messages the bot +receives using the `add_reaction` and `remove_reaction` methods. For example, you could add an hourglass to messages +that will take a long time to reply fully to. + +.. code-block:: python + + from errbot import BotPlugin, botcmd + + class PluginExample(BotPlugin): + @botcmd + def longcompute(self, mess, args): + if self._bot.mode == "slack": + self._bot.add_reaction(mess, "hourglass") + else: + yield "Finding the answer..." + + time.sleep(10) + + yield "The answer is: 42" + if self._bot.mode == "slack": + self._bot.remove_reaction(mess, "hourglass") + + +Getting to the underlying client library +------------------------------------------------------------------------ + +Most of the backends use a third-party library in order to connect to their respective network. +These libraries often support additional features which Errbot doesn't expose in a generic +way so you may wish to make use of these in order to access advanced functionality. + +Backends set their own attribute(s) to point to the underlying libraries' client instance(s). +The following table lists these attributes for the official backends, along with the library used by the backend: + + +============================================ =============================== ==================================================== +Backend Library Attribute(s) +============================================ =============================== ==================================================== +:class:`~errbot.backends.irc` `irc`_ ``self._bot.conn`` ``self._bot.conn.connection`` +:class:`~errbot.backends.slackv3` `slacksdk`_, `_slackeventsapi`_ ``self._bot.slack_sdk`` ``self._bot.slackeventsapi`` +:class:`~errbot.backends.telegram_messenger` `telegram-python-bot`_ ``self._bot.telegram`` +:class:`~errbot.backends.xmpp` `slixmpp`_ ``self._bot.conn`` +============================================ =============================== ==================================================== + +.. _irc: https://pypi.org/project/irc/ +.. _`telegram-python-bot`: https://pypi.org/project/python-telegram-bot +.. _slacksdk: https://slack.dev/python-slack-sdk/ +.. _slackeventsapi: https://github.com/slackapi/python-slack-events-api +.. _slixmpp: https://pypi.org/project/slixmpp + + +Slack v3 Backend +======================================================================== + +.. Note:: + + Slack provides advanced features above and beyond simple text messaging in the form of Slack Applications and Workflows. These features cross into the domain of application development and use + specialised events and data structures. Support for these features is asked for by plugin developers, and for good reasons as their ChatOps requirements grow. It is at this level of sophistication + that errbot's framework becomes a hinderance rather than a help because errbot's design goal is to be backend agnostic to ensure portability between chat service providers. For advanced use cases + as mentioned early, it is strongly recommended to use (Slack's Bolt Application Framework)[https://slack.dev/bolt-python/concepts] to write complex application/workflows in Slack. + +The Slack v3 backend provides some advanced formatting through direct access to the underlying python module functionality. +Below are examples of how to make use of Slack specific features. + +Slack attachments and block +------------------------------------------------------------------------ + +It is possible to pass additional payload data along with the message. When this extra information is present, the slack python module will process it. +The below example shows how to send attachments (deprecated) or blocks for advanced text message formatting. + +.. code-block:: python + + from slack_sdk.models.blocks import SectionBlock, TextObject + from errbot.backends.base import Message + + @botcmd + def hello(self, msg, args): + """Say hello to someone""" + msg.body = "Using the sent message to shorten the code example" + msg.extras['attachments'] = [{ + 'color': '5F4B48', + 'fallback': 'Help text for: Bot plugin', + 'footer': 'For these commands: `help Bot`', + 'text': 'General commands to do with the ChatOps bot', + 'title': 'Bot' + },{ + 'color': 'FAF5F5', + 'fallback': 'Help text for: Example plugin', + 'footer': 'For these commands: `help Example`', + 'text': 'This is a very basic plugin to try out your new installation and get you started.\n Feel free to tweak me to experiment with Errbot.\n You can find me in your init directory in the subdirectory plugins.', + 'title': 'Example' + }] + + self._bot.send_message(msg) + + + # Example with the blocks SDK + msg = Message() + msg.extras['blocks'] = [ + SectionBlock( + text=TextObject( + text="Welcome to Slack! :wave: We're so glad you're here. :blush:\n\n", + type="mrkdwn" + ) + ).to_dict() + ] + self._bot.send_message(msg) + + +Bot online status indicator +------------------------------------------------------------------------ + +The online stuats indicator is an option in slack when you configure the bot and assign the oauth roles. + + + +Bot manifest +------------------------------------------------------------------------ + +Slack allows configuration of bot oauth and other parameters through a manifest file. +An example below is provided to demonstrate what information can be supplied. + +display_information: + name: Your Bot Name + description: Description + background_color: "#000000" +features: + bot_user: + display_name: Your Bot Name + always_online: true +oauth_config: + scopes: + bot: + - channels:history + - channels:read + - chat:write + - groups:history + - groups:read + - groups:write + - im:history + - im:read + - im:write + - mpim:read + - mpim:write + - reactions:read + - team:read + - users:read + - users:read.email + - channels:manage +settings: + event_subscriptions: + bot_events: + - message.channels + - message.groups + - message.im + - reaction_added + interactivity: + is_enabled: true + org_deploy_enabled: false + socket_mode_enabled: true + token_rotation_enabled: false + +It may also be necessary to enable _users being able to send message_ checkbox and create an app-level token with `connections:write` access diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b27a9c2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[tool.isort] +multi_line_output = 3 +line_length = 100 +include_trailing_comma = true + +[tool.black] +line-length = 100 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..406fa08 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[pycodestyle] +max-line-length = 100 +statistics = True +count = False diff --git a/src/slackv3/markdown.py b/src/slackv3/markdown.py index df6d4fb..43b7bfb 100644 --- a/src/slackv3/markdown.py +++ b/src/slackv3/markdown.py @@ -5,9 +5,7 @@ from markdown.extensions.extra import ExtraExtension from markdown.preprocessors import Preprocessor -MARKDOWN_LINK_REGEX = re.compile( - r"(?[^\]]+?)\]\((?P[a-zA-Z0-9]+?:\S+?)\)" -) +MARKDOWN_LINK_REGEX = re.compile(r"(?[^\]]+?)\]\((?P[a-zA-Z0-9]+?:\S+?)\)") def slack_markdown_converter(compact_output=False): @@ -15,9 +13,7 @@ def slack_markdown_converter(compact_output=False): This is a Markdown converter for use with Slack. """ enable_format("imtext", IMTEXT_CHRS, borders=not compact_output) - md = Markdown( - output_format="imtext", extensions=[ExtraExtension(), AnsiExtension()] - ) + md = Markdown(output_format="imtext", extensions=[ExtraExtension(), AnsiExtension()]) md.preprocessors.register(LinkPreProcessor(md), "LinkPreProcessor", 30) md.stripTopLevelTags = False return md diff --git a/src/slackv3/person.py b/src/slackv3/person.py index 8f84e45..3e1d911 100644 --- a/src/slackv3/person.py +++ b/src/slackv3/person.py @@ -30,14 +30,12 @@ class SlackPerson(Person): def __init__(self, webclient: WebClient, userid=None, channelid=None): if userid is not None and userid[0] not in ("U", "W", "B"): raise Exception( - f"This is not a Slack user or bot id: {userid} " - "(should start with B, U or W)" + f"This is not a Slack user or bot id: {userid} " "(should start with B, U or W)" ) if channelid is not None and channelid[0] not in ("D", "C", "G"): raise Exception( - f"This is not a valid Slack channelid: {channelid} " - "(should start with D, C or G)" + f"This is not a valid Slack channelid: {channelid} " "(should start with D, C or G)" ) self._userid = userid @@ -95,17 +93,13 @@ def _cache_user_info(self): res = self._webclient.users_info(user=self._userid) if res["ok"] is False: - log.error( - f"Cannot find user with ID {self._userid}. Slack Error: {res['error']}" - ) + log.error(f"Cannot find user with ID {self._userid}. Slack Error: {res['error']}") else: if "bot" in res: self._user_info["display_name"] = res["bot"].get("name", "") else: for attribute in ["real_name", "display_name", "email"]: - self._user_info[attribute] = res["user"]["profile"].get( - attribute, "" - ) + self._user_info[attribute] = res["user"]["profile"].get(attribute, "") team = None # Normal users diff --git a/src/slackv3/room.py b/src/slackv3/room.py index cef4539..b106ffe 100644 --- a/src/slackv3/room.py +++ b/src/slackv3/room.py @@ -163,9 +163,7 @@ def create(self, private=False): try: if private: log.info(f"Creating private conversation {self}.") - self._bot.slack_web.conversations_create( - name=self.name, is_private=True - ) + self._bot.slack_web.conversations_create(name=self.name, is_private=True) else: log.info(f"Creating conversation {self}.") self._bot.slack_web.conversations_create(name=self.name) @@ -240,9 +238,7 @@ def occupants(self): ) if res["ok"] is True: for member in res["members"]: - occupants.append( - SlackRoomOccupant(self._webclient, member, self.id, self._bot) - ) + occupants.append(SlackRoomOccupant(self._webclient, member, self.id, self._bot)) cursor = res["response_metadata"]["next_cursor"] else: log.exception( @@ -253,8 +249,7 @@ def occupants(self): def invite(self, *args): users = { - user["name"]: user["id"] - for user in self._webclient.api_call("users.list")["members"] + user["name"]: user["id"] for user in self._webclient.api_call("users.list")["members"] } for user in args: diff --git a/src/slackv3/slackv3.py b/src/slackv3/slackv3.py index 27e5a5e..b468d46 100644 --- a/src/slackv3/slackv3.py +++ b/src/slackv3/slackv3.py @@ -145,9 +145,7 @@ def _register_identifiers_pickling(self): """ SlackBackend.__build_identifier = self.build_identifier for cls in (SlackPerson, SlackRoomOccupant, SlackRoom): - copyreg.pickle( - cls, SlackBackend._pickle_identifier, SlackBackend._unpickle_identifier - ) + copyreg.pickle(cls, SlackBackend._pickle_identifier, SlackBackend._unpickle_identifier) def update_alternate_prefixes(self): """Converts BOT_ALT_PREFIXES to use the slack ID instead of name @@ -170,9 +168,7 @@ def update_alternate_prefixes(self): f'Failed to look up Slack userid for alternate prefix "{prefix}": {str(e)}' ) - self.bot_alt_prefixes = tuple( - x.lower() for x in self.bot_config.BOT_ALT_PREFIXES - ) + self.bot_alt_prefixes = tuple(x.lower() for x in self.bot_config.BOT_ALT_PREFIXES) log.debug(f"Converted bot_alt_prefixes: {self.bot_config.BOT_ALT_PREFIXES}") def _setup_event_callbacks(self): @@ -370,9 +366,7 @@ def _generic_wrapper(self, event_data): except KeyError: log.debug("Ignoring unsupported Slack event!") - def _sm_generic_event_handler( - self, client: SocketModeClient, req: SocketModeRequest - ): + def _sm_generic_event_handler(self, client: SocketModeClient, req: SocketModeRequest): log.debug( f"Event type: {req.type}\n" f"Envelope ID: {req.envelope_id}\n" @@ -381,9 +375,7 @@ def _sm_generic_event_handler( f"Retry Reason: {req.retry_reason}\n" ) # Acknowledge the request - client.send_socket_mode_response( - SocketModeResponse(envelope_id=req.envelope_id) - ) + client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id)) # Dispatch event to the Event API generic event handler. self._generic_wrapper(req.payload) @@ -394,9 +386,7 @@ def _sm_handle_hello(self, *args): log.debug(f"message listeners : {sm_client.message_listeners}") if event["type"] == "hello": self.connect_callback() - self.callback_presence( - Presence(identifier=self.bot_identifier, status=ONLINE) - ) + self.callback_presence(Presence(identifier=self.bot_identifier, status=ONLINE)) # Stop calling hello handler for future events. sm_client.message_listeners.remove(self._sm_handle_hello) log.info("Unregistered 'hello' handler from socket-mode client") @@ -607,9 +597,7 @@ def username_to_userid(self, name: str): if len(user_ids) == 0: raise UserDoesNotExistError(f"Cannot find user '{username}'.") if len(user_ids) > 1: - raise UserNotUniqueError( - f"'{username}' isn't unique: {len(user_ids)} matches found." - ) + raise UserNotUniqueError(f"'{username}' isn't unique: {len(user_ids)} matches found.") return user_ids[0] @lru_cache(1024) @@ -645,14 +633,10 @@ def channels( References: - https://slack.com/api/conversations.list """ - response = self.slack_web.conversations_list( - exclude_archived=exclude_archived, types=types - ) + response = self.slack_web.conversations_list(exclude_archived=exclude_archived, types=types) channels = [ - channel - for channel in response["channels"] - if channel["is_member"] or not joined_only + channel for channel in response["channels"] if channel["is_member"] or not joined_only ] return channels @@ -679,17 +663,13 @@ def _prepare_message(self, msg): # or card if msg.is_group: to_channel_id = msg.to.id to_humanreadable = ( - msg.to.name - if msg.to.name - else self.channelid_to_channelname(to_channel_id) + msg.to.name if msg.to.name else self.channelid_to_channelname(to_channel_id) ) else: to_humanreadable = msg.to.username to_channel_id = msg.to.channelid if to_channel_id.startswith("C"): - log.debug( - "This is a divert to private message, sending it directly to the user." - ) + log.debug("This is a divert to private message, sending it directly to the user.") to_channel_id = self.get_im_channel(msg.to.userid) return to_humanreadable, to_channel_id @@ -712,9 +692,7 @@ def send_message(self, msg) -> Message: if msg.is_group: to_channel_id = msg.to.id to_humanreadable = ( - msg.to.name - if msg.to.name - else self.channelid_to_channelname(to_channel_id) + msg.to.name if msg.to.name else self.channelid_to_channelname(to_channel_id) ) else: to_humanreadable = msg.to.username @@ -729,9 +707,7 @@ def send_message(self, msg) -> Message: to_channel_id = msg.to.channelid msgtype = "direct" if msg.is_direct else "channel" - log.debug( - f"Sending {msgtype} message to {to_humanreadable} ({to_channel_id})." - ) + log.debug(f"Sending {msgtype} message to {to_humanreadable} ({to_channel_id}).") body = self.md.convert(msg.body) log.debug(f"Message size: {len(body)}.") @@ -823,9 +799,7 @@ def _slack_upload(self, stream: Stream) -> None: else: stream.error() except Exception: - log.exception( - f"Upload of {stream.name} to {stream.identifier.channelname} failed." - ) + log.exception(f"Upload of {stream.name} to {stream.identifier.channelname} failed.") def send_stream_request( self, @@ -871,14 +845,11 @@ def send_card(self, card: Card): attachment["thumb_url"] = card.thumbnail if card.color: - attachment["color"] = ( - COLORS[card.color] if card.color in COLORS else card.color - ) + attachment["color"] = COLORS[card.color] if card.color in COLORS else card.color if card.fields: attachment["fields"] = [ - {"title": key, "value": value, "short": True} - for key, value in card.fields + {"title": key, "value": value, "short": True} for key, value in card.fields ] parts = self.prepare_message_body(card.body, self.message_size_limit) @@ -917,9 +888,7 @@ def __hash__(self): return 0 # this is a singleton anyway def change_presence(self, status: str = ONLINE, message: str = "") -> None: - self.slack_web.users_setPresence( - presence="auto" if status == ONLINE else "away" - ) + self.slack_web.users_setPresence(presence="auto" if status == ONLINE else "away") @staticmethod def prepare_message_body(body, size_limit): @@ -996,9 +965,7 @@ def extract_identifiers_from_string(text): "Unparseable Slack ID, should start with U, B, C, G, D or W (got `%s`)" ) if text[1] not in ("@", "#"): - raise ValueError( - f"Expected '@' or '#' Slack ID prefix but got '{text[1]}'." - ) + raise ValueError(f"Expected '@' or '#' Slack ID prefix but got '{text[1]}'.") text = text[2:-1] if text == "": raise ValueError(exception_message % "") @@ -1034,9 +1001,7 @@ def build_identifier(self, txtrep): :func:`~extract_identifiers_from_string`. """ log.debug(f"Building an identifier from {txtrep}.") - username, userid, channelname, channelid = self.extract_identifiers_from_string( - txtrep - ) + username, userid, channelname, channelid = self.extract_identifiers_from_string(txtrep) if userid is None and username is not None: userid = self.username_to_userid(username) @@ -1148,9 +1113,7 @@ def query_room(self, room): m = SLACK_CLIENT_CHANNEL_HYPERLINK.match(room) if m is not None: - return SlackRoom( - webclient=self.slack_web, channelid=m.groupdict()["id"], bot=self - ) + return SlackRoom(webclient=self.slack_web, channelid=m.groupdict()["id"], bot=self) return SlackRoom(webclient=self.slack_web, name=room, bot=self) @@ -1204,10 +1167,7 @@ def process_mentions(self, text): try: identifier = self.build_identifier(word) except Exception as e: - log.debug( - f"Tried to build an identifier from '{word}' " - f"but got exception: {e}" - ) + log.debug(f"Tried to build an identifier from '{word}' " f"but got exception: {e}") continue # We track mentions of persons and rooms. diff --git a/tests/person_test.py b/tests/person_test.py index 3f8c08f..1161ce0 100644 --- a/tests/person_test.py +++ b/tests/person_test.py @@ -229,14 +229,10 @@ class SlackPersonTests(unittest.TestCase): def setUp(self): self.webclient = MagicMock() self.webclient.users_info.return_value = SlackPersonTests.USER_INFO_OK - self.webclient.conversations_info.return_value = ( - SlackPersonTests.CHANNEL_INFO_PUBLIC_OK - ) + self.webclient.conversations_info.return_value = SlackPersonTests.CHANNEL_INFO_PUBLIC_OK self.userid = "W012A3CDE" self.channelid = "C012AB3CD" - self.p = SlackPerson( - self.webclient, userid=self.userid, channelid=self.channelid - ) + self.p = SlackPerson(self.webclient, userid=self.userid, channelid=self.channelid) def test_wrong_userid(self): with self.assertRaises(Exception): @@ -293,9 +289,7 @@ def test_channelname(self): self.webclient.conversations_info.assert_called_once_with(channel="C012AB3CD") def test_channelname_channel_not_found(self): - self.webclient.conversations_info.return_value = ( - SlackPersonTests.CHANNEL_INFO_FAIL - ) + self.webclient.conversations_info.return_value = SlackPersonTests.CHANNEL_INFO_FAIL with self.assertRaises(RoomDoesNotExistError) as e: self.p = SlackPerson(self.webclient, channelid="C012AB3CD") self.p.channelname @@ -324,9 +318,7 @@ def test_to_string(self): self.assertEqual(str(self.p), "<@W012A3CDE>") def test_equal(self): - self.another_p = SlackPerson( - self.webclient, userid=self.userid, channelid=self.channelid - ) + self.another_p = SlackPerson(self.webclient, userid=self.userid, channelid=self.channelid) self.assertTrue(self.p == self.another_p) self.assertFalse(self.p == "this is not a person") diff --git a/tests/slack_test.py b/tests/slack_test.py index e2e2aac..0ddcdd3 100644 --- a/tests/slack_test.py +++ b/tests/slack_test.py @@ -353,9 +353,7 @@ def test_extract_identifiers(self): self.assertEqual(extract_from("@person"), ("person", None, None, None)) - self.assertEqual( - extract_from("#general/someuser"), ("someuser", None, "general", None) - ) + self.assertEqual(extract_from("#general/someuser"), ("someuser", None, "general", None)) self.assertEqual(extract_from("#general"), (None, None, "general", None)) @@ -379,9 +377,7 @@ def test_extract_identifiers(self): def test_build_identifier(self): self.slack.slack_web = MagicMock() - self.slack.slack_web.conversations_info.return_value = ( - CONVERSATION_INFO_PUBLIC_OK - ) + self.slack.slack_web.conversations_info.return_value = CONVERSATION_INFO_PUBLIC_OK self.slack.slack_web.users_info.return_value = USER_INFO_OK self.slack.slack_web.conversations_open.return_value = CONVERSATION_OPEN_OK @@ -412,9 +408,7 @@ def test_uri_sanitization(self): ) self.assertEqual( - sanitize( - "Pretty URL Testing: with " "more text" - ), + sanitize("Pretty URL Testing: with " "more text"), "Pretty URL Testing: example.org with more text", ) @@ -468,12 +462,8 @@ def test_slack_markdown_link_preprocessor(self): ) def test_mention_processing(self): - self.slack.slack_web.conversations_info.return_value = ( - CHANNEL_INFO_DIRECT_1TO1_OK - ) - self.slack.slack_web.conversations_open.return_value = ( - CHANNEL_INFO_DIRECT_1TO1_OK - ) + self.slack.slack_web.conversations_info.return_value = CHANNEL_INFO_DIRECT_1TO1_OK + self.slack.slack_web.conversations_open.return_value = CHANNEL_INFO_DIRECT_1TO1_OK mentions = self.slack.process_mentions @@ -532,9 +522,7 @@ def test_send_message(self): def test_send_ephemeral_message(self): self.slack.slack_web = MagicMock() - self.slack.slack_web.chat_postEphemeral.return_value = ( - SUCCESSFUL_EPHEMERAL_MESSAGE_RESPONSE - ) + self.slack.slack_web.chat_postEphemeral.return_value = SUCCESSFUL_EPHEMERAL_MESSAGE_RESPONSE # Mock an empty plugin manager (we're not testing plugins here) mocked_plugin_manager = MagicMock() @@ -550,9 +538,7 @@ def test_send_ephemeral_message(self): def test_update_message(self): self.slack.slack_web = MagicMock() - self.slack.slack_web.chat_update.return_value = ( - SUCCESSFUL_UPDATE_MESSAGE_RESPONSE - ) + self.slack.slack_web.chat_update.return_value = SUCCESSFUL_UPDATE_MESSAGE_RESPONSE # Mock an empty plugin manager (we're not testing plugins here) mocked_plugin_manager = MagicMock()