From 97cd4aeb2f26edc18263e73f428b5848d252a538 Mon Sep 17 00:00:00 2001 From: Kumi Date: Fri, 8 Nov 2024 07:52:09 +0100 Subject: [PATCH] feat: rename and extend bot for application handling Renamed the bot from "SupportBot" to "ApplicationBot" to reflect expanded functionality including room application management. Updated README to specify the fork's purpose and extended the bot with features like automated ticket creation for new members, application processing, and command handling for room monitoring and user access management. This enhances the bot's use case by supporting room access applications alongside support tickets. --- README.md | 14 +- .../__init__.py | 0 .../classes/__init__.py | 0 .../classes/bot.py | 218 +++++++++++++++++- .../main.py | 4 +- 5 files changed, 225 insertions(+), 11 deletions(-) rename src/{matrix_supportbot => matrix_applicationbot}/__init__.py (100%) rename src/{matrix_supportbot => matrix_applicationbot}/classes/__init__.py (100%) rename src/{matrix_supportbot => matrix_applicationbot}/classes/bot.py (53%) rename src/{matrix_supportbot => matrix_applicationbot}/main.py (84%) diff --git a/README.md b/README.md index 78f3154..be87abb 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ -# Matrix Support Bot +# Matrix Application Bot [![Support Private.coffee!](https://shields.private.coffee/badge/private.coffee-support%20us!-pink?logo=coffeescript)](https://private.coffee) [![Matrix](https://shields.private.coffee/badge/Matrix-join%20us!-blue?logo=matrix)](https://matrix.to/#/#matrix-supportbot:private.coffee) -[![PyPI](https://shields.private.coffee/pypi/v/matrix-supportbot)](https://pypi.org/project/matrix-supportbot/) -[![PyPI - Python Version](https://shields.private.coffee/pypi/pyversions/matrix-supportbot)](https://pypi.org/project/matrix-supportbot/) -[![PyPI - License](https://shields.private.coffee/pypi/l/matrix-supportbot)](https://pypi.org/project/matrix-supportbot/) -[![Latest Git Commit](https://shields.private.coffee/gitea/last-commit/privatecoffee/matrix-supportbot?gitea_url=https://git.private.coffee)](https://git.private.coffee/privatecoffee/matrix-supportbot) +[![PyPI](https://shields.private.coffee/pypi/v/matrix-applicationbot)](https://pypi.org/project/matrix-applicationbot/) +[![PyPI - Python Version](https://shields.private.coffee/pypi/pyversions/matrix-applicationbot)](https://pypi.org/project/matrix-applicationbot/) +[![PyPI - License](https://shields.private.coffee/pypi/l/matrix-applicationbot)](https://pypi.org/project/matrix-applicationbot/) +[![Latest Git Commit](https://shields.private.coffee/gitea/last-commit/privatecoffee/matrix-applicationbot?gitea_url=https://git.private.coffee)](https://git.private.coffee/privatecoffee/matrix-applicationbot) -Matrix Support Bot is a support ticket bot for the Matrix protocol built using the `matrix-nio` library. The bot allows users to open support tickets and communicate with support operators in a structured manner. Operators can manage tickets and relay messages between customer-facing and operator-facing rooms. +*Note:* This project is a fork of [matrix-supportbot](https://git.private.coffee/privatecoffee/matrix-supportbot) to extend its functionality by handling room join applications. This README needs to be updated to reflect the changes. + +Matrix Application Bot is a support ticket bot for the Matrix protocol built using the `matrix-nio` library. The bot allows users to open support tickets and communicate with support operators in a structured manner. Operators can manage tickets and relay messages between customer-facing and operator-facing rooms. ## Features diff --git a/src/matrix_supportbot/__init__.py b/src/matrix_applicationbot/__init__.py similarity index 100% rename from src/matrix_supportbot/__init__.py rename to src/matrix_applicationbot/__init__.py diff --git a/src/matrix_supportbot/classes/__init__.py b/src/matrix_applicationbot/classes/__init__.py similarity index 100% rename from src/matrix_supportbot/classes/__init__.py rename to src/matrix_applicationbot/classes/__init__.py diff --git a/src/matrix_supportbot/classes/bot.py b/src/matrix_applicationbot/classes/bot.py similarity index 53% rename from src/matrix_supportbot/classes/bot.py rename to src/matrix_applicationbot/classes/bot.py index 01fd3b1..20b9e74 100644 --- a/src/matrix_supportbot/classes/bot.py +++ b/src/matrix_applicationbot/classes/bot.py @@ -9,12 +9,15 @@ from nio import ( InviteMemberEvent, JoinedMembersResponse, RoomPutStateResponse, + RoomMemberEvent, + RoomKickResponse, + RoomInviteResponse, ) import logging -class SupportBot: +class ApplicationBot: def __init__(self, config): self.client = AsyncClient(config["homeserver"], config["username"]) self.username = config["username"] @@ -48,6 +51,7 @@ class SupportBot: self.client.add_event_callback(self.message_callback, RoomMessageText) self.client.add_event_callback(self.message_callback, RoomMessageMedia) self.client.add_event_callback(self.invite_callback, InviteMemberEvent) + self.client.add_event_callback(self.member_event_callback, RoomMemberEvent) await self.client.sync_forever(timeout=30000) async def message_callback(self, room: MatrixRoom, event): @@ -56,6 +60,8 @@ class SupportBot: if body and body.startswith("!supportbot"): await self.handle_command(room, sender, body) + elif body and body.startswith("!apply"): + await self.process_application(room, sender, body) else: await self.relay_message(room, sender, event) @@ -72,18 +78,42 @@ class SupportBot: "body": "Hello! To open a support ticket, please type `!supportbot openticket`.", }, ) + + async def member_event_callback(self, room: MatrixRoom, event: RoomMemberEvent): + # Check if the room is monitored + monitored_rooms = await self.get_monitored_rooms() + if room.room_id in monitored_rooms and event.membership == "join": + # Automatically create a ticket for the new member + await self.open_ticket(room, event.state_key) async def handle_command(self, room, sender, command): - if command == "!supportbot openticket": + if command == "!supportbot help": + await self.help_command(room, sender) + elif command == "!supportbot openticket": await self.open_ticket(room, sender) - return elif await self.is_operator(sender): if command.startswith("!supportbot invite"): await self.invite_operator(room, sender, command) elif command.startswith("!supportbot close"): await self.close_ticket(room, sender, command) + elif command.startswith("!supportbot approve"): + await self.approve_or_reject_application(room, sender, command, approved=True) + elif command.startswith("!supportbot reject"): + await self.approve_or_reject_application(room, sender, command, approved=False) elif command == "!supportbot list": await self.list_tickets(room) + elif command.startswith("!supportbot monitor"): + await self.add_monitored_room(command.split()[2]) + elif command.startswith("!supportbot unmonitor"): + await self.remove_monitored_room(command.split()[2]) + elif command.startswith("!supportbot monitored"): + await self.list_monitored_rooms(room) + elif command.startswith("!supportbot protect"): + await self.add_protected_room(command.split()[2]) + elif command.startswith("!supportbot unprotect"): + await self.remove_protected_room(command.split()[2]) + elif command.startswith("!supportbot protected"): + await self.list_protected_rooms(room) return await self.client.room_send( @@ -95,6 +125,187 @@ class SupportBot: }, ) + async def process_application(self, room: MatrixRoom, sender, message_body): + # Check if the message is from a ticket room + ticket_id = await self.get_ticket_id_from_room(room.room_id) + if ticket_id: + # Message format + formatted_message = f"Application received from {sender} in Ticket #{ticket_id}: {message_body}" + # Send the message to the operator room + await self.client.room_send( + self.operator_room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": formatted_message, + }, + ) + + await self.relay_message(room, sender, message_body) + + async def approve_or_reject_application(self, room, sender, command, approved): + user_id = command.split()[2] if len(command.split()) > 2 else await self.get_user_id_from_ticket_room(room.room_id) + + if not user_id: + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": "No user found or specified for the operation.", + }, + ) + return + + if approved: + # Invite to all protected rooms + protected_rooms = await self.get_protected_rooms() + for protected_room in protected_rooms: + invite_response = await self.client.room_invite(protected_room, user_id) + if not isinstance(invite_response, RoomInviteResponse): + logging.error(f"Failed to invite user {user_id} to protected room {protected_room}: {invite_response}") + + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": f"User {user_id} has been approved and invited to protected rooms.", + }, + ) + else: + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": f"User {user_id} has been rejected.", + }, + ) + + # Kick user from all monitored rooms + monitored_rooms = await self.get_monitored_rooms() + for monitored_room in monitored_rooms: + kick_response = await self.client.room_kick(monitored_room, user_id) + if not isinstance(kick_response, RoomKickResponse): + logging.error(f"Failed to kick user {user_id} from monitored room {monitored_room}: {kick_response}") + + async def get_user_id_from_ticket_room(self, room_id): + # Find the user associated with the ticket id based on the room id + response = await self.client.room_get_state(self.operator_room_id) + for event in response.events: + if event["type"] == "m.room.custom.ticket": + ticket_info = event["content"] + if ( + ticket_info["customer_room"] == room_id + or ticket_info["operator_room"] == room_id + ): + # Return the user from the customer_room + return ticket_info.get("customer_user_id") + return None + + async def add_monitored_room(self, room_id): + monitored_rooms = await self.get_monitored_rooms() + if room_id not in monitored_rooms: + monitored_rooms.append(room_id) + await self.set_monitored_rooms(monitored_rooms) + logging.info(f"Added room {room_id} to monitored rooms") + + async def remove_monitored_room(self, room_id): + monitored_rooms = await self.get_monitored_rooms() + if room_id in monitored_rooms: + monitored_rooms.remove(room_id) + await self.set_monitored_rooms(monitored_rooms) + logging.info(f"Removed room {room_id} from monitored rooms") + + async def get_monitored_rooms(self): + response = await self.client.room_get_state_event( + self.operator_room_id, "m.room.monitored" + ) + if isinstance(response, RoomGetStateEventResponse): + return response.content.get("rooms", []) + return [] + + async def set_monitored_rooms(self, rooms): + await self.client.room_put_state( + room_id=self.operator_room_id, + event_type="m.room.monitored", + content={"rooms": rooms} + ) + + async def list_monitored_rooms(self, room): + monitored_rooms = await self.get_monitored_rooms() + if monitored_rooms: + rooms_list = ', '.join(monitored_rooms) + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": f"Monitored rooms: {rooms_list}", + }, + ) + else: + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": "No monitored rooms currently.", + }, + ) + + async def add_protected_room(self, room_id): + protected_rooms = await self.get_protected_rooms() + if room_id not in protected_rooms: + protected_rooms.append(room_id) + await self.set_protected_rooms(protected_rooms) + logging.info(f"Added room {room_id} to protected rooms") + + async def remove_protected_room(self, room_id): + protected_rooms = await self.get_protected_rooms() + if room_id in protected_rooms: + protected_rooms.remove(room_id) + await self.set_protected_rooms(protected_rooms) + logging.info(f"Removed room {room_id} from protected rooms") + + async def get_protected_rooms(self): + response = await self.client.room_get_state_event( + self.operator_room_id, "m.room.protected" + ) + if isinstance(response, RoomGetStateEventResponse): + return response.content.get("rooms", []) + return [] + + async def set_protected_rooms(self, rooms): + await self.client.room_put_state( + room_id=self.operator_room_id, + event_type="m.room.protected", + content={"rooms": rooms} + ) + + async def list_protected_rooms(self, room): + protected_rooms = await self.get_protected_rooms() + if protected_rooms: + rooms_list = ', '.join(protected_rooms) + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": f"Protected rooms: {rooms_list}", + }, + ) + else: + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": "No protected rooms currently.", + }, + ) + def generate_ticket_id(self): return str(random.randint(10000000, 99999999)) @@ -119,6 +330,7 @@ class SupportBot: "ticket_id": ticket_id, "customer_room": customer_room_id, "operator_room": operator_room_id, + "customer_user_id": sender, "status": "open", } diff --git a/src/matrix_supportbot/main.py b/src/matrix_applicationbot/main.py similarity index 84% rename from src/matrix_supportbot/main.py rename to src/matrix_applicationbot/main.py index 7f24d46..5d88027 100644 --- a/src/matrix_supportbot/main.py +++ b/src/matrix_applicationbot/main.py @@ -3,7 +3,7 @@ import yaml import logging import asyncio -from .classes.bot import SupportBot +from .classes.bot import ApplicationBot def load_config(config_file): with open(config_file, 'r') as file: @@ -11,7 +11,7 @@ def load_config(config_file): def main(): config = load_config("config.yaml") - bot = SupportBot(config) + bot = ApplicationBot(config) logging.basicConfig(level=logging.DEBUG) logging.getLogger("nio").setLevel(logging.WARNING)