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:
@@ -40,11 +40,11 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St
|
||||
| 💬 **Triggers** | Auto-Antworten auf Schlüsselwörter (30s-Cooldown), verwaltet im Composer-Tab |
|
||||
| ⏰ **/remind** | Erinnerungen per DM (`/remind dauer:2h text:…`) |
|
||||
| 📣 **Twitch/YouTube** | 🔴-Live- und ▶️-Neues-Video-Announcements (YouTube ohne Key via RSS, Twitch mit App-Credentials) |
|
||||
| 🔊 **Temp-Voice** | „Join to Create": Hub-Kanal betreten → eigener Voice-Kanal, löscht sich wenn leer |
|
||||
| 🔊 **Temp-Voice** | „Join to Create": Hub-Kanal betreten → eigener Voice-Kanal mit Control-Panel (Umbenennen, Sperren, Limit, Löschen — nur für den Besitzer), löscht sich wenn leer |
|
||||
| 📊 **Server-Stats** | Nachrichten/Joins/Leaves pro Tag → Aktivitäts-Chart auf der Level-Seite |
|
||||
| 👋 **Willkommens-Embed** | Begrüßung neuer Member im Brand-Look (braucht Server-Members-Intent) |
|
||||
| 📋 **Mod-Log** | Gelöschte/bearbeitete Nachrichten in einen privaten Log-Kanal |
|
||||
| 🎮 **Server-Monitor** | DiscordGSM-Stil: eigener Setup-Tab, pro Server ein Live-Embed (🟢/🔴, Spieler-Balken, Map, Copy-Paste-Adresse); Typen FiveM + HTTP-Check; Spielerzahl in der Presence |
|
||||
| 🎮 **Server-Monitor** | DiscordGSM-Stil: eigener Setup-Tab, pro Server ein Live-Embed (🟢/🔴, Spieler-Balken mit %, Map, Spieler-Liste, klickbarer Connect-Link, Ping); **300+ Spiele via gamedig** (Minecraft, Rust, CS2, Valheim, ARK …) + FiveM + HTTP-Check; Down/Up-Alerts (🚨 nach 2 Fehlversuchen, ✅ mit Downtime bei Recovery) in eigenen Alert-Kanal; Spielerzahl in der Presence |
|
||||
| 👥 **Team-Rechte** | Team-Mitglieder bekommen gezielten Web-Zugriff (Composer, Bewerbungen, Rollen, Server, …) — Brand, System, API-Keys bleiben Owner-only |
|
||||
| 💡 **Feature-Voting** | `/wunsch` → Voting-Post mit 👍; Top-Wünsche öffentlich auf der Roadmap-Seite |
|
||||
| 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung nach Ablauf |
|
||||
@@ -82,7 +82,10 @@ SQLite, Env-Variablen sind nur Fallback, kein Redeploy nötig:
|
||||
- **Community:** Playtester-Rolle (+ Liste), Starboard-Kanal + ⭐-Schwellwert,
|
||||
Screenshot-Kanal, Voting-Kanal
|
||||
- **Moderation & Kontakt:** Modmail-, Willkommens-, Mod-Log-Kanal
|
||||
- **Game-Server:** Status-Kanal + Server-Liste (`Name=URL`, FiveM-kompatibel)
|
||||
- **Game-Server:** eigener Tab — Spiel-Auswahl (19 Presets + freie gamedig-ID), Host/Port
|
||||
oder Query-URL, Connect-URL, Status- & Alert-Kanal
|
||||
- **Composer:** visueller **Embed-Builder mit Live-Discord-Preview** (Autor, Felder,
|
||||
Bilder, Farbe, Footer), Nachrichten senden & nachträglich bearbeiten, speicherbare Vorlagen
|
||||
- **Bug-Reports / Roadmap:** Ziel-Repos
|
||||
- **Backups:** an/aus, Upload-Kanal, „Backup jetzt"
|
||||
- **Watchdog:** überwachte URLs
|
||||
@@ -258,6 +261,7 @@ urllib.request.urlopen(req)
|
||||
| `POST/GET /api/v1/*` | API-Key (Bearer) | Externe Skripte — siehe „API v1" oben |
|
||||
| `GET/POST/DELETE /api/apikeys` | Admin | API-Key-Verwaltung |
|
||||
| `GET/POST/PUT/DELETE /api/rolemenus`, `POST /api/rolemenus/:id/publish` | Admin | Rollen-Menüs |
|
||||
| `POST /api/compose` · `GET/PUT/DELETE /api/templates` | Team (content) | Embed-Composer senden/bearbeiten · Vorlagen |
|
||||
| `GET/PUT /api/settings`, `POST /api/settings/test/:target` | Admin | Setup-Seite |
|
||||
| `GET /api/commits?page=` · `DELETE /api/devlogs/:id` | Admin | Commit-Archiv · Devlog löschen |
|
||||
| `GET /auth/login` · `/auth/callback` · `/auth/logout` | — | Discord-OAuth2 |
|
||||
@@ -278,6 +282,7 @@ d4rkbot/
|
||||
│ ├── runtime-settings.js # Effektive Settings (DB vor Env)
|
||||
│ ├── gitea-api.js # Gitea-REST (Issues + Assets für /bug)
|
||||
│ ├── backup.js # Nächtliche DB-Backups (gzip, Rotation, Upload)
|
||||
│ ├── embeds.js # brandEmbed-Factory (Farbe, Footer, Bot-Icon)
|
||||
│ ├── bot/
|
||||
│ │ ├── client.js # Discord-Client, Commands, Listener, Button-Handler
|
||||
│ │ ├── commit-feed.js # Push → Embed
|
||||
@@ -285,7 +290,7 @@ d4rkbot/
|
||||
│ │ ├── devlog-archive.js # Nachricht → SQLite (+ Bild-Download)
|
||||
│ │ ├── community.js # Starboard + Screenshot-Galerie
|
||||
│ │ ├── mod-tools.js # Modmail, Willkommens-Embed, Mod-Log
|
||||
│ │ ├── server-monitor.js # Game-Server-Status (FiveM-kompatibel)
|
||||
│ │ ├── server-monitor.js # Game-Server-Status (gamedig 300+ Spiele, Alerts)
|
||||
│ │ ├── giveaways.js # Giveaway-Ziehung (Scheduler)
|
||||
│ │ ├── weekly-recap.js # Sonntags-Zusammenfassung + Scheduler
|
||||
│ │ ├── watchdog.js # URL-Checks + DM-Alarm
|
||||
@@ -300,6 +305,7 @@ d4rkbot/
|
||||
│ └── src/
|
||||
│ ├── App.jsx # Layout, Router, Nav, Login-Status
|
||||
│ ├── markdown.jsx # Mini-Markdown (fett/kursiv/Code/Links/Listen/---/##)
|
||||
│ ├── components/ # EmbedComposer (Builder + Discord-Preview)
|
||||
│ └── pages/ # Devlogs, Roadmap, Galerie, Changelog, Commits, Settings
|
||||
├── tools/test-webhook.mjs # Lokaler Commit-Feed-Test
|
||||
├── .env.example # Vorlage für alle Env-Variablen
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
// Composer v2 (Vorbild: message.style / embed-generator): voller Embed-Builder
|
||||
// mit Live-Vorschau im Discord-Look + speicherbaren Vorlagen.
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet, apiPost, apiPut, apiDelete } from '../api.js';
|
||||
import { Markdown } from '../markdown.jsx';
|
||||
|
||||
const EMPTY_EMBED = {
|
||||
author_name: '', author_icon: '', title: '', url: '', description: '',
|
||||
color: '#f5c518', thumbnail: '', image: '', footer: '', timestamp: true,
|
||||
fields: [],
|
||||
};
|
||||
|
||||
/** Builder-State → Discord-Embed-Objekt (nur gesetzte Felder) */
|
||||
function toApiEmbed(e) {
|
||||
const out = {};
|
||||
if (e.author_name.trim()) {
|
||||
out.author = { name: e.author_name.trim() };
|
||||
if (e.author_icon.trim()) out.author.icon_url = e.author_icon.trim();
|
||||
}
|
||||
if (e.title.trim()) out.title = e.title.trim();
|
||||
if (e.url.trim()) out.url = e.url.trim();
|
||||
if (e.description.trim()) out.description = e.description;
|
||||
out.color = parseInt(e.color.replace('#', ''), 16) || 0xf5c518;
|
||||
if (e.thumbnail.trim()) out.thumbnail = { url: e.thumbnail.trim() };
|
||||
if (e.image.trim()) out.image = { url: e.image.trim() };
|
||||
if (e.footer.trim()) out.footer = { text: e.footer.trim() };
|
||||
if (e.timestamp) out.timestamp = new Date().toISOString();
|
||||
const fields = e.fields.filter((f) => f.name.trim() && f.value.trim());
|
||||
if (fields.length) out.fields = fields;
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Discord-Embed-Objekt (aus Vorlage) → Builder-State */
|
||||
function fromApiEmbed(embed) {
|
||||
if (!embed) return { ...EMPTY_EMBED, fields: [] };
|
||||
return {
|
||||
author_name: embed.author?.name ?? '',
|
||||
author_icon: embed.author?.icon_url ?? '',
|
||||
title: embed.title ?? '',
|
||||
url: embed.url ?? '',
|
||||
description: embed.description ?? '',
|
||||
color: `#${Number(embed.color ?? 0xf5c518).toString(16).padStart(6, '0')}`,
|
||||
thumbnail: embed.thumbnail?.url ?? '',
|
||||
image: embed.image?.url ?? '',
|
||||
footer: embed.footer?.text ?? '',
|
||||
timestamp: Boolean(embed.timestamp),
|
||||
fields: (embed.fields ?? []).map((f) => ({ name: f.name, value: f.value, inline: Boolean(f.inline) })),
|
||||
};
|
||||
}
|
||||
|
||||
/** Live-Vorschau im Discord-Look */
|
||||
function DiscordPreview({ content, embed, useEmbed }) {
|
||||
const hasEmbed = useEmbed && (embed.title || embed.description || embed.author_name || embed.fields.some((f) => f.name));
|
||||
return (
|
||||
<div className="dpv">
|
||||
<div className="dpv-msg">
|
||||
<div className="dpv-avatar">🤖</div>
|
||||
<div className="dpv-body">
|
||||
<div className="dpv-name">d4rkbot <span className="dpv-tag">APP</span> <span className="dpv-time">heute um {new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} Uhr</span></div>
|
||||
{content.trim() && <div className="dpv-content"><Markdown text={content} /></div>}
|
||||
{hasEmbed && (
|
||||
<div className="dpv-embed" style={{ borderLeftColor: embed.color }}>
|
||||
<div className="dpv-embed-main">
|
||||
{embed.author_name && (
|
||||
<div className="dpv-author">
|
||||
{embed.author_icon && <img src={embed.author_icon} alt="" />}
|
||||
{embed.author_name}
|
||||
</div>
|
||||
)}
|
||||
{embed.title && <div className="dpv-title">{embed.title}</div>}
|
||||
{embed.description && <div className="dpv-desc"><Markdown text={embed.description} /></div>}
|
||||
{embed.fields.filter((f) => f.name && f.value).length > 0 && (
|
||||
<div className="dpv-fields">
|
||||
{embed.fields.filter((f) => f.name && f.value).map((f, i) => (
|
||||
<div className={`dpv-field ${f.inline ? 'inline' : ''}`} key={i}>
|
||||
<div className="dpv-field-name">{f.name}</div>
|
||||
<div className="dpv-field-value"><Markdown text={f.value} /></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{embed.image && <img className="dpv-image" src={embed.image} alt="" />}
|
||||
{(embed.footer || embed.timestamp) && (
|
||||
<div className="dpv-footer">
|
||||
{embed.footer}{embed.footer && embed.timestamp ? ' • ' : ''}{embed.timestamp ? 'heute' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{embed.thumbnail && <img className="dpv-thumb" src={embed.thumbnail} alt="" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function EmbedComposer({ channelOptions, flash }) {
|
||||
const [channel, setChannel] = useState('');
|
||||
const [messageId, setMessageId] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
const [useEmbed, setUseEmbed] = useState(true);
|
||||
const [embed, setEmbed] = useState({ ...EMPTY_EMBED, fields: [] });
|
||||
const [templates, setTemplates] = useState([]);
|
||||
const [templateName, setTemplateName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
apiGet('/api/templates').then((d) => setTemplates(d.templates)).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const setE = (patch) => setEmbed({ ...embed, ...patch });
|
||||
const setField = (i, patch) =>
|
||||
setE({ fields: embed.fields.map((f, j) => (j === i ? { ...f, ...patch } : f)) });
|
||||
|
||||
async function send() {
|
||||
if (!channel) return flash('✗ Kanal wählen');
|
||||
const apiEmbed = useEmbed ? toApiEmbed(embed) : undefined;
|
||||
if (!content.trim() && !useEmbed) return flash('✗ Text oder Embed nötig');
|
||||
try {
|
||||
const res = await apiPost('/api/compose', {
|
||||
channel_id: channel,
|
||||
message_id: messageId.trim() || undefined,
|
||||
content,
|
||||
embed: apiEmbed,
|
||||
});
|
||||
flash(res.edited ? '✓ Aktualisiert' : `✓ Gesendet (ID ${res.message_id})`);
|
||||
if (!res.edited) setMessageId(res.message_id);
|
||||
} catch {
|
||||
flash('✗ Senden fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (!templateName.trim()) return flash('✗ Vorlagen-Name nötig');
|
||||
try {
|
||||
const res = await apiPut(`/api/templates/${encodeURIComponent(templateName.trim())}`, {
|
||||
content,
|
||||
embed: useEmbed ? toApiEmbed(embed) : null,
|
||||
});
|
||||
setTemplates(res.templates);
|
||||
flash('✓ Vorlage gespeichert');
|
||||
} catch {
|
||||
flash('✗ Vorlage fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
function loadTemplate(name) {
|
||||
const t = templates.find((x) => x.name === name);
|
||||
if (!t) return;
|
||||
setContent(t.payload.content ?? '');
|
||||
setUseEmbed(Boolean(t.payload.embed));
|
||||
setEmbed(fromApiEmbed(t.payload.embed));
|
||||
setTemplateName(name);
|
||||
}
|
||||
|
||||
async function removeTemplate() {
|
||||
if (!templateName.trim() || !window.confirm(`Vorlage „${templateName}" löschen?`)) return;
|
||||
const res = await apiDelete(`/api/templates/${encodeURIComponent(templateName.trim())}`).catch(() => null);
|
||||
if (res) { setTemplates(res.templates); setTemplateName(''); flash('✓ Vorlage gelöscht'); }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Post verfassen</h2>
|
||||
|
||||
<div className="field">
|
||||
<div className="field-row">
|
||||
<select value={channel} onChange={(e) => setChannel(e.target.value)}>
|
||||
<option value="" disabled>— Kanal wählen —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
<input type="text" style={{ flex: 'none', width: '13rem' }} placeholder="Message-ID (= bearbeiten)" value={messageId} onChange={(e) => setMessageId(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="field-row">
|
||||
<select style={{ flex: 1 }} value={templateName} onChange={(e) => loadTemplate(e.target.value)}>
|
||||
<option value="">— Vorlage laden —</option>
|
||||
{templates.map((t) => <option key={t.name} value={t.name}>{t.name}</option>)}
|
||||
</select>
|
||||
<input type="text" style={{ flex: 1 }} placeholder="Als Vorlage speichern unter…" value={templateName} onChange={(e) => setTemplateName(e.target.value)} />
|
||||
<button className="btn" onClick={saveTemplate}>💾</button>
|
||||
<button className="card-delete" title="Vorlage löschen" onClick={removeTemplate}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="composer-grid">
|
||||
<div className="composer-form">
|
||||
<div className="field">
|
||||
<label>Text über dem Embed <span className="field-hint">Discord-Markdown</span></label>
|
||||
<textarea rows={3} value={content} onChange={(e) => setContent(e.target.value)} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="toggle">
|
||||
<input type="checkbox" checked={useEmbed} onChange={(e) => setUseEmbed(e.target.checked)} />
|
||||
<span>Embed anhängen</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{useEmbed && (
|
||||
<>
|
||||
<div className="field">
|
||||
<div className="field-row">
|
||||
<input type="color" className="color-pick" title="Embed-Farbe" value={embed.color} onChange={(e) => setE({ color: e.target.value })} />
|
||||
<input type="text" placeholder="Author-Name" value={embed.author_name} onChange={(e) => setE({ author_name: e.target.value })} />
|
||||
<input type="text" placeholder="Author-Icon-URL" value={embed.author_icon} onChange={(e) => setE({ author_icon: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="field-row">
|
||||
<input type="text" placeholder="Titel" value={embed.title} onChange={(e) => setE({ title: e.target.value })} />
|
||||
<input type="text" placeholder="Titel-URL" value={embed.url} onChange={(e) => setE({ url: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<textarea rows={5} placeholder="Beschreibung (Markdown: **fett**, Listen, ## Überschriften …)" value={embed.description} onChange={(e) => setE({ description: e.target.value })} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="field-row">
|
||||
<input type="text" placeholder="Thumbnail-URL (klein, rechts)" value={embed.thumbnail} onChange={(e) => setE({ thumbnail: e.target.value })} />
|
||||
<input type="text" placeholder="Bild-URL (groß, unten)" value={embed.image} onChange={(e) => setE({ image: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Felder</label>
|
||||
{embed.fields.map((f, i) => (
|
||||
<div className="rr-row" key={i}>
|
||||
<input type="text" style={{ flex: 'none', width: '11rem' }} placeholder="Feld-Name" value={f.name} onChange={(e) => setField(i, { name: e.target.value })} />
|
||||
<input type="text" placeholder="Feld-Wert" value={f.value} onChange={(e) => setField(i, { value: e.target.value })} />
|
||||
<label className="scope-pick" title="Nebeneinander">
|
||||
<input type="checkbox" checked={f.inline} onChange={(e) => setField(i, { inline: e.target.checked })} />
|
||||
inline
|
||||
</label>
|
||||
<button className="card-delete" onClick={() => setE({ fields: embed.fields.filter((_, j) => j !== i) })}>✕</button>
|
||||
</div>
|
||||
))}
|
||||
<button className="btn btn-mini" style={{ marginTop: '.4rem' }} disabled={embed.fields.length >= 25}
|
||||
onClick={() => setE({ fields: [...embed.fields, { name: '', value: '', inline: true }] })}>+ Feld</button>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="field-row">
|
||||
<input type="text" placeholder="Footer-Text" value={embed.footer} onChange={(e) => setE({ footer: e.target.value })} />
|
||||
<label className="scope-pick"><input type="checkbox" checked={embed.timestamp} onChange={(e) => setE({ timestamp: e.target.checked })} /> Zeitstempel</label>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="settings-actions">
|
||||
<button className="btn btn-save" onClick={send}>{messageId.trim() ? 'Aktualisieren' : 'Senden'}</button>
|
||||
{messageId.trim() && <button className="btn btn-ghost" onClick={() => setMessageId('')}>Neu statt bearbeiten</button>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="composer-preview">
|
||||
<label>Vorschau</label>
|
||||
<DiscordPreview content={content} embed={embed} useEmbed={useEmbed} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { apiGet, apiPut, apiPost, apiDelete } from '../api.js';
|
||||
import EmbedComposer from '../components/EmbedComposer.jsx';
|
||||
|
||||
const API_SCOPES = [
|
||||
{ id: 'message', label: 'message', hint: 'Nachrichten/Embeds in Kanäle posten' },
|
||||
@@ -77,8 +78,6 @@ export default function Settings({ me }) {
|
||||
};
|
||||
const [menus, setMenus] = useState([]);
|
||||
const [menuDraft, setMenuDraft] = useState(emptyMenu);
|
||||
// Composer
|
||||
const [composer, setComposer] = useState({ channel: '', title: '', text: '', messageId: '' });
|
||||
// Branding
|
||||
const [giteaToken, setGiteaToken] = useState('');
|
||||
const [botName, setBotName] = useState('');
|
||||
@@ -97,7 +96,17 @@ export default function Settings({ me }) {
|
||||
const [triggers, setTriggers] = useState([]);
|
||||
const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' });
|
||||
// Game-Server
|
||||
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '' };
|
||||
const GAME_PRESETS = [
|
||||
['fivem', '🚗 FiveM'], ['http', '🌐 HTTP-Check'], ['minecraft', '⛏️ Minecraft (Java)'],
|
||||
['minecraftbe', '⛏️ Minecraft (Bedrock)'], ['rust', '🪓 Rust'], ['valheim', '🛡️ Valheim'],
|
||||
['palworld', '🐑 Palworld'], ['arkse', '🦖 ARK: Survival Evolved'], ['dayz', '🧟 DayZ'],
|
||||
['projectzomboid', '🧟 Project Zomboid'], ['sevendaystodie', '🧟 7 Days to Die'],
|
||||
['teamfortress2', '🎩 Team Fortress 2'], ['counterstrike2', '🔫 Counter-Strike 2'],
|
||||
['garrysmod', '🔧 Garry’s Mod'], ['satisfactory', '🏭 Satisfactory'],
|
||||
['terraria', '🌳 Terraria'], ['arma3', '🪖 Arma 3'], ['squad', '🪖 Squad'],
|
||||
['unturned', '🏝️ Unturned'],
|
||||
];
|
||||
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '' };
|
||||
const [servers, setServers] = useState([]);
|
||||
const [serverDraft, setServerDraft] = useState(emptyServer);
|
||||
// Bot-Beschreibung + Team
|
||||
@@ -177,9 +186,10 @@ export default function Settings({ me }) {
|
||||
if (res) setTeam(res.admins);
|
||||
}
|
||||
|
||||
const serverNeedsUrl = ['fivem', 'http'].includes(serverDraft.type);
|
||||
async function saveServer() {
|
||||
if (!serverDraft.name.trim() || !serverDraft.query_url.trim()) {
|
||||
flash('✗ Name und Query-URL nötig');
|
||||
if (!serverDraft.name.trim() || (serverNeedsUrl ? !serverDraft.query_url.trim() : !serverDraft.host.trim())) {
|
||||
flash(serverNeedsUrl ? '✗ Name und Query-URL nötig' : '✗ Name und Host nötig');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -388,25 +398,6 @@ export default function Settings({ me }) {
|
||||
if (createdKey?.id === id) setCreatedKey(null);
|
||||
}
|
||||
|
||||
async function sendCompose() {
|
||||
if (!composer.channel || !composer.text.trim()) {
|
||||
flash('✗ Kanal und Text nötig');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await apiPost('/api/compose', {
|
||||
channel_id: composer.channel,
|
||||
message_id: composer.messageId.trim() || undefined,
|
||||
embed_title: composer.title.trim() || undefined,
|
||||
content: composer.text,
|
||||
});
|
||||
flash(res.edited ? '✓ Aktualisiert' : `✓ Gesendet (ID ${res.message_id})`);
|
||||
if (!res.edited) setComposer({ ...composer, messageId: res.message_id });
|
||||
} catch {
|
||||
flash('✗ Senden fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
const reloadMenus = async () => setMenus((await apiGet('/api/rolemenus')).menus);
|
||||
|
||||
async function saveMenu(publish = false) {
|
||||
@@ -1024,33 +1015,7 @@ export default function Settings({ me }) {
|
||||
|
||||
const tabComposer = (
|
||||
<>
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Post verfassen</h2>
|
||||
<div className="field">
|
||||
<label>Kanal</label>
|
||||
<select value={composer.channel} onChange={(e) => setComposer({ ...composer, channel: e.target.value })}>
|
||||
<option value="" disabled>— Kanal wählen —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Embed-Titel <span className="field-hint">leer = normale Nachricht statt Embed</span></label>
|
||||
<input type="text" placeholder="z. B. 📣 Ankündigung" value={composer.title} onChange={(e) => setComposer({ ...composer, title: e.target.value })} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Text <span className="field-hint">Discord-Markdown (##, **, Listen …)</span></label>
|
||||
<textarea rows={8} value={composer.text} onChange={(e) => setComposer({ ...composer, text: e.target.value })} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Message-ID <span className="field-hint">optional — vorhandenen Bot-Post bearbeiten</span></label>
|
||||
<div className="field-row">
|
||||
<input type="text" placeholder="leer = neue Nachricht" value={composer.messageId} onChange={(e) => setComposer({ ...composer, messageId: e.target.value })} />
|
||||
<button className="btn btn-save" onClick={sendCompose}>
|
||||
{composer.messageId.trim() ? 'Aktualisieren' : 'Senden'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<EmbedComposer channelOptions={channelOptions} flash={flash} />
|
||||
|
||||
<div className="settings-section">
|
||||
<h2 className="settings-title">// Geplante Posts</h2>
|
||||
@@ -1311,14 +1276,25 @@ export default function Settings({ me }) {
|
||||
Spieler-Balken, Map, Adresse) — aktualisiert alle 2 Minuten, DiscordGSM-Style.
|
||||
</p>
|
||||
<div className="field">
|
||||
<label>Status-Kanal</label>
|
||||
<select value={form.status_channel_id ?? ''} onChange={(e) => setForm({ ...form, status_channel_id: e.target.value })}>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
<div className="field-row">
|
||||
<div style={{ flex: 1 }}>
|
||||
<label>Status-Kanal</label>
|
||||
<select value={form.status_channel_id ?? ''} onChange={(e) => setForm({ ...form, status_channel_id: e.target.value })}>
|
||||
<option value="">— deaktiviert —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label>Alert-Kanal <span className="field-hint">🚨 down / ✅ online</span></label>
|
||||
<select value={form.server_alert_channel_id ?? ''} onChange={(e) => setForm({ ...form, server_alert_channel_id: e.target.value })}>
|
||||
<option value="">— keine Alerts —</option>
|
||||
{channelOptions}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-actions" style={{ marginBottom: '1.4rem' }}>
|
||||
<button className="btn btn-save" onClick={save}>Kanal speichern</button>
|
||||
<button className="btn btn-save" onClick={save}>Kanäle speichern</button>
|
||||
</div>
|
||||
|
||||
{servers.length > 0 && (
|
||||
@@ -1329,7 +1305,8 @@ export default function Settings({ me }) {
|
||||
<span className="key-scopes">{s.type}</span>
|
||||
<span className="key-used" title={s.query_url}>{s.query_url}</span>
|
||||
<button className="btn btn-mini" onClick={() => setServerDraft({
|
||||
id: s.id, name: s.name, type: s.type, query_url: s.query_url, address: s.address ?? '',
|
||||
id: s.id, name: s.name, type: s.type, query_url: s.query_url ?? '',
|
||||
address: s.address ?? '', host: s.host ?? '', port: s.port ?? '', connect_url: s.connect_url ?? '',
|
||||
})}>Bearbeiten</button>
|
||||
<button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}>✕</button>
|
||||
</div>
|
||||
@@ -1341,17 +1318,35 @@ export default function Settings({ me }) {
|
||||
<label>{serverDraft.id ? `Server #${serverDraft.id} bearbeiten` : 'Neuer Server'}</label>
|
||||
<div className="field-row">
|
||||
<input type="text" style={{ flex: 'none', width: '11rem' }} placeholder="Name, z. B. FiveM" value={serverDraft.name} onChange={(e) => setServerDraft({ ...serverDraft, name: e.target.value })} />
|
||||
<select style={{ flex: 'none', width: '8rem' }} value={serverDraft.type} onChange={(e) => setServerDraft({ ...serverDraft, type: e.target.value })}>
|
||||
<option value="fivem">FiveM</option>
|
||||
<option value="http">HTTP-Check</option>
|
||||
</select>
|
||||
<input type="text" placeholder={serverDraft.type === 'fivem' ? 'http://ip:30120' : 'https://dienst.example/health'} value={serverDraft.query_url} onChange={(e) => setServerDraft({ ...serverDraft, query_url: e.target.value })} />
|
||||
<input
|
||||
type="text" list="game-presets" style={{ flex: 'none', width: '13rem' }}
|
||||
placeholder="Spiel (gamedig-ID)" title="Vorschläge oder beliebige gamedig-Spiel-ID"
|
||||
value={serverDraft.type}
|
||||
onChange={(e) => setServerDraft({ ...serverDraft, type: e.target.value })}
|
||||
/>
|
||||
<datalist id="game-presets">
|
||||
{GAME_PRESETS.map(([id, label]) => <option key={id} value={id}>{label}</option>)}
|
||||
</datalist>
|
||||
{serverNeedsUrl ? (
|
||||
<input type="text" placeholder={serverDraft.type === 'fivem' ? 'http://ip:30120' : 'https://dienst.example/health'} value={serverDraft.query_url} onChange={(e) => setServerDraft({ ...serverDraft, query_url: e.target.value })} />
|
||||
) : (
|
||||
<>
|
||||
<input type="text" placeholder="Host / IP" value={serverDraft.host} onChange={(e) => setServerDraft({ ...serverDraft, host: e.target.value })} />
|
||||
<input type="text" style={{ flex: 'none', width: '6rem' }} placeholder="Port" value={serverDraft.port} onChange={(e) => setServerDraft({ ...serverDraft, port: e.target.value })} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="field-hint" style={{ marginTop: '.3rem' }}>
|
||||
Neben FiveM/HTTP funktioniert jede <a href="https://github.com/gamedig/node-gamedig#games-list" target="_blank" rel="noreferrer" style={{ textDecoration: 'underline' }}>gamedig-Spiel-ID</a> (300+ Spiele) — Host + Query-Port eintragen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Anzeige-Adresse <span className="field-hint">optional — steht als Copy-Paste-Block im Embed (z. B. connect play.d4rkst3r.de)</span></label>
|
||||
<label>Connect-Link + Anzeige-Adresse <span className="field-hint">Link ist im Embed klickbar (auch steam:// / fivem://); Adresse als Copy-Paste-Block</span></label>
|
||||
<div className="field-row">
|
||||
<input type="text" placeholder="fivem://connect/play.d4rkst3r.de" value={serverDraft.connect_url} onChange={(e) => setServerDraft({ ...serverDraft, connect_url: e.target.value })} />
|
||||
<input type="text" placeholder="connect play.d4rkst3r.de" value={serverDraft.address} onChange={(e) => setServerDraft({ ...serverDraft, address: e.target.value })} />
|
||||
</div>
|
||||
<div className="field-row" style={{ marginTop: '.6rem' }}>
|
||||
<button className="btn btn-save" onClick={saveServer}>{serverDraft.id ? 'Aktualisieren' : 'Hinzufügen'}</button>
|
||||
{serverDraft.id && <button className="btn btn-ghost" onClick={() => setServerDraft(emptyServer)}>Abbrechen</button>}
|
||||
</div>
|
||||
|
||||
@@ -1009,3 +1009,85 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
|
||||
transition: opacity .15s;
|
||||
}
|
||||
.act-bar:hover { opacity: .7; }
|
||||
|
||||
/* ── COMPOSER v2: Builder + Discord-Preview ────────── */
|
||||
.composer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
@media (max-width: 900px) { .composer-grid { grid-template-columns: 1fr; } }
|
||||
.composer-preview > label {
|
||||
display: block;
|
||||
font-family: var(--mono);
|
||||
font-size: .68rem; letter-spacing: .15em; text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
|
||||
/* Discord-Look (bewusst Discord-Farben, nicht Brand) */
|
||||
.dpv {
|
||||
background: #313338;
|
||||
border-radius: 10px;
|
||||
padding: 1rem;
|
||||
font-family: 'gg sans', 'Noto Sans', sans-serif;
|
||||
font-size: .95rem;
|
||||
line-height: 1.35;
|
||||
color: #dbdee1;
|
||||
position: sticky;
|
||||
top: 5rem;
|
||||
}
|
||||
.dpv-msg { display: flex; gap: .8rem; }
|
||||
.dpv-avatar {
|
||||
width: 40px; height: 40px; flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: #f5c518;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
.dpv-body { min-width: 0; flex: 1; }
|
||||
.dpv-name { font-weight: 600; color: #f2f3f5; margin-bottom: .15rem; }
|
||||
.dpv-tag {
|
||||
background: #5865f2; color: #fff;
|
||||
font-size: .6rem; font-weight: 700;
|
||||
border-radius: 4px; padding: .05rem .3rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.dpv-time { font-size: .7rem; color: #949ba4; font-weight: 400; }
|
||||
.dpv-content { white-space: pre-wrap; word-break: break-word; }
|
||||
.dpv-content p, .dpv-desc p { margin: .15rem 0; }
|
||||
.dpv-content .md-h, .dpv-desc .md-h {
|
||||
font-family: inherit; text-transform: none; letter-spacing: 0;
|
||||
font-size: 1.15rem; font-weight: 700; color: #f2f3f5; margin: .4rem 0 .1rem;
|
||||
}
|
||||
.dpv .md-list li::before { color: #dbdee1; }
|
||||
.dpv .md-hr { background: #3f4147; }
|
||||
.dpv code { background: #1e1f22; border: none; color: #dbdee1; }
|
||||
|
||||
.dpv-embed {
|
||||
display: flex; gap: 1rem;
|
||||
background: #2b2d31;
|
||||
border-left: 4px solid #f5c518;
|
||||
border-radius: 4px;
|
||||
padding: .7rem .9rem .8rem;
|
||||
margin-top: .35rem;
|
||||
max-width: 520px;
|
||||
}
|
||||
.dpv-embed-main { min-width: 0; flex: 1; }
|
||||
.dpv-author {
|
||||
display: flex; align-items: center; gap: .5rem;
|
||||
font-size: .82rem; font-weight: 600; color: #f2f3f5;
|
||||
margin-bottom: .3rem;
|
||||
}
|
||||
.dpv-author img { width: 22px; height: 22px; border-radius: 50%; }
|
||||
.dpv-title { font-weight: 700; color: #f2f3f5; margin-bottom: .3rem; }
|
||||
.dpv-desc { font-size: .9rem; word-break: break-word; }
|
||||
.dpv-fields { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin-top: .5rem; }
|
||||
.dpv-field { flex: 0 0 100%; }
|
||||
.dpv-field.inline { flex: 1 0 30%; min-width: 8rem; }
|
||||
.dpv-field-name { font-size: .8rem; font-weight: 700; color: #f2f3f5; margin-bottom: .1rem; }
|
||||
.dpv-field-value { font-size: .85rem; }
|
||||
.dpv-image { max-width: 100%; border-radius: 6px; margin-top: .6rem; display: block; }
|
||||
.dpv-thumb { width: 76px; height: 76px; object-fit: cover; border-radius: 6px; flex-shrink: 0; }
|
||||
.dpv-footer { font-size: .72rem; color: #949ba4; margin-top: .55rem; }
|
||||
|
||||
Generated
+612
-1
@@ -15,7 +15,8 @@
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"discord.js": "^14.16.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"fastify": "^5.10.0"
|
||||
"fastify": "^5.10.0",
|
||||
"gamedig": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -413,6 +414,18 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodable/entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodable"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
@@ -452,6 +465,36 @@
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sindresorhus/is": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
|
||||
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/is?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@szmarczak/http-timer": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
|
||||
"integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"defer-to-connect": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
|
||||
@@ -519,6 +562,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/any-promise": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
"integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/anynum": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz",
|
||||
"integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
@@ -557,6 +618,33 @@
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/barse": {
|
||||
"version": "0.4.3",
|
||||
"resolved": "https://registry.npmjs.org/barse/-/barse-0.4.3.tgz",
|
||||
"integrity": "sha512-UEpvriJqAn8zuVinYICuKoPttZy3XxXEoqX/V2uYAL4zzJRuNzCK3+20nAu3YUIa2U7G53kf90wfBIp9/A+Odw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"readable-stream": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/barse/node_modules/readable-stream": {
|
||||
"version": "1.0.34",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
|
||||
"integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.1",
|
||||
"isarray": "0.0.1",
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/barse/node_modules/string_decoder": {
|
||||
"version": "0.10.31",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -647,12 +735,60 @@
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-lookup": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
|
||||
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request": {
|
||||
"version": "10.2.14",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz",
|
||||
"integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-cache-semantics": "^4.0.2",
|
||||
"get-stream": "^6.0.1",
|
||||
"http-cache-semantics": "^4.1.1",
|
||||
"keyv": "^4.5.3",
|
||||
"mimic-response": "^4.0.0",
|
||||
"normalize-url": "^8.0.0",
|
||||
"responselike": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
}
|
||||
},
|
||||
"node_modules/cacheable-request/node_modules/mimic-response": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
|
||||
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
|
||||
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
|
||||
@@ -679,6 +815,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
@@ -703,6 +845,15 @@
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/defer-to-connect": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
|
||||
"integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -778,6 +929,14 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/emitter-component": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/emitter-component/-/emitter-component-1.1.2.tgz",
|
||||
"integrity": "sha512-QdXO3nXOzZB4pAjM0n6ZE+R9/+kPpECA/XSELIcc54NeYVnBqIk+4DFiBgK+8QbV3mdvTG6nedl7dTYgO+5wDw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
@@ -793,6 +952,13 @@
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/event-to-promise": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz",
|
||||
"integrity": "sha512-VOBBfyaADfe378ZzG0tgkzmsvzUyeU5arehrFzNRt5yaASUDshgctTwSrPI17ocAwR3+YftsxRClHF+GBKFByQ==",
|
||||
"deprecated": "Use promise-toolbox/fromEvent instead",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
@@ -879,6 +1045,59 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.0.tgz",
|
||||
"integrity": "sha512-F74cZEdCvuw9P41GAC3rod4X04jjWGM1JPEv/GWSqFTWLsdyMSBMBMlm9Hk3GLBgLBbdBNY8yee0pQh2RBVESQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-expression-matcher": "^1.6.2",
|
||||
"xml-naming": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-builder/node_modules/xml-naming": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
|
||||
"integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "5.8.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.8.0.tgz",
|
||||
"integrity": "sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodable/entities": "^2.1.0",
|
||||
"fast-xml-builder": "^1.2.0",
|
||||
"path-expression-matcher": "^1.5.0",
|
||||
"strnum": "^2.3.0",
|
||||
"xml-naming": "^0.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fastify": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/fastify/-/fastify-5.10.0.tgz",
|
||||
@@ -957,12 +1176,71 @@
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data-encoder": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
|
||||
"integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gamedig": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.3.tgz",
|
||||
"integrity": "sha512-35z+ijImna5Ty84efoVXDHBYwH0VmvcKQzwvZarCrcpqWe6nn8X3rbKlLAtmt1HC7EvdPvBE7VjXJmwn/e1K9A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-xml-parser": "5.8.0",
|
||||
"gbxremote": "0.2.1",
|
||||
"got": "13.0.0",
|
||||
"iconv-lite": "0.7.2",
|
||||
"long": "5.3.2",
|
||||
"minimist": "1.2.8",
|
||||
"seek-bzip": "2.0.0",
|
||||
"telnet-client": "2.2.13",
|
||||
"varint": "6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"gamedig": "bin/gamedig.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gbxremote": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/gbxremote/-/gbxremote-0.2.1.tgz",
|
||||
"integrity": "sha512-SMehu6Y6ndq2Qgp9VxAb8Np3f+UUD+RWoW2SAMaxzGS96rWXyr4T1GGkecO0HHtxeH1m7pEh4FJWB8a/6aM2XQ==",
|
||||
"dependencies": {
|
||||
"any-promise": "^1.1.0",
|
||||
"barse": "~0.4.2",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"string-to-stream": "^1.0.1",
|
||||
"xmlrpc": "^1.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/get-stream": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
@@ -986,6 +1264,37 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "13.0.0",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
|
||||
"integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^5.2.0",
|
||||
"@szmarczak/http-timer": "^5.0.1",
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"cacheable-request": "^10.2.8",
|
||||
"decompress-response": "^6.0.0",
|
||||
"form-data-encoder": "^2.1.2",
|
||||
"get-stream": "^6.0.1",
|
||||
"http2-wrapper": "^2.1.10",
|
||||
"lowercase-keys": "^3.0.0",
|
||||
"p-cancelable": "^3.0.0",
|
||||
"responselike": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/got?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -1006,6 +1315,35 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http2-wrapper": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
|
||||
"integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"quick-lru": "^5.1.1",
|
||||
"resolve-alpn": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -1047,6 +1385,18 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
|
||||
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-ref-resolver": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz",
|
||||
@@ -1072,6 +1422,15 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"json-buffer": "3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/light-my-request": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
|
||||
@@ -1121,6 +1480,24 @@
|
||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
|
||||
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.2",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
|
||||
@@ -1205,6 +1582,12 @@
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/net": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz",
|
||||
"integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.94.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz",
|
||||
@@ -1217,6 +1600,18 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-url": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz",
|
||||
"integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||
@@ -1235,6 +1630,30 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/p-cancelable": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
|
||||
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/path-expression-matcher": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz",
|
||||
"integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
|
||||
@@ -1315,6 +1734,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
@@ -1347,6 +1772,18 @@
|
||||
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
|
||||
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
@@ -1394,6 +1831,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-alpn": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
|
||||
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/responselike": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
|
||||
"integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lowercase-keys": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/ret": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
|
||||
@@ -1470,6 +1928,18 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/secure-json-parse": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz",
|
||||
@@ -1486,6 +1956,19 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/seek-bzip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-2.0.0.tgz",
|
||||
"integrity": "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"seek-bunzip": "bin/seek-bunzip",
|
||||
"seek-table": "bin/seek-bzip-table"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||
@@ -1582,6 +2065,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/stream": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/stream/-/stream-0.0.2.tgz",
|
||||
"integrity": "sha512-gCq3NDI2P35B2n6t76YJuOp7d6cN/C7Rt0577l91wllh0sY9ZBuw9KaSGqH/b0hzn3CWWJbpbW0W0WvQ1H/Q7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emitter-component": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -1591,6 +2083,52 @@
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-to-stream": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz",
|
||||
"integrity": "sha512-QySF2+3Rwq0SdO3s7BAp4x+c3qsClpPQ6abAmb0DGViiSBAkT5kL6JT2iyzEVP+T1SmzHrQD1TwlP9QAHCc+Sw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"readable-stream": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-to-stream/node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-to-stream/node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/string-to-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-to-stream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
@@ -1600,6 +2138,21 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.1.tgz",
|
||||
"integrity": "sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"anynum": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
|
||||
@@ -1628,6 +2181,20 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/telnet-client": {
|
||||
"version": "2.2.13",
|
||||
"resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.13.tgz",
|
||||
"integrity": "sha512-pXAGggyeE7/STEnXhN8oe0V1hRzT6oNiGvs4CU4ilppledV40dok+70yl/Xb2newAXXdM9C5rW8otOKmRvkZxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"net": "^1.0.2",
|
||||
"stream": "^0.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/kozjak"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
|
||||
@@ -1709,6 +2276,12 @@
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/varint": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
|
||||
"integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
@@ -1735,6 +2308,44 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-naming": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz",
|
||||
"integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
|
||||
"integrity": "sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlrpc": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz",
|
||||
"integrity": "sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "1.2.x",
|
||||
"xmlbuilder": "8.2.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8",
|
||||
"npm": ">=1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@
|
||||
"better-sqlite3": "^12.11.1",
|
||||
"discord.js": "^14.16.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"fastify": "^5.10.0"
|
||||
"fastify": "^5.10.0",
|
||||
"gamedig": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
+110
-3
@@ -1,9 +1,12 @@
|
||||
// Kleinkram-Paket: Triggers (Auto-Antworten), Reminder-Scheduler, Temp-Voice
|
||||
// (Join to Create), Event-Ankündigungen und Aktivitäts-Zählung.
|
||||
import { ChannelType, EmbedBuilder, Events } from 'discord.js';
|
||||
import {
|
||||
ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, EmbedBuilder, Events,
|
||||
MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle,
|
||||
} from 'discord.js';
|
||||
import {
|
||||
listTriggers, dueReminders, markReminderSent, countActivity,
|
||||
trackTempVoice, untrackTempVoice, isTempVoice, listTempVoice,
|
||||
trackTempVoice, untrackTempVoice, isTempVoice, listTempVoice, tempVoiceOwner,
|
||||
} from '../db.js';
|
||||
import {
|
||||
tempVoiceChannelId, eventsAnnounceChannelId, brandColor, brandFooter,
|
||||
@@ -40,8 +43,30 @@ async function handleVoiceState(oldState, newState) {
|
||||
type: ChannelType.GuildVoice,
|
||||
parent: hub.parentId ?? undefined,
|
||||
});
|
||||
trackTempVoice(channel.id);
|
||||
trackTempVoice(channel.id, newState.member.id);
|
||||
await newState.member.voice.setChannel(channel).catch(() => {});
|
||||
|
||||
// Control-Panel in den Kanal-Textchat (nur der Besitzer darf bedienen)
|
||||
await channel.send({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(brandColor())
|
||||
.setTitle('🔊 Dein Kanal')
|
||||
.setDescription(
|
||||
`<@${newState.member.id}>, das hier ist dein temporärer Voice-Kanal — ` +
|
||||
'er verschwindet automatisch, wenn alle raus sind. Steuern kannst du ihn hier:'
|
||||
)
|
||||
.setFooter({ text: brandFooter('VOICE') }),
|
||||
],
|
||||
components: [
|
||||
new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder().setCustomId('tv:rename').setStyle(ButtonStyle.Secondary).setLabel('Umbenennen').setEmoji('✏️'),
|
||||
new ButtonBuilder().setCustomId('tv:lock').setStyle(ButtonStyle.Secondary).setLabel('Sperren/Öffnen').setEmoji('🔒'),
|
||||
new ButtonBuilder().setCustomId('tv:limit').setStyle(ButtonStyle.Secondary).setLabel('Limit').setEmoji('👥'),
|
||||
new ButtonBuilder().setCustomId('tv:delete').setStyle(ButtonStyle.Danger).setLabel('Löschen').setEmoji('🗑️')
|
||||
),
|
||||
],
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
// Temp-Kanal leer → löschen
|
||||
@@ -99,6 +124,77 @@ async function announceEvent(event) {
|
||||
await channel.send({ embeds: [embed] }).catch(() => {});
|
||||
}
|
||||
|
||||
/* ── Temp-Voice-Control-Panel (Buttons + Modals) ───── */
|
||||
|
||||
async function handleTempVoiceInteraction(interaction) {
|
||||
const channel = interaction.channel;
|
||||
const owner = tempVoiceOwner(channel?.id);
|
||||
if (!owner) {
|
||||
await interaction.reply({ content: '❌ Das ist kein Temp-Voice-Kanal (mehr).', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
if (interaction.user.id !== owner) {
|
||||
await interaction.reply({ content: '❌ Nur der Kanal-Besitzer darf das.', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
const action = interaction.customId.split(':')[1];
|
||||
|
||||
if (action === 'rename') {
|
||||
const modal = new ModalBuilder().setCustomId('tvmodal:rename').setTitle('Kanal umbenennen');
|
||||
modal.addComponents(new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder().setCustomId('value').setLabel('Neuer Name').setStyle(TextInputStyle.Short).setMaxLength(90).setRequired(true)
|
||||
));
|
||||
await interaction.showModal(modal);
|
||||
return;
|
||||
}
|
||||
if (action === 'limit') {
|
||||
const modal = new ModalBuilder().setCustomId('tvmodal:limit').setTitle('User-Limit setzen');
|
||||
modal.addComponents(new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder().setCustomId('value').setLabel('Limit (0 = unbegrenzt, max 99)').setStyle(TextInputStyle.Short).setMaxLength(2).setRequired(true)
|
||||
));
|
||||
await interaction.showModal(modal);
|
||||
return;
|
||||
}
|
||||
if (action === 'lock') {
|
||||
const everyone = channel.guild.roles.everyone;
|
||||
const current = channel.permissionOverwrites.cache.get(everyone.id);
|
||||
const locked = current?.deny?.has?.('Connect') ?? false;
|
||||
await channel.permissionOverwrites.edit(everyone, { Connect: locked ? null : false });
|
||||
await interaction.reply({
|
||||
content: locked ? '🔓 Kanal ist wieder offen.' : '🔒 Kanal gesperrt — nur wer drin ist, bleibt drin.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (action === 'delete') {
|
||||
await interaction.reply({ content: '🗑️ Kanal wird gelöscht …', flags: MessageFlags.Ephemeral });
|
||||
untrackTempVoice(channel.id);
|
||||
await channel.delete().catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTempVoiceModal(interaction) {
|
||||
const channel = interaction.channel;
|
||||
if (tempVoiceOwner(channel?.id) !== interaction.user.id) {
|
||||
await interaction.reply({ content: '❌ Nur der Kanal-Besitzer darf das.', flags: MessageFlags.Ephemeral });
|
||||
return;
|
||||
}
|
||||
const kind = interaction.customId.split(':')[1];
|
||||
const value = interaction.fields.getTextInputValue('value').trim();
|
||||
|
||||
if (kind === 'rename') {
|
||||
await channel.setName(`🔊 ${value}`.slice(0, 90)).catch(() => {});
|
||||
await interaction.reply({ content: `✏️ Kanal heißt jetzt **🔊 ${value}**.`, flags: MessageFlags.Ephemeral });
|
||||
} else if (kind === 'limit') {
|
||||
const n = Math.max(0, Math.min(99, Number(value) || 0));
|
||||
await channel.setUserLimit(n).catch(() => {});
|
||||
await interaction.reply({
|
||||
content: n === 0 ? '👥 Limit entfernt.' : `👥 Limit: **${n}** Plätze.`,
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Registrierung ─────────────────────────────────── */
|
||||
|
||||
export function registerExtras(client) {
|
||||
@@ -117,6 +213,17 @@ export function registerExtras(client) {
|
||||
client.on(Events.VoiceStateUpdate, (oldState, newState) =>
|
||||
handleVoiceState(oldState, newState).catch((e) => console.error('[tempvoice]', e))
|
||||
);
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
try {
|
||||
if (interaction.isButton() && interaction.customId.startsWith('tv:')) {
|
||||
await handleTempVoiceInteraction(interaction);
|
||||
} else if (interaction.isModalSubmit() && interaction.customId.startsWith('tvmodal:')) {
|
||||
await handleTempVoiceModal(interaction);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[tempvoice] Panel-Fehler:', error);
|
||||
}
|
||||
});
|
||||
client.on(Events.GuildScheduledEventCreate, (event) =>
|
||||
announceEvent(event).catch((e) => console.error('[events]', e))
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function giveawayTick(client) {
|
||||
? `🏆 Gewonnen: ${mentions}\n👥 ${entries.length} Teilnahmen`
|
||||
: 'Keine Teilnahmen — niemand hat gewonnen. 😢'
|
||||
)
|
||||
.setFooter({ text: brandFooter('GIVEAWAY') }),
|
||||
.setFooter({ text: brandFooter('GIVEAWAY'), iconURL: client?.user?.displayAvatarURL?.({ size: 64 }) }),
|
||||
],
|
||||
components: [],
|
||||
}).catch(() => {});
|
||||
|
||||
@@ -36,7 +36,7 @@ async function ensureModmailThread(client, user) {
|
||||
`Neue Modmail-Konversation mit **${user.username}** (<@${user.id}>).\n` +
|
||||
'Antworten in diesem Thread gehen als DM an den User.'
|
||||
)
|
||||
.setFooter({ text: brandFooter('MODMAIL') }),
|
||||
.setFooter({ text: brandFooter('MODMAIL'), iconURL: member.client?.user?.displayAvatarURL?.({ size: 64 }) }),
|
||||
],
|
||||
});
|
||||
return thread;
|
||||
@@ -93,7 +93,7 @@ async function handleMemberAdd(member) {
|
||||
'🧪 Playtester werden? Schau nach dem Bewerbungs-Post!\n' +
|
||||
'🐛 Bug gefunden? Einfach `/bug` benutzen.'
|
||||
)
|
||||
.setFooter({ text: brandFooter('COMMUNITY') })
|
||||
.setFooter({ text: brandFooter('COMMUNITY'), iconURL: member.client?.user?.displayAvatarURL?.({ size: 64 }) })
|
||||
.setTimestamp();
|
||||
|
||||
await channel.send({ content: `<@${member.id}>`, embeds: [embed] });
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function postReleaseEmbed(client, release) {
|
||||
.setDescription(body || '*Keine Release-Notes.*')
|
||||
.addFields({ name: 'Version', value: `\`${release.tag}\``, inline: true },
|
||||
{ name: 'Projekt', value: release.repo, inline: true })
|
||||
.setFooter({ text: brandFooter('RELEASE') })
|
||||
.setFooter({ text: brandFooter('RELEASE'), iconURL: client?.user?.displayAvatarURL?.({ size: 64 }) })
|
||||
.setTimestamp();
|
||||
|
||||
const buttons = new ActionRowBuilder().addComponents(
|
||||
|
||||
+110
-20
@@ -1,65 +1,153 @@
|
||||
// Game-Server-Monitor (DiscordGSM-Stil): pro Server ein eigenes Status-Embed
|
||||
// im Status-Kanal (edit in place, grün/rot), alle 2 Minuten aktualisiert.
|
||||
// Server werden im Setup-Tab "Server" verwaltet (Typ: fivem | http).
|
||||
// Game-Server-Monitor v3 (DiscordGSM-Stil): gamedig-Queries für 300+ Spiele,
|
||||
// pro Server ein Live-Embed (Spieler-Balken, Map, Player-Liste, Connect-Link),
|
||||
// Down/Up-Alerts mit 2-Fails-Schwelle. Verwaltet im Setup-Tab "Server".
|
||||
import { ActivityType, EmbedBuilder, Events } from 'discord.js';
|
||||
import { GameDig } from 'gamedig';
|
||||
import { getSetting, listGameservers, setGameserverMessage } from '../db.js';
|
||||
import { brandFooter, botStatusText } from '../runtime-settings.js';
|
||||
import { brandEmbed } from '../embeds.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
|
||||
const TIMEOUT_MS = 8000;
|
||||
const FAILS_BEFORE_ALERT = 2;
|
||||
const GREEN = 0x23a55a;
|
||||
const RED = 0xf23f43;
|
||||
|
||||
/** Einen Server abfragen — je nach Typ */
|
||||
// Emoji pro Spieltyp (Fallback 🎮)
|
||||
export const GAME_ICONS = {
|
||||
fivem: '🚗', http: '🌐', minecraft: '⛏️', minecraftbe: '⛏️',
|
||||
teamfortress2: '🎩', counterstrike2: '🔫', csgo: '🔫', garrysmod: '🔧',
|
||||
rust: '🪓', valheim: '🛡️', palworld: '🐑', ark: '🦖', arkse: '🦖',
|
||||
dayz: '🧟', projectzomboid: '🧟', sevendaystodie: '🧟',
|
||||
satisfactory: '🏭', terraria: '🌳', arma3: '🪖', squad: '🪖', unturned: '🏝️',
|
||||
};
|
||||
|
||||
// Alert-Zustand pro Server (in-memory)
|
||||
const alertState = new Map(); // id → { fails, down, since }
|
||||
|
||||
/** Einen Server abfragen — fivem/http direkt, alles andere über gamedig */
|
||||
export async function queryServer(server) {
|
||||
const base = { ...server, online: false, players: null, max: null, map: null };
|
||||
const base = {
|
||||
...server, online: false, players: null, max: null, map: null,
|
||||
playerNames: [], ping: null,
|
||||
};
|
||||
const started = Date.now();
|
||||
try {
|
||||
if (server.type === 'fivem') {
|
||||
const res = await fetch(`${server.query_url.replace(/\/$/, '')}/dynamic.json`, {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/2.0' },
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/3.0' },
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
return {
|
||||
...base,
|
||||
online: true,
|
||||
...base, online: true, ping: Date.now() - started,
|
||||
players: Number(data.clients) || 0,
|
||||
max: Number(data.sv_maxclients) || 0,
|
||||
map: data.mapname ?? null,
|
||||
};
|
||||
}
|
||||
// Typ 'http': generischer Healthcheck (2xx/3xx/4xx = erreichbar)
|
||||
const res = await fetch(server.query_url, {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
redirect: 'follow',
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/2.0' },
|
||||
if (server.type === 'http') {
|
||||
const res = await fetch(server.query_url, {
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
redirect: 'follow',
|
||||
headers: { 'User-Agent': 'd4rkbot-monitor/3.0' },
|
||||
});
|
||||
return { ...base, online: res.status < 500, ping: Date.now() - started };
|
||||
}
|
||||
// gamedig: Typ = Spiel-ID (minecraft, teamfortress2, rust, …)
|
||||
const state = await GameDig.query({
|
||||
type: server.type,
|
||||
host: server.host,
|
||||
port: server.port || undefined,
|
||||
maxRetries: 1,
|
||||
socketTimeout: TIMEOUT_MS,
|
||||
});
|
||||
return { ...base, online: res.status < 500 };
|
||||
return {
|
||||
...base, online: true, ping: state.ping ?? Date.now() - started,
|
||||
players: state.players?.length ?? state.numplayers ?? 0,
|
||||
max: state.maxplayers ?? null,
|
||||
map: state.map || null,
|
||||
playerNames: (state.players ?? []).map((p) => p?.name).filter(Boolean).slice(0, 15),
|
||||
};
|
||||
} catch {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
/** Status-Embed für einen Server bauen (DiscordGSM-Look) */
|
||||
/** GSM-Style-Embed für ein Query-Ergebnis */
|
||||
export function buildServerEmbed(r) {
|
||||
const icon = GAME_ICONS[r.type] ?? '🎮';
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(r.online ? GREEN : RED)
|
||||
.setTitle(`${r.online ? '🟢' : '🔴'} ${r.name}`)
|
||||
.addFields({ name: 'Status', value: r.online ? '**Online**' : '**Offline**', inline: true })
|
||||
.setFooter({ text: `${brandFooter('STATUS')} • alle 2 min` })
|
||||
.setTitle(`${icon} ${r.name}`)
|
||||
.addFields({ name: 'Status', value: r.online ? '🟢 **Online**' : '🔴 **Offline**', inline: true })
|
||||
.setFooter({ text: `${brandFooter('STATUS')}${r.ping != null ? ` • ${r.ping}ms` : ''} • alle 2 min` })
|
||||
.setTimestamp();
|
||||
|
||||
if (r.online && r.players != null && r.max) {
|
||||
const pct = Math.min(1, r.players / r.max);
|
||||
const bar = '█'.repeat(Math.round(pct * 10)).padEnd(10, '░');
|
||||
embed.addFields({ name: 'Spieler', value: `\`${bar}\` ${r.players}/${r.max}`, inline: true });
|
||||
embed.addFields({
|
||||
name: 'Spieler',
|
||||
value: `\`${bar}\` ${r.players}/${r.max} (${Math.round(pct * 100)}%)`,
|
||||
inline: true,
|
||||
});
|
||||
}
|
||||
if (r.map) embed.addFields({ name: 'Map', value: r.map, inline: true });
|
||||
if (r.connect_url) {
|
||||
// steam:// & Co. sind in Embeds klickbar (Link-Buttons erlauben nur https)
|
||||
embed.addFields({ name: 'Connect', value: `[${r.connect_url}](${r.connect_url})`, inline: false });
|
||||
}
|
||||
if (r.address) embed.addFields({ name: 'Adresse', value: `\`\`\`\n${r.address}\n\`\`\`` });
|
||||
if (r.online && r.playerNames.length > 0) {
|
||||
embed.addFields({
|
||||
name: `Spieler-Liste (${r.playerNames.length})`,
|
||||
value: r.playerNames.map((n) => `\`${n}\``).join(' ').slice(0, 1000),
|
||||
});
|
||||
}
|
||||
return embed;
|
||||
}
|
||||
|
||||
/** Down/Up-Alerts in den Alert-Kanal (2 Fails in Folge = down) */
|
||||
async function handleAlerts(client, r) {
|
||||
const channelId = getSetting('server_alert_channel_id');
|
||||
if (!channelId) return;
|
||||
const s = alertState.get(r.id) ?? { fails: 0, down: false, since: null };
|
||||
|
||||
if (r.online) {
|
||||
if (s.down) {
|
||||
const minutes = Math.max(1, Math.round((Date.now() - s.since) / 60000));
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
await channel?.send({
|
||||
embeds: [
|
||||
brandEmbed(client, 'ALERT')
|
||||
.setColor(GREEN)
|
||||
.setTitle(`✅ ${r.name} ist wieder online!`)
|
||||
.setDescription(`Downtime: ~${minutes} min`),
|
||||
],
|
||||
}).catch(() => {});
|
||||
}
|
||||
alertState.set(r.id, { fails: 0, down: false, since: null });
|
||||
return;
|
||||
}
|
||||
const fails = s.fails + 1;
|
||||
if (!s.down && fails >= FAILS_BEFORE_ALERT) {
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
await channel?.send({
|
||||
embeds: [
|
||||
brandEmbed(client, 'ALERT')
|
||||
.setColor(RED)
|
||||
.setTitle(`🚨 ${r.name} scheint down zu sein!`)
|
||||
.setDescription(r.address ? `\`${r.address}\`` : null),
|
||||
],
|
||||
}).catch(() => {});
|
||||
alertState.set(r.id, { fails, down: true, since: Date.now() });
|
||||
} else {
|
||||
alertState.set(r.id, { ...s, fails });
|
||||
}
|
||||
}
|
||||
|
||||
/** Ein Poll-Durchlauf — exportiert für Tests und den Timer */
|
||||
export async function monitorTick(client) {
|
||||
const servers = listGameservers();
|
||||
@@ -79,7 +167,9 @@ export async function monitorTick(client) {
|
||||
} catch { /* Presence ist nice-to-have */ }
|
||||
}
|
||||
|
||||
// Pro Server ein Embed pflegen
|
||||
for (const r of results) await handleAlerts(client, r).catch(() => {});
|
||||
|
||||
// Pro Server ein Embed pflegen (edit in place)
|
||||
const channelId = getSetting('status_channel_id');
|
||||
if (!channelId) return;
|
||||
const channel = await client.channels.fetch(channelId).catch(() => null);
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function postWeeklyRecap(client) {
|
||||
{ name: 'Devlogs', value: String(stats.devlogs), inline: true },
|
||||
...(repoLine ? [{ name: 'Aktive Projekte', value: repoLine, inline: false }] : [])
|
||||
)
|
||||
.setFooter({ text: brandFooter('WEEKLY') })
|
||||
.setFooter({ text: brandFooter('WEEKLY'), iconURL: client?.user?.displayAvatarURL?.({ size: 64 }) })
|
||||
.setTimestamp();
|
||||
|
||||
await channel.send({ embeds: [embed] });
|
||||
|
||||
@@ -361,7 +361,26 @@ db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS temp_voice (
|
||||
channel_id TEXT PRIMARY KEY
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS message_templates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
payload TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
// Migration: Temp-Voice-Besitzer (fürs Control-Panel)
|
||||
{
|
||||
const cols = db.prepare('PRAGMA table_info(temp_voice)').all().map((c) => c.name);
|
||||
if (!cols.includes('owner_id')) db.exec(`ALTER TABLE temp_voice ADD COLUMN owner_id TEXT NOT NULL DEFAULT ''`);
|
||||
}
|
||||
const upsertTemplate = db.prepare(`
|
||||
INSERT INTO message_templates (name, payload) VALUES (?, ?)
|
||||
ON CONFLICT(name) DO UPDATE SET payload = excluded.payload
|
||||
`);
|
||||
const listTemplatesStmt = db.prepare('SELECT id, name, payload FROM message_templates ORDER BY name');
|
||||
const deleteTemplateStmt = db.prepare('DELETE FROM message_templates WHERE name = ?');
|
||||
export const saveTemplate = (name, payload) => upsertTemplate.run(name, JSON.stringify(payload));
|
||||
export const listTemplates = () => listTemplatesStmt.all().map((t) => ({ ...t, payload: JSON.parse(t.payload) }));
|
||||
export const deleteTemplate = (name) => deleteTemplateStmt.run(name).changes > 0;
|
||||
const upsertTrigger = db.prepare(`
|
||||
INSERT INTO triggers (keyword, reply) VALUES (?, ?)
|
||||
ON CONFLICT(keyword) DO UPDATE SET reply = excluded.reply
|
||||
@@ -395,10 +414,12 @@ const activityRangeStmt = db.prepare(
|
||||
);
|
||||
export const activityRange = (days = 30) => activityRangeStmt.all(`-${days} days`);
|
||||
|
||||
const insertTempVoice = db.prepare('INSERT OR IGNORE INTO temp_voice (channel_id) VALUES (?)');
|
||||
const insertTempVoice = db.prepare('INSERT OR IGNORE INTO temp_voice (channel_id, owner_id) VALUES (?, ?)');
|
||||
const tempVoiceOwnerStmt = db.prepare('SELECT owner_id FROM temp_voice WHERE channel_id = ?');
|
||||
export const tempVoiceOwner = (id) => tempVoiceOwnerStmt.get(id)?.owner_id ?? null;
|
||||
const deleteTempVoiceStmt = db.prepare('DELETE FROM temp_voice WHERE channel_id = ?');
|
||||
const listTempVoiceStmt = db.prepare('SELECT channel_id FROM temp_voice');
|
||||
export const trackTempVoice = (id) => insertTempVoice.run(id);
|
||||
export const trackTempVoice = (id, ownerId = '') => insertTempVoice.run(id, ownerId);
|
||||
export const untrackTempVoice = (id) => deleteTempVoiceStmt.run(id);
|
||||
export const listTempVoice = () => listTempVoiceStmt.all().map((r) => r.channel_id);
|
||||
const isTempVoiceStmt = db.prepare('SELECT 1 FROM temp_voice WHERE channel_id = ?');
|
||||
@@ -453,11 +474,21 @@ db.exec(`
|
||||
db.prepare(`DELETE FROM settings WHERE key = 'gameservers'`).run();
|
||||
}
|
||||
}
|
||||
// Migration v3: host/port (gamedig) + connect_url (Connect-Link im Embed)
|
||||
{
|
||||
const cols = db.prepare('PRAGMA table_info(gameservers)').all().map((c) => c.name);
|
||||
if (!cols.includes('host')) db.exec(`ALTER TABLE gameservers ADD COLUMN host TEXT NOT NULL DEFAULT ''`);
|
||||
if (!cols.includes('port')) db.exec('ALTER TABLE gameservers ADD COLUMN port INTEGER');
|
||||
if (!cols.includes('connect_url')) db.exec(`ALTER TABLE gameservers ADD COLUMN connect_url TEXT NOT NULL DEFAULT ''`);
|
||||
}
|
||||
const insertGameserver = db.prepare(`
|
||||
INSERT INTO gameservers (name, type, query_url, address) VALUES (@name, @type, @query_url, @address)
|
||||
INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url)
|
||||
VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url)
|
||||
`);
|
||||
const updateGameserverStmt = db.prepare(`
|
||||
UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address WHERE id = @id
|
||||
UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address,
|
||||
host = @host, port = @port, connect_url = @connect_url
|
||||
WHERE id = @id
|
||||
`);
|
||||
const setGameserverMessageStmt = db.prepare('UPDATE gameservers SET message_id = ? WHERE id = ?');
|
||||
const listGameserversStmt = db.prepare('SELECT * FROM gameservers ORDER BY id');
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Zentrale Embed-Factory: einheitlicher Brand-Look für alle Bot-Embeds.
|
||||
// Footer mit Bot-Avatar, Brand-Farbe und Timestamp kommen automatisch.
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { brandColor, brandColor2, brandFooter } from './runtime-settings.js';
|
||||
|
||||
/**
|
||||
* Gebrandetes Embed erzeugen.
|
||||
* @param {import('discord.js').Client|null} client — für das Footer-Icon (Bot-Avatar)
|
||||
* @param {string} tag — Footer-Tag, z. B. 'DEVLOG' → "D4RKST3R // DEVLOG"
|
||||
* @param {{ secondary?: boolean, footerSuffix?: string, timestamp?: boolean }} [opts]
|
||||
*/
|
||||
export function brandEmbed(client, tag, opts = {}) {
|
||||
const embed = new EmbedBuilder().setColor(opts.secondary ? brandColor2() : brandColor());
|
||||
embed.setFooter({
|
||||
text: brandFooter(tag) + (opts.footerSuffix ? ` • ${opts.footerSuffix}` : ''),
|
||||
iconURL: client?.user?.displayAvatarURL?.({ size: 64 }) ?? undefined,
|
||||
});
|
||||
if (opts.timestamp !== false) embed.setTimestamp();
|
||||
return embed;
|
||||
}
|
||||
+6
-1
@@ -20,12 +20,17 @@ function requireScope(request, reply, scope) {
|
||||
}
|
||||
|
||||
/** Nur erlaubte Embed-Felder durchreichen (kein Blind-Passthrough) */
|
||||
function sanitizeEmbed(raw) {
|
||||
export function sanitizeEmbed(raw) {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const embed = {};
|
||||
for (const key of ['title', 'description', 'url', 'color', 'timestamp']) {
|
||||
if (raw[key] !== undefined) embed[key] = raw[key];
|
||||
}
|
||||
if (raw.author?.name) {
|
||||
embed.author = { name: String(raw.author.name).slice(0, 256) };
|
||||
if (raw.author.icon_url) embed.author.icon_url = String(raw.author.icon_url);
|
||||
if (raw.author.url) embed.author.url = String(raw.author.url);
|
||||
}
|
||||
if (raw.footer?.text) embed.footer = { text: String(raw.footer.text) };
|
||||
if (raw.image?.url) embed.image = { url: String(raw.image.url) };
|
||||
if (raw.thumbnail?.url) embed.thumbnail = { url: String(raw.thumbnail.url) };
|
||||
|
||||
+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 };
|
||||
|
||||
@@ -103,6 +103,7 @@ export function registerDevlogEndpoint(app, client) {
|
||||
.setDescription(prose)
|
||||
.setFooter({
|
||||
text: `${brandFooter('DEVLOG')}${commitCount ? ` • ${commitCount} Commits` : ''}`,
|
||||
iconURL: client?.user?.displayAvatarURL?.({ size: 64 }),
|
||||
})
|
||||
.setTimestamp();
|
||||
if (project) {
|
||||
|
||||
Reference in New Issue
Block a user