feat: improve bot functionality and logging setup

Enhanced the bot's functionality with several improvements:
- Added YAML dependency for configuration loading
- Introduced detailed logging for better monitoring
- Implemented invite event handling and operator check
- Updated error handling and user feedback messages
- Ensured the bot ignores its own messages

Increased the version to 0.1.1 to reflect these changes.
This commit is contained in:
Kumi 2024-08-18 18:53:59 +02:00
parent 6447362d04
commit 14e8d44999
Signed by: kumi
GPG key ID: ECBCC9082395383F
3 changed files with 80 additions and 21 deletions

View file

@ -7,7 +7,7 @@ allow-direct-references = true
[project] [project]
name = "matrix-supportbot" name = "matrix-supportbot"
version = "0.1.0" version = "0.1.1"
authors = [{ name = "Private.coffee Team", email = "support@private.coffee" }] authors = [{ name = "Private.coffee Team", email = "support@private.coffee" }]
@ -26,6 +26,7 @@ classifiers = [
dependencies = [ dependencies = [
"matrix-nio>=0.24.0", "matrix-nio>=0.24.0",
"pyyaml"
] ]
[project.urls] [project.urls]

View file

@ -6,33 +6,48 @@ from nio import (
RoomMessageMedia, RoomMessageMedia,
LoginResponse, LoginResponse,
RoomGetStateEventResponse, RoomGetStateEventResponse,
RoomMemberEvent, InviteMemberEvent,
JoinedMembersResponse,
RoomPutStateResponse,
) )
import logging
class SupportBot: class SupportBot:
def __init__(self, config): def __init__(self, config):
self.client = AsyncClient(config["homeserver"]) self.client = AsyncClient(config["homeserver"], config["username"])
self.username = config["username"] self.username = config["username"]
self.password = config["password"] self.password = config["password"]
self.operator_room_id = config["operator_room_id"] self.operator_room_id = config["operator_room_id"]
async def login(self): async def login(self):
response = await self.client.login(self.username, self.password) response = await self.client.login(self.password)
if isinstance(response, LoginResponse): if isinstance(response, LoginResponse):
print("Logged in successfully") logging.info("Logged in successfully")
else: else:
print("Failed to log in") logging.fatal("Failed to log in")
async def is_operator(self, user_id): 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) 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 return False
async def start(self): 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, RoomMessageText)
self.client.add_event_callback(self.message_callback, RoomMessageMedia) 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) await self.client.sync_forever(timeout=30000)
async def message_callback(self, room: MatrixRoom, event): async def message_callback(self, room: MatrixRoom, event):
@ -44,9 +59,24 @@ class SupportBot:
else: else:
await self.relay_message(room, sender, event) 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): async def handle_command(self, room, sender, command):
if command == "!supportbot openticket": if command == "!supportbot openticket":
await self.open_ticket(room, sender) await self.open_ticket(room, sender)
return
elif await self.is_operator(sender): elif await self.is_operator(sender):
if command.startswith("!supportbot invite"): if command.startswith("!supportbot invite"):
await self.invite_operator(room, sender, command) await self.invite_operator(room, sender, command)
@ -54,15 +84,16 @@ class SupportBot:
await self.close_ticket(room, sender, command) await self.close_ticket(room, sender, command)
elif command == "!supportbot list": elif command == "!supportbot list":
await self.list_tickets(room) await self.list_tickets(room)
else: return
await self.client.room_send(
room.room_id, await self.client.room_send(
"m.room.message", room.room_id,
{ "m.room.message",
"msgtype": "m.text", {
"body": "You are not authorized to use this command.", "msgtype": "m.text",
}, "body": "Sorry, I do not know this command, or you are not authorized to use it.",
) },
)
def generate_ticket_id(self): def generate_ticket_id(self):
return str(random.randint(10000000, 99999999)) return str(random.randint(10000000, 99999999))
@ -91,13 +122,19 @@ class SupportBot:
"status": "open", "status": "open",
} }
await self.client.room_put_state( state_event_response = await self.client.room_put_state(
room_id=self.operator_room_id, room_id=self.operator_room_id,
event_type="m.room.custom.ticket", event_type="m.room.custom.ticket",
state_key=state_event_key, state_key=state_event_key,
content=state_event_content, 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 # Inform the operator room
await self.client.room_send( await self.client.room_send(
self.operator_room_id, 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): async def invite_operator(self, room, sender, command):
ticket_id = command.split()[2] ticket_id = command.split()[2]
state_event_key = f"ticket_{ticket_id}" state_event_key = f"ticket_{ticket_id}"
@ -164,7 +210,10 @@ class SupportBot:
await self.client.room_send( await self.client.room_send(
customer_room_id, customer_room_id,
"m.room.message", "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( await self.client.room_send(
operator_room_id, operator_room_id,
@ -212,6 +261,10 @@ class SupportBot:
return None return None
async def relay_message(self, room, sender, event): 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) ticket_id = await self.get_ticket_id_from_room(room.room_id)
if ticket_id: if ticket_id:
state_event_key = f"ticket_{ticket_id}" state_event_key = f"ticket_{ticket_id}"
@ -229,5 +282,5 @@ class SupportBot:
# Relay the entire event content to the target room # Relay the entire event content to the target room
await self.client.room_send( await self.client.room_send(
target_room_id, event.type, event.source["content"] target_room_id, "m.room.message", event.source["content"]
) )

View file

@ -1,8 +1,9 @@
import yaml import yaml
import logging
import asyncio import asyncio
from .bot import SupportBot from .classes.bot import SupportBot
def load_config(config_file): def load_config(config_file):
with open(config_file, 'r') as file: with open(config_file, 'r') as file:
@ -11,6 +12,10 @@ def load_config(config_file):
def main(): def main():
config = load_config("config.yaml") config = load_config("config.yaml")
bot = SupportBot(config) 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.login())
asyncio.get_event_loop().run_until_complete(bot.start()) asyncio.get_event_loop().run_until_complete(bot.start())