diff --git a/pyproject.toml b/pyproject.toml index 0a9e7fb..9b285e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ allow-direct-references = true [project] name = "matrix-supportbot" -version = "0.1.0" +version = "0.1.1" authors = [{ name = "Private.coffee Team", email = "support@private.coffee" }] @@ -26,6 +26,7 @@ classifiers = [ dependencies = [ "matrix-nio>=0.24.0", + "pyyaml" ] [project.urls] diff --git a/src/matrix_supportbot/classes/bot.py b/src/matrix_supportbot/classes/bot.py index 0f245df..a5e092d 100644 --- a/src/matrix_supportbot/classes/bot.py +++ b/src/matrix_supportbot/classes/bot.py @@ -6,33 +6,48 @@ from nio import ( RoomMessageMedia, LoginResponse, RoomGetStateEventResponse, - RoomMemberEvent, + InviteMemberEvent, + JoinedMembersResponse, + RoomPutStateResponse, ) +import logging + class SupportBot: def __init__(self, config): - self.client = AsyncClient(config["homeserver"]) + self.client = AsyncClient(config["homeserver"], config["username"]) self.username = config["username"] self.password = config["password"] self.operator_room_id = config["operator_room_id"] async def login(self): - response = await self.client.login(self.username, self.password) + response = await self.client.login(self.password) if isinstance(response, LoginResponse): - print("Logged in successfully") + logging.info("Logged in successfully") else: - print("Failed to log in") + logging.fatal("Failed to log in") async def is_operator(self, user_id): + logging.info(f"Checking if {user_id} is an operator") + response = await self.client.joined_members(self.operator_room_id) - if isinstance(response, RoomMemberEvent): - return user_id in response.members + + if not isinstance(response, JoinedMembersResponse): + logging.error(f"Failed to get members in operator room: {response}") + return False + + for member in response.members: + if member.user_id == user_id: + return True + return False async def start(self): + await self.client.sync(timeout=30000) 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) await self.client.sync_forever(timeout=30000) async def message_callback(self, room: MatrixRoom, event): @@ -44,9 +59,24 @@ class SupportBot: else: await self.relay_message(room, sender, event) + async def invite_callback(self, room: MatrixRoom, event: InviteMemberEvent): + logging.info(f"Received invite event: {event}") + + if event.membership == "invite" and event.state_key == self.client.user_id: + await self.client.join(room.room_id) + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": "Hello! To open a support ticket, please type `!supportbot openticket`.", + }, + ) + async def handle_command(self, room, sender, command): if 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) @@ -54,15 +84,16 @@ class SupportBot: await self.close_ticket(room, sender, command) elif command == "!supportbot list": await self.list_tickets(room) - else: - await self.client.room_send( - room.room_id, - "m.room.message", - { - "msgtype": "m.text", - "body": "You are not authorized to use this command.", - }, - ) + return + + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": "Sorry, I do not know this command, or you are not authorized to use it.", + }, + ) def generate_ticket_id(self): return str(random.randint(10000000, 99999999)) @@ -91,13 +122,19 @@ class SupportBot: "status": "open", } - await self.client.room_put_state( + state_event_response = await self.client.room_put_state( room_id=self.operator_room_id, event_type="m.room.custom.ticket", state_key=state_event_key, content=state_event_content, ) + if not isinstance(state_event_response, RoomPutStateResponse): + logging.error( + f"Failed to update state in operator room: {state_event_response}" + ) + return + # Inform the operator room await self.client.room_send( self.operator_room_id, @@ -118,6 +155,15 @@ class SupportBot: }, ) + await self.client.room_send( + room.room_id, + "m.room.message", + { + "msgtype": "m.text", + "body": f"Ticket #{ticket_id} has been created. Please check your DMs.", + }, + ) + async def invite_operator(self, room, sender, command): ticket_id = command.split()[2] state_event_key = f"ticket_{ticket_id}" @@ -164,7 +210,10 @@ class SupportBot: await self.client.room_send( customer_room_id, "m.room.message", - {"msgtype": "m.text", "body": f"Ticket #{ticket_id} has been closed."}, + { + "msgtype": "m.text", + "body": f"Ticket #{ticket_id} has been closed. If you need further assistance, please open a new ticket.", + }, ) await self.client.room_send( operator_room_id, @@ -212,6 +261,10 @@ class SupportBot: return None async def relay_message(self, room, sender, event): + # Ignore any messages from the bot itself + if sender == self.client.user_id: + return + ticket_id = await self.get_ticket_id_from_room(room.room_id) if ticket_id: state_event_key = f"ticket_{ticket_id}" @@ -229,5 +282,5 @@ class SupportBot: # Relay the entire event content to the target room await self.client.room_send( - target_room_id, event.type, event.source["content"] + target_room_id, "m.room.message", event.source["content"] ) diff --git a/src/matrix_supportbot/main.py b/src/matrix_supportbot/main.py index 8515d7a..7f24d46 100644 --- a/src/matrix_supportbot/main.py +++ b/src/matrix_supportbot/main.py @@ -1,8 +1,9 @@ import yaml +import logging import asyncio -from .bot import SupportBot +from .classes.bot import SupportBot def load_config(config_file): with open(config_file, 'r') as file: @@ -11,6 +12,10 @@ def load_config(config_file): def main(): config = load_config("config.yaml") bot = SupportBot(config) + + logging.basicConfig(level=logging.DEBUG) + logging.getLogger("nio").setLevel(logging.WARNING) + asyncio.get_event_loop().run_until_complete(bot.login()) asyncio.get_event_loop().run_until_complete(bot.start())