From 07c8a7768669cafcf79b1e58fe05df702f767959 Mon Sep 17 00:00:00 2001 From: William Kray Date: Sun, 29 Jun 2025 11:59:01 -0700 Subject: [PATCH] first pass at slug logic to prevent community collision --- base-config.yaml | 5 ++ community/bot.py | 139 ++++++++++++++++++++++++++++++++- example-standalone-config.yaml | 5 ++ 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/base-config.yaml b/base-config.yaml index 3bae0ae..596efa9 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -5,6 +5,11 @@ # based on opinionated defaults. parent_room: '' +# community slug +# this will be used to suffix room aliases in order to avoid collisions with other communities +# leave blank to generate an acronym of your community name during initialization +community_slug: '' + # sleep time between actions. you can drop this to 0 if your bot has no # ratelimits imposed on its homeserver, otherwise you may want to increase this # to avoid errors. diff --git a/community/bot.py b/community/bot.py index d4b48b2..40b61b4 100644 --- a/community/bot.py +++ b/community/bot.py @@ -55,6 +55,7 @@ class Config(BaseProxyConfig): helper.copy("admins") helper.copy("moderators") helper.copy("parent_room") + helper.copy("community_slug") helper.copy("track_users") helper.copy("track_messages") helper.copy("track_reactions") @@ -120,6 +121,73 @@ class CommunityBot(Plugin): self.log.error(f"Failed to check user power level: {e}") return False + def generate_community_slug(self, community_name: str) -> str: + """Generate a community slug from the community name. + + Args: + community_name: The full community name + + Returns: + str: A slug made from the first letter of each word, lowercase + """ + # Split by whitespace and get first letter of each word + words = community_name.strip().split() + slug = ''.join(word[0].lower() for word in words if word) + return slug + + async def validate_room_alias(self, alias_localpart: str, server: str) -> bool: + """Check if a room alias already exists. + + Args: + alias_localpart: The localpart of the alias (without # and :server) + server: The server domain + + Returns: + bool: True if alias is available, False if it already exists + """ + try: + full_alias = f"#{alias_localpart}:{server}" + await self.client.resolve_room_alias(full_alias) + # If we get here, the alias exists + return False + except MNotFound: + # Alias doesn't exist, so it's available + return True + except Exception as e: + # For other errors, assume alias is available to be safe + self.log.warning(f"Error checking alias {full_alias}: {e}") + return True + + async def validate_room_aliases(self, room_names: list[str], evt: MessageEvent = None) -> tuple[bool, list[str]]: + """Validate that all room aliases are available. + + Args: + room_names: List of room names to validate + evt: Optional MessageEvent for progress updates + + Returns: + tuple: (is_valid, list_of_conflicting_aliases) + """ + if not self.config["community_slug"]: + if evt: + await evt.respond("Error: No community slug configured. Please run initialize command first.") + return False, [] + + server = self.client.parse_user_id(self.client.mxid)[1] + conflicting_aliases = [] + + for room_name in room_names: + # Clean the room name and create alias + sanitized_name = re.sub(r"[^a-zA-Z0-9]", "", room_name).lower() + alias_localpart = f"{sanitized_name}-{self.config['community_slug']}" + + # Check if alias is available + is_available = await self.validate_room_alias(alias_localpart, server) + if not is_available: + conflicting_aliases.append(f"#{alias_localpart}:{server}") + + return len(conflicting_aliases) == 0, conflicting_aliases + async def _redaction_loop(self) -> None: while True: try: @@ -1607,6 +1675,26 @@ class CommunityBot(Plugin): parent_room = self.config["parent_room"] server = self.client.parse_user_id(self.client.mxid)[1] + # Check if community slug is configured + if not self.config["community_slug"]: + error_msg = "No community slug configured. Please run initialize command first." + self.log.error(error_msg) + if evt: + await evt.respond(error_msg) + return None + + # Create alias with community slug + alias_localpart = f"{sanitized_name}-{self.config['community_slug']}" + + # Validate that the alias is available + is_available = await self.validate_room_alias(alias_localpart, server) + if not is_available: + error_msg = f"Room alias #{alias_localpart}:{server} already exists. Cannot create room." + self.log.error(error_msg) + if evt: + await evt.respond(error_msg) + return None + # Get power levels from parent room if not provided if not power_level_override and parent_room: power_levels = await self.client.get_state_event( @@ -1629,7 +1717,7 @@ class CommunityBot(Plugin): if evt: mymsg = await evt.respond( - f"creating {sanitized_name}, give me a minute..." + f"creating {alias_localpart}, give me a minute..." ) # Prepare initial state events @@ -1669,7 +1757,7 @@ class CommunityBot(Plugin): # Create the room with all initial states room_id = await self.client.create_room( - alias_localpart=sanitized_name, + alias_localpart=alias_localpart, name=roomname, invitees=invitees, initial_state=initial_state, @@ -1692,12 +1780,12 @@ class CommunityBot(Plugin): if evt: await evt.respond( - f"#{sanitized_name}:{server} has been created and added to the space.", + f"#{alias_localpart}:{server} has been created and added to the space.", edits=mymsg, allow_html=True ) - return room_id, f"#{sanitized_name}:{server}" + return room_id, f"#{alias_localpart}:{server}" except Exception as e: error_msg = f"Failed to create room: {e}" @@ -1727,6 +1815,17 @@ class CommunityBot(Plugin): await evt.reply("You don't have permission to use this command") return + # Check if community slug is configured + if not self.config["community_slug"]: + await evt.reply("No community slug configured. Please run initialize command first.") + return + + # Validate the room alias before creating + is_valid, conflicting_aliases = await self.validate_room_aliases([roomname], evt) + if not is_valid: + await evt.reply(f"Cannot create room: {conflicting_aliases[0]} already exists.") + return + result = await self.create_room(roomname, evt) if not result: return # Error already logged and reported to user by create_room @@ -1821,6 +1920,17 @@ class CommunityBot(Plugin): # Get list of aliases to transfer while removing them from the old room aliases_to_transfer = await self.remove_room_aliases(room_id, evt) + # Check if community slug is configured + if not self.config["community_slug"]: + await evt.respond("No community slug configured. Please run initialize command first.") + return + + # Validate that the new room alias is available + is_valid, conflicting_aliases = await self.validate_room_aliases([room_name], evt) + if not is_valid: + await evt.respond(f"Cannot replace room: {conflicting_aliases[0]} already exists.") + return + # Now we can start the process of replacing the room # First we need to create the new room. this will create the initial alias, # as well as bot defaults such as power levels, initial invitations, encryption, @@ -2277,6 +2387,26 @@ class CommunityBot(Plugin): msg = await evt.respond("Initializing new community space...") try: + # Generate community slug if not already set + if not self.config["community_slug"]: + community_slug = self.generate_community_slug(community_name) + self.config["community_slug"] = community_slug + self.log.info(f"Generated community slug: {community_slug}") + + # Define all rooms that will be created during initialization + rooms_to_create = [ + community_name, # Main space + f"{community_name} Moderators", # Moderators room + f"{community_name} Waiting Room" # Waiting room + ] + + # Validate all room aliases before creating any rooms + is_valid, conflicting_aliases = await self.validate_room_aliases(rooms_to_create, evt) + if not is_valid: + error_msg = f"Cannot initialize community: The following room aliases already exist:\n" + "\n".join(conflicting_aliases) + await evt.respond(error_msg, edits=msg) + return + # Add initiator to invitees list if not already there if evt.sender not in self.config["invitees"]: self.config["invitees"].append(evt.sender) @@ -2363,6 +2493,7 @@ class CommunityBot(Plugin): await evt.respond( f"Community space initialized successfully!\n\n" + f"Community Slug: {self.config['community_slug']}\n" f"Space: {space_alias}\n" f"Moderators Room: {mod_room_alias}\n" f"Waiting Room: {waiting_room_alias}", diff --git a/example-standalone-config.yaml b/example-standalone-config.yaml index 2665346..c1d6195 100644 --- a/example-standalone-config.yaml +++ b/example-standalone-config.yaml @@ -62,6 +62,11 @@ plugin_config: # leave this empty to use the initialize command to create a new community to manage, # based on opinionated defaults. parent_room: '' + # community slug + # this will be used to suffix room aliases in order to avoid collisions with other communities + # leave blank to generate an acronym of your community name during initialization + community_slug: '' + # sleep time between actions. you can drop this to 0 if your bot has no # ratelimits imposed on its homeserver, otherwise you may want to increase this