synapse-invite-checker/synapse_invite_checker/invite_checker.py

208 lines
9 KiB
Python
Raw Normal View History

2024-09-14 17:12:42 +00:00
import treq
2024-09-14 18:17:43 +00:00
import json
2024-09-14 18:37:38 +00:00
import time
2024-09-14 17:12:42 +00:00
from twisted.internet.defer import inlineCallbacks, returnValue
from synapse.module_api import ModuleApi, errors
from synapse.types import UserID
import logging
logger = logging.getLogger(__name__)
class InviteCheckerConfig:
def __init__(self, config):
2024-09-14 18:17:43 +00:00
self.use_allowlist = config.get("use_allowlist", True)
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
2024-09-14 21:16:32 +00:00
self.policy_room_ids = config.get("policy_room_ids", []) # List of policy room IDs
2024-09-14 17:12:42 +00:00
@staticmethod
def parse_config(config):
return InviteCheckerConfig(config)
class InviteChecker:
def __init__(self, config, api: ModuleApi):
self.api = api
self.config = InviteCheckerConfig.parse_config(config)
2024-09-14 18:17:43 +00:00
self.use_allowlist = self.config.use_allowlist
self.use_blocklist = self.config.use_blocklist
2024-09-14 17:12:42 +00:00
self.cache_expiry_time = 60
self.cache_timestamp = 0
2024-09-14 18:17:43 +00:00
self.blocklist = set()
self.allowlist = set()
self.allow_all_invites_on_error = False
self.room_id_cache = {}
self.blocklist_room_ids = set()
2024-09-14 17:12:42 +00:00
self.api.register_spam_checker_callbacks(user_may_invite=self.user_may_invite)
logger.info("InviteChecker initialized")
@inlineCallbacks
def fetch_json(self, url):
2024-09-14 18:17:43 +00:00
logger.info(f"Fetching JSON data from: {url}")
2024-09-14 17:12:42 +00:00
try:
response = yield treq.get(url)
if response.code == 200:
2024-09-14 18:17:43 +00:00
try:
content = yield response.content()
data = json.loads(content.decode('utf-8'))
logger.debug(f"Received JSON data: {data}")
2024-09-14 18:17:43 +00:00
returnValue(data)
except Exception as json_error:
logger.error(f"Failed to decode JSON data: {json_error}")
returnValue(None)
2024-09-14 17:12:42 +00:00
else:
logger.error(f"Failed to fetch JSON data. Status code: {response.code}")
returnValue(None)
except Exception as e:
logger.error(f"Error while fetching JSON: {str(e)}")
returnValue(None)
2024-09-14 21:16:32 +00:00
@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
2024-09-14 17:12:42 +00:00
@inlineCallbacks
2024-09-14 18:17:43 +00:00
def update_blocklist_allowlist(self):
2024-09-14 21:16:32 +00:00
"""Fetch and update the blocklist, allowlist, and blocklisted room IDs."""
logger.info("Updating blocklist, allowlist, and room blocklist")
2024-09-14 17:12:42 +00:00
2024-09-14 18:17:43 +00:00
json_data = yield self.fetch_json(self.config.blocklist_allowlist_url)
2024-09-14 17:12:42 +00:00
if json_data:
self.allow_all_invites_on_error = False
2024-09-14 18:17:43 +00:00
self.use_allowlist = json_data.get('use_allowlist', True)
self.use_blocklist = json_data.get('use_blocklist', True)
self.blocklist = set(json_data.get('blocklist', []))
self.allowlist = set(json_data.get('allowlist', []))
2024-09-14 21:16:32 +00:00
# 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('!'):
logger.info(f"Blocklisting room ID directly: {room_entry}")
self.blocklist_room_ids.add(room_entry)
else:
2024-09-14 21:16:32 +00:00
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])
2024-09-14 17:12:42 +00:00
2024-09-14 21:16:32 +00:00
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)
2024-09-14 17:12:42 +00:00
@inlineCallbacks
2024-09-14 18:17:43 +00:00
def get_blocklist_allowlist(self):
2024-09-14 18:37:38 +00:00
current_time = time.time()
2024-09-14 18:37:38 +00:00
if current_time - self.cache_timestamp > self.cache_expiry_time:
yield self.update_blocklist_allowlist()
2024-09-14 17:12:42 +00:00
if self.allow_all_invites_on_error:
2024-09-14 18:17:43 +00:00
logger.info("Skipping allowlist/blocklist checks because of previous JSON fetch failure.")
returnValue((set(), set(), set()))
2024-09-14 17:12:42 +00:00
returnValue((self.blocklist, self.allowlist, self.blocklist_room_ids))
2024-09-14 17:12:42 +00:00
@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}")
2024-09-14 17:12:42 +00:00
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")
2024-09-14 17:12:42 +00:00
blocklist, allowlist, blocklist_room_ids = yield self.get_blocklist_allowlist()
2024-09-14 17:12:42 +00:00
inviter_domain = UserID.from_string(inviter).domain
2024-09-14 21:16:32 +00:00
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")
returnValue(errors.Codes.FORBIDDEN)
2024-09-14 18:17:43 +00:00
if self.use_allowlist:
2024-09-14 18:37:38 +00:00
logger.info(f"Allowlist enabled. Checking domain: {inviter_domain}")
2024-09-14 18:17:43 +00:00
if inviter_domain in allowlist:
logger.info(f"Invite allowed: {inviter_domain} is in the allowlist")
returnValue("NOT_SPAM")
2024-09-14 18:17:43 +00:00
if self.use_blocklist:
2024-09-14 18:37:38 +00:00
logger.info(f"Blocklist enabled. Checking domain: {inviter_domain}")
2024-09-14 18:17:43 +00:00
if inviter_domain in blocklist:
logger.info(f"Invite blocked: {inviter_domain} is in the blocklist")
returnValue(errors.Codes.FORBIDDEN)
2024-09-14 18:17:43 +00:00
if self.use_allowlist and inviter_domain not in allowlist:
logger.info(f"Invite blocked: {inviter_domain} is not in the allowlist")
2024-09-14 17:12:42 +00:00
returnValue(errors.Codes.FORBIDDEN)
logger.info(f"Invite allowed by default: {inviter_domain}")
returnValue("NOT_SPAM")