XP-Beträge, Wartezeiten, Prüf-Intervalle, Obergrenzen und Postzeiten
standen als Konstanten im Code. Wer den Server-Monitor seltener prüfen
lassen wollte oder das Level-System anders austarieren, musste die
Quelldateien anfassen.
src/tuning.js hält sie jetzt an einer Stelle, mit Standard, erlaubtem
Bereich und Erklärung. Der neue Config-Bereich „Werte" zeigt sie nach
Themen gruppiert; neben jedem Feld steht der erlaubte Bereich und der
Standard, angepasste Werte lassen sich einzeln zurücksetzen.
Die Grenzen sind nicht Kosmetik: ein Vertipper beim Prüf-Intervall würde
sonst den Bot in eine Schleife im Sekundentakt schicken. Gespeichert wird
ganz oder gar nicht — ein ungültiger Wert im Formular lässt auch die
gültigen daneben unverändert, statt die Hälfte zu schreiben.
Intervalle laufen nicht mehr über setInterval mit festem Abstand, sondern
über everyTuned: der Wert wird vor jeder Runde neu gelesen. Ein geändertes
Intervall greift damit ab dem nächsten Durchlauf, ohne Neustart. Ein
Fehler in einer Runde beendet die Schleife nicht.
Beim Testen aufgefallen und behoben: Number('') ist 0 und nicht NaN — ein
nie gesetzter Wert wäre dadurch auf sein Minimum gefallen statt auf den
Standard. Das Level-System hätte also 1 XP pro Nachricht vergeben und der
Monitor jede Minute geprüft.
Der Fetch-Wrapper im Frontend hat die Fehlermeldung des Servers verworfen;
jetzt steht im Panel „XP pro Nachricht: 1–500 XP" statt „fehlgeschlagen".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
5.3 KiB
JavaScript
138 lines
5.3 KiB
JavaScript
// Community-Features: Starboard (⭐ → Best-of-Kanal) + Screenshot-Galerie
|
|
import { mkdirSync } from 'node:fs';
|
|
import { writeFile, rm } from 'node:fs/promises';
|
|
import { join, dirname, resolve } from 'node:path';
|
|
import { EmbedBuilder, Events } from 'discord.js';
|
|
import {
|
|
hasStarboard, saveStarboard,
|
|
saveGalleryItem, hasGalleryItem, deleteGalleryItem,
|
|
} from '../db.js';
|
|
import { starboardChannelId, starboardThreshold, screenshotChannelId, brandColor } from '../runtime-settings.js';
|
|
import { moduleEnabled } from '../modules.js';
|
|
import { tuning } from '../tuning.js';
|
|
import { config } from '../config.js';
|
|
|
|
|
|
// Galerie-Bilder liegen neben der SQLite: data/gallery/
|
|
export const galleryDir = join(dirname(resolve(config.dbPath)), 'gallery');
|
|
mkdirSync(galleryDir, { recursive: true });
|
|
|
|
/* ── Screenshot-Galerie ────────────────────────────── */
|
|
|
|
/**
|
|
* Nachricht aus dem Screenshot-Kanal archivieren (Bilder lokal speichern).
|
|
* @returns {Promise<boolean>} true, wenn neu gespeichert
|
|
*/
|
|
export async function archiveGalleryMessage(message) {
|
|
if (!moduleEnabled('gallery')) return;
|
|
if (hasGalleryItem(message.id)) return false;
|
|
|
|
const files = [];
|
|
for (const att of message.attachments?.values?.() ?? []) {
|
|
if (files.length >= tuning('gallery_max_images')) break;
|
|
if (!att.contentType?.startsWith('image/')) continue;
|
|
const ext = (att.name?.split('.').pop() || 'png').toLowerCase().replace(/[^a-z0-9]/g, '') || 'png';
|
|
const file = `${message.id}_${files.length}.${ext}`;
|
|
try {
|
|
const res = await fetch(att.url);
|
|
if (!res.ok) continue;
|
|
await writeFile(join(galleryDir, file), Buffer.from(await res.arrayBuffer()));
|
|
files.push(file);
|
|
} catch (error) {
|
|
console.error(`[galerie] Bild-Download fehlgeschlagen: ${error.message}`);
|
|
}
|
|
}
|
|
if (files.length === 0) return false;
|
|
|
|
return saveGalleryItem({
|
|
message_id: message.id,
|
|
author_name: message.member?.displayName ?? message.author?.username ?? 'unbekannt',
|
|
images: JSON.stringify(files),
|
|
posted_at: message.createdAt.toISOString(),
|
|
});
|
|
}
|
|
|
|
/** Galerie-Eintrag inkl. Bild-Dateien entfernen */
|
|
export async function removeGalleryMessage(messageId) {
|
|
const { deleted, images } = deleteGalleryItem(messageId);
|
|
for (const file of images) {
|
|
await rm(join(galleryDir, file), { force: true });
|
|
}
|
|
return deleted;
|
|
}
|
|
|
|
/* ── Starboard ─────────────────────────────────────── */
|
|
|
|
async function handleStarReaction(reaction) {
|
|
if (!moduleEnabled('starboard')) return;
|
|
const boardChannelId = starboardChannelId();
|
|
if (!boardChannelId) return;
|
|
if (reaction.emoji.name !== '⭐') return;
|
|
|
|
if (reaction.partial) await reaction.fetch();
|
|
let message = reaction.message;
|
|
if (message.partial) message = await message.fetch();
|
|
|
|
// Nicht das Starboard selbst boarden, Dedupe, Schwellwert
|
|
if (message.channelId === boardChannelId) return;
|
|
if (hasStarboard(message.id)) return;
|
|
if ((reaction.count ?? 0) < starboardThreshold()) return;
|
|
|
|
const board = await message.client.channels.fetch(boardChannelId);
|
|
if (!board?.isTextBased()) return;
|
|
|
|
const image = [...message.attachments.values()].find((a) =>
|
|
a.contentType?.startsWith('image/')
|
|
);
|
|
const embed = new EmbedBuilder()
|
|
.setColor(brandColor())
|
|
.setAuthor({
|
|
name: message.member?.displayName ?? message.author?.username ?? 'unbekannt',
|
|
iconURL: message.author?.displayAvatarURL?.() ?? undefined,
|
|
})
|
|
.setDescription(
|
|
`${(message.content || '').slice(0, 1800) || '*[Bild]*'}\n\n[→ Zum Original](${message.url})`
|
|
)
|
|
.setFooter({ text: `⭐ ${reaction.count} • #${message.channel?.name ?? ''}` })
|
|
.setTimestamp(message.createdAt);
|
|
if (image) embed.setImage(image.url);
|
|
|
|
const boardMessage = await board.send({ embeds: [embed] });
|
|
saveStarboard(message.id, boardMessage.id);
|
|
console.log(`[starboard] ${message.id} geboardet (${reaction.count} ⭐)`);
|
|
}
|
|
|
|
/* ── Listener-Registrierung ────────────────────────── */
|
|
|
|
export function registerCommunityListeners(client) {
|
|
client.on(Events.MessageReactionAdd, async (reaction) => {
|
|
try {
|
|
await handleStarReaction(reaction);
|
|
} catch (error) {
|
|
console.error('[starboard] Fehler:', error);
|
|
}
|
|
});
|
|
|
|
client.on(Events.MessageCreate, async (message) => {
|
|
if (message.channelId !== screenshotChannelId()) return;
|
|
try {
|
|
if (await archiveGalleryMessage(message)) {
|
|
console.log(`[galerie] Screenshot archiviert (${message.id})`);
|
|
await message.react('⭐').catch(() => {});
|
|
}
|
|
} catch (error) {
|
|
console.error('[galerie] Archivierung fehlgeschlagen:', error);
|
|
}
|
|
});
|
|
|
|
client.on(Events.MessageDelete, async (message) => {
|
|
try {
|
|
if (await removeGalleryMessage(message.id)) {
|
|
console.log(`[galerie] Eintrag entfernt (${message.id})`);
|
|
}
|
|
} catch (error) {
|
|
console.error('[galerie] Löschung fehlgeschlagen:', error);
|
|
}
|
|
});
|
|
}
|