From 2f55ca2c4cf0a37c96bdeec83548a0ba1c3a15b6 Mon Sep 17 00:00:00 2001 From: svierne Date: Sat, 23 Dec 2023 23:07:40 +0100 Subject: [PATCH 1/7] Add allowlist and blocklist sections to sample config. --- sample.config.yaml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/sample.config.yaml b/sample.config.yaml index 7062f54..6e9438f 100644 --- a/sample.config.yaml +++ b/sample.config.yaml @@ -36,6 +36,33 @@ reminders: # If not set, UTC will be used #timezone: "Europe/London" +# Restrict the bot to only respond to certain MXIDs +allowlist: + # Set to true to enable the allowlist + enabled: false + # A list of MXID regexes to be allowed + # To allow a certain homeserver: + # regexes: ["@[a-z0-9-_.]+:myhomeserver.tld"] + # To allow a set of users: + # regexes: ["@alice:someserver.tld", "@bob:anotherserver.tld"] + # To allow nobody (same as blocking every MXID): + # regexes: [] + regexes: [] + +# Prevent the bot from responding to certain MXIDs +# If both allowlist and blocklist are enabled, blocklist entries takes precedence +blocklist: + # Set to true to enable the blocklist + enabled: false + # A list of MXID regexes to be blocked + # To block a certain homeserver: + # regexes: [".*:myhomeserver.tld"] + # To block a set of users: + # regexes: ["@alice:someserver.tld", "@bob:anotherserver.tld"] + # To block absolutely everyone (same as allowing nobody): + # regexes: [".*"] + regexes: [] + # Logging setup logging: # Logging level From 2211443b903895df8dbb018fab779bd1f6ce3411 Mon Sep 17 00:00:00 2001 From: svierne Date: Sun, 24 Dec 2023 00:01:54 +0100 Subject: [PATCH 2/7] Expose allowlist and blocklist sections in CONFIG. --- matrix_reminder_bot/config.py | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/matrix_reminder_bot/config.py b/matrix_reminder_bot/config.py index 220d980..051090c 100644 --- a/matrix_reminder_bot/config.py +++ b/matrix_reminder_bot/config.py @@ -42,6 +42,12 @@ def __init__(self): self.timezone: str = "" + self.allowlist_enabled: bool = False + self.allowlist_regexes: list[str] = [] + + self.blocklist_enabled: bool = False + self.blocklist_regexes: list[str] = [] + def read_config(self, filepath: str): if not os.path.isfile(filepath): raise ConfigError(f"Config file '{filepath}' does not exist") @@ -122,6 +128,48 @@ def read_config(self, filepath: str): # Reminder configuration self.timezone = self._get_cfg(["reminders", "timezone"], default="Etc/UTC") + # Allowlist configuration + allowlist_enabled = self._get_cfg(["allowlist", "enabled"], required=True) + if not isinstance(allowlist_enabled, bool): + raise ConfigError("allowlist.enabled must be a boolean value") + self.allowlist_enabled = allowlist_enabled + + allowlist_regexes = self._get_cfg(["allowlist", "regexes"], required=True) + if not isinstance(allowlist_regexes, list) or ( + isinstance(allowlist_regexes, list) + and any(not isinstance(x, str) for x in allowlist_regexes) + ): + raise ConfigError("allowlist.regexes must be a list of strings") + for regex in allowlist_regexes: + try: + re.compile(regex) + except re.error: + raise ConfigError( + f"'{regex}' contained in allowlist.regexes is not a valid regular expression" + ) + self.allowlist_regexes = allowlist_regexes + + # Blocklist configuration + blocklist_enabled = self._get_cfg(["blocklist", "enabled"], required=True) + if not isinstance(blocklist_enabled, bool): + raise ConfigError("blocklist.enabled must be a boolean value") + self.blocklist_enabled = blocklist_enabled + + blocklist_regexes = self._get_cfg(["blocklist", "regexes"], required=True) + if not isinstance(blocklist_regexes, list) or ( + isinstance(blocklist_regexes, list) + and any(not isinstance(x, str) for x in blocklist_regexes) + ): + raise ConfigError("blocklist.regexes must be a list of strings") + for regex in blocklist_regexes: + try: + re.compile(regex) + except re.error: + raise ConfigError( + f"'{regex}' contained in blocklist.regexes is not a valid regular expression" + ) + self.blocklist_regexes = blocklist_regexes + def _get_cfg( self, path: List[str], From 6e1a78ef16221fda7ad41fb707b5f72b35097c74 Mon Sep 17 00:00:00 2001 From: svierne Date: Sun, 24 Dec 2023 00:21:47 +0100 Subject: [PATCH 3/7] Directly compile regexes when reading the config. --- matrix_reminder_bot/config.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/matrix_reminder_bot/config.py b/matrix_reminder_bot/config.py index 051090c..f1d2273 100644 --- a/matrix_reminder_bot/config.py +++ b/matrix_reminder_bot/config.py @@ -43,10 +43,10 @@ def __init__(self): self.timezone: str = "" self.allowlist_enabled: bool = False - self.allowlist_regexes: list[str] = [] + self.allowlist_regexes: list[re.Pattern] = [] self.blocklist_enabled: bool = False - self.blocklist_regexes: list[str] = [] + self.blocklist_regexes: list[re.Pattern] = [] def read_config(self, filepath: str): if not os.path.isfile(filepath): @@ -134,15 +134,17 @@ def read_config(self, filepath: str): raise ConfigError("allowlist.enabled must be a boolean value") self.allowlist_enabled = allowlist_enabled - allowlist_regexes = self._get_cfg(["allowlist", "regexes"], required=True) - if not isinstance(allowlist_regexes, list) or ( - isinstance(allowlist_regexes, list) - and any(not isinstance(x, str) for x in allowlist_regexes) + allowlist_strings = self._get_cfg(["allowlist", "regexes"], required=True) + if not isinstance(allowlist_strings, list) or ( + isinstance(allowlist_strings, list) + and any(not isinstance(x, str) for x in allowlist_strings) ): raise ConfigError("allowlist.regexes must be a list of strings") - for regex in allowlist_regexes: + + allowlist_regexes = [] + for regex in allowlist_strings: try: - re.compile(regex) + allowlist_regexes.append(re.compile(regex)) except re.error: raise ConfigError( f"'{regex}' contained in allowlist.regexes is not a valid regular expression" @@ -155,15 +157,17 @@ def read_config(self, filepath: str): raise ConfigError("blocklist.enabled must be a boolean value") self.blocklist_enabled = blocklist_enabled - blocklist_regexes = self._get_cfg(["blocklist", "regexes"], required=True) - if not isinstance(blocklist_regexes, list) or ( - isinstance(blocklist_regexes, list) - and any(not isinstance(x, str) for x in blocklist_regexes) + blocklist_strings = self._get_cfg(["blocklist", "regexes"], required=True) + if not isinstance(blocklist_strings, list) or ( + isinstance(blocklist_strings, list) + and any(not isinstance(x, str) for x in blocklist_strings) ): raise ConfigError("blocklist.regexes must be a list of strings") - for regex in blocklist_regexes: + + blocklist_regexes = [] + for regex in blocklist_strings: try: - re.compile(regex) + blocklist_regexes.append(re.compile(regex)) except re.error: raise ConfigError( f"'{regex}' contained in blocklist.regexes is not a valid regular expression" From 7cbff9cc2ade754308b5a024b000c484ee26ad9b Mon Sep 17 00:00:00 2001 From: svierne Date: Sun, 24 Dec 2023 00:46:23 +0100 Subject: [PATCH 4/7] Implement allowlist/blocklist checking function. --- matrix_reminder_bot/functions.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/matrix_reminder_bot/functions.py b/matrix_reminder_bot/functions.py index f2dbbe7..db6affc 100644 --- a/matrix_reminder_bot/functions.py +++ b/matrix_reminder_bot/functions.py @@ -115,3 +115,29 @@ def make_pill(user_id: str, displayname: str = None) -> str: displayname = user_id return f'{displayname}' + + +def is_allowed_user(user_id: str) -> bool: + """Returns if the bot is allowed to interact with the given user + + Args: + user_id: The MXID of the user. + + Returns: + True, if the bot is allowed to interact with the given user. + """ + allowed = not CONFIG.allowlist_enabled + + if CONFIG.allowlist_enabled: + for regex in CONFIG.allowlist_regexes: + if regex.fullmatch(user_id): + allowed = True + break + + if CONFIG.blocklist_enabled: + for regex in CONFIG.blocklist_regexes: + if regex.fullmatch(user_id): + allowed = False + break + + return allowed From 7657e25580f1d1b4ef73a79e4db73b02aa029f47 Mon Sep 17 00:00:00 2001 From: svierne Date: Sun, 24 Dec 2023 00:48:40 +0100 Subject: [PATCH 5/7] Check if the bot is allowed to interact with the sender before responding to events. --- matrix_reminder_bot/callbacks.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/matrix_reminder_bot/callbacks.py b/matrix_reminder_bot/callbacks.py index ba1f7fe..ab444d5 100644 --- a/matrix_reminder_bot/callbacks.py +++ b/matrix_reminder_bot/callbacks.py @@ -14,7 +14,7 @@ from matrix_reminder_bot.bot_commands import Command from matrix_reminder_bot.config import CONFIG from matrix_reminder_bot.errors import CommandError -from matrix_reminder_bot.functions import send_text_to_room +from matrix_reminder_bot.functions import is_allowed_user, send_text_to_room from matrix_reminder_bot.storage import Storage logger = logging.getLogger(__name__) @@ -71,6 +71,13 @@ async def message(self, room: MatrixRoom, event: RoomMessageText): if event.sender == self.client.user: return + # Ignore messages from disallowed users + if not is_allowed_user(event.sender): + logger.debug( + f"Ignoring event {event.event_id} in room {room.room_id} as the sender {event.sender} is not allowed." + ) + return + # Ignore broken events if not event.body: return @@ -123,6 +130,11 @@ async def invite(self, room: MatrixRoom, event: InviteMemberEvent): """Callback for when an invite is received. Join the room specified in the invite""" logger.debug(f"Got invite to {room.room_id} from {event.sender}.") + # Don't respond to invites from disallowed users + if not is_allowed_user(event.sender): + logger.info(f"{event.sender} is not allowed, not responding to invite.") + return + # Attempt to join 3 times before giving up for attempt in range(3): result = await self.client.join(room.room_id) From fb87d88d05f41cda0650b43b2b78dca194bc3282 Mon Sep 17 00:00:00 2001 From: svierne Date: Thu, 28 Dec 2023 21:45:47 +0100 Subject: [PATCH 6/7] Move regex compilation to private function. --- matrix_reminder_bot/config.py | 67 +++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/matrix_reminder_bot/config.py b/matrix_reminder_bot/config.py index f1d2273..b4d1aa1 100644 --- a/matrix_reminder_bot/config.py +++ b/matrix_reminder_bot/config.py @@ -134,22 +134,9 @@ def read_config(self, filepath: str): raise ConfigError("allowlist.enabled must be a boolean value") self.allowlist_enabled = allowlist_enabled - allowlist_strings = self._get_cfg(["allowlist", "regexes"], required=True) - if not isinstance(allowlist_strings, list) or ( - isinstance(allowlist_strings, list) - and any(not isinstance(x, str) for x in allowlist_strings) - ): - raise ConfigError("allowlist.regexes must be a list of strings") - - allowlist_regexes = [] - for regex in allowlist_strings: - try: - allowlist_regexes.append(re.compile(regex)) - except re.error: - raise ConfigError( - f"'{regex}' contained in allowlist.regexes is not a valid regular expression" - ) - self.allowlist_regexes = allowlist_regexes + self.allowlist_regexes = self._compile_regexes( + ["allowlist", "regexes"], required=True + ) # Blocklist configuration blocklist_enabled = self._get_cfg(["blocklist", "enabled"], required=True) @@ -157,22 +144,48 @@ def read_config(self, filepath: str): raise ConfigError("blocklist.enabled must be a boolean value") self.blocklist_enabled = blocklist_enabled - blocklist_strings = self._get_cfg(["blocklist", "regexes"], required=True) - if not isinstance(blocklist_strings, list) or ( - isinstance(blocklist_strings, list) - and any(not isinstance(x, str) for x in blocklist_strings) + self.blocklist_regexes = self._compile_regexes( + ["blocklist", "regexes"], required=True + ) + + def _compile_regexes( + self, path: list[str], required: bool = True + ) -> list[re.Pattern]: + """Compile a config option containing a list of strings into re.Pattern objects. + + Args: + path: The path to the config option. + required: True, if the config option is mandatory. + + Returns: + A list of re.Pattern objects. + + Raises: + ConfigError: + - If required is specified, but the config option does not exist. + - If the config option is not a list of strings. + - If the config option contains an invalid regular expression. + """ + + readable_path = ".".join(path) + regex_strings = self._get_cfg(path, required=required) # raises ConfigError + + if not isinstance(regex_strings, list) or ( + isinstance(regex_strings, list) + and any(not isinstance(x, str) for x in regex_strings) ): - raise ConfigError("blocklist.regexes must be a list of strings") + raise ConfigError(f"{readable_path} must be a list of strings") - blocklist_regexes = [] - for regex in blocklist_strings: + compiled_regexes = [] + for regex in regex_strings: try: - blocklist_regexes.append(re.compile(regex)) - except re.error: + compiled_regexes.append(re.compile(regex)) + except re.error as e: raise ConfigError( - f"'{regex}' contained in blocklist.regexes is not a valid regular expression" + f"'{e.pattern}' contained in {readable_path} is not a valid regular expression" ) - self.blocklist_regexes = blocklist_regexes + + return compiled_regexes def _get_cfg( self, From 1c0537d0ea1c3349a10da95763239a5397fc1dd7 Mon Sep 17 00:00:00 2001 From: svierne Date: Thu, 28 Dec 2023 21:47:32 +0100 Subject: [PATCH 7/7] Change loglevel to DEBUG for rejected invites. --- matrix_reminder_bot/callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix_reminder_bot/callbacks.py b/matrix_reminder_bot/callbacks.py index ab444d5..64e7dde 100644 --- a/matrix_reminder_bot/callbacks.py +++ b/matrix_reminder_bot/callbacks.py @@ -132,7 +132,7 @@ async def invite(self, room: MatrixRoom, event: InviteMemberEvent): # Don't respond to invites from disallowed users if not is_allowed_user(event.sender): - logger.info(f"{event.sender} is not allowed, not responding to invite.") + logger.debug(f"{event.sender} is not allowed, not responding to invite.") return # Attempt to join 3 times before giving up