Server-Monitor v3, Composer v2, Temp-Voice-Panel und Embed-Polish
- Server-Monitor v3: gamedig-Integration (300+ Spiele), GSM-Stil-Embeds (Spieler-Balken mit %, Map, Spieler-Liste, klickbarer Connect-Link, Ping im Footer), Down/Up-Alerts (2 Fails → 🚨, Recovery → ✅ mit Downtime) in konfigurierbaren Alert-Kanal; Server-Tab mit 19 Spiel-Presets, Host/Port bzw. Query-URL und Connect-URL - Composer v2: visueller Embed-Builder mit Live-Discord-Preview (Autor, Felder, Bilder, Farbe, Footer, Timestamp), speicherbare Vorlagen (/api/templates), /api/compose versteht volle Embeds - Temp-Voice: Control-Panel im erstellten Kanal (Umbenennen, Sperren, Limit, Löschen) — nur der Kanal-Besitzer darf bedienen - Embed-Polish: brandEmbed-Factory (src/embeds.js), Footer überall mit Bot-Avatar-Icon Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+65
-13
@@ -12,8 +12,10 @@ import {
|
||||
saveTrigger, listTriggers, deleteTrigger, activityRange,
|
||||
createGameserver, updateGameserver, listGameservers, getGameserver, deleteGameserver,
|
||||
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
|
||||
saveTemplate, listTemplates, deleteTemplate,
|
||||
} from '../db.js';
|
||||
import { publishAppForm, unpublishAppForm, MAX_QUESTIONS } from '../bot/app-forms.js';
|
||||
import { sanitizeEmbed } from './api-v1.js';
|
||||
import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-menus.js';
|
||||
import { computeNextRun } from './scheduled-posts.js';
|
||||
import { config } from '../config.js';
|
||||
@@ -299,6 +301,7 @@ ${rssItems}
|
||||
twitch_channel: getSetting('twitch_channel') ?? '',
|
||||
twitch_creds_set: Boolean(getSetting('twitch_client_id') && getSetting('twitch_client_secret')),
|
||||
tempvoice_channel_id: getSetting('tempvoice_channel_id') ?? '',
|
||||
server_alert_channel_id: getSetting('server_alert_channel_id') ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -344,7 +347,7 @@ ${rssItems}
|
||||
'release_channel_id', 'backup_channel_id', 'starboard_channel_id',
|
||||
'screenshot_channel_id', 'modmail_channel_id', 'welcome_channel_id',
|
||||
'modlog_channel_id', 'status_channel_id', 'voting_channel_id', 'ticket_channel_id',
|
||||
'events_announce_channel_id', 'social_announce_channel_id',
|
||||
'events_announce_channel_id', 'social_announce_channel_id', 'server_alert_channel_id',
|
||||
];
|
||||
for (const key of ['commit_channel_id', 'devlog_channel_id', ...OPTIONAL_CHANNELS]) {
|
||||
if (body[key] === undefined) continue;
|
||||
@@ -738,10 +741,23 @@ ${rssItems}
|
||||
function parseServerBody(request, reply) {
|
||||
const body = request.body ?? {};
|
||||
const name = String(body.name ?? '').trim();
|
||||
const type = String(body.type ?? 'fivem').trim().toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const queryUrl = String(body.query_url ?? '').trim().replace(/\/$/, '');
|
||||
const type = ['fivem', 'http'].includes(body.type) ? body.type : 'fivem';
|
||||
if (!name || !/^https?:\/\/.+/.test(queryUrl)) {
|
||||
reply.code(400).send({ error: 'name und query_url (http/https) nötig' });
|
||||
const host = String(body.host ?? '').trim();
|
||||
const port = Number(body.port) || null;
|
||||
|
||||
if (!name || !type) {
|
||||
reply.code(400).send({ error: 'name und type nötig' });
|
||||
return null;
|
||||
}
|
||||
if (['fivem', 'http'].includes(type)) {
|
||||
if (!/^https?:\/\/.+/.test(queryUrl)) {
|
||||
reply.code(400).send({ error: 'query_url (http/https) nötig' });
|
||||
return null;
|
||||
}
|
||||
} else if (!host) {
|
||||
// gamedig-Typ: Host (+ optionaler Port) statt URL
|
||||
reply.code(400).send({ error: 'host nötig (gamedig-Query)' });
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
@@ -749,6 +765,9 @@ ${rssItems}
|
||||
type,
|
||||
query_url: queryUrl,
|
||||
address: String(body.address ?? '').trim().slice(0, 120),
|
||||
host: host.slice(0, 120),
|
||||
port,
|
||||
connect_url: String(body.connect_url ?? '').trim().slice(0, 200),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -885,19 +904,23 @@ ${rssItems}
|
||||
app.post('/api/compose', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'content')) return;
|
||||
|
||||
const { channel_id: channelId, message_id: messageId, content, embed_title: embedTitle } = request.body ?? {};
|
||||
const { channel_id: channelId, message_id: messageId, content, embed_title: embedTitle, embed } = request.body ?? {};
|
||||
const text = String(content ?? '').trim();
|
||||
if (!channelId || !text) {
|
||||
return reply.code(400).send({ error: 'channel_id und content nötig' });
|
||||
const cleanEmbed = embed ? sanitizeEmbed(embed) : null;
|
||||
if (!channelId || (!text && !cleanEmbed)) {
|
||||
return reply.code(400).send({ error: 'channel_id und content oder embed nötig' });
|
||||
}
|
||||
const channel = await client.channels.fetch(String(channelId)).catch(() => null);
|
||||
if (!channel?.isTextBased()) {
|
||||
return reply.code(404).send({ error: 'Kanal nicht gefunden' });
|
||||
}
|
||||
|
||||
// Mit Embed-Titel → gebrandetes Embed, sonst Plaintext (Discord-Markdown)
|
||||
const payload = embedTitle?.trim()
|
||||
? {
|
||||
// Drei Modi: volles Embed (Composer v2) > Embed-Titel-Kurzform > Plaintext
|
||||
let payload;
|
||||
if (cleanEmbed) {
|
||||
payload = { content: text ? text.slice(0, 2000) : null, embeds: [cleanEmbed] };
|
||||
} else if (embedTitle?.trim()) {
|
||||
payload = {
|
||||
content: null,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
@@ -907,8 +930,10 @@ ${rssItems}
|
||||
.setFooter({ text: brandName() })
|
||||
.setTimestamp(),
|
||||
],
|
||||
}
|
||||
: { content: text.slice(0, 2000), embeds: [] };
|
||||
};
|
||||
} else {
|
||||
payload = { content: text.slice(0, 2000), embeds: [] };
|
||||
}
|
||||
|
||||
if (messageId) {
|
||||
const message = await channel.messages.fetch(String(messageId)).catch(() => null);
|
||||
@@ -954,6 +979,33 @@ ${rssItems}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Composer-Vorlagen (Scope: content) ---
|
||||
|
||||
app.get('/api/templates', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'content')) return;
|
||||
return { templates: listTemplates() };
|
||||
});
|
||||
|
||||
app.put('/api/templates/:name', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'content')) return;
|
||||
const name = String(request.params.name).trim().slice(0, 60);
|
||||
if (!name) return reply.code(400).send({ error: 'name nötig' });
|
||||
const payload = {
|
||||
content: String(request.body?.content ?? '').slice(0, 2000),
|
||||
embed: request.body?.embed ? sanitizeEmbed(request.body.embed) : null,
|
||||
};
|
||||
if (!payload.content && !payload.embed) {
|
||||
return reply.code(400).send({ error: 'Vorlage ist leer' });
|
||||
}
|
||||
saveTemplate(name, payload);
|
||||
return { ok: true, templates: listTemplates() };
|
||||
});
|
||||
|
||||
app.delete('/api/templates/:name', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'content')) return;
|
||||
return { deleted: deleteTemplate(String(request.params.name)), templates: listTemplates() };
|
||||
});
|
||||
|
||||
// --- Team-Verwaltung (nur Owner) ---
|
||||
|
||||
const TEAM_SCOPES = ['content', 'community', 'rollen', 'bewerbungen', 'server', 'settings', 'devlogs'];
|
||||
@@ -1068,7 +1120,7 @@ ${rssItems}
|
||||
.setDescription(
|
||||
`Test-Nachricht für den **${{ commit: 'Commit', devlog: 'Devlog', release: 'Release' }[target]}-Kanal** — von der Settings-Seite ausgelöst.`
|
||||
)
|
||||
.setFooter({ text: brandFooter('SETUP') }),
|
||||
.setFooter({ text: brandFooter('SETUP'), iconURL: client?.user?.displayAvatarURL?.({ size: 64 }) }),
|
||||
],
|
||||
});
|
||||
return { ok: true };
|
||||
|
||||
Reference in New Issue
Block a user