Repository: matrix-org/synapse

Issue:
"Internal server error" when upgrading a room created with Conduit
### Description

Upgrading a room created with Conduit returns "Internal server error"

### Steps to reproduce

1. Create a room with Conduit
2. Invite a Synapse user to it, make them admin
3. Make the Synapse user upgrade the room (eg. `/upgraderoom 11` in Eleweb)

Synapse returns `{"errcode":"M_UNKNOWN","error":"Internal server error"}`

### Homeserver

matrix.org and Conduit

### Synapse Version

1.96.1 (b=matrix-org-hotfixes,9c3b906b3a)

### Installation Method

I don't know

### Database

postgresql

### Workers

Multiple workers

### Platform

n/a

### Configuration

_No response_

### Relevant log output

```shell
Nothing from Conduit.

I tried it on matrix.org, `synapse-trace-id: b6592675a1c241fa`
```


### Anything else that would be useful to know?

_No response_


Discussion:
Raw stack trace:

```
ValueError: max() arg is an empty sequence
  File "twisted/internet/defer.py", line 2000, in _inlineCallbacks
    result = context.run(gen.send, result)
  File "synapse/util/caches/response_cache.py", line 258, in cb
    return await callback(*args, **kwargs)
  File "synapse/handlers/room.py", line 306, in _upgrade_room
    await self.clone_existing_room(
  File "synapse/handlers/room.py", line 552, in clone_existing_room
    state_default_int, ban, max(event_power_levels.values())
```

Corresponds to: https://github.com/matrix-org/synapse/blob/63d96bfc61fcbf53e9607c63f215d2dde387de29/synapse/handlers/room.py#L552

Sentry link: https://sentry.tools.element.io/organizations/element/issues/85240/?project=2

The power level content is:

`{"users":{"@<redacted>":100,"@<redacted>":100,"@<redacted>":100}}`

So looks like we need to correctly handle an empty/missing `events` key. I think we can change the `max(event_power_levels.values())` to include a `default=0`?

Resolving patch:
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -549,7 +549,7 @@ async def clone_existing_room(
         except (TypeError, ValueError):
             ban = 50
         needed_power_level = max(
-            state_default_int, ban, max(event_power_levels.values())
+            state_default_int, ban, max(event_power_levels.values(), default=0)
         )
 
         # Get the user's current power level, this matches the logic in get_user_power_level,


Test patch:
diff --git a/tests/rest/client/test_upgrade_room.py b/tests/rest/client/test_upgrade_room.py
--- a/tests/rest/client/test_upgrade_room.py
+++ b/tests/rest/client/test_upgrade_room.py
@@ -246,6 +246,34 @@ def test_stringy_power_levels(self) -> None:
         # We should now have an integer power level.
         self.assertEqual(new_power_levels["users"][self.creator], 100, new_power_levels)
 
+    def test_events_field_missing(self) -> None:
+        """Regression test for https://github.com/matrix-org/synapse/issues/16715."""
+        # Create a new room.
+        room_id = self.helper.create_room_as(
+            self.creator, tok=self.creator_token, room_version="10"
+        )
+        self.helper.join(room_id, self.other, tok=self.other_token)
+
+        # Retrieve the room's current power levels.
+        power_levels = self.helper.get_state(
+            room_id,
+            "m.room.power_levels",
+            tok=self.creator_token,
+        )
+
+        # Remove the events field and re-set the power levels.
+        del power_levels["events"]
+        self.helper.send_state(
+            room_id,
+            "m.room.power_levels",
+            body=power_levels,
+            tok=self.creator_token,
+        )
+
+        # Upgrade the room. Check the homeserver reports success.
+        channel = self._upgrade_room(room_id=room_id)
+        self.assertEqual(200, channel.code, channel.result)
+
     def test_space(self) -> None:
         """Test upgrading a space."""