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:
parent
f343b59cc6
commit
97cd4aeb2f
5 changed files with 225 additions and 11 deletions
14
README.md
14
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)
|
[![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)
|
[![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](https://shields.private.coffee/pypi/v/matrix-applicationbot)](https://pypi.org/project/matrix-applicationbot/)
|
||||||
[![PyPI - Python Version](https://shields.private.coffee/pypi/pyversions/matrix-supportbot)](https://pypi.org/project/matrix-supportbot/)
|
[![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-supportbot)](https://pypi.org/project/matrix-supportbot/)
|
[![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-supportbot?gitea_url=https://git.private.coffee)](https://git.private.coffee/privatecoffee/matrix-supportbot)
|
[![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
|
## Features
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,15 @@ from nio import (
|
||||||
InviteMemberEvent,
|
InviteMemberEvent,
|
||||||
JoinedMembersResponse,
|
JoinedMembersResponse,
|
||||||
RoomPutStateResponse,
|
RoomPutStateResponse,
|
||||||
|
RoomMemberEvent,
|
||||||
|
RoomKickResponse,
|
||||||
|
RoomInviteResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class SupportBot:
|
class ApplicationBot:
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.client = AsyncClient(config["homeserver"], config["username"])
|
self.client = AsyncClient(config["homeserver"], config["username"])
|
||||||
self.username = 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, 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)
|
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)
|
await self.client.sync_forever(timeout=30000)
|
||||||
|
|
||||||
async def message_callback(self, room: MatrixRoom, event):
|
async def message_callback(self, room: MatrixRoom, event):
|
||||||
|
@ -56,6 +60,8 @@ class SupportBot:
|
||||||
|
|
||||||
if body and body.startswith("!supportbot"):
|
if body and body.startswith("!supportbot"):
|
||||||
await self.handle_command(room, sender, body)
|
await self.handle_command(room, sender, body)
|
||||||
|
elif body and body.startswith("!apply"):
|
||||||
|
await self.process_application(room, sender, body)
|
||||||
else:
|
else:
|
||||||
await self.relay_message(room, sender, event)
|
await self.relay_message(room, sender, event)
|
||||||
|
|
||||||
|
@ -72,18 +78,42 @@ class SupportBot:
|
||||||
"body": "Hello! To open a support ticket, please type `!supportbot openticket`.",
|
"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):
|
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)
|
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)
|
||||||
elif command.startswith("!supportbot close"):
|
elif command.startswith("!supportbot close"):
|
||||||
await self.close_ticket(room, sender, command)
|
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":
|
elif command == "!supportbot list":
|
||||||
await self.list_tickets(room)
|
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
|
return
|
||||||
|
|
||||||
await self.client.room_send(
|
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):
|
def generate_ticket_id(self):
|
||||||
return str(random.randint(10000000, 99999999))
|
return str(random.randint(10000000, 99999999))
|
||||||
|
|
||||||
|
@ -119,6 +330,7 @@ class SupportBot:
|
||||||
"ticket_id": ticket_id,
|
"ticket_id": ticket_id,
|
||||||
"customer_room": customer_room_id,
|
"customer_room": customer_room_id,
|
||||||
"operator_room": operator_room_id,
|
"operator_room": operator_room_id,
|
||||||
|
"customer_user_id": sender,
|
||||||
"status": "open",
|
"status": "open",
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import yaml
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from .classes.bot import SupportBot
|
from .classes.bot import ApplicationBot
|
||||||
|
|
||||||
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,7 +11,7 @@ def load_config(config_file):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
config = load_config("config.yaml")
|
config = load_config("config.yaml")
|
||||||
bot = SupportBot(config)
|
bot = ApplicationBot(config)
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
logging.getLogger("nio").setLevel(logging.WARNING)
|
logging.getLogger("nio").setLevel(logging.WARNING)
|
Loading…
Reference in a new issue