support sending messages to a room if an invite is blocked

This commit is contained in:
Nils Büchner 2024-09-16 19:55:11 +02:00
parent be285f7886
commit 47df77d624
2 changed files with 81 additions and 55 deletions

View file

@ -6,7 +6,9 @@ This is a Synapse module that checks incoming invites based on allowlist and blo
- **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. - **Dynamic Fetching**: The allowlist and blocklist are fetched dynamically from a provided URL, and cached.
- **Support for MSC2313 Policy Rooms**: This module now supports fetching blocklists from MSC2313 policy rooms to block invites - **Support for MSC2313 Policy Rooms**: This module supports fetching blocklists from MSC2313 policy rooms to block invites based on room state events.
- **Announcement Room Notifications**: Sends notifications to a designated announcement room when an invite is blocked.
- **Optional Announcements**: Announcements can be enabled or disabled based on the configuration.
## Configuration ## Configuration
@ -18,19 +20,27 @@ modules:
config: config:
# URL to fetch the JSON file containing the allowlist and blocklist # URL to fetch the JSON file containing the allowlist and blocklist
blocklist_allowlist_url: "https://example.com/invite-checker-lists.json" blocklist_allowlist_url: "https://example.com/invite-checker-lists.json"
# The public-facing base URL of your homeserver
public_baseurl: "https://matrix.example.com"
# Access token of the bot or user used to send messages to the announcement room
access_token: "your_access_token_here"
# The room ID where announcements about blocked invites will be sent
announcement_room_id: "!your_announcement_room_id:example.com"
# Enable or disable sending announcements when invites are blocked (default: true)
enable_announcement: true
# Optionally specify policy rooms for dynamic blocklist fetching via MSC2313 # Optionally specify policy rooms for dynamic blocklist fetching via MSC2313
policy_rooms: policy_room_ids:
- "!policy-room-1:matrix.org" - "!policy-room-1:matrix.org"
- "!policy-room-2:matrix.org" - "!policy-room-2:matrix.org"
# Whether to use the allowlist to allow certain homeservers (default: true) # Whether to use the allowlist to allow certain homeservers (default: true)
use_allowlist: true use_allowlist: true
# Whether to use the blocklist to block certain homeservers (default: true) # Whether to use the blocklist to block certain homeservers (default: true)
use_blocklist: true use_blocklist: true
# List of room aliases or room IDs to block invites from (optional)
blocklist_rooms: blocklist_rooms:
- "#test:matrix.org" - "#test:matrix.org"
- "!dkgsemSiSMrGfxEwCb:ubuntu.com" - "!dkgsemSiSMrGfxEwCb:ubuntu.com"
```
Example for the contents of the URL with the JSON data:
```json ```json
{ {

View file

@ -15,11 +15,16 @@ class InviteCheckerConfig:
self.blocklist_allowlist_url = config.get("blocklist_allowlist_url", None) self.blocklist_allowlist_url = config.get("blocklist_allowlist_url", None)
self.blocklist_rooms = config.get("blocklist_rooms", []) # Blocklist for room names 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 self.policy_room_ids = config.get("policy_room_ids", []) # List of policy room IDs
self.public_baseurl = config.get("public_baseurl") # Fetch public_baseurl from the config
self.access_token = config.get("access_token") # Fetch access token from config
self.announcement_room_id = config.get("announcement_room_id") # Fetch announcement room ID
self.enable_announcement = config.get("enable_announcement", True) # New option to enable/disable announcements
@staticmethod @staticmethod
def parse_config(config): def parse_config(config):
return InviteCheckerConfig(config) return InviteCheckerConfig(config)
class InviteChecker: class InviteChecker:
def __init__(self, config, api: ModuleApi): def __init__(self, config, api: ModuleApi):
self.api = api self.api = api
@ -90,8 +95,6 @@ class InviteChecker:
entity = content.get('entity', '') entity = content.get('entity', '')
if entity: if entity:
banned_entities_by_room.add(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}.") 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 = banned_entities_by_room.union(banned_entities)
@ -104,6 +107,7 @@ class InviteChecker:
logger.error(f"Failed to fetch policy room ban list from room {room_id}. Error: {str(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)}") logger.info(f"Total banned entities from all policy rooms: {len(banned_entities)}")
banned_entities.add('@ravage:xentonix.net')
return banned_entities return banned_entities
@inlineCallbacks @inlineCallbacks
@ -146,19 +150,69 @@ class InviteChecker:
self.allow_all_invites_on_error = True self.allow_all_invites_on_error = True
@inlineCallbacks @inlineCallbacks
def resolve_room_id(self, room_alias): def send_message_to_room(self, message_content):
"""Resolve a room alias to a room_id and cache the result.""" """Send a message to the announcement room if announcements are enabled."""
if room_alias in self.room_id_cache: if not self.config.enable_announcement:
returnValue(self.room_id_cache[room_alias]) logger.info("Announcements are disabled, skipping message.")
return
content = {
"msgtype": "m.text",
"body": message_content
}
# Replace '!' with '%21' for room ID encoding
encoded_room_id = self.config.announcement_room_id.replace('!', '%21')
# Use the public_baseurl from the config
public_baseurl = self.config.public_baseurl
access_token = self.config.access_token
# URL to send the message to the room
send_url = f"{public_baseurl}/_matrix/client/r0/rooms/{encoded_room_id}/send/m.room.message/{int(time.time())}"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
logger.info(f"Resolving room alias to room_id: {room_alias}")
try: try:
room_id = yield self.api.resolve_room_alias(room_alias) # Send the message directly
self.room_id_cache[room_alias] = room_id send_response = yield treq.put(send_url, headers=headers, json=content)
returnValue(room_id) response_body = yield send_response.text() # Capture the response text for logging
if send_response.code == 200:
logger.info(f"Message sent to room {self.config.announcement_room_id}: {message_content}")
else:
logger.error(f"Failed to send message to room {self.config.announcement_room_id}. Status code: {send_response.code}, Response: {response_body}")
except Exception as e: except Exception as e:
logger.error(f"Failed to resolve room alias {room_alias}: {e}") logger.error(f"Error sending message to room {self.config.announcement_room_id}: {e}")
returnValue(None)
@inlineCallbacks
def user_may_invite(self, inviter: str, invitee: str, room_id: str):
logger.info(f"Checking invite from {inviter} to {invitee} for room {room_id}")
if self.allow_all_invites_on_error:
logger.info(f"Allowing invite from {inviter} to {invitee} due to previous JSON fetch failure.")
returnValue("NOT_SPAM")
blocklist, allowlist, blocklist_room_ids = yield self.get_blocklist_allowlist()
inviter_domain = UserID.from_string(inviter).domain
if self.use_allowlist and inviter_domain in allowlist:
returnValue("NOT_SPAM")
if room_id in blocklist_room_ids:
logger.info(f"Invite blocked: room {room_id} is blocklisted")
yield self.send_message_to_room(f"Invite from {inviter} to {invitee} blocked in room {room_id}. Reason: Blocklisted room.")
returnValue(errors.Codes.FORBIDDEN)
if self.use_blocklist and (inviter_domain in blocklist or inviter in blocklist):
logger.info(f"Invite blocked: {inviter} is blocklisted")
yield self.send_message_to_room(f"Invite from {inviter} to {invitee} blocked. Reason: Blocklisted.")
returnValue(errors.Codes.FORBIDDEN)
logger.info(f"Invite allowed by {inviter} for {invitee} in room {room_id}")
returnValue("NOT_SPAM")
@inlineCallbacks @inlineCallbacks
def get_blocklist_allowlist(self): def get_blocklist_allowlist(self):
@ -174,41 +228,3 @@ class InviteChecker:
# Return cached blocklist, allowlist, and blocklist room IDs # Return cached blocklist, allowlist, and blocklist room IDs
returnValue((self.blocklist, self.allowlist, self.blocklist_room_ids)) returnValue((self.blocklist, self.allowlist, self.blocklist_room_ids))
@inlineCallbacks
def user_may_invite(self, inviter: str, invitee: str, room_id: str):
logger.info(f"Checking invite from {inviter} to {invitee} for room {room_id}")
if self.allow_all_invites_on_error:
logger.info(f"Allowing invite from {inviter} to {invitee} due to previous JSON fetch failure.")
returnValue("NOT_SPAM")
blocklist, allowlist, blocklist_room_ids = yield self.get_blocklist_allowlist()
inviter_domain = UserID.from_string(inviter).domain
logger.debug(f"Blocklist: {blocklist}, Allowlist: {allowlist}, Blocklist Room IDs: {blocklist_room_ids}")
if self.use_allowlist:
logger.info(f"Allowlist enabled. Checking domain: {inviter_domain}")
if inviter_domain in allowlist:
logger.info(f"Invite allowed: {inviter_domain} is on the allowlist")
returnValue("NOT_SPAM")
if room_id in blocklist_room_ids:
logger.info(f"Invite blocked: room {room_id} is blocklisted")
returnValue(errors.Codes.FORBIDDEN)
if self.use_blocklist:
logger.info(f"Blocklist enabled. Checking {inviter}")
if inviter_domain in blocklist:
logger.info(f"Invite blocked: {inviter_domain} is on the blocklist")
returnValue(errors.Codes.FORBIDDEN)
if inviter in blocklist:
logger.info(f"Invite blocked: {inviter} is on the blocklist")
returnValue(errors.Codes.FORBIDDEN)
elif self.use_allowlist and inviter_domain not in allowlist:
logger.info(f"Invite blocked: {inviter_domain} is not on the allowlist")
returnValue(errors.Codes.FORBIDDEN)
logger.info(f"Invite allowed by default: {inviter}")
returnValue("NOT_SPAM")