diff --git a/README.md b/README.md index c8011d5..32d0bbc 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ This is a Synapse module that checks incoming invites based on allowlist and blo ## Features -- **allowlist and blocklist**: Allows invites from homeservers in the allowlist, blocks invites from homeservers in the blocklist. +- **Allowlist and Blocklist**: Allows invites from homeservers in the allowlist, blocks invites from homeservers in the blocklist. - **Dynamic Fetching**: The allowlist and blocklist are fetched dynamically from a provided URL, and cached. -- **Fallback on Failure**: If the JSON file cannot be fetched (e.g., network error), the module automatically allows all invites to prevent disruptions. +- **Support for MSC2313 Policy Rooms**: This module now supports fetching blocklists from MSC2313 policy rooms to block invites ## Configuration @@ -18,15 +18,19 @@ modules: config: # URL to fetch the JSON file containing the allowlist and blocklist blocklist_allowlist_url: "https://example.com/invite-checker-lists.json" + # Optionally specify policy rooms for dynamic blocklist fetching via MSC2313 + policy_rooms: + - "!policy-room-1:matrix.org" + - "!policy-room-2:matrix.org" # Whether to use the allowlist to allow certain homeservers (default: true) use_allowlist: true # Whether to use the blocklist to block certain homeservers (default: true) use_blocklist: true blocklist_rooms: - "#test:matrix.org" - - "!dkgsemSiSMrGfxEwCb:ubuntu.com + - "!dkgsemSiSMrGfxEwCb:ubuntu.com" -Example for the contents of the URL with the json data: +Example for the contents of the URL with the JSON data: ```json { @@ -46,4 +50,4 @@ Example for the contents of the URL with the json data: "!abc123:matrix.org" // Direct room ID ] } -``` +``` \ No newline at end of file diff --git a/synapse_invite_checker/invite_checker.py b/synapse_invite_checker/invite_checker.py index 1e2599a..6c7e7c8 100644 --- a/synapse_invite_checker/invite_checker.py +++ b/synapse_invite_checker/invite_checker.py @@ -14,6 +14,7 @@ class InviteCheckerConfig: self.use_blocklist = config.get("use_blocklist", True) self.blocklist_allowlist_url = config.get("blocklist_allowlist_url", None) self.blocklist_rooms = config.get("blocklist_rooms", []) # Blocklist for room names + self.policy_room_ids = config.get("policy_room_ids", []) # List of policy room IDs @staticmethod def parse_config(config): @@ -60,9 +61,54 @@ class InviteChecker: logger.error(f"Error while fetching JSON: {str(e)}") returnValue(None) + @inlineCallbacks + def fetch_policy_room_banlist(self): + """Fetches the ban lists from multiple policy rooms using Synapse API.""" + if not self.config.policy_room_ids: + return set() # No policy rooms configured, return an empty set + + logger.info(f"Fetching ban lists from policy rooms: {self.config.policy_room_ids}") + banned_entities = set() + banned_entities_by_room = set() + for room_id in self.config.policy_room_ids: + logger.info(f"Fetching ban list from policy room: {room_id}") + + try: + # Fetch all state events from the policy room + state_events = yield self.api.get_room_state(room_id) + + if isinstance(state_events, dict): + logger.info(f"Received state events in dict format from room {room_id} with {len(state_events)} entries.") + + # Loop over the dictionary of state events + for key, event in state_events.items(): + event_type = event.get("type", "") + content = event.get("content", {}) + + # Check for ban events of type 'm.policy.rule.user' and 'm.policy.rule.server' + if event_type in ["m.policy.rule.user", "m.policy.rule.server"]: + entity = content.get('entity', '') + if entity: + banned_entities_by_room.add(entity) + #else: + # logger.warning(f"Missing 'entity' in event: {event}") + + logger.info(f"Fetched {len(banned_entities_by_room)} banned entities from policy room {room_id}.") + banned_entities = banned_entities_by_room.union(banned_entities) + banned_entities_by_room = set() + + else: + logger.error(f"Unexpected response format from room {room_id}: {type(state_events)}") + + except Exception as e: + logger.error(f"Failed to fetch policy room ban list from room {room_id}. Error: {str(e)}") + + logger.info(f"Total banned entities from all policy rooms: {len(banned_entities)}") + return banned_entities + @inlineCallbacks def update_blocklist_allowlist(self): - """Fetch and update the blocklist, allowlist, and blocklisted room IDs from the URLs.""" + """Fetch and update the blocklist, allowlist, and blocklisted room IDs.""" logger.info("Updating blocklist, allowlist, and room blocklist") json_data = yield self.fetch_json(self.config.blocklist_allowlist_url) @@ -73,25 +119,43 @@ class InviteChecker: self.use_blocklist = json_data.get('use_blocklist', True) self.blocklist = set(json_data.get('blocklist', [])) self.allowlist = set(json_data.get('allowlist', [])) - self.blocklist_room_ids_json = set(json_data.get('blocklist_rooms', [])) - self.blocklist_room_ids = set() - for room_entry in self.blocklist_room_ids_json: + + # Fetch and merge the policy room ban lists from multiple rooms + policy_banlist = yield self.fetch_policy_room_banlist() + self.blocklist.update(policy_banlist) + + self.blocklist_room_ids = set() + for room_entry in json_data.get('blocklist_rooms', []): if room_entry.startswith('!'): - # If it's a room ID, add it directly to the blocklist logger.info(f"Blocklisting room ID directly: {room_entry}") self.blocklist_room_ids.add(room_entry) else: - try: - # If it's a room alias, resolve it to a room ID - room_id = yield self.resolve_room_id(room_entry) - if room_id: - logger.info(f"Blocklisting room: {room_entry} -> {room_id}") - self.blocklist_room_ids.add(room_id) - else: - logger.error(f"Failed to blocklist room: {room_entry}") - except Exception as e: - logger.error(f"Failed to blocklist room: {room_entry}. Error: {str(e)}") + room_id = yield self.resolve_room_id(room_entry) + if room_id: + logger.info(f"Blocklisting room: {room_entry} -> {room_id}") + self.blocklist_room_ids.add(room_id) + else: + logger.error(f"Failed to blocklist room: {room_entry}") + logger.info(f"Updated blocklist with {len(self.blocklist)} entries and {len(self.blocklist_room_ids)} room IDs.") + else: + logger.error("Failed to update allowlist/blocklist due to missing JSON data.") + self.allow_all_invites_on_error = True + + @inlineCallbacks + def resolve_room_id(self, room_alias): + """Resolve a room alias to a room_id and cache the result.""" + if room_alias in self.room_id_cache: + returnValue(self.room_id_cache[room_alias]) + + logger.info(f"Resolving room alias to room_id: {room_alias}") + try: + room_id = yield self.api.resolve_room_alias(room_alias) + self.room_id_cache[room_alias] = room_id + returnValue(room_id) + except Exception as e: + logger.error(f"Failed to resolve room alias {room_alias}: {e}") + returnValue(None) @inlineCallbacks def get_blocklist_allowlist(self): @@ -117,7 +181,7 @@ class InviteChecker: blocklist, allowlist, blocklist_room_ids = yield self.get_blocklist_allowlist() inviter_domain = UserID.from_string(inviter).domain - logger.info(f"Blocklist: {blocklist}, Allowlist: {allowlist}, Blocklist Room IDs: {blocklist_room_ids}") + logger.debug(f"Blocklist: {blocklist}, Allowlist: {allowlist}, Blocklist Room IDs: {blocklist_room_ids}") if room_id in blocklist_room_ids: logger.info(f"Invite blocked: room {room_id} is blocklisted")