1414# limitations under the License.
1515import email .utils
1616import logging
17- from typing import Dict , List , Optional , Tuple
17+ from typing import Dict , List , Optional , Tuple , Union
1818
1919from synapse .api .constants import EventTypes , JoinRules , Membership , RoomCreationPreset
2020from synapse .api .errors import SynapseError
2121from synapse .config ._base import ConfigError
2222from synapse .events import EventBase
2323from synapse .module_api import ModuleApi
2424from synapse .types import Requester , StateMap , UserID , get_domain_from_id
25+ from synapse .util .frozenutils import unfreeze
2526
2627logger = logging .getLogger (__name__ )
2728
2829ACCESS_RULES_TYPE = "im.vector.room.access_rules"
30+ FROZEN_STATE_TYPE = "io.element.room.frozen"
2931
3032
3133class AccessRules :
@@ -108,7 +110,7 @@ def parse_config(config: Dict) -> Dict:
108110 ConfigError: If there was an issue with the provided module configuration.
109111 """
110112 if "id_server" not in config :
111- raise ConfigError ("No IS for event rules TchapEventRules " )
113+ raise ConfigError ("No IS for event rules RoomAccessRules " )
112114
113115 return config
114116
@@ -320,7 +322,7 @@ async def check_event_allowed(
320322 self ,
321323 event : EventBase ,
322324 state_events : StateMap [EventBase ],
323- ) -> bool :
325+ ) -> Union [ bool , dict ] :
324326 """Implements synapse.events.ThirdPartyEventRules.check_event_allowed.
325327
326328 Checks the event's type and the current rule and calls the right function to
@@ -332,8 +334,18 @@ async def check_event_allowed(
332334 State events in the room the event originated from.
333335
334336 Returns:
335- True if the event can be allowed, False otherwise.
337+ True if the event should be allowed, False if it should be rejected, or a dictionary if the
338+ event needs to be rebuilt (containing the event's new content).
336339 """
340+ if event .type == FROZEN_STATE_TYPE :
341+ return await self ._on_frozen_state_change (event , state_events )
342+
343+ # If the room is frozen, we allow a very small number of events to go through
344+ # (unfreezing, leaving, etc.).
345+ frozen_state = state_events .get ((FROZEN_STATE_TYPE , "" ))
346+ if frozen_state and frozen_state .content .get ("frozen" , False ):
347+ return await self ._on_event_when_frozen (event , state_events )
348+
337349 if event .type == ACCESS_RULES_TYPE :
338350 return await self ._on_rules_change (event , state_events )
339351
@@ -394,6 +406,129 @@ async def check_visibility_can_be_modified(
394406 # published to the public rooms directory.
395407 return True
396408
409+ async def _on_event_when_frozen (
410+ self ,
411+ event : EventBase ,
412+ state_events : StateMap [EventBase ],
413+ ) -> Union [bool , dict ]:
414+ """Check if the provided event is allowed when the room is frozen.
415+
416+ The only events allowed are for a member to leave the room, and for the room to
417+ be (un)frozen. In the latter case, also attempt to unfreeze the room.
418+
419+
420+ Args:
421+ event: The event to allow or deny.
422+ state_events: A dict mapping (event type, state key) to state event.
423+ State events in the room before the event was sent.
424+ Returns:
425+ A boolean indicating whether the event is allowed, or a dict if the event is
426+ allowed but the state of the room has been modified (i.e. the room has been
427+ unfrozen). This is because returning a dict of the event forces Synapse to
428+ rebuild it, which is needed if the state of the room has changed.
429+ """
430+ # Allow users to leave the room; don't allow kicks though.
431+ if (
432+ event .type == EventTypes .Member
433+ and event .membership == Membership .LEAVE
434+ and event .sender == event .state_key
435+ ):
436+ return True
437+
438+ if event .type == EventTypes .PowerLevels :
439+ # Check if the power level event is associated with a room unfreeze (because
440+ # the power level events will be sent before the frozen state event). This
441+ # means we check that the users_default is back to 0 and the sender set
442+ # themselves as admin.
443+ current_power_levels = state_events .get ((EventTypes .PowerLevels , "" ))
444+ if current_power_levels :
445+ old_content = current_power_levels .content .copy ()
446+ old_content ["users_default" ] = 0
447+
448+ new_content = unfreeze (event .content )
449+ sender_pl = new_content .get ("users" , {}).get (event .sender , 0 )
450+
451+ # We don't care about the users section as long as the new event gives
452+ # full power to the sender.
453+ del old_content ["users" ]
454+ del new_content ["users" ]
455+
456+ if new_content == old_content and sender_pl == 100 :
457+ return True
458+
459+ return False
460+
461+ async def _on_frozen_state_change (
462+ self ,
463+ event : EventBase ,
464+ state_events : StateMap [EventBase ],
465+ ) -> Union [bool , dict ]:
466+ frozen = event .content .get ("frozen" , None )
467+ if not isinstance (frozen , bool ):
468+ # Invalid event: frozen is either missing or not a boolean.
469+ return False
470+
471+ # If the event was sent from a restricted homeserver, don't allow the state
472+ # change.
473+ if (
474+ UserID .from_string (event .sender ).domain
475+ in self .domains_forbidden_when_restricted
476+ ):
477+ return False
478+
479+ current_frozen_state = state_events .get (
480+ (FROZEN_STATE_TYPE , "" ),
481+ ) # type: EventBase
482+
483+ if (
484+ current_frozen_state is not None
485+ and current_frozen_state .content .get ("frozen" ) == frozen
486+ ):
487+ # This is a noop, accept the new event but don't do anything more.
488+ return True
489+
490+ # If the event was received over federation, we want to accept it but not to
491+ # change the power levels.
492+ if not self ._is_local_user (event .sender ):
493+ return True
494+
495+ current_power_levels = state_events .get (
496+ (EventTypes .PowerLevels , "" ),
497+ ) # type: EventBase
498+
499+ power_levels_content = unfreeze (current_power_levels .content )
500+
501+ if not frozen :
502+ # We're unfreezing the room: enforce the right value for the power levels so
503+ # the room isn't in a weird/broken state afterwards.
504+ users = power_levels_content .setdefault ("users" , {})
505+ users [event .sender ] = 100
506+ power_levels_content ["users_default" ] = 0
507+ else :
508+ # Send a new power levels event with a similar content to the previous one
509+ # except users_default is 100 to allow any user to unfreeze the room.
510+ power_levels_content ["users_default" ] = 100
511+
512+ # Just to be safe, also delete all users that don't have a power level of
513+ # 100, in order to prevent anyone from being unable to unfreeze the room.
514+ users = {}
515+ for user , level in power_levels_content ["users" ].items ():
516+ if level == 100 :
517+ users [user ] = level
518+ power_levels_content ["users" ] = users
519+
520+ await self .module_api .create_and_send_event_into_room (
521+ {
522+ "room_id" : event .room_id ,
523+ "sender" : event .sender ,
524+ "type" : EventTypes .PowerLevels ,
525+ "content" : power_levels_content ,
526+ "state_key" : "" ,
527+ }
528+ )
529+
530+ return event .get_dict ()
531+
397532 async def _on_rules_change (
398533 self , event : EventBase , state_events : StateMap [EventBase ]
399534 ):
@@ -448,7 +583,7 @@ async def _on_membership_or_invite(
448583 event : EventBase ,
449584 rule : str ,
450585 state_events : StateMap [EventBase ],
451- ) -> bool :
586+ ) -> Union [ bool , dict ] :
452587 """Applies the correct rule for incoming m.room.member and
453588 m.room.third_party_invite events.
454589
@@ -459,7 +594,10 @@ async def _on_membership_or_invite(
459594 The state of the room before the event was sent.
460595
461596 Returns:
462- True if the event can be allowed, False otherwise.
597+ A boolean indicating whether the event is allowed, or a dict if the event is
598+ allowed but the state of the room has been modified (i.e. the room has been
599+ frozen). This is because returning a dict of the event forces Synapse to
600+ rebuild it, which is needed if the state of the room has changed.
463601 """
464602 if rule == AccessRules .RESTRICTED :
465603 ret = self ._on_membership_or_invite_restricted (event )
@@ -472,7 +610,7 @@ async def _on_membership_or_invite(
472610 # might want to change that in the future.
473611 ret = self ._on_membership_or_invite_restricted (event )
474612
475- if event .type == "m.room.member" :
613+ if event .type == EventTypes . Member :
476614 # If this is an admin leaving, and they are the last admin in the room,
477615 # raise the power levels of the room so that the room is 'frozen'.
478616 #
@@ -484,6 +622,9 @@ async def _on_membership_or_invite(
484622 and event .membership == Membership .LEAVE
485623 ):
486624 await self ._freeze_room_if_last_admin_is_leaving (event , state_events )
625+ if ret :
626+ # Return an event dict to force Synapse into rebuilding the event.
627+ return event .get_dict ()
487628
488629 return ret
489630
@@ -535,88 +676,13 @@ async def _freeze_room_if_last_admin_is_leaving(
535676 # Freeze the room by raising the required power level to send events to 100
536677 logger .info ("Freezing room '%s'" , event .room_id )
537678
538- # Modify the existing power levels to raise all required types to 100
539- #
540- # This changes a power level state event's content from something like:
541- # {
542- # "redact": 50,
543- # "state_default": 50,
544- # "ban": 50,
545- # "notifications": {
546- # "room": 50
547- # },
548- # "events": {
549- # "m.room.avatar": 50,
550- # "m.room.encryption": 50,
551- # "m.room.canonical_alias": 50,
552- # "m.room.name": 50,
553- # "im.vector.modular.widgets": 50,
554- # "m.room.topic": 50,
555- # "m.room.tombstone": 50,
556- # "m.room.history_visibility": 100,
557- # "m.room.power_levels": 100
558- # },
559- # "users_default": 0,
560- # "events_default": 0,
561- # "users": {
562- # "@admin:example.com": 100,
563- # },
564- # "kick": 50,
565- # "invite": 0
566- # }
567- #
568- # to
569- #
570- # {
571- # "redact": 100,
572- # "state_default": 100,
573- # "ban": 100,
574- # "notifications": {
575- # "room": 50
576- # },
577- # "events": {}
578- # "users_default": 0,
579- # "events_default": 100,
580- # "users": {
581- # "@admin:example.com": 100,
582- # },
583- # "kick": 100,
584- # "invite": 100
585- # }
586- new_content = {}
587- for key , value in power_level_content .items ():
588- # Do not change "users_default", as that key specifies the default power
589- # level of new users
590- if isinstance (value , int ) and key != "users_default" :
591- value = 100
592- new_content [key ] = value
593-
594- # Set some values in case they are missing from the original
595- # power levels event content
596- new_content .update (
597- {
598- # Clear out any special-cased event keys
599- "events" : {},
600- # Ensure state_default and events_default keys exist and are 100.
601- # Otherwise a lower PL user could potentially send state events that
602- # aren't explicitly mentioned elsewhere in the power level dict
603- "state_default" : 100 ,
604- "events_default" : 100 ,
605- # Membership events default to 50 if they aren't present. Set them
606- # to 100 here, as they would be set to 100 if they were present anyways
607- "ban" : 100 ,
608- "kick" : 100 ,
609- "invite" : 100 ,
610- "redact" : 100 ,
611- }
612- )
613-
679+ # Mark the room as frozen
614680 await self .module_api .create_and_send_event_into_room (
615681 {
616682 "room_id" : event .room_id ,
617683 "sender" : user_id ,
618- "type" : EventTypes . PowerLevels ,
619- "content" : new_content ,
684+ "type" : FROZEN_STATE_TYPE ,
685+ "content" : { "frozen" : True } ,
620686 "state_key" : "" ,
621687 }
622688 )
0 commit comments