Major expansion: - All 8 modules fully expanded with advanced features - New: Embed Builder (webhook-based, templates, scheduling, variables) - New: Dashboard Integration (web config for all modules) - Serverlog: 30+ event types, invite tracking, per-event channels - Moderation: quarantine, reputation, warning decay, staff leaderboard, anti-raid, anti-nuke, 6 automod modules, appeals, cross-server bans - Economy: roulette, dice, fishing, mining, crafting, achievements, income roles, auction house, pets (6 types), heists, gambling stats - Tickets: auto-close, tags, HTML transcripts, staff tracking, reopen - Applications: staff voting, bulk actions, webhook notifications, templates - Suggestions: edit, merge, 10 statuses, tag system, auto-approve/deny - Reaction Roles: verification panels, exclusive groups, sticky/temp roles - Giveaways: drop mode, bonus entries, templates, DM notifications - All views use custom_id for persistence across restarts - Full syntax verification on all 13 Python files
523 lines
24 KiB
Python
523 lines
24 KiB
Python
"""NexusCore — Application / forms system v2: modals, review, multi-type, auto-role,
|
|
conditional questions, voting, templates, stats, bulk actions, webhook notifications."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
from typing import Optional
|
|
|
|
import discord
|
|
from redbot.core import Config, commands
|
|
|
|
from .utils import (
|
|
Clr, ok_embed, err_embed, info_embed, short_id, ts_now, ts_relative,
|
|
safe_send, safe_dm, ConfirmView, Paginator, chunk_list,
|
|
)
|
|
|
|
# ── Defaults ───────────────────────────────────────────────────────────────
|
|
APP_DEFAULTS_GUILD = {
|
|
"enabled": False,
|
|
"review_channel": None,
|
|
"log_channel": None,
|
|
"webhook_url": None,
|
|
"types": {},
|
|
"templates": {}, # template_name -> {questions, description, role_on_accept, ...}
|
|
"submissions": {},
|
|
"blacklisted": [],
|
|
"global_cooldown": 86400,
|
|
"dm_results": True,
|
|
"anonymous_review": False,
|
|
"voting_enabled": False,
|
|
"voting_threshold": 3,
|
|
"auto_accept_votes": 0, # 0 = disabled
|
|
"auto_deny_votes": 0,
|
|
"stats": {"total": 0, "accepted": 0, "denied": 0, "avg_review_time": 0},
|
|
}
|
|
|
|
|
|
# ── Views ──────────────────────────────────────────────────────────────────
|
|
class AppPanelView(discord.ui.View):
|
|
def __init__(self, cog: "ApplicationsMixin"):
|
|
super().__init__(timeout=None)
|
|
self.cog = cog
|
|
|
|
@discord.ui.button(label="📋 Apply", style=discord.ButtonStyle.success, custom_id="nexus_app_apply")
|
|
async def apply_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
data = await self.cog.app_config.guild(interaction.guild).all()
|
|
if not data["enabled"]:
|
|
return await interaction.response.send_message("Applications are disabled.", ephemeral=True)
|
|
|
|
types = {k: v for k, v in data["types"].items() if v.get("enabled", True)}
|
|
if not types:
|
|
return await interaction.response.send_message("No application types available.", ephemeral=True)
|
|
|
|
if len(types) == 1:
|
|
t_name = list(types.keys())[0]
|
|
return await self.cog._start_application(interaction, t_name)
|
|
|
|
view = AppTypeSelectView(self.cog, types)
|
|
await interaction.response.send_message("Choose an application type:", view=view, ephemeral=True)
|
|
|
|
|
|
class AppTypeSelectView(discord.ui.View):
|
|
def __init__(self, cog, types: dict):
|
|
super().__init__(timeout=60)
|
|
self.cog = cog
|
|
options = []
|
|
for name, td in list(types.items())[:25]:
|
|
options.append(discord.SelectOption(
|
|
label=name.title(), value=name,
|
|
description=(td.get("description", "") or "")[:100],
|
|
emoji=td.get("emoji") or "📋",
|
|
))
|
|
self.sel = discord.ui.Select(placeholder="Select type...", options=options)
|
|
self.sel.callback = self.on_select
|
|
self.add_item(self.sel)
|
|
|
|
async def on_select(self, interaction: discord.Interaction):
|
|
await self.cog._start_application(interaction, self.sel.values[0])
|
|
|
|
|
|
class AppFormModal(discord.ui.Modal):
|
|
def __init__(self, cog, app_type: str, questions: list[dict], page: int = 0):
|
|
super().__init__(title=f"Application — {app_type.title()}"[:45])
|
|
self.cog = cog
|
|
self.app_type = app_type
|
|
self.page = page
|
|
self.inputs = []
|
|
start = page * 5
|
|
for q in questions[start:start + 5]:
|
|
style = discord.TextStyle.paragraph if q.get("style") == "long" else discord.TextStyle.short
|
|
inp = discord.ui.TextInput(
|
|
label=q["label"][:45],
|
|
placeholder=q.get("placeholder", "")[:100] or None,
|
|
style=style,
|
|
required=q.get("required", True),
|
|
max_length=q.get("max_length", 1024),
|
|
)
|
|
self.inputs.append(inp)
|
|
self.add_item(inp)
|
|
self._questions = questions
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
answers = {}
|
|
start = self.page * 5
|
|
for i, inp in enumerate(self.inputs):
|
|
answers[self._questions[start + i]["label"]] = inp.value
|
|
|
|
cache_key = f"{interaction.user.id}_{self.app_type}"
|
|
if not hasattr(self.cog, "_app_answer_cache"):
|
|
self.cog._app_answer_cache = {}
|
|
if cache_key not in self.cog._app_answer_cache:
|
|
self.cog._app_answer_cache[cache_key] = {}
|
|
self.cog._app_answer_cache[cache_key].update(answers)
|
|
|
|
next_page = self.page + 1
|
|
if next_page * 5 < len(self._questions):
|
|
modal = AppFormModal(self.cog, self.app_type, self._questions, next_page)
|
|
await interaction.response.send_modal(modal)
|
|
else:
|
|
all_answers = self.cog._app_answer_cache.pop(cache_key, answers)
|
|
await interaction.response.defer(ephemeral=True)
|
|
await self.cog._submit_application(interaction, self.app_type, all_answers)
|
|
await interaction.followup.send("✅ Your application has been submitted!", ephemeral=True)
|
|
|
|
|
|
class ReviewView(discord.ui.View):
|
|
def __init__(self, cog: "ApplicationsMixin", sub_id: str):
|
|
super().__init__(timeout=None)
|
|
self.cog = cog
|
|
self.sub_id = sub_id
|
|
|
|
@discord.ui.button(label="Accept", style=discord.ButtonStyle.success, emoji="✅", custom_id="nexus_app_accept")
|
|
async def accept_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await self.cog._review_application(interaction, self.sub_id, "accepted")
|
|
|
|
@discord.ui.button(label="Deny", style=discord.ButtonStyle.danger, emoji="❌", custom_id="nexus_app_deny")
|
|
async def deny_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await self.cog._review_application(interaction, self.sub_id, "denied")
|
|
|
|
@discord.ui.button(label="Interview", style=discord.ButtonStyle.primary, emoji="🎙️", custom_id="nexus_app_interview")
|
|
async def interview_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await self.cog._review_application(interaction, self.sub_id, "interview")
|
|
|
|
@discord.ui.button(label="Note", style=discord.ButtonStyle.secondary, emoji="📝", custom_id="nexus_app_note")
|
|
async def note_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
modal = AppNoteModal(self.cog, self.sub_id)
|
|
await interaction.response.send_modal(modal)
|
|
|
|
@discord.ui.button(label="👍", style=discord.ButtonStyle.secondary, custom_id="nexus_app_vote_up")
|
|
async def vote_up(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await self.cog._vote_application(interaction, self.sub_id, "up")
|
|
|
|
@discord.ui.button(label="👎", style=discord.ButtonStyle.secondary, custom_id="nexus_app_vote_down")
|
|
async def vote_down(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
await self.cog._vote_application(interaction, self.sub_id, "down")
|
|
|
|
|
|
class AppNoteModal(discord.ui.Modal):
|
|
def __init__(self, cog, sub_id: str):
|
|
super().__init__(title="Add Staff Note")
|
|
self.cog = cog
|
|
self.sub_id = sub_id
|
|
self.note_input = discord.ui.TextInput(label="Note", style=discord.TextStyle.paragraph, max_length=500)
|
|
self.add_item(self.note_input)
|
|
|
|
async def on_submit(self, interaction: discord.Interaction):
|
|
async with self.cog.app_config.guild(interaction.guild).submissions() as subs:
|
|
if self.sub_id in subs:
|
|
if "notes" not in subs[self.sub_id]:
|
|
subs[self.sub_id]["notes"] = []
|
|
subs[self.sub_id]["notes"].append({
|
|
"author": interaction.user.id,
|
|
"text": self.note_input.value,
|
|
"at": ts_now(),
|
|
})
|
|
await interaction.response.send_message("📝 Note added.", ephemeral=True)
|
|
|
|
|
|
class BulkActionView(discord.ui.View):
|
|
"""View for bulk accept/deny multiple apps at once."""
|
|
def __init__(self, cog, sub_ids: list[str]):
|
|
super().__init__(timeout=120)
|
|
self.cog = cog
|
|
self.sub_ids = sub_ids
|
|
|
|
@discord.ui.button(label="Accept All", style=discord.ButtonStyle.success, emoji="✅")
|
|
async def accept_all(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
count = 0
|
|
for sid in self.sub_ids:
|
|
try:
|
|
await self.cog._review_application(interaction, sid, "accepted", silent=True)
|
|
count += 1
|
|
except Exception:
|
|
pass
|
|
await interaction.response.send_message(f"✅ Bulk accepted {count}/{len(self.sub_ids)} applications.", ephemeral=True)
|
|
self.stop()
|
|
|
|
@discord.ui.button(label="Deny All", style=discord.ButtonStyle.danger, emoji="❌")
|
|
async def deny_all(self, interaction: discord.Interaction, button: discord.ui.Button):
|
|
count = 0
|
|
for sid in self.sub_ids:
|
|
try:
|
|
await self.cog._review_application(interaction, sid, "denied", silent=True)
|
|
count += 1
|
|
except Exception:
|
|
pass
|
|
await interaction.response.send_message(f"❌ Bulk denied {count}/{len(self.sub_ids)} applications.", ephemeral=True)
|
|
self.stop()
|
|
|
|
|
|
# ── Mixin ──────────────────────────────────────────────────────────────────
|
|
class ApplicationsMixin:
|
|
"""Application system mixin — v2 with voting, templates, bulk actions, webhook."""
|
|
|
|
def _init_applications(self, bot):
|
|
self.app_config = Config.get_conf(None, identifier=900002, cog_name="NexusCoreApps")
|
|
self.app_config.register_guild(**APP_DEFAULTS_GUILD)
|
|
self._app_answer_cache = {}
|
|
self._app_panel_view = AppPanelView(self)
|
|
bot.add_view(self._app_panel_view)
|
|
|
|
async def _start_application(self, interaction: discord.Interaction, type_name: str):
|
|
guild = interaction.guild
|
|
data = await self.app_config.guild(guild).all()
|
|
|
|
if str(interaction.user.id) in [str(x) for x in data["blacklisted"]]:
|
|
return await interaction.response.send_message("You are blacklisted from applications.", ephemeral=True)
|
|
|
|
type_data = data["types"].get(type_name)
|
|
if not type_data or not type_data.get("enabled", True):
|
|
return await interaction.response.send_message("This application type is unavailable.", ephemeral=True)
|
|
|
|
cooldown = type_data.get("cooldown") or data["global_cooldown"]
|
|
for sub in data["submissions"].values():
|
|
if str(sub["user_id"]) == str(interaction.user.id) and sub["type"] == type_name:
|
|
if sub["status"] == "pending":
|
|
return await interaction.response.send_message("You already have a pending application.", ephemeral=True)
|
|
if ts_now() - sub.get("submitted_at", 0) < cooldown:
|
|
remaining = cooldown - (ts_now() - sub.get("submitted_at", 0))
|
|
return await interaction.response.send_message(
|
|
f"Cooldown active. Try again {ts_relative(ts_now() + remaining)}.", ephemeral=True)
|
|
|
|
req_age = type_data.get("require_account_age_days", 0)
|
|
if req_age:
|
|
age = (datetime.datetime.now(datetime.timezone.utc) - interaction.user.created_at).days
|
|
if age < req_age:
|
|
return await interaction.response.send_message(f"Your account must be at least {req_age} days old.", ephemeral=True)
|
|
|
|
req_server = type_data.get("require_server_days", 0)
|
|
if req_server and interaction.user.joined_at:
|
|
days = (datetime.datetime.now(datetime.timezone.utc) - interaction.user.joined_at).days
|
|
if days < req_server:
|
|
return await interaction.response.send_message(f"You must be in the server for at least {req_server} days.", ephemeral=True)
|
|
|
|
questions = type_data.get("questions", [])
|
|
if not questions:
|
|
questions = [{"label": "Why are you applying?", "style": "long"}]
|
|
|
|
modal = AppFormModal(self, type_name, questions, 0)
|
|
await interaction.response.send_modal(modal)
|
|
|
|
async def _submit_application(self, interaction: discord.Interaction, type_name: str, answers: dict):
|
|
guild = interaction.guild
|
|
data = await self.app_config.guild(guild).all()
|
|
type_data = data["types"].get(type_name, {})
|
|
|
|
sub_id = short_id(12)
|
|
submission = {
|
|
"user_id": interaction.user.id,
|
|
"user_name": str(interaction.user),
|
|
"type": type_name,
|
|
"answers": answers,
|
|
"status": "pending",
|
|
"submitted_at": ts_now(),
|
|
"reviewed_by": None,
|
|
"reviewed_at": None,
|
|
"review_msg_id": None,
|
|
"notes": [],
|
|
"votes_up": [],
|
|
"votes_down": [],
|
|
}
|
|
|
|
async with self.app_config.guild(guild).submissions() as subs:
|
|
subs[sub_id] = submission
|
|
|
|
# Update stats
|
|
async with self.app_config.guild(guild).stats() as stats:
|
|
stats["total"] = stats.get("total", 0) + 1
|
|
|
|
review_ch_id = type_data.get("review_channel") or data["review_channel"]
|
|
if review_ch_id:
|
|
review_ch = guild.get_channel(review_ch_id)
|
|
if review_ch:
|
|
embed = discord.Embed(
|
|
title=f"📋 New Application — {type_name.title()}",
|
|
colour=Clr.APP,
|
|
timestamp=datetime.datetime.now(datetime.timezone.utc),
|
|
)
|
|
embed.set_author(
|
|
name=str(interaction.user),
|
|
icon_url=interaction.user.display_avatar.url if interaction.user.display_avatar else None,
|
|
)
|
|
embed.add_field(name="User", value=f"{interaction.user.mention} (`{interaction.user.id}`)", inline=True)
|
|
embed.add_field(name="Type", value=type_name.title(), inline=True)
|
|
embed.add_field(name="ID", value=f"`{sub_id}`", inline=True)
|
|
|
|
for q, a in answers.items():
|
|
embed.add_field(name=q, value=a[:1024] or "*No answer*", inline=False)
|
|
|
|
if interaction.user.joined_at:
|
|
embed.add_field(name="Server Member Since", value=ts_relative(int(interaction.user.joined_at.timestamp())), inline=True)
|
|
embed.add_field(name="Account Created", value=ts_relative(int(interaction.user.created_at.timestamp())), inline=True)
|
|
|
|
if data.get("voting_enabled"):
|
|
embed.add_field(name="Votes", value="👍 0 | 👎 0", inline=True)
|
|
|
|
embed.set_footer(text=f"Application ID: {sub_id}")
|
|
|
|
view = ReviewView(self, sub_id)
|
|
msg = await review_ch.send(embed=embed, view=view)
|
|
|
|
async with self.app_config.guild(guild).submissions() as subs:
|
|
if sub_id in subs:
|
|
subs[sub_id]["review_msg_id"] = msg.id
|
|
|
|
if type_data.get("auto_thread"):
|
|
try:
|
|
await msg.create_thread(name=f"Review — {interaction.user.name}")
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
# Webhook notification
|
|
webhook_url = data.get("webhook_url")
|
|
if webhook_url:
|
|
try:
|
|
import aiohttp
|
|
payload = {
|
|
"content": f"📋 New **{type_name}** application from **{interaction.user}**",
|
|
"embeds": [{
|
|
"title": f"Application: {type_name.title()}",
|
|
"description": "\n".join(f"**{q}:** {a}" for q, a in answers.items()),
|
|
"color": Clr.APP.value,
|
|
}],
|
|
}
|
|
async with aiohttp.ClientSession() as session:
|
|
await session.post(webhook_url, json=payload)
|
|
except Exception:
|
|
pass
|
|
|
|
log_ch_id = data.get("log_channel")
|
|
if log_ch_id:
|
|
log_ch = guild.get_channel(log_ch_id)
|
|
if log_ch:
|
|
le = discord.Embed(title="📋 Application Submitted", colour=Clr.APP)
|
|
le.add_field(name="User", value=interaction.user.mention, inline=True)
|
|
le.add_field(name="Type", value=type_name.title(), inline=True)
|
|
le.add_field(name="ID", value=sub_id, inline=True)
|
|
await safe_send(log_ch, embed=le)
|
|
|
|
async def _vote_application(self, interaction: discord.Interaction, sub_id: str, direction: str):
|
|
"""Staff vote on an application."""
|
|
data = await self.app_config.guild(interaction.guild).all()
|
|
if not data.get("voting_enabled"):
|
|
return await interaction.response.send_message("Voting is not enabled.", ephemeral=True)
|
|
|
|
async with self.app_config.guild(interaction.guild).submissions() as subs:
|
|
sub = subs.get(sub_id)
|
|
if not sub:
|
|
return await interaction.response.send_message("Not found.", ephemeral=True)
|
|
if sub["status"] != "pending":
|
|
return await interaction.response.send_message("Already reviewed.", ephemeral=True)
|
|
|
|
uid = interaction.user.id
|
|
ups = sub.get("votes_up", [])
|
|
downs = sub.get("votes_down", [])
|
|
if direction == "up":
|
|
if uid in ups:
|
|
ups.remove(uid)
|
|
else:
|
|
ups.append(uid)
|
|
if uid in downs:
|
|
downs.remove(uid)
|
|
else:
|
|
if uid in downs:
|
|
downs.remove(uid)
|
|
else:
|
|
downs.append(uid)
|
|
if uid in ups:
|
|
ups.remove(uid)
|
|
sub["votes_up"] = ups
|
|
sub["votes_down"] = downs
|
|
subs[sub_id] = sub
|
|
|
|
await interaction.response.send_message(f"Vote recorded! 👍 {len(ups)} | 👎 {len(downs)}", ephemeral=True)
|
|
|
|
# Auto-accept/deny based on vote thresholds
|
|
auto_accept = data.get("auto_accept_votes", 0)
|
|
auto_deny = data.get("auto_deny_votes", 0)
|
|
if auto_accept and len(ups) >= auto_accept:
|
|
await self._review_application(interaction, sub_id, "accepted", silent=True)
|
|
elif auto_deny and len(downs) >= auto_deny:
|
|
await self._review_application(interaction, sub_id, "denied", silent=True)
|
|
|
|
async def _review_application(self, interaction, sub_id: str, status: str, silent: bool = False):
|
|
guild = interaction.guild
|
|
data = await self.app_config.guild(guild).all()
|
|
sub = data["submissions"].get(sub_id)
|
|
if not sub:
|
|
if not silent:
|
|
return await interaction.response.send_message("Application not found.", ephemeral=True)
|
|
return
|
|
if sub["status"] != "pending" and status != "interview":
|
|
if not silent:
|
|
return await interaction.response.send_message(f"Already {sub['status']}.", ephemeral=True)
|
|
return
|
|
|
|
async with self.app_config.guild(guild).submissions() as subs:
|
|
if sub_id in subs:
|
|
subs[sub_id]["status"] = status
|
|
subs[sub_id]["reviewed_by"] = interaction.user.id
|
|
subs[sub_id]["reviewed_at"] = ts_now()
|
|
|
|
# Update stats
|
|
async with self.app_config.guild(guild).stats() as stats:
|
|
if status in ("accepted", "denied"):
|
|
stats[status] = stats.get(status, 0) + 1
|
|
review_time = ts_now() - sub.get("submitted_at", ts_now())
|
|
total_reviewed = stats.get("accepted", 0) + stats.get("denied", 0)
|
|
old_avg = stats.get("avg_review_time", 0)
|
|
stats["avg_review_time"] = int(((old_avg * (total_reviewed - 1)) + review_time) / max(total_reviewed, 1))
|
|
|
|
type_data = data["types"].get(sub["type"], {})
|
|
status_emojis = {"accepted": "✅", "denied": "❌", "interview": "🎙️"}
|
|
status_colours = {"accepted": Clr.SUCCESS, "denied": Clr.ERROR, "interview": Clr.INFO}
|
|
|
|
if not silent:
|
|
embed = discord.Embed(
|
|
title=f"{status_emojis.get(status, '❓')} Application {status.title()}",
|
|
description=f"**Applicant:** <@{sub['user_id']}>\n**Type:** {sub['type'].title()}\n**Reviewed by:** {interaction.user.mention}",
|
|
colour=status_colours.get(status, Clr.INFO),
|
|
)
|
|
try:
|
|
await interaction.response.send_message(embed=embed)
|
|
except discord.InteractionResponded:
|
|
pass
|
|
|
|
member = guild.get_member(sub["user_id"])
|
|
if member:
|
|
if status == "accepted" and type_data.get("role_on_accept"):
|
|
role = guild.get_role(type_data["role_on_accept"])
|
|
if role:
|
|
try:
|
|
await member.add_roles(role, reason=f"Application {sub_id} accepted")
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
if status == "denied" and type_data.get("role_on_deny"):
|
|
role = guild.get_role(type_data["role_on_deny"])
|
|
if role:
|
|
try:
|
|
await member.add_roles(role, reason=f"Application {sub_id} denied")
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
if data["dm_results"]:
|
|
if status == "accepted":
|
|
msg_text = type_data.get("accept_msg") or f"Your **{sub['type'].title()}** application in **{guild.name}** has been **accepted**! 🎉"
|
|
elif status == "denied":
|
|
msg_text = type_data.get("deny_msg") or f"Your **{sub['type'].title()}** application in **{guild.name}** has been **denied**."
|
|
else:
|
|
msg_text = f"You've been selected for an **interview** regarding your **{sub['type'].title()}** application in **{guild.name}**!"
|
|
await safe_dm(member, embed=discord.Embed(description=msg_text, colour=status_colours.get(status, Clr.INFO)))
|
|
|
|
# Disable buttons on review message
|
|
if sub.get("review_msg_id"):
|
|
review_ch_id = type_data.get("review_channel") or data["review_channel"]
|
|
if review_ch_id:
|
|
review_ch = guild.get_channel(review_ch_id)
|
|
if review_ch:
|
|
try:
|
|
review_msg = await review_ch.fetch_message(sub["review_msg_id"])
|
|
await review_msg.edit(view=None)
|
|
except discord.HTTPException:
|
|
pass
|
|
|
|
# ── Template management ────────────────────────────────────────────────
|
|
async def _save_app_template(self, guild, name: str, type_name: str):
|
|
"""Save an application type's config as a reusable template."""
|
|
data = await self.app_config.guild(guild).all()
|
|
type_data = data["types"].get(type_name)
|
|
if not type_data:
|
|
return False
|
|
async with self.app_config.guild(guild).templates() as templates:
|
|
templates[name] = dict(type_data)
|
|
return True
|
|
|
|
async def _load_app_template(self, guild, template_name: str, type_name: str):
|
|
"""Load a template into an application type."""
|
|
data = await self.app_config.guild(guild).all()
|
|
template = data["templates"].get(template_name)
|
|
if not template:
|
|
return False
|
|
async with self.app_config.guild(guild).types() as types:
|
|
types[type_name] = dict(template)
|
|
return True
|
|
|
|
def _get_app_stats(self, data: dict) -> dict:
|
|
"""Compute application statistics."""
|
|
subs = data.get("submissions", {})
|
|
total = len(subs)
|
|
by_status = {}
|
|
by_type = {}
|
|
for sub in subs.values():
|
|
s = sub.get("status", "pending")
|
|
by_status[s] = by_status.get(s, 0) + 1
|
|
t = sub.get("type", "unknown")
|
|
by_type[t] = by_type.get(t, 0) + 1
|
|
return {
|
|
"total": total,
|
|
"by_status": by_status,
|
|
"by_type": by_type,
|
|
"stats": data.get("stats", {}),
|
|
}
|