diff --git a/matrix_keycloak_bot.py b/matrix_keycloak_bot.py index 6faa808..3cdb639 100644 --- a/matrix_keycloak_bot.py +++ b/matrix_keycloak_bot.py @@ -49,6 +49,25 @@ def get_keycloak_client() -> KeycloakAdmin: verify=True, ) +def get_requestable_groups() -> tuple[bool, str]: + """Fetches available Keycloak groups, filtering out the 'admin' group.""" + try: + kc = get_keycloak_client() + groups = kc.get_groups() + + # Filter out 'admin' (case-insensitive) + valid_groups = [g["name"] for g in groups if g["name"].lower() != "admin"] + + if not valid_groups: + return True, "No groups are currently available to request." + + group_list = "\n".join([f"• `{name}`" for name in valid_groups]) + return True, f"**Available Groups:**\n{group_list}" + + except KeycloakError as e: + return False, f"Keycloak API error: {str(e)}" + except Exception as e: + return False, f"Unexpected error: {str(e)}" def add_user_to_kc_group(username: str, group_name: str) -> tuple[bool, str]: """Queries Keycloak for the user and group UUIDs and adds the user to the group.""" @@ -100,12 +119,29 @@ async def on_invite(client: AsyncClient, room: MatrixRoom, event: InviteMemberEv async def on_message(client: AsyncClient, room: MatrixRoom, event: RoomMessageText): - """Listens for `!request ` commands.""" + """Listens for commands.""" # Ignore messages sent by the bot itself if event.sender == client.user_id: return body = event.body.strip() + + # --- COMMAND: !groups --- + if body.startswith("!groups"): + success, msg = get_requestable_groups() + await send_message_safe( + client, + room_id=room.room_id, + content={ + "msgtype": "m.notice" if not success else "m.text", + "body": msg, + "format": "org.matrix.custom.html", + "formatted_body": msg.replace("\n", "
") + }, + ) + return + + # --- COMMAND: !request --- if not body.startswith("!request"): return @@ -114,17 +150,27 @@ async def on_message(client: AsyncClient, room: MatrixRoom, event: RoomMessageTe await send_message_safe( client, room_id=room.room_id, - message_type="m.room.message", - content={"msgtype": "m.notice", "body": "Usage: !request "}, + content={"msgtype": "m.notice", "body": "Usage: !request or !groups"}, ) return requested_group = parts[1].strip() + + # Block 'admin' requests immediately + if requested_group.lower() == "admin": + await send_message_safe( + client, + room_id=room.room_id, + content={ + "msgtype": "m.notice", + "body": "❌ Requests for the 'admin' group are not permitted." + }, + ) + return + matrix_user = event.sender - # Extract username (e.g., "@alice:nest.local" -> "alice") kc_username = matrix_user.split(":")[0].lstrip("@") - # Send notification message to the private Admin Room admin_msg_body = ( f"📋 **Access Request**\n" f"**User:** `{matrix_user}` (KC Username: `{kc_username}`)\n" @@ -144,7 +190,6 @@ async def on_message(client: AsyncClient, room: MatrixRoom, event: RoomMessageTe ) if isinstance(res, RoomSendResponse): - # Store pending request keyed by the admin message event_id pending_requests[res.event_id] = { "matrix_user": matrix_user, "kc_username": kc_username, @@ -152,7 +197,6 @@ async def on_message(client: AsyncClient, room: MatrixRoom, event: RoomMessageTe "origin_room_id": room.room_id, } - # Confirm request receipt to the user await send_message_safe( client, room_id=room.room_id, @@ -245,15 +289,26 @@ async def on_reaction(client: AsyncClient, room: MatrixRoom, event: ReactionEven return # Ignore other reactions # Update the original admin room message to reflect resolution status + fallback_text = f"* {status_text}" + await send_message_safe( client, room_id=ADMIN_ROOM_ID, content={ "msgtype": "m.text", - "body": status_text, + "body": fallback_text, "format": "org.matrix.custom.html", - "formatted_body": status_text.replace("\n", "
"), - "m.relates_to": {"rel_type": "m.replace", "event_id": target_event_id}, + "formatted_body": fallback_text.replace("\n", "
"), + "m.new_content": { + "msgtype": "m.text", + "body": status_text, + "format": "org.matrix.custom.html", + "formatted_body": status_text.replace("\n", "
"), + }, + "m.relates_to": { + "rel_type": "m.replace", + "event_id": target_event_id + }, }, )