Connect-Links im Discord anklickbar, und Mods bekommen einen eigenen Knopf
Im Kanal stand woertlich das da:
[https://cfx.re/join/4odv68](https://cfx.re/join/4odv68)
Der Link war sein eigener Linktext. Discord macht aus der nackten URL zwischen
den eckigen Klammern zuerst selbst einen Link und kommt mit der
Masked-Link-Schreibweise dann nicht mehr durch — heraus kommen die Klammern im
Klartext. Der Kommentar an der Stelle behauptete ausserdem, so seien steam://
und Konsorten klickbar. Waren sie nie.
Jetzt sind es Link-Knoepfe unter dem Embed. Die Frage nach der Schreibweise
stellt sich damit gar nicht mehr, und ein Knopf sieht auch nach einem aus.
Discord nimmt dort nur http(s) an; fivem:// und steam:// stehen deshalb weiter
im Embed, aber als Codeblock zum Kopieren statt als toter Link.
Dazu ein eigenes Feld fuer den Mod-Download. Vorher landete der beim
Connect-Link, weil es kein anderes gab — und dann fuehrte "Verbinden" zu einer
ZIP-Datei. Farming Simulator liefert die Modliste ohnehin mit, also steht die
Anzahl jetzt auch im Embed und als Chip auf der Seite.
Beim Testen aufgefallen: eine neue Spalte hat jeden Aufrufer umgebracht, der
sie noch nicht kannte — better-sqlite3 wirft bei einem fehlenden benannten
Parameter. createGameserver und updateGameserver fuellen die Felder jetzt
einmal auf, statt das an jeder Aufrufstelle nachzuziehen.
Geprueft: was in einen Knopf darf und was nicht (https, http, fivem://,
steam://, leer, null, Leerzeichen, Satz mit Link drin), beide Knoepfe in einer
Reihe, nur einer, gar keiner — eine leere Reihe weist Discord ab —, https
landet im Knopf und nicht doppelt auch im Embed, fivem:// umgekehrt, Mods bei
47/0/null, und der Aufruf ohne die neue Spalte. Seite im laufenden Frontend
angesehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,7 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen.
|
|||||||
|
|
||||||
| Funktion | Beschreibung |
|
| Funktion | Beschreibung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 🎮 **Game-Server-Monitor** | DiscordGSM-Stil: pro Server ein Live-Embed (🟢/🔴, Spieler-Balken, Map, Version, Spieler-Liste, Connect-Link, Ping); **alle Spiele, die gamedig kennt** — die Auswahl im Panel kommt aus der Bibliothek selbst, inklusive Standard-Port und Hinweis auf nötige Zugangs-Codes (Farming Simulator, Terraria) — plus FiveM + HTTP-Check; Down/Up-Alerts mit Ausfalldauer |
|
| 🎮 **Game-Server-Monitor** | DiscordGSM-Stil: pro Server ein Live-Embed (🟢/🔴, Spieler-Balken, Map, Version, Mod-Anzahl, Spieler-Liste, Knöpfe für Verbinden und Mod-Download, Ping); **alle Spiele, die gamedig kennt** — die Auswahl im Panel kommt aus der Bibliothek selbst, inklusive Standard-Port und Hinweis auf nötige Zugangs-Codes (Farming Simulator, Terraria) — plus FiveM + HTTP-Check; Down/Up-Alerts mit Ausfalldauer |
|
||||||
| 🚨 **Wächter** | Prüft eigene Dienste in einstellbarem Takt, DM-Alarm bei Ausfall + Entwarnung; speist die öffentliche Statusseite |
|
| 🚨 **Wächter** | Prüft eigene Dienste in einstellbarem Takt, DM-Alarm bei Ausfall + Entwarnung; speist die öffentliche Statusseite |
|
||||||
| 💓 **Herzschlag** | Der Bot zeichnet sich selbst auf — im Dashboard steht neben der Laufzeit ein Erreichbarkeits-Balken über 7 Tage. Fehlende Stunden sind rot: dass nichts dasteht, *ist* die Aussage |
|
| 💓 **Herzschlag** | Der Bot zeichnet sich selbst auf — im Dashboard steht neben der Laufzeit ein Erreichbarkeits-Balken über 7 Tage. Fehlende Stunden sind rot: dass nichts dasteht, *ist* die Aussage |
|
||||||
| 🔊 **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 |
|
| 🔊 **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 |
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ export default {
|
|||||||
'server.agoDays': 'vor %s Tagen',
|
'server.agoDays': 'vor %s Tagen',
|
||||||
'server.neverOnline': 'Seit Beginn der Messung nicht erreichbar.',
|
'server.neverOnline': 'Seit Beginn der Messung nicht erreichbar.',
|
||||||
'server.version': 'Version',
|
'server.version': 'Version',
|
||||||
|
'server.mods': '%s Mods',
|
||||||
|
'server.modsDownload': 'Mods laden',
|
||||||
|
|
||||||
// ── Galerie ───────────────────────────────────────
|
// ── Galerie ───────────────────────────────────────
|
||||||
'gallery.tag': '// Community',
|
'gallery.tag': '// Community',
|
||||||
|
|||||||
@@ -189,6 +189,8 @@ export default {
|
|||||||
'server.agoDays': '%s days ago',
|
'server.agoDays': '%s days ago',
|
||||||
'server.neverOnline': 'Not reachable since measuring began.',
|
'server.neverOnline': 'Not reachable since measuring began.',
|
||||||
'server.version': 'Version',
|
'server.version': 'Version',
|
||||||
|
'server.mods': '%s mods',
|
||||||
|
'server.modsDownload': 'Get mods',
|
||||||
|
|
||||||
// ── Gallery ───────────────────────────────────────
|
// ── Gallery ───────────────────────────────────────
|
||||||
'gallery.tag': '// Community',
|
'gallery.tag': '// Community',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { apiGet } from '../api.js';
|
import { apiGet } from '../api.js';
|
||||||
import { SkeletonCards } from '../components/Skeleton.jsx';
|
import { SkeletonCards } from '../components/Skeleton.jsx';
|
||||||
import { IconGame, IconMapPin, IconClock, IconCheck, IconTeam, IconPackage, IconZap } from '../icons.jsx';
|
import { IconGame, IconMapPin, IconClock, IconCheck, IconTeam, IconPackage, IconZap, IconDownload, IconArchive } from '../icons.jsx';
|
||||||
import { useT, useFmt } from '../i18n.jsx';
|
import { useT, useFmt } from '../i18n.jsx';
|
||||||
import UptimeTrack from '../components/UptimeTrack.jsx';
|
import UptimeTrack from '../components/UptimeTrack.jsx';
|
||||||
|
|
||||||
@@ -130,6 +130,9 @@ function ServerCard({ s }) {
|
|||||||
<div className="srv-chips">
|
<div className="srv-chips">
|
||||||
{s.map && <span className="srv-chip"><IconMapPin size={12} /> {s.map}</span>}
|
{s.map && <span className="srv-chip"><IconMapPin size={12} /> {s.map}</span>}
|
||||||
{s.version && <span className="srv-chip"><IconPackage size={12} /> {s.version}</span>}
|
{s.version && <span className="srv-chip"><IconPackage size={12} /> {s.version}</span>}
|
||||||
|
{/* Eigenes Zeichen — mit dem Paket-Icon der Version daneben
|
||||||
|
sähen die beiden Chips wie zusammengehörig aus. */}
|
||||||
|
{s.mods > 0 && <span className="srv-chip"><IconArchive size={12} /> {t('server.mods', s.mods)}</span>}
|
||||||
{s.ping != null && <span className="srv-chip"><IconZap size={12} /> {s.ping} ms</span>}
|
{s.ping != null && <span className="srv-chip"><IconZap size={12} /> {s.ping} ms</span>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -177,13 +180,18 @@ function ServerCard({ s }) {
|
|||||||
|
|
||||||
<UptimeTrack buckets={s.uptime} />
|
<UptimeTrack buckets={s.uptime} />
|
||||||
|
|
||||||
{(s.connect_url || s.address) && (
|
{(s.connect_url || s.mods_url || s.address) && (
|
||||||
<div className="srv-connect">
|
<div className="srv-connect">
|
||||||
{s.connect_url && (
|
{s.connect_url && (
|
||||||
<a className="btn btn-save with-icon" href={s.connect_url}>
|
<a className="btn btn-save with-icon" href={s.connect_url}>
|
||||||
<IconGame size={15} /> {t('server.connect')}
|
<IconGame size={15} /> {t('server.connect')}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
{s.mods_url && (
|
||||||
|
<a className="btn btn-ghost with-icon" href={s.mods_url} rel="noreferrer">
|
||||||
|
<IconDownload size={15} /> {t('server.modsDownload')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
{s.address && (
|
{s.address && (
|
||||||
<button className="alpha-key srv-addr" onClick={copyAddress} title={t('server.copyAddress')}>
|
<button className="alpha-key srv-addr" onClick={copyAddress} title={t('server.copyAddress')}>
|
||||||
{copied ? <span className="with-icon"><IconCheck size={14} /> {t('server.copied')}</span> : s.address}
|
{copied ? <span className="with-icon"><IconCheck size={14} /> {t('server.copied')}</span> : s.address}
|
||||||
|
|||||||
@@ -396,7 +396,7 @@ export default function Settings({ me }) {
|
|||||||
const [triggers, setTriggers] = useState([]);
|
const [triggers, setTriggers] = useState([]);
|
||||||
const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' });
|
const [triggerDraft, setTriggerDraft] = useState({ keyword: '', reply: '' });
|
||||||
// Game-Server — die Spielliste kommt aus gamedig (siehe /api/gamedig-games)
|
// Game-Server — die Spielliste kommt aus gamedig (siehe /api/gamedig-games)
|
||||||
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '', image_url: '', token: '' };
|
const emptyServer = { id: null, name: '', type: 'fivem', query_url: '', address: '', host: '', port: '', connect_url: '', image_url: '', token: '', mods_url: '' };
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [games, setGames] = useState([]);
|
const [games, setGames] = useState([]);
|
||||||
const [serverDraft, setServerDraft] = useState(emptyServer);
|
const [serverDraft, setServerDraft] = useState(emptyServer);
|
||||||
@@ -3343,7 +3343,7 @@ export default function Settings({ me }) {
|
|||||||
<button className="btn btn-mini" onClick={() => setServerDraft({
|
<button className="btn btn-mini" onClick={() => setServerDraft({
|
||||||
id: s.id, name: s.name, type: s.type, query_url: s.query_url ?? '',
|
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 ?? '',
|
address: s.address ?? '', host: s.host ?? '', port: s.port ?? '', connect_url: s.connect_url ?? '',
|
||||||
image_url: s.image_url ?? '', token: s.token ?? '',
|
image_url: s.image_url ?? '', token: s.token ?? '', mods_url: s.mods_url ?? '',
|
||||||
})}>Bearbeiten</button>
|
})}>Bearbeiten</button>
|
||||||
<button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}>✕</button>
|
<button className="card-delete" title="Server + Embed entfernen" onClick={() => removeServer(s.id)}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -3396,11 +3396,13 @@ export default function Settings({ me }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="field">
|
<div className="field">
|
||||||
<label>Connect-Link + Anzeige-Adresse <span className="field-hint">Link ist im Embed klickbar (auch steam:// / fivem://); Adresse als Copy-Paste-Block</span></label>
|
<label>Connect-Link + Anzeige-Adresse <span className="field-hint">https wird zum Knopf; steam:// / fivem:// stehen als Codeblock zum Kopieren. Adresse ebenfalls als Copy-Paste-Block</span></label>
|
||||||
<div className="field-row">
|
<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="https://cfx.re/join/abc123" 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 })} />
|
<input type="text" placeholder="connect play.d4rkst3r.de" value={serverDraft.address} onChange={(e) => setServerDraft({ ...serverDraft, address: e.target.value })} />
|
||||||
</div>
|
</div>
|
||||||
|
<label style={{ marginTop: '.6rem' }}>Mod-Download <span className="field-hint">eigener Knopf neben „Verbinden" — bei LS zum Beispiel <code>http://host:port/all_mods_download?onlyActive=true</code>. Nur http(s), sonst nimmt Discord den Knopf nicht.</span></label>
|
||||||
|
<input type="text" placeholder="http://host:8200/all_mods_download?onlyActive=true" value={serverDraft.mods_url} onChange={(e) => setServerDraft({ ...serverDraft, mods_url: e.target.value })} />
|
||||||
<label style={{ marginTop: '.6rem' }}>Logo-URL <span className="field-hint">https-Bild (z. B. vom CDN) — erscheint auf der /server-Seite und als Embed-Thumbnail</span></label>
|
<label style={{ marginTop: '.6rem' }}>Logo-URL <span className="field-hint">https-Bild (z. B. vom CDN) — erscheint auf der /server-Seite und als Embed-Thumbnail</span></label>
|
||||||
<input type="text" placeholder="https://cdn.d4rkst3r.de/logos/eco.png" value={serverDraft.image_url} onChange={(e) => setServerDraft({ ...serverDraft, image_url: e.target.value })} />
|
<input type="text" placeholder="https://cdn.d4rkst3r.de/logos/eco.png" value={serverDraft.image_url} onChange={(e) => setServerDraft({ ...serverDraft, image_url: e.target.value })} />
|
||||||
<div className="field-row" style={{ marginTop: '.6rem' }}>
|
<div className="field-row" style={{ marginTop: '.6rem' }}>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Game-Server-Monitor v3 (DiscordGSM-Stil): gamedig-Queries für 300+ Spiele,
|
// Game-Server-Monitor v3 (DiscordGSM-Stil): gamedig-Queries für 300+ Spiele,
|
||||||
// pro Server ein Live-Embed (Spieler-Balken, Map, Player-Liste, Connect-Link),
|
// pro Server ein Live-Embed (Spieler-Balken, Map, Player-Liste, Connect-Link),
|
||||||
// Down/Up-Alerts mit 2-Fails-Schwelle. Verwaltet im Setup-Tab "Server".
|
// Down/Up-Alerts mit 2-Fails-Schwelle. Verwaltet im Setup-Tab "Server".
|
||||||
import { ActivityType, EmbedBuilder, Events } from 'discord.js';
|
import { ActionRowBuilder, ActivityType, ButtonBuilder, ButtonStyle, EmbedBuilder, Events } from 'discord.js';
|
||||||
import { GameDig, games as gamedigGames } from 'gamedig';
|
import { GameDig, games as gamedigGames } from 'gamedig';
|
||||||
import { getSetting, listGameservers, setGameserverMessage, recordPlayerSample, prunePlayerHistory } from '../db.js';
|
import { getSetting, listGameservers, setGameserverMessage, recordPlayerSample, prunePlayerHistory } from '../db.js';
|
||||||
import { brandFooter, botStatusText } from '../runtime-settings.js';
|
import { brandFooter, botStatusText } from '../runtime-settings.js';
|
||||||
@@ -75,7 +75,7 @@ export const lastResults = new Map(); // id → Query-Ergebnis + checkedAt
|
|||||||
export async function queryServer(server) {
|
export async function queryServer(server) {
|
||||||
const base = {
|
const base = {
|
||||||
...server, online: false, players: null, max: null, map: null,
|
...server, online: false, players: null, max: null, map: null,
|
||||||
playerNames: [], ping: null, version: null,
|
playerNames: [], ping: null, version: null, mods: null,
|
||||||
};
|
};
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
try {
|
try {
|
||||||
@@ -118,6 +118,9 @@ export async function queryServer(server) {
|
|||||||
max: state.maxplayers ?? null,
|
max: state.maxplayers ?? null,
|
||||||
map: state.map || null,
|
map: state.map || null,
|
||||||
version: state.version || null,
|
version: state.version || null,
|
||||||
|
// Nur Farming Simulator liefert die Modliste mit; anderswo bleibt
|
||||||
|
// das Feld leer statt auf 0 zu stehen und eine Aussage zu machen.
|
||||||
|
mods: Array.isArray(state.raw?.mods) ? state.raw.mods.length : null,
|
||||||
playerNames: (state.players ?? []).map((p) => p?.name).filter(Boolean).slice(0, 15),
|
playerNames: (state.players ?? []).map((p) => p?.name).filter(Boolean).slice(0, 15),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
@@ -125,6 +128,38 @@ export async function queryServer(server) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Nur http(s) darf in einen Link-Knopf — Discord weist alles andere ab */
|
||||||
|
export function istWebLink(url) {
|
||||||
|
return /^https?:\/\/\S+$/i.test(String(url ?? '').trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die klickbaren Links unter dem Embed.
|
||||||
|
*
|
||||||
|
* Vorher stand der Connect-Link als `[url](url)` im Embed-Feld. Discord macht
|
||||||
|
* aus der nackten URL zwischen den Klammern selbst einen Link und kommt mit
|
||||||
|
* der Masked-Link-Schreibweise dann nicht mehr durch — im Kanal standen die
|
||||||
|
* eckigen Klammern wörtlich da. Ein Knopf lässt diese Frage gar nicht erst
|
||||||
|
* aufkommen und ist obendrein als Knopf erkennbar.
|
||||||
|
*/
|
||||||
|
export function serverKnoepfe(r) {
|
||||||
|
const knoepfe = [
|
||||||
|
['🎮 Verbinden', r.connect_url],
|
||||||
|
['📦 Mods laden', r.mods_url],
|
||||||
|
]
|
||||||
|
.filter(([, url]) => istWebLink(url))
|
||||||
|
.map(([label, url]) => new ButtonBuilder()
|
||||||
|
.setStyle(ButtonStyle.Link)
|
||||||
|
.setLabel(label)
|
||||||
|
.setURL(url.trim()));
|
||||||
|
return knoepfe.length > 0 ? [new ActionRowBuilder().addComponents(knoepfe)] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Embed + Knöpfe — das, was der Monitor in den Kanal schreibt */
|
||||||
|
export function buildServerNachricht(r) {
|
||||||
|
return { embeds: [buildServerEmbed(r)], components: serverKnoepfe(r) };
|
||||||
|
}
|
||||||
|
|
||||||
/** GSM-Style-Embed für ein Query-Ergebnis */
|
/** GSM-Style-Embed für ein Query-Ergebnis */
|
||||||
export function buildServerEmbed(r) {
|
export function buildServerEmbed(r) {
|
||||||
const icon = GAME_ICONS[r.type] ?? '🎮';
|
const icon = GAME_ICONS[r.type] ?? '🎮';
|
||||||
@@ -152,9 +187,14 @@ export function buildServerEmbed(r) {
|
|||||||
}
|
}
|
||||||
if (r.map) embed.addFields({ name: 'Map', value: r.map, inline: true });
|
if (r.map) embed.addFields({ name: 'Map', value: r.map, inline: true });
|
||||||
if (r.version) embed.addFields({ name: 'Version', value: String(r.version).slice(0, 60), inline: true });
|
if (r.version) embed.addFields({ name: 'Version', value: String(r.version).slice(0, 60), inline: true });
|
||||||
if (r.connect_url) {
|
if (r.mods != null) embed.addFields({ name: 'Mods', value: String(r.mods), inline: true });
|
||||||
// steam:// & Co. sind in Embeds klickbar (Link-Buttons erlauben nur https)
|
// Connect und Mods stehen als Knopf darunter, wenn es https ist. Alles
|
||||||
embed.addFields({ name: 'Connect', value: `[${r.connect_url}](${r.connect_url})`, inline: false });
|
// andere (fivem://, steam://) macht Discord nicht klickbar — dann lieber
|
||||||
|
// als Codeblock zum Kopieren, statt einen toten Link vorzugaukeln.
|
||||||
|
for (const [titel, url] of [['Connect', r.connect_url], ['Mods', r.mods_url]]) {
|
||||||
|
if (url && !istWebLink(url)) {
|
||||||
|
embed.addFields({ name: titel, value: `\`\`\`\n${url}\n\`\`\`` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (r.address) embed.addFields({ name: 'Adresse', value: `\`\`\`\n${r.address}\n\`\`\`` });
|
if (r.address) embed.addFields({ name: 'Adresse', value: `\`\`\`\n${r.address}\n\`\`\`` });
|
||||||
if (r.online && r.playerNames.length > 0) {
|
if (r.online && r.playerNames.length > 0) {
|
||||||
@@ -241,7 +281,7 @@ export async function monitorTick(client) {
|
|||||||
if (!channel?.isTextBased()) return;
|
if (!channel?.isTextBased()) return;
|
||||||
|
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
const payload = { embeds: [buildServerEmbed(r)] };
|
const payload = buildServerNachricht(r);
|
||||||
try {
|
try {
|
||||||
if (r.message_id) {
|
if (r.message_id) {
|
||||||
const existing = await channel.messages.fetch(r.message_id).catch(() => null);
|
const existing = await channel.messages.fetch(r.message_id).catch(() => null);
|
||||||
|
|||||||
@@ -924,6 +924,9 @@ db.exec(`
|
|||||||
if (!cols.includes('image_url')) db.exec(`ALTER TABLE gameservers ADD COLUMN image_url TEXT NOT NULL DEFAULT ''`);
|
if (!cols.includes('image_url')) db.exec(`ALTER TABLE gameservers ADD COLUMN image_url TEXT NOT NULL DEFAULT ''`);
|
||||||
// Zugangs-Code für Spiele, die ohne nicht antworten (Farming Simulator, Terraria)
|
// Zugangs-Code für Spiele, die ohne nicht antworten (Farming Simulator, Terraria)
|
||||||
if (!cols.includes('token')) db.exec(`ALTER TABLE gameservers ADD COLUMN token TEXT NOT NULL DEFAULT ''`);
|
if (!cols.includes('token')) db.exec(`ALTER TABLE gameservers ADD COLUMN token TEXT NOT NULL DEFAULT ''`);
|
||||||
|
// Eigener Link für den Mod-Download. Vorher landete der beim Connect-Link,
|
||||||
|
// weil es kein anderes Feld gab — und dann führte "Connect" zu einer ZIP.
|
||||||
|
if (!cols.includes('mods_url')) db.exec(`ALTER TABLE gameservers ADD COLUMN mods_url TEXT NOT NULL DEFAULT ''`);
|
||||||
}
|
}
|
||||||
// Die Auswahl im Panel bot mal Spiel-IDs an, die es bei gamedig gar nicht gibt.
|
// Die Auswahl im Panel bot mal Spiel-IDs an, die es bei gamedig gar nicht gibt.
|
||||||
// Wer eine davon gewählt hat, hatte einen Server, der dauerhaft offline stand,
|
// Wer eine davon gewählt hat, hatte einen Server, der dauerhaft offline stand,
|
||||||
@@ -937,20 +940,29 @@ db.exec(`
|
|||||||
for (const [alt, neu] of Object.entries(echteIds)) um.run(neu, alt);
|
for (const [alt, neu] of Object.entries(echteIds)) um.run(neu, alt);
|
||||||
}
|
}
|
||||||
const insertGameserver = db.prepare(`
|
const insertGameserver = db.prepare(`
|
||||||
INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url, image_url, token)
|
INSERT INTO gameservers (name, type, query_url, address, host, port, connect_url, image_url, token, mods_url)
|
||||||
VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url, @image_url, @token)
|
VALUES (@name, @type, @query_url, @address, @host, @port, @connect_url, @image_url, @token, @mods_url)
|
||||||
`);
|
`);
|
||||||
const updateGameserverStmt = db.prepare(`
|
const updateGameserverStmt = db.prepare(`
|
||||||
UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address,
|
UPDATE gameservers SET name = @name, type = @type, query_url = @query_url, address = @address,
|
||||||
host = @host, port = @port, connect_url = @connect_url, image_url = @image_url, token = @token
|
host = @host, port = @port, connect_url = @connect_url, image_url = @image_url, token = @token,
|
||||||
|
mods_url = @mods_url
|
||||||
WHERE id = @id
|
WHERE id = @id
|
||||||
`);
|
`);
|
||||||
const setGameserverMessageStmt = db.prepare('UPDATE gameservers SET message_id = ? WHERE id = ?');
|
const setGameserverMessageStmt = db.prepare('UPDATE gameservers SET message_id = ? WHERE id = ?');
|
||||||
const listGameserversStmt = db.prepare('SELECT * FROM gameservers ORDER BY id');
|
const listGameserversStmt = db.prepare('SELECT * FROM gameservers ORDER BY id');
|
||||||
const getGameserverStmt = db.prepare('SELECT * FROM gameservers WHERE id = ?');
|
const getGameserverStmt = db.prepare('SELECT * FROM gameservers WHERE id = ?');
|
||||||
const deleteGameserverStmt = db.prepare('DELETE FROM gameservers WHERE id = ?');
|
const deleteGameserverStmt = db.prepare('DELETE FROM gameservers WHERE id = ?');
|
||||||
export const createGameserver = (s) => insertGameserver.run(s).lastInsertRowid;
|
// Jede neue Spalte bricht sonst jeden Aufrufer, der sie noch nicht kennt —
|
||||||
export const updateGameserver = (s) => updateGameserverStmt.run(s).changes > 0;
|
// better-sqlite3 wirft bei einem fehlenden benannten Parameter. Hier einmal
|
||||||
|
// auffüllen ist billiger, als das an jeder Aufrufstelle nachzuziehen.
|
||||||
|
const gameserverFelder = {
|
||||||
|
name: '', type: 'fivem', query_url: '', address: '', host: '', port: null,
|
||||||
|
connect_url: '', image_url: '', token: '', mods_url: '',
|
||||||
|
};
|
||||||
|
const vollstaendig = (s) => ({ ...gameserverFelder, ...s });
|
||||||
|
export const createGameserver = (s) => insertGameserver.run(vollstaendig(s)).lastInsertRowid;
|
||||||
|
export const updateGameserver = (s) => updateGameserverStmt.run(vollstaendig(s)).changes > 0;
|
||||||
export const setGameserverMessage = (id, messageId) => setGameserverMessageStmt.run(messageId, id);
|
export const setGameserverMessage = (id, messageId) => setGameserverMessageStmt.run(messageId, id);
|
||||||
export const listGameservers = () => listGameserversStmt.all();
|
export const listGameservers = () => listGameserversStmt.all();
|
||||||
export const getGameserver = (id) => getGameserverStmt.get(id) ?? null;
|
export const getGameserver = (id) => getGameserverStmt.get(id) ?? null;
|
||||||
|
|||||||
@@ -225,11 +225,13 @@ export function registerApiRoutes(app, client) {
|
|||||||
image_url: s.image_url || null,
|
image_url: s.image_url || null,
|
||||||
address: s.address || null,
|
address: s.address || null,
|
||||||
connect_url: s.connect_url || null,
|
connect_url: s.connect_url || null,
|
||||||
|
mods_url: s.mods_url || null,
|
||||||
online: r?.online ?? null, // null = noch nie gecheckt
|
online: r?.online ?? null, // null = noch nie gecheckt
|
||||||
players: r?.players ?? null,
|
players: r?.players ?? null,
|
||||||
max: r?.max ?? null,
|
max: r?.max ?? null,
|
||||||
map: r?.map ?? null,
|
map: r?.map ?? null,
|
||||||
version: r?.version ?? null,
|
version: r?.version ?? null,
|
||||||
|
mods: r?.mods ?? null,
|
||||||
ping: r?.ping ?? null,
|
ping: r?.ping ?? null,
|
||||||
// Namen im Spiel, so wie sie im Discord-Embed auch stehen
|
// Namen im Spiel, so wie sie im Discord-Embed auch stehen
|
||||||
spielerNamen: r?.online ? (r.playerNames ?? []) : [],
|
spielerNamen: r?.online ? (r.playerNames ?? []) : [],
|
||||||
@@ -1576,6 +1578,11 @@ ${rssItems}
|
|||||||
? String(body.image_url).trim().slice(0, 300)
|
? String(body.image_url).trim().slice(0, 300)
|
||||||
: '',
|
: '',
|
||||||
token: String(body.token ?? '').trim().slice(0, 200),
|
token: String(body.token ?? '').trim().slice(0, 200),
|
||||||
|
// Nur http(s) — daraus wird ein Link-Knopf, und Discord weist
|
||||||
|
// alles andere ab. Unbrauchbares lieber hier verwerfen.
|
||||||
|
mods_url: /^https?:\/\/.+/i.test(String(body.mods_url ?? '').trim())
|
||||||
|
? String(body.mods_url).trim().slice(0, 300)
|
||||||
|
: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user