Repository: opsdroid/opsdroid

Issue:
Matrix database raises an error when it should return None on getting a nonexistent key
<!-- Before you post an issue or if you are unsure about something join our matrix channel https://riot.im/app/#/room/#opsdroid-general:matrix.org and ask away! We are more than happy to help you. -->
# Description
Please include a summary of the issue.


## Steps to Reproduce

Run `opsdroid.database.get("key")` when `"key"` has never been added to the room state.


## Expected Functionality

the `get()` method should return None


## Experienced Functionality
An exception was rasied:

<details>
```
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/opsdroid/core.py", line 427, in run_skill
    return await skill(self, config, event)
  File "/root/.local/share/opsdroid/opsdroid-modules/skill/motw/__init__.py", line 72, in _wrapper
    return await func(opsdroid, config, message)
  File "/root/.local/share/opsdroid/opsdroid-modules/skill/motw/__init__.py", line 277, in level_up
    all_exp = await opsdroid.memory.get("motw_experience") or {}
  File "/usr/local/lib/python3.8/site-packages/opsdroid/memory.py", line 37, in get
    result = await self._get_from_database(key)
  File "/usr/local/lib/python3.8/site-packages/opsdroid/memory.py", line 86, in _get_from_database
    results.append(await database.get(key))
  File "/usr/local/lib/python3.8/site-packages/opsdroid/database/matrix/__init__.py", line 173, in get
    raise RuntimeError(
RuntimeError: Error getting motw_experience from matrix room !pgoIAkPLtxFWwdEVJf:cadair.com: Event not found.(M_NOT_FOUND)
```
</details>

## Versions
- **Opsdroid version:** master
- **Python version:** 3.8

## Database Config:
```
databases:
  matrix:
    single_state_key: False
    no_cache: true
```


Discussion:

Resolving patch:
diff --git a/opsdroid/database/matrix/__init__.py b/opsdroid/database/matrix/__init__.py
--- a/opsdroid/database/matrix/__init__.py
+++ b/opsdroid/database/matrix/__init__.py
@@ -89,8 +89,7 @@ async def migrate_database(self):
                         )
                     )
                     await self.connector.connection.room_redact(
-                        room_id=self.room_id,
-                        event_id=event["event_id"],
+                        room_id=self.room_id, event_id=event["event_id"]
                     )
 
         self.should_migrate = False
@@ -165,19 +164,21 @@ async def get(self, key, get_full=False):
         )
 
         ori_data = await self.connector.connection.room_get_state_event(
-            room_id=self.room_id,
-            event_type=self._event_type,
-            state_key=state_key,
+            room_id=self.room_id, event_type=self._event_type, state_key=state_key
         )
+
         if isinstance(ori_data, RoomGetStateEventError):
+            if (
+                ori_data.transport_response is not None
+                and ori_data.transport_response.status == 404
+            ):
+                _LOGGER.error(
+                    f"Error getting {key} from matrix room {self.room_id}: Event not found"
+                )
+                return
             raise RuntimeError(
                 f"Error getting {key} from matrix room {self.room_id}: {ori_data.message}({ori_data.status_code})"
             )
-        elif ori_data.transport_response.status == 404:
-            _LOGGER.error(
-                f"Error getting {key} from matrix room {self.room_id}: Event not found"
-            )
-            return
 
         data = ori_data.content
 
@@ -197,8 +198,7 @@ async def get(self, key, get_full=False):
         for k, v in data.items():
             if isinstance(v, dict) and len(v) == 1 and "encrypted_val" in v:
                 resp = await self.connector.connection.room_get_event(
-                    room_id=self.room_id,
-                    event_id=v["encrypted_val"],
+                    room_id=self.room_id, event_id=v["encrypted_val"]
                 )
                 if isinstance(resp, RoomGetEventError):
                     _LOGGER.error(
@@ -230,9 +230,7 @@ async def delete(self, key):
         )
 
         data = await self.connector.connection.room_get_state_event(
-            room_id=self.room_id,
-            event_type=self._event_type,
-            state_key=state_key,
+            room_id=self.room_id, event_type=self._event_type, state_key=state_key
         )
         if isinstance(data, RoomGetStateEventError):
             _LOGGER.error(
@@ -240,7 +238,10 @@ async def delete(self, key):
             )
             return
 
-        if data.transport_response.status == 404:
+        if (
+            data.transport_response is not None
+            and data.transport_response.status == 404
+        ):
             _LOGGER.error(
                 f"State event {self._event_type} with state key '{state_key}' doesn't exist."
             )
diff --git a/opsdroid/events.py b/opsdroid/events.py
--- a/opsdroid/events.py
+++ b/opsdroid/events.py
@@ -55,7 +55,7 @@ def __new__(mcls, name, bases, members):  # noqa: D102
         if "_no_register" in members:
             return cls
 
-        if name in mcls.event_registry:
+        if name in mcls.event_registry and mcls.event_registry[name] is not cls:
             raise NameError(
                 "An event subclass named {name} has already been "
                 "defined. Event subclass names must be globally "
@@ -77,14 +77,14 @@ class Event(metaclass=EventMetaClass):
     Args:
         user_id (string, optional): String id of user sending message
         user (string, optional): String name of user sending message
-        room (string, optional): String name of the room or chat channel in
-                                 which message was sent
+        target (string, optional): String name of the room or chat channel in
+                                   which message was sent
         connector (Connector, optional): Connector object used to interact with
                                          given chat service
         raw_event (dict, optional): Raw message as provided by chat service.
                                     None by default
         raw_parses (dict, optional): Raw response as provided by parse service.
-                            None by default
+                                     None by default
         event_id (object, optional): The unique id for this event as provided
                                      by the connector.
         linked_event (Event, optional): An event to link to this one, i.e. the
@@ -195,19 +195,19 @@ class Message(Event):
 
     Args:
         text (string): String text of message
-        room (string, optional): String name of the room or chat channel in
-                                 which message was sent
+        target (string, optional): String name of the room or chat channel in
+                                   which message was sent
         connector (Connector, optional): Connector object used to interact with
                                          given chat service
         raw_event (dict, optional): Raw message as provided by chat service.
                                     None by default
         raw_parses (dict, optional): Raw response as provided by parse service.
-                    None by default
+                                     None by default
 
     Attributes:
         created: Local date and time that message object was created
         user: String name of user sending message
-        room: String name of the room or chat channel in which message was sent
+        target: String name of the room or chat channel in which message was sent
         connector: Connector object used to interact with given chat service
         text: Text of message as string
         raw_event: Raw message provided by chat service


Test patch:
diff --git a/tests/test_database_matrix.py b/tests/test_database_matrix.py
--- a/tests/test_database_matrix.py
+++ b/tests/test_database_matrix.py
@@ -8,6 +8,7 @@
 from opsdroid.core import OpsDroid
 from opsdroid.database.matrix import DatabaseMatrix, memory_in_event_room
 from opsdroid.events import Message
+from opsdroid.cli.start import configure_lang  # noqa
 
 
 @pytest.fixture
@@ -97,7 +98,7 @@ async def test_default_config(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
                 {"twim": {"hello": "world"}},
             ),
-        ],
+        ]
     )
 
 
@@ -166,7 +167,7 @@ async def test_put_custom_state_key(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/wibble",
                 {"twim": {"hello": "world"}},
             ),
-        ],
+        ]
     )
 
 
@@ -234,7 +235,7 @@ async def test_single_state_key_false(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/hello",
                 {"hello": "world"},
             ),
-        ],
+        ]
     )
 
 
@@ -302,7 +303,7 @@ async def test_single_state_key_false_dict(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/twim",
                 {"twim": {"hello": "world", "twim": "hello"}},
             ),
-        ],
+        ]
     )
 
 
@@ -373,7 +374,7 @@ async def test_single_state_not_a_dict(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
                 {"twim": value},
             ),
-        ],
+        ]
     )
 
 
@@ -382,8 +383,6 @@ async def test_default_update_same_key(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": {"hello": "world"}}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix(
         {"should_encrypt": False, "single_state_key": False}, opsdroid=opsdroid_matrix
@@ -403,7 +402,7 @@ async def test_default_update_same_key(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/twim",
                 {"twim": {"hello": "bob"}},
             ),
-        ],
+        ]
     )
 
 
@@ -417,8 +416,6 @@ def side_effect(resp, *args, **kwargs):
             resp = nio.RoomGetStateEventResponse(
                 {"twim": {"encrypted_val": "enceventid"}}, "", "", ""
             )
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         elif resp is nio.RoomGetEventResponse:
             event = nio.Event(
@@ -469,8 +466,6 @@ async def test_update_same_key_single_state_key(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": {"hello": "world"}}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix(
         {"should_encrypt": False, "single_state_key": True}, opsdroid=opsdroid_matrix
@@ -490,7 +485,7 @@ async def test_update_same_key_single_state_key(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
                 {"twim": {"hello": "bob"}},
             ),
-        ],
+        ]
     )
 
 
@@ -506,8 +501,6 @@ def side_effect(resp, *args, **kwargs):
             resp = nio.RoomGetStateEventResponse(
                 {"twim": {"encrypted_val": "enceventid"}}, "", "", ""
             )
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         elif resp is nio.RoomGetEventResponse:
             event = nio.Event(
@@ -558,8 +551,6 @@ async def test_default_update_same_key_value(patched_send, opsdroid_matrix, capl
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": {"hello": "world"}}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix(
         {"should_encrypt": False, "single_state_key": False}, opsdroid=opsdroid_matrix
@@ -574,7 +565,7 @@ async def test_default_update_same_key_value(patched_send, opsdroid_matrix, capl
                 "GET",
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/twim",
             )
-        ],
+        ]
     )
 
     assert ["Not updating matrix state, as content hasn't changed."] == [
@@ -592,8 +583,6 @@ def side_effect(resp, *args, **kwargs):
             resp = nio.RoomGetStateEventResponse(
                 {"twim": {"encrypted_val": "enceventid"}}, "", "", ""
             )
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         else:
             event = nio.Event(
@@ -623,7 +612,7 @@ def side_effect(resp, *args, **kwargs):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/twim",
             ),
             matrix_call("GET", "/_matrix/client/r0/rooms/%21notaroomid/event/"),
-        ],
+        ]
     )
 
     assert ["Not updating matrix state, as content hasn't changed."] == [
@@ -638,8 +627,6 @@ async def test_default_update_same_key_value_single_state_key(
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": {"hello": "world"}}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix(
         {"should_encrypt": False, "single_state_key": True}, opsdroid=opsdroid_matrix
@@ -654,7 +641,7 @@ async def test_default_update_same_key_value_single_state_key(
                 "GET",
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
             )
-        ],
+        ]
     )
 
     assert ["Not updating matrix state, as content hasn't changed."] == [
@@ -672,8 +659,6 @@ def side_effect(resp, *args, **kwargs):
             resp = nio.RoomGetStateEventResponse(
                 {"twim": {"encrypted_val": "enceventid"}}, "", "", ""
             )
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         else:
             event = nio.Event(
@@ -703,7 +688,7 @@ def side_effect(resp, *args, **kwargs):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
             ),
             matrix_call("GET", "/_matrix/client/r0/rooms/%21notaroomid/event/"),
-        ],
+        ]
     )
 
     assert ["Not updating matrix state, as content hasn't changed."] == [
@@ -716,8 +701,6 @@ async def test_default_update_single_state_key(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": "hello"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix(
         {"should_encrypt": False, "single_state_key": True}, opsdroid=opsdroid_matrix
@@ -737,7 +720,7 @@ async def test_default_update_single_state_key(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
                 {"twim": "hello", "pill": "red"},
             ),
-        ],
+        ]
     )
 
 
@@ -751,8 +734,6 @@ async def test_default_update_single_state_key_enc(
     def side_effect(resp, *args, **kwargs):
         if resp is nio.RoomGetStateEventResponse:
             resp = nio.RoomGetStateEventResponse({"twim": "hello"}, "", "", "")
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         else:
             return nio.RoomSendResponse("enceventid", "!notaroomid")
@@ -789,8 +770,6 @@ async def test_get_single_state_key(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": "hello", "wibble": "wobble"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix({}, opsdroid=opsdroid_matrix)
     db.should_migrate = False
@@ -817,8 +796,6 @@ def side_effect(resp, *args, **kwargs):
                 "",
                 "",
             )
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         else:
             event = nio.Event(
@@ -847,7 +824,7 @@ def side_effect(resp, *args, **kwargs):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
             ),
             matrix_call("GET", "/_matrix/client/r0/rooms/%21notaroomid/event/"),
-        ],
+        ]
     )
 
     assert data == "hello"
@@ -858,8 +835,6 @@ async def test_get(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": "world"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix(
         {"should_encrypt": False, "single_state_key": False}, opsdroid=opsdroid_matrix
@@ -885,8 +860,6 @@ def side_effect(resp, *args, **kwargs):
             resp = nio.RoomGetStateEventResponse(
                 {"twim": {"encrypted_val": "enceventid"}}, "", "", ""
             )
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         else:
             event = nio.Event(
@@ -915,7 +888,7 @@ def side_effect(resp, *args, **kwargs):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/twim",
             ),
             matrix_call("GET", "/_matrix/client/r0/rooms/%21notaroomid/event/"),
-        ],
+        ]
     )
 
     assert data == "world"
@@ -926,8 +899,6 @@ async def test_get_no_key_single_state_key(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"wibble": "wobble"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix(
         {"should_encrypt": False, "single_state_key": True}, opsdroid=opsdroid_matrix
@@ -941,16 +912,17 @@ async def test_get_no_key_single_state_key(patched_send, opsdroid_matrix):
 
 @pytest.mark.asyncio
 async def test_get_no_key_404(patched_send, opsdroid_matrix):
-    patched_send.return_value = nio.RoomGetStateEventError({"errcode": 404})
+    patched_send.return_value = nio.RoomGetStateEventError({"errcode": "M_NOTFOUND"})
+    patched_send.return_value.transport_response = AsyncMock()
+    patched_send.return_value.transport_response.status = 404
 
     db = DatabaseMatrix(
         {"should_encrypt": False, "single_state_key": False}, opsdroid=opsdroid_matrix
     )
     db.should_migrate = False
 
-    with pytest.raises(RuntimeError):
-        data = await db.get("twim")
-        assert data is None
+    data = await db.get("twim")
+    assert data is None
 
 
 @pytest.mark.asyncio
@@ -963,8 +935,7 @@ async def test_get_no_key_500(patched_send, opsdroid_matrix):
     db.should_migrate = False
 
     with pytest.raises(RuntimeError):
-        data = await db.get("twim")
-        assert data is None
+        await db.get("twim")
 
 
 @pytest.mark.asyncio
@@ -972,8 +943,6 @@ async def test_delete(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": "hello"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix({}, opsdroid=opsdroid_matrix)
     db.should_migrate = False
@@ -990,7 +959,7 @@ async def test_delete(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
                 {},
             ),
-        ],
+        ]
     )
 
     assert data == "hello"
@@ -1001,8 +970,6 @@ async def test_delete_single_state_key_false(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": "hello"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix({"single_state_key": False}, opsdroid=opsdroid_matrix)
     db.should_migrate = False
@@ -1019,7 +986,7 @@ async def test_delete_single_state_key_false(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/twim",
                 {},
             ),
-        ],
+        ]
     )
 
     assert data == "hello"
@@ -1030,8 +997,6 @@ async def test_delete_multiple_keys(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"hello": "world", "twim": "hello", "pill": "red"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix({}, opsdroid=opsdroid_matrix)
     db.should_migrate = False
@@ -1048,7 +1013,7 @@ async def test_delete_multiple_keys(patched_send, opsdroid_matrix):
                 "/_matrix/client/r0/rooms/%21notaroomid/state/dev.opsdroid.database/",
                 {"pill": "red"},
             ),
-        ],
+        ]
     )
 
     assert data == ["world", "hello"]
@@ -1059,8 +1024,6 @@ async def test_delete_no_key(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"twim": "hello"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix({}, opsdroid=opsdroid_matrix)
     db.should_migrate = False
@@ -1101,8 +1064,6 @@ async def test_room_switch(patched_send, opsdroid_matrix):
     patched_send.return_value = nio.RoomGetStateEventResponse(
         {"hello": "world"}, "", "", ""
     )
-    patched_send.return_value.transport_response = AsyncMock()
-    patched_send.return_value.transport_response.status = 200
 
     db = DatabaseMatrix({"should_encrypt": False}, opsdroid=opsdroid_matrix)
     db.should_migrate = False
@@ -1199,7 +1160,7 @@ def side_effect(resp, *args, **kwargs):
                 "{}",
                 response_data=("!notaroomid",),
             ),
-        ],
+        ]
     )
 
 
@@ -1254,7 +1215,7 @@ def side_effect(resp, *args, **kwargs):
                 "{}",
                 response_data=("!notaroomid",),
             ),
-        ],
+        ]
     )
 
 
@@ -1265,8 +1226,6 @@ def side_effect(resp, *args, **kwargs):
             resp = nio.RoomGetStateEventResponse(
                 {"twim": {"encrypted_val": "enceventid"}}, "", "", ""
             )
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         else:
             return nio.RoomGetEventError(message="testing")
@@ -1292,7 +1251,7 @@ def side_effect(resp, *args, **kwargs):
         await db.get("hello")
 
     assert [
-        "Error migrating from opsdroid.database to dev.opsdroid.database in room !notaroomid: testing(None)",
+        "Error migrating from opsdroid.database to dev.opsdroid.database in room !notaroomid: testing(None)"
     ] == [rec.message for rec in caplog.records]
 
     patched_send.side_effect = [
@@ -1313,8 +1272,6 @@ def side_effect(resp, *args, **kwargs):
             resp = nio.RoomGetStateEventResponse(
                 {"hello": {"encrypted_val": "enceventid"}}, "", "", ""
             )
-            resp.transport_response = AsyncMock()
-            resp.transport_response.status = 200
             return resp
         else:
             return nio.RoomGetEventError(message="testing")