Compare commits
14 Commits
main
...
ea9a387ee0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea9a387ee0 | ||
|
|
96579f07e9 | ||
|
|
1d583462e0 | ||
|
|
dca8dda045 | ||
|
|
8639eafbf7 | ||
|
|
a9cc01dfaa | ||
|
|
90bb12d054 | ||
|
|
c7136553c7 | ||
|
|
5acd81f87d | ||
|
|
336708191b | ||
|
|
e21d9e11b6 | ||
|
|
5681e14f8d | ||
|
|
67643cb54d | ||
|
|
86282fbe07 |
79
node_modules/.prisma/client/edge.js
generated
vendored
79
node_modules/.prisma/client/edge.js
generated
vendored
File diff suppressed because one or more lines are too long
73
node_modules/.prisma/client/index-browser.js
generated
vendored
73
node_modules/.prisma/client/index-browser.js
generated
vendored
@@ -141,6 +141,8 @@ exports.Prisma.GuildSettingsScalarFieldEnum = {
|
||||
reactionRolesEnabled: 'reactionRolesEnabled',
|
||||
reactionRolesConfig: 'reactionRolesConfig',
|
||||
eventsEnabled: 'eventsEnabled',
|
||||
registerEnabled: 'registerEnabled',
|
||||
registerConfig: 'registerConfig',
|
||||
supportRoleId: 'supportRoleId',
|
||||
updatedAt: 'updatedAt',
|
||||
createdAt: 'createdAt'
|
||||
@@ -157,6 +159,30 @@ exports.Prisma.TicketScalarFieldEnum = {
|
||||
status: 'status',
|
||||
claimedBy: 'claimedBy',
|
||||
transcript: 'transcript',
|
||||
firstClaimAt: 'firstClaimAt',
|
||||
firstResponseAt: 'firstResponseAt',
|
||||
kbSuggestionSentAt: 'kbSuggestionSentAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.TicketAutomationRuleScalarFieldEnum = {
|
||||
id: 'id',
|
||||
guildId: 'guildId',
|
||||
name: 'name',
|
||||
condition: 'condition',
|
||||
action: 'action',
|
||||
active: 'active',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.KnowledgeBaseArticleScalarFieldEnum = {
|
||||
id: 'id',
|
||||
guildId: 'guildId',
|
||||
title: 'title',
|
||||
keywords: 'keywords',
|
||||
content: 'content',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
@@ -228,6 +254,45 @@ exports.Prisma.EventSignupScalarFieldEnum = {
|
||||
canceledAt: 'canceledAt'
|
||||
};
|
||||
|
||||
exports.Prisma.RegisterFormScalarFieldEnum = {
|
||||
id: 'id',
|
||||
guildId: 'guildId',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
reviewChannelId: 'reviewChannelId',
|
||||
notifyRoleIds: 'notifyRoleIds',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.RegisterFormFieldScalarFieldEnum = {
|
||||
id: 'id',
|
||||
formId: 'formId',
|
||||
label: 'label',
|
||||
type: 'type',
|
||||
required: 'required',
|
||||
sortOrder: 'sortOrder'
|
||||
};
|
||||
|
||||
exports.Prisma.RegisterApplicationScalarFieldEnum = {
|
||||
id: 'id',
|
||||
guildId: 'guildId',
|
||||
userId: 'userId',
|
||||
formId: 'formId',
|
||||
status: 'status',
|
||||
reviewedBy: 'reviewedBy',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.RegisterApplicationAnswerScalarFieldEnum = {
|
||||
id: 'id',
|
||||
applicationId: 'applicationId',
|
||||
fieldId: 'fieldId',
|
||||
value: 'value'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
@@ -262,12 +327,18 @@ exports.Prisma.NullsOrder = {
|
||||
exports.Prisma.ModelName = {
|
||||
GuildSettings: 'GuildSettings',
|
||||
Ticket: 'Ticket',
|
||||
TicketAutomationRule: 'TicketAutomationRule',
|
||||
KnowledgeBaseArticle: 'KnowledgeBaseArticle',
|
||||
Level: 'Level',
|
||||
TicketSupportSession: 'TicketSupportSession',
|
||||
Birthday: 'Birthday',
|
||||
ReactionRoleSet: 'ReactionRoleSet',
|
||||
Event: 'Event',
|
||||
EventSignup: 'EventSignup'
|
||||
EventSignup: 'EventSignup',
|
||||
RegisterForm: 'RegisterForm',
|
||||
RegisterFormField: 'RegisterFormField',
|
||||
RegisterApplication: 'RegisterApplication',
|
||||
RegisterApplicationAnswer: 'RegisterApplicationAnswer'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
8519
node_modules/.prisma/client/index.d.ts
generated
vendored
8519
node_modules/.prisma/client/index.d.ts
generated
vendored
File diff suppressed because it is too large
Load Diff
79
node_modules/.prisma/client/index.js
generated
vendored
79
node_modules/.prisma/client/index.js
generated
vendored
File diff suppressed because one or more lines are too long
73
node_modules/.prisma/client/wasm.js
generated
vendored
73
node_modules/.prisma/client/wasm.js
generated
vendored
@@ -141,6 +141,8 @@ exports.Prisma.GuildSettingsScalarFieldEnum = {
|
||||
reactionRolesEnabled: 'reactionRolesEnabled',
|
||||
reactionRolesConfig: 'reactionRolesConfig',
|
||||
eventsEnabled: 'eventsEnabled',
|
||||
registerEnabled: 'registerEnabled',
|
||||
registerConfig: 'registerConfig',
|
||||
supportRoleId: 'supportRoleId',
|
||||
updatedAt: 'updatedAt',
|
||||
createdAt: 'createdAt'
|
||||
@@ -157,6 +159,30 @@ exports.Prisma.TicketScalarFieldEnum = {
|
||||
status: 'status',
|
||||
claimedBy: 'claimedBy',
|
||||
transcript: 'transcript',
|
||||
firstClaimAt: 'firstClaimAt',
|
||||
firstResponseAt: 'firstResponseAt',
|
||||
kbSuggestionSentAt: 'kbSuggestionSentAt',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.TicketAutomationRuleScalarFieldEnum = {
|
||||
id: 'id',
|
||||
guildId: 'guildId',
|
||||
name: 'name',
|
||||
condition: 'condition',
|
||||
action: 'action',
|
||||
active: 'active',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.KnowledgeBaseArticleScalarFieldEnum = {
|
||||
id: 'id',
|
||||
guildId: 'guildId',
|
||||
title: 'title',
|
||||
keywords: 'keywords',
|
||||
content: 'content',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
@@ -228,6 +254,45 @@ exports.Prisma.EventSignupScalarFieldEnum = {
|
||||
canceledAt: 'canceledAt'
|
||||
};
|
||||
|
||||
exports.Prisma.RegisterFormScalarFieldEnum = {
|
||||
id: 'id',
|
||||
guildId: 'guildId',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
reviewChannelId: 'reviewChannelId',
|
||||
notifyRoleIds: 'notifyRoleIds',
|
||||
isActive: 'isActive',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.RegisterFormFieldScalarFieldEnum = {
|
||||
id: 'id',
|
||||
formId: 'formId',
|
||||
label: 'label',
|
||||
type: 'type',
|
||||
required: 'required',
|
||||
sortOrder: 'sortOrder'
|
||||
};
|
||||
|
||||
exports.Prisma.RegisterApplicationScalarFieldEnum = {
|
||||
id: 'id',
|
||||
guildId: 'guildId',
|
||||
userId: 'userId',
|
||||
formId: 'formId',
|
||||
status: 'status',
|
||||
reviewedBy: 'reviewedBy',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
};
|
||||
|
||||
exports.Prisma.RegisterApplicationAnswerScalarFieldEnum = {
|
||||
id: 'id',
|
||||
applicationId: 'applicationId',
|
||||
fieldId: 'fieldId',
|
||||
value: 'value'
|
||||
};
|
||||
|
||||
exports.Prisma.SortOrder = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
@@ -262,12 +327,18 @@ exports.Prisma.NullsOrder = {
|
||||
exports.Prisma.ModelName = {
|
||||
GuildSettings: 'GuildSettings',
|
||||
Ticket: 'Ticket',
|
||||
TicketAutomationRule: 'TicketAutomationRule',
|
||||
KnowledgeBaseArticle: 'KnowledgeBaseArticle',
|
||||
Level: 'Level',
|
||||
TicketSupportSession: 'TicketSupportSession',
|
||||
Birthday: 'Birthday',
|
||||
ReactionRoleSet: 'ReactionRoleSet',
|
||||
Event: 'Event',
|
||||
EventSignup: 'EventSignup'
|
||||
EventSignup: 'EventSignup',
|
||||
RegisterForm: 'RegisterForm',
|
||||
RegisterFormField: 'RegisterFormField',
|
||||
RegisterApplication: 'RegisterApplication',
|
||||
RegisterApplicationAnswer: 'RegisterApplicationAnswer'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -172,17 +172,18 @@ model RegisterForm {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
fields RegisterFormField[]
|
||||
applications RegisterApplication[]
|
||||
|
||||
@@index([guildId, isActive])
|
||||
}
|
||||
|
||||
model RegisterFormField {
|
||||
id String @id @default(cuid())
|
||||
formId String
|
||||
label String
|
||||
type String
|
||||
required Boolean @default(false)
|
||||
"order" Int @default(0)
|
||||
id String @id @default(cuid())
|
||||
formId String
|
||||
label String
|
||||
type String
|
||||
required Boolean @default(false)
|
||||
sortOrder Int @default(0)
|
||||
|
||||
form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "GuildSettings" ADD COLUMN "registerConfig" JSONB,
|
||||
ADD COLUMN "registerEnabled" BOOLEAN;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RegisterForm" (
|
||||
"id" TEXT NOT NULL,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"reviewChannelId" TEXT,
|
||||
"notifyRoleIds" TEXT[],
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "RegisterForm_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RegisterFormField" (
|
||||
"id" TEXT NOT NULL,
|
||||
"formId" TEXT NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"required" BOOLEAN NOT NULL DEFAULT false,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "RegisterFormField_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RegisterApplication" (
|
||||
"id" TEXT NOT NULL,
|
||||
"guildId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"formId" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL DEFAULT 'pending',
|
||||
"reviewedBy" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "RegisterApplication_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "RegisterApplicationAnswer" (
|
||||
"id" TEXT NOT NULL,
|
||||
"applicationId" TEXT NOT NULL,
|
||||
"fieldId" TEXT NOT NULL,
|
||||
"value" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "RegisterApplicationAnswer_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RegisterForm_guildId_isActive_idx" ON "RegisterForm"("guildId", "isActive");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "RegisterApplication_guildId_formId_status_idx" ON "RegisterApplication"("guildId", "formId", "status");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RegisterFormField" ADD CONSTRAINT "RegisterFormField_formId_fkey" FOREIGN KEY ("formId") REFERENCES "RegisterForm"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RegisterApplication" ADD CONSTRAINT "RegisterApplication_formId_fkey" FOREIGN KEY ("formId") REFERENCES "RegisterForm"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "RegisterApplicationAnswer" ADD CONSTRAINT "RegisterApplicationAnswer_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "RegisterApplication"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -174,17 +174,18 @@ model RegisterForm {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
fields RegisterFormField[]
|
||||
applications RegisterApplication[]
|
||||
|
||||
@@index([guildId, isActive])
|
||||
}
|
||||
|
||||
model RegisterFormField {
|
||||
id String @id @default(cuid())
|
||||
formId String
|
||||
label String
|
||||
type String
|
||||
required Boolean @default(false)
|
||||
"order" Int @default(0)
|
||||
id String @id @default(cuid())
|
||||
formId String
|
||||
label String
|
||||
type String
|
||||
required Boolean @default(false)
|
||||
sortOrder Int @default(0)
|
||||
|
||||
form RegisterForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AudioPlayer, AudioPlayerStatus, AudioResource, VoiceConnection, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel, VoiceConnectionStatus, getVoiceConnection } from '@discordjs/voice';
|
||||
import { AudioPlayer, AudioPlayerStatus, AudioResource, VoiceConnection, StreamType, createAudioPlayer, createAudioResource, entersState, joinVoiceChannel, VoiceConnectionStatus, getVoiceConnection } from '@discordjs/voice';
|
||||
import { ChatInputCommandInteraction, GuildMember, TextChannel } from 'discord.js';
|
||||
import play from 'play-dl';
|
||||
import { Readable } from 'stream';
|
||||
import { logger } from '../utils/logger';
|
||||
import { settingsStore } from '../config/state';
|
||||
|
||||
@@ -8,7 +8,8 @@ export type LoopMode = 'off' | 'song' | 'queue';
|
||||
|
||||
interface QueueItem {
|
||||
title: string;
|
||||
url: string;
|
||||
streamUrl: string;
|
||||
displayUrl?: string;
|
||||
requester: string;
|
||||
originalQuery?: string;
|
||||
}
|
||||
@@ -24,6 +25,7 @@ interface QueueState {
|
||||
|
||||
export class MusicService {
|
||||
private queues = new Map<string, QueueState>();
|
||||
private spotifyToken: { value: string; expiresAt: number } | null = null;
|
||||
|
||||
private getQueue(guildId: string) {
|
||||
const cfg = settingsStore.get(guildId);
|
||||
@@ -82,7 +84,13 @@ export class MusicService {
|
||||
await interaction.reply({ content: 'Der gefundene Link ist ungueltig.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
const queueItem: QueueItem = { title: track.title ?? 'Unbekannt', url: track.url, requester: interaction.user.tag, originalQuery: trimmedQuery };
|
||||
const queueItem: QueueItem = {
|
||||
title: track.title ?? 'Unbekannt',
|
||||
streamUrl: track.url,
|
||||
displayUrl: track.url,
|
||||
requester: interaction.user.tag,
|
||||
originalQuery: trimmedQuery
|
||||
};
|
||||
const queue = this.getQueue(interaction.guildId);
|
||||
if (!queue) {
|
||||
const player = createAudioPlayer();
|
||||
@@ -160,7 +168,7 @@ export class MusicService {
|
||||
next = queue.current;
|
||||
}
|
||||
if (!next) break;
|
||||
const streamUrlCheck = typeof next.url === 'string' ? next.url.trim() : '';
|
||||
const streamUrlCheck = typeof next.streamUrl === 'string' ? next.streamUrl.trim() : '';
|
||||
if (streamUrlCheck && streamUrlCheck !== 'undefined' && /^https?:\/\//i.test(streamUrlCheck)) {
|
||||
break;
|
||||
}
|
||||
@@ -172,24 +180,16 @@ export class MusicService {
|
||||
queue.channel.send({ content: 'Queue beendet.' }).catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
const streamUrl = (next.url || '').trim();
|
||||
const streamUrl = (next.streamUrl || '').trim();
|
||||
|
||||
queue.current = next;
|
||||
try {
|
||||
const kind = await play.validate(streamUrl);
|
||||
if (kind !== 'so_track') {
|
||||
logger.error('Music stream error', { reason: 'unsupported_url', kind, item: next });
|
||||
queue.channel.send({ content: `Nur SoundCloud wird unterstuetzt, ueberspringe: **${next.title ?? 'Unbekannt'}**.` }).catch(() => undefined);
|
||||
queue.current = undefined;
|
||||
this.processQueue(guildId);
|
||||
return;
|
||||
}
|
||||
const finalUrl = streamUrl;
|
||||
if (!finalUrl || !/^https?:\/\//i.test(finalUrl) || finalUrl === 'undefined') throw new Error('soundcloud_url_invalid');
|
||||
const stream = await play.stream(finalUrl);
|
||||
if (!stream?.stream) throw new Error('stream_invalid');
|
||||
const resource: AudioResource = createAudioResource(stream.stream, {
|
||||
inputType: stream.type
|
||||
if (!/^https?:\/\//i.test(streamUrl)) throw new Error('spotify_stream_url_invalid');
|
||||
const res = await fetch(streamUrl);
|
||||
if (!res.ok || !res.body) throw new Error('spotify_stream_fetch_failed');
|
||||
const body: any = typeof (res as any).body?.getReader === 'function' ? Readable.fromWeb(res.body as any) : (res as any).body;
|
||||
const resource: AudioResource = createAudioResource(body, {
|
||||
inputType: StreamType.Arbitrary
|
||||
});
|
||||
queue.player.play(resource);
|
||||
queue.connection.subscribe(queue.player);
|
||||
@@ -234,39 +234,79 @@ export class MusicService {
|
||||
private async resolveTrack(query: string, opts?: { skipPlaylist?: boolean }): Promise<{ title: string; url: string } | null> {
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed) return null;
|
||||
try {
|
||||
let validation: string | null = null;
|
||||
try {
|
||||
validation = await play.validate(trimmed);
|
||||
} catch (err) {
|
||||
logger.warn('Music validate error', err);
|
||||
}
|
||||
if (validation === 'so_track') {
|
||||
return { title: trimmed, url: trimmed };
|
||||
}
|
||||
// nur SoundCloud erlaubt, alles andere ignorieren
|
||||
} catch (err) {
|
||||
logger.error('Music resolve error', err);
|
||||
const token = await this.getSpotifyToken();
|
||||
const trackId = this.extractSpotifyTrackId(trimmed);
|
||||
if (trackId) {
|
||||
const track = await this.fetchSpotifyTrack(trackId, token);
|
||||
if (track) return track;
|
||||
}
|
||||
const search = await this.searchSpotifyTrack(trimmed, token);
|
||||
return search;
|
||||
}
|
||||
|
||||
const scSearch = await play.search(trimmed, { source: { soundcloud: 'tracks' }, limit: 1 }).catch((err) => {
|
||||
logger.warn('SoundCloud search skipped', err?.message || err);
|
||||
return [];
|
||||
});
|
||||
if (scSearch && scSearch.length) {
|
||||
const sc = scSearch[0];
|
||||
const url = sc.url || '';
|
||||
if (url && /^https?:\/\//i.test(url)) return { title: sc.title ?? 'Unbekannt', url };
|
||||
private async getSpotifyToken(): Promise<string> {
|
||||
if (this.spotifyToken && this.spotifyToken.expiresAt > Date.now() + 30000) {
|
||||
return this.spotifyToken.value;
|
||||
}
|
||||
const clientId = process.env.SPOTIFY_CLIENT_ID;
|
||||
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
|
||||
if (!clientId || !clientSecret) throw new Error('missing_spotify_credentials');
|
||||
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
||||
const res = await fetch('https://accounts.spotify.com/api/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${basic}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: 'grant_type=client_credentials'
|
||||
});
|
||||
if (!res.ok) throw new Error('spotify_auth_failed');
|
||||
const data = (await res.json()) as { access_token: string; expires_in: number };
|
||||
const expiresInMs = Math.max(30_000, (data.expires_in || 3600) * 1000);
|
||||
this.spotifyToken = { value: data.access_token, expiresAt: Date.now() + expiresInMs };
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
private extractSpotifyTrackId(query: string) {
|
||||
const urlMatch = query.match(/spotify\.com\/track\/([A-Za-z0-9]+)/i);
|
||||
if (urlMatch?.[1]) return urlMatch[1];
|
||||
const uriMatch = query.match(/spotify:track:([A-Za-z0-9]+)/i);
|
||||
if (uriMatch?.[1]) return uriMatch[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
private buildVideoUrl(details: any): string | null {
|
||||
if (!details) return null;
|
||||
const url = details.url || details.permalink;
|
||||
if (typeof url === 'string' && /^https?:\/\//i.test(url)) return url;
|
||||
if (details.id) return `https://www.youtube.com/watch?v=${details.id}`;
|
||||
if (details.videoId) return `https://www.youtube.com/watch?v=${details.videoId}`;
|
||||
return null;
|
||||
private async fetchSpotifyTrack(id: string, token: string): Promise<{ title: string; url: string } | null> {
|
||||
const res = await fetch(`https://api.spotify.com/v1/tracks/${id}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) {
|
||||
logger.warn('Spotify track fetch failed', { status: res.status });
|
||||
return null;
|
||||
}
|
||||
const data: any = await res.json();
|
||||
if (!data?.preview_url) {
|
||||
logger.warn('Spotify track has no preview_url', { id });
|
||||
return null;
|
||||
}
|
||||
const artists = Array.isArray(data.artists) ? data.artists.map((a: any) => a.name).filter(Boolean).join(', ') : '';
|
||||
const title = [data.name, artists].filter(Boolean).join(' - ');
|
||||
return { title: title || data.name || 'Track', url: data.preview_url };
|
||||
}
|
||||
|
||||
private async searchSpotifyTrack(query: string, token: string): Promise<{ title: string; url: string } | null> {
|
||||
const market = process.env.SPOTIFY_MARKET || 'DE';
|
||||
const res = await fetch(`https://api.spotify.com/v1/search?q=${encodeURIComponent(query)}&type=track&limit=1&market=${market}`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
if (!res.ok) {
|
||||
logger.warn('Spotify search failed', { status: res.status });
|
||||
return null;
|
||||
}
|
||||
const data: any = await res.json();
|
||||
const track = data?.tracks?.items?.[0];
|
||||
if (!track || !track.preview_url) return null;
|
||||
const artists = Array.isArray(track.artists) ? track.artists.map((a: any) => a.name).filter(Boolean).join(', ') : '';
|
||||
const title = [track.name, artists].filter(Boolean).join(' - ');
|
||||
return { title: title || track.name || 'Track', url: track.preview_url };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export class RegisterService {
|
||||
}
|
||||
|
||||
public async listForms(guildId: string) {
|
||||
return prisma.registerForm.findMany({ where: { guildId }, include: { fields: { orderBy: { order: 'asc' } } }, orderBy: { createdAt: 'desc' } });
|
||||
return prisma.registerForm.findMany({ where: { guildId }, include: { fields: { orderBy: { sortOrder: 'asc' } } }, orderBy: { createdAt: 'desc' } });
|
||||
}
|
||||
|
||||
public async saveForm(form: {
|
||||
@@ -55,10 +55,10 @@ export class RegisterService {
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: f.required ?? false,
|
||||
order: f.order ?? idx
|
||||
sortOrder: f.order ?? idx
|
||||
}))
|
||||
});
|
||||
return prisma.registerForm.findUnique({ where: { id: form.id }, include: { fields: { orderBy: { order: 'asc' } } } });
|
||||
return prisma.registerForm.findUnique({ where: { id: form.id }, include: { fields: { orderBy: { sortOrder: 'asc' } } } });
|
||||
}
|
||||
const created = await prisma.registerForm.create({
|
||||
data: {
|
||||
@@ -73,11 +73,11 @@ export class RegisterService {
|
||||
label: f.label,
|
||||
type: f.type,
|
||||
required: f.required ?? false,
|
||||
order: f.order ?? idx
|
||||
sortOrder: f.order ?? idx
|
||||
}))
|
||||
}
|
||||
},
|
||||
include: { fields: { orderBy: { order: 'asc' } } }
|
||||
include: { fields: { orderBy: { sortOrder: 'asc' } } }
|
||||
});
|
||||
return created;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export class RegisterService {
|
||||
public async handleButton(interaction: ButtonInteraction) {
|
||||
if (interaction.customId.startsWith('register:form:')) {
|
||||
const formId = interaction.customId.split(':')[2];
|
||||
const form = await prisma.registerForm.findFirst({ where: { id: formId }, include: { fields: { orderBy: { order: 'asc' } } } });
|
||||
const form = await prisma.registerForm.findFirst({ where: { id: formId }, include: { fields: { orderBy: { sortOrder: 'asc' } } } });
|
||||
if (!form) return interaction.reply({ content: 'Formular nicht gefunden.', ephemeral: true });
|
||||
const modal = new ModalBuilder().setTitle(form.name).setCustomId(`register:submit:${form.id}`);
|
||||
const components: any[] = [];
|
||||
@@ -164,7 +164,7 @@ export class RegisterService {
|
||||
const formId = interaction.customId.split(':')[2];
|
||||
const form = await prisma.registerForm.findFirst({
|
||||
where: { id: formId },
|
||||
include: { fields: { orderBy: { order: 'asc' } } }
|
||||
include: { fields: { orderBy: { sortOrder: 'asc' } } }
|
||||
});
|
||||
if (!form) {
|
||||
await interaction.reply({ content: 'Formular nicht gefunden.', ephemeral: true });
|
||||
@@ -200,7 +200,7 @@ export class RegisterService {
|
||||
const channel = await guild.channels.fetch(channelId).catch(() => null);
|
||||
if (!channel || !channel.isTextBased()) return;
|
||||
const member = await guild.members.fetch(userId).catch(() => null);
|
||||
const fields = await prisma.registerFormField.findMany({ where: { formId: form.id }, orderBy: { order: 'asc' } });
|
||||
const fields = await prisma.registerFormField.findMany({ where: { formId: form.id }, orderBy: { sortOrder: 'asc' } });
|
||||
const answers = await prisma.registerApplicationAnswer.findMany({ where: { applicationId: app.id } });
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`Registrierung: ${form.name}`)
|
||||
@@ -254,3 +254,5 @@ export class RegisterService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -43,20 +43,21 @@ router.get('/', (req, res) => {
|
||||
<aside class="sidebar">
|
||||
<div class="brand">Papo Control</div>
|
||||
<div class="nav">
|
||||
<a class="active" href="#overview" data-target="overview"><span class="icon">🏠</span> Uebersicht</a>
|
||||
<a href="#tickets" data-target="tickets"><span class="icon">🎫</span> Ticketsystem</a>
|
||||
<a href="#automod" data-target="automod" class="automod-link"><span class="icon">🛡️</span> Automod</a>
|
||||
<a href="#welcome" data-target="welcome" class="welcome-link"><span class="icon">✨</span> Willkommen</a>
|
||||
<a href="#dynamicvoice" data-target="dynamicvoice" class="dynamicvoice-link"><span class="icon">🎧</span> Dynamic Voice</a>
|
||||
<a href="#birthday" data-target="birthday" class="birthday-link"><span class="icon">🎂</span> Birthday</a>
|
||||
<a href="#reactionroles" data-target="reactionroles" class="reactionroles-link"><span class="icon">😎</span> Reaction Roles</a>
|
||||
<a href="#statuspage" data-target="statuspage" class="statuspage-link"><span class="icon">🖥️</span> Statuspage</a>
|
||||
<a href="#settings" data-target="settings"><span class="icon">⚙️</span> Einstellungen</a>
|
||||
<a href="#modules" data-target="modules"><span class="icon">🧩</span> Module</a>
|
||||
<a href="#events" data-target="events" class="events-link"><span class="icon">📅</span> Events</a>
|
||||
<a href="#admin" data-target="admin" class="admin-link hidden"><span class="icon">🛡</span> Admin</a>
|
||||
<a class="active" href="#overview" data-target="overview"><span class="icon">-</span> Uebersicht</a>
|
||||
<a href="#tickets" data-target="tickets"><span class="icon">-</span> Ticketsystem</a>
|
||||
<a href="#automod" data-target="automod" class="automod-link"><span class="icon">-</span> Automod</a>
|
||||
<a href="#welcome" data-target="welcome" class="welcome-link"><span class="icon">-</span> Willkommen</a>
|
||||
<a href="#dynamicvoice" data-target="dynamicvoice" class="dynamicvoice-link"><span class="icon">-</span> Dynamic Voice</a>
|
||||
<a href="#birthday" data-target="birthday" class="birthday-link"><span class="icon">-</span> Birthday</a>
|
||||
<a href="#reactionroles" data-target="reactionroles" class="reactionroles-link"><span class="icon">-</span> Reaction Roles</a>
|
||||
<a href="#statuspage" data-target="statuspage" class="statuspage-link"><span class="icon">-</span> Statuspage</a>
|
||||
<a href="#settings" data-target="settings"><span class="icon">-</span> Einstellungen</a>
|
||||
<a href="#modules" data-target="modules"><span class="icon">-</span> Module</a>
|
||||
<a href="#register" data-target="register" class="register-link"><span class="icon">-</span> Register</a>
|
||||
<a href="#events" data-target="events" class="events-link"><span class="icon">-</span> Events</a>
|
||||
<a href="#admin" data-target="admin" class="admin-link hidden"><span class="icon">-</span> Admin</a>
|
||||
</div>
|
||||
<div class="muted">Angemeldet als <span id="userInfo"></span></div>
|
||||
<div class="muted"> Angemeldet als <span id="userInfo"></span></div>
|
||||
<button id="logoutBtn" class="logout">Logout</button>
|
||||
</aside>
|
||||
`;
|
||||
@@ -214,6 +215,11 @@ router.get('/', (req, res) => {
|
||||
.module-meta { display:flex; flex-direction:column; gap:4px; }
|
||||
.module-title { font-weight:700; color:var(--text); }
|
||||
.module-desc { color:var(--muted); font-size:13px; }
|
||||
.register-tabs { display:flex; gap:8px; flex-wrap:wrap; margin-top:6px; }
|
||||
.register-tab { display:none; }
|
||||
.register-tab.active { display:block; }
|
||||
.register-meta { display:flex; gap:8px; flex-wrap:wrap; color:var(--muted); font-size:12px; }
|
||||
.register-answer { padding:10px 12px; border-radius:12px; border:1px solid rgba(255,255,255,0.08); background:rgba(255,255,255,0.04); }
|
||||
.toast { position:fixed; top:20px; right:24px; padding:12px 14px; border-radius:12px; background:rgba(249,115,22,0.18); border:1px solid rgba(249,115,22,0.45); color:#ffe6d0; font-weight:700; box-shadow:0 12px 32px rgba(0,0,0,0.34); opacity:0; transform:translateY(-10px); transition:opacity 150ms ease, transform 150ms ease; z-index:1100; backdrop-filter:blur(10px); }
|
||||
.toast.error { background:rgba(239,68,68,0.18); border-color:rgba(239,68,68,0.4); color:#ffe4e6; }
|
||||
.toast.show { opacity:1; transform:translateY(0); }
|
||||
@@ -330,10 +336,10 @@ router.get('/', (req, res) => {
|
||||
<div class="card" style="display:flex; gap:10px; flex-wrap:wrap; align-items:center; justify-content:space-between;">
|
||||
<div>
|
||||
<p class="section-title">Tickets</p>
|
||||
<p class="section-sub"><EFBFBD>bersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
|
||||
<p class="section-sub"> bersicht, Pipeline, SLA, Automationen, Knowledge-Base.</p>
|
||||
</div>
|
||||
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
||||
<button class="secondary-btn ticket-tab-btn active" data-tab="overview"><EFBFBD>bersicht</button>
|
||||
<button class="secondary-btn ticket-tab-btn active" data-tab="overview"> bersicht</button>
|
||||
<button class="secondary-btn ticket-tab-btn" data-tab="pipeline">Pipeline</button>
|
||||
<button class="secondary-btn ticket-tab-btn" data-tab="sla">SLA</button>
|
||||
<button class="secondary-btn ticket-tab-btn" data-tab="automations">Automationen</button>
|
||||
@@ -346,7 +352,7 @@ router.get('/', (req, res) => {
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||||
<div>
|
||||
<p class="section-title">Ticketliste</p>
|
||||
<p class="section-sub">Links ausw<EFBFBD>hlen, Details im Modal. Plus <EFBFBD>ffnet Panel-Erstellung.</p>
|
||||
<p class="section-sub">Links ausw hlen, Details im Modal. Plus ffnet Panel-Erstellung.</p>
|
||||
</div>
|
||||
<div class="row" style="gap:10px;">
|
||||
<button class="secondary-btn" id="openSupportLogin" type="button">Support-Login Einstellungen</button>
|
||||
@@ -381,7 +387,7 @@ router.get('/', (req, res) => {
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px;">
|
||||
<div>
|
||||
<p class="section-title">Status-Pipeline</p>
|
||||
<p class="section-sub">Tickets nach Phase. Status per Dropdown <EFBFBD>ndern.</p>
|
||||
<p class="section-sub">Tickets nach Phase. Status per Dropdown ndern.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid" style="grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap:12px;" id="pipelineGrid">
|
||||
@@ -430,7 +436,7 @@ router.get('/', (req, res) => {
|
||||
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||
<div>
|
||||
<p class="section-title">Automationen</p>
|
||||
<p class="section-sub">Regeln f<EFBFBD>r Ticket-Aktionen.</p>
|
||||
<p class="section-sub">Regeln f r Ticket-Aktionen.</p>
|
||||
</div>
|
||||
<button class="secondary-btn" id="addAutomation">Neue Regel</button>
|
||||
</div>
|
||||
@@ -467,7 +473,7 @@ router.get('/', (req, res) => {
|
||||
<div style="display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||||
<div>
|
||||
<p class="section-title">Knowledge-Base</p>
|
||||
<p class="section-sub">Artikel f<EFBFBD>r Self-Service.</p>
|
||||
<p class="section-sub">Artikel f r Self-Service.</p>
|
||||
</div>
|
||||
<button class="secondary-btn" id="addKb">Neuer Artikel</button>
|
||||
</div>
|
||||
@@ -586,7 +592,7 @@ router.get('/', (req, res) => {
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Embed Footer</label>
|
||||
<input id="welcomeFooter" placeholder="Schön, dass du da bist!" />
|
||||
<input id="welcomeFooter" placeholder="Sch n, dass du da bist!" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
@@ -609,7 +615,7 @@ router.get('/', (req, res) => {
|
||||
<div class="embed-color" id="welcomePreviewColor"></div>
|
||||
<div class="embed-body">
|
||||
<div class="embed-title" id="welcomePreviewTitle">Willkommen!</div>
|
||||
<div class="embed-desc" id="welcomePreviewDesc">Schön, dass du da bist.</div>
|
||||
<div class="embed-desc" id="welcomePreviewDesc">Sch n, dass du da bist.</div>
|
||||
<div class="embed-footer" id="welcomePreviewFooter">Footer</div>
|
||||
<img id="welcomePreviewImage" class="embed-image" style="display:none;" />
|
||||
</div>
|
||||
@@ -670,6 +676,79 @@ router.get('/', (req, res) => {
|
||||
<div id="moduleList" class="module-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="section" data-section="register">
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||
<div>
|
||||
<p class="section-title">Register Modul</p>
|
||||
<p class="section-sub">Formulare verwalten und Antraege einsehen.</p>
|
||||
</div>
|
||||
<span class="badge" id="registerStatus">Aktiv</span>
|
||||
</div>
|
||||
<div class="register-tabs">
|
||||
<button class="ticket-tab-btn register-tab-btn active" data-tab="forms">Formulare</button>
|
||||
<button class="ticket-tab-btn register-tab-btn" data-tab="apps">Registrierungen</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="register-tab ticket-tab active" data-tab="forms">
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||
<div>
|
||||
<p class="section-title">Formulare</p>
|
||||
<p class="section-sub">Formulare anlegen, bearbeiten und Panels senden.</p>
|
||||
</div>
|
||||
<div class="row" style="gap:8px; flex-wrap:wrap;">
|
||||
<button class="secondary-btn" id="registerFormsReload" type="button">Reload</button>
|
||||
<button class="secondary-btn" id="registerFormNew" type="button">Neues Formular</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="registerFormList" class="module-list" style="margin-top:12px;"></div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<form id="registerFormForm" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap:12px;">
|
||||
<input type="hidden" id="registerFormId" />
|
||||
<div class="form-field"><label class="form-label">Name</label><input id="registerFormName" placeholder="Bewerbung" /></div>
|
||||
<div class="form-field"><label class="form-label">Review Channel ID</label><input id="registerFormChannel" placeholder="123456789012345678" /></div>
|
||||
<div class="form-field"><label class="form-label">Notify Rollen (kommagetrennt)</label><input id="registerFormRoles" placeholder="123,456" /></div>
|
||||
<div class="form-field"><label class="form-label">Aktiv</label><div id="registerFormActive" class="switch on"></div></div>
|
||||
<div class="form-field" style="grid-column:1/-1;"><label class="form-label">Beschreibung</label><textarea id="registerFormDescription" rows="2" placeholder="Kurze Beschreibung"></textarea></div>
|
||||
<div class="form-field" style="grid-column:1/-1;"><label class="form-label">Felder (Label | Typ | Pflicht)</label><textarea id="registerFormFields" rows="4" placeholder="Name | shortText | true Begruendung | longText | true"></textarea><p class="muted">Typ: shortText oder longText. Eine Zeile pro Feld.</p></div>
|
||||
<div class="form-field" style="grid-column:1/-1; display:flex; gap:8px;">
|
||||
<button type="submit">Speichern</button>
|
||||
<button type="button" class="secondary-btn" id="registerFormReset">Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="muted" id="registerFormStatus"></p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="register-tab ticket-tab" data-tab="apps">
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;">
|
||||
<div>
|
||||
<p class="section-title">Registrierungen</p>
|
||||
<p class="section-sub">Antraege filtern und einsehen.</p>
|
||||
</div>
|
||||
<div class="row" style="gap:8px; flex-wrap:wrap; align-items:center;">
|
||||
<select id="registerAppsFilter" class="muted" style="min-width:140px;">
|
||||
<option value="">Alle Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="accepted">Accepted</option>
|
||||
<option value="invited">Invited</option>
|
||||
<option value="rejected">Rejected</option>
|
||||
</select>
|
||||
<select id="registerAppsFormFilter" class="muted" style="min-width:160px;">
|
||||
<option value="">Alle Formulare</option>
|
||||
</select>
|
||||
<button class="secondary-btn" id="registerAppsReload" type="button">Reload</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="registerAppsList" class="ticket-list-pane"></div>
|
||||
</section>
|
||||
<section class="card">
|
||||
<div id="registerAppDetail" class="muted">Waehle einen Antrag aus der Liste.</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" data-section="birthday">
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between; align-items:flex-start; gap:12px;">
|
||||
@@ -736,7 +815,7 @@ router.get('/', (req, res) => {
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Eintraege (Emoji | Role ID | Label | Beschreibung)</label>
|
||||
<textarea id="reactionRoleEntries" rows="4" placeholder="😀 | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
|
||||
<textarea id="reactionRoleEntries" rows="4" placeholder=" | 123456789 | Freunde | Erhaelt Freunde Rolle"></textarea>
|
||||
<p class="muted">Eine Zeile pro Zuordnung. Label/Beschreibung optional.</p>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px;">
|
||||
@@ -810,7 +889,7 @@ router.get('/', (req, res) => {
|
||||
<section class="card">
|
||||
<div class="row" style="justify-content:space-between;">
|
||||
<div>
|
||||
<p class="section-title">Aktivität (letzte 24h)</p>
|
||||
<p class="section-title">Aktivit t (letzte 24h)</p>
|
||||
<p class="section-sub">Events/Commands pro Stunde</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -820,7 +899,7 @@ router.get('/', (req, res) => {
|
||||
<div class="row" style="justify-content:space-between;">
|
||||
<div>
|
||||
<p class="section-title">Logs</p>
|
||||
<p class="section-sub">Neueste Einträge</p>
|
||||
<p class="section-sub">Neueste Eintr ge</p>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="log-list" id="adminLogs"></ul>
|
||||
@@ -847,13 +926,13 @@ router.get('/', (req, res) => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="option-grid">
|
||||
<div class="option-card"><span>👋 User Join / Leave</span><div id="logJoinLeave" class="switch on"></div></div>
|
||||
<div class="option-card"><span>✏️ Message Edit</span><div id="logMsgEdit" class="switch on"></div></div>
|
||||
<div class="option-card"><span>🗑️ Message Delete</span><div id="logMsgDelete" class="switch on"></div></div>
|
||||
<div class="option-card"><span>🛡️ Automod Actions</span><div id="logAutomod" class="switch on"></div></div>
|
||||
<div class="option-card"><span>🎫 Ticket Actions</span><div id="logTickets" class="switch on"></div></div>
|
||||
<div class="option-card"><span>🎵 Musik-Events</span><div id="logMusic" class="switch on"></div></div>
|
||||
<div class="option-card"><span>⚙️ System / Channels</span><div id="logSystem" class="switch on"></div></div>
|
||||
<div class="option-card"><span> User Join / Leave</span><div id="logJoinLeave" class="switch on"></div></div>
|
||||
<div class="option-card"><span> Message Edit</span><div id="logMsgEdit" class="switch on"></div></div>
|
||||
<div class="option-card"><span> Message Delete</span><div id="logMsgDelete" class="switch on"></div></div>
|
||||
<div class="option-card"><span> Automod Actions</span><div id="logAutomod" class="switch on"></div></div>
|
||||
<div class="option-card"><span> Ticket Actions</span><div id="logTickets" class="switch on"></div></div>
|
||||
<div class="option-card"><span> Musik-Events</span><div id="logMusic" class="switch on"></div></div>
|
||||
<div class="option-card"><span> System / Channels</span><div id="logSystem" class="switch on"></div></div>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:flex-end; gap:10px; margin-top:12px;">
|
||||
<button id="loggingSave" type="button">Logging speichern</button>
|
||||
@@ -1211,6 +1290,10 @@ router.get('/', (req, res) => {
|
||||
let editingReactionRole = null;
|
||||
let supportLoginCache = {};
|
||||
let eventsCache = [];
|
||||
let registerFormsCache = [];
|
||||
let registerAppsCache = [];
|
||||
let registerSelectedApp = null;
|
||||
let registerConfigCache = {};
|
||||
|
||||
function activateSection(key) {
|
||||
sections.forEach((s) => s.classList.toggle('active', s.dataset.section === key));
|
||||
@@ -1243,6 +1326,7 @@ router.get('/', (req, res) => {
|
||||
const birthdayEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'birthdayEnabled') ? modulesCache['birthdayEnabled'] : true;
|
||||
const reactionRolesEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'reactionRolesEnabled') ? modulesCache['reactionRolesEnabled'] : true;
|
||||
const eventsEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'eventsEnabled') ? modulesCache['eventsEnabled'] : true;
|
||||
const registerEnabled = Object.prototype.hasOwnProperty.call(modulesCache, 'registerEnabled') ? modulesCache['registerEnabled'] : true;
|
||||
if (automodNav) automodNav.classList.toggle('hidden', !autoEnabled);
|
||||
if (welcomeNav) welcomeNav.classList.toggle('hidden', !welcomeEnabled);
|
||||
if (dynamicVoiceNav) dynamicVoiceNav.classList.toggle('hidden', !dynamicVoiceEnabled);
|
||||
@@ -1254,6 +1338,12 @@ router.get('/', (req, res) => {
|
||||
if (reactionRolesNav) reactionRolesNav.classList.toggle('hidden', !reactionRolesEnabled);
|
||||
const eventsNav = document.querySelector('.nav .events-link');
|
||||
if (eventsNav) eventsNav.classList.toggle('hidden', !eventsEnabled);
|
||||
if (registerNav) registerNav.classList.toggle('hidden', !registerEnabled);
|
||||
if (registerSection) registerSection.classList.toggle('hidden', !registerEnabled);
|
||||
if (registerStatus) {
|
||||
registerStatus.textContent = registerEnabled ? 'Aktiv' : 'Deaktiviert';
|
||||
registerStatus.className = 'badge' + (registerEnabled ? ' active' : '');
|
||||
}
|
||||
const adminNav = document.querySelector('.nav .admin-link');
|
||||
if (adminNav) adminNav.classList.toggle('hidden', !isAdmin);
|
||||
const current = location.hash.replace('#','') || 'overview';
|
||||
@@ -1265,6 +1355,7 @@ router.get('/', (req, res) => {
|
||||
(current === 'birthday' && !birthdayEnabled) ||
|
||||
(current === 'reactionroles' && !reactionRolesEnabled) ||
|
||||
(current === 'events' && !eventsEnabled) ||
|
||||
(current === 'register' && !registerEnabled) ||
|
||||
(current === 'admin' && !isAdmin)
|
||||
) {
|
||||
activateSection('overview');
|
||||
@@ -1283,7 +1374,7 @@ router.get('/', (req, res) => {
|
||||
|
||||
function parseList(val) {
|
||||
return (val || '')
|
||||
.split(/[,\\n]/)
|
||||
.split(/[,\n]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
@@ -1419,7 +1510,7 @@ router.get('/', (req, res) => {
|
||||
actions.className = 'row';
|
||||
const del = document.createElement('button');
|
||||
del.className = 'danger-btn';
|
||||
del.textContent = 'Löschen';
|
||||
del.textContent = 'L schen';
|
||||
del.addEventListener('click', async () => {
|
||||
await deleteService(svc.id);
|
||||
});
|
||||
@@ -1477,7 +1568,7 @@ router.get('/', (req, res) => {
|
||||
if (res.ok) {
|
||||
await loadStatuspage();
|
||||
} else {
|
||||
showToast('Service löschen fehlgeschlagen', true);
|
||||
showToast('Service l schen fehlgeschlagen', true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1755,7 +1846,7 @@ router.get('/', (req, res) => {
|
||||
'</div>' +
|
||||
'<div class=\"ticket-meta\">User: ' +
|
||||
(t.userId || '-') +
|
||||
(t.claimedBy ? ' <EFBFBD> Supporter: ' + t.claimedBy : '') +
|
||||
(t.claimedBy ? ' Supporter: ' + t.claimedBy : '') +
|
||||
'</div>';
|
||||
const select = document.createElement('select');
|
||||
select.innerHTML =
|
||||
@@ -1869,14 +1960,14 @@ router.get('/', (req, res) => {
|
||||
edit.addEventListener('click', () => fillAutomationForm(r));
|
||||
const del = document.createElement('button');
|
||||
del.className = 'danger-btn';
|
||||
del.textContent = 'L<EFBFBD>schen';
|
||||
del.textContent = 'L schen';
|
||||
del.addEventListener('click', async () => {
|
||||
const res = await fetch('/api/automations/' + r.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ guildId: currentGuild })
|
||||
});
|
||||
showToast(res.ok ? 'Regel gel<EFBFBD>scht' : 'L<EFBFBD>schen fehlgeschlagen', !res.ok);
|
||||
showToast(res.ok ? 'Regel gel scht' : 'L schen fehlgeschlagen', !res.ok);
|
||||
if (res.ok) loadAutomations();
|
||||
});
|
||||
actions.appendChild(edit);
|
||||
@@ -1936,14 +2027,14 @@ router.get('/', (req, res) => {
|
||||
edit.addEventListener('click', () => fillKbForm(a));
|
||||
const del = document.createElement('button');
|
||||
del.className = 'danger-btn';
|
||||
del.textContent = 'L<EFBFBD>schen';
|
||||
del.textContent = 'L schen';
|
||||
del.addEventListener('click', async () => {
|
||||
const res = await fetch('/api/kb/' + a.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ guildId: currentGuild })
|
||||
});
|
||||
showToast(res.ok ? 'Artikel gel<EFBFBD>scht' : 'L<EFBFBD>schen fehlgeschlagen', !res.ok);
|
||||
showToast(res.ok ? 'Artikel gel scht' : 'L schen fehlgeschlagen', !res.ok);
|
||||
if (res.ok) loadKb();
|
||||
});
|
||||
actions.appendChild(edit);
|
||||
@@ -2116,7 +2207,7 @@ router.get('/', (req, res) => {
|
||||
(s.userId || '-') +
|
||||
'</strong></div><div class="muted">Ende: ' +
|
||||
formatDate(s.endedAt || Date.now()) +
|
||||
' · Dauer: ' +
|
||||
' Dauer: ' +
|
||||
dur +
|
||||
'</div>';
|
||||
supportRecentList.appendChild(div);
|
||||
@@ -2174,9 +2265,9 @@ router.get('/', (req, res) => {
|
||||
(ev.repeatType || 'none') +
|
||||
'</span></div><div class="ticket-meta">Start: ' +
|
||||
formatDate(ev.startTime) +
|
||||
' · Channel: ' +
|
||||
' Channel: ' +
|
||||
(ev.channelId || '-') +
|
||||
' · Anmeldungen: ' +
|
||||
' Anmeldungen: ' +
|
||||
(ev._count?.signups ?? 0) +
|
||||
'</div>';
|
||||
const actions = document.createElement('div');
|
||||
@@ -2389,7 +2480,7 @@ router.get('/', (req, res) => {
|
||||
meta.className = 'module-meta';
|
||||
const descParts = ['Channel: ' + (set.channelId || '-')];
|
||||
if (set.messageId) descParts.push('Message: ' + set.messageId);
|
||||
meta.innerHTML = '<div class="module-title">' + (set.title || 'Reaction Role') + '</div><div class="module-desc">' + descParts.join(' • ') + '</div>';
|
||||
meta.innerHTML = '<div class="module-title">' + (set.title || 'Reaction Role') + '</div><div class="module-desc">' + descParts.join(' - ') + '</div>';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'row';
|
||||
const editBtn = document.createElement('button');
|
||||
@@ -2445,6 +2536,262 @@ router.get('/', (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
function parseRegisterFields(raw) {
|
||||
return (raw || '')
|
||||
.split('\\n')
|
||||
.map((l, idx) => {
|
||||
const parts = l.split('|').map((p) => p.trim());
|
||||
const label = parts[0];
|
||||
if (!label) return null;
|
||||
const typeRaw = (parts[1] || 'shortText').toLowerCase();
|
||||
const type = typeRaw.includes('long') ? 'longText' : 'shortText';
|
||||
const requiredRaw = (parts[2] || '').toLowerCase();
|
||||
const required = ['true', '1', 'yes', 'ja'].includes(requiredRaw);
|
||||
return { label, type, required, order: idx };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function formatRegisterFields(fields) {
|
||||
return (fields || [])
|
||||
.slice()
|
||||
.sort((a, b) => (a.sortOrder ?? a.order ?? 0) - (b.sortOrder ?? b.order ?? 0))
|
||||
.map((f) => [f.label || 'Feld', f.type || 'shortText', f.required ? 'true' : 'false'].join(' | '))
|
||||
.join('\\n');
|
||||
}
|
||||
|
||||
function setRegisterFormDefaults() {
|
||||
if (registerFormId) registerFormId.value = '';
|
||||
if (registerFormName) registerFormName.value = '';
|
||||
if (registerFormDescription) registerFormDescription.value = '';
|
||||
if (registerFormChannel) registerFormChannel.value = registerConfigCache.reviewChannelId || '';
|
||||
if (registerFormRoles) registerFormRoles.value = (registerConfigCache.notifyRoleIds || []).join(', ');
|
||||
if (registerFormFields) registerFormFields.value = '';
|
||||
setSwitch(registerFormActive, true);
|
||||
if (registerFormStatus) registerFormStatus.textContent = '';
|
||||
}
|
||||
|
||||
function fillRegisterForm(form) {
|
||||
if (!form) return;
|
||||
if (registerFormId) registerFormId.value = form.id || '';
|
||||
if (registerFormName) registerFormName.value = form.name || '';
|
||||
if (registerFormDescription) registerFormDescription.value = form.description || '';
|
||||
if (registerFormChannel) registerFormChannel.value = form.reviewChannelId || registerConfigCache.reviewChannelId || '';
|
||||
if (registerFormRoles) registerFormRoles.value = (form.notifyRoleIds || []).join(', ');
|
||||
setSwitch(registerFormActive, form.isActive !== false);
|
||||
if (registerFormFields) registerFormFields.value = formatRegisterFields(form.fields || []);
|
||||
if (registerFormStatus) registerFormStatus.textContent = 'Bearbeitung aktiv';
|
||||
}
|
||||
|
||||
function clearRegisterUi() {
|
||||
if (registerFormList) registerFormList.innerHTML = '<div class="muted">Modul deaktiviert.</div>';
|
||||
if (registerAppsList) registerAppsList.innerHTML = '<div class="muted">Modul deaktiviert.</div>';
|
||||
if (registerAppDetail) registerAppDetail.innerHTML = '<div class="muted">Register deaktiviert.</div>';
|
||||
setRegisterFormDefaults();
|
||||
}
|
||||
|
||||
async function loadRegisterForms() {
|
||||
if (!currentGuild) return;
|
||||
if (modulesCache['registerEnabled'] === false) { clearRegisterUi(); return; }
|
||||
const res = await fetch('/api/register/forms?guildId=' + encodeURIComponent(currentGuild));
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
registerFormsCache = data.forms || [];
|
||||
renderRegisterForms();
|
||||
}
|
||||
|
||||
function renderRegisterForms() {
|
||||
if (!registerFormList) return;
|
||||
registerFormList.innerHTML = '';
|
||||
if (!registerFormsCache.length) {
|
||||
registerFormList.innerHTML = '<div class="muted">Keine Formulare.</div>';
|
||||
} else {
|
||||
registerFormsCache.forEach((form) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'module-item';
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'module-meta';
|
||||
const descParts = [];
|
||||
descParts.push('Felder: ' + ((form.fields || []).length));
|
||||
if (form.reviewChannelId) descParts.push('Review: ' + form.reviewChannelId);
|
||||
if (form.notifyRoleIds?.length) descParts.push('Notify: ' + form.notifyRoleIds.join(', '));
|
||||
meta.innerHTML = '<div class="module-title">' + (form.name || 'Formular') + '</div><div class="module-desc">' + descParts.join(' - ') + '</div>';
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'row';
|
||||
const activeBadge = document.createElement('span');
|
||||
activeBadge.className = 'badge' + (form.isActive !== false ? ' active' : '');
|
||||
activeBadge.textContent = form.isActive !== false ? 'Aktiv' : 'Inaktiv';
|
||||
actions.appendChild(activeBadge);
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.className = 'secondary-btn';
|
||||
editBtn.style.padding = '8px 10px';
|
||||
editBtn.style.fontSize = '12px';
|
||||
editBtn.textContent = 'Bearbeiten';
|
||||
editBtn.addEventListener('click', () => fillRegisterForm(form));
|
||||
const panelBtn = document.createElement('button');
|
||||
panelBtn.className = 'secondary-btn';
|
||||
panelBtn.style.padding = '8px 10px';
|
||||
panelBtn.style.fontSize = '12px';
|
||||
panelBtn.textContent = 'Panel senden';
|
||||
panelBtn.addEventListener('click', () => sendRegisterPanel(form));
|
||||
const delBtn = document.createElement('button');
|
||||
delBtn.className = 'danger-btn';
|
||||
delBtn.style.padding = '8px 10px';
|
||||
delBtn.style.fontSize = '12px';
|
||||
delBtn.textContent = 'Loeschen';
|
||||
delBtn.addEventListener('click', () => deleteRegisterForm(form.id));
|
||||
actions.appendChild(editBtn);
|
||||
actions.appendChild(panelBtn);
|
||||
actions.appendChild(delBtn);
|
||||
row.appendChild(meta);
|
||||
row.appendChild(actions);
|
||||
registerFormList.appendChild(row);
|
||||
});
|
||||
}
|
||||
populateRegisterFormFilter();
|
||||
}
|
||||
|
||||
async function sendRegisterPanel(form) {
|
||||
if (!currentGuild || !form?.id) return;
|
||||
const channelId = prompt('Channel ID fuer Panel', form.reviewChannelId || registerConfigCache.reviewChannelId || '') || '';
|
||||
if (!channelId.trim()) return;
|
||||
const message = prompt('Nachricht im Panel (optional)', 'Klicke auf Registrieren, um das Formular zu oeffnen.');
|
||||
const res = await fetch('/api/register/forms/' + form.id + '/panel', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ guildId: currentGuild, channelId, message: message || undefined })
|
||||
});
|
||||
showToast(res.ok ? 'Panel gesendet' : 'Panel fehlgeschlagen', !res.ok);
|
||||
}
|
||||
|
||||
async function deleteRegisterForm(id) {
|
||||
if (!currentGuild || !id) return;
|
||||
const res = await fetch('/api/register/forms/' + id, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ guildId: currentGuild }) });
|
||||
showToast(res.ok ? 'Formular geloescht' : 'Loeschen fehlgeschlagen', !res.ok);
|
||||
if (res.ok) await loadRegisterForms();
|
||||
}
|
||||
|
||||
function parseRegisterRoles(raw) {
|
||||
return (raw || '')
|
||||
.split(/[,\n]/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function populateRegisterFormFilter() {
|
||||
if (!registerAppsFormFilter) return;
|
||||
const current = registerAppsFormFilter.value;
|
||||
registerAppsFormFilter.innerHTML = '<option value="">Alle Formulare</option>';
|
||||
registerFormsCache.forEach((f) => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = f.id;
|
||||
opt.textContent = f.name || 'Formular';
|
||||
registerAppsFormFilter.appendChild(opt);
|
||||
});
|
||||
if (current) registerAppsFormFilter.value = current;
|
||||
}
|
||||
|
||||
async function saveRegisterForm(e) {
|
||||
if (e) e.preventDefault();
|
||||
if (!currentGuild) return;
|
||||
const fields = parseRegisterFields(registerFormFields?.value || '');
|
||||
const payload = {
|
||||
guildId: currentGuild,
|
||||
name: registerFormName?.value || 'Formular',
|
||||
description: registerFormDescription?.value || '',
|
||||
reviewChannelId: registerFormChannel?.value || undefined,
|
||||
notifyRoleIds: parseRegisterRoles(registerFormRoles?.value || ''),
|
||||
isActive: getSwitch(registerFormActive),
|
||||
fields
|
||||
};
|
||||
const id = registerFormId?.value;
|
||||
const url = id ? '/api/register/forms/' + id : '/api/register/forms';
|
||||
const method = id ? 'PUT' : 'POST';
|
||||
const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
|
||||
if (registerFormStatus) registerFormStatus.textContent = res.ok ? 'Gespeichert' : 'Fehler';
|
||||
showToast(res.ok ? 'Formular gespeichert' : 'Speichern fehlgeschlagen', !res.ok);
|
||||
if (res.ok) {
|
||||
setRegisterFormDefaults();
|
||||
await loadRegisterForms();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRegisterApps() {
|
||||
if (!currentGuild) return;
|
||||
if (modulesCache['registerEnabled'] === false) { clearRegisterUi(); return; }
|
||||
registerSelectedApp = null;
|
||||
if (registerAppDetail) registerAppDetail.innerHTML = 'Waehle einen Antrag aus der Liste.';
|
||||
const qs = new URLSearchParams({ guildId: currentGuild });
|
||||
if (registerAppsFilter?.value) qs.set('status', registerAppsFilter.value);
|
||||
if (registerAppsFormFilter?.value) qs.set('formId', registerAppsFormFilter.value);
|
||||
const res = await fetch('/api/register/apps?' + qs.toString());
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
registerAppsCache = data.applications || [];
|
||||
renderRegisterApps();
|
||||
}
|
||||
|
||||
function registerStatusClass(status) {
|
||||
const val = (status || '').toLowerCase();
|
||||
if (val === 'accepted') return 'status-open';
|
||||
if (val === 'invited') return 'status-in-progress';
|
||||
if (val === 'rejected') return 'status-closed';
|
||||
return 'status-open';
|
||||
}
|
||||
|
||||
function renderRegisterApps() {
|
||||
if (!registerAppsList) return;
|
||||
registerAppsList.innerHTML = '';
|
||||
if (!registerAppsCache.length) {
|
||||
registerSelectedApp = null;
|
||||
registerAppsList.innerHTML = '<div class="ticket-empty">Keine Antraege.</div>';
|
||||
if (registerAppDetail) registerAppDetail.innerHTML = 'Waehle einen Antrag aus der Liste.';
|
||||
return;
|
||||
}
|
||||
registerAppsCache.forEach((app) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'ticket-list-item';
|
||||
row.innerHTML = '<div class="ticket-item-top"><div class="ticket-title">' + (app.form?.name || 'Formular') + '</div><span class="ticket-status-badge ' + registerStatusClass(app.status) + '">' + (app.status || 'pending') + '</span></div>' +
|
||||
'<div class="ticket-meta">User: ' + (app.userId || '-') + ' - Erstellt: ' + formatDate(app.createdAt) + '</div>';
|
||||
row.addEventListener('click', () => loadRegisterApplication(app.id));
|
||||
registerAppsList.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRegisterApplication(id) {
|
||||
if (!id) return;
|
||||
const res = await fetch('/api/register/apps/' + id);
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
registerSelectedApp = data.application || null;
|
||||
renderRegisterDetail();
|
||||
}
|
||||
|
||||
function renderRegisterDetail() {
|
||||
if (!registerAppDetail) return;
|
||||
if (!registerSelectedApp) {
|
||||
registerAppDetail.innerHTML = '<div class="muted">Waehle einen Antrag.</div>';
|
||||
return;
|
||||
}
|
||||
const app = registerSelectedApp;
|
||||
const formFields = registerFormsCache.find((f) => f.id === app.formId)?.fields || [];
|
||||
const answers = app.answers || [];
|
||||
const answersHtml = answers
|
||||
.map((a) => {
|
||||
const field = formFields.find((f) => f.id === a.fieldId);
|
||||
const label = field?.label || 'Feld';
|
||||
const value = (a.value || '').replace(/</g, '<');
|
||||
return '<div class="register-answer"><div class="form-label">' + label + '</div><div class="module-desc">' + value + '</div></div>';
|
||||
})
|
||||
.join('');
|
||||
registerAppDetail.innerHTML =
|
||||
'<div class="row" style="justify-content:space-between; align-items:center; gap:10px; flex-wrap:wrap;">' +
|
||||
'<div><p class="section-title">' + (app.form?.name || 'Formular') + '</p><p class="section-sub">User: ' + (app.userId || '-') + '</p></div>' +
|
||||
'<span class="ticket-status-badge ' + registerStatusClass(app.status) + '">' + (app.status || 'pending') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="module-list" style="margin-top:12px;">' + (answersHtml || '<div class="muted">Keine Antworten vorhanden.</div>') + '</div>';
|
||||
}
|
||||
|
||||
// TODO: MODULE: Liste um Musik/Forum/Automod-Konfiguration ergaenzen.
|
||||
// - Module-Status inkl. Direktlinks zu Detailseiten (Automod/Welcome/Musik) rendern.
|
||||
// - Module-Flags aus BotModuleService spiegeln statt doppeltem Fetch.
|
||||
@@ -2460,6 +2807,7 @@ router.get('/', (req, res) => {
|
||||
let birthdayActive = false;
|
||||
let reactionRolesActive = false;
|
||||
let eventsActive = false;
|
||||
let registerActive = false;
|
||||
(data.modules || []).forEach((m) => {
|
||||
modulesCache[m.key] = !!m.enabled;
|
||||
const row = document.createElement('div');
|
||||
@@ -2484,7 +2832,10 @@ router.get('/', (req, res) => {
|
||||
if (m.key === 'birthdayEnabled') modulesCache['birthdayEnabled'] = willEnable;
|
||||
if (m.key === 'reactionRolesEnabled') modulesCache['reactionRolesEnabled'] = willEnable;
|
||||
if (m.key === 'eventsEnabled') modulesCache['eventsEnabled'] = willEnable;
|
||||
if (m.key === 'registerEnabled') modulesCache['registerEnabled'] = willEnable;
|
||||
applyNavVisibility();
|
||||
if (m.key === 'registerEnabled' && willEnable) { await loadRegisterForms(); await loadRegisterApps(); }
|
||||
if (m.key === 'registerEnabled' && !willEnable) { clearRegisterUi(); }
|
||||
} else {
|
||||
showToast('Speichern fehlgeschlagen', true);
|
||||
}
|
||||
@@ -2500,6 +2851,7 @@ router.get('/', (req, res) => {
|
||||
if (m.key === 'birthdayEnabled') birthdayActive = !!m.enabled;
|
||||
if (m.key === 'reactionRolesEnabled') reactionRolesActive = !!m.enabled;
|
||||
if (m.key === 'eventsEnabled') eventsActive = !!m.enabled;
|
||||
if (m.key === 'registerEnabled') registerActive = !!m.enabled;
|
||||
});
|
||||
applyNavVisibility();
|
||||
applyTicketsVisibility(ticketsActive);
|
||||
@@ -2507,12 +2859,13 @@ router.get('/', (req, res) => {
|
||||
if (birthdayActive) loadBirthday();
|
||||
if (reactionRolesActive) loadReactionRoles();
|
||||
if (eventsActive) loadEvents();
|
||||
if (registerActive) { loadRegisterForms(); loadRegisterApps(); }
|
||||
}
|
||||
|
||||
async function saveModuleToggle(key, enabled) {
|
||||
if (!currentGuild) return false;
|
||||
const payload = { guildId: currentGuild };
|
||||
['ticketsEnabled', 'automodEnabled', 'welcomeEnabled', 'musicEnabled', 'levelingEnabled', 'statuspageEnabled', 'birthdayEnabled', 'reactionRolesEnabled', 'eventsEnabled'].forEach((k) => {
|
||||
['ticketsEnabled', 'automodEnabled', 'welcomeEnabled', 'musicEnabled', 'levelingEnabled', 'statuspageEnabled', 'birthdayEnabled', 'reactionRolesEnabled', 'eventsEnabled', 'registerEnabled'].forEach((k) => {
|
||||
if (modulesCache[k] !== undefined) payload[k] = modulesCache[k];
|
||||
});
|
||||
payload['dynamicVoiceEnabled'] = modulesCache['dynamicVoiceEnabled'];
|
||||
@@ -2623,6 +2976,26 @@ router.get('/', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
const registerTabs = Array.from(document.querySelectorAll('.register-tab'));
|
||||
document.querySelectorAll('.register-tab-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', async () => {
|
||||
document.querySelectorAll('.register-tab-btn').forEach((b) => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const tab = btn.dataset.tab || 'forms';
|
||||
registerTabs.forEach((t) => t.classList.toggle('active', t.dataset.tab === tab));
|
||||
if (tab === 'forms') await loadRegisterForms();
|
||||
if (tab === 'apps') await loadRegisterApps();
|
||||
});
|
||||
});
|
||||
|
||||
if (registerFormForm) registerFormForm.addEventListener('submit', saveRegisterForm);
|
||||
if (registerFormReset) registerFormReset.addEventListener('click', (e) => { e.preventDefault(); setRegisterFormDefaults(); });
|
||||
if (registerFormNew) registerFormNew.addEventListener('click', () => setRegisterFormDefaults());
|
||||
if (registerFormsReload) registerFormsReload.addEventListener('click', loadRegisterForms);
|
||||
if (registerAppsReload) registerAppsReload.addEventListener('click', loadRegisterApps);
|
||||
if (registerAppsFilter) registerAppsFilter.addEventListener('change', loadRegisterApps);
|
||||
if (registerAppsFormFilter) registerAppsFormFilter.addEventListener('change', loadRegisterApps);
|
||||
|
||||
const slaRange = document.getElementById('slaRange');
|
||||
if (slaRange) slaRange.addEventListener('change', loadSla);
|
||||
|
||||
@@ -2706,7 +3079,7 @@ router.get('/', (req, res) => {
|
||||
|
||||
document.getElementById('logoutBtn').addEventListener('click', () => window.location.href = BASE_AUTH + '/logout');
|
||||
|
||||
[automodToggle, badWordToggle, linkFilterToggle, spamFilterToggle, capsFilterToggle, logJoinLeave, logMsgEdit, logMsgDelete, logAutomod, logTickets, logMusic, dynamicVoiceToggle, supportLoginAuto].forEach((el) => {
|
||||
[automodToggle, badWordToggle, linkFilterToggle, spamFilterToggle, capsFilterToggle, logJoinLeave, logMsgEdit, logMsgDelete, logAutomod, logTickets, logMusic, dynamicVoiceToggle, supportLoginAuto, registerFormActive].forEach((el) => {
|
||||
if (el) el.addEventListener('click', () => el.classList.toggle('on'));
|
||||
});
|
||||
if (logSystem) logSystem.addEventListener('click', () => logSystem.classList.toggle('on'));
|
||||
@@ -2883,3 +3256,24 @@ router.get('/settings', (_req, res) => {
|
||||
export default router;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
tmp_locate.log
Normal file
0
tmp_locate.log
Normal file
Reference in New Issue
Block a user