merge slug-suffix and doctor command branches
This commit is contained in:
@@ -5,6 +5,11 @@
|
|||||||
# based on opinionated defaults.
|
# based on opinionated defaults.
|
||||||
parent_room: ''
|
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
|
# 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
|
# ratelimits imposed on its homeserver, otherwise you may want to increase this
|
||||||
# to avoid errors.
|
# to avoid errors.
|
||||||
|
|||||||
+228
-12
@@ -35,7 +35,7 @@ from mautrix.types import (
|
|||||||
SpaceParentStateEventContent,
|
SpaceParentStateEventContent,
|
||||||
JoinRulesStateEventContent,
|
JoinRulesStateEventContent,
|
||||||
JoinRule,
|
JoinRule,
|
||||||
RoomCreatePreset,
|
RoomCreatePreset
|
||||||
)
|
)
|
||||||
from mautrix.errors import MNotFound
|
from mautrix.errors import MNotFound
|
||||||
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
|
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
|
||||||
@@ -55,6 +55,7 @@ class Config(BaseProxyConfig):
|
|||||||
helper.copy("admins")
|
helper.copy("admins")
|
||||||
helper.copy("moderators")
|
helper.copy("moderators")
|
||||||
helper.copy("parent_room")
|
helper.copy("parent_room")
|
||||||
|
helper.copy("community_slug")
|
||||||
helper.copy("track_users")
|
helper.copy("track_users")
|
||||||
helper.copy("track_messages")
|
helper.copy("track_messages")
|
||||||
helper.copy("track_reactions")
|
helper.copy("track_reactions")
|
||||||
@@ -120,6 +121,131 @@ class CommunityBot(Plugin):
|
|||||||
self.log.error(f"Failed to check user power level: {e}")
|
self.log.error(f"Failed to check user power level: {e}")
|
||||||
return False
|
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 create_space(self, space_name: str, evt: MessageEvent = None, power_level_override: Optional[PowerLevelStateEventContent] = None) -> tuple[str, str]:
|
||||||
|
"""Create a new space without community slug suffix.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
space_name: The name for the new space
|
||||||
|
evt: Optional MessageEvent for progress updates
|
||||||
|
power_level_override: Optional power levels to use
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (space_id, space_alias) if successful, None if failed
|
||||||
|
"""
|
||||||
|
mymsg = None
|
||||||
|
try:
|
||||||
|
sanitized_name = re.sub(r"[^a-zA-Z0-9]", "", space_name).lower()
|
||||||
|
invitees = self.config["invitees"]
|
||||||
|
server = self.client.parse_user_id(self.client.mxid)[1]
|
||||||
|
|
||||||
|
# Validate that the space alias is available
|
||||||
|
is_available = await self.validate_room_alias(sanitized_name, server)
|
||||||
|
if not is_available:
|
||||||
|
error_msg = f"Space alias #{sanitized_name}:{server} already exists. Cannot create space."
|
||||||
|
self.log.error(error_msg)
|
||||||
|
if evt:
|
||||||
|
await evt.respond(error_msg)
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if evt:
|
||||||
|
mymsg = await evt.respond(
|
||||||
|
f"creating space {sanitized_name}, give me a minute..."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the space with space-specific content
|
||||||
|
space_id = await self.client.create_room(
|
||||||
|
alias_localpart=sanitized_name,
|
||||||
|
name=space_name,
|
||||||
|
invitees=invitees,
|
||||||
|
power_level_override=power_level_override,
|
||||||
|
creation_content={"type": "m.space"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if evt:
|
||||||
|
await evt.respond(
|
||||||
|
f"<a href='https://matrix.to/#/#{sanitized_name}:{server}'>#{sanitized_name}:{server}</a> has been created.",
|
||||||
|
edits=mymsg,
|
||||||
|
allow_html=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return space_id, f"#{sanitized_name}:{server}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Failed to create space: {e}"
|
||||||
|
self.log.error(error_msg)
|
||||||
|
if evt and mymsg:
|
||||||
|
await evt.respond(error_msg, edits=mymsg)
|
||||||
|
elif evt:
|
||||||
|
await evt.respond(error_msg)
|
||||||
|
return None, None
|
||||||
|
|
||||||
async def _redaction_loop(self) -> None:
|
async def _redaction_loop(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@@ -1593,6 +1719,7 @@ class CommunityBot(Plugin):
|
|||||||
Returns:
|
Returns:
|
||||||
tuple: (room_id, room_alias) if successful, None if failed
|
tuple: (room_id, room_alias) if successful, None if failed
|
||||||
"""
|
"""
|
||||||
|
mymsg = None
|
||||||
encrypted_flag_regex = re.compile(r"(\s+|^)-+encrypt(ed)?\s?")
|
encrypted_flag_regex = re.compile(r"(\s+|^)-+encrypt(ed)?\s?")
|
||||||
unencrypted_flag_regex = re.compile(r"(\s+|^)-+unencrypt(ed)?\s?")
|
unencrypted_flag_regex = re.compile(r"(\s+|^)-+unencrypt(ed)?\s?")
|
||||||
force_encryption = bool(encrypted_flag_regex.search(roomname))
|
force_encryption = bool(encrypted_flag_regex.search(roomname))
|
||||||
@@ -1607,6 +1734,26 @@ class CommunityBot(Plugin):
|
|||||||
parent_room = self.config["parent_room"]
|
parent_room = self.config["parent_room"]
|
||||||
server = self.client.parse_user_id(self.client.mxid)[1]
|
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
|
# Get power levels from parent room if not provided
|
||||||
if not power_level_override and parent_room:
|
if not power_level_override and parent_room:
|
||||||
power_levels = await self.client.get_state_event(
|
power_levels = await self.client.get_state_event(
|
||||||
@@ -1629,7 +1776,7 @@ class CommunityBot(Plugin):
|
|||||||
|
|
||||||
if evt:
|
if evt:
|
||||||
mymsg = await evt.respond(
|
mymsg = await evt.respond(
|
||||||
f"creating {sanitized_name}, give me a minute..."
|
f"creating {alias_localpart}, give me a minute..."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare initial state events
|
# Prepare initial state events
|
||||||
@@ -1667,9 +1814,18 @@ class CommunityBot(Plugin):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Add history visibility if specified in creation_content
|
||||||
|
if creation_content and "m.room.history_visibility" in creation_content:
|
||||||
|
initial_state.append({
|
||||||
|
"type": str(EventType.ROOM_HISTORY_VISIBILITY),
|
||||||
|
"content": {
|
||||||
|
"history_visibility": creation_content["m.room.history_visibility"]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
# Create the room with all initial states
|
# Create the room with all initial states
|
||||||
room_id = await self.client.create_room(
|
room_id = await self.client.create_room(
|
||||||
alias_localpart=sanitized_name,
|
alias_localpart=alias_localpart,
|
||||||
name=roomname,
|
name=roomname,
|
||||||
invitees=invitees,
|
invitees=invitees,
|
||||||
initial_state=initial_state,
|
initial_state=initial_state,
|
||||||
@@ -1692,18 +1848,20 @@ class CommunityBot(Plugin):
|
|||||||
|
|
||||||
if evt:
|
if evt:
|
||||||
await evt.respond(
|
await evt.respond(
|
||||||
f"<a href='https://matrix.to/#/#{sanitized_name}:{server}'>#{sanitized_name}:{server}</a> has been created and added to the space.",
|
f"<a href='https://matrix.to/#/#{alias_localpart}:{server}'>#{alias_localpart}:{server}</a> has been created and added to the space.",
|
||||||
edits=mymsg,
|
edits=mymsg,
|
||||||
allow_html=True
|
allow_html=True
|
||||||
)
|
)
|
||||||
|
|
||||||
return room_id, f"#{sanitized_name}:{server}"
|
return room_id, f"#{alias_localpart}:{server}"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Failed to create room: {e}"
|
error_msg = f"Failed to create room: {e}"
|
||||||
self.log.error(error_msg)
|
self.log.error(error_msg)
|
||||||
if evt:
|
if evt and mymsg:
|
||||||
await evt.respond(error_msg, edits=mymsg)
|
await evt.respond(error_msg, edits=mymsg)
|
||||||
|
elif evt:
|
||||||
|
await evt.respond(error_msg)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@community.subcommand(
|
@community.subcommand(
|
||||||
@@ -1727,6 +1885,17 @@ class CommunityBot(Plugin):
|
|||||||
await evt.reply("You don't have permission to use this command")
|
await evt.reply("You don't have permission to use this command")
|
||||||
return
|
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)
|
result = await self.create_room(roomname, evt)
|
||||||
if not result:
|
if not result:
|
||||||
return # Error already logged and reported to user by create_room
|
return # Error already logged and reported to user by create_room
|
||||||
@@ -1821,6 +1990,17 @@ class CommunityBot(Plugin):
|
|||||||
# Get list of aliases to transfer while removing them from the old room
|
# Get list of aliases to transfer while removing them from the old room
|
||||||
aliases_to_transfer = await self.remove_room_aliases(room_id, evt)
|
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
|
# Now we can start the process of replacing the room
|
||||||
# First we need to create the new room. this will create the initial alias,
|
# 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,
|
# as well as bot defaults such as power levels, initial invitations, encryption,
|
||||||
@@ -2277,6 +2457,25 @@ class CommunityBot(Plugin):
|
|||||||
msg = await evt.respond("Initializing new community space...")
|
msg = await evt.respond("Initializing new community space...")
|
||||||
|
|
||||||
try:
|
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 child rooms that will be created during initialization (excluding the space itself)
|
||||||
|
child_rooms_to_create = [
|
||||||
|
f"{community_name} Moderators", # Moderators room
|
||||||
|
f"{community_name} Waiting Room" # Waiting room
|
||||||
|
]
|
||||||
|
|
||||||
|
# Validate child room aliases before creating any rooms
|
||||||
|
is_valid, conflicting_aliases = await self.validate_room_aliases(child_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
|
# Add initiator to invitees list if not already there
|
||||||
if evt.sender not in self.config["invitees"]:
|
if evt.sender not in self.config["invitees"]:
|
||||||
self.config["invitees"].append(evt.sender)
|
self.config["invitees"].append(evt.sender)
|
||||||
@@ -2297,13 +2496,16 @@ class CommunityBot(Plugin):
|
|||||||
power_levels.invite = self.config["invite_power_level"]
|
power_levels.invite = self.config["invite_power_level"]
|
||||||
|
|
||||||
# Create the space with appropriate metadata and power levels
|
# Create the space with appropriate metadata and power levels
|
||||||
space_id, space_alias = await self.create_room(
|
space_id, space_alias = await self.create_space(
|
||||||
community_name,
|
community_name,
|
||||||
evt,
|
evt,
|
||||||
power_level_override=power_levels,
|
power_level_override=power_levels
|
||||||
creation_content={"type": "m.space"}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not space_id:
|
||||||
|
await evt.respond("Failed to create space", edits=msg)
|
||||||
|
return
|
||||||
|
|
||||||
# Set the space as the parent room in config
|
# Set the space as the parent room in config
|
||||||
self.config["parent_room"] = space_id
|
self.config["parent_room"] = space_id
|
||||||
|
|
||||||
@@ -2336,10 +2538,18 @@ class CommunityBot(Plugin):
|
|||||||
|
|
||||||
# Create waiting room
|
# Create waiting room
|
||||||
waiting_room_id, waiting_room_alias = await self.create_room(
|
waiting_room_id, waiting_room_alias = await self.create_room(
|
||||||
f"{community_name} Waiting Room",
|
f"{community_name} Waiting Room --unencrypted",
|
||||||
evt
|
evt,
|
||||||
|
creation_content={
|
||||||
|
"m.federate": True,
|
||||||
|
"m.room.history_visibility": "joined"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not waiting_room_id:
|
||||||
|
await evt.respond("Failed to create waiting room", edits=msg)
|
||||||
|
return
|
||||||
|
|
||||||
# Set waiting room to be joinable by anyone
|
# Set waiting room to be joinable by anyone
|
||||||
await self.client.send_state_event(
|
await self.client.send_state_event(
|
||||||
waiting_room_id,
|
waiting_room_id,
|
||||||
@@ -2361,11 +2571,17 @@ class CommunityBot(Plugin):
|
|||||||
# Save the updated config
|
# Save the updated config
|
||||||
self.config.save()
|
self.config.save()
|
||||||
|
|
||||||
|
# Check if default encryption is enabled and add warning for waiting room
|
||||||
|
warning_msg = ""
|
||||||
|
if self.config.get("encrypt", False):
|
||||||
|
warning_msg = "\n\n⚠️ **Note: Waiting room created without encryption (as it is a public room)**"
|
||||||
|
|
||||||
await evt.respond(
|
await evt.respond(
|
||||||
f"Community space initialized successfully!\n\n"
|
f"Community space initialized successfully!\n\n"
|
||||||
|
f"Community Slug: {self.config['community_slug']}\n"
|
||||||
f"Space: <a href='https://matrix.to/#/{space_alias}'>{space_alias}</a>\n"
|
f"Space: <a href='https://matrix.to/#/{space_alias}'>{space_alias}</a>\n"
|
||||||
f"Moderators Room: <a href='https://matrix.to/#/{mod_room_alias}'>{mod_room_alias}</a>\n"
|
f"Moderators Room: <a href='https://matrix.to/#/{mod_room_alias}'>{mod_room_alias}</a>\n"
|
||||||
f"Waiting Room: <a href='https://matrix.to/#/{waiting_room_alias}'>{waiting_room_alias}</a>",
|
f"Waiting Room: <a href='https://matrix.to/#/{waiting_room_alias}'>{waiting_room_alias}</a>{warning_msg}",
|
||||||
edits=msg,
|
edits=msg,
|
||||||
allow_html=True
|
allow_html=True
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ plugin_config:
|
|||||||
# leave this empty to use the initialize command to create a new community to manage,
|
# leave this empty to use the initialize command to create a new community to manage,
|
||||||
# based on opinionated defaults.
|
# based on opinionated defaults.
|
||||||
parent_room: ''
|
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
|
# 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
|
# ratelimits imposed on its homeserver, otherwise you may want to increase this
|
||||||
|
|||||||
Reference in New Issue
Block a user