Repository: opsdroid/opsdroid

Issue:
Mattermost messages should be properly filtered before they are even pared by OpsDroid
# Description
Mattermost send the full event stream to OpsDroid, which in turn will parse all events it can handle. At the skill level we have written a custom constraint that will then filter out the following events:

1. Emoji Reactions where the emoji was not the first; OpsDroid should only parse on the first reaction of the configured emoji. If people later add the same reaction, OpsDroid should not spam additional responses
2. If the channel is not a direct message channel, OpsDroid should ignore all messages except for those where it was specifically tagged to not spam its responses and annoy everyone; this needs to be configurable, maybe someone has a use case in the future where they want all messages to be processed

At that level the messages have already been process by Rasa NLU for example, and thus consumed unnecessary CPU cycles. In addition it's not very intuitive to new users that they have to implement custom constraints to get the most common use cases running.

This issue aims at adding sensible defaults to cover the common use cases out of the box, but allow them to be disabled for advanced users.

## Additional Details
You can assign this issue to me 😃

Hints from issue comments:

Solution patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index e9634b4..31663ff 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -8,6 +8,8 @@ connector/gitter:
   - opsdroid/connector/gitter/**/*
 connector/matrix:
   - opsdroid/connector/matrix/**/*
+connector/mattermost:
+  - opsdroid/connector/mattermost/**/*
 connector/rocketchat:
   - opsdroid/connector/rocketchat/**/*
 connector/slack:
diff --git a/.github/workflows/deploy-docker-dev.yml b/.github/workflows/deploy-docker-dev.yml
index a4c8393..a697191 100644
--- a/.github/workflows/deploy-docker-dev.yml
+++ b/.github/workflows/deploy-docker-dev.yml
@@ -7,7 +7,39 @@ on:
       - 'master'
 
 jobs:
+  deploy_ghcr_only:
+    if: github.repository!='opsdroid/opsdroid' && vars.ALLOW_DEPLOYMENTS=='true' # only run on forks with the opt-in variable; no need for docker hub there
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout Repos
+      uses: actions/checkout@v3
+    - name: Set up Docker Buildx
+      id: builder
+      uses: docker/setup-buildx-action@v2
+    - name: Docker meta
+      id: docker_meta
+      uses: docker/metadata-action@v3
+      with:
+        images: |
+          ghcr.io/${{ github.repository_owner }}/opsdroid
+        tags: |
+          type=raw,value=dev
+    - name: Login to GitHub Container Registry
+      uses: docker/login-action@v2
+      with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+    - name: Build and push
+      uses: docker/build-push-action@v3
+      with:
+        push: true
+        tags: ${{ steps.docker_meta.outputs.tags }}
+        platforms: linux/amd64
+        labels: ${{ steps.docker_meta.outputs.labels }}
+
   deploy:
+    if: github.repository=='opsdroid/opsdroid' # only run on official repo, not on forks
     runs-on: ubuntu-latest
     steps:
     - name: Checkout Repos
diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml
index 81568ad..9246017 100644
--- a/.github/workflows/deploy-docker.yml
+++ b/.github/workflows/deploy-docker.yml
@@ -5,7 +5,41 @@ on:
     types: [released]
 
 jobs:
+  deploy_ghcr_only:
+    if: github.repository!='opsdroid/opsdroid' && vars.ALLOW_DEPLOYMENTS=='true' # only run on forks with the opt-in variable; no need for docker hub there
+    runs-on: ubuntu-latest
+    steps:
+    - name: Checkout Repo
+      uses: actions/checkout@v3
+    - name: Setup QEMU
+      uses: docker/setup-qemu-action@v2
+      with:
+        platforms: 'arm64,arm'
+    - name: Set up Docker Buildx
+      uses: docker/setup-buildx-action@v2
+    - name: Docker meta
+      id: docker_meta
+      uses: docker/metadata-action@v3
+      with:
+        images: |
+          ghcr.io/${{ github.repository_owner }}/opsdroid
+        tags: |
+          type=semver,pattern={{raw}}
+    - name: Login to GitHub Container Registry
+      uses: docker/login-action@v2 
+      with:
+        registry: ghcr.io
+        username: ${{ github.repository_owner }}
+        password: ${{ secrets.GITHUB_TOKEN }}
+    - name: Build and push
+      uses: docker/build-push-action@v3
+      with:
+        push: true
+        tags: ${{ steps.docker_meta.outputs.tags }}
+        platforms: linux/amd64
+        labels: ${{ steps.docker_meta.outputs.labels }}
   deploy:
+    if: github.repository=='opsdroid/opsdroid' # only run on official repo, not on forks
     runs-on: ubuntu-latest
     steps:
     - name: Checkout Repo
diff --git a/.github/workflows/deploy-pypi.yml b/.github/workflows/deploy-pypi.yml
index 9beca60..d1c83cb 100644
--- a/.github/workflows/deploy-pypi.yml
+++ b/.github/workflows/deploy-pypi.yml
@@ -6,6 +6,7 @@ on:
 
 jobs:
   deploy:
+    if: github.repository=='opsdroid/opsdroid' # only run on official repo, not on forks
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v2
diff --git a/docs/connectors/mattermost.md b/docs/connectors/mattermost.md
index 04266cf..3366269 100644
--- a/docs/connectors/mattermost.md
+++ b/docs/connectors/mattermost.md
@@ -28,6 +28,9 @@ connectors:
 
     # OpsDroid should parse messages that have received this emoji
     emoji-trigger: "opsdroid-emoji" # default: None
+
+    # OpsDroid should only parse messages in channels if the bot is being tagged directly; direct messages between you and the bot always ignore this and OpsDroid will always respond
+    trigger-on-mention: True # default: True
 ```
 
 ## Usage
diff --git a/docs/contributing/workflow.md b/docs/contributing/workflow.md
index becbeeb..1d871e4 100644
--- a/docs/contributing/workflow.md
+++ b/docs/contributing/workflow.md
@@ -120,3 +120,9 @@ congratulations :)
 ```
 
 This tells you that your linting is good and you can push these changes to Github.
+
+## Manual tests
+
+Make sure to test the changes not just in unit tests but also in a running application. To help with using an objective baseline for the test, you can enable deployments for your local fork and test with the docker images created from your `main` branch and deployed directly to the GitHub Container Registry (ghcr.io). These images receive the tag `dev`.
+
+In order to activate deployments, add a variable to your fork under Settings 🠊 Secrets and variables 🠊 Actions 🠊 Variables 🠊 New repository variable 🠊 `ALLOW_DEPLOYMENTS = true`
\ No newline at end of file
diff --git a/opsdroid/connector/mattermost/connector.py b/opsdroid/connector/mattermost/connector.py
index 2d329d6..09136d6 100644
--- a/opsdroid/connector/mattermost/connector.py
+++ b/opsdroid/connector/mattermost/connector.py
@@ -11,6 +11,8 @@ from .driver import Driver
 
 import asyncio
 
+from enum import Enum
+
 _LOGGER = logging.getLogger(__name__)
 CONFIG_SCHEMA = {
     Required("token"): str,
@@ -21,8 +23,16 @@ CONFIG_SCHEMA = {
     "port": int,
     "connect-timeout": int,
     "emoji-trigger": str,
+    "trigger-on-mention": bool,
 }
 
+DIRECT_MESSAGE_TYPE = "D"
+
+
+class EventType(Enum):
+    MESSAGE = 1
+    REACTION = 2
+
 
 class ConnectorMattermost(Connector):
     """A connector for Mattermost."""
@@ -40,8 +50,10 @@ class ConnectorMattermost(Connector):
         self._timeout = config.get("connect-timeout", 30)
         self._use_threads = config.get("use-threads", False)
         self._emoji_trigger = config.get("emoji-trigger", None)
+        self._trigger_on_mention = config.get("trigger-on-mention", True)
 
         self._bot_id = None
+        self._bot_name = None
 
         self._mm_driver = Driver(
             {
@@ -76,9 +88,10 @@ class ConnectorMattermost(Connector):
                 "Mattermost response must contain our own client ID. Otherwise OpsDroid would respond to itself indefinitely."
             )
         self._bot_id = body["id"]
+        self._bot_name = body["username"]
 
         if "username" in body:
-            _LOGGER.info(_("Connected as %s"), body["username"])
+            _LOGGER.info(_("Connected as %s"), self._bot_name)
 
         _LOGGER.info(_("Connected successfully"))
 
@@ -105,10 +118,13 @@ class ConnectorMattermost(Connector):
             post = json.loads(data["post"])
 
             await self.process_mattermost_post(
+                event_type=EventType.MESSAGE,
                 message=post["message"],
                 user_name=data["sender_name"],
                 user_id=post["user_id"],
                 channel_name=data["channel_name"],
+                channel_type=data["channel_type"],
+                reactions=[],  # message was just now created, no reactions yet
                 raw_json_message=raw_json_message,
             )
         elif (
@@ -157,34 +173,67 @@ class ConnectorMattermost(Connector):
                 raw_message["data"]["post"] = raw_post
 
                 await self.process_mattermost_post(
+                    event_type=EventType.REACTION,
                     message=message,
                     user_name=user_name,
                     user_id=user_id,
                     channel_name=channel_name,
+                    channel_type=channel["type"],
+                    reactions=post["metadata"]["reactions"],
                     raw_json_message=raw_message,
                 )
 
     async def process_mattermost_post(
         self,
+        event_type: EventType,
         message: str,
         user_name: str,
         user_id: str,
         channel_name: str,
+        channel_type: str,
+        reactions: list,
         raw_json_message: dict,
     ):
         """Process a Mattermost post."""
         # if connected to Mattermost, don't parse our own messages
-        # (https://github.com/opsdroid/opsdroid/issues/1775)
-        if self._bot_id != user_id:
-            await self.opsdroid.parse(
-                Message(
-                    text=message,
-                    user=user_name,
-                    target=channel_name,
-                    connector=self,
-                    raw_event=raw_json_message,
+        if self._bot_id == user_id:
+            _LOGGER.debug(_("I am the author: ignoring message"))
+            return
+
+        # if a message is not direct message, check if the bot has been mentioned
+        if (
+            event_type == EventType.MESSAGE
+            and self._trigger_on_mention
+            and channel_type != DIRECT_MESSAGE_TYPE
+            and self._bot_name not in message
+        ):
+            _LOGGER.debug(
+                _("Not a direct message and I am not mentioned: ignoring message")
+            )
+            return
+
+        # if the emoji is not the first of its kind, ignore it
+        if event_type == EventType.REACTION and self._emoji_trigger:
+            # check if the emoji is the first of its kind
+            reaction_count = 0
+            for reaction in reactions:
+                if reaction["emoji_name"] == self._emoji_trigger:
+                    reaction_count += 1
+            if reaction_count > 1:
+                _LOGGER.debug(
+                    _("Reaction is not the first of its kind: ignoring message")
                 )
+                return
+
+        await self.opsdroid.parse(
+            Message(
+                text=message,
+                user=user_name,
+                target=channel_name,
+                connector=self,
+                raw_event=raw_json_message,
             )
+        )
 
     @register_event(Message)
     async def send_message(self, message):
diff --git a/opsdroid/connector/mattermost/driver.py b/opsdroid/connector/mattermost/driver.py
index 0979079..341c69f 100644
--- a/opsdroid/connector/mattermost/driver.py
+++ b/opsdroid/connector/mattermost/driver.py
@@ -149,6 +149,9 @@ class Driver:
         return self
 
     async def __aexit__(self, *exc_info):
+        _LOGGER.debug(
+            _("Mattermost: driver teardown initiated"),
+        )
         return await self._client.__aexit__(*exc_info)
 
     async def init_websocket(
@@ -185,6 +188,8 @@ class Driver:
                 aiohttp.WebSocketError,
                 aiohttp.ClientResponseError,
                 aiohttp.ClientConnectionError,
+                # when the aiohttp-connection is closed because of teardown, this error is sometimes raised instead of a more meaningful one
+                AttributeError,
             ) as ex:
                 _LOGGER.info(
                     _("Mattermost: An exception occured in the Websocket: '%s'"),
@@ -195,7 +200,7 @@ class Driver:
                     _(
                         "Mattermost: An unexpected exception occured in the Websocket: '%s'"
                     ),
-                    ex,
+                    repr(ex),
                 )
                 raise ex
 
@@ -207,6 +212,9 @@ class Driver:
         Exceptions are allowed to be raised in here, as the calling function contains
         the (semi-)infinite loop that only breaks if the driver object is closed.
         """
+        _LOGGER.debug(
+            _("Mattermost: started inner websocket loop"),
+        )
         if self._options["scheme"] == "https":
             scheme = "wss"
         elif self._options["scheme"] == "http":
@@ -254,4 +262,4 @@ class Driver:
                 elif received.type in [WSMsgType.CLOSE, WSMsgType.ERROR]:
                     break  # this happens if Mattermost is restarted or the network connection is lost, etc.
 
-        return await asyncio.sleep(0)  # allow for graceful websocket removal
+        return await asyncio.sleep(1)  # allow for graceful websocket removal
diff --git a/opsdroid/helper.py b/opsdroid/helper.py
index 8fb4319..3c05ac2 100644
--- a/opsdroid/helper.py
+++ b/opsdroid/helper.py
@@ -425,6 +425,16 @@ class Timeout:
                 raise self.exception
         return True
 
+    def get_elapsed_seconds(self):
+        """Get the elapsed seconds since the timeout started.
+
+        Returns:
+            float: The elapsed seconds since the timeout started.
+        """
+        if self.start is None:
+            return 0
+        return (datetime.datetime.now() - self.start).total_seconds()
+
     def set_exception(self, e):
         """Modify the default timeout exception.
 
diff --git a/opsdroid/web.py b/opsdroid/web.py
index 96b4378..7698f5f 100644
--- a/opsdroid/web.py
+++ b/opsdroid/web.py
@@ -230,6 +230,11 @@ class Web:
                 await self.site.start()
                 break
             except OSError as e:
+                _LOGGER.debug(
+                    _(
+                        f"Error starting the web server; retry for a total of {self.start_timeout} seconds; already consumed {timeout.get_elapsed_seconds()}: {repr(e)}"
+                    )
+                )
                 await asyncio.sleep(0.1)
                 timeout.set_exception(e)
                 await self.site.stop()


Test patch:
diff --git a/opsdroid/connector/mattermost/tests/responses/channels.789.success.json b/opsdroid/connector/mattermost/tests/responses/channels.789.success.json
deleted file mode 100644
index 8f9da22..0000000
--- a/opsdroid/connector/mattermost/tests/responses/channels.789.success.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "name": "unit_test_channel_789"  
-}
diff --git a/opsdroid/connector/mattermost/tests/responses/posts.456.success.json b/opsdroid/connector/mattermost/tests/responses/posts.456.success.json
deleted file mode 100644
index 6b76cb3..0000000
--- a/opsdroid/connector/mattermost/tests/responses/posts.456.success.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "message": "Unit Test Message"
-}
diff --git a/opsdroid/connector/mattermost/tests/responses/users.123.success.json b/opsdroid/connector/mattermost/tests/responses/users.123.success.json
deleted file mode 100644
index c929488..0000000
--- a/opsdroid/connector/mattermost/tests/responses/users.123.success.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
-    "id": "123",
-    "username": "Name for user 123"  
-}
diff --git a/opsdroid/connector/mattermost/tests/responses/users.me.success.json b/opsdroid/connector/mattermost/tests/responses/users.me.success.json
index 98f5eb6..ea01038 100644
--- a/opsdroid/connector/mattermost/tests/responses/users.me.success.json
+++ b/opsdroid/connector/mattermost/tests/responses/users.me.success.json
@@ -1,4 +1,4 @@
 {
     "id": "test1234",
-    "username": "Some Test Account"  
+    "username": "test_1234_account"
 }
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket.message.success.json b/opsdroid/connector/mattermost/tests/responses/websocket/message/direct/websocket.json
similarity index 71%
rename from opsdroid/connector/mattermost/tests/responses/websocket.message.success.json
rename to opsdroid/connector/mattermost/tests/responses/websocket/message/direct/websocket.json
index 41d5b00..fcb0c20 100644
--- a/opsdroid/connector/mattermost/tests/responses/websocket.message.success.json
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/message/direct/websocket.json
@@ -4,6 +4,7 @@
     "data": {
         "post": "{\"user_id\": \"123\", \"message\": \"Unit Test Message\"}",
         "sender_name": "Unit Test Sender",
-        "channel_name": "Unit Test Channel"
+        "channel_name": "Unit Test Channel",
+        "channel_type": "D"
     }
 }
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/message/mentioned/websocket.json b/opsdroid/connector/mattermost/tests/responses/websocket/message/mentioned/websocket.json
new file mode 100644
index 0000000..64b0ad2
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/message/mentioned/websocket.json
@@ -0,0 +1,10 @@
+{
+    "seq": 1,
+    "event": "posted",
+    "data": {
+        "post": "{\"user_id\": \"123\", \"message\": \"@test_1234_account Unit Test Message\"}",
+        "sender_name": "Unit Test Sender",
+        "channel_name": "Unit Test Channel",
+        "channel_type": "O"
+    }
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/message/self/websocket.json b/opsdroid/connector/mattermost/tests/responses/websocket/message/self/websocket.json
new file mode 100644
index 0000000..961f87c
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/message/self/websocket.json
@@ -0,0 +1,10 @@
+{
+    "seq": 1,
+    "event": "posted",
+    "data": {
+        "post": "{\"user_id\": \"test1234\", \"message\": \"Unit Test Message\"}",
+        "sender_name": "Unit Test Sender",
+        "channel_name": "Unit Test Channel",
+        "channel_type": "D"
+    }
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/message/unmentioned/websocket.json b/opsdroid/connector/mattermost/tests/responses/websocket/message/unmentioned/websocket.json
new file mode 100644
index 0000000..a8be3fb
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/message/unmentioned/websocket.json
@@ -0,0 +1,10 @@
+{
+    "seq": 1,
+    "event": "posted",
+    "data": {
+        "post": "{\"user_id\": \"123\", \"message\": \"Unit Test Message\"}",
+        "sender_name": "Unit Test Sender",
+        "channel_name": "Unit Test Channel",
+        "channel_type": "O"
+    }
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/reaction/first/channels.789.json b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/first/channels.789.json
new file mode 100644
index 0000000..83c17c7
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/first/channels.789.json
@@ -0,0 +1,4 @@
+{
+    "name": "unit_test_channel_789",
+    "type": "O"
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/reaction/first/posts.456.json b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/first/posts.456.json
new file mode 100644
index 0000000..cdfee47
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/first/posts.456.json
@@ -0,0 +1,17 @@
+{
+    "message": "Unit Test Message",
+    "metadata" : {
+        "reactions" : [
+            {
+                "emoji_name" : "emoji_test",
+                "user_id" : "user1",
+                "post_id" : "post1"
+            },
+            {
+                "emoji_name" : "thumbsdown",
+                "user_id" : "user2",
+                "post_id" : "post2"
+            }
+        ]
+    }
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket.reaction.success.json b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/first/websocket.json
similarity index 100%
rename from opsdroid/connector/mattermost/tests/responses/websocket.reaction.success.json
rename to opsdroid/connector/mattermost/tests/responses/websocket/reaction/first/websocket.json
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/channels.789.json b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/channels.789.json
new file mode 100644
index 0000000..83c17c7
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/channels.789.json
@@ -0,0 +1,4 @@
+{
+    "name": "unit_test_channel_789",
+    "type": "O"
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/posts.456.json b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/posts.456.json
new file mode 100644
index 0000000..352e899
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/posts.456.json
@@ -0,0 +1,17 @@
+{
+    "message": "Unit Test Message",
+    "metadata" : {
+        "reactions" : [
+            {
+                "emoji_name" : "emoji_test",
+                "user_id" : "user1",
+                "post_id" : "post1"
+            },
+            {
+                "emoji_name" : "emoji_test",
+                "user_id" : "user2",
+                "post_id" : "post2"
+            }
+        ]
+    }
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/websocket.json b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/websocket.json
new file mode 100644
index 0000000..8d8f67b
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/reaction/second/websocket.json
@@ -0,0 +1,9 @@
+{
+    "seq": 1,
+    "event": "reaction_added",
+    "data": {
+        "reaction": "{\"user_id\": \"123\", \"message\": \"Unit Test Message\", \"emoji_name\": \"emoji_test\", \"post_id\": \"456\", \"channel_id\": \"789\"}",
+        "sender_name": "Not the sender we want!",
+        "channel_name": "Unit Test Channel"
+    }
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket/users.123.json b/opsdroid/connector/mattermost/tests/responses/websocket/users.123.json
new file mode 100644
index 0000000..0683339
--- /dev/null
+++ b/opsdroid/connector/mattermost/tests/responses/websocket/users.123.json
@@ -0,0 +1,4 @@
+{
+    "id": "123",
+    "username": "test_123_account"  
+}
diff --git a/opsdroid/connector/mattermost/tests/responses/websocket.success.json b/opsdroid/connector/mattermost/tests/responses/websocket/websocket.success.json
similarity index 100%
rename from opsdroid/connector/mattermost/tests/responses/websocket.success.json
rename to opsdroid/connector/mattermost/tests/responses/websocket/websocket.success.json
diff --git a/opsdroid/connector/mattermost/tests/skills.py b/opsdroid/connector/mattermost/tests/skills.py
index 9986aaa..52bf56f 100644
--- a/opsdroid/connector/mattermost/tests/skills.py
+++ b/opsdroid/connector/mattermost/tests/skills.py
@@ -6,6 +6,6 @@ _LOGGER = logging.getLogger(__name__)
 
 
 class HelloSkill(Skill):
-    @match_regex(r"^Unit Test Message$")
+    @match_regex(r"^.*Unit Test Message$")
     async def hello(self, message):
         _LOGGER.info("Message arrived in Unit Test skill: '%s'", message.text)
diff --git a/opsdroid/connector/mattermost/tests/test_connector.py b/opsdroid/connector/mattermost/tests/test_connector.py
index dc6b8f0..4463125 100644
--- a/opsdroid/connector/mattermost/tests/test_connector.py
+++ b/opsdroid/connector/mattermost/tests/test_connector.py
@@ -16,29 +16,10 @@ GET_USER_ME_SUCCESS = (
 )
 GET_USER_ME_ERROR = ("/api/v4/users/me", "GET", None, 401)
 
-GET_USER_123_SUCCESS = (
-    "/api/v4/users/123",
-    "GET",
-    get_path("users.123.success.json"),
-    200,
-)
-
 WEBSOCKET_HELLO = (
     "/api/v4/websocket",
     "WEBSOCKET",
-    get_path("websocket.success.json"),
-    200,
-)
-WEBSOCKET_MESSAGE = (
-    "/api/v4/websocket",
-    "WEBSOCKET",
-    get_path("websocket.message.success.json"),
-    200,
-)
-WEBSOCKET_REACTION = (
-    "/api/v4/websocket",
-    "WEBSOCKET",
-    get_path("websocket.reaction.success.json"),
+    get_path("websocket/websocket.success.json"),
     200,
 )
 
@@ -49,18 +30,6 @@ GET_CHANNEL_ID_FOR_CHANNEL_SUCCESS = (
     200,
 )
 CREATE_POST_SUCCESS = ("/api/v4/posts", "POST", None, 200)
-GET_POST_456_SUCCESS = (
-    "/api/v4/posts/456",
-    "GET",
-    get_path("posts.456.success.json"),
-    200,
-)
-GET_CHANNEL_789_SUCCESS = (
-    "/api/v4/channels/789",
-    "GET",
-    get_path("channels.789.success.json"),
-    200,
-)
 
 
 @pytest.fixture
@@ -81,7 +50,7 @@ async def assert_websocket(func):
     retries = 10
     for i in range(retries):
         await asyncio.sleep(
-            0
+            0.01
         )  # wait for the websocket event loop to catch up before checking its log outputs
         try:
             assert func()
@@ -104,12 +73,13 @@ async def test_api_key_success(connector, mock_api, caplog):
     )
 
     assert connector._bot_id == "test1234"
+    assert connector._bot_name == "test_1234_account"
 
     whoami_call = mock_api.get_request("/api/v4/users/me", "GET", 0)
     assert "Authorization" in whoami_call.headers
     assert whoami_call.headers["Authorization"] == "Bearer unittest_token"
 
-    assert "Connected as Some Test Account" in caplog.text
+    assert "Connected as test_1234_account" in caplog.text
     await assert_websocket(
         lambda: "Mattermost Websocket authentification OK" in caplog.text
     )
@@ -247,9 +217,14 @@ async def test_api_send_message_threaded_existing(connector, mock_api, caplog):
 
 
 @pytest.mark.add_response(*GET_USER_ME_SUCCESS)
-@pytest.mark.add_response(*WEBSOCKET_MESSAGE)
+@pytest.mark.add_response(
+    "/api/v4/websocket",
+    "WEBSOCKET",
+    get_path("websocket/message/mentioned/websocket.json"),
+    200,
+)
 @pytest.mark.add_response(*WEBSOCKET_HELLO)
-async def test_websocket_message(connector, mock_api, caplog):
+async def test_websocket_message_mentioned(connector, mock_api, caplog):
     """Tests that messages over the websocket are properly processed by opsdroid."""
     caplog.set_level(
         "DEBUG"
@@ -259,33 +234,117 @@ async def test_websocket_message(connector, mock_api, caplog):
         connector.listen(), name="Unit Test Websocket task"
     )
 
-    assert connector._bot_id == "test1234"
+    await assert_websocket(lambda: "Processing raw Mattermost message" in caplog.text)
 
-    whoami_call = mock_api.get_request("/api/v4/users/me", "GET", 0)
-    assert "Authorization" in whoami_call.headers
-    assert whoami_call.headers["Authorization"] == "Bearer unittest_token"
+    await assert_websocket(
+        lambda: "Message arrived in Unit Test skill: '@test_1234_account Unit Test Message'"
+        in caplog.text
+    )
 
-    assert "Connected as Some Test Account" in caplog.text
+
+@pytest.mark.add_response(*GET_USER_ME_SUCCESS)
+@pytest.mark.add_response(
+    "/api/v4/websocket",
+    "WEBSOCKET",
+    get_path("websocket/message/direct/websocket.json"),
+    200,
+)
+@pytest.mark.add_response(*WEBSOCKET_HELLO)
+async def test_websocket_message_direct(connector, mock_api, caplog):
+    """Tests that messages over the websocket are properly processed by opsdroid."""
+    caplog.set_level(
+        "DEBUG"
+    )  # cannot use 'with caplog.at_level' because it wouldn't apply to the websocket loop
+    await connector.connect()
+    asyncio.get_event_loop().create_task(
+        connector.listen(), name="Unit Test Websocket task"
+    )
+
+    await assert_websocket(lambda: "Processing raw Mattermost message" in caplog.text)
 
     await assert_websocket(
-        lambda: "Mattermost Websocket authentification OK" in caplog.text
+        lambda: "Message arrived in Unit Test skill: 'Unit Test Message'" in caplog.text
+    )
+
+
+@pytest.mark.add_response(*GET_USER_ME_SUCCESS)
+@pytest.mark.add_response(
+    "/api/v4/websocket",
+    "WEBSOCKET",
+    get_path("websocket/message/unmentioned/websocket.json"),
+    200,
+)
+@pytest.mark.add_response(*WEBSOCKET_HELLO)
+async def test_websocket_message_unmentioned(connector, mock_api, caplog):
+    """Tests that messages over the websocket are ignored if the bot was not mentioned in a proper channel."""
+    caplog.set_level(
+        "DEBUG"
+    )  # cannot use 'with caplog.at_level' because it wouldn't apply to the websocket loop
+    await connector.connect()
+    asyncio.get_event_loop().create_task(
+        connector.listen(), name="Unit Test Websocket task"
+    )
+
+    await assert_websocket(lambda: "Processing raw Mattermost message" in caplog.text)
+
+    await assert_websocket(
+        lambda: "Not a direct message and I am not mentioned: ignoring message"
+        in caplog.text
     )
+
+    await assert_websocket(
+        lambda: "Message arrived in Unit Test skill" not in caplog.text
+    )
+
+
+@pytest.mark.add_response(*GET_USER_ME_SUCCESS)
+@pytest.mark.add_response(
+    "/api/v4/websocket",
+    "WEBSOCKET",
+    get_path("websocket/message/self/websocket.json"),
+    200,
+)
+@pytest.mark.add_response(*WEBSOCKET_HELLO)
+async def test_websocket_message_self(connector, mock_api, caplog):
+    """Tests that messages over the websocket are ignored if the bot was not mentioned in a proper channel."""
+    caplog.set_level(
+        "DEBUG"
+    )  # cannot use 'with caplog.at_level' because it wouldn't apply to the websocket loop
+    await connector.connect()
+    asyncio.get_event_loop().create_task(
+        connector.listen(), name="Unit Test Websocket task"
+    )
+
     await assert_websocket(lambda: "Processing raw Mattermost message" in caplog.text)
 
-    print("caplog.text", caplog.text)
+    await assert_websocket(lambda: "I am the author: ignoring message" in caplog.text)
 
     await assert_websocket(
-        lambda: "Message arrived in Unit Test skill: 'Unit Test Message'" in caplog.text
+        lambda: "Message arrived in Unit Test skill" not in caplog.text
     )
 
 
-@pytest.mark.add_response(*GET_CHANNEL_789_SUCCESS)
-@pytest.mark.add_response(*GET_POST_456_SUCCESS)
-@pytest.mark.add_response(*GET_USER_123_SUCCESS)
+@pytest.mark.add_response(
+    "/api/v4/channels/789",
+    "GET",
+    get_path("websocket/reaction/first/channels.789.json"),
+    200,
+)
+@pytest.mark.add_response(
+    "/api/v4/posts/456", "GET", get_path("websocket/reaction/first/posts.456.json"), 200
+)
+@pytest.mark.add_response(
+    "/api/v4/users/123", "GET", get_path("websocket/users.123.json"), 200
+)
 @pytest.mark.add_response(*GET_USER_ME_SUCCESS)
-@pytest.mark.add_response(*WEBSOCKET_REACTION)
+@pytest.mark.add_response(
+    "/api/v4/websocket",
+    "WEBSOCKET",
+    get_path("websocket/reaction/first/websocket.json"),
+    200,
+)
 @pytest.mark.add_response(*WEBSOCKET_HELLO)
-async def test_websocket_reaction(connector, mock_api, caplog):
+async def test_websocket_reaction_first(connector, mock_api, caplog):
     """Tests that messages over the websocket are propery processed by opsdroid."""
 
     connector._emoji_trigger = "emoji_test"
@@ -297,20 +356,54 @@ async def test_websocket_reaction(connector, mock_api, caplog):
     asyncio.get_event_loop().create_task(
         connector.listen(), name="Unit Test Websocket task"
     )
+    await assert_websocket(lambda: "Processing raw Mattermost message" in caplog.text)
 
-    assert connector._bot_id == "test1234"
+    print("CAPLOG TEXT: ", caplog.text)
 
-    whoami_call = mock_api.get_request("/api/v4/users/me", "GET", 0)
-    assert "Authorization" in whoami_call.headers
-    assert whoami_call.headers["Authorization"] == "Bearer unittest_token"
+    await assert_websocket(lambda: "Message arrived in Unit Test skill" in caplog.text)
 
-    assert "Connected as Some Test Account" in caplog.text
 
-    await assert_websocket(
-        lambda: "Mattermost Websocket authentification OK" in caplog.text
+@pytest.mark.add_response(
+    "/api/v4/channels/789",
+    "GET",
+    get_path("websocket/reaction/second/channels.789.json"),
+    200,
+)
+@pytest.mark.add_response(
+    "/api/v4/posts/456",
+    "GET",
+    get_path("websocket/reaction/second/posts.456.json"),
+    200,
+)
+@pytest.mark.add_response(
+    "/api/v4/users/123", "GET", get_path("websocket/users.123.json"), 200
+)
+@pytest.mark.add_response(*GET_USER_ME_SUCCESS)
+@pytest.mark.add_response(
+    "/api/v4/websocket",
+    "WEBSOCKET",
+    get_path("websocket/reaction/second/websocket.json"),
+    200,
+)
+@pytest.mark.add_response(*WEBSOCKET_HELLO)
+async def test_websocket_reaction_second(connector, mock_api, caplog):
+    """Tests that messages over the websocket are propery processed by opsdroid."""
+
+    connector._emoji_trigger = "emoji_test"
+
+    caplog.set_level(
+        "DEBUG"
+    )  # cannot use 'with caplog.at_level' because it wouldn't apply to the websocket loop
+    await connector.connect()
+    asyncio.get_event_loop().create_task(
+        connector.listen(), name="Unit Test Websocket task"
     )
     await assert_websocket(lambda: "Processing raw Mattermost message" in caplog.text)
 
     await assert_websocket(
-        lambda: "Message arrived in Unit Test skill: 'Unit Test Message'" in caplog.text
+        lambda: "Reaction is not the first of its kind: ignoring message" in caplog.text
+    )
+
+    await assert_websocket(
+        lambda: "Message arrived in Unit Test skill" not in caplog.text
     )