1313# See the License for the specific language governing permissions and
1414# limitations under the License.
1515import email .utils
16+ import logging
1617from typing import Dict , List , Optional , Tuple
1718
1819from twisted .internet import defer
2223from synapse .config ._base import ConfigError
2324from synapse .events import EventBase
2425from synapse .module_api import ModuleApi
25- from synapse .types import Requester , StateMap , get_domain_from_id
26+ from synapse .types import Requester , StateMap , UserID , get_domain_from_id
27+
28+ logger = logging .getLogger (__name__ )
2629
2730ACCESS_RULES_TYPE = "im.vector.room.access_rules"
2831
@@ -323,7 +326,7 @@ async def check_event_allowed(
323326 )
324327
325328 if event .type == EventTypes .Member or event .type == EventTypes .ThirdPartyInvite :
326- return self ._on_membership_or_invite (event , rule , state_events )
329+ return await self ._on_membership_or_invite (event , rule , state_events )
327330
328331 if event .type == EventTypes .JoinRules :
329332 return self ._on_join_rule_change (event , rule )
@@ -420,7 +423,7 @@ async def _on_rules_change(
420423 prev_rule == AccessRules .RESTRICTED and new_rule == AccessRules .UNRESTRICTED
421424 )
422425
423- def _on_membership_or_invite (
426+ async def _on_membership_or_invite (
424427 self , event : EventBase , rule : str , state_events : StateMap [EventBase ],
425428 ) -> bool :
426429 """Applies the correct rule for incoming m.room.member and
@@ -446,8 +449,154 @@ def _on_membership_or_invite(
446449 # might want to change that in the future.
447450 ret = self ._on_membership_or_invite_restricted (event )
448451
452+ if event .type == "m.room.member" :
453+ # If this is an admin leaving, and they are the last admin in the room,
454+ # raise the power levels of the room so that the room is 'frozen'.
455+ #
456+ # We have to freeze the room by puppeting an admin user, which we can
457+ # only do for local users
458+ if (
459+ self ._is_local_user (event .sender )
460+ and event .membership == Membership .LEAVE
461+ ):
462+ await self ._freeze_room_if_last_admin_is_leaving (event , state_events )
463+
449464 return ret
450465
466+ async def _freeze_room_if_last_admin_is_leaving (
467+ self , event : EventBase , state_events : StateMap [EventBase ]
468+ ):
469+ power_level_state_event = state_events .get (
470+ (EventTypes .PowerLevels , "" )
471+ ) # type: EventBase
472+ if not power_level_state_event :
473+ return
474+ power_level_content = power_level_state_event .content
475+
476+ # Do some validation checks on the power level state event
477+ if (
478+ not isinstance (power_level_content , dict )
479+ or "users" not in power_level_content
480+ or not isinstance (power_level_content ["users" ], dict )
481+ ):
482+ # We can't use this power level event to determine whether the room should be
483+ # frozen. Bail out.
484+ return
485+
486+ user_id = event .get ("sender" )
487+ if not user_id :
488+ return
489+
490+ # Get every admin user defined in the room's state
491+ admin_users = {
492+ user
493+ for user , power_level in power_level_content ["users" ].items ()
494+ if power_level >= 100
495+ }
496+
497+ if user_id not in admin_users :
498+ # This user is not an admin, ignore them
499+ return
500+
501+ if any (
502+ event_type == EventTypes .Member
503+ and event .membership in [Membership .JOIN , Membership .INVITE ]
504+ and state_key in admin_users
505+ and state_key != user_id
506+ for (event_type , state_key ), event in state_events .items ()
507+ ):
508+ # There's another admin user in, or invited to, the room
509+ return
510+
511+ # Freeze the room by raising the required power level to send events to 100
512+ logger .info ("Freezing room '%s'" , event .room_id )
513+
514+ # Modify the existing power levels to raise all required types to 100
515+ #
516+ # This changes a power level state event's content from something like:
517+ # {
518+ # "redact": 50,
519+ # "state_default": 50,
520+ # "ban": 50,
521+ # "notifications": {
522+ # "room": 50
523+ # },
524+ # "events": {
525+ # "m.room.avatar": 50,
526+ # "m.room.encryption": 50,
527+ # "m.room.canonical_alias": 50,
528+ # "m.room.name": 50,
529+ # "im.vector.modular.widgets": 50,
530+ # "m.room.topic": 50,
531+ # "m.room.tombstone": 50,
532+ # "m.room.history_visibility": 100,
533+ # "m.room.power_levels": 100
534+ # },
535+ # "users_default": 0,
536+ # "events_default": 0,
537+ # "users": {
538+ # "@admin:example.com": 100,
539+ # },
540+ # "kick": 50,
541+ # "invite": 0
542+ # }
543+ #
544+ # to
545+ #
546+ # {
547+ # "redact": 100,
548+ # "state_default": 100,
549+ # "ban": 100,
550+ # "notifications": {
551+ # "room": 50
552+ # },
553+ # "events": {}
554+ # "users_default": 0,
555+ # "events_default": 100,
556+ # "users": {
557+ # "@admin:example.com": 100,
558+ # },
559+ # "kick": 100,
560+ # "invite": 100
561+ # }
562+ new_content = {}
563+ for key , value in power_level_content .items ():
564+ # Do not change "users_default", as that key specifies the default power
565+ # level of new users
566+ if isinstance (value , int ) and key != "users_default" :
567+ value = 100
568+ new_content [key ] = value
569+
570+ # Set some values in case they are missing from the original
571+ # power levels event content
572+ new_content .update (
573+ {
574+ # Clear out any special-cased event keys
575+ "events" : {},
576+ # Ensure state_default and events_default keys exist and are 100.
577+ # Otherwise a lower PL user could potentially send state events that
578+ # aren't explicitly mentioned elsewhere in the power level dict
579+ "state_default" : 100 ,
580+ "events_default" : 100 ,
581+ # Membership events default to 50 if they aren't present. Set them
582+ # to 100 here, as they would be set to 100 if they were present anyways
583+ "ban" : 100 ,
584+ "kick" : 100 ,
585+ "invite" : 100 ,
586+ "redact" : 100 ,
587+ }
588+ )
589+
590+ await self .module_api .create_and_send_event_into_room (
591+ {
592+ "room_id" : event .room_id ,
593+ "sender" : user_id ,
594+ "type" : EventTypes .PowerLevels ,
595+ "content" : new_content ,
596+ "state_key" : "" ,
597+ }
598+ )
599+
451600 def _on_membership_or_invite_restricted (self , event : EventBase ) -> bool :
452601 """Implements the checks and behaviour specified for the "restricted" rule.
453602
@@ -753,6 +902,25 @@ def _is_invite_from_threepid(invite: EventBase, threepid_invite_token: str) -> b
753902
754903 return token == threepid_invite_token
755904
905+ def _is_local_user (self , user_id : str ) -> bool :
906+ """Checks whether a given user ID belongs to this homeserver, or a remote
907+
908+ Args:
909+ user_id: A user ID to check.
910+
911+ Returns:
912+ True if the user belongs to this homeserver, False otherwise.
913+ """
914+ user = UserID .from_string (user_id )
915+
916+ # Extract the localpart and ask the module API for a user ID from the localpart
917+ # The module API will append the local homeserver's server_name
918+ local_user_id = self .module_api .get_qualified_user_id (user .localpart )
919+
920+ # If the user ID we get based on the localpart is the same as the original user ID,
921+ # then they were a local user
922+ return user_id == local_user_id
923+
756924 def _user_is_invited_to_room (
757925 self , user_id : str , state_events : StateMap [EventBase ]
758926 ) -> bool :
0 commit comments