diff --git a/community/bot.py b/community/bot.py index e0e4bbe..2f3ccbe 100644 --- a/community/bot.py +++ b/community/bot.py @@ -150,14 +150,14 @@ class CommunityBot(Plugin): Returns: tuple: (is_valid, list_of_conflicting_aliases) """ - if not self.config["community_slug"]: + if not self.config.get("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] 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]: @@ -166,7 +166,7 @@ class CommunityBot(Plugin): Returns: 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]: """Create a new space without community slug suffix. @@ -182,7 +182,7 @@ class CommunityBot(Plugin): mymsg = None try: 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] # Validate that the space alias is available @@ -196,25 +196,28 @@ class CommunityBot(Plugin): if evt: 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 + # Spaces are created by setting the type to "m.space" in 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 # 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: - self.log.info(f"Modern room version {self.config['room_version']} detected - removing bot from power levels") + 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.get('room_version', '1')} detected - removing bot from power levels") if power_level_override.users: # Remove bot from users list but keep other important settings power_level_override.users.pop(self.client.mxid, None) # Create the space with space-specific 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"Calling client.create_room with parameters:") 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" - power_level_override: {power_level_override}") 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( alias_localpart=sanitized_name, @@ -230,15 +233,15 @@ class CommunityBot(Plugin): invitees=invitees, power_level_override=power_level_override, 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 try: 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']})") - if actual_version != self.config["room_version"]: - self.log.warning(f"Space version mismatch: requested {self.config['room_version']}, got {actual_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.get("room_version", "1"): + self.log.warning(f"Space version mismatch: requested {self.config.get('room_version', '1')}, got {actual_version}") # Verify the space type was set state_events = await self.client.get_state(space_id) @@ -277,6 +280,7 @@ class CommunityBot(Plugin): allow_html=True ) + self.log.info(f"Space creation completed successfully: {space_id}") return space_id, f"#{sanitized_name}:{server}" except Exception as e: @@ -356,9 +360,15 @@ class CommunityBot(Plugin): return results - async def get_space_roomlist(self) -> None: + async def get_space_roomlist(self) -> list[str]: space = self.config["parent_room"] rooms = [] + + # Check if parent room is configured + if not space: + self.log.warning("No parent room configured, cannot get space roomlist") + return rooms + try: self.log.debug(f"DEBUG getting roomlist from {space} space") state = await self.client.get_state(space) @@ -1233,11 +1243,12 @@ class CommunityBot(Plugin): await evt.react("✅") @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_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(): await evt.reply("user tracking is disabled") return @@ -1255,13 +1266,37 @@ class CommunityBot(Plugin): allow_html=True, ) - @community.subcommand( - "inactive", help="generate a list of mxids who have been inactive" + @report.subcommand( + "all", help="generate a full report of all user activity status" ) @decorators.require_parent_room @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"

Users inactive for between {self.config['warn_threshold_days']} and \ + {self.config['kick_threshold_days']} days:
\ + {'
'.join(report['warn_inactive'])}

\ +

Users inactive for at least {self.config['kick_threshold_days']} days:
\ + {'
'.join(report['kick_inactive'])}

\ +

Ignored users:
\ + {'
'.join(report['ignored'])}

", + 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(): await evt.reply("user tracking is disabled") return @@ -1275,16 +1310,13 @@ class CommunityBot(Plugin): allow_html=True, ) - @community.subcommand( - "purgable", help="generate a list of matrix IDs that have been inactive long enough to be purged" + @report.subcommand( + "purgable", help="generate a list of users that would be kicked with the purge command" ) - async def get_purgable_report(self, evt: MessageEvent) -> None: - if not await self.check_parent_room(evt): - return - if not await self.user_permitted(evt.sender): - await evt.reply("You don't have permission to use this command") - return - + @decorators.require_parent_room + @decorators.require_permission() + async def report_purgable(self, evt: MessageEvent) -> None: + """Report users who are inactive long enough to be purged""" if not self.config_manager.is_tracking_enabled(): await evt.reply("user tracking is disabled") return @@ -1297,16 +1329,13 @@ class CommunityBot(Plugin): allow_html=True, ) - @community.subcommand( - "ignored", help="generate a list of matrix IDs that have activity tracking disabled" + @report.subcommand( + "ignored", help="generate a list of users that have activity tracking disabled" ) - async def get_ignored_report(self, evt: MessageEvent) -> None: - if not await self.check_parent_room(evt): - return - if not await self.user_permitted(evt.sender): - await evt.reply("You don't have permission to use this command") - return - + @decorators.require_parent_room + @decorators.require_permission() + async def report_ignored(self, evt: MessageEvent) -> None: + """Report users who are ignored for activity tracking""" if not self.config_manager.is_tracking_enabled(): await evt.reply("user tracking is disabled") return @@ -1532,7 +1561,7 @@ class CommunityBot(Plugin): ) 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. Args: @@ -1548,7 +1577,7 @@ class CommunityBot(Plugin): mymsg = None try: # 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 ) if error_msg: @@ -1572,9 +1601,14 @@ class CommunityBot(Plugin): return None # Prepare power levels - power_levels = await room_creation_utils.prepare_power_levels( - self.client, self.config, parent_room, power_level_override - ) + try: + 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 power_levels = room_creation_utils.adjust_power_levels_for_modern_rooms( @@ -1603,15 +1637,20 @@ class CommunityBot(Plugin): else: self.log.info("No power level override") - room_id = await self.client.create_room( - alias_localpart=alias_localpart, - name=roomname, - invitees=room_invitees, - initial_state=initial_state, - power_level_override=power_levels, - creation_content=creation_content, - room_version=self.config["room_version"] - ) + try: + room_id = await self.client.create_room( + alias_localpart=alias_localpart, + name=cleaned_roomname, + invitees=room_invitees, + initial_state=initial_state, + power_level_override=power_levels, + 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 await room_creation_utils.verify_room_creation( @@ -2685,20 +2724,24 @@ class CommunityBot(Plugin): # Set up power levels for the space power_levels = PowerLevelStateEventContent() - # For modern room versions (12+), don't set power levels for creators - # as they have unlimited power by default - if self.is_modern_room_version(self.config["room_version"]): - # Don't set any user power levels for modern versions - # Creators have unlimited power by default - power_levels.users = {} + # Set up power levels for users + # For modern room versions (12+), the bot (creator) has unlimited power by default + # but we still need to set power levels for other users + if self.is_modern_room_version(self.config.get("room_version", "1")): + # For modern rooms, don't set bot power level (it has unlimited power) + # but still set power levels for other users + power_levels.users = { + evt.sender: 100 # Initiator gets admin power + } else: + # For legacy rooms, set both bot and initiator power levels power_levels.users = { self.client.mxid: 1000, # Bot gets highest power evt.sender: 100 # Initiator gets admin power } # 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 space_id, space_alias = await self.create_space( @@ -2713,15 +2756,27 @@ class CommunityBot(Plugin): # Set the space as the parent room in config self.config["parent_room"] = space_id + self.log.info(f"Set parent_room to: {space_id}") # Save the updated config self.config.save() + self.log.info("Config saved successfully") # Verify the space exists and has correct power levels try: 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: error_msg = f"Failed to verify space setup: {e}" self.log.error(error_msg) @@ -2729,21 +2784,35 @@ class CommunityBot(Plugin): return # Create moderators room - # Get moderators and above from the space instead of using config invitees - moderators = await self.get_moderators_and_above() - 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") + # Include the initiator as a moderator, plus any other moderators from the space + moderators = [evt.sender] # Always include the initiator - 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", evt, 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 await self.client.send_state_event( @@ -2752,8 +2821,8 @@ class CommunityBot(Plugin): JoinRulesStateEventContent(join_rule=JoinRule.INVITE) ) - # Create waiting room - waiting_room_id, waiting_room_alias = await self.create_room( + # Create waiting room (force unencrypted for public access) + waiting_room_result = await self.create_room( f"{community_name} Waiting Room --unencrypted", evt, creation_content={ @@ -2762,9 +2831,13 @@ class CommunityBot(Plugin): } ) - if not waiting_room_id: - await evt.respond("Failed to create waiting room", edits=msg) + if not waiting_room_result: + error_msg = "Failed to create waiting room" + self.log.error(error_msg) + await evt.respond(error_msg, edits=msg) return + + waiting_room_id, waiting_room_alias = waiting_room_result # Set waiting room to be joinable by anyone await self.client.send_state_event( diff --git a/community/helpers/diagnostic_utils.py b/community/helpers/diagnostic_utils.py index c39050d..2381bed 100644 --- a/community/helpers/diagnostic_utils.py +++ b/community/helpers/diagnostic_utils.py @@ -26,10 +26,15 @@ async def check_space_permissions( ) 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 = { "room_id": parent_room, "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_equal": [], "users_higher": [] @@ -196,7 +201,12 @@ def generate_space_summary( space_status = "✅" if space_data.get("has_admin", False) else "❌" response = f"

📋 Parent Space


" - response += f"{space_status} Administrative privileges: {'Yes' if space_data['has_admin'] else 'No'} (level: {space_data['bot_power_level']})
" + + # Show admin status with appropriate details + if space_data.get("bot_has_unlimited_power", False): + response += f"{space_status} Administrative privileges: Yes (unlimited power - creator)
" + else: + response += f"{space_status} Administrative privileges: {'Yes' if space_data['has_admin'] else 'No'} (level: {space_data['bot_power_level']})
" if space_data.get("users_higher"): response += f"⚠️ Users with higher power: {', '.join([f'{u['user']} ({u['level']})' for u in space_data['users_higher']])}
" diff --git a/community/helpers/report_utils.py b/community/helpers/report_utils.py index d020f58..36d723b 100644 --- a/community/helpers/report_utils.py +++ b/community/helpers/report_utils.py @@ -8,20 +8,20 @@ def generate_activity_report(database_results: Dict[str, List[Dict]]) -> Dict[st """Generate an activity report from database results. Args: - database_results: Dictionary containing 'active', 'inactive', 'ignored' results + database_results: Dictionary containing 'warn_inactive', 'kick_inactive', 'ignored' results Returns: dict: Formatted activity report """ report = {} - # Process active users - active_results = database_results.get("active", []) - report["active"] = [row["mxid"] for row in active_results] or ["none"] + # Process warn inactive users (between warn and kick thresholds) + warn_inactive_results = database_results.get("warn_inactive", []) + report["warn_inactive"] = [row["mxid"] for row in warn_inactive_results] or ["none"] - # Process inactive users - inactive_results = database_results.get("inactive", []) - report["inactive"] = [row["mxid"] for row in inactive_results] or ["none"] + # Process kick inactive users (beyond kick threshold) + kick_inactive_results = database_results.get("kick_inactive", []) + report["kick_inactive"] = [row["mxid"] for row in kick_inactive_results] or ["none"] # Process ignored users ignored_results = database_results.get("ignored", []) diff --git a/community/helpers/room_creation_utils.py b/community/helpers/room_creation_utils.py index b6b7cbe..5e79a26 100644 --- a/community/helpers/room_creation_utils.py +++ b/community/helpers/room_creation_utils.py @@ -22,26 +22,29 @@ async def validate_room_creation_params( Returns: Tuple of (sanitized_name, force_encryption, force_unencryption, error_msg) """ - # Check for encryption flags - encrypted_flag_regex = re.compile(r"(\s+|^)-+encrypt(ed)?\s?") - unencrypted_flag_regex = re.compile(r"(\s+|^)-+unencrypt(ed)?\s?") + # Check for encryption flags (at beginning, middle, or end of string) + encrypted_flag_regex = re.compile(r"(\s+|^)-+encrypt(ed)?(\s+|$)") + unencrypted_flag_regex = re.compile(r"(\s+|^)-+unencrypt(ed)?(\s+|$)") force_encryption = bool(encrypted_flag_regex.search(roomname)) force_unencryption = bool(unencrypted_flag_regex.search(roomname)) # Clean up room name if force_encryption: - roomname = encrypted_flag_regex.sub("", roomname) + roomname = encrypted_flag_regex.sub("", roomname) # Remove encryption flag 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() # 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." - 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( @@ -62,12 +65,12 @@ async def prepare_room_creation_data( Tuple of (alias_localpart, server, room_invitees, parent_room) """ # 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 server = client.parse_user_id(client.mxid)[1] - room_invitees = invitees if invitees is not None else config["invitees"] - parent_room = config["parent_room"] + room_invitees = invitees if invitees is not None else config.get("invitees", []) + parent_room = config.get("parent_room", "") return alias_localpart, server, room_invitees, parent_room @@ -93,36 +96,51 @@ async def prepare_power_levels( return power_level_override if parent_room: - # Get parent room power levels to extract user 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 - power_levels = PowerLevelStateEventContent() - - # Copy only user power levels from parent space, not the entire permission set - if parent_power_levels.users: - user_power_levels = parent_power_levels.users.copy() - # Ensure bot has highest power - user_power_levels[client.mxid] = 1000 - power_levels.users = user_power_levels - else: + try: + # Get parent room power levels to extract user 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 + power_levels = PowerLevelStateEventContent() + + # Copy only user power levels from parent space, not the entire permission set + if parent_power_levels and hasattr(parent_power_levels, 'users') and parent_power_levels.users: + try: + user_power_levels = parent_power_levels.users.copy() + # Ensure bot has highest power + user_power_levels[client.mxid] = 1000 + 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 = { client.mxid: 1000, # Bot gets highest power } - - # Set explicit config values - power_levels.invite = config["invite_power_level"] - - return power_levels + power_levels.invite = config.get("invite_power_level", 50) + return power_levels else: # If no parent room, create default power levels power_levels = PowerLevelStateEventContent() power_levels.users = { 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 @@ -186,7 +204,7 @@ def prepare_initial_state( initial_state.append({ "type": str(EventType.ROOM_HISTORY_VISIBILITY), "content": { - "history_visibility": creation_content["m.room.history_visibility"] + "history_visibility": creation_content.get("m.room.history_visibility", "joined") } }) diff --git a/community/helpers/room_utils.py b/community/helpers/room_utils.py index 9975776..f77644a 100644 --- a/community/helpers/room_utils.py +++ b/community/helpers/room_utils.py @@ -61,7 +61,7 @@ async def validate_room_aliases(client, room_names: list[str], community_slug: s 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. 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 """ 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 if is_modern_room_version(room_version): diff --git a/tests/test_space_creation_simple.py b/tests/test_space_creation_simple.py new file mode 100644 index 0000000..c60424d --- /dev/null +++ b/tests/test_space_creation_simple.py @@ -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