add permission scanner, info panels, image QR scan, invite tracker, ticket templates

Five new features:
- /permissions scan: audits admin/ban roles, dangerous bot permissions,
  public channels, empty/duplicate/useless roles, too-many-admins warning.
- /panel create: fixed info panels (rules/support/bewerbung/partner/
  rollen/events/faq) with buttons wired into the existing ticket/
  register/partner flows, FAQ panels get a question dropdown.
- Image moderation: QR-code detection in message attachments via jimp+jsqr
  (pure JS, no native build deps) with a small scam-pattern check; opt-in
  toggle in dashboard settings since decoding costs per-message.
- Invite tracker extension: now resolves the inviter (not just the code),
  tracks suspicious joins per invite, and the dashboard growth page gets
  an invite breakdown + recent-joins table.
- Ticket categories: /ticketconfig lets each ticket topic get its own
  ping role and a modal question template shown before the channel is
  created; topics without config behave exactly as before.

Also fixes two rough edges found along the way: the welcome embed had
no way to attach an image despite the backend already supporting it
(added URL/file-upload fields + live preview), and Reaction Roles
required typing raw role IDs into a textarea (replaced with a proper
role picker). Incidentally repairs node_modules/.bin symlinks that were
committed as literal broken shell-script text instead of real symlinks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 00:46:23 +02:00
parent 54376fdad7
commit 9943254aa4
49 changed files with 3992 additions and 339 deletions

View File

@@ -6,13 +6,14 @@ type Props = {
title?: string; title?: string;
description?: string; description?: string;
footer?: string; footer?: string;
image?: string;
accentColor?: string; accentColor?: string;
children?: ReactNode; children?: ReactNode;
}; };
const now = () => new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }); const now = () => new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
export function DiscordPreview({ botName = 'Papo', title, description, footer, accentColor = '#5865f2', children }: Props) { export function DiscordPreview({ botName = 'Papo', title, description, footer, image, accentColor = '#5865f2', children }: Props) {
return ( return (
<div className="rounded-lg bg-[#313338] p-4"> <div className="rounded-lg bg-[#313338] p-4">
<div className="flex gap-3"> <div className="flex gap-3">
@@ -26,7 +27,7 @@ export function DiscordPreview({ botName = 'Papo', title, description, footer, a
<span className="text-xs text-[#949ba4]">Heute um {now()}</span> <span className="text-xs text-[#949ba4]">Heute um {now()}</span>
</div> </div>
{(title || description || footer) && ( {(title || description || footer || image) && (
<div <div
className="mt-1 max-w-md rounded border-l-4 bg-[#2b2d31] p-3" className="mt-1 max-w-md rounded border-l-4 bg-[#2b2d31] p-3"
style={{ borderLeftColor: accentColor }} style={{ borderLeftColor: accentColor }}
@@ -36,6 +37,7 @@ export function DiscordPreview({ botName = 'Papo', title, description, footer, a
<div className="mt-1 whitespace-pre-wrap text-sm leading-snug text-[#dbdee1]">{description}</div> <div className="mt-1 whitespace-pre-wrap text-sm leading-snug text-[#dbdee1]">{description}</div>
)} )}
{footer && <div className="mt-2.5 text-xs text-[#949ba4]">{footer}</div>} {footer && <div className="mt-2.5 text-xs text-[#949ba4]">{footer}</div>}
{image && <img src={image} alt="" className="mt-2.5 max-h-64 w-full max-w-full rounded object-cover" />}
</div> </div>
)} )}

View File

@@ -4,7 +4,8 @@ import type {
AppConfig, User, Guild, NavKey, TicketRecord, StatusService, AppConfig, User, Guild, NavKey, TicketRecord, StatusService,
EventItem, ReactionRoleSet, ModuleItem, LogEntry, SettingsState, EventItem, ReactionRoleSet, ModuleItem, LogEntry, SettingsState,
SupportLoginConfig, SupportLoginStatus, RegisterForm, RegisterFormField, SupportLoginConfig, SupportLoginStatus, RegisterForm, RegisterFormField,
RegisterApplication, MusicSession, StaffTask, WatchlistEntry, GrowthStats RegisterApplication, MusicSession, StaffTask, WatchlistEntry, GrowthStats,
InviteBreakdownEntry, RecentJoinEntry
} from '../types'; } from '../types';
const appConfig: AppConfig = (window as any).__PAPO__ || {}; const appConfig: AppConfig = (window as any).__PAPO__ || {};
@@ -48,6 +49,8 @@ type AppState = {
taskDraft: { title: string; description: string }; taskDraft: { title: string; description: string };
watchlistEntries: WatchlistEntry[]; watchlistEntries: WatchlistEntry[];
growthStats: GrowthStats | null; growthStats: GrowthStats | null;
inviteBreakdown: InviteBreakdownEntry[];
recentJoins: RecentJoinEntry[];
}; };
type AppContextType = AppState & { type AppContextType = AppState & {
@@ -131,6 +134,7 @@ type AppContextType = AppState & {
loadWatchlist: () => Promise<void>; loadWatchlist: () => Promise<void>;
removeFromWatchlist: (userId: string) => Promise<void>; removeFromWatchlist: (userId: string) => Promise<void>;
loadGrowthStats: () => Promise<void>; loadGrowthStats: () => Promise<void>;
loadInviteBreakdown: () => Promise<void>;
}; };
const AppContext = createContext<AppContextType | null>(null); const AppContext = createContext<AppContextType | null>(null);
@@ -165,7 +169,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
const [eventDraft, setEventDraft] = useState({ title: '', description: '', channelId: '', startsAt: '' }); const [eventDraft, setEventDraft] = useState({ title: '', description: '', channelId: '', startsAt: '' });
const [statusDraft, setStatusDraft] = useState<any>(null); const [statusDraft, setStatusDraft] = useState<any>(null);
const [statsDraft, setStatsDraft] = useState<any>(null); const [statsDraft, setStatsDraft] = useState<any>(null);
const [reactionDraft, setReactionDraft] = useState({ title: '', channelId: '', entries: '' }); const [reactionDraft, setReactionDraft] = useState<{ title: string; channelId: string; entries: { emoji: string; roleId: string; label: string; description: string }[] }>({ title: '', channelId: '', entries: [] });
const [supportLogin, setSupportLogin] = useState<{ config: SupportLoginConfig; status: SupportLoginStatus; supportRoleId?: string } | null>(null); const [supportLogin, setSupportLogin] = useState<{ config: SupportLoginConfig; status: SupportLoginStatus; supportRoleId?: string } | null>(null);
const [registerForms, setRegisterForms] = useState<RegisterForm[]>([]); const [registerForms, setRegisterForms] = useState<RegisterForm[]>([]);
const [registerApps, setRegisterApps] = useState<RegisterApplication[]>([]); const [registerApps, setRegisterApps] = useState<RegisterApplication[]>([]);
@@ -190,6 +194,8 @@ export function AppProvider({ children }: { children: ReactNode }) {
const [taskDraft, setTaskDraft] = useState({ title: '', description: '' }); const [taskDraft, setTaskDraft] = useState({ title: '', description: '' });
const [watchlistEntries, setWatchlistEntries] = useState<WatchlistEntry[]>([]); const [watchlistEntries, setWatchlistEntries] = useState<WatchlistEntry[]>([]);
const [growthStats, setGrowthStats] = useState<GrowthStats | null>(null); const [growthStats, setGrowthStats] = useState<GrowthStats | null>(null);
const [inviteBreakdown, setInviteBreakdown] = useState<InviteBreakdownEntry[]>([]);
const [recentJoins, setRecentJoins] = useState<RecentJoinEntry[]>([]);
const setSection = useCallback((key: NavKey) => { const setSection = useCallback((key: NavKey) => {
setSectionState(key); setSectionState(key);
@@ -277,7 +283,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
setRegisterApps(registerAppsRes.applications || []); setRegisterApps(registerAppsRes.applications || []);
setTasks(tasksRes.tasks || []); setTasks(tasksRes.tasks || []);
setWatchlistEntries(watchlistRes.entries || []); setWatchlistEntries(watchlistRes.entries || []);
setReactionDraft({ title: '', channelId: '', entries: '' }); setReactionDraft({ title: '', channelId: '', entries: [] });
await Promise.all([loadTicketData(guildId), loadAdminData()]); await Promise.all([loadTicketData(guildId), loadAdminData()]);
setStatusMessage(''); setStatusMessage('');
} catch { setStatusMessage('Daten konnten nicht geladen werden'); } } catch { setStatusMessage('Daten konnten nicht geladen werden'); }
@@ -356,11 +362,10 @@ export function AppProvider({ children }: { children: ReactNode }) {
} }
async function saveReactionRole() { async function saveReactionRole() {
const entries = reactionDraft.entries.split('\n').map((l) => l.trim()).filter(Boolean) const entries = (reactionDraft.entries as any[]).filter((e) => e.emoji && e.roleId);
.map((line) => { const p = line.split('|').map((s) => s.trim()); return { emoji: p[0], roleId: p[1], label: p[2], description: p[3] }; })
.filter((e) => e.emoji && e.roleId);
await apiFetch('/reactionroles', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, channelId: reactionDraft.channelId, title: reactionDraft.title, entries }) }); await apiFetch('/reactionroles', { method: 'POST', body: JSON.stringify({ guildId: currentGuildId, channelId: reactionDraft.channelId, title: reactionDraft.title, entries }) });
await loadGuildData(currentGuildId); await loadGuildData(currentGuildId);
setReactionDraft({ title: '', channelId: '', entries: [] });
setStatusMessage('Reaction Role gespeichert'); setStatusMessage('Reaction Role gespeichert');
} }
@@ -606,6 +611,13 @@ export function AppProvider({ children }: { children: ReactNode }) {
setGrowthStats(res.stats || null); setGrowthStats(res.stats || null);
} }
async function loadInviteBreakdown() {
if (!currentGuildId) return;
const res = await apiFetch<any>(`/growth/invites?guildId=${encodeURIComponent(currentGuildId)}`);
setInviteBreakdown(res.breakdown || []);
setRecentJoins(res.recentJoins || []);
}
const handleLogout = useCallback(() => { const handleLogout = useCallback(() => {
window.location.href = `${appConfig.baseAuth || '/auth'}/logout`; window.location.href = `${appConfig.baseAuth || '/auth'}/logout`;
}, []); }, []);
@@ -621,6 +633,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
ticketDetail, ticketMessages, kbEditDraft, automationEditDraft, ticketDetail, ticketMessages, kbEditDraft, automationEditDraft,
automodStrikes, registerStatusFilter, registerFormFilter, selectedAppId, automodStrikes, registerStatusFilter, registerFormFilter, selectedAppId,
appNotes, appHistory, noteDraft, tasks, taskDraft, watchlistEntries, growthStats, appNotes, appHistory, noteDraft, tasks, taskDraft, watchlistEntries, growthStats,
inviteBreakdown, recentJoins,
setCurrentGuildId, setSection, setSettings, setBirthday, setSupportLogin, setCurrentGuildId, setSection, setSettings, setBirthday, setSupportLogin,
setStatusDraft, setStatsDraft, setStatusMessage, loadGuildData, setStatusDraft, setStatsDraft, setStatusMessage, loadGuildData,
saveSettingsPayload, saveBirthday, saveStatuspage, saveServerStats, saveSettingsPayload, saveBirthday, saveStatuspage, saveServerStats,
@@ -636,7 +649,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
loadAutomodStrikes, resetAutomodStrike, setRegisterStatusFilter, setRegisterFormFilter, loadAutomodStrikes, resetAutomodStrike, setRegisterStatusFilter, setRegisterFormFilter,
loadRegisterApps, openAppDetail, setNoteDraft, addAppNote, loadRegisterApps, openAppDetail, setNoteDraft, addAppNote,
setTaskDraft, createTask, updateTaskStatus, deleteTask, setTaskDraft, createTask, updateTaskStatus, deleteTask,
loadWatchlist, removeFromWatchlist, loadGrowthStats, loadWatchlist, removeFromWatchlist, loadGrowthStats, loadInviteBreakdown,
}}> }}>
{children} {children}
</AppContext.Provider> </AppContext.Provider>

View File

@@ -1,16 +1,20 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Card, CardContent, CardHeader } from '@heroui/react'; import { Card, CardContent, CardHeader, Chip } from '@heroui/react';
import { TrendingUp, TrendingDown, Gem, Link2, Handshake } from 'lucide-react'; import { TrendingUp, TrendingDown, Gem, Link2, Handshake, Users } from 'lucide-react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import { StatCard } from '../components/shared/StatCard'; import { StatCard } from '../components/shared/StatCard';
import { BarComparisonChart } from '../components/shared/BarComparisonChart'; import { BarComparisonChart } from '../components/shared/BarComparisonChart';
import { formatDate } from '../utils/formatters';
export function Growth() { export function Growth() {
const { currentGuildId, growthStats, loadGrowthStats } = useApp(); const { currentGuildId, growthStats, loadGrowthStats, inviteBreakdown, recentJoins, loadInviteBreakdown } = useApp();
useEffect(() => { useEffect(() => {
if (currentGuildId) loadGrowthStats(); if (currentGuildId) {
loadGrowthStats();
loadInviteBreakdown();
}
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentGuildId]); }, [currentGuildId]);
@@ -71,6 +75,64 @@ export function Growth() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
<div className="mt-5 grid gap-5 xl:grid-cols-2">
<Card>
<CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Invite-Übersicht (30 Tage)</h3>
</CardHeader>
<CardContent className="flex flex-col gap-2 p-5">
{inviteBreakdown.length ? inviteBreakdown.map((inv) => (
<div key={inv.code} className="bg-surface-tertiary flex items-center justify-between gap-2 rounded-xl px-4 py-3 text-sm">
<div className="min-w-0">
<div className="font-medium truncate">discord.gg/{inv.code}</div>
<div className="text-xs text-muted truncate">
{inv.inviterId ? `von <@${inv.inviterId}>` : 'Unbekannter Ersteller'} · {inv.uses} Nutzungen gesamt
</div>
</div>
<div className="flex shrink-0 items-center gap-2">
<Chip size="sm" variant="soft" color="accent">{inv.joins30} Joins</Chip>
{inv.suspiciousJoins30 > 0 && (
<Chip size="sm" variant="soft" color="danger">{inv.suspiciousJoins30} auffällig</Chip>
)}
</div>
</div>
)) : (
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
<Link2 size={20} />
Keine Invite-Nutzung in den letzten 30 Tagen
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Letzte Beitritte</h3>
</CardHeader>
<CardContent className="flex max-h-96 flex-col gap-2 overflow-y-auto p-5">
{recentJoins.length ? recentJoins.map((j) => (
<div key={j.id} className="bg-surface-tertiary flex items-center gap-3 rounded-xl px-4 py-3 text-sm">
<Users size={14} className="text-muted shrink-0" />
<div className="min-w-0 flex-1">
<div className="truncate">
User-ID {j.userId}{j.inviterId ? ` · eingeladen von ${j.inviterId}` : ''}
</div>
<div className="text-xs text-muted">
{j.inviteCode ? `discord.gg/${j.inviteCode}` : 'Unbekannter Invite'} · {formatDate(j.createdAt)}
</div>
</div>
{j.suspicious && <Chip size="sm" variant="soft" color="danger">Verdächtig</Chip>}
</div>
)) : (
<div className="flex flex-col items-center gap-2 py-4 text-center text-xs text-muted">
<Users size={20} />
Noch keine Beitritte erfasst
</div>
)}
</CardContent>
</Card>
</div>
</SectionCard> </SectionCard>
); );
} }

View File

@@ -1,13 +1,29 @@
import { Card, CardContent, CardHeader, Input, TextArea, Button, Chip, Separator, TextField, Label } from '@heroui/react'; import { useState } from 'react';
import { Tag, Save, Hash, List } from 'lucide-react'; import { Card, CardContent, CardHeader, Input, Button, Chip, TextField, Label } from '@heroui/react';
import { Tag, Save, Plus, X } from 'lucide-react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import { ChannelSelect } from '../components/shared/ChannelSelect'; import { ChannelSelect } from '../components/shared/ChannelSelect';
import { RoleSelect } from '../components/shared/RoleSelect';
import { useGuildResources } from '../hooks/useGuildResources'; import { useGuildResources } from '../hooks/useGuildResources';
export function ReactionRoles() { export function ReactionRoles() {
const { reactionRoles, reactionDraft, setReactionDraft, saveReactionRole, currentGuildId } = useApp(); const { reactionRoles, reactionDraft, setReactionDraft, saveReactionRole, currentGuildId } = useApp();
const { channels } = useGuildResources(currentGuildId); const { channels, roles } = useGuildResources(currentGuildId);
const [entryDraft, setEntryDraft] = useState({ emoji: '', roleId: '', label: '' });
const addEntry = () => {
if (!entryDraft.emoji.trim() || !entryDraft.roleId) return;
setReactionDraft((s) => ({
...s,
entries: [...s.entries, { emoji: entryDraft.emoji.trim(), roleId: entryDraft.roleId, label: entryDraft.label.trim(), description: '' }]
}));
setEntryDraft({ emoji: '', roleId: '', label: '' });
};
const removeEntry = (index: number) => {
setReactionDraft((s) => ({ ...s, entries: s.entries.filter((_: any, i: number) => i !== index) }));
};
return ( return (
<SectionCard title="Reaction Roles" subtitle="Sets anzeigen und neue Zuordnungen anlegen"> <SectionCard title="Reaction Roles" subtitle="Sets anzeigen und neue Zuordnungen anlegen">
@@ -61,20 +77,56 @@ export function ReactionRoles() {
/> />
</TextField> </TextField>
<TextField> <div>
<Label>Einträge</Label> <Label>Einträge</Label>
<TextArea <div className="mt-2 flex flex-col gap-2">
placeholder="Emoji | Role ID | Label&#10;:emoji: | 123456789 | Rolle 1&#10;:wave: | 987654321 | Rolle 2" {reactionDraft.entries.map((entry, i) => {
rows={6} const role = roles.find((r) => r.id === entry.roleId);
value={reactionDraft.entries} return (
onChange={(e) => setReactionDraft((s) => ({ ...s, entries: e.target.value }))} <div key={i} className="bg-default-soft flex items-center gap-2 rounded-xl py-1.5 pl-3 pr-1.5 text-sm">
/> <span>{entry.emoji}</span>
<p className="mt-1 text-xs text-muted"> {role?.color && <span className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: role.color }} />}
Pro Zeile: Emoji | Role ID | Label (optional) | Beschreibung (optional) <span className="min-w-0 flex-1 truncate">{entry.label || role?.name || entry.roleId}</span>
</p> <button
</TextField> type="button"
className="flex size-5 shrink-0 items-center justify-center rounded-full text-muted hover:bg-danger-soft hover:text-danger"
onClick={() => removeEntry(i)}
>
<X size={12} />
</button>
</div>
);
})}
{!reactionDraft.entries.length && <p className="text-xs text-muted">Noch keine Einträge hinzugefügt.</p>}
</div>
<Button variant="primary" onPress={saveReactionRole}> <div className="mt-3 flex flex-col gap-2 rounded-xl border border-border p-3">
<div className="flex gap-2">
<Input
className="w-16 shrink-0"
placeholder="😀"
value={entryDraft.emoji}
onChange={(e) => setEntryDraft((s) => ({ ...s, emoji: e.target.value }))}
/>
<RoleSelect
options={roles}
value={entryDraft.roleId}
onChange={(id) => setEntryDraft((s) => ({ ...s, roleId: id }))}
placeholder="Rolle wählen"
/>
</div>
<Input
placeholder="Label (optional, z.B. Gamer)"
value={entryDraft.label}
onChange={(e) => setEntryDraft((s) => ({ ...s, label: e.target.value }))}
/>
<Button size="sm" variant="tertiary" onPress={addEntry} isDisabled={!entryDraft.emoji.trim() || !entryDraft.roleId}>
<Plus size={14} /> Eintrag hinzufügen
</Button>
</div>
</div>
<Button variant="primary" onPress={saveReactionRole} isDisabled={!reactionDraft.entries.length || !reactionDraft.channelId}>
<Save size={16} /> Reaction Role speichern <Save size={16} /> Reaction Role speichern
</Button> </Button>
</CardContent> </CardContent>

View File

@@ -1,10 +1,11 @@
import { Card, CardContent, CardHeader, Button, Separator, TextField, Label } from '@heroui/react'; import { Card, CardContent, CardHeader, Button, Separator, TextField, Label } from '@heroui/react';
import { Settings, Save, Logs, Bell, Shield, Edit3, Trash2 } from 'lucide-react'; import { Settings, Save, Logs, Bell, Shield, Edit3, Trash2, ImageIcon, QrCode } from 'lucide-react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import { ChannelSelect } from '../components/shared/ChannelSelect'; import { ChannelSelect } from '../components/shared/ChannelSelect';
import { RoleSelect } from '../components/shared/RoleSelect'; import { RoleSelect } from '../components/shared/RoleSelect';
import { AppSwitch } from '../components/shared/AppSwitch'; import { AppSwitch } from '../components/shared/AppSwitch';
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
import { useGuildResources } from '../hooks/useGuildResources'; import { useGuildResources } from '../hooks/useGuildResources';
export function SettingsPage() { export function SettingsPage() {
@@ -99,6 +100,34 @@ export function SettingsPage() {
</Button> </Button>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader className="px-5 pt-5 pb-0">
<h3 className="text-base font-semibold">Bild-Moderation</h3>
</CardHeader>
<CardContent className="flex flex-col gap-4 p-5">
<ModuleActiveToggle
icon={<ImageIcon size={16} />}
title="Bild-Moderation aktiv"
description="Scannt Bild-Anhänge auf QR-Codes und gleicht sie mit bekannten Scam-Mustern ab."
isSelected={settings.imageModerationConfig?.enabled === true}
onChange={(v) => setSettings((s) => ({ ...s, imageModerationConfig: { ...(s.imageModerationConfig || {}), enabled: v } }))}
/>
<AppSwitch
isSelected={settings.imageModerationConfig?.alertOnly === true}
onChange={(v) => setSettings((s) => ({ ...s, imageModerationConfig: { ...(s.imageModerationConfig || {}), alertOnly: v } }))}
label={<div className="flex items-center gap-2"><QrCode size={14} /> Nur bei erkanntem Scam-Verdacht alarmieren</div>}
description="Wenn aus, wird bei jedem gefundenen QR-Code alarmiert, nicht nur bei verdächtigen."
/>
<Separator />
<Button variant="primary" onPress={() => saveSettingsPayload(settings, 'Settings gespeichert')}>
<Save size={16} /> Speichern
</Button>
</CardContent>
</Card>
</div> </div>
</SectionCard> </SectionCard>
); );

View File

@@ -1,5 +1,5 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, TextArea, Button, Separator, TextField, Label } from '@heroui/react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Input, TextArea, Button, Separator, TextField, Label } from '@heroui/react';
import { Sparkles, Save } from 'lucide-react'; import { Sparkles, Save, X } from 'lucide-react';
import { useApp } from '../context/AppContext'; import { useApp } from '../context/AppContext';
import { SectionCard } from '../components/shared/SectionCard'; import { SectionCard } from '../components/shared/SectionCard';
import { DiscordPreview } from '../components/shared/DiscordPreview'; import { DiscordPreview } from '../components/shared/DiscordPreview';
@@ -7,10 +7,28 @@ import { ChannelSelect } from '../components/shared/ChannelSelect';
import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle'; import { ModuleActiveToggle } from '../components/shared/ModuleActiveToggle';
import { useGuildResources } from '../hooks/useGuildResources'; import { useGuildResources } from '../hooks/useGuildResources';
const MAX_IMAGE_BYTES = 3 * 1024 * 1024;
export function Welcome() { export function Welcome() {
const { settings, setSettings, saveSettingsPayload, currentGuildId } = useApp(); const { settings, setSettings, saveSettingsPayload, currentGuildId } = useApp();
const { channels } = useGuildResources(currentGuildId); const { channels } = useGuildResources(currentGuildId);
const imagePreview = settings.welcomeConfig?.embedImageData || settings.welcomeConfig?.embedImage;
const handleImageUpload = (file: File) => {
if (file.size > MAX_IMAGE_BYTES) {
alert('Bild ist zu groß (max. 3 MB).');
return;
}
const reader = new FileReader();
reader.onload = () => {
setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedImageData: reader.result as string, embedImage: undefined } }));
};
reader.readAsDataURL(file);
};
const clearImage = () => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedImage: undefined, embedImageData: undefined } }));
return ( return (
<SectionCard title="Willkommen" subtitle="Welcome-Embeds und Join-Nachrichten"> <SectionCard title="Willkommen" subtitle="Welcome-Embeds und Join-Nachrichten">
<div className="grid gap-5 xl:grid-cols-2"> <div className="grid gap-5 xl:grid-cols-2">
@@ -67,6 +85,38 @@ export function Welcome() {
/> />
</TextField> </TextField>
<TextField>
<Label>Bild-URL</Label>
<Input
placeholder="https://..."
value={settings.welcomeConfig?.embedImage || ''}
onChange={(e) => setSettings((s) => ({ ...s, welcomeConfig: { ...(s.welcomeConfig || {}), embedImage: e.target.value, embedImageData: undefined } }))}
/>
</TextField>
<TextField>
<Label>oder Bild hochladen</Label>
<input
type="file"
accept="image/png,image/jpeg,image/gif,image/webp"
className="text-sm text-muted"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleImageUpload(file);
e.target.value = '';
}}
/>
</TextField>
{imagePreview && (
<div className="flex items-center gap-2">
<img src={imagePreview} alt="Vorschau" className="h-12 w-20 rounded object-cover" />
<Button size="sm" variant="danger-soft" onPress={clearImage}>
<X size={14} /> Bild entfernen
</Button>
</div>
)}
<Separator /> <Separator />
<Button size="lg" variant="primary" onPress={() => saveSettingsPayload({ welcomeConfig: settings.welcomeConfig || {} }, 'Welcome gespeichert')}> <Button size="lg" variant="primary" onPress={() => saveSettingsPayload({ welcomeConfig: settings.welcomeConfig || {} }, 'Welcome gespeichert')}>
@@ -87,6 +137,7 @@ export function Welcome() {
title={settings.welcomeConfig?.embedTitle || 'Willkommen!'} title={settings.welcomeConfig?.embedTitle || 'Willkommen!'}
description={settings.welcomeConfig?.embedDescription || 'Willkommen auf dem Server!'} description={settings.welcomeConfig?.embedDescription || 'Willkommen auf dem Server!'}
footer={settings.welcomeConfig?.embedFooter} footer={settings.welcomeConfig?.embedFooter}
image={imagePreview}
/> />
<p className="text-sm text-muted"> <p className="text-sm text-muted">

View File

@@ -179,6 +179,23 @@ export type GrowthStats = {
dailyJoins: { day: string; count: number }[]; dailyJoins: { day: string; count: number }[];
}; };
export type InviteBreakdownEntry = {
code: string;
inviterId?: string;
uses: number;
joins30: number;
suspiciousJoins30: number;
};
export type RecentJoinEntry = {
id: string;
userId: string;
inviterId?: string | null;
inviteCode?: string | null;
suspicious: boolean;
createdAt: string;
};
export type MusicSession = { export type MusicSession = {
guildId: string; guildId: string;
nowPlaying?: { title: string; url: string } | null; nowPlaying?: { title: string; url: string } | null;

17
node_modules/.bin/acorn generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../acorn/bin/acorn
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../acorn/bin/acorn" "$@"
else
exec node "$basedir/../acorn/bin/acorn" "$@"
fi

17
node_modules/.bin/mime generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../mime/cli.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mime/cli.js" "$@"
else
exec node "$basedir/../mime/cli.js" "$@"
fi

17
node_modules/.bin/mkdirp generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../mkdirp/bin/cmd.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mkdirp/bin/cmd.js" "$@"
else
exec node "$basedir/../mkdirp/bin/cmd.js" "$@"
fi

17
node_modules/.bin/prisma generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../prisma/build/index.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../prisma/build/index.js" "$@"
else
exec node "$basedir/../prisma/build/index.js" "$@"
fi

17
node_modules/.bin/resolve generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../resolve/bin/resolve
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../resolve/bin/resolve" "$@"
else
exec node "$basedir/../resolve/bin/resolve" "$@"
fi

17
node_modules/.bin/rimraf generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../rimraf/bin.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../rimraf/bin.js" "$@"
else
exec node "$basedir/../rimraf/bin.js" "$@"
fi

17
node_modules/.bin/tree-kill generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../tree-kill/cli.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../tree-kill/cli.js" "$@"
else
exec node "$basedir/../tree-kill/cli.js" "$@"
fi

17
node_modules/.bin/ts-node generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../ts-node/dist/bin.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin.js" "$@"
fi

17
node_modules/.bin/ts-node-cwd generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../ts-node/dist/bin-cwd.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin-cwd.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin-cwd.js" "$@"
fi

17
node_modules/.bin/ts-node-dev generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../ts-node-dev/lib/bin.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node-dev/lib/bin.js" "$@"
else
exec node "$basedir/../ts-node-dev/lib/bin.js" "$@"
fi

17
node_modules/.bin/ts-node-esm generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../ts-node/dist/bin-esm.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin-esm.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin-esm.js" "$@"
fi

17
node_modules/.bin/ts-node-script generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../ts-node/dist/bin-script.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin-script.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin-script.js" "$@"
fi

View File

@@ -1,16 +1 @@
#!/bin/sh ../ts-node/dist/bin-transpile.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin-transpile.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin-transpile.js" "$@"
fi

17
node_modules/.bin/ts-script generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../ts-node/dist/bin-script-deprecated.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node/dist/bin-script-deprecated.js" "$@"
else
exec node "$basedir/../ts-node/dist/bin-script-deprecated.js" "$@"
fi

17
node_modules/.bin/tsc generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../typescript/bin/tsc
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi

17
node_modules/.bin/tsnd generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../ts-node-dev/lib/bin.js
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../ts-node-dev/lib/bin.js" "$@"
else
exec node "$basedir/../ts-node-dev/lib/bin.js" "$@"
fi

17
node_modules/.bin/tsserver generated vendored
View File

@@ -1,16 +1 @@
#!/bin/sh ../typescript/bin/tsserver
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi

823
node_modules/.package-lock.json generated vendored
View File

@@ -4,6 +4,16 @@
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"node_modules/@borewit/text-codec": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
"integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -264,6 +274,430 @@
"scripts/actions/documentation" "scripts/actions/documentation"
] ]
}, },
"node_modules/@jimp/core": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz",
"integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==",
"license": "MIT",
"dependencies": {
"@jimp/file-ops": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"await-to-js": "^3.0.0",
"exif-parser": "^0.1.12",
"file-type": "^21.3.3",
"mime": "3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/core/node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@jimp/diff": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz",
"integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==",
"license": "MIT",
"dependencies": {
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"pixelmatch": "^5.3.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/file-ops": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz",
"integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-bmp": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz",
"integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"bmp-ts": "^1.0.9"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-gif": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz",
"integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"gifwrap": "^0.10.1",
"omggif": "^1.0.10"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-jpeg": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz",
"integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"jpeg-js": "^0.4.4"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-png": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz",
"integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"pngjs": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-tiff": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz",
"integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"utif2": "^4.1.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-blit": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz",
"integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-blur": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz",
"integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/utils": "1.6.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-circle": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz",
"integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-color": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz",
"integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"tinycolor2": "^1.6.0",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-contain": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz",
"integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/plugin-blit": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-cover": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz",
"integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/plugin-crop": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-crop": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz",
"integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-displace": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz",
"integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-dither": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz",
"integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-fisheye": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz",
"integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-flip": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz",
"integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-hash": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz",
"integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/js-bmp": "1.6.1",
"@jimp/js-jpeg": "1.6.1",
"@jimp/js-png": "1.6.1",
"@jimp/js-tiff": "1.6.1",
"@jimp/plugin-color": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"any-base": "^1.1.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-mask": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz",
"integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-print": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz",
"integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/js-jpeg": "1.6.1",
"@jimp/js-png": "1.6.1",
"@jimp/plugin-blit": "1.6.1",
"@jimp/types": "1.6.1",
"parse-bmfont-ascii": "^1.0.6",
"parse-bmfont-binary": "^1.0.6",
"parse-bmfont-xml": "^1.1.6",
"simple-xml-to-json": "^1.2.2",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-quantize": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz",
"integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==",
"license": "MIT",
"dependencies": {
"image-q": "^4.0.0",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-resize": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz",
"integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-rotate": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz",
"integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/plugin-crop": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-threshold": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz",
"integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/plugin-color": "1.6.1",
"@jimp/plugin-hash": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/types": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz",
"integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==",
"license": "MIT",
"dependencies": {
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/utils": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz",
"integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"tinycolor2": "^1.6.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -421,22 +855,84 @@
"@snazzah/davey-win32-x64-msvc": "0.1.8" "@snazzah/davey-win32-x64-msvc": "0.1.8"
} }
}, },
"node_modules/@snazzah/davey-win32-x64-msvc": { "node_modules/@snazzah/davey-linux-x64-gnu": {
"version": "0.1.8", "version": "0.1.8",
"resolved": "https://registry.npmjs.org/@snazzah/davey-win32-x64-msvc/-/davey-win32-x64-msvc-0.1.8.tgz", "resolved": "https://registry.npmjs.org/@snazzah/davey-linux-x64-gnu/-/davey-linux-x64-gnu-0.1.8.tgz",
"integrity": "sha512-JKIco1miwtM4NgVwU/H9TJdUaSlJ+kdtydy3+tiV9cmJv0u1SM2NpwjV85H44xAQWW2zcRBHP0ZDriciHw09qQ==", "integrity": "sha512-yghgG7iXZUHy734Cq3PcgrbRnLhhB233JNTX5VPRxRqdwFAg2MzAJ2iSWpP12K6hSqKq9hw0sdt8CNpr0mEXjQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "linux"
], ],
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@snazzah/davey-linux-x64-musl": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/@snazzah/davey-linux-x64-musl/-/davey-linux-x64-musl-0.1.8.tgz",
"integrity": "sha512-WwCiAge27ZOEu7NRx5NFjpCAkGcUGHGtoROBY4ElRYE0Tp3DfuzWU06qSu6JBQPlzhTTBN29X2/kGP8iRUwqnQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tokenizer/inflate": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
"integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"token-types": "^6.1.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/@tokenizer/inflate/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@tokenizer/inflate/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"license": "MIT"
},
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -724,6 +1220,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/any-base": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==",
"license": "MIT"
},
"node_modules/anymatch": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -771,6 +1273,15 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/await-to-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -831,6 +1342,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/bmp-ts": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz",
"integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==",
"license": "MIT"
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -1279,6 +1796,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/exif-parser": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="
},
"node_modules/express": { "node_modules/express": {
"version": "4.21.2", "version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -1381,6 +1903,24 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/file-type": {
"version": "21.3.4",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz",
"integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==",
"license": "MIT",
"dependencies": {
"@tokenizer/inflate": "^0.4.1",
"strtok3": "^10.3.4",
"token-types": "^6.1.1",
"uint8array-extras": "^1.4.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1527,6 +2067,16 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/gifwrap": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz",
"integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==",
"license": "MIT",
"dependencies": {
"image-q": "^4.0.0",
"omggif": "^1.0.10"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1682,6 +2232,41 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/image-q": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz",
"integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==",
"license": "MIT",
"dependencies": {
"@types/node": "16.9.1"
}
},
"node_modules/image-q/node_modules/@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
"license": "MIT"
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -1779,6 +2364,56 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/jimp": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz",
"integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/diff": "1.6.1",
"@jimp/js-bmp": "1.6.1",
"@jimp/js-gif": "1.6.1",
"@jimp/js-jpeg": "1.6.1",
"@jimp/js-png": "1.6.1",
"@jimp/js-tiff": "1.6.1",
"@jimp/plugin-blit": "1.6.1",
"@jimp/plugin-blur": "1.6.1",
"@jimp/plugin-circle": "1.6.1",
"@jimp/plugin-color": "1.6.1",
"@jimp/plugin-contain": "1.6.1",
"@jimp/plugin-cover": "1.6.1",
"@jimp/plugin-crop": "1.6.1",
"@jimp/plugin-displace": "1.6.1",
"@jimp/plugin-dither": "1.6.1",
"@jimp/plugin-fisheye": "1.6.1",
"@jimp/plugin-flip": "1.6.1",
"@jimp/plugin-hash": "1.6.1",
"@jimp/plugin-mask": "1.6.1",
"@jimp/plugin-print": "1.6.1",
"@jimp/plugin-quantize": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/plugin-rotate": "1.6.1",
"@jimp/plugin-threshold": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause"
},
"node_modules/jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
"license": "Apache-2.0"
},
"node_modules/libsodium": { "node_modules/libsodium": {
"version": "0.7.15", "version": "0.7.15",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz",
@@ -2080,6 +2715,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/omggif": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
"license": "MIT"
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -2110,6 +2751,34 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parse-bmfont-ascii": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
"integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==",
"license": "MIT"
},
"node_modules/parse-bmfont-binary": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz",
"integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==",
"license": "MIT"
},
"node_modules/parse-bmfont-xml": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz",
"integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==",
"license": "MIT",
"dependencies": {
"xml-parse-from-string": "^1.0.0",
"xml2js": "^0.5.0"
}
},
"node_modules/parse-cache-control": { "node_modules/parse-cache-control": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
@@ -2159,6 +2828,27 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pixelmatch": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
"integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
"license": "ISC",
"dependencies": {
"pngjs": "^6.0.0"
},
"bin": {
"pixelmatch": "bin/pixelmatch"
}
},
"node_modules/pixelmatch/node_modules/pngjs": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
"license": "MIT",
"engines": {
"node": ">=12.13.0"
}
},
"node_modules/play-audio": { "node_modules/play-audio": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz", "resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz",
@@ -2177,6 +2867,15 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/prism-media": { "node_modules/prism-media": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
@@ -2393,6 +3092,15 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -2549,6 +3257,15 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/simple-xml-to-json": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz",
"integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==",
"license": "MIT",
"engines": {
"node": ">=20.12.2"
}
},
"node_modules/sodium-native": { "node_modules/sodium-native": {
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
@@ -2643,6 +3360,22 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/strtok3": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
"integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -2673,6 +3406,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
"license": "MIT"
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2695,6 +3434,24 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/token-types": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
"integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
"license": "MIT",
"dependencies": {
"@borewit/text-codec": "^0.2.1",
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -2860,6 +3617,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/uint8array-extras": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici": { "node_modules/undici": {
"version": "6.21.3", "version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
@@ -2884,6 +3653,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/utif2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz",
"integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.11"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -2967,6 +3745,34 @@
} }
} }
}, },
"node_modules/xml-parse-from-string": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
"integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==",
"license": "MIT"
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -2992,6 +3798,15 @@
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

25
node_modules/.prisma/client/edge.js generated vendored

File diff suppressed because one or more lines are too long

View File

@@ -151,6 +151,8 @@ exports.Prisma.GuildSettingsScalarFieldEnum = {
brandingConfig: 'brandingConfig', brandingConfig: 'brandingConfig',
partnerConfig: 'partnerConfig', partnerConfig: 'partnerConfig',
galleryConfig: 'galleryConfig', galleryConfig: 'galleryConfig',
imageModerationConfig: 'imageModerationConfig',
ticketConfig: 'ticketConfig',
supportRoleId: 'supportRoleId', supportRoleId: 'supportRoleId',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdAt: 'createdAt' createdAt: 'createdAt'
@@ -469,6 +471,20 @@ exports.Prisma.GuildGrowthEventScalarFieldEnum = {
type: 'type', type: 'type',
userId: 'userId', userId: 'userId',
inviteCode: 'inviteCode', inviteCode: 'inviteCode',
inviterId: 'inviterId',
suspicious: 'suspicious',
createdAt: 'createdAt'
};
exports.Prisma.InfoPanelScalarFieldEnum = {
id: 'id',
guildId: 'guildId',
channelId: 'channelId',
messageId: 'messageId',
type: 'type',
title: 'title',
description: 'description',
items: 'items',
createdAt: 'createdAt' createdAt: 'createdAt'
}; };
@@ -534,7 +550,8 @@ exports.Prisma.ModelName = {
PartnerRequest: 'PartnerRequest', PartnerRequest: 'PartnerRequest',
GalleryPost: 'GalleryPost', GalleryPost: 'GalleryPost',
GalleryVote: 'GalleryVote', GalleryVote: 'GalleryVote',
GuildGrowthEvent: 'GuildGrowthEvent' GuildGrowthEvent: 'GuildGrowthEvent',
InfoPanel: 'InfoPanel'
}; };
/** /**

1321
node_modules/.prisma/client/index.d.ts generated vendored

File diff suppressed because it is too large Load Diff

25
node_modules/.prisma/client/index.js generated vendored

File diff suppressed because one or more lines are too long

19
node_modules/.prisma/client/wasm.js generated vendored
View File

@@ -151,6 +151,8 @@ exports.Prisma.GuildSettingsScalarFieldEnum = {
brandingConfig: 'brandingConfig', brandingConfig: 'brandingConfig',
partnerConfig: 'partnerConfig', partnerConfig: 'partnerConfig',
galleryConfig: 'galleryConfig', galleryConfig: 'galleryConfig',
imageModerationConfig: 'imageModerationConfig',
ticketConfig: 'ticketConfig',
supportRoleId: 'supportRoleId', supportRoleId: 'supportRoleId',
updatedAt: 'updatedAt', updatedAt: 'updatedAt',
createdAt: 'createdAt' createdAt: 'createdAt'
@@ -469,6 +471,20 @@ exports.Prisma.GuildGrowthEventScalarFieldEnum = {
type: 'type', type: 'type',
userId: 'userId', userId: 'userId',
inviteCode: 'inviteCode', inviteCode: 'inviteCode',
inviterId: 'inviterId',
suspicious: 'suspicious',
createdAt: 'createdAt'
};
exports.Prisma.InfoPanelScalarFieldEnum = {
id: 'id',
guildId: 'guildId',
channelId: 'channelId',
messageId: 'messageId',
type: 'type',
title: 'title',
description: 'description',
items: 'items',
createdAt: 'createdAt' createdAt: 'createdAt'
}; };
@@ -534,7 +550,8 @@ exports.Prisma.ModelName = {
PartnerRequest: 'PartnerRequest', PartnerRequest: 'PartnerRequest',
GalleryPost: 'GalleryPost', GalleryPost: 'GalleryPost',
GalleryVote: 'GalleryVote', GalleryVote: 'GalleryVote',
GuildGrowthEvent: 'GuildGrowthEvent' GuildGrowthEvent: 'GuildGrowthEvent',
InfoPanel: 'InfoPanel'
}; };
/** /**

802
package-lock.json generated
View File

@@ -20,6 +20,8 @@
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"ffmpeg-static": "^5.2.0", "ffmpeg-static": "^5.2.0",
"jimp": "^1.6.1",
"jsqr": "^1.4.0",
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"play-dl": "^1.9.7", "play-dl": "^1.9.7",
"sodium-native": "^4.0.4" "sodium-native": "^4.0.4"
@@ -34,6 +36,16 @@
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }
}, },
"node_modules/@borewit/text-codec": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
"integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -325,6 +337,430 @@
"tslib": "^2.4.0" "tslib": "^2.4.0"
} }
}, },
"node_modules/@jimp/core": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/core/-/core-1.6.1.tgz",
"integrity": "sha512-+BoKC5G6hkrSy501zcJ2EpfnllP+avPevcBfRcZe/CW+EwEfY6X1EZ8QWyT7NpDIvEEJb1fdJnMMfUnFkxmw9A==",
"license": "MIT",
"dependencies": {
"@jimp/file-ops": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"await-to-js": "^3.0.0",
"exif-parser": "^0.1.12",
"file-type": "^21.3.3",
"mime": "3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/core/node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
"license": "MIT",
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@jimp/diff": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/diff/-/diff-1.6.1.tgz",
"integrity": "sha512-YkKDPdHjLgo1Api3+Bhc0GLAygldlpt97NfOKoNg1U6IUNXA6X2MgosCjPfSBiSvJvrrz1fsIR+/4cfYXBI/HQ==",
"license": "MIT",
"dependencies": {
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"pixelmatch": "^5.3.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/file-ops": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/file-ops/-/file-ops-1.6.1.tgz",
"integrity": "sha512-T+gX6osHjprbDRad0/B71Evyre7ZdVY1z/gFGEG9Z8KOtZPKboWvPeP2UjbZYWQLy9UKCPQX1FNAnDiOPkJL7w==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-bmp": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-bmp/-/js-bmp-1.6.1.tgz",
"integrity": "sha512-xzWzNT4/u5zGrTT3Tme9sGU7YzIKxi13+BCQwLqACbt5DXf9SAfdzRkopZQnmDko+6In5nqaT89Gjs43/WdnYQ==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"bmp-ts": "^1.0.9"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-gif": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-gif/-/js-gif-1.6.1.tgz",
"integrity": "sha512-YjY2W26rQa05XhanYhRZ7dingCiNN+T2Ymb1JiigIbABY0B28wHE3v3Cf1/HZPWGu0hOg36ylaKgV5KxF2M58w==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"gifwrap": "^0.10.1",
"omggif": "^1.0.10"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-jpeg": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-jpeg/-/js-jpeg-1.6.1.tgz",
"integrity": "sha512-HT9H3yOmlOFzYmdI15IYdfy6ggQhSRIaHeA+OTJSEORXBqEo97sUZu/DsgHIcX5NJ7TkJBTgZ9BZXsV6UbsyMg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"jpeg-js": "^0.4.4"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-png": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-png/-/js-png-1.6.1.tgz",
"integrity": "sha512-SZ/KVhI5UjcSzzlXsXdIi/LhJ7UShf2NkMOtVrbZQcGzsqNtynAelrOXeoTxcanfVqmNhAoVHg8yR2cYoqrYjA==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"pngjs": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/js-tiff": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/js-tiff/-/js-tiff-1.6.1.tgz",
"integrity": "sha512-jDG/eJquID1M4MBlKMmDRBmz2TpXMv7TUyu2nIRUxhlUc2ogC82T+VQUkca9GJH1BBJ9dx5sSE5dGkWNjIbZxw==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"utif2": "^4.1.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-blit": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-blit/-/plugin-blit-1.6.1.tgz",
"integrity": "sha512-MwnI7C7K81uWddY9FLw1fCOIy6SsPIUftUz36Spt7jisCn8/40DhQMlSxpxTNelnZb/2SnloFimQfRZAmHLOqQ==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-blur": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-blur/-/plugin-blur-1.6.1.tgz",
"integrity": "sha512-lIo7Tzp5jQu30EFFSK/phXANK3citKVEjepDjQ6ljHoIFtuMRrnybnmI2Md24ulvWlDaz+hh3n6qrMb8ydwhZQ==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/utils": "1.6.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-circle": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-circle/-/plugin-circle-1.6.1.tgz",
"integrity": "sha512-kK1PavY6cKHNNKce37vdV4Tmpc1/zDKngGoeOV3j+EMatoHFZUinV3s6F9aWryPs3A0xhCLZgdJ6Zeea1d5LCQ==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-color": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-color/-/plugin-color-1.6.1.tgz",
"integrity": "sha512-LtUN1vAP+LRlZAtTNVhDRSiXx+26Kbz3zJaG6a5k59gQ95jgT5mknnF8lxkHcqJthM4MEk3/tPxkdJpEybyF/A==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"tinycolor2": "^1.6.0",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-contain": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-contain/-/plugin-contain-1.6.1.tgz",
"integrity": "sha512-m0qhrfA8jkTqretGv4w+T/ADFR4GwBpE0sCOC2uJ0dzr44/ddOMsIdrpi89kabqYiPYIrxkgdCVCLm3zn1Vkkg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/plugin-blit": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-cover": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-cover/-/plugin-cover-1.6.1.tgz",
"integrity": "sha512-hZytnsth0zoll6cPf434BrT+p/v569Wr5tyO6Dp0dH1IDPhzhB5F38sZGMLDo7bzQiN9JFVB3fxkcJ/WYCJ3Mg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/plugin-crop": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-crop": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-crop/-/plugin-crop-1.6.1.tgz",
"integrity": "sha512-EerRSLlclXyKDnYc/H9w/1amZW7b7v3OGi/VlerPd2M/pAu5X8TkyYWtfqYCXnNp1Ixtd8oCo9zGfY9zoXT4rg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-displace": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-displace/-/plugin-displace-1.6.1.tgz",
"integrity": "sha512-K07QVl7xQwIfD6KfxRV/c3E9e7ZBXxUXdWuvoTWcKHL2qV48MOF5Nqbz/aJW4ThnQARIsxvYlZjPFiqkCjlU+g==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-dither": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-dither/-/plugin-dither-1.6.1.tgz",
"integrity": "sha512-+2V+GCV2WycMoX1/z977TkZ8Zq/4MVSKElHYatgUqtwXMi2fDK2gKYU2g9V39IqFvTJsTIsK0+58VFz/ROBVew==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-fisheye": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-fisheye/-/plugin-fisheye-1.6.1.tgz",
"integrity": "sha512-XtS5ZyoZ0vxZxJ6gkqI63SivhtI58vX95foMPM+cyzYkRsJXMOYCr8DScxF5bp4Xr003NjYm/P+7+08tibwzHA==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-flip": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-flip/-/plugin-flip-1.6.1.tgz",
"integrity": "sha512-ws38W/sGj7LobNRayQ83garxiktOyWxM5vO/y4a/2cy9v65SLEUzVkrj+oeAaUSSObdz4HcCEla7XtGlnAGAaA==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-hash": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-hash/-/plugin-hash-1.6.1.tgz",
"integrity": "sha512-sZt6ZcMX6i8vFWb4GYnw0pR/o9++ef0dTVcboTB5B/g7nrxCODIB4wfEkJ/YqZM5wUvol77K1qeS0/rVO6z21A==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/js-bmp": "1.6.1",
"@jimp/js-jpeg": "1.6.1",
"@jimp/js-png": "1.6.1",
"@jimp/js-tiff": "1.6.1",
"@jimp/plugin-color": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"any-base": "^1.1.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-mask": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-mask/-/plugin-mask-1.6.1.tgz",
"integrity": "sha512-SIG0/FcmEj3tkwFxc7fAGLO8o4uNzMpSOdQOhbCgxefQKq5wOVMk9BQx/sdMPBwtMLr9WLq0GzLA/rk6t2v20A==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-print": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-print/-/plugin-print-1.6.1.tgz",
"integrity": "sha512-BYVz/X3Xzv8XYilVeDy11NOp0h7BTDjlOtu0BekIFHP1yHVd24AXNzbOy52XlzYZWQ0Dl36HOHEpl/nSNrzc6w==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/js-jpeg": "1.6.1",
"@jimp/js-png": "1.6.1",
"@jimp/plugin-blit": "1.6.1",
"@jimp/types": "1.6.1",
"parse-bmfont-ascii": "^1.0.6",
"parse-bmfont-binary": "^1.0.6",
"parse-bmfont-xml": "^1.1.6",
"simple-xml-to-json": "^1.2.2",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-quantize": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-quantize/-/plugin-quantize-1.6.1.tgz",
"integrity": "sha512-J2En9PLURfP+vwYDtuZ9T8yBW6BWYZBScydAjRiPBmJfEhTcNQqiiQODrZf7EqbbX/Sy5H6dAeRiqkgoV9N6Ww==",
"license": "MIT",
"dependencies": {
"image-q": "^4.0.0",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-resize": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-resize/-/plugin-resize-1.6.1.tgz",
"integrity": "sha512-CLkrtJoIz2HdWnpYiN6p8KYcPc00rCH/SUu6o+lfZL05Q4uhecJlnvXuj9x+U6mDn3ldPmJj6aZqMHuUJzdVqg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/types": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-rotate": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-rotate/-/plugin-rotate-1.6.1.tgz",
"integrity": "sha512-nOjVjbbj705B02ksysKnh0POAwEBXZtJ9zQ5qC+X7Tavl3JNn+P3BzQovbBxLPSbUSld6XID9z5ijin4PtOAUg==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/plugin-crop": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/plugin-threshold": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/plugin-threshold/-/plugin-threshold-1.6.1.tgz",
"integrity": "sha512-JOKv9F8s6tnVLf4sB/2fF0F339EFnHvgEdFYugO6VhowKLsap0pEZmLyE/DlRnYtIj2RddHZVxVMp/eKJ04l2Q==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/plugin-color": "1.6.1",
"@jimp/plugin-hash": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1",
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/types": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/types/-/types-1.6.1.tgz",
"integrity": "sha512-leI7YbveTNi565m910XgIOwXyuu074H5qazAD1357HImJSv2hqxnWXpwxQbadGWZ7goZRYBDZy5lpqud0p7q5w==",
"license": "MIT",
"dependencies": {
"zod": "^3.23.8"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jimp/utils": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@jimp/utils/-/utils-1.6.1.tgz",
"integrity": "sha512-veFPRd93FCnS7AgmCkPgARVGoDRrJ9cm1ujuNyA+UfQ5VKbED2002sm5XfFLFwTsKC8j04heTrwe+tU1dluXOw==",
"license": "MIT",
"dependencies": {
"@jimp/types": "1.6.1",
"tinycolor2": "^1.6.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@@ -718,6 +1154,52 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@tokenizer/inflate": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
"integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"token-types": "^6.1.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/@tokenizer/inflate/node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@tokenizer/inflate/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/@tokenizer/token": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
"integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
"license": "MIT"
},
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.12", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
@@ -1015,6 +1497,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/any-base": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==",
"license": "MIT"
},
"node_modules/anymatch": { "node_modules/anymatch": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -1062,6 +1550,15 @@
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/await-to-js": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz",
"integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1122,6 +1619,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/bmp-ts": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/bmp-ts/-/bmp-ts-1.0.9.tgz",
"integrity": "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw==",
"license": "MIT"
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
@@ -1570,6 +2073,11 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/exif-parser": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="
},
"node_modules/express": { "node_modules/express": {
"version": "4.21.2", "version": "4.21.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
@@ -1672,6 +2180,24 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/file-type": {
"version": "21.3.4",
"resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz",
"integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==",
"license": "MIT",
"dependencies": {
"@tokenizer/inflate": "^0.4.1",
"strtok3": "^10.3.4",
"token-types": "^6.1.1",
"uint8array-extras": "^1.4.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sindresorhus/file-type?sponsor=1"
}
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1755,7 +2281,6 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -1833,6 +2358,16 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/gifwrap": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.10.1.tgz",
"integrity": "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw==",
"license": "MIT",
"dependencies": {
"image-q": "^4.0.0",
"omggif": "^1.0.10"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -1988,6 +2523,41 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/image-q": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/image-q/-/image-q-4.0.0.tgz",
"integrity": "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==",
"license": "MIT",
"dependencies": {
"@types/node": "16.9.1"
}
},
"node_modules/image-q/node_modules/@types/node": {
"version": "16.9.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz",
"integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==",
"license": "MIT"
},
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -2085,6 +2655,56 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/jimp": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/jimp/-/jimp-1.6.1.tgz",
"integrity": "sha512-hNQh6rZtWfSVWSNVmvq87N5BPJsNH7k7I7qyrXf9DOma9xATQk3fsyHazCQe51nCjdkoWdTmh0vD7bjVSLoxxw==",
"license": "MIT",
"dependencies": {
"@jimp/core": "1.6.1",
"@jimp/diff": "1.6.1",
"@jimp/js-bmp": "1.6.1",
"@jimp/js-gif": "1.6.1",
"@jimp/js-jpeg": "1.6.1",
"@jimp/js-png": "1.6.1",
"@jimp/js-tiff": "1.6.1",
"@jimp/plugin-blit": "1.6.1",
"@jimp/plugin-blur": "1.6.1",
"@jimp/plugin-circle": "1.6.1",
"@jimp/plugin-color": "1.6.1",
"@jimp/plugin-contain": "1.6.1",
"@jimp/plugin-cover": "1.6.1",
"@jimp/plugin-crop": "1.6.1",
"@jimp/plugin-displace": "1.6.1",
"@jimp/plugin-dither": "1.6.1",
"@jimp/plugin-fisheye": "1.6.1",
"@jimp/plugin-flip": "1.6.1",
"@jimp/plugin-hash": "1.6.1",
"@jimp/plugin-mask": "1.6.1",
"@jimp/plugin-print": "1.6.1",
"@jimp/plugin-quantize": "1.6.1",
"@jimp/plugin-resize": "1.6.1",
"@jimp/plugin-rotate": "1.6.1",
"@jimp/plugin-threshold": "1.6.1",
"@jimp/types": "1.6.1",
"@jimp/utils": "1.6.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause"
},
"node_modules/jsqr": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
"license": "Apache-2.0"
},
"node_modules/libsodium": { "node_modules/libsodium": {
"version": "0.7.15", "version": "0.7.15",
"resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz", "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.15.tgz",
@@ -2386,6 +3006,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/omggif": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
"license": "MIT"
},
"node_modules/on-finished": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -2416,6 +3042,34 @@
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parse-bmfont-ascii": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz",
"integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==",
"license": "MIT"
},
"node_modules/parse-bmfont-binary": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz",
"integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==",
"license": "MIT"
},
"node_modules/parse-bmfont-xml": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz",
"integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==",
"license": "MIT",
"dependencies": {
"xml-parse-from-string": "^1.0.0",
"xml2js": "^0.5.0"
}
},
"node_modules/parse-cache-control": { "node_modules/parse-cache-control": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz",
@@ -2465,6 +3119,27 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pixelmatch": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.3.0.tgz",
"integrity": "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q==",
"license": "ISC",
"dependencies": {
"pngjs": "^6.0.0"
},
"bin": {
"pixelmatch": "bin/pixelmatch"
}
},
"node_modules/pixelmatch/node_modules/pngjs": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz",
"integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==",
"license": "MIT",
"engines": {
"node": ">=12.13.0"
}
},
"node_modules/play-audio": { "node_modules/play-audio": {
"version": "0.5.2", "version": "0.5.2",
"resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz", "resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.5.2.tgz",
@@ -2483,6 +3158,15 @@
"node": ">=16.0.0" "node": ">=16.0.0"
} }
}, },
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/prism-media": { "node_modules/prism-media": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz", "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.5.tgz",
@@ -2699,6 +3383,15 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=11.0.0"
}
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -2855,6 +3548,15 @@
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/simple-xml-to-json": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/simple-xml-to-json/-/simple-xml-to-json-1.2.7.tgz",
"integrity": "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q==",
"license": "MIT",
"engines": {
"node": ">=20.12.2"
}
},
"node_modules/sodium-native": { "node_modules/sodium-native": {
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz", "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-4.3.3.tgz",
@@ -2949,6 +3651,22 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/strtok3": {
"version": "10.3.5",
"resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
"integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
@@ -2979,6 +3697,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
"license": "MIT"
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -3001,6 +3725,24 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/token-types": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
"integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
"license": "MIT",
"dependencies": {
"@borewit/text-codec": "^0.2.1",
"@tokenizer/token": "^0.3.0",
"ieee754": "^1.2.1"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -3166,6 +3908,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/uint8array-extras": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/undici": { "node_modules/undici": {
"version": "6.21.3", "version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
@@ -3190,6 +3944,15 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/utif2": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/utif2/-/utif2-4.1.0.tgz",
"integrity": "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.11"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -3273,6 +4036,34 @@
} }
} }
}, },
"node_modules/xml-parse-from-string": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz",
"integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==",
"license": "MIT"
},
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/xtend": { "node_modules/xtend": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -3298,6 +4089,15 @@
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
},
"node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View File

@@ -13,9 +13,9 @@
"start": "node dist/index.js" "start": "node dist/index.js"
}, },
"dependencies": { "dependencies": {
"@discordjs/opus": "^0.9.0",
"@discordjs/rest": "^2.2.0", "@discordjs/rest": "^2.2.0",
"@discordjs/voice": "^0.19.0", "@discordjs/voice": "^0.19.0",
"@discordjs/opus": "^0.9.0",
"@prisma/client": "^5.4.2", "@prisma/client": "^5.4.2",
"@snazzah/davey": "^0.1.8", "@snazzah/davey": "^0.1.8",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
@@ -24,10 +24,12 @@
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"ffmpeg-static": "^5.2.0",
"jimp": "^1.6.1",
"jsqr": "^1.4.0",
"libsodium-wrappers": "^0.7.13", "libsodium-wrappers": "^0.7.13",
"play-dl": "^1.9.7", "play-dl": "^1.9.7",
"sodium-native": "^4.0.4", "sodium-native": "^4.0.4"
"ffmpeg-static": "^5.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",

View File

@@ -0,0 +1,65 @@
import { ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
function formatList(items: { id: string; name: string }[], max = 10): string {
if (!items.length) return 'Keine';
const shown = items.slice(0, max).map((i) => i.name);
const rest = items.length - shown.length;
return shown.join(', ') + (rest > 0 ? ` (+${rest} weitere)` : '');
}
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('permissions')
.setDescription('Analysiert Rollen und Berechtigungen auf Sicherheitsrisiken.')
.addSubcommand((sub) => sub.setName('scan').setDescription('Startet die Rechte-Analyse.'))
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guild) return;
await interaction.deferReply({ ephemeral: true });
const result = await context.permissionScan.scan(interaction.guild);
const embed = new EmbedBuilder()
.setTitle('🔐 Rechte-Scan')
.setColor(result.tooManyAdmins ? 0xdc2626 : 0xf97316)
.setDescription(`Analyse für **${interaction.guild.name}**`)
.addFields(
{ name: 'Admin-Rollen', value: formatList(result.adminRoles), inline: false },
{ name: 'Rollen, die bannen können', value: formatList(result.banRoles), inline: false },
{
name: 'Bots mit gefährlichen Rechten',
value: result.dangerousBots.length
? result.dangerousBots.slice(0, 10).map((b) => `${b.tag}: ${b.perms.join(', ')}`).join('\n')
: 'Keine',
inline: false
},
{ name: 'Öffentliche Kanäle', value: formatList(result.publicChannels), inline: false },
{ name: '@everyone kann schreiben', value: formatList(result.everyoneCanSendChannels), inline: false },
{ name: 'Leere Rollen', value: formatList(result.emptyRoles), inline: false },
{
name: 'Doppelte Rollen (gleiche Rechte)',
value: result.duplicateRoleGroups.length
? result.duplicateRoleGroups.slice(0, 5).map((g) => g.map((r) => r.name).join(' = ')).join('\n')
: 'Keine',
inline: false
},
{ name: 'Nutzlose Rollen (keine Rechte, keine Mitglieder)', value: formatList(result.uselessRoles), inline: false },
{
name: '⚠️ Zu viele Admin-Rechte?',
value: result.tooManyAdmins
? `Ja — ${result.adminRoles.length} Admin-Rolle(n), ${result.adminMemberCount} Mitglied(er) mit Administrator.`
: 'Nein, sieht unauffällig aus.',
inline: false
}
)
.setTimestamp();
context.branding.applyFooter(embed, interaction.guild.id);
await interaction.editReply({ embeds: [embed] });
}
};
export default command;

View File

@@ -0,0 +1,67 @@
import { ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types';
import { settingsStore } from '../../config/state';
import { context } from '../../config/context';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('ticketconfig')
.setDescription('Konfiguriert Rollen und Vorlagen je Ticket-Kategorie.')
.addSubcommandGroup((group) =>
group
.setName('topic')
.setDescription('Verwaltet Ticket-Kategorien')
.addSubcommand((sub) =>
sub
.setName('set')
.setDescription('Setzt Rolle und/oder Fragen-Vorlage für eine Kategorie.')
.addStringOption((opt) => opt.setName('topic').setDescription('Kategorie-Slug (z.B. ban, help, feedback, other)').setRequired(true))
.addRoleOption((opt) => opt.setName('role').setDescription('Rolle, die bei dieser Kategorie gepingt wird'))
.addStringOption((opt) => opt.setName('questions').setDescription('Fragen für die Vorlage, eine pro Zeile (max. 5)'))
)
.addSubcommand((sub) => sub.setName('list').setDescription('Zeigt die aktuelle Konfiguration aller Kategorien.'))
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild),
async execute(interaction: ChatInputCommandInteraction) {
if (!interaction.guildId) return;
const sub = interaction.options.getSubcommand();
if (sub === 'set') {
const topic = interaction.options.getString('topic', true).trim().toLowerCase();
const role = interaction.options.getRole('role');
const questionsRaw = interaction.options.getString('questions');
const current = settingsStore.get(interaction.guildId)?.ticketConfig?.topics?.[topic] || {};
const questions = questionsRaw
? questionsRaw.split('\n').map((q) => q.trim()).filter(Boolean).slice(0, 5)
: current.questions;
await settingsStore.set(interaction.guildId, {
ticketConfig: { topics: { [topic]: { roleId: role?.id ?? current.roleId, questions } } }
});
await interaction.reply({ content: `Konfiguration für Kategorie \`${topic}\` gespeichert.`, ephemeral: true });
return;
}
if (sub === 'list') {
const topics = settingsStore.get(interaction.guildId)?.ticketConfig?.topics || {};
const entries = Object.entries(topics);
const embed = new EmbedBuilder()
.setTitle('Ticket-Kategorien')
.setColor(context.branding.getColor(interaction.guildId))
.setDescription(
entries.length
? entries
.map(([topic, cfg]) => `**${topic}** — Rolle: ${cfg.roleId ? `<@&${cfg.roleId}>` : 'Standard'} · Fragen: ${cfg.questions?.length ?? 0}`)
.join('\n')
: 'Noch keine Kategorien konfiguriert.'
);
context.branding.applyFooter(embed, interaction.guildId);
await interaction.reply({ embeds: [embed], ephemeral: true });
}
}
};
export default command;

View File

@@ -0,0 +1,40 @@
import { ChannelType, ChatInputCommandInteraction, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
import { SlashCommand } from '../../utils/types';
import { context } from '../../config/context';
import { PanelType } from '../../services/infoPanelService';
const command: SlashCommand = {
guildOnly: true,
data: new SlashCommandBuilder()
.setName('panel')
.setDescription('Erstellt feste Info-Panels mit Buttons/Dropdowns.')
.addSubcommand((sub) =>
sub
.setName('create')
.setDescription('Erstellt ein neues Panel.')
.addStringOption((opt) =>
opt
.setName('type')
.setDescription('Panel-Typ')
.setRequired(true)
.addChoices(
{ name: 'Regeln', value: 'rules' },
{ name: 'Support', value: 'support' },
{ name: 'Bewerbungen', value: 'bewerbung' },
{ name: 'Partner', value: 'partner' },
{ name: 'Rollen', value: 'rollen' },
{ name: 'Events', value: 'events' },
{ name: 'FAQ', value: 'faq' }
)
)
.addChannelOption((opt) => opt.setName('channel').setDescription('Zielkanal').addChannelTypes(ChannelType.GuildText).setRequired(true))
)
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild),
async execute(interaction: ChatInputCommandInteraction) {
const type = interaction.options.getString('type', true) as PanelType;
const channel = interaction.options.getChannel('channel', true);
await context.infoPanels.openModal(interaction, type, channel.id);
}
};
export default command;

View File

@@ -32,6 +32,9 @@ import { PartnerService } from '../services/partnerService';
import { AltAccountService } from '../services/altAccountService'; import { AltAccountService } from '../services/altAccountService';
import { GalleryService } from '../services/galleryService'; import { GalleryService } from '../services/galleryService';
import { GrowthService } from '../services/growthService'; import { GrowthService } from '../services/growthService';
import { PermissionScanService } from '../services/permissionScanService';
import { InfoPanelService } from '../services/infoPanelService';
import { ImageModerationService } from '../services/imageModerationService';
const logging = new LoggingService(); const logging = new LoggingService();
const moduleService = new BotModuleService(); const moduleService = new BotModuleService();
@@ -74,7 +77,10 @@ export const context = {
partners: new PartnerService(), partners: new PartnerService(),
altAccounts: new AltAccountService(), altAccounts: new AltAccountService(),
gallery: new GalleryService(), gallery: new GalleryService(),
growth: new GrowthService() growth: new GrowthService(),
permissionScan: new PermissionScanService(),
infoPanels: new InfoPanelService(),
imageModeration: new ImageModerationService()
}; };
birthdayService.setBadgeService(badgeService); birthdayService.setBadgeService(badgeService);

View File

@@ -103,6 +103,13 @@ export interface GuildSettings {
artistRoleId?: string; artistRoleId?: string;
lastWinnerAt?: string; lastWinnerAt?: string;
}; };
imageModerationConfig?: {
enabled?: boolean;
alertOnly?: boolean;
};
ticketConfig?: {
topics?: Record<string, { roleId?: string; questions?: string[] }>;
};
} }
class SettingsStore { class SettingsStore {
@@ -170,6 +177,8 @@ class SettingsStore {
brandingConfig: (row as any).brandingConfig ?? undefined, brandingConfig: (row as any).brandingConfig ?? undefined,
partnerConfig: (row as any).partnerConfig ?? undefined, partnerConfig: (row as any).partnerConfig ?? undefined,
galleryConfig: (row as any).galleryConfig ?? undefined, galleryConfig: (row as any).galleryConfig ?? undefined,
imageModerationConfig: (row as any).imageModerationConfig ?? undefined,
ticketConfig: (row as any).ticketConfig ?? undefined,
supportRoleId: row.supportRoleId ?? undefined supportRoleId: row.supportRoleId ?? undefined
} satisfies GuildSettings; } satisfies GuildSettings;
this.cache.set(row.guildId, this.applyModuleDefaults(cfg)); this.cache.set(row.guildId, this.applyModuleDefaults(cfg));
@@ -237,6 +246,16 @@ class SettingsStore {
if (partial.galleryConfig) { if (partial.galleryConfig) {
merged.galleryConfig = { ...(merged.galleryConfig ?? {}), ...partial.galleryConfig }; merged.galleryConfig = { ...(merged.galleryConfig ?? {}), ...partial.galleryConfig };
} }
if (partial.imageModerationConfig) {
merged.imageModerationConfig = { ...(merged.imageModerationConfig ?? {}), ...partial.imageModerationConfig };
}
if (partial.ticketConfig) {
merged.ticketConfig = {
...(merged.ticketConfig ?? {}),
...partial.ticketConfig,
topics: { ...(merged.ticketConfig?.topics ?? {}), ...(partial.ticketConfig.topics ?? {}) }
};
}
merged.automodConfig = { ...mergedAutomod, supportLoginConfig: merged.supportLoginConfig ?? mergedAutomod['supportLoginConfig'] }; merged.automodConfig = { ...mergedAutomod, supportLoginConfig: merged.supportLoginConfig ?? mergedAutomod['supportLoginConfig'] };
merged.statuspageEnabled = mergedAutomod.statuspageEnabled; merged.statuspageEnabled = mergedAutomod.statuspageEnabled;
merged.statuspageConfig = mergedAutomod.statuspageConfig; merged.statuspageConfig = mergedAutomod.statuspageConfig;
@@ -272,6 +291,8 @@ class SettingsStore {
brandingConfig: merged.brandingConfig ?? Prisma.JsonNull, brandingConfig: merged.brandingConfig ?? Prisma.JsonNull,
partnerConfig: merged.partnerConfig ?? Prisma.JsonNull, partnerConfig: merged.partnerConfig ?? Prisma.JsonNull,
galleryConfig: merged.galleryConfig ?? Prisma.JsonNull, galleryConfig: merged.galleryConfig ?? Prisma.JsonNull,
imageModerationConfig: merged.imageModerationConfig ?? Prisma.JsonNull,
ticketConfig: merged.ticketConfig ?? Prisma.JsonNull,
supportRoleId: merged.supportRoleId ?? null supportRoleId: merged.supportRoleId ?? null
}, },
create: { create: {
@@ -302,6 +323,8 @@ class SettingsStore {
brandingConfig: merged.brandingConfig ?? Prisma.JsonNull, brandingConfig: merged.brandingConfig ?? Prisma.JsonNull,
partnerConfig: merged.partnerConfig ?? Prisma.JsonNull, partnerConfig: merged.partnerConfig ?? Prisma.JsonNull,
galleryConfig: merged.galleryConfig ?? Prisma.JsonNull, galleryConfig: merged.galleryConfig ?? Prisma.JsonNull,
imageModerationConfig: merged.imageModerationConfig ?? Prisma.JsonNull,
ticketConfig: merged.ticketConfig ?? Prisma.JsonNull,
supportRoleId: merged.supportRoleId ?? null supportRoleId: merged.supportRoleId ?? null
} }
}); });

View File

@@ -0,0 +1,25 @@
-- AlterTable
ALTER TABLE "GuildSettings" ADD COLUMN "imageModerationConfig" JSONB,
ADD COLUMN "ticketConfig" JSONB;
-- AlterTable
ALTER TABLE "GuildGrowthEvent" ADD COLUMN "inviterId" TEXT,
ADD COLUMN "suspicious" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "InfoPanel" (
"id" TEXT NOT NULL,
"guildId" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
"messageId" TEXT,
"type" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"items" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "InfoPanel_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "InfoPanel_guildId_idx" ON "InfoPanel"("guildId");

View File

@@ -36,6 +36,8 @@ model GuildSettings {
brandingConfig Json? brandingConfig Json?
partnerConfig Json? partnerConfig Json?
galleryConfig Json? galleryConfig Json?
imageModerationConfig Json?
ticketConfig Json?
supportRoleId String? supportRoleId String?
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -442,7 +444,23 @@ model GuildGrowthEvent {
type String type String
userId String userId String
inviteCode String? inviteCode String?
inviterId String?
suspicious Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@index([guildId, type, createdAt]) @@index([guildId, type, createdAt])
} }
model InfoPanel {
id String @id @default(cuid())
guildId String
channelId String
messageId String?
type String
title String
description String?
items Json?
createdAt DateTime @default(now())
@@index([guildId])
}

View File

@@ -12,13 +12,13 @@ const event: EventHandler = {
return; return;
} }
context.altAccounts.check(member).then(({ score, reasons }) => { Promise.all([context.altAccounts.check(member), context.growth.resolveUsedInvite(member.guild)])
if (context.altAccounts.isSuspicious(score)) context.logging.logSuspiciousJoin(member, reasons); .then(([{ score, reasons }, invite]) => {
}).catch(() => undefined); const suspicious = context.altAccounts.isSuspicious(score);
if (suspicious) context.logging.logSuspiciousJoin(member, reasons);
context.growth.resolveUsedInvite(member.guild).then((inviteCode) => { context.growth.recordJoin(member.guild.id, member.id, invite, suspicious);
context.growth.recordJoin(member.guild.id, member.id, inviteCode); })
}).catch(() => undefined); .catch(() => undefined);
const guildConfig = settingsStore.get(member.guild.id); const guildConfig = settingsStore.get(member.guild.id);
const welcomeCfg = guildConfig?.welcomeConfig || guildConfig?.automodConfig?.welcomeConfig; const welcomeCfg = guildConfig?.welcomeConfig || guildConfig?.automodConfig?.welcomeConfig;

View File

@@ -49,6 +49,10 @@ const event: EventHandler = {
await context.gallery.handleComponent(interaction); await context.gallery.handleComponent(interaction);
return; return;
} }
if (interaction.customId.startsWith('panel:')) {
await context.infoPanels.handleComponent(interaction as any);
return;
}
} }
if (interaction.isButton()) { if (interaction.isButton()) {
@@ -87,6 +91,14 @@ const event: EventHandler = {
await context.partners.handleModal(interaction); await context.partners.handleModal(interaction);
return; return;
} }
if (interaction.customId.startsWith('panel:create:')) {
await context.infoPanels.handleModal(interaction);
return;
}
if (interaction.customId.startsWith('ticket:template:')) {
await context.tickets.handleModal(interaction);
return;
}
} }
} }
}; };

View File

@@ -11,6 +11,22 @@ const event: EventHandler = {
if (cfg?.automodEnabled === true) context.automod.checkMessage(message, cfg); if (cfg?.automodEnabled === true) context.automod.checkMessage(message, cfg);
if (cfg?.levelingEnabled === true) context.leveling.handleMessage(message); if (cfg?.levelingEnabled === true) context.leveling.handleMessage(message);
if (cfg?.badgesEnabled !== false) context.badges.trackMessage(message); if (cfg?.badgesEnabled !== false) context.badges.trackMessage(message);
if (cfg?.imageModerationConfig?.enabled === true && message.guild) {
const imageAttachments = message.attachments.filter((a) => a.contentType?.startsWith('image/'));
imageAttachments.forEach((attachment) => {
context.imageModeration.scanAttachment(attachment.url).then((result) => {
if (!result.found) return;
// alertOnly = nur bei erkanntem Scam-Verdacht alarmieren, sonst bei jedem gefundenen QR-Code
if (cfg.imageModerationConfig?.alertOnly === true && !result.flagged) return;
context.logging.logImageAlert(message.guild!, {
userTag: message.author.tag,
channel: message.channel as any,
content: result.content || '',
messageUrl: message.url
});
}).catch(() => undefined);
});
}
// Ticket SLA + KB // Ticket SLA + KB
await context.tickets.trackFirstResponse(message); await context.tickets.trackFirstResponse(message);
await context.tickets.suggestKnowledgeBase(message); await context.tickets.suggestKnowledgeBase(message);

View File

@@ -2,6 +2,11 @@ import { Guild } from 'discord.js';
import { prisma } from '../database'; import { prisma } from '../database';
import { logger } from '../utils/logger'; import { logger } from '../utils/logger';
interface UsedInvite {
code: string;
inviterId?: string;
}
export class GrowthService { export class GrowthService {
private inviteCache = new Map<string, Map<string, number>>(); private inviteCache = new Map<string, Map<string, number>>();
@@ -16,19 +21,19 @@ export class GrowthService {
} }
} }
public async resolveUsedInvite(guild: Guild): Promise<string | null> { public async resolveUsedInvite(guild: Guild): Promise<UsedInvite | null> {
const before = this.inviteCache.get(guild.id); const before = this.inviteCache.get(guild.id);
try { try {
const invites = await guild.invites.fetch(); const invites = await guild.invites.fetch();
let usedCode: string | null = null; let used: UsedInvite | null = null;
invites.forEach((inv) => { invites.forEach((inv) => {
const prevUses = before?.get(inv.code) ?? 0; const prevUses = before?.get(inv.code) ?? 0;
if ((inv.uses ?? 0) > prevUses) usedCode = inv.code; if ((inv.uses ?? 0) > prevUses) used = { code: inv.code, inviterId: inv.inviter?.id };
}); });
if (!usedCode && before) { if (!used && before) {
for (const code of before.keys()) { for (const code of before.keys()) {
if (!invites.has(code)) { if (!invites.has(code)) {
usedCode = code; used = { code };
break; break;
} }
} }
@@ -36,15 +41,17 @@ export class GrowthService {
const map = new Map<string, number>(); const map = new Map<string, number>();
invites.forEach((inv) => map.set(inv.code, inv.uses ?? 0)); invites.forEach((inv) => map.set(inv.code, inv.uses ?? 0));
this.inviteCache.set(guild.id, map); this.inviteCache.set(guild.id, map);
return usedCode; return used;
} catch (err) { } catch (err) {
logger.warn(`Failed to resolve used invite for ${guild.id}: ${err}`); logger.warn(`Failed to resolve used invite for ${guild.id}: ${err}`);
return null; return null;
} }
} }
public async recordJoin(guildId: string, userId: string, inviteCode: string | null) { public async recordJoin(guildId: string, userId: string, invite: UsedInvite | null, suspicious = false) {
await prisma.guildGrowthEvent.create({ data: { guildId, userId, type: 'join', inviteCode: inviteCode ?? undefined } }).catch(() => undefined); await prisma.guildGrowthEvent
.create({ data: { guildId, userId, type: 'join', inviteCode: invite?.code, inviterId: invite?.inviterId, suspicious } })
.catch(() => undefined);
} }
public async recordLeave(guildId: string, userId: string) { public async recordLeave(guildId: string, userId: string) {
@@ -93,4 +100,42 @@ export class GrowthService {
.sort((a, b) => a.day.localeCompare(b.day)) .sort((a, b) => a.day.localeCompare(b.day))
}; };
} }
public async getInviteBreakdown(guild: Guild) {
const since30 = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const [liveInvites, groups, suspiciousGroups] = await Promise.all([
guild.invites.fetch().catch(() => null),
prisma.guildGrowthEvent.groupBy({
by: ['inviteCode', 'inviterId'],
where: { guildId: guild.id, type: 'join', inviteCode: { not: null }, createdAt: { gte: since30 } },
_count: { _all: true }
}),
prisma.guildGrowthEvent.groupBy({
by: ['inviteCode'],
where: { guildId: guild.id, type: 'join', inviteCode: { not: null }, suspicious: true, createdAt: { gte: since30 } },
_count: { _all: true }
})
]);
const suspiciousByCode = new Map(suspiciousGroups.map((g) => [g.inviteCode, g._count._all]));
const usesByCode = new Map((liveInvites?.map((inv) => [inv.code, inv.uses ?? 0]) as [string, number][]) ?? []);
return groups
.map((g) => ({
code: g.inviteCode as string,
inviterId: g.inviterId ?? undefined,
uses: usesByCode.get(g.inviteCode as string) ?? g._count._all,
joins30: g._count._all,
suspiciousJoins30: suspiciousByCode.get(g.inviteCode) ?? 0
}))
.sort((a, b) => b.joins30 - a.joins30);
}
public async getRecentJoins(guildId: string, limit = 20) {
return prisma.guildGrowthEvent.findMany({
where: { guildId, type: 'join' },
orderBy: { createdAt: 'desc' },
take: limit
});
}
} }

View File

@@ -0,0 +1,37 @@
import { Jimp } from 'jimp';
import jsQR from 'jsqr';
import { logger } from '../utils/logger';
const SCAM_PATTERNS = [
/discord-?nitro/i,
/free-?nitro/i,
/steamcommunlty/i,
/steamcommunity\.[a-z]{2,}\.[a-z]{2,}/i,
/discordapp-?gift/i,
/dlscord/i,
/dicsord/i
];
export interface ImageScanResult {
found: boolean;
content?: string;
flagged?: boolean;
}
export class ImageModerationService {
public async scanAttachment(url: string): Promise<ImageScanResult> {
try {
const image = await Jimp.read(url);
const { data, width, height } = image.bitmap;
const clamped = new Uint8ClampedArray(data.buffer, data.byteOffset, data.length);
const result = jsQR(clamped, width, height);
if (!result) return { found: false };
const content = result.data;
const flagged = SCAM_PATTERNS.some((p) => p.test(content));
return { found: true, content, flagged };
} catch (err) {
logger.warn(`Image scan failed for ${url}: ${err}`);
return { found: false };
}
}
}

View File

@@ -0,0 +1,140 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
ChatInputCommandInteraction,
EmbedBuilder,
ModalBuilder,
ModalSubmitInteraction,
StringSelectMenuBuilder,
StringSelectMenuInteraction,
TextInputBuilder,
TextInputStyle
} from 'discord.js';
import { prisma } from '../database';
import { context } from '../config/context';
export type PanelType = 'rules' | 'support' | 'bewerbung' | 'partner' | 'rollen' | 'events' | 'faq';
interface FaqItem {
question: string;
answer: string;
}
export class InfoPanelService {
public async openModal(interaction: ChatInputCommandInteraction, type: PanelType, channelId: string) {
const modal = new ModalBuilder().setCustomId(`panel:create:${type}:${channelId}`).setTitle('Panel erstellen');
const title = new TextInputBuilder().setCustomId('title').setLabel('Titel').setStyle(TextInputStyle.Short).setRequired(true);
const description = new TextInputBuilder().setCustomId('description').setLabel('Beschreibung').setStyle(TextInputStyle.Paragraph).setRequired(type !== 'faq');
const rows = [new ActionRowBuilder<TextInputBuilder>().addComponents(title), new ActionRowBuilder<TextInputBuilder>().addComponents(description)];
if (type === 'faq') {
const items = new TextInputBuilder()
.setCustomId('items')
.setLabel('Fragen (ein "Frage: Antwort" pro Zeile)')
.setStyle(TextInputStyle.Paragraph)
.setPlaceholder('Wie erstelle ich ein Ticket?: Klicke auf den Support-Button.')
.setRequired(true);
rows.push(new ActionRowBuilder<TextInputBuilder>().addComponents(items));
}
modal.addComponents(...rows);
await interaction.showModal(modal);
}
public async handleModal(interaction: ModalSubmitInteraction) {
if (!interaction.customId.startsWith('panel:create:') || !interaction.guildId || !interaction.guild) return;
const [, , type, channelId] = interaction.customId.split(':') as [string, string, PanelType, string];
const channel = await interaction.guild.channels.fetch(channelId).catch(() => null);
if (!channel || !channel.isTextBased()) {
await interaction.reply({ content: 'Zielkanal nicht gefunden.', ephemeral: true });
return;
}
const title = interaction.fields.getTextInputValue('title');
const description = interaction.fields.getTextInputValue('description') || undefined;
let items: FaqItem[] | undefined;
if (type === 'faq') {
items = interaction.fields
.getTextInputValue('items')
.split('\n')
.map((l) => l.trim())
.filter(Boolean)
.slice(0, 25)
.map((line) => {
const [q, ...rest] = line.split(':');
return { question: (q || '').trim(), answer: rest.join(':').trim() || 'Keine Antwort hinterlegt.' };
})
.filter((i) => i.question);
}
const panel = await prisma.infoPanel.create({
data: { guildId: interaction.guildId, channelId, type, title, description, items: (items as any) ?? undefined }
});
const { embed, components } = await this.render(panel.id, type, title, description, items, interaction.guildId);
const sent = await (channel as any).send({ embeds: [embed], components });
await prisma.infoPanel.update({ where: { id: panel.id }, data: { messageId: sent.id } });
await interaction.reply({ content: `Panel wurde in ${channel} gepostet.`, ephemeral: true });
}
public async handleComponent(interaction: ButtonInteraction | StringSelectMenuInteraction) {
if (!interaction.isStringSelectMenu() || !interaction.customId.startsWith('panel:faq:')) return;
const panelId = interaction.customId.split(':')[2];
const panel = await prisma.infoPanel.findUnique({ where: { id: panelId } });
if (!panel) {
await interaction.reply({ content: 'Dieses Panel ist nicht mehr verfügbar.', ephemeral: true });
return;
}
const items = (panel.items as unknown as FaqItem[]) || [];
const index = parseInt(interaction.values[0], 10);
const item = items[index];
await interaction.reply({
content: item ? `**${item.question}**\n${item.answer}` : 'Diese Frage wurde nicht gefunden.',
ephemeral: true
});
}
private async render(panelId: string, type: PanelType, title: string, description: string | undefined, items: FaqItem[] | undefined, guildId: string) {
const embed = new EmbedBuilder().setTitle(title).setColor(context.branding.getColor(guildId));
if (description) embed.setDescription(description);
context.branding.applyFooter(embed, guildId);
const components: ActionRowBuilder<any>[] = [];
if (type === 'faq' && items?.length) {
const select = new StringSelectMenuBuilder()
.setCustomId(`panel:faq:${panelId}`)
.setPlaceholder('Frage auswählen')
.addOptions(items.slice(0, 25).map((item, idx) => ({ label: item.question.slice(0, 100), value: String(idx) })));
components.push(new ActionRowBuilder<StringSelectMenuBuilder>().addComponents(select));
} else if (type === 'support') {
components.push(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId('ticket:create:other').setLabel('Ticket erstellen').setEmoji('🎫').setStyle(ButtonStyle.Primary)
)
);
} else if (type === 'bewerbung') {
const forms = await context.register.listForms(guildId);
const active = forms.find((f) => f.isActive);
if (active) {
components.push(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId(`register:form:${active.id}`).setLabel('Bewerben').setEmoji('📝').setStyle(ButtonStyle.Primary)
)
);
}
} else if (type === 'partner') {
components.push(
new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId('partner:apply').setLabel('Partner werden').setEmoji('🤝').setStyle(ButtonStyle.Primary)
)
);
}
return { embed, components };
}
}

View File

@@ -241,6 +241,27 @@ export class LoggingService {
adminSink?.pushGuildLog({ guildId: guild.id, level: 'INFO', message: `Partner: ${message}`, timestamp: Date.now(), category: 'system' }); adminSink?.pushGuildLog({ guildId: guild.id, level: 'INFO', message: `Partner: ${message}`, timestamp: Date.now(), category: 'system' });
} }
logImageAlert(guild: Guild, options: { userTag: string; channel?: GuildBasedChannel | null; content: string; messageUrl?: string }) {
const { channel } = this.resolve(guild);
if (!channel) return;
const embed = new EmbedBuilder()
.setTitle('🖼️ QR-Code in Bild erkannt')
.setDescription(`${options.userTag} in ${options.channel ? `<#${options.channel.id}>` : 'einem Kanal'}`)
.addFields({ name: 'Inhalt', value: this.safeField(options.content) })
.setColor(0xdc2626)
.setTimestamp();
if (options.messageUrl) embed.addFields({ name: 'Link', value: options.messageUrl });
context.branding.applyFooter(embed, guild.id);
channel.send({ embeds: [embed] }).catch((err) => logger.error('Failed to log image alert', err));
adminSink?.pushGuildLog({
guildId: guild.id,
level: 'WARN',
message: `Bild-Alarm: ${options.userTag} (QR-Code erkannt)`,
timestamp: Date.now(),
category: 'automodActions'
});
}
logRoleUpdate(member: GuildMember, added: string[], removed: string[]) { logRoleUpdate(member: GuildMember, added: string[], removed: string[]) {
const guildId = member.guild.id; const guildId = member.guild.id;
adminSink?.pushGuildLog({ adminSink?.pushGuildLog({

View File

@@ -0,0 +1,102 @@
import { ChannelType, Guild, PermissionFlagsBits, Role, TextChannel } from 'discord.js';
const DANGEROUS_BOT_PERMS: { flag: bigint; label: string }[] = [
{ flag: PermissionFlagsBits.Administrator, label: 'Administrator' },
{ flag: PermissionFlagsBits.BanMembers, label: 'Bannen' },
{ flag: PermissionFlagsBits.KickMembers, label: 'Kicken' },
{ flag: PermissionFlagsBits.ManageGuild, label: 'Server verwalten' },
{ flag: PermissionFlagsBits.ManageRoles, label: 'Rollen verwalten' },
{ flag: PermissionFlagsBits.ManageChannels, label: 'Kanäle verwalten' },
{ flag: PermissionFlagsBits.ManageWebhooks, label: 'Webhooks verwalten' }
];
interface NamedEntry {
id: string;
name: string;
}
export interface PermissionScanResult {
adminRoles: NamedEntry[];
banRoles: NamedEntry[];
dangerousBots: { id: string; tag: string; perms: string[] }[];
publicChannels: NamedEntry[];
everyoneCanSendChannels: NamedEntry[];
emptyRoles: NamedEntry[];
duplicateRoleGroups: NamedEntry[][];
uselessRoles: NamedEntry[];
tooManyAdmins: boolean;
adminMemberCount: number;
}
export class PermissionScanService {
public async scan(guild: Guild): Promise<PermissionScanResult> {
await guild.members.fetch().catch(() => undefined);
const everyone = guild.roles.everyone;
const roles: Role[] = Array.from(guild.roles.cache.values()).filter((r) => r.id !== guild.id);
const adminRoles = roles.filter((r) => r.permissions.has(PermissionFlagsBits.Administrator)).map((r) => ({ id: r.id, name: r.name }));
const banRoles = roles.filter((r) => r.permissions.has(PermissionFlagsBits.BanMembers)).map((r) => ({ id: r.id, name: r.name }));
const dangerousBots = Array.from(guild.members.cache.values())
.filter((m) => m.user.bot)
.map((m) => ({
id: m.id,
tag: m.user.tag,
perms: DANGEROUS_BOT_PERMS.filter((p) => m.permissions.has(p.flag)).map((p) => p.label)
}))
.filter((b) => b.perms.length > 0);
const publicChannels: NamedEntry[] = [];
const everyoneCanSendChannels: NamedEntry[] = [];
guild.channels.cache
.filter((c) => c.type === ChannelType.GuildText)
.forEach((channel) => {
const c = channel as TextChannel;
const perms = c.permissionsFor(everyone);
if (!perms) return;
if (perms.has(PermissionFlagsBits.ViewChannel)) {
publicChannels.push({ id: c.id, name: c.name });
if (perms.has(PermissionFlagsBits.SendMessages)) everyoneCanSendChannels.push({ id: c.id, name: c.name });
}
});
const emptyRoles = roles.filter((r) => !r.managed && r.members.size === 0).map((r) => ({ id: r.id, name: r.name }));
const byBitfield = new Map<string, Role[]>();
roles
.filter((r) => !r.managed && r.permissions.bitfield !== 0n)
.forEach((r) => {
const key = r.permissions.bitfield.toString();
const arr = byBitfield.get(key) || [];
arr.push(r);
byBitfield.set(key, arr);
});
const duplicateRoleGroups = Array.from(byBitfield.values())
.filter((g) => g.length > 1)
.map((g) => g.map((r) => ({ id: r.id, name: r.name })));
const uselessRoles = roles
.filter((r) => !r.managed && r.permissions.bitfield === 0n && r.members.size === 0)
.map((r) => ({ id: r.id, name: r.name }));
const adminMemberIds = new Set<string>();
guild.members.cache.forEach((m) => {
if (m.permissions.has(PermissionFlagsBits.Administrator)) adminMemberIds.add(m.id);
});
const memberCount = guild.memberCount || guild.members.cache.size;
const tooManyAdmins = adminRoles.length > 3 || (memberCount > 0 && adminMemberIds.size / memberCount > 0.1);
return {
adminRoles,
banRoles,
dangerousBots,
publicChannels,
everyoneCanSendChannels,
emptyRoles,
duplicateRoleGroups,
uselessRoles,
tooManyAdmins,
adminMemberCount: adminMemberIds.size
};
}
}

View File

@@ -9,8 +9,12 @@ import {
EmbedBuilder, EmbedBuilder,
Guild, Guild,
GuildMember, GuildMember,
ModalBuilder,
ModalSubmitInteraction,
PermissionsBitField, PermissionsBitField,
TextChannel, TextChannel,
TextInputBuilder,
TextInputStyle,
Client, Client,
Message Message
} from 'discord.js'; } from 'discord.js';
@@ -88,6 +92,21 @@ export class TicketService {
return; return;
} }
const questions = settingsStore.get(interaction.guild.id)?.ticketConfig?.topics?.[topic]?.questions;
if (questions?.length) {
const modal = new ModalBuilder().setCustomId(`ticket:template:${topic}`).setTitle('Angaben zum Ticket');
questions.slice(0, 5).forEach((q, i) => {
const input = new TextInputBuilder()
.setCustomId(`q${i}`)
.setLabel(q.slice(0, 45))
.setStyle(TextInputStyle.Paragraph)
.setRequired(true);
modal.addComponents(new ActionRowBuilder<TextInputBuilder>().addComponents(input));
});
await interaction.showModal(modal);
return;
}
const record = await this.openTicket(interaction.guild, interaction.member as GuildMember, topic); const record = await this.openTicket(interaction.guild, interaction.member as GuildMember, topic);
await interaction.reply({ await interaction.reply({
content: record ? 'Ticket erstellt! Schau im neuen Kanal nach.' : 'Ticket konnte nicht erstellt werden.', content: record ? 'Ticket erstellt! Schau im neuen Kanal nach.' : 'Ticket konnte nicht erstellt werden.',
@@ -156,6 +175,39 @@ export class TicketService {
} }
} }
public async handleModal(interaction: ModalSubmitInteraction) {
if (!interaction.customId.startsWith('ticket:template:') || !interaction.guild) return;
const topic = interaction.customId.split(':')[2] || 'allgemein';
const existing = await prisma.ticket.findFirst({
where: { userId: interaction.user.id, guildId: interaction.guild.id, status: { notIn: ['closed', 'erledigt'] } }
});
if (existing) {
await interaction.reply({ content: 'Du hast bereits ein offenes Ticket. Bitte schließe es zuerst.', ephemeral: true });
return;
}
const questions = settingsStore.get(interaction.guild.id)?.ticketConfig?.topics?.[topic]?.questions || [];
const answers = questions.slice(0, 5).map((q, i) => ({ question: q, answer: interaction.fields.getTextInputValue(`q${i}`) }));
const record = await this.openTicket(interaction.guild, interaction.member as GuildMember, topic);
if (record && answers.length) {
const channel = await interaction.guild.channels.fetch(record.channelId).catch(() => null);
if (channel && channel.isTextBased()) {
const embed = new EmbedBuilder()
.setTitle('Angaben zum Ticket')
.setColor(context.branding.getColor(interaction.guild.id))
.addFields(answers.map((a) => ({ name: a.question.slice(0, 256), value: a.answer.slice(0, 1024) || '-' })));
await (channel as any).send({ embeds: [embed] }).catch(() => undefined);
}
}
await interaction.reply({
content: record ? 'Ticket erstellt! Schau im neuen Kanal nach.' : 'Ticket konnte nicht erstellt werden.',
ephemeral: true
});
}
public async claimTicket(interaction: ChatInputCommandInteraction) { public async claimTicket(interaction: ChatInputCommandInteraction) {
const channel = interaction.channel as TextChannel; const channel = interaction.channel as TextChannel;
const ticket = await prisma.ticket.findFirst({ where: { channelId: channel.id } }); const ticket = await prisma.ticket.findFirst({ where: { channelId: channel.id } });
@@ -247,7 +299,9 @@ export class TicketService {
private async openTicket(guild: Guild, member: GuildMember, topic: string): Promise<TicketRecord | null> { private async openTicket(guild: Guild, member: GuildMember, topic: string): Promise<TicketRecord | null> {
if (!this.isEnabled(guild.id)) return null; if (!this.isEnabled(guild.id)) return null;
const category = await this.ensureCategory(guild); const category = await this.ensureCategory(guild);
const supportRoleId = settingsStore.get(guild.id)?.supportRoleId || env.supportRoleId || null; const guildCfg = settingsStore.get(guild.id);
const topicRoleId = guildCfg?.ticketConfig?.topics?.[topic]?.roleId;
const supportRoleId = topicRoleId || guildCfg?.supportRoleId || env.supportRoleId || null;
const overwrites: any[] = [ const overwrites: any[] = [
{ {
id: guild.id, id: guild.id,

View File

@@ -102,6 +102,15 @@ router.get('/growth', requireAuth, async (req, res) => {
res.json({ stats }); res.json({ stats });
}); });
router.get('/growth/invites', requireAuth, async (req, res) => {
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
if (!guildId) return res.status(400).json({ error: 'guildId required' });
const guild = context.client?.guilds.cache.get(guildId) || (await context.client?.guilds.fetch(guildId).catch(() => null));
if (!guild) return res.status(404).json({ error: 'guild not found' });
const [breakdown, recentJoins] = await Promise.all([context.growth.getInviteBreakdown(guild), context.growth.getRecentJoins(guildId)]);
res.json({ breakdown, recentJoins });
});
router.get('/guild/activity', requireAuth, (req, res) => { router.get('/guild/activity', requireAuth, (req, res) => {
const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined; const guildId = typeof req.query.guildId === 'string' ? req.query.guildId : undefined;
if (!guildId) return res.status(400).json({ error: 'guildId required' }); if (!guildId) return res.status(400).json({ error: 'guildId required' });