Repository: opsdroid/opsdroid

Issue:
Add support for end to end encryption in Matrix Connector
I tried opsdroid in a docker container to connect to a matrix server. So far everything works fine. But I realized that opsdroid does react in unencrypted rooms only. I assume that there is no end-to-end-encryption support built in the matrix connector. Is there a possibility to enable encryption in opsdroid?


Discussion:
cc @Cadair @SolarDrew 
It's something I want to add, but I am unlikely to have time in the near future. To do it we need to change the underlying matrix library. 

In the short term if you want to use opsdroid in encrypted rooms the best thing would be to use https://github.com/matrix-org/pantalaimon which is an e2ee proxy.
Is there anything we can do in opsdroid in the short term to warn users about this? Perhaps detect the room is encrypted and log a warning message.
We could emit a warning if we receive encrypted events, would be easier to detect than rooms.
The way I propose we would fix this, is change the underlying layer of the matrix connector to use [matrix-nio](https://github.com/poljar/matrix-nio) which is a more actively maintained Python library, and has built in support for asyncio.

For more details on using `matrix-nio` there is this: https://matrix.org/docs/guides/usage-of-matrix-nio guide.

Resolving patch:
diff --git a/opsdroid/connector/matrix/connector.py b/opsdroid/connector/matrix/connector.py
--- a/opsdroid/connector/matrix/connector.py
+++ b/opsdroid/connector/matrix/connector.py
@@ -1,5 +1,4 @@
 """Connector for Matrix (https://matrix.org)."""
-
 import re
 import logging
 import functools
@@ -10,7 +9,7 @@
 from voluptuous import Required
 
 from opsdroid.connector import Connector, register_event
-from opsdroid import events
+from opsdroid import events, const
 
 from .html_cleaner import clean
 from .create_events import MatrixEventCreator
@@ -18,6 +17,7 @@
 
 import nio
 import json
+from pathlib import Path
 
 _LOGGER = logging.getLogger(__name__)
 CONFIG_SCHEMA = {
@@ -27,6 +27,9 @@
     "homeserver": str,
     "nick": str,
     "room_specific_nicks": bool,
+    "device_name": str,
+    "device_id": str,
+    "store_path": str,
 }
 
 __all__ = ["ConnectorMatrix"]
@@ -74,7 +77,7 @@ def __init__(self, config, opsdroid=None):  # noqa: D107
         self.room_ids = {}
         self.default_target = self.rooms["main"]["alias"]
         self.mxid = config["mxid"]
-        self.nick = config.get("nick", None)
+        self.nick = config.get("nick")
         self.homeserver = config.get("homeserver", "https://matrix.org")
         self.password = config["password"]
         self.room_specific_nicks = config.get("room_specific_nicks", False)
@@ -83,6 +86,12 @@ def __init__(self, config, opsdroid=None):  # noqa: D107
         self.filter_id = None
         self.connection = None
         self.device_name = config.get("device_name", "opsdroid")
+        self.device_id = config.get("device_id", "opsdroid")
+        self.store_path = config.get(
+            "store_path", str(Path(const.DEFAULT_ROOT_PATH, "matrix"))
+        )
+        self._ignore_unverified = True
+        self._allow_encryption = nio.crypto.ENCRYPTION_ENABLED
 
         self._event_creator = MatrixEventCreator(self)
 
@@ -131,10 +140,45 @@ async def make_filter(self, api, fjson):
         resp_json = await resp.json()
         return int(resp_json["filter_id"])
 
+    async def exchange_keys(self, initial_sync=False):
+        """Send to-device messages and perform key exchange operations."""
+        await self.connection.send_to_device_messages()
+        if self.connection.should_upload_keys:
+            await self.connection.keys_upload()
+        if self.connection.should_query_keys:
+            await self.connection.keys_query()
+
+        if initial_sync:
+            for room_id in self.room_ids.values():
+                try:
+                    await self.connection.keys_claim(
+                        self.connection.get_missing_sessions(room_id)
+                    )
+                except nio.LocalProtocolError:
+                    continue
+        elif self.connection.should_claim_keys:
+            await self.connection.keys_claim(
+                self.connection.get_users_for_key_claiming()
+            )
+
     async def connect(self):
         """Create connection object with chat library."""
-        config = nio.AsyncClientConfig(encryption_enabled=False, pickle_key="")
-        mapi = nio.AsyncClient(self.homeserver, self.mxid, config=config)
+
+        if self._allow_encryption:
+            Path(self.store_path).mkdir(exist_ok=True)
+
+        config = nio.AsyncClientConfig(
+            encryption_enabled=self._allow_encryption,
+            pickle_key="",
+            store_name="opsdroid.db" if self._allow_encryption else "",
+        )
+        mapi = nio.AsyncClient(
+            self.homeserver,
+            self.mxid,
+            config=config,
+            store_path=self.store_path if self._allow_encryption else "",
+            device_id=self.device_id,
+        )
 
         login_response = await mapi.login(
             password=self.password, device_name=self.device_name
@@ -167,7 +211,9 @@ async def connect(self):
         )
 
         # Do initial sync so we don't get old messages later.
-        response = await self.connection.sync(timeout=3000, sync_filter=first_filter_id)
+        response = await self.connection.sync(
+            timeout=3000, sync_filter=first_filter_id, full_state=True
+        )
 
         if isinstance(response, nio.SyncError):
             _LOGGER.error(
@@ -177,6 +223,8 @@ async def connect(self):
 
         self.connection.sync_token = response.next_batch
 
+        await self.exchange_keys(initial_sync=True)
+
         if self.nick:
             display_name = await self.connection.get_displayname(self.mxid)
             if display_name != self.nick:
@@ -214,13 +262,8 @@ async def _parse_sync_response(self, response):
             if roomInfo.timeline:
                 for event in roomInfo.timeline.events:
                     if event.sender != self.mxid:
-                        # if isinstance(event, nio.MegolmEvent):
-                        # event is encrypted
-                        # event = await self.connection.decrypt_event(event)
-
                         if event.source["type"] == "m.room.member":
                             event.source["content"] = event.content
-
                         return await self._event_creator.create_event(
                             event.source, roomid
                         )
@@ -241,6 +284,8 @@ async def listen(self):  # pragma: no cover
 
             _LOGGER.debug(_("Matrix sync request returned."))
 
+            await self.exchange_keys()
+
             message = await self._parse_sync_response(response)
 
             if message:
@@ -323,6 +368,7 @@ async def _send_message(self, message):
             self._get_formatted_message_body(
                 message.text, msgtype=self.message_type(message.target)
             ),
+            ignore_unverified_devices=self._ignore_unverified,
         )
 
     @register_event(events.EditedMessage)
@@ -349,8 +395,11 @@ async def _send_edit(self, message):
             "m.relates_to": {"rel_type": "m.replace", "event_id": edited_event_id},
         }
 
-        return (
-            await self.connection.room_send(message.target, "m.room.message", content),
+        return await self.connection.room_send(
+            message.target,
+            "m.room.message",
+            content,
+            ignore_unverified_devices=self._ignore_unverified,
         )
 
     @register_event(events.Reply)
@@ -368,8 +417,11 @@ async def _send_reply(self, reply):
 
         content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_event_id}}
 
-        return (
-            await self.connection.room_send(reply.target, "m.room.message", content),
+        return await self.connection.room_send(
+            reply.target,
+            "m.room.message",
+            content,
+            ignore_unverified_devices=self._ignore_unverified,
         )
 
     @register_event(events.Reaction)
@@ -382,63 +434,94 @@ async def _send_reaction(self, reaction):
                 "key": reaction.emoji,
             }
         }
-        return await self.connection.room_send(reaction.target, "m.reaction", content)
+        return await self.connection.room_send(
+            reaction.target,
+            "m.reaction",
+            content,
+            ignore_unverified_devices=self._ignore_unverified,
+        )
 
-    async def _get_image_info(self, image):
-        width, height = await image.get_dimensions()
-        return {
-            "w": width,
-            "h": height,
-            "mimetype": await image.get_mimetype(),
-            "size": len(await image.get_file_bytes()),
-        }
+    async def _get_file_info(self, file_event):
+        info_dict = {}
+        if isinstance(file_event, events.Image):
+            info_dict["w"], info_dict["h"] = await file_event.get_dimensions()
+
+        info_dict["mimetype"] = await file_event.get_mimetype()
+        info_dict["size"] = len(await file_event.get_file_bytes())
+
+        return info_dict
 
     async def _file_to_mxc_url(self, file_event):
         """Given a file event return the mxc url."""
         uploaded = False
         mxc_url = None
+        file_dict = None
         if file_event.url:
             url = urlparse(file_event.url)
             if url.scheme == "mxc":
                 mxc_url = file_event.url
 
         if not mxc_url:
+            encrypt_file = (
+                self._allow_encryption
+                and file_event.target in self.connection.store.load_encrypted_rooms()
+            )
             upload_file = await file_event.get_file_bytes()
-            mxc_url = await self.connection.upload(
-                lambda x, y: upload_file, await file_event.get_mimetype()
+            mimetype = await file_event.get_mimetype()
+
+            response = await self.connection.upload(
+                lambda x, y: upload_file, content_type=mimetype, encrypt=encrypt_file
             )
 
-            mxc_url = mxc_url[0].content_uri
+            response, file_dict = response
+
+            if isinstance(response, nio.UploadError):
+                _LOGGER.error(
+                    f"Error while sending the file. Reason: {response.message} (status code {response.status_code})"
+                )
+                return response, None, None
+
+            mxc_url = response.content_uri
             uploaded = True
 
-        return mxc_url, uploaded
+            if file_dict:
+                file_dict["url"] = mxc_url
+                file_dict["mimetype"] = mimetype
+
+        return mxc_url, uploaded, file_dict
 
     @register_event(events.File)
     @register_event(events.Image)
     @ensure_room_id_and_send
     async def _send_file(self, file_event):
-        mxc_url, uploaded = await self._file_to_mxc_url(file_event)
+        mxc_url, uploaded, file_dict = await self._file_to_mxc_url(file_event)
 
-        if isinstance(file_event, events.Image):
-            if uploaded:
-                extra_info = await self._get_image_info(file_event)
-            else:
-                extra_info = {}
-            msg_type = "m.image"
+        if isinstance(mxc_url, nio.UploadError):
+            return
+
+        name = file_event.name or "opsdroid_upload"
+        if uploaded:
+            extra_info = await self._get_file_info(file_event)
         else:
             extra_info = {}
-            msg_type = "m.file"
+        msg_type = f"m.{file_event.__class__.__name__}".lower()
+
+        content = {
+            "body": name,
+            "info": extra_info,
+            "msgtype": msg_type,
+            "url": mxc_url,
+        }
+
+        if file_dict:
+            content["file"] = file_dict
+            del content["url"]
 
-        name = file_event.name or "opsdroid_upload"
         await self.connection.room_send(
             room_id=file_event.target,
             message_type="m.room.message",
-            content={
-                "body": name,
-                "info": extra_info,
-                "msgtype": msg_type,
-                "url": mxc_url,
-            },
+            content=content,
+            ignore_unverified_devices=self._ignore_unverified,
         )
 
     @register_event(events.NewRoom)
@@ -518,7 +601,7 @@ async def _send_room_desciption(self, desc_event):
     @register_event(events.RoomImage)
     @ensure_room_id_and_send
     async def _send_room_image(self, image_event):
-        mxc_url, _ = await self._file_to_mxc_url(image_event.room_image)
+        mxc_url, _, _ = await self._file_to_mxc_url(image_event.room_image)
         return await image_event.respond(matrixevents.MatrixRoomAvatar(mxc_url))
 
     @register_event(events.UserRole)
diff --git a/opsdroid/connector/matrix/create_events.py b/opsdroid/connector/matrix/create_events.py
--- a/opsdroid/connector/matrix/create_events.py
+++ b/opsdroid/connector/matrix/create_events.py
@@ -119,7 +119,13 @@ async def create_message(self, event, roomid):
         return events.Message(**kwargs)
 
     async def _file_kwargs(self, event, roomid):
-        url = await self.connector.mxc_to_http(event["content"]["url"])
+
+        if "url" in event["content"]:
+            url = event["content"]["url"]
+        else:
+            url = event["content"]["file"]["url"]
+
+        url = await self.connector.connection.mxc_to_http(url)
         user = await self.connector.get_nick(roomid, event["sender"])
 
         return dict(


Test patch:
diff --git a/requirements_test.txt b/requirements_test.txt
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -7,6 +7,7 @@ pytest==5.4.2
 pytest-asyncio==0.12.0
 pytest-cov==2.7.1
 pytest-timeout==1.4.0
+pytest-mock==3.2.0
 pydocstyle==5.0.2
 asynctest==0.13.0
 mypy-lang==0.5.0
diff --git a/tests/test_connector_matrix.py b/tests/test_connector_matrix.py
--- a/tests/test_connector_matrix.py
+++ b/tests/test_connector_matrix.py
@@ -3,7 +3,7 @@
 from copy import deepcopy
 
 import aiohttp
-import asynctest
+
 import asynctest.mock as amock
 
 import nio
@@ -19,7 +19,8 @@
 api_string = "nio.AsyncClient.{}"
 
 
-def setup_connector():
+@pytest.fixture
+def connector():
     """Initiate a basic connector setup for testing on"""
     connector = ConnectorMatrix(
         {
@@ -29,16 +30,16 @@ def setup_connector():
             "homeserver": "http://localhost:8008",
         }
     )
+    api = nio.AsyncClient("https://notaurl.com", None)
+    connector.connection = api
+
     return connector
 
 
-class TestConnectorMatrixAsync(asynctest.TestCase):
+@pytest.mark.asyncio
+class TestConnectorMatrixAsync:
     """Test the async methods of the opsdroid Matrix connector class."""
 
-    @pytest.fixture(autouse=True)
-    def inject_fixtures(self, caplog):
-        self._caplog = caplog
-
     @property
     def sync_return(self):
         """Define some mock json to return from the sync method"""
@@ -213,13 +214,7 @@ def filter_json(self):
             },
         }
 
-    def setUp(self):
-        """Basic setting up for tests"""
-        self.connector = setup_connector()
-        self.api = nio.AsyncClient("https://notaurl.com", None)
-        self.connector.connection = self.api
-
-    async def test_make_filter(self):
+    async def test_make_filter(self, connector):
         with amock.patch(api_string.format("send")) as patched_filter:
 
             connect_response = amock.Mock()
@@ -227,17 +222,68 @@ async def test_make_filter(self):
             connect_response.json = amock.CoroutineMock()
             connect_response.json.return_value = {"filter_id": 10}
 
-            self.api.token = "abc"
+            connector.connection.token = "abc"
 
             patched_filter.return_value = asyncio.Future()
             patched_filter.return_value.set_result(connect_response)
 
-            filter_id = await self.connector.make_filter(self.api, self.filter_json)
+            filter_id = await connector.make_filter(
+                connector.connection, self.filter_json
+            )
 
             assert filter_id == 10
             assert patched_filter.called
 
-    async def test_connect(self):
+    async def test_exchange_keys(self, mocker, connector):
+
+        connector.room_ids = {"main": "!aroomid:localhost"}
+
+        patched_send_to_device = mocker.patch(
+            api_string.format("send_to_device_messages"), return_value=asyncio.Future()
+        )
+        patched_send_to_device.return_value.set_result(None)
+
+        mocker.patch(api_string.format("should_upload_keys"), return_value=True)
+        patched_keys_upload = mocker.patch(
+            api_string.format("keys_upload"), return_value=asyncio.Future()
+        )
+        patched_keys_upload.return_value.set_result(None)
+
+        mocker.patch(api_string.format("should_query_keys"), return_value=True)
+        patched_keys_query = mocker.patch(
+            api_string.format("keys_query"), return_value=asyncio.Future()
+        )
+        patched_keys_query.return_value.set_result(None)
+
+        mocker.patch(api_string.format("should_claim_keys"), return_value=True)
+        patched_keys_claim = mocker.patch(
+            api_string.format("keys_claim"), return_value=asyncio.Future()
+        )
+        patched_keys_claim.return_value.set_result(None)
+
+        patched_missing_sessions = mocker.patch(
+            api_string.format("get_missing_sessions"), return_value=None
+        )
+
+        await connector.exchange_keys(initial_sync=True)
+
+        assert patched_send_to_device.called
+        assert patched_keys_upload.called
+        assert patched_keys_query.called
+        patched_keys_claim.assert_called_once_with(patched_missing_sessions())
+
+        patched_get_users = mocker.patch(
+            api_string.format("get_users_for_key_claiming"), return_value=None
+        )
+
+        await connector.exchange_keys(initial_sync=False)
+
+        assert patched_send_to_device.called
+        assert patched_keys_upload.called
+        assert patched_keys_query.called
+        patched_keys_claim.assert_called_with(patched_get_users())
+
+    async def test_connect(self, mocker, caplog, connector):
         with amock.patch(api_string.format("login")) as patched_login, amock.patch(
             api_string.format("join")
         ) as patched_join, amock.patch(
@@ -249,6 +295,20 @@ async def test_connect(self):
         ) as patched_get_nick, amock.patch(
             api_string.format("set_displayname")
         ) as patch_set_nick, amock.patch(
+            api_string.format("send_to_device_messages")
+        ) as patched_send_to_device, amock.patch(
+            api_string.format("should_upload_keys")
+        ) as patched_should_upload, amock.patch(
+            api_string.format("should_query_keys")
+        ) as patched_should_query, amock.patch(
+            api_string.format("keys_upload")
+        ) as patched_keys_upload, amock.patch(
+            api_string.format("keys_query")
+        ) as patched_keys_query, amock.patch(
+            "pathlib.Path.mkdir"
+        ) as patched_mkdir, amock.patch(
+            "pathlib.Path.is_dir"
+        ) as patched_is_dir, amock.patch(
             "aiohttp.ClientSession"
         ) as patch_cs, OpsDroid() as _:
 
@@ -292,20 +352,41 @@ async def test_connect(self):
             patch_set_nick.return_value = asyncio.Future()
             patch_set_nick.return_value.set_result(nio.ProfileSetDisplayNameResponse())
 
-            await self.connector.connect()
+            patched_is_dir.return_value = False
+            patched_mkdir.return_value = None
 
-            assert "!aroomid:localhost" in self.connector.room_ids.values()
+            patched_send_to_device.return_value = asyncio.Future()
+            patched_send_to_device.return_value.set_result(None)
 
-            assert self.connector.connection.token == "arbitrary string1"
+            patched_should_upload.return_value = True
+            patched_keys_upload.return_value = asyncio.Future()
+            patched_keys_upload.return_value.set_result(None)
 
-            assert self.connector.connection.sync_token == "arbitrary string2"
+            patched_should_query.return_value = True
+            patched_keys_query.return_value = asyncio.Future()
+            patched_keys_query.return_value.set_result(None)
 
-            self.connector.nick = "Rabbit Hole"
+            await connector.connect()
+
+            if nio.crypto.ENCRYPTION_ENABLED:
+                assert patched_mkdir.called
+
+            assert patched_send_to_device.called
+            assert patched_keys_upload.called
+            assert patched_keys_query.called
+
+            assert "!aroomid:localhost" in connector.room_ids.values()
+
+            assert connector.connection.token == "arbitrary string1"
+
+            assert connector.connection.sync_token == "arbitrary string2"
+
+            connector.nick = "Rabbit Hole"
 
             patched_get_nick.return_value = asyncio.Future()
             patched_get_nick.return_value.set_result("Rabbit Hole")
 
-            await self.connector.connect()
+            await connector.connect()
 
             assert patched_get_nick.called
             assert not patch_set_nick.called
@@ -313,9 +394,9 @@ async def test_connect(self):
             patched_get_nick.return_value = asyncio.Future()
             patched_get_nick.return_value.set_result("Neo")
 
-            self.connector.mxid = "@morpheus:matrix.org"
+            connector.mxid = "@morpheus:matrix.org"
 
-            await self.connector.connect()
+            await connector.connect()
 
             assert patched_get_nick.called
             patch_set_nick.assert_called_once_with("Rabbit Hole")
@@ -328,11 +409,11 @@ async def test_connect(self):
             patched_sync.return_value.set_result(
                 nio.SyncError(message=error_message, status_code=error_code)
             )
-            self._caplog.clear()
-            await self.connector.connect()
+            caplog.clear()
+            await connector.connect()
             assert [
                 f"Error during initial sync: {error_message} (status code {error_code})"
-            ] == [rec.message for rec in self._caplog.records]
+            ] == [rec.message for rec in caplog.records]
 
             # test join error
             patched_sync.return_value = asyncio.Future()
@@ -350,26 +431,26 @@ async def test_connect(self):
             patched_join.return_value.set_result(
                 nio.JoinError(message=error_message, status_code=error_code)
             )
-            self._caplog.clear()
-            await self.connector.connect()
+            caplog.clear()
+            await connector.connect()
             assert [
-                f"Error while joining room: {self.connector.rooms['main']['alias']}, Message: {error_message} (status code {error_code})"
-            ] == [rec.message for rec in self._caplog.records]
+                f"Error while joining room: {connector.rooms['main']['alias']}, Message: {error_message} (status code {error_code})"
+            ] == [rec.message for rec in caplog.records]
 
             # test login error
             patched_login.return_value = asyncio.Future()
             patched_login.return_value.set_result(
                 nio.LoginError(message=error_message, status_code=error_code)
             )
-            self._caplog.clear()
-            await self.connector.connect()
+            caplog.clear()
+            await connector.connect()
             assert [
                 f"Error while connecting: {error_message} (status code {error_code})"
-            ] == [rec.message for rec in self._caplog.records]
+            ] == [rec.message for rec in caplog.records]
 
-    async def test_parse_sync_response(self):
-        self.connector.room_ids = {"main": "!aroomid:localhost"}
-        self.connector.filter_id = "arbitrary string"
+    async def test_parse_sync_response(self, connector):
+        connector.room_ids = {"main": "!aroomid:localhost"}
+        connector.filter_id = "arbitrary string"
 
         with amock.patch(api_string.format("get_displayname")) as patched_name:
 
@@ -378,15 +459,13 @@ async def test_parse_sync_response(self):
                 nio.ProfileGetDisplayNameResponse("SomeUsersName")
             )
 
-            returned_message = await self.connector._parse_sync_response(
-                self.sync_return
-            )
+            returned_message = await connector._parse_sync_response(self.sync_return)
 
             assert isinstance(returned_message, events.Message)
             assert returned_message.text == "LOUD NOISES"
             assert returned_message.user == "SomeUsersName"
             assert returned_message.target == "!aroomid:localhost"
-            assert returned_message.connector == self.connector
+            assert returned_message.connector == connector
             raw_message = (
                 self.sync_return.rooms.join["!aroomid:localhost"]
                 .timeline.events[0]
@@ -394,14 +473,14 @@ async def test_parse_sync_response(self):
             )
             assert returned_message.raw_event == raw_message
 
-            returned_message = await self.connector._parse_sync_response(
+            returned_message = await connector._parse_sync_response(
                 self.sync_return_join
             )
 
             assert isinstance(returned_message, events.JoinRoom)
             assert returned_message.user == "SomeUsersName"
             assert returned_message.target == "!aroomid:localhost"
-            assert returned_message.connector == self.connector
+            assert returned_message.connector == connector
             raw_message = (
                 self.sync_return_join.rooms.join["!aroomid:localhost"]
                 .timeline.events[0]
@@ -410,27 +489,27 @@ async def test_parse_sync_response(self):
             raw_message["content"] = {"membership": "join"}
             assert returned_message.raw_event == raw_message
 
-    async def test_sync_parse_invites(self):
+    async def test_sync_parse_invites(self, connector):
         with amock.patch(api_string.format("get_displayname")) as patched_name:
-            self.connector.opsdroid = amock.MagicMock()
-            self.connector.opsdroid.parse.return_value = asyncio.Future()
-            self.connector.opsdroid.parse.return_value.set_result("")
+            connector.opsdroid = amock.MagicMock()
+            connector.opsdroid.parse.return_value = asyncio.Future()
+            connector.opsdroid.parse.return_value.set_result("")
             patched_name.return_value = asyncio.Future()
             patched_name.return_value.set_result(
                 nio.ProfileGetDisplayNameResponse("SomeUsersName")
             )
 
-            await self.connector._parse_sync_response(self.sync_invite)
+            await connector._parse_sync_response(self.sync_invite)
 
-            (invite,), _ = self.connector.opsdroid.parse.call_args
+            (invite,), _ = connector.opsdroid.parse.call_args
 
             assert invite.target == "!AWtmOvkBPTCSPbdaHn:localhost"
             assert invite.user == "SomeUsersName"
             assert invite.user_id == "@neo:matrix.org"
-            assert invite.connector is self.connector
+            assert invite.connector is connector
 
-    async def test_get_nick(self):
-        self.connector.room_specific_nicks = False
+    async def test_get_nick(self, connector):
+        connector.room_specific_nicks = False
 
         with amock.patch(api_string.format("get_displayname")) as patched_globname:
 
@@ -440,13 +519,10 @@ async def test_get_nick(self):
             patched_globname.return_value.set_result(
                 nio.ProfileGetDisplayNameResponse(displayname="notaperson")
             )
-            assert (
-                await self.connector.get_nick("#notaroom:localhost", mxid)
-                == "notaperson"
-            )
+            assert await connector.get_nick("#notaroom:localhost", mxid) == "notaperson"
 
-    async def test_get_room_specific_nick(self):
-        self.connector.room_specific_nicks = True
+    async def test_get_room_specific_nick(self, caplog, connector):
+        connector.room_specific_nicks = True
 
         with amock.patch(
             api_string.format("get_displayname")
@@ -475,32 +551,26 @@ async def test_get_room_specific_nick(self):
                 )
             )
 
-            assert (
-                await self.connector.get_nick("#notaroom:localhost", mxid)
-                == "notaperson"
-            )
+            assert await connector.get_nick("#notaroom:localhost", mxid) == "notaperson"
 
-            assert await self.connector.get_nick(None, mxid) == "notaperson"
+            assert await connector.get_nick(None, mxid) == "notaperson"
 
             # test member not in list
             patched_joined.return_value = asyncio.Future()
             patched_joined.return_value.set_result(
                 nio.JoinedMembersResponse(members=[], room_id="notanid")
             )
-            assert await self.connector.get_nick("#notaroom:localhost", mxid) == mxid
+            assert await connector.get_nick("#notaroom:localhost", mxid) == mxid
 
             # test JoinedMembersError
             patched_joined.return_value = asyncio.Future()
             patched_joined.return_value.set_result(
                 nio.JoinedMembersError(message="Some error", status_code=400)
             )
-            self._caplog.clear()
-            assert (
-                await self.connector.get_nick("#notaroom:localhost", mxid)
-                == "notaperson"
-            )
+            caplog.clear()
+            assert await connector.get_nick("#notaroom:localhost", mxid) == "notaperson"
             assert ["Failed to lookup room members for #notaroom:localhost."] == [
-                rec.message for rec in self._caplog.records
+                rec.message for rec in caplog.records
             ]
 
             # test displayname is not set
@@ -508,10 +578,10 @@ async def test_get_room_specific_nick(self):
             patched_globname.return_value.set_result(
                 nio.ProfileGetDisplayNameResponse(displayname=None)
             )
-            self._caplog.clear()
-            assert await self.connector.get_nick("#notaroom:localhost", mxid) == mxid
+            caplog.clear()
+            assert await connector.get_nick("#notaroom:localhost", mxid) == mxid
             assert ["Failed to lookup room members for #notaroom:localhost."] == [
-                rec.message for rec in self._caplog.records
+                rec.message for rec in caplog.records
             ]
 
             # test ProfileGetDisplayNameError
@@ -519,14 +589,12 @@ async def test_get_room_specific_nick(self):
             patched_globname.return_value.set_result(
                 nio.ProfileGetDisplayNameError(message="Some error", status_code=400)
             )
-            self._caplog.clear()
-            assert await self.connector.get_nick("#notaroom:localhost", mxid) == mxid
-            assert (
-                f"Failed to lookup nick for {mxid}." == self._caplog.records[1].message
-            )
+            caplog.clear()
+            assert await connector.get_nick("#notaroom:localhost", mxid) == mxid
+            assert f"Failed to lookup nick for {mxid}." == caplog.records[1].message
 
-    async def test_get_nick_not_set(self):
-        self.connector.room_specific_nicks = False
+    async def test_get_nick_not_set(self, connector):
+        connector.room_specific_nicks = False
 
         with amock.patch(api_string.format("get_displayname")) as patched_globname:
 
@@ -537,10 +605,10 @@ async def test_get_nick_not_set(self):
             patched_globname.return_value.set_result(
                 nio.ProfileGetDisplayNameResponse(displayname=None)
             )
-            assert await self.connector.get_nick("#notaroom:localhost", mxid) == mxid
+            assert await connector.get_nick("#notaroom:localhost", mxid) == mxid
 
-    async def test_get_nick_error(self):
-        self.connector.room_specific_nicks = False
+    async def test_get_nick_error(self, connector):
+        connector.room_specific_nicks = False
 
         with amock.patch(api_string.format("get_displayname")) as patched_globname:
 
@@ -551,38 +619,36 @@ async def test_get_nick_error(self):
             patched_globname.return_value.set_result(
                 nio.ProfileGetDisplayNameError(message="Error")
             )
-            assert await self.connector.get_nick("#notaroom:localhost", mxid) == mxid
+            assert await connector.get_nick("#notaroom:localhost", mxid) == mxid
 
-    async def test_get_formatted_message_body(self):
+    async def test_get_formatted_message_body(self, connector):
         original_html = "<p><h3><no>Hello World</no></h3></p>"
         original_body = "### Hello World"
-        message = self.connector._get_formatted_message_body(original_html)
+        message = connector._get_formatted_message_body(original_html)
         assert message["formatted_body"] == "<h3>Hello World</h3>"
         assert message["body"] == "Hello World"
 
-        message = self.connector._get_formatted_message_body(
-            original_html, original_body
-        )
+        message = connector._get_formatted_message_body(original_html, original_body)
         assert message["formatted_body"] == "<h3>Hello World</h3>"
         assert message["body"] == "### Hello World"
 
-    async def _get_message(self):
-        self.connector.room_ids = {"main": "!aroomid:localhost"}
-        self.connector.filter_id = "arbitrary string"
+    async def _get_message(self, connector):
+        connector.room_ids = {"main": "!aroomid:localhost"}
+        connector.filter_id = "arbitrary string"
         m = "opsdroid.connector.matrix.ConnectorMatrix.get_nick"
 
         with amock.patch(m) as patched_nick:
             patched_nick.return_value = asyncio.Future()
             patched_nick.return_value.set_result("Neo")
 
-            return await self.connector._parse_sync_response(self.sync_return)
+            return await connector._parse_sync_response(self.sync_return)
 
-    async def test_send_edited_message(self):
+    async def test_send_edited_message(self, connector):
         message = events.EditedMessage(
             text="New message",
             target="!test:localhost",
             linked_event=events.Message("hello", event_id="$hello"),
-            connector=self.connector,
+            connector=connector,
         )
         with amock.patch(
             api_string.format("room_send")
@@ -590,7 +656,7 @@ async def test_send_edited_message(self):
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result({})
 
-            new_content = self.connector._get_formatted_message_body(message.text)
+            new_content = connector._get_formatted_message_body(message.text)
             content = {
                 "msgtype": "m.text",
                 "m.new_content": new_content,
@@ -601,34 +667,50 @@ async def test_send_edited_message(self):
                 },
             }
 
-            await self.connector.send(message)
+            await connector.send(message)
 
             patched_send.assert_called_once_with(
-                message.target, "m.room.message", content
+                message.target,
+                "m.room.message",
+                content,
+                ignore_unverified_devices=True,
             )
 
             # Test linked event as event id
             message.linked_event = "$hello"
 
-            await self.connector.send(message)
+            await connector.send(message)
 
-            patched_send.assert_called_with(message.target, "m.room.message", content)
+            patched_send.assert_called_with(
+                message.target,
+                "m.room.message",
+                content,
+                ignore_unverified_devices=True,
+            )
 
             # Test responding to an edit
             await message.respond(events.EditedMessage("hello"))
 
-            patched_send.assert_called_with(message.target, "m.room.message", content)
+            patched_send.assert_called_with(
+                message.target,
+                "m.room.message",
+                content,
+                ignore_unverified_devices=True,
+            )
 
-    async def test_respond_retry(self):
-        message = await self._get_message()
+    async def test_respond_retry(self, connector):
+        message = await self._get_message(connector)
         with amock.patch(api_string.format("room_send")) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result(None)
-            await self.connector.send(message)
+            await connector.send(message)
 
-            message_obj = self.connector._get_formatted_message_body(message.text)
+            message_obj = connector._get_formatted_message_body(message.text)
             patched_send.assert_called_once_with(
-                message.target, "m.room.message", message_obj
+                message.target,
+                "m.room.message",
+                message_obj,
+                ignore_unverified_devices=True,
             )
 
             patched_send.side_effect = [
@@ -636,89 +718,156 @@ async def test_respond_retry(self):
                 patched_send.return_value,
             ]
 
-            await self.connector.send(message)
+            await connector.send(message)
 
-            message_obj = self.connector._get_formatted_message_body(message.text)
+            message_obj = connector._get_formatted_message_body(message.text)
             patched_send.assert_called_with(
-                message.target, "m.room.message", message_obj
+                message.target,
+                "m.room.message",
+                message_obj,
+                ignore_unverified_devices=True,
             )
 
-    async def test_respond_room(self):
-        message = await self._get_message()
+    async def test_respond_room(self, connector):
+        message = await self._get_message(connector)
         with amock.patch(api_string.format("room_send")) as patched_send:
 
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result(None)
 
             message.target = "main"
-            await self.connector.send(message)
+            await connector.send(message)
 
-            message_obj = self.connector._get_formatted_message_body(message.text)
+            message_obj = connector._get_formatted_message_body(message.text)
             patched_send.assert_called_once_with(
-                "!aroomid:localhost", "m.room.message", message_obj
+                "!aroomid:localhost",
+                "m.room.message",
+                message_obj,
+                ignore_unverified_devices=True,
             )
 
-    async def test_disconnect(self):
-        self.connector.connection = amock.MagicMock()
-        self.connector.connection.close = amock.CoroutineMock()
-        await self.connector.disconnect()
-        assert self.connector.connection.close.called
+    async def test_disconnect(self, mocker, connector):
+        patched_close = mocker.patch(
+            api_string.format("close"), return_value=asyncio.Future()
+        )
+        patched_close.return_value.set_result(None)
+
+        await connector.disconnect()
+        assert patched_close.called
 
-    def test_get_roomname(self):
-        self.connector.rooms = {
+    def test_get_roomname(self, connector):
+        connector.rooms = {
             "main": {"alias": "#notthisroom:localhost"},
             "test": {"alias": "#thisroom:localhost"},
         }
-        self.connector.room_ids = dict(
+        connector.room_ids = dict(
             zip(
-                self.connector.rooms.keys(),
+                connector.rooms.keys(),
                 ["!aroomid:localhost", "!anotherroomid:localhost"],
             )
         )
 
-        assert self.connector.get_roomname("#thisroom:localhost") == "test"
-        assert self.connector.get_roomname("!aroomid:localhost") == "main"
-        assert self.connector.get_roomname("someroom") == "someroom"
+        assert connector.get_roomname("#thisroom:localhost") == "test"
+        assert connector.get_roomname("!aroomid:localhost") == "main"
+        assert connector.get_roomname("someroom") == "someroom"
 
-    def test_lookup_target(self):
-        self.connector.room_ids = {"main": "!aroomid:localhost"}
+    def test_lookup_target(self, connector):
+        connector.room_ids = {
+            "main": "!aroomid:localhost",
+            "test": "#test:localhost",
+        }
 
-        assert self.connector.lookup_target("main") == "!aroomid:localhost"
-        assert self.connector.lookup_target("#test:localhost") == "!aroomid:localhost"
-        assert (
-            self.connector.lookup_target("!aroomid:localhost") == "!aroomid:localhost"
-        )
+        assert connector.lookup_target("main") == "!aroomid:localhost"
+        assert connector.lookup_target("#test:localhost") == "!aroomid:localhost"
+        assert connector.lookup_target("!aroomid:localhost") == "!aroomid:localhost"
 
-    async def test_respond_image(self):
+    async def test_respond_image(self, mocker, caplog, connector):
         gif_bytes = (
             b"GIF89a\x01\x00\x01\x00\x00\xff\x00,"
             b"\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;"
         )
 
         image = events.Image(file_bytes=gif_bytes, target="!test:localhost")
-        with amock.patch(api_string.format("room_send")) as patched_send, amock.patch(
-            api_string.format("upload")
-        ) as patched_upload:
 
-            patched_upload.return_value = asyncio.Future()
-            patched_upload.return_value.set_result([nio.UploadResponse("mxc://aurl")])
+        patched_send = mocker.patch(
+            api_string.format("room_send"), return_value=asyncio.Future()
+        )
+        patched_send.return_value.set_result(None)
 
-            patched_send.return_value = asyncio.Future()
-            patched_send.return_value.set_result(None)
-            await self.connector.send(image)
+        connector.connection.store = mocker.MagicMock()
+        connector.connection.store.load_encrypted_rooms.return_value = []
 
-            patched_send.assert_called_once_with(
-                room_id="!test:localhost",
-                message_type="m.room.message",
-                content={
-                    "body": "opsdroid_upload",
-                    "info": {"w": 1, "h": 1, "mimetype": "image/gif", "size": 26},
-                    "msgtype": "m.image",
-                    "url": "mxc://aurl",
-                },
-            )
+        patched_upload = mocker.patch(
+            api_string.format("upload"), return_value=asyncio.Future()
+        )
+        patched_upload.return_value.set_result([nio.UploadResponse("mxc://aurl"), None])
+
+        await connector.send(image)
+
+        patched_send.assert_called_once_with(
+            room_id="!test:localhost",
+            message_type="m.room.message",
+            content={
+                "body": "opsdroid_upload",
+                "info": {"w": 1, "h": 1, "mimetype": "image/gif", "size": 26},
+                "msgtype": "m.image",
+                "url": "mxc://aurl",
+            },
+            ignore_unverified_devices=True,
+        )
+
+        file_dict = {
+            "v": "v2",
+            "key": {
+                "kty": "oct",
+                "alg": "A256CTR",
+                "ext": True,
+                "key_ops": ["encrypt", "decrypt"],
+                "k": "randomkey",
+            },
+            "iv": "randomiv",
+            "hashes": {"sha256": "shakey"},
+            "url": "mxc://aurl",
+            "mimetype": "image/gif",
+        }
+
+        connector.connection.store.load_encrypted_rooms.return_value = [
+            "!test:localhost"
+        ]
+        patched_upload.return_value = asyncio.Future()
+        patched_upload.return_value.set_result(
+            [nio.UploadResponse("mxc://aurl"), file_dict]
+        )
 
-    async def test_respond_mxc(self):
+        await connector.send(image)
+
+        patched_send.assert_called_with(
+            room_id="!test:localhost",
+            message_type="m.room.message",
+            content={
+                "body": "opsdroid_upload",
+                "info": {"w": 1, "h": 1, "mimetype": "image/gif", "size": 26},
+                "msgtype": "m.image",
+                "file": file_dict,
+            },
+            ignore_unverified_devices=True,
+        )
+
+        error_message = "Some error message"
+        error_code = 400
+        connector.connection.store.load_encrypted_rooms.return_value = []
+        patched_upload.return_value = asyncio.Future()
+        patched_upload.return_value.set_result(
+            [nio.UploadError(message=error_message, status_code=error_code), None]
+        )
+
+        caplog.clear()
+        await connector.send(image)
+        assert [
+            f"Error while sending the file. Reason: {error_message} (status code {error_code})"
+        ] == [rec.message for rec in caplog.records]
+
+    async def test_respond_mxc(self, connector):
         gif_bytes = (
             b"GIF89a\x01\x00\x01\x00\x00\xff\x00,"
             b"\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;"
@@ -734,7 +883,7 @@ async def test_respond_mxc(self):
 
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result(None)
-            await self.connector.send(image)
+            await connector.send(image)
 
             patched_send.assert_called_once_with(
                 content={
@@ -745,35 +894,81 @@ async def test_respond_mxc(self):
                 },
                 message_type="m.room.message",
                 room_id="!test:localhost",
+                ignore_unverified_devices=True,
             )
 
-    async def test_respond_file(self):
+    async def test_respond_file(self, mocker, connector):
         file_event = events.File(
-            file_bytes=b"aslkdjlaksdjlkajdlk", target="!test:localhost"
+            file_bytes=b"aslkdjlaksdjlkajdlk",
+            target="!test:localhost",
+            mimetype="text/plain",
         )
-        with amock.patch(api_string.format("room_send")) as patched_send, amock.patch(
-            api_string.format("upload")
-        ) as patched_upload:
 
-            patched_upload.return_value = asyncio.Future()
-            patched_upload.return_value.set_result([nio.UploadResponse("mxc://aurl")])
+        patched_send = mocker.patch(
+            api_string.format("room_send"), return_value=asyncio.Future()
+        )
+        patched_send.return_value.set_result(None)
 
-            patched_send.return_value = asyncio.Future()
-            patched_send.return_value.set_result(None)
-            await self.connector.send(file_event)
+        connector.connection.store = mocker.MagicMock()
+        connector.connection.store.load_encrypted_rooms.return_value = []
 
-            patched_send.assert_called_once_with(
-                room_id="!test:localhost",
-                message_type="m.room.message",
-                content={
-                    "body": "opsdroid_upload",
-                    "info": {},
-                    "msgtype": "m.file",
-                    "url": "mxc://aurl",
-                },
-            )
+        patched_upload = mocker.patch(
+            api_string.format("upload"), return_value=asyncio.Future()
+        )
+        patched_upload.return_value.set_result([nio.UploadResponse("mxc://aurl"), None])
+
+        await connector.send(file_event)
+
+        patched_send.assert_called_once_with(
+            room_id="!test:localhost",
+            message_type="m.room.message",
+            content={
+                "body": "opsdroid_upload",
+                "info": {"mimetype": "text/plain", "size": 19},
+                "msgtype": "m.file",
+                "url": "mxc://aurl",
+            },
+            ignore_unverified_devices=True,
+        )
+
+        file_dict = {
+            "v": "v2",
+            "key": {
+                "kty": "oct",
+                "alg": "A256CTR",
+                "ext": True,
+                "key_ops": ["encrypt", "decrypt"],
+                "k": "randomkey",
+            },
+            "iv": "randomiv",
+            "hashes": {"sha256": "shakey"},
+            "url": "mxc://aurl",
+            "mimetype": "text/plain",
+        }
+
+        connector.connection.store.load_encrypted_rooms.return_value = [
+            "!test:localhost"
+        ]
+        patched_upload.return_value = asyncio.Future()
+        patched_upload.return_value.set_result(
+            [nio.UploadResponse("mxc://aurl"), file_dict]
+        )
 
-    async def test_respond_new_room(self):
+        await connector.send(file_event)
+
+        patched_send.assert_called_with(
+            room_id="!test:localhost",
+            message_type="m.room.message",
+            content={
+                "body": "opsdroid_upload",
+                "info": {"mimetype": "text/plain", "size": 19},
+                "msgtype": "m.file",
+                "file": file_dict,
+            },
+            ignore_unverified_devices=True,
+        )
+
+    async def test_respond_new_room(self, caplog, connector):
         event = events.NewRoom(name="test", target="!test:localhost")
         with amock.patch(api_string.format("room_create")) as patched_send, amock.patch(
             api_string.format("room_put_state")
@@ -792,7 +987,7 @@ async def test_respond_new_room(self):
                 nio.RoomCreateResponse(room_id="!test:localhost")
             )
 
-            resp = await self.connector.send(event)
+            resp = await connector.send(event)
             assert resp == "!test:localhost"
 
             assert patched_send.called_once_with(name="test")
@@ -804,24 +999,24 @@ async def test_respond_new_room(self):
             patched_send.return_value.set_result(
                 nio.RoomCreateError(message=error_message, status_code=error_code)
             )
-            self._caplog.clear()
-            resp = await self.connector.send(event)
+            caplog.clear()
+            resp = await connector.send(event)
             assert [
                 f"Error while creating the room. Reason: {error_message} (status code {error_code})"
-            ] == [rec.message for rec in self._caplog.records]
+            ] == [rec.message for rec in caplog.records]
 
-    async def test_respond_room_address(self):
+    async def test_respond_room_address(self, connector):
         event = events.RoomAddress("#test:localhost", target="!test:localhost")
 
         with amock.patch(api_string.format("room_put_state")) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result({})
 
-            await self.connector.send(event)
+            await connector.send(event)
 
             assert patched_send.called_once_with("!test:localhost", "#test:localhost")
 
-    async def test_respond_join_room(self):
+    async def test_respond_join_room(self, connector):
         event = events.JoinRoom(target="#test:localhost")
         with amock.patch(
             api_string.format("room_resolve_alias")
@@ -838,10 +1033,10 @@ async def test_respond_join_room(self):
 
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result({})
-            await self.connector.send(event)
+            await connector.send(event)
             assert patched_send.called_once_with("#test:localhost")
 
-    async def test_respond_join_room_error(self):
+    async def test_respond_join_room_error(self, caplog, connector):
         event = events.JoinRoom(target="#test:localhost")
         with amock.patch(
             api_string.format("room_resolve_alias")
@@ -859,32 +1054,32 @@ async def test_respond_join_room_error(self):
             patched_get_room_id.return_value.set_result(
                 nio.RoomResolveAliasError(message=error_message, status_code=error_code)
             )
-            self._caplog.clear()
-            await self.connector.send(event)
+            caplog.clear()
+            await connector.send(event)
             assert patched_send.called_once_with("#test:localhost")
             assert patched_get_room_id.called_once_with("#test:localhost")
             assert [
                 f"Error resolving room id for #test:localhost: {error_message} (status code {error_code})"
-            ] == [rec.message for rec in self._caplog.records]
+            ] == [rec.message for rec in caplog.records]
 
-    async def test_respond_user_invite(self):
+    async def test_respond_user_invite(self, connector):
         event = events.UserInvite("@test:localhost", target="!test:localhost")
         with amock.patch(api_string.format("room_invite")) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result({})
-            await self.connector.send(event)
+            await connector.send(event)
             assert patched_send.called_once_with("#test:localhost", "@test:localhost")
 
-    async def test_respond_room_description(self):
+    async def test_respond_room_description(self, connector):
         event = events.RoomDescription("A test room", target="!test:localhost")
 
         with amock.patch(api_string.format("room_put_state")) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result({})
-            await self.connector.send(event)
+            await connector.send(event)
             assert patched_send.called_once_with("#test:localhost", "A test room")
 
-    async def test_respond_room_image(self):
+    async def test_respond_room_image(self, connector):
         image = events.Image(url="mxc://aurl")
         event = events.RoomImage(image, target="!test:localhost")
 
@@ -893,16 +1088,17 @@ async def test_respond_room_image(self):
         ) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result({})
-            opsdroid.connectors = [self.connector]
-            await self.connector.send(event)
+            opsdroid.connectors = [connector]
+            await connector.send(event)
             assert patched_send.called_once_with(
                 "#test:localhost",
                 "m.room.avatar",
                 {"url": "mxc://aurl"},
                 state_key=None,
+                ignore_unverified_devices=True,
             )
 
-    async def test_respond_user_role(self):
+    async def test_respond_user_role(self, connector):
         existing_power_levels = {
             "ban": 50,
             "events": {"m.room.name": 100, "m.room.power_levels": 100},
@@ -943,7 +1139,7 @@ async def test_respond_user_role(self):
                 with amock.patch(
                     api_string.format("room_get_state_event")
                 ) as patched_power_levels:
-                    opsdroid.connectors = [self.connector]
+                    opsdroid.connectors = [connector]
 
                     patched_power_levels.return_value = asyncio.Future()
                     patched_power_levels.return_value.set_result(
@@ -957,7 +1153,7 @@ async def test_respond_user_role(self):
                     patched_send.return_value = asyncio.Future()
                     patched_send.return_value.set_result({})
 
-                    await self.connector.send(event)
+                    await connector.send(event)
 
                     modified_power_levels = deepcopy(existing_power_levels)
                     modified_power_levels["users"]["@test:localhost"] = pl
@@ -969,12 +1165,9 @@ async def test_respond_user_role(self):
                         state_key=None,
                     )
 
-    async def test_send_reaction(self):
+    async def test_send_reaction(self, connector):
         message = events.Message(
-            "hello",
-            event_id="$11111",
-            connector=self.connector,
-            target="!test:localhost",
+            "hello", event_id="$11111", connector=connector, target="!test:localhost",
         )
         reaction = events.Reaction("⭕")
         with OpsDroid() as _:
@@ -996,12 +1189,9 @@ async def test_send_reaction(self):
                     "!test:localhost", "m.reaction", content
                 )
 
-    async def test_send_reply(self):
+    async def test_send_reply(self, connector):
         message = events.Message(
-            "hello",
-            event_id="$11111",
-            connector=self.connector,
-            target="!test:localhost",
+            "hello", event_id="$11111", connector=connector, target="!test:localhost",
         )
         reply = events.Reply("reply")
         with OpsDroid() as _:
@@ -1011,7 +1201,7 @@ async def test_send_reply(self):
 
                 await message.respond(reply)
 
-                content = self.connector._get_formatted_message_body(
+                content = connector._get_formatted_message_body(
                     reply.text, msgtype="m.text"
                 )
 
@@ -1023,16 +1213,16 @@ async def test_send_reply(self):
                     "!test:localhost", "m.room.message", content
                 )
 
-    async def test_send_reply_id(self):
+    async def test_send_reply_id(self, connector):
         reply = events.Reply("reply", linked_event="$hello", target="!hello:localhost")
         with OpsDroid() as _:
             with amock.patch(api_string.format("room_send")) as patched_send:
                 patched_send.return_value = asyncio.Future()
                 patched_send.return_value.set_result(None)
 
-                await self.connector.send(reply)
+                await connector.send(reply)
 
-                content = self.connector._get_formatted_message_body(
+                content = connector._get_formatted_message_body(
                     reply.text, msgtype="m.text"
                 )
 
@@ -1042,7 +1232,7 @@ async def test_send_reply_id(self):
                     "!test:localhost", "m.room.message", content
                 )
 
-    async def test_alias_already_exists(self):
+    async def test_alias_already_exists(self, caplog, connector):
 
         with amock.patch(api_string.format("room_put_state")) as patched_alias:
             patched_alias.return_value = asyncio.Future()
@@ -1050,7 +1240,7 @@ async def test_alias_already_exists(self):
                 nio.RoomPutStateResponse(event_id="some_id", room_id="!test:localhost")
             )
 
-            resp = await self.connector._send_room_address(
+            resp = await connector._send_room_address(
                 events.RoomAddress(target="!test:localhost", address="hello")
             )
             assert resp.event_id == "some_id"
@@ -1063,28 +1253,28 @@ async def test_alias_already_exists(self):
             patched_alias.return_value.set_result(
                 nio.RoomPutStateError(message=error_message, status_code=error_code)
             )
-            self._caplog.clear()
-            resp = await self.connector._send_room_address(
+            caplog.clear()
+            resp = await connector._send_room_address(
                 events.RoomAddress(target="!test:localhost", address="hello")
             )
             assert [
                 f"Error while setting room alias: {error_message} (status code {error_code})"
-            ] == [rec.message for rec in self._caplog.records]
+            ] == [rec.message for rec in caplog.records]
 
             error_code = 409
             patched_alias.return_value = asyncio.Future()
             patched_alias.return_value.set_result(
                 nio.RoomPutStateError(message=error_message, status_code=error_code)
             )
-            self._caplog.clear()
-            resp = await self.connector._send_room_address(
+            caplog.clear()
+            resp = await connector._send_room_address(
                 events.RoomAddress(target="!test:localhost", address="hello")
             )
             assert ["A room with the alias hello already exists."] == [
-                rec.message for rec in self._caplog.records
+                rec.message for rec in caplog.records
             ]
 
-    async def test_already_in_room(self):
+    async def test_already_in_room(self, caplog, connector):
         with amock.patch(api_string.format("room_invite")) as patched_invite:
 
             patched_invite.return_value = asyncio.Future()
@@ -1094,14 +1284,14 @@ async def test_already_in_room(self):
                 )
             )
 
-            self._caplog.clear()
-            resp = await self.connector._send_user_invitation(
+            caplog.clear()
+            resp = await connector._send_user_invitation(
                 events.UserInvite(target="!test:localhost", user_id="@neo:matrix.org")
             )
             assert resp.message == "@neo.matrix.org is already in the room"
             assert [
                 "Error while inviting user @neo:matrix.org to room !test:localhost: @neo.matrix.org is already in the room (status code 400)"
-            ] == [rec.message for rec in self._caplog.records]
+            ] == [rec.message for rec in caplog.records]
 
             patched_invite.return_value = asyncio.Future()
             patched_invite.return_value.set_result(
@@ -1109,48 +1299,48 @@ async def test_already_in_room(self):
                     message="@neo.matrix.org is already in the room", status_code=403
                 )
             )
-            resp = await self.connector._send_user_invitation(
+            resp = await connector._send_user_invitation(
                 events.UserInvite(target="!test:localhost", user_id="@neo:matrix.org")
             )
             assert resp.message == "@neo.matrix.org is already in the room"
 
-    async def test_invalid_role(self):
-        with self.assertRaises(ValueError):
-            await self.connector._set_user_role(
+    async def test_invalid_role(self, connector):
+        with pytest.raises(ValueError):
+            await connector._set_user_role(
                 events.UserRole(
                     "wibble", target="!test:localhost", user_id="@test:localhost"
                 )
             )
 
-    async def test_no_user_id(self):
-        with self.assertRaises(ValueError):
-            await self.connector._set_user_role(
+    async def test_no_user_id(self, connector):
+        with pytest.raises(ValueError):
+            await connector._set_user_role(
                 events.UserRole("wibble", target="!test:localhost")
             )
 
-    def test_m_notice(self):
-        self.connector.rooms["test"] = {
+    def test_m_notice(self, connector):
+        connector.rooms["test"] = {
             "alias": "#test:localhost",
             "send_m_notice": True,
         }
 
-        assert self.connector.message_type("main") == "m.text"
-        assert self.connector.message_type("test") == "m.notice"
-        self.connector.send_m_notice = True
-        assert self.connector.message_type("main") == "m.notice"
+        assert connector.message_type("main") == "m.text"
+        assert connector.message_type("test") == "m.notice"
+        connector.send_m_notice = True
+        assert connector.message_type("main") == "m.notice"
 
         # Reset the state
-        self.connector.send_m_notice = False
-        del self.connector.rooms["test"]
+        connector.send_m_notice = False
+        del connector.rooms["test"]
 
-    def test_construct(self):
+    def test_construct(self, connector):
         jr = matrix_events.MatrixJoinRules("hello")
         assert jr.content["join_rule"] == "hello"
 
         hv = matrix_events.MatrixHistoryVisibility("hello")
         assert hv.content["history_visibility"] == "hello"
 
-    async def test_send_generic_event(self):
+    async def test_send_generic_event(self, connector):
         event = matrix_events.GenericMatrixRoomEvent(
             "opsdroid.dev", {"hello": "world"}, target="!test:localhost"
         )
@@ -1159,18 +1349,15 @@ async def test_send_generic_event(self):
                 patched_send.return_value = asyncio.Future()
                 patched_send.return_value.set_result(None)
 
-                await self.connector.send(event)
+                await connector.send(event)
                 assert patched_send.called_once_with(
                     "!test:localhost", "opsdroid.dev", {"hello": "world"}
                 )
 
 
-class TestEventCreatorMatrixAsync(asynctest.TestCase):
-    def setUp(self):
-        """Basic setting up for tests"""
-        self.connector = setup_connector()
-        self.api = nio.AsyncClient("https://notaurl.com", None)
-        self.connector.connection = self.api
+@pytest.mark.asyncio
+class TestEventCreatorMatrixAsync:
+    """Basic setting up for tests"""
 
     @property
     def message_json(self):
@@ -1203,6 +1390,37 @@ def file_json(self):
             "age": 23394532373,
         }
 
+    @property
+    def encrypted_file_json(self):
+        return {
+            "origin_server_ts": 1534013434328,
+            "sender": "@neo:matrix.org",
+            "event_id": "$1534013434516721kIgMV:matrix.org",
+            "content": {
+                "body": "stereo_reproject.py",
+                "info": {"mimetype": "text/x-python", "size": 1239},
+                "msgtype": "m.file",
+                "file": {
+                    "v": "v2",
+                    "key": {
+                        "kty": "oct",
+                        "alg": "A256CTR",
+                        "ext": True,
+                        "key_ops": ["encrypt", "decrypt"],
+                        "k": "randomkey",
+                    },
+                    "iv": "randomiv",
+                    "hashes": {"sha256": "shakey"},
+                    "url": "mxc://matrix.org/vtgAIrGtuYJQCXNKRGhVfSMX",
+                    "mimetype": "text/x-python",
+                },
+            },
+            "room_id": "!MeRdFpEonLoCwhoHeT:matrix.org",
+            "type": "m.room.message",
+            "unsigned": {"age": 23394532373},
+            "age": 23394532373,
+        }
+
     @property
     def image_json(self):
         return {
@@ -1233,6 +1451,62 @@ def image_json(self):
             "age": 2542608318,
         }
 
+    @property
+    def encrypted_image_json(self):
+        return {
+            "content": {
+                "body": "index.png",
+                "info": {
+                    "h": 1149,
+                    "mimetype": "image/png",
+                    "size": 1949708,
+                    "thumbnail_info": {
+                        "h": 600,
+                        "mimetype": "image/png",
+                        "size": 568798,
+                        "w": 612,
+                    },
+                    "thumbnail_file": {
+                        "v": "v2",
+                        "key": {
+                            "kty": "oct",
+                            "alg": "A256CTR",
+                            "ext": True,
+                            "key_ops": ["encrypt", "decrypt"],
+                            "k": "randomkey",
+                        },
+                        "iv": "randomiv",
+                        "hashes": {"sha256": "shakey"},
+                        "url": "mxc://matrix.org/HjHqeJDDxcnOEGydCQlJZQwC",
+                        "mimetype": "image/png",
+                    },
+                    "w": 1172,
+                },
+                "msgtype": "m.image",
+                "file": {
+                    "v": "v2",
+                    "key": {
+                        "kty": "oct",
+                        "alg": "A256CTR",
+                        "ext": True,
+                        "key_ops": ["encrypt", "decrypt"],
+                        "k": "randomkey",
+                    },
+                    "iv": "randomiv",
+                    "hashes": {"sha256": "shakey"},
+                    "url": "mxc://matrix.org/iDHKYJSQZZrrhOxAkMBMOaeo",
+                    "mimetype": "image/png",
+                },
+            },
+            "event_id": "$15548652221495790FYlHC:matrix.org",
+            "origin_server_ts": 1554865222742,
+            "room_id": "!MeRdFpEonLoCwhoHeT:matrix.org",
+            "sender": "@neo:matrix.org",
+            "type": "m.room.message",
+            "unsigned": {"age": 2542608318},
+            "age": 2542608318,
+        }
+
     @property
     def room_name_json(self):
         return {
@@ -1334,22 +1608,17 @@ def join_room_json(self):
             "unsigned": {"age": 1234},
         }
 
-    @property
-    def event_creator(self):
+    @pytest.fixture
+    def event_creator(self, connector):
         patched_get_nick = amock.MagicMock()
         patched_get_nick.return_value = asyncio.Future()
         patched_get_nick.return_value.set_result("Rabbit Hole")
-        self.connector.get_nick = patched_get_nick
-
-        patched_mxc = amock.MagicMock()
-        patched_mxc.return_value = asyncio.Future()
-        patched_mxc.return_value.set_result("http://somefileurl")
-        self.connector.mxc_to_http = patched_mxc
+        connector.get_nick = patched_get_nick
 
-        return MatrixEventCreator(self.connector)
+        return MatrixEventCreator(connector)
 
-    async def test_create_message(self):
-        event = await self.event_creator.create_event(self.message_json, "hello")
+    async def test_create_message(self, connector, event_creator):
+        event = await event_creator.create_event(self.message_json, "hello")
         assert isinstance(event, events.Message)
         assert event.text == "I just did it manually."
         assert event.user == "Rabbit Hole"
@@ -1358,62 +1627,86 @@ async def test_create_message(self):
         assert event.event_id == "$15573463541827394vczPd:matrix.org"
         assert event.raw_event == self.message_json
 
-    async def test_create_file(self):
-        event = await self.event_creator.create_event(self.file_json, "hello")
+    async def test_create_file(self, connector, event_creator):
+        event = await event_creator.create_event(self.file_json, "hello")
+        encrypted_event = await event_creator.create_event(
+            self.encrypted_file_json, "hello"
+        )
+
         assert isinstance(event, events.File)
-        assert event.url == "http://somefileurl"
-        assert event.user == "Rabbit Hole"
-        assert event.user_id == "@neo:matrix.org"
-        assert event.target == "hello"
-        assert event.event_id == "$1534013434516721kIgMV:matrix.org"
+        assert isinstance(encrypted_event, events.File)
+        assert (
+            event.url
+            == encrypted_event.url
+            == "https://notaurl.com/_matrix/media/r0/download/matrix.org/vtgAIrGtuYJQCXNKRGhVfSMX"
+        )
+        assert event.user == encrypted_event.user == "Rabbit Hole"
+        assert event.user_id == encrypted_event.user_id == "@neo:matrix.org"
+        assert event.target == encrypted_event.target == "hello"
+        assert (
+            event.event_id
+            == encrypted_event.event_id
+            == "$1534013434516721kIgMV:matrix.org"
+        )
         assert event.raw_event == self.file_json
+        assert encrypted_event.raw_event == self.encrypted_file_json
 
-    async def test_create_image(self):
-        event = await self.event_creator.create_event(self.image_json, "hello")
+    async def test_create_image(self, connector, event_creator):
+        event = await event_creator.create_event(self.image_json, "hello")
+        encrypted_event = await event_creator.create_event(
+            self.encrypted_image_json, "hello"
+        )
         assert isinstance(event, events.Image)
-        assert event.url == "http://somefileurl"
-        assert event.user == "Rabbit Hole"
-        assert event.user_id == "@neo:matrix.org"
-        assert event.target == "hello"
-        assert event.event_id == "$15548652221495790FYlHC:matrix.org"
+        assert (
+            event.url
+            == encrypted_event.url
+            == "https://notaurl.com/_matrix/media/r0/download/matrix.org/iDHKYJSQZZrrhOxAkMBMOaeo"
+        )
+        assert event.user == encrypted_event.user == "Rabbit Hole"
+        assert event.user_id == encrypted_event.user_id == "@neo:matrix.org"
+        assert event.target == encrypted_event.target == "hello"
+        assert (
+            event.event_id
+            == encrypted_event.event_id
+            == "$15548652221495790FYlHC:matrix.org"
+        )
         assert event.raw_event == self.image_json
+        assert encrypted_event.raw_event == self.encrypted_image_json
 
-    async def test_unsupported_type(self):
+    async def test_unsupported_type(self, connector, event_creator):
         json = self.message_json
         json["type"] = "wibble"
-        event = await self.event_creator.create_event(json, "hello")
+        event = await event_creator.create_event(json, "hello")
         assert isinstance(event, matrix_events.GenericMatrixRoomEvent)
         assert event.event_type == "wibble"
         assert "wibble" in repr(event)
         assert event.target in repr(event)
         assert str(event.content) in repr(event)
 
-    async def test_unsupported_message_type(self):
+    async def test_unsupported_message_type(self, connector, event_creator):
         json = self.message_json
         json["content"]["msgtype"] = "wibble"
-        event = await self.event_creator.create_event(json, "hello")
+        event = await event_creator.create_event(json, "hello")
         assert isinstance(event, matrix_events.GenericMatrixRoomEvent)
         assert event.content["msgtype"] == "wibble"
 
-    async def test_room_name(self):
-        event = await self.event_creator.create_event(self.room_name_json, "hello")
+    async def test_room_name(self, connector, event_creator):
+        event = await event_creator.create_event(self.room_name_json, "hello")
         assert isinstance(event, events.RoomName)
         assert event.user == "Rabbit Hole"
         assert event.user_id == "@neo:matrix.org"
         assert event.event_id == "$3r_PWCT9Vurlv-OFleAsf5gEnoZd-LEGHY6AGqZ5tJg"
         assert event.raw_event == self.room_name_json
 
-    async def test_room_description(self):
-        event = await self.event_creator.create_event(
-            self.room_description_json, "hello"
-        )
+    async def test_room_description(self, connector, event_creator):
+        event = await event_creator.create_event(self.room_description_json, "hello")
         assert isinstance(event, events.RoomDescription)
         assert event.user == "Rabbit Hole"
         assert event.user_id == "@neo:matrix.org"
         assert event.event_id == "$bEg2XISusHMKLBw9b4lMNpB2r9qYoesp512rKvbo5LA"
         assert event.raw_event == self.room_description_json
 
-    async def test_edited_message(self):
+    async def test_edited_message(self, connector, event_creator):
         with amock.patch(api_string.format("room_context")) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result(
@@ -1427,9 +1720,7 @@ async def test_edited_message(self):
                     state=[],
                 )
             )
-            event = await self.event_creator.create_event(
-                self.message_edit_json, "hello"
-            )
+            event = await event_creator.create_event(self.message_edit_json, "hello")
 
         assert isinstance(event, events.EditedMessage)
         assert event.text == "hello"
@@ -1441,7 +1732,7 @@ async def test_edited_message(self):
 
         assert isinstance(event.linked_event, events.Message)
 
-    async def test_reaction(self):
+    async def test_reaction(self, connector, event_creator):
         with amock.patch(api_string.format("room_context")) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result(
@@ -1455,7 +1746,7 @@ async def test_reaction(self):
                     state=[],
                 )
             )
-            event = await self.event_creator.create_event(self.reaction_json, "hello")
+            event = await event_creator.create_event(self.reaction_json, "hello")
 
         assert isinstance(event, events.Reaction)
         assert event.emoji == "👍"
@@ -1467,7 +1758,7 @@ async def test_reaction(self):
 
         assert isinstance(event.linked_event, events.Message)
 
-    async def test_reply(self):
+    async def test_reply(self, connector, event_creator):
         with amock.patch(api_string.format("room_context")) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result(
@@ -1481,7 +1772,7 @@ async def test_reply(self):
                     state=[],
                 )
             )
-            event = await self.event_creator.create_event(self.reply_json, "hello")
+            event = await event_creator.create_event(self.reply_json, "hello")
 
         assert isinstance(event, events.Reply)
         assert event.text == "hello"
@@ -1493,7 +1784,7 @@ async def test_reply(self):
 
         assert isinstance(event.linked_event, events.Message)
 
-    async def test_create_joinroom(self):
+    async def test_create_joinroom(self, connector, event_creator):
         with amock.patch(api_string.format("room_context")) as patched_send:
             patched_send.return_value = asyncio.Future()
             patched_send.return_value.set_result(
@@ -1508,7 +1799,7 @@ async def test_create_joinroom(self):
                 )
             )
 
-        event = await self.event_creator.create_event(self.join_room_json, "hello")
+        event = await event_creator.create_event(self.join_room_json, "hello")
         assert isinstance(event, events.JoinRoom)
         assert event.user == "Rabbit Hole"
         assert event.user_id == "@example:example.org"
@@ -1529,8 +1820,8 @@ def custom_json(self):
             "age": 48926251,
         }
 
-    async def test_create_generic(self):
-        event = await self.event_creator.create_event(self.custom_json, "hello")
+    async def test_create_generic(self, connector, event_creator):
+        event = await event_creator.create_event(self.custom_json, "hello")
         assert isinstance(event, matrix_events.GenericMatrixRoomEvent)
         assert event.user == "Rabbit Hole"
         assert event.user_id == "@neo:matrix.org"
@@ -1552,8 +1843,8 @@ def custom_state_json(self):
             "event_id": "$bEg2XISusHMKLBw9b4lMNpB2r9qYoesp512rKvbo5LA",
         }
 
-    async def test_create_generic_state(self):
-        event = await self.event_creator.create_event(self.custom_state_json, "hello")
+    async def test_create_generic_state(self, connector, event_creator):
+        event = await event_creator.create_event(self.custom_state_json, "hello")
         assert isinstance(event, matrix_events.MatrixStateEvent)
         assert event.user == "Rabbit Hole"
         assert event.user_id == "@neo:matrix.org"