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.
This commit is contained in:
Kumi 2024-11-08 07:52:09 +01:00
parent f343b59cc6
commit 97cd4aeb2f
Signed by: kumi
GPG key ID: ECBCC9082395383F
5 changed files with 225 additions and 11 deletions

View file

@ -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

View file

@ -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",
}

View file

@ -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)