fix reporting
This commit is contained in:
+148
-75
@@ -150,14 +150,14 @@ class CommunityBot(Plugin):
|
|||||||
Returns:
|
Returns:
|
||||||
tuple: (is_valid, list_of_conflicting_aliases)
|
tuple: (is_valid, list_of_conflicting_aliases)
|
||||||
"""
|
"""
|
||||||
if not self.config["community_slug"]:
|
if not self.config.get("community_slug", ""):
|
||||||
if evt:
|
if evt:
|
||||||
await evt.respond("Error: No community slug configured. Please run initialize command first.")
|
await evt.respond("Error: No community slug configured. Please run initialize command first.")
|
||||||
return False, []
|
return False, []
|
||||||
|
|
||||||
server = self.client.parse_user_id(self.client.mxid)[1]
|
server = self.client.parse_user_id(self.client.mxid)[1]
|
||||||
return await room_utils.validate_room_aliases(
|
return await room_utils.validate_room_aliases(
|
||||||
self.client, room_names, self.config["community_slug"], server
|
self.client, room_names, self.config.get("community_slug", ""), server
|
||||||
)
|
)
|
||||||
|
|
||||||
async def get_moderators_and_above(self) -> list[str]:
|
async def get_moderators_and_above(self) -> list[str]:
|
||||||
@@ -166,7 +166,7 @@ class CommunityBot(Plugin):
|
|||||||
Returns:
|
Returns:
|
||||||
list: List of user IDs with power level >= 50 (moderator or above)
|
list: List of user IDs with power level >= 50 (moderator or above)
|
||||||
"""
|
"""
|
||||||
return await room_utils.get_moderators_and_above(self.client, self.config["parent_room"])
|
return await room_utils.get_moderators_and_above(self.client, self.config.get("parent_room", ""))
|
||||||
|
|
||||||
async def create_space(self, space_name: str, evt: MessageEvent = None, power_level_override: Optional[PowerLevelStateEventContent] = None) -> tuple[str, str]:
|
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.
|
"""Create a new space without community slug suffix.
|
||||||
@@ -182,7 +182,7 @@ class CommunityBot(Plugin):
|
|||||||
mymsg = None
|
mymsg = None
|
||||||
try:
|
try:
|
||||||
sanitized_name = re.sub(r"[^a-zA-Z0-9]", "", space_name).lower()
|
sanitized_name = re.sub(r"[^a-zA-Z0-9]", "", space_name).lower()
|
||||||
invitees = self.config["invitees"]
|
invitees = self.config.get("invitees", [])
|
||||||
server = self.client.parse_user_id(self.client.mxid)[1]
|
server = self.client.parse_user_id(self.client.mxid)[1]
|
||||||
|
|
||||||
# Validate that the space alias is available
|
# Validate that the space alias is available
|
||||||
@@ -196,25 +196,28 @@ class CommunityBot(Plugin):
|
|||||||
|
|
||||||
if evt:
|
if evt:
|
||||||
mymsg = await evt.respond(
|
mymsg = await evt.respond(
|
||||||
f"creating space {sanitized_name} with room version {self.config['room_version']}, give me a minute..."
|
f"creating space {sanitized_name} with room version {self.config.get('room_version', '1')}, give me a minute..."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Prepare creation content with space type
|
# Prepare creation content with space type
|
||||||
|
# Spaces are created by setting the type to "m.space" in creation_content
|
||||||
creation_content = {
|
creation_content = {
|
||||||
"type": "m.space"
|
"type": "m.space",
|
||||||
|
"m.federate": True,
|
||||||
|
"m.room.history_visibility": "joined"
|
||||||
}
|
}
|
||||||
|
|
||||||
# For modern room versions (12+), remove the bot from power levels
|
# For modern room versions (12+), remove the bot from power levels
|
||||||
# as creators have unlimited power by default and cannot appear in power levels
|
# as creators have unlimited power by default and cannot appear in power levels
|
||||||
if self.is_modern_room_version(self.config["room_version"]) and power_level_override:
|
if self.is_modern_room_version(self.config.get("room_version", "1")) and power_level_override:
|
||||||
self.log.info(f"Modern room version {self.config['room_version']} detected - removing bot from power levels")
|
self.log.info(f"Modern room version {self.config.get('room_version', '1')} detected - removing bot from power levels")
|
||||||
if power_level_override.users:
|
if power_level_override.users:
|
||||||
# Remove bot from users list but keep other important settings
|
# Remove bot from users list but keep other important settings
|
||||||
power_level_override.users.pop(self.client.mxid, None)
|
power_level_override.users.pop(self.client.mxid, None)
|
||||||
|
|
||||||
# Create the space with space-specific content
|
# Create the space with space-specific content
|
||||||
# Note: room_version is set via the room_version parameter, not creation_content
|
# Note: room_version is set via the room_version parameter, not creation_content
|
||||||
self.log.info(f"Creating space with room_version={self.config['room_version']}")
|
self.log.info(f"Creating space with room_version={self.config.get('room_version', '1')}")
|
||||||
self.log.info(f"Creation content: {creation_content}")
|
self.log.info(f"Creation content: {creation_content}")
|
||||||
self.log.info(f"Calling client.create_room with parameters:")
|
self.log.info(f"Calling client.create_room with parameters:")
|
||||||
self.log.info(f" - alias_localpart: {sanitized_name}")
|
self.log.info(f" - alias_localpart: {sanitized_name}")
|
||||||
@@ -222,7 +225,7 @@ class CommunityBot(Plugin):
|
|||||||
self.log.info(f" - invitees: {invitees}")
|
self.log.info(f" - invitees: {invitees}")
|
||||||
self.log.info(f" - power_level_override: {power_level_override}")
|
self.log.info(f" - power_level_override: {power_level_override}")
|
||||||
self.log.info(f" - creation_content: {creation_content}")
|
self.log.info(f" - creation_content: {creation_content}")
|
||||||
self.log.info(f" - room_version: {self.config['room_version']}")
|
self.log.info(f" - room_version: {self.config.get('room_version', '1')}")
|
||||||
|
|
||||||
space_id = await self.client.create_room(
|
space_id = await self.client.create_room(
|
||||||
alias_localpart=sanitized_name,
|
alias_localpart=sanitized_name,
|
||||||
@@ -230,15 +233,15 @@ class CommunityBot(Plugin):
|
|||||||
invitees=invitees,
|
invitees=invitees,
|
||||||
power_level_override=power_level_override,
|
power_level_override=power_level_override,
|
||||||
creation_content=creation_content,
|
creation_content=creation_content,
|
||||||
room_version=self.config["room_version"]
|
room_version=self.config.get("room_version", "1")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Verify the space version and type were set correctly
|
# Verify the space version and type were set correctly
|
||||||
try:
|
try:
|
||||||
actual_version, actual_creators = await self.get_room_version_and_creators(space_id)
|
actual_version, actual_creators = await self.get_room_version_and_creators(space_id)
|
||||||
self.log.info(f"Space {space_id} created with version {actual_version} (requested: {self.config['room_version']})")
|
self.log.info(f"Space {space_id} created with version {actual_version} (requested: {self.config.get('room_version', '1')})")
|
||||||
if actual_version != self.config["room_version"]:
|
if actual_version != self.config.get("room_version", "1"):
|
||||||
self.log.warning(f"Space version mismatch: requested {self.config['room_version']}, got {actual_version}")
|
self.log.warning(f"Space version mismatch: requested {self.config.get('room_version', '1')}, got {actual_version}")
|
||||||
|
|
||||||
# Verify the space type was set
|
# Verify the space type was set
|
||||||
state_events = await self.client.get_state(space_id)
|
state_events = await self.client.get_state(space_id)
|
||||||
@@ -277,6 +280,7 @@ class CommunityBot(Plugin):
|
|||||||
allow_html=True
|
allow_html=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.log.info(f"Space creation completed successfully: {space_id}")
|
||||||
return space_id, f"#{sanitized_name}:{server}"
|
return space_id, f"#{sanitized_name}:{server}"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -356,9 +360,15 @@ class CommunityBot(Plugin):
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
async def get_space_roomlist(self) -> None:
|
async def get_space_roomlist(self) -> list[str]:
|
||||||
space = self.config["parent_room"]
|
space = self.config["parent_room"]
|
||||||
rooms = []
|
rooms = []
|
||||||
|
|
||||||
|
# Check if parent room is configured
|
||||||
|
if not space:
|
||||||
|
self.log.warning("No parent room configured, cannot get space roomlist")
|
||||||
|
return rooms
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.log.debug(f"DEBUG getting roomlist from {space} space")
|
self.log.debug(f"DEBUG getting roomlist from {space} space")
|
||||||
state = await self.client.get_state(space)
|
state = await self.client.get_state(space)
|
||||||
@@ -1233,11 +1243,12 @@ class CommunityBot(Plugin):
|
|||||||
await evt.react("✅")
|
await evt.react("✅")
|
||||||
|
|
||||||
@community.subcommand(
|
@community.subcommand(
|
||||||
"report", help="generate a full list of activity tracking status"
|
"report", help="generate reports of user activity and inactivity"
|
||||||
)
|
)
|
||||||
@decorators.require_parent_room
|
@decorators.require_parent_room
|
||||||
@decorators.require_permission()
|
@decorators.require_permission()
|
||||||
async def get_report(self, evt: MessageEvent) -> None:
|
async def report(self, evt: MessageEvent) -> None:
|
||||||
|
"""Main report command - shows full report by default"""
|
||||||
if not self.config_manager.is_tracking_enabled():
|
if not self.config_manager.is_tracking_enabled():
|
||||||
await evt.reply("user tracking is disabled")
|
await evt.reply("user tracking is disabled")
|
||||||
return
|
return
|
||||||
@@ -1255,13 +1266,37 @@ class CommunityBot(Plugin):
|
|||||||
allow_html=True,
|
allow_html=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@community.subcommand(
|
@report.subcommand(
|
||||||
"inactive", help="generate a list of mxids who have been inactive"
|
"all", help="generate a full report of all user activity status"
|
||||||
)
|
)
|
||||||
@decorators.require_parent_room
|
@decorators.require_parent_room
|
||||||
@decorators.require_permission()
|
@decorators.require_permission()
|
||||||
async def get_inactive_report(self, evt: MessageEvent) -> None:
|
async def report_all(self, evt: MessageEvent) -> None:
|
||||||
|
"""Report all user activity status - same as main report command"""
|
||||||
|
if not self.config_manager.is_tracking_enabled():
|
||||||
|
await evt.reply("user tracking is disabled")
|
||||||
|
return
|
||||||
|
|
||||||
|
sync_results = await self.do_sync()
|
||||||
|
report = await self.generate_report()
|
||||||
|
await evt.respond(
|
||||||
|
f"<p><b>Users inactive for between {self.config['warn_threshold_days']} and \
|
||||||
|
{self.config['kick_threshold_days']} days:</b><br /> \
|
||||||
|
{'<br />'.join(report['warn_inactive'])} <br /></p>\
|
||||||
|
<p><b>Users inactive for at least {self.config['kick_threshold_days']} days:</b><br /> \
|
||||||
|
{'<br />'.join(report['kick_inactive'])} <br /></p> \
|
||||||
|
<p><b>Ignored users:</b><br /> \
|
||||||
|
{'<br />'.join(report['ignored'])}</p>",
|
||||||
|
allow_html=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@report.subcommand(
|
||||||
|
"inactive", help="generate a list of users who have been inactive"
|
||||||
|
)
|
||||||
|
@decorators.require_parent_room
|
||||||
|
@decorators.require_permission()
|
||||||
|
async def report_inactive(self, evt: MessageEvent) -> None:
|
||||||
|
"""Report users who are inactive but not yet at kick threshold"""
|
||||||
if not self.config_manager.is_tracking_enabled():
|
if not self.config_manager.is_tracking_enabled():
|
||||||
await evt.reply("user tracking is disabled")
|
await evt.reply("user tracking is disabled")
|
||||||
return
|
return
|
||||||
@@ -1275,16 +1310,13 @@ class CommunityBot(Plugin):
|
|||||||
allow_html=True,
|
allow_html=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@community.subcommand(
|
@report.subcommand(
|
||||||
"purgable", help="generate a list of matrix IDs that have been inactive long enough to be purged"
|
"purgable", help="generate a list of users that would be kicked with the purge command"
|
||||||
)
|
)
|
||||||
async def get_purgable_report(self, evt: MessageEvent) -> None:
|
@decorators.require_parent_room
|
||||||
if not await self.check_parent_room(evt):
|
@decorators.require_permission()
|
||||||
return
|
async def report_purgable(self, evt: MessageEvent) -> None:
|
||||||
if not await self.user_permitted(evt.sender):
|
"""Report users who are inactive long enough to be purged"""
|
||||||
await evt.reply("You don't have permission to use this command")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.config_manager.is_tracking_enabled():
|
if not self.config_manager.is_tracking_enabled():
|
||||||
await evt.reply("user tracking is disabled")
|
await evt.reply("user tracking is disabled")
|
||||||
return
|
return
|
||||||
@@ -1297,16 +1329,13 @@ class CommunityBot(Plugin):
|
|||||||
allow_html=True,
|
allow_html=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@community.subcommand(
|
@report.subcommand(
|
||||||
"ignored", help="generate a list of matrix IDs that have activity tracking disabled"
|
"ignored", help="generate a list of users that have activity tracking disabled"
|
||||||
)
|
)
|
||||||
async def get_ignored_report(self, evt: MessageEvent) -> None:
|
@decorators.require_parent_room
|
||||||
if not await self.check_parent_room(evt):
|
@decorators.require_permission()
|
||||||
return
|
async def report_ignored(self, evt: MessageEvent) -> None:
|
||||||
if not await self.user_permitted(evt.sender):
|
"""Report users who are ignored for activity tracking"""
|
||||||
await evt.reply("You don't have permission to use this command")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self.config_manager.is_tracking_enabled():
|
if not self.config_manager.is_tracking_enabled():
|
||||||
await evt.reply("user tracking is disabled")
|
await evt.reply("user tracking is disabled")
|
||||||
return
|
return
|
||||||
@@ -1532,7 +1561,7 @@ class CommunityBot(Plugin):
|
|||||||
)
|
)
|
||||||
await evt.respond(f"Queued {len(messages)} messages for redaction in {room_id}")
|
await evt.respond(f"Queued {len(messages)} messages for redaction in {room_id}")
|
||||||
|
|
||||||
async def create_room(self, roomname: str, evt: MessageEvent = None, power_level_override: Optional[PowerLevelStateEventContent] = None, creation_content: Optional[dict] = None, invitees: Optional[list[str]] = None) -> None:
|
async def create_room(self, roomname: str, evt: MessageEvent = None, power_level_override: Optional[PowerLevelStateEventContent] = None, creation_content: Optional[dict] = None, invitees: Optional[list[str]] = None) -> tuple[str, str] | None:
|
||||||
"""Create a new room and add it to the parent space.
|
"""Create a new room and add it to the parent space.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -1548,7 +1577,7 @@ class CommunityBot(Plugin):
|
|||||||
mymsg = None
|
mymsg = None
|
||||||
try:
|
try:
|
||||||
# Validate and process room creation parameters
|
# Validate and process room creation parameters
|
||||||
sanitized_name, force_encryption, force_unencryption, error_msg = await room_creation_utils.validate_room_creation_params(
|
sanitized_name, force_encryption, force_unencryption, error_msg, cleaned_roomname = await room_creation_utils.validate_room_creation_params(
|
||||||
roomname, self.config, evt
|
roomname, self.config, evt
|
||||||
)
|
)
|
||||||
if error_msg:
|
if error_msg:
|
||||||
@@ -1572,9 +1601,14 @@ class CommunityBot(Plugin):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# Prepare power levels
|
# Prepare power levels
|
||||||
power_levels = await room_creation_utils.prepare_power_levels(
|
try:
|
||||||
self.client, self.config, parent_room, power_level_override
|
power_levels = await room_creation_utils.prepare_power_levels(
|
||||||
)
|
self.client, self.config, parent_room, power_level_override
|
||||||
|
)
|
||||||
|
self.log.info(f"Power levels prepared successfully: {power_levels}")
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(f"Failed to prepare power levels: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
# Adjust power levels for modern rooms
|
# Adjust power levels for modern rooms
|
||||||
power_levels = room_creation_utils.adjust_power_levels_for_modern_rooms(
|
power_levels = room_creation_utils.adjust_power_levels_for_modern_rooms(
|
||||||
@@ -1603,15 +1637,20 @@ class CommunityBot(Plugin):
|
|||||||
else:
|
else:
|
||||||
self.log.info("No power level override")
|
self.log.info("No power level override")
|
||||||
|
|
||||||
room_id = await self.client.create_room(
|
try:
|
||||||
alias_localpart=alias_localpart,
|
room_id = await self.client.create_room(
|
||||||
name=roomname,
|
alias_localpart=alias_localpart,
|
||||||
invitees=room_invitees,
|
name=cleaned_roomname,
|
||||||
initial_state=initial_state,
|
invitees=room_invitees,
|
||||||
power_level_override=power_levels,
|
initial_state=initial_state,
|
||||||
creation_content=creation_content,
|
power_level_override=power_levels,
|
||||||
room_version=self.config["room_version"]
|
creation_content=creation_content,
|
||||||
)
|
room_version=self.config["room_version"]
|
||||||
|
)
|
||||||
|
self.log.info(f"Room created successfully: {room_id}")
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error(f"Failed to create room via Matrix API: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
# Verify room creation
|
# Verify room creation
|
||||||
await room_creation_utils.verify_room_creation(
|
await room_creation_utils.verify_room_creation(
|
||||||
@@ -2685,20 +2724,24 @@ class CommunityBot(Plugin):
|
|||||||
# Set up power levels for the space
|
# Set up power levels for the space
|
||||||
power_levels = PowerLevelStateEventContent()
|
power_levels = PowerLevelStateEventContent()
|
||||||
|
|
||||||
# For modern room versions (12+), don't set power levels for creators
|
# Set up power levels for users
|
||||||
# as they have unlimited power by default
|
# For modern room versions (12+), the bot (creator) has unlimited power by default
|
||||||
if self.is_modern_room_version(self.config["room_version"]):
|
# but we still need to set power levels for other users
|
||||||
# Don't set any user power levels for modern versions
|
if self.is_modern_room_version(self.config.get("room_version", "1")):
|
||||||
# Creators have unlimited power by default
|
# For modern rooms, don't set bot power level (it has unlimited power)
|
||||||
power_levels.users = {}
|
# but still set power levels for other users
|
||||||
|
power_levels.users = {
|
||||||
|
evt.sender: 100 # Initiator gets admin power
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
|
# For legacy rooms, set both bot and initiator power levels
|
||||||
power_levels.users = {
|
power_levels.users = {
|
||||||
self.client.mxid: 1000, # Bot gets highest power
|
self.client.mxid: 1000, # Bot gets highest power
|
||||||
evt.sender: 100 # Initiator gets admin power
|
evt.sender: 100 # Initiator gets admin power
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set invite power level from config
|
# Set invite power level from config
|
||||||
power_levels.invite = self.config["invite_power_level"]
|
power_levels.invite = self.config.get("invite_power_level", 50)
|
||||||
|
|
||||||
# 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_space(
|
space_id, space_alias = await self.create_space(
|
||||||
@@ -2713,15 +2756,27 @@ class CommunityBot(Plugin):
|
|||||||
|
|
||||||
# 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
|
||||||
|
self.log.info(f"Set parent_room to: {space_id}")
|
||||||
|
|
||||||
# Save the updated config
|
# Save the updated config
|
||||||
self.config.save()
|
self.config.save()
|
||||||
|
self.log.info("Config saved successfully")
|
||||||
|
|
||||||
# Verify the space exists and has correct power levels
|
# Verify the space exists and has correct power levels
|
||||||
try:
|
try:
|
||||||
space_power_levels = await self.client.get_state_event(space_id, EventType.ROOM_POWER_LEVELS)
|
space_power_levels = await self.client.get_state_event(space_id, EventType.ROOM_POWER_LEVELS)
|
||||||
if space_power_levels.users.get(self.client.mxid) != 1000:
|
|
||||||
raise Exception("Space power levels not set correctly")
|
# For modern room versions, creators have unlimited power and don't appear in power levels
|
||||||
|
if self.is_modern_room_version(self.config.get("room_version", "1")):
|
||||||
|
# Just verify the space exists and has power levels
|
||||||
|
if not space_power_levels:
|
||||||
|
raise Exception("Space power levels not set correctly")
|
||||||
|
self.log.info("Space power levels verified for modern room version")
|
||||||
|
else:
|
||||||
|
# For legacy room versions, check that bot has admin power
|
||||||
|
if space_power_levels.users.get(self.client.mxid) != 1000:
|
||||||
|
raise Exception("Space power levels not set correctly")
|
||||||
|
self.log.info("Space power levels verified for legacy room version")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Failed to verify space setup: {e}"
|
error_msg = f"Failed to verify space setup: {e}"
|
||||||
self.log.error(error_msg)
|
self.log.error(error_msg)
|
||||||
@@ -2729,22 +2784,36 @@ class CommunityBot(Plugin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Create moderators room
|
# Create moderators room
|
||||||
# Get moderators and above from the space instead of using config invitees
|
# Include the initiator as a moderator, plus any other moderators from the space
|
||||||
moderators = await self.get_moderators_and_above()
|
moderators = [evt.sender] # Always include the initiator
|
||||||
if not moderators:
|
|
||||||
self.log.warning("No moderators found in space, moderators room will be created without initial members")
|
|
||||||
else:
|
|
||||||
# Filter out the bot's own user ID to prevent self-invitation
|
|
||||||
moderators = [user for user in moderators if user != self.client.mxid]
|
|
||||||
if not moderators:
|
|
||||||
self.log.info("Only bot found in moderators list, moderators room will be created without initial members")
|
|
||||||
|
|
||||||
mod_room_id, mod_room_alias = await self.create_room(
|
# Also get any other moderators from the space
|
||||||
|
try:
|
||||||
|
space_moderators = await self.get_moderators_and_above()
|
||||||
|
if space_moderators:
|
||||||
|
# Add other moderators, excluding the bot and the initiator (already added)
|
||||||
|
for user in space_moderators:
|
||||||
|
if user != self.client.mxid and user != evt.sender:
|
||||||
|
moderators.append(user)
|
||||||
|
except Exception as e:
|
||||||
|
self.log.warning(f"Could not get additional moderators from space: {e}")
|
||||||
|
|
||||||
|
self.log.info(f"Moderators room will be created with initial members: {moderators}")
|
||||||
|
|
||||||
|
room_result = await self.create_room(
|
||||||
f"{community_name} Moderators",
|
f"{community_name} Moderators",
|
||||||
evt,
|
evt,
|
||||||
invitees=moderators # Use moderators list instead of config invitees
|
invitees=moderators # Use moderators list instead of config invitees
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not room_result:
|
||||||
|
error_msg = "Failed to create moderators room"
|
||||||
|
self.log.error(error_msg)
|
||||||
|
await evt.respond(error_msg, edits=msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
mod_room_id, mod_room_alias = room_result
|
||||||
|
|
||||||
# Set moderators room to invite-only
|
# Set moderators room to invite-only
|
||||||
await self.client.send_state_event(
|
await self.client.send_state_event(
|
||||||
mod_room_id,
|
mod_room_id,
|
||||||
@@ -2752,8 +2821,8 @@ class CommunityBot(Plugin):
|
|||||||
JoinRulesStateEventContent(join_rule=JoinRule.INVITE)
|
JoinRulesStateEventContent(join_rule=JoinRule.INVITE)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create waiting room
|
# Create waiting room (force unencrypted for public access)
|
||||||
waiting_room_id, waiting_room_alias = await self.create_room(
|
waiting_room_result = await self.create_room(
|
||||||
f"{community_name} Waiting Room --unencrypted",
|
f"{community_name} Waiting Room --unencrypted",
|
||||||
evt,
|
evt,
|
||||||
creation_content={
|
creation_content={
|
||||||
@@ -2762,10 +2831,14 @@ class CommunityBot(Plugin):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not waiting_room_id:
|
if not waiting_room_result:
|
||||||
await evt.respond("Failed to create waiting room", edits=msg)
|
error_msg = "Failed to create waiting room"
|
||||||
|
self.log.error(error_msg)
|
||||||
|
await evt.respond(error_msg, edits=msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
waiting_room_id, waiting_room_alias = waiting_room_result
|
||||||
|
|
||||||
# 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,
|
||||||
|
|||||||
@@ -26,10 +26,15 @@ async def check_space_permissions(
|
|||||||
)
|
)
|
||||||
bot_level = space_power_levels.get_user_level(client.mxid)
|
bot_level = space_power_levels.get_user_level(client.mxid)
|
||||||
|
|
||||||
|
# Check if bot has unlimited power (creator in modern room versions)
|
||||||
|
from .room_utils import user_has_unlimited_power
|
||||||
|
bot_has_unlimited_power = await user_has_unlimited_power(client, client.mxid, parent_room)
|
||||||
|
|
||||||
space_info = {
|
space_info = {
|
||||||
"room_id": parent_room,
|
"room_id": parent_room,
|
||||||
"bot_power_level": bot_level,
|
"bot_power_level": bot_level,
|
||||||
"has_admin": bot_level >= 100,
|
"has_admin": bot_level >= 100 or bot_has_unlimited_power,
|
||||||
|
"bot_has_unlimited_power": bot_has_unlimited_power,
|
||||||
"users_higher_or_equal": [],
|
"users_higher_or_equal": [],
|
||||||
"users_equal": [],
|
"users_equal": [],
|
||||||
"users_higher": []
|
"users_higher": []
|
||||||
@@ -196,7 +201,12 @@ def generate_space_summary(
|
|||||||
|
|
||||||
space_status = "✅" if space_data.get("has_admin", False) else "❌"
|
space_status = "✅" if space_data.get("has_admin", False) else "❌"
|
||||||
response = f"<h4>📋 Parent Space</h4><br />"
|
response = f"<h4>📋 Parent Space</h4><br />"
|
||||||
response += f"{space_status} <b>Administrative privileges:</b> {'Yes' if space_data['has_admin'] else 'No'} (level: {space_data['bot_power_level']})<br />"
|
|
||||||
|
# Show admin status with appropriate details
|
||||||
|
if space_data.get("bot_has_unlimited_power", False):
|
||||||
|
response += f"{space_status} <b>Administrative privileges:</b> Yes (unlimited power - creator)<br />"
|
||||||
|
else:
|
||||||
|
response += f"{space_status} <b>Administrative privileges:</b> {'Yes' if space_data['has_admin'] else 'No'} (level: {space_data['bot_power_level']})<br />"
|
||||||
|
|
||||||
if space_data.get("users_higher"):
|
if space_data.get("users_higher"):
|
||||||
response += f"⚠️ <b>Users with higher power:</b> {', '.join([f'{u['user']} ({u['level']})' for u in space_data['users_higher']])}<br />"
|
response += f"⚠️ <b>Users with higher power:</b> {', '.join([f'{u['user']} ({u['level']})' for u in space_data['users_higher']])}<br />"
|
||||||
|
|||||||
@@ -8,20 +8,20 @@ def generate_activity_report(database_results: Dict[str, List[Dict]]) -> Dict[st
|
|||||||
"""Generate an activity report from database results.
|
"""Generate an activity report from database results.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
database_results: Dictionary containing 'active', 'inactive', 'ignored' results
|
database_results: Dictionary containing 'warn_inactive', 'kick_inactive', 'ignored' results
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Formatted activity report
|
dict: Formatted activity report
|
||||||
"""
|
"""
|
||||||
report = {}
|
report = {}
|
||||||
|
|
||||||
# Process active users
|
# Process warn inactive users (between warn and kick thresholds)
|
||||||
active_results = database_results.get("active", [])
|
warn_inactive_results = database_results.get("warn_inactive", [])
|
||||||
report["active"] = [row["mxid"] for row in active_results] or ["none"]
|
report["warn_inactive"] = [row["mxid"] for row in warn_inactive_results] or ["none"]
|
||||||
|
|
||||||
# Process inactive users
|
# Process kick inactive users (beyond kick threshold)
|
||||||
inactive_results = database_results.get("inactive", [])
|
kick_inactive_results = database_results.get("kick_inactive", [])
|
||||||
report["inactive"] = [row["mxid"] for row in inactive_results] or ["none"]
|
report["kick_inactive"] = [row["mxid"] for row in kick_inactive_results] or ["none"]
|
||||||
|
|
||||||
# Process ignored users
|
# Process ignored users
|
||||||
ignored_results = database_results.get("ignored", [])
|
ignored_results = database_results.get("ignored", [])
|
||||||
|
|||||||
@@ -22,26 +22,29 @@ async def validate_room_creation_params(
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (sanitized_name, force_encryption, force_unencryption, error_msg)
|
Tuple of (sanitized_name, force_encryption, force_unencryption, error_msg)
|
||||||
"""
|
"""
|
||||||
# Check for encryption flags
|
# Check for encryption flags (at beginning, middle, or end of string)
|
||||||
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))
|
||||||
force_unencryption = bool(unencrypted_flag_regex.search(roomname))
|
force_unencryption = bool(unencrypted_flag_regex.search(roomname))
|
||||||
|
|
||||||
# Clean up room name
|
# Clean up room name
|
||||||
if force_encryption:
|
if force_encryption:
|
||||||
roomname = encrypted_flag_regex.sub("", roomname)
|
roomname = encrypted_flag_regex.sub("", roomname) # Remove encryption flag
|
||||||
if force_unencryption:
|
if force_unencryption:
|
||||||
roomname = unencrypted_flag_regex.sub("", roomname)
|
roomname = unencrypted_flag_regex.sub("", roomname) # Remove unencryption flag
|
||||||
|
|
||||||
|
# Clean up any extra whitespace
|
||||||
|
roomname = re.sub(r"\s+", " ", roomname).strip()
|
||||||
|
|
||||||
sanitized_name = re.sub(r"[^a-zA-Z0-9]", "", roomname).lower()
|
sanitized_name = re.sub(r"[^a-zA-Z0-9]", "", roomname).lower()
|
||||||
|
|
||||||
# Check if community slug is configured
|
# Check if community slug is configured
|
||||||
if not config.get("community_slug"):
|
if not config.get("community_slug", ""):
|
||||||
error_msg = "No community slug configured. Please run initialize command first."
|
error_msg = "No community slug configured. Please run initialize command first."
|
||||||
return sanitized_name, force_encryption, force_unencryption, error_msg
|
return sanitized_name, force_encryption, force_unencryption, error_msg, roomname
|
||||||
|
|
||||||
return sanitized_name, force_encryption, force_unencryption, ""
|
return sanitized_name, force_encryption, force_unencryption, "", roomname
|
||||||
|
|
||||||
|
|
||||||
async def prepare_room_creation_data(
|
async def prepare_room_creation_data(
|
||||||
@@ -62,12 +65,12 @@ async def prepare_room_creation_data(
|
|||||||
Tuple of (alias_localpart, server, room_invitees, parent_room)
|
Tuple of (alias_localpart, server, room_invitees, parent_room)
|
||||||
"""
|
"""
|
||||||
# Create alias with community slug
|
# Create alias with community slug
|
||||||
alias_localpart = f"{sanitized_name}-{config['community_slug']}"
|
alias_localpart = f"{sanitized_name}-{config.get('community_slug', '')}"
|
||||||
|
|
||||||
# Get server and invitees
|
# Get server and invitees
|
||||||
server = client.parse_user_id(client.mxid)[1]
|
server = client.parse_user_id(client.mxid)[1]
|
||||||
room_invitees = invitees if invitees is not None else config["invitees"]
|
room_invitees = invitees if invitees is not None else config.get("invitees", [])
|
||||||
parent_room = config["parent_room"]
|
parent_room = config.get("parent_room", "")
|
||||||
|
|
||||||
return alias_localpart, server, room_invitees, parent_room
|
return alias_localpart, server, room_invitees, parent_room
|
||||||
|
|
||||||
@@ -93,36 +96,51 @@ async def prepare_power_levels(
|
|||||||
return power_level_override
|
return power_level_override
|
||||||
|
|
||||||
if parent_room:
|
if parent_room:
|
||||||
# Get parent room power levels to extract user power levels
|
try:
|
||||||
parent_power_levels = await client.get_state_event(
|
# Get parent room power levels to extract user power levels
|
||||||
parent_room, EventType.ROOM_POWER_LEVELS
|
parent_power_levels = await client.get_state_event(
|
||||||
)
|
parent_room, EventType.ROOM_POWER_LEVELS
|
||||||
|
)
|
||||||
|
|
||||||
# Create new power levels with server defaults, not copying all permissions from space
|
# Create new power levels with server defaults, not copying all permissions from space
|
||||||
power_levels = PowerLevelStateEventContent()
|
power_levels = PowerLevelStateEventContent()
|
||||||
|
|
||||||
# Copy only user power levels from parent space, not the entire permission set
|
# Copy only user power levels from parent space, not the entire permission set
|
||||||
if parent_power_levels.users:
|
if parent_power_levels and hasattr(parent_power_levels, 'users') and parent_power_levels.users:
|
||||||
user_power_levels = parent_power_levels.users.copy()
|
try:
|
||||||
# Ensure bot has highest power
|
user_power_levels = parent_power_levels.users.copy()
|
||||||
user_power_levels[client.mxid] = 1000
|
# Ensure bot has highest power
|
||||||
power_levels.users = user_power_levels
|
user_power_levels[client.mxid] = 1000
|
||||||
else:
|
power_levels.users = user_power_levels
|
||||||
|
except Exception as e:
|
||||||
|
# If copying users fails, create default power levels
|
||||||
|
power_levels.users = {
|
||||||
|
client.mxid: 1000, # Bot gets highest power
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
power_levels.users = {
|
||||||
|
client.mxid: 1000, # Bot gets highest power
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set explicit config values
|
||||||
|
power_levels.invite = config.get("invite_power_level", 50)
|
||||||
|
|
||||||
|
return power_levels
|
||||||
|
except Exception as e:
|
||||||
|
# If we can't get parent power levels, create default ones
|
||||||
|
power_levels = PowerLevelStateEventContent()
|
||||||
power_levels.users = {
|
power_levels.users = {
|
||||||
client.mxid: 1000, # Bot gets highest power
|
client.mxid: 1000, # Bot gets highest power
|
||||||
}
|
}
|
||||||
|
power_levels.invite = config.get("invite_power_level", 50)
|
||||||
# Set explicit config values
|
return power_levels
|
||||||
power_levels.invite = config["invite_power_level"]
|
|
||||||
|
|
||||||
return power_levels
|
|
||||||
else:
|
else:
|
||||||
# If no parent room, create default power levels
|
# If no parent room, create default power levels
|
||||||
power_levels = PowerLevelStateEventContent()
|
power_levels = PowerLevelStateEventContent()
|
||||||
power_levels.users = {
|
power_levels.users = {
|
||||||
client.mxid: 1000, # Bot gets highest power
|
client.mxid: 1000, # Bot gets highest power
|
||||||
}
|
}
|
||||||
power_levels.invite = config["invite_power_level"]
|
power_levels.invite = config.get("invite_power_level", 50)
|
||||||
return power_levels
|
return power_levels
|
||||||
|
|
||||||
|
|
||||||
@@ -186,7 +204,7 @@ def prepare_initial_state(
|
|||||||
initial_state.append({
|
initial_state.append({
|
||||||
"type": str(EventType.ROOM_HISTORY_VISIBILITY),
|
"type": str(EventType.ROOM_HISTORY_VISIBILITY),
|
||||||
"content": {
|
"content": {
|
||||||
"history_visibility": creation_content["m.room.history_visibility"]
|
"history_visibility": creation_content.get("m.room.history_visibility", "joined")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ async def validate_room_aliases(client, room_names: list[str], community_slug: s
|
|||||||
return len(conflicting_aliases) == 0, conflicting_aliases
|
return len(conflicting_aliases) == 0, conflicting_aliases
|
||||||
|
|
||||||
|
|
||||||
async def get_room_version_and_creators(client, room_id: str) -> Tuple[str, List[str]]:
|
async def get_room_version_and_creators(client, room_id: str, logger=None) -> Tuple[str, List[str]]:
|
||||||
"""Get the room version and creators for a room.
|
"""Get the room version and creators for a room.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -134,7 +134,7 @@ async def user_has_unlimited_power(client, user_id: str, room_id: str) -> bool:
|
|||||||
bool: True if user has unlimited power
|
bool: True if user has unlimited power
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
room_version, creators = await get_room_version_and_creators(client, room_id)
|
room_version, creators = await get_room_version_and_creators(client, room_id, None)
|
||||||
|
|
||||||
# In modern room versions (12+), creators have unlimited power
|
# In modern room versions (12+), creators have unlimited power
|
||||||
if is_modern_room_version(room_version):
|
if is_modern_room_version(room_version):
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Simple tests for space creation functionality."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from unittest.mock import Mock, AsyncMock
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpaceCreationSimple:
|
||||||
|
"""Simple tests for space creation functionality."""
|
||||||
|
|
||||||
|
def test_get_space_roomlist_empty_parent_room(self):
|
||||||
|
"""Test get_space_roomlist with empty parent room."""
|
||||||
|
from community.bot import CommunityBot
|
||||||
|
|
||||||
|
# Create a mock bot instance
|
||||||
|
bot = Mock(spec=CommunityBot)
|
||||||
|
bot.config = {"parent_room": ""}
|
||||||
|
bot.log = Mock()
|
||||||
|
bot.client = Mock()
|
||||||
|
|
||||||
|
# Mock the get_space_roomlist method
|
||||||
|
bot.get_space_roomlist = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
# Test that empty parent room returns empty list
|
||||||
|
import asyncio
|
||||||
|
result = asyncio.run(bot.get_space_roomlist())
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
def test_get_space_roomlist_with_parent_room(self):
|
||||||
|
"""Test get_space_roomlist with configured parent room."""
|
||||||
|
from community.bot import CommunityBot
|
||||||
|
|
||||||
|
# Create a mock bot instance
|
||||||
|
bot = Mock(spec=CommunityBot)
|
||||||
|
bot.config = {"parent_room": "!space:example.com"}
|
||||||
|
bot.log = Mock()
|
||||||
|
bot.client = Mock()
|
||||||
|
bot.client.get_state = AsyncMock(return_value=[])
|
||||||
|
|
||||||
|
# Mock the get_space_roomlist method
|
||||||
|
bot.get_space_roomlist = AsyncMock(return_value=["!room1:example.com", "!room2:example.com"])
|
||||||
|
|
||||||
|
# Test that configured parent room returns room list
|
||||||
|
import asyncio
|
||||||
|
result = asyncio.run(bot.get_space_roomlist())
|
||||||
|
assert result == ["!room1:example.com", "!room2:example.com"]
|
||||||
|
|
||||||
|
def test_space_creation_parameters(self):
|
||||||
|
"""Test that space creation parameters are correct."""
|
||||||
|
# Test that the space creation logic uses correct parameters
|
||||||
|
creation_content = {
|
||||||
|
"type": "m.space",
|
||||||
|
"m.federate": True,
|
||||||
|
"m.room.history_visibility": "joined"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify the creation content has the correct space type
|
||||||
|
assert creation_content["type"] == "m.space"
|
||||||
|
assert creation_content["m.federate"] is True
|
||||||
|
assert creation_content["m.room.history_visibility"] == "joined"
|
||||||
|
|
||||||
|
def test_power_level_verification_modern_room(self):
|
||||||
|
"""Test power level verification for modern room versions."""
|
||||||
|
# Test that modern room version verification logic is correct
|
||||||
|
room_version = "12"
|
||||||
|
is_modern = int(room_version) >= 12
|
||||||
|
|
||||||
|
assert is_modern is True
|
||||||
|
|
||||||
|
# For modern rooms, creators have unlimited power and don't appear in power levels
|
||||||
|
power_levels = {"users": {}}
|
||||||
|
bot_power_level = power_levels.get("users", {}).get("@bot:example.com")
|
||||||
|
|
||||||
|
# Bot should not have a power level in modern rooms (unlimited power)
|
||||||
|
assert bot_power_level is None
|
||||||
|
|
||||||
|
def test_power_level_verification_legacy_room(self):
|
||||||
|
"""Test power level verification for legacy room versions."""
|
||||||
|
# Test that legacy room version verification logic is correct
|
||||||
|
room_version = "1"
|
||||||
|
is_modern = int(room_version) >= 12
|
||||||
|
|
||||||
|
assert is_modern is False
|
||||||
|
|
||||||
|
# For legacy rooms, bot should have power level 1000
|
||||||
|
power_levels = {"users": {"@bot:example.com": 1000}}
|
||||||
|
bot_power_level = power_levels.get("users", {}).get("@bot:example.com")
|
||||||
|
|
||||||
|
assert bot_power_level == 1000
|
||||||
|
|
||||||
|
def test_space_type_verification(self):
|
||||||
|
"""Test space type verification logic."""
|
||||||
|
# Mock state events
|
||||||
|
state_events = [
|
||||||
|
Mock(type="m.room.create", content={"type": "m.space"}),
|
||||||
|
Mock(type="m.room.power_levels", content={})
|
||||||
|
]
|
||||||
|
|
||||||
|
# Find the room create event
|
||||||
|
space_type_set = False
|
||||||
|
for event in state_events:
|
||||||
|
if event.type == "m.room.create":
|
||||||
|
space_type = event.content.get("type")
|
||||||
|
space_type_set = (space_type == "m.space")
|
||||||
|
break
|
||||||
|
|
||||||
|
assert space_type_set is True
|
||||||
|
|
||||||
|
def test_space_type_not_set(self):
|
||||||
|
"""Test space type verification when type is not set."""
|
||||||
|
# Mock state events with wrong type
|
||||||
|
state_events = [
|
||||||
|
Mock(type="m.room.create", content={"type": "m.room"}),
|
||||||
|
Mock(type="m.room.power_levels", content={})
|
||||||
|
]
|
||||||
|
|
||||||
|
# Find the room create event
|
||||||
|
space_type_set = False
|
||||||
|
for event in state_events:
|
||||||
|
if event.type == "m.room.create":
|
||||||
|
space_type = event.content.get("type")
|
||||||
|
space_type_set = (space_type == "m.space")
|
||||||
|
break
|
||||||
|
|
||||||
|
assert space_type_set is False
|
||||||
Reference in New Issue
Block a user