feat: modmail ticket system with Components V2 + HTML transcripts
- DM-based modmail: user DMs bot -> ticket channel created - Staff relay: messages in ticket -> DM to user - Configurable categories via /modmail addcategory - Setup via /modmail setup (category, staff role, log channel) - Close command: /close or !close -> HTML transcript generated - Components V2: Container, TextDisplay, Separator, persistent views - Transcript: dark theme HTML with staff badges, avatars, timestamps
This commit is contained in:
parent
f0e93d6262
commit
f505b6c552
5 changed files with 837 additions and 0 deletions
110
utils/modmail_config.py
Normal file
110
utils/modmail_config.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from config import DATA_DIR
|
||||
|
||||
CONFIG_PATH = DATA_DIR / "modmail.json"
|
||||
|
||||
DEFAULT_CONFIG = {
|
||||
"enabled": False,
|
||||
"category_id": None,
|
||||
"staff_role_id": None,
|
||||
"log_channel_id": None,
|
||||
"categories": {},
|
||||
"open_tickets": {},
|
||||
}
|
||||
|
||||
|
||||
def _load() -> dict:
|
||||
if CONFIG_PATH.exists():
|
||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
return DEFAULT_CONFIG.copy()
|
||||
|
||||
|
||||
def _save(data: dict):
|
||||
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def get_config() -> dict:
|
||||
return _load()
|
||||
|
||||
|
||||
def set_enabled(guild_id: int, enabled: bool):
|
||||
data = _load()
|
||||
data["enabled"] = enabled
|
||||
_save(data)
|
||||
|
||||
|
||||
def set_category(guild_id: int, category_id: int):
|
||||
data = _load()
|
||||
data["category_id"] = category_id
|
||||
_save(data)
|
||||
|
||||
|
||||
def set_staff_role(guild_id: int, role_id: int):
|
||||
data = _load()
|
||||
data["staff_role_id"] = role_id
|
||||
_save(data)
|
||||
|
||||
|
||||
def set_log_channel(guild_id: int, channel_id: int):
|
||||
data = _load()
|
||||
data["log_channel_id"] = channel_id
|
||||
_save(data)
|
||||
|
||||
|
||||
def add_category(guild_id: int, name: str, emoji: str, description: str = ""):
|
||||
data = _load()
|
||||
data["categories"][name.lower()] = {
|
||||
"name": name,
|
||||
"emoji": emoji,
|
||||
"description": description,
|
||||
}
|
||||
_save(data)
|
||||
|
||||
|
||||
def remove_category(guild_id: int, name: str):
|
||||
data = _load()
|
||||
data["categories"].pop(name.lower(), None)
|
||||
_save(data)
|
||||
|
||||
|
||||
def get_categories() -> dict:
|
||||
data = _load()
|
||||
return data.get("categories", {})
|
||||
|
||||
|
||||
def register_ticket(guild_id: int, user_id: int, channel_id: int, category: str):
|
||||
data = _load()
|
||||
data["open_tickets"][str(user_id)] = {
|
||||
"channel_id": channel_id,
|
||||
"category": category,
|
||||
"guild_id": guild_id,
|
||||
}
|
||||
_save(data)
|
||||
|
||||
|
||||
def unregister_ticket(user_id: int):
|
||||
data = _load()
|
||||
data["open_tickets"].pop(str(user_id), None)
|
||||
_save(data)
|
||||
|
||||
|
||||
def get_ticket_by_user(user_id: int) -> dict | None:
|
||||
data = _load()
|
||||
return data["open_tickets"].get(str(user_id))
|
||||
|
||||
|
||||
def get_ticket_by_channel(channel_id: int) -> dict | None:
|
||||
data = _load()
|
||||
for uid, ticket in data.get("open_tickets", {}).items():
|
||||
if ticket["channel_id"] == channel_id:
|
||||
return {"user_id": int(uid), **ticket}
|
||||
return None
|
||||
|
||||
|
||||
def is_enabled() -> bool:
|
||||
return _load().get("enabled", False)
|
||||
Loading…
Add table
Add a link
Reference in a new issue