Ticket-System, Web-Composer und Nachrichten-Bearbeitung
- Tickets: /ticket-setup postet 🎫-Button; Klick erstellt privaten Thread (ein offenes Ticket pro User), Schließen-Button sperrt + archiviert - Composer auf der Setup-Seite: Nachricht/Embed als Bot in beliebigen Kanal senden oder per Message-ID bearbeiten (nur eigene Bot-Posts) - API v1: PATCH /api/v1/message — Skripte können ihre Bot-Posts aktualisieren - Setting ticket_channel_id; README ergänzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,7 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St
|
|||||||
| ⭐ **Starboard** | Nachrichten mit genug ⭐-Reaktionen landen automatisch im Best-of-Kanal |
|
| ⭐ **Starboard** | Nachrichten mit genug ⭐-Reaktionen landen automatisch im Best-of-Kanal |
|
||||||
| 📸 **Screenshot-Galerie** | Bilder aus dem Screenshot-Kanal → öffentliche `/galerie` (lokal gespeichert); Historie via `/galerie-backfill` |
|
| 📸 **Screenshot-Galerie** | Bilder aus dem Screenshot-Kanal → öffentliche `/galerie` (lokal gespeichert); Historie via `/galerie-backfill` |
|
||||||
| 📬 **Modmail** | DM an den Bot → Thread im privaten Staff-Kanal; Antworten im Thread gehen als DM zurück |
|
| 📬 **Modmail** | DM an den Bot → Thread im privaten Staff-Kanal; Antworten im Thread gehen als DM zurück |
|
||||||
|
| 🎫 **Tickets** | `/ticket-setup` postet den Button; Klick öffnet einen privaten Thread (ein offenes Ticket pro User, Schließen-Button) |
|
||||||
| 👋 **Willkommens-Embed** | Begrüßung neuer Member im Brand-Look (braucht Server-Members-Intent) |
|
| 👋 **Willkommens-Embed** | Begrüßung neuer Member im Brand-Look (braucht Server-Members-Intent) |
|
||||||
| 📋 **Mod-Log** | Gelöschte/bearbeitete Nachrichten in einen privaten Log-Kanal |
|
| 📋 **Mod-Log** | Gelöschte/bearbeitete Nachrichten in einen privaten Log-Kanal |
|
||||||
| 🎮 **Server-Monitor** | FiveM-kompatible Game-Server (/dynamic.json): Live-Status-Embed + Spielerzahl in der Bot-Presence |
|
| 🎮 **Server-Monitor** | FiveM-kompatible Game-Server (/dynamic.json): Live-Status-Embed + Spielerzahl in der Bot-Presence |
|
||||||
@@ -202,6 +203,7 @@ Authorization: Bearer d4rk_<key>
|
|||||||
| Endpoint | Scope | Body / Antwort |
|
| Endpoint | Scope | Body / Antwort |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `POST /api/v1/message` | `message` | `{ channel_id, content?, embed? }` → postet als Bot (Embed: title, description, color, url, image, thumbnail, footer, fields) |
|
| `POST /api/v1/message` | `message` | `{ channel_id, content?, embed? }` → postet als Bot (Embed: title, description, color, url, image, thumbnail, footer, fields) |
|
||||||
|
| `PATCH /api/v1/message` | `message` | `{ channel_id, message_id, content?, embed? }` → eigenen Bot-Post bearbeiten |
|
||||||
| `POST /api/v1/dm` | `dm` | `{ user_id, content }` → Direktnachricht |
|
| `POST /api/v1/dm` | `dm` | `{ user_id, content }` → Direktnachricht |
|
||||||
| `POST /api/v1/roles` | `roles` | `{ user_id, role_id, action: "add"\|"remove" }` → Rolle vergeben (z. B. Shop-Kauf → Kunden-Rolle) |
|
| `POST /api/v1/roles` | `roles` | `{ user_id, role_id, action: "add"\|"remove" }` → Rolle vergeben (z. B. Shop-Kauf → Kunden-Rolle) |
|
||||||
| `GET /api/v1/member/:id` | `read` | Member-Info: Name, Rollen, Beitritt — für Login-/Berechtigungs-Checks |
|
| `GET /api/v1/member/:id` | `read` | Member-Info: Name, Rollen, Beitritt — für Login-/Berechtigungs-Checks |
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default function Settings({ me }) {
|
|||||||
const [feedback, setFeedback] = useState(null);
|
const [feedback, setFeedback] = useState(null);
|
||||||
// Community
|
// Community
|
||||||
const [playtesters, setPlaytesters] = useState([]);
|
const [playtesters, setPlaytesters] = useState([]);
|
||||||
|
// Composer
|
||||||
|
const [composer, setComposer] = useState({ channel: '', title: '', text: '', messageId: '' });
|
||||||
// API-Keys
|
// API-Keys
|
||||||
const [keys, setKeys] = useState([]);
|
const [keys, setKeys] = useState([]);
|
||||||
const [newKeyName, setNewKeyName] = useState('');
|
const [newKeyName, setNewKeyName] = useState('');
|
||||||
@@ -80,6 +82,25 @@ export default function Settings({ me }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function sendTest(target) {
|
async function sendTest(target) {
|
||||||
try {
|
try {
|
||||||
await apiPost(`/api/settings/test/${target}`);
|
await apiPost(`/api/settings/test/${target}`);
|
||||||
@@ -400,6 +421,59 @@ export default function Settings({ me }) {
|
|||||||
{channelOptions}
|
{channelOptions}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<label>Ticket-Kanal <span className="field-hint">hier entstehen die privaten Ticket-Threads; Post via /ticket-setup</span></label>
|
||||||
|
<select
|
||||||
|
value={form.ticket_channel_id ?? ''}
|
||||||
|
onChange={(e) => setForm({ ...form, ticket_channel_id: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">— deaktiviert —</option>
|
||||||
|
{channelOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Composer */}
|
||||||
|
<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={6}
|
||||||
|
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 statt neu senden</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" onClick={sendCompose}>
|
||||||
|
{composer.messageId.trim() ? 'Aktualisieren' : 'Senden'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Game-Server */}
|
{/* Game-Server */}
|
||||||
|
|||||||
@@ -586,6 +586,20 @@ a.sha:hover { border-color: var(--neon); background: rgba(245, 197, 24, .07); }
|
|||||||
.field-row { display: flex; gap: .6rem; }
|
.field-row { display: flex; gap: .6rem; }
|
||||||
.field-row select { flex: 1; }
|
.field-row select { flex: 1; }
|
||||||
|
|
||||||
|
.field textarea {
|
||||||
|
width: 100%;
|
||||||
|
background: #0d0d0d;
|
||||||
|
border: 1px solid rgba(255, 255, 255, .12);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--body);
|
||||||
|
font-size: .95rem;
|
||||||
|
padding: .5rem .7rem;
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
transition: border-color .2s;
|
||||||
|
}
|
||||||
|
.field textarea:focus { border-color: var(--neon); }
|
||||||
|
|
||||||
.field select, .field input[type="text"] {
|
.field select, .field input[type="text"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #0d0d0d;
|
background: #0d0d0d;
|
||||||
|
|||||||
+81
-3
@@ -1,11 +1,16 @@
|
|||||||
// Discord-Client: Commands laden, registrieren und Interactions verarbeiten
|
// Discord-Client: Commands laden, registrieren und Interactions verarbeiten
|
||||||
import { Client, Collection, Events, GatewayIntentBits, MessageFlags, Partials, REST, Routes } from 'discord.js';
|
import { Client, Collection, Events, GatewayIntentBits, MessageFlags, Partials, REST, Routes } from 'discord.js';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
import { devlogChannelId, devlogPingRoleId, playtesterRoleId } from '../runtime-settings.js';
|
import { devlogChannelId, devlogPingRoleId, playtesterRoleId, ticketChannelId } from '../runtime-settings.js';
|
||||||
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
|
||||||
import { registerCommunityListeners } from './community.js';
|
import { registerCommunityListeners } from './community.js';
|
||||||
import { registerModTools } from './mod-tools.js';
|
import { registerModTools } from './mod-tools.js';
|
||||||
import { addPlaytester, removePlaytester, isWish, bumpWish, getGiveaway, toggleGiveawayEntry, giveawayEntries } from '../db.js';
|
import {
|
||||||
|
addPlaytester, removePlaytester, isWish, bumpWish,
|
||||||
|
getGiveaway, toggleGiveawayEntry, giveawayEntries,
|
||||||
|
saveTicket, getTicket, closeTicket, openTicketOf,
|
||||||
|
} from '../db.js';
|
||||||
|
import { ChannelType, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
|
||||||
import * as ping from './commands/ping.js';
|
import * as ping from './commands/ping.js';
|
||||||
import * as devlogBackfill from './commands/devlog-backfill.js';
|
import * as devlogBackfill from './commands/devlog-backfill.js';
|
||||||
import * as bug from './commands/bug.js';
|
import * as bug from './commands/bug.js';
|
||||||
@@ -13,9 +18,10 @@ import * as playtesterSetup from './commands/playtester-setup.js';
|
|||||||
import * as galerieBackfill from './commands/galerie-backfill.js';
|
import * as galerieBackfill from './commands/galerie-backfill.js';
|
||||||
import * as wunsch from './commands/wunsch.js';
|
import * as wunsch from './commands/wunsch.js';
|
||||||
import * as giveaway from './commands/giveaway.js';
|
import * as giveaway from './commands/giveaway.js';
|
||||||
|
import * as ticketSetup from './commands/ticket-setup.js';
|
||||||
|
|
||||||
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
|
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
|
||||||
const commandModules = [ping, devlogBackfill, bug, playtesterSetup, galerieBackfill, wunsch, giveaway];
|
const commandModules = [ping, devlogBackfill, bug, playtesterSetup, galerieBackfill, wunsch, giveaway, ticketSetup];
|
||||||
|
|
||||||
export async function startBot() {
|
export async function startBot() {
|
||||||
const client = new Client({
|
const client = new Client({
|
||||||
@@ -117,6 +123,78 @@ export async function startBot() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🎫-Button: Ticket erstellen (privater Thread) / schließen
|
||||||
|
if (interaction.isButton() && interaction.customId === 'ticket_create') {
|
||||||
|
try {
|
||||||
|
const channelId = ticketChannelId();
|
||||||
|
if (!channelId) throw new Error('Kein Ticket-Kanal konfiguriert');
|
||||||
|
|
||||||
|
// Pro User nur ein offenes Ticket
|
||||||
|
const existing = openTicketOf(interaction.user.id);
|
||||||
|
if (existing) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: `❕ Du hast schon ein offenes Ticket: <#${existing.thread_id}>`,
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await interaction.client.channels.fetch(channelId);
|
||||||
|
const thread = await channel.threads.create({
|
||||||
|
name: `🎫 ${interaction.user.username}`,
|
||||||
|
type: ChannelType.PrivateThread,
|
||||||
|
invitable: false,
|
||||||
|
autoArchiveDuration: 10080,
|
||||||
|
});
|
||||||
|
await thread.members.add(interaction.user.id);
|
||||||
|
saveTicket(thread.id, interaction.user.id);
|
||||||
|
|
||||||
|
await thread.send({
|
||||||
|
content: `<@${interaction.user.id}>`,
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setColor(0xf5c518)
|
||||||
|
.setDescription(
|
||||||
|
'Willkommen in deinem Ticket! Beschreib dein Anliegen — das Team meldet sich.\n' +
|
||||||
|
'Wenn alles geklärt ist, kann das Ticket hier geschlossen werden.'
|
||||||
|
)
|
||||||
|
.setFooter({ text: 'D4RKST3R // SUPPORT' }),
|
||||||
|
],
|
||||||
|
components: [
|
||||||
|
new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder().setCustomId('ticket_close').setStyle(ButtonStyle.Danger).setLabel('Ticket schließen').setEmoji('🔒')
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
await interaction.reply({
|
||||||
|
content: `✅ Dein Ticket: <#${thread.id}>`,
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ticket] Erstellung fehlgeschlagen:', error);
|
||||||
|
await interaction
|
||||||
|
.reply({ content: '❌ Ticket konnte nicht erstellt werden — dem Bot fehlen vermutlich Thread-Rechte.', flags: MessageFlags.Ephemeral })
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (interaction.isButton() && interaction.customId === 'ticket_close') {
|
||||||
|
try {
|
||||||
|
const ticket = getTicket(interaction.channelId);
|
||||||
|
if (!ticket) {
|
||||||
|
await interaction.reply({ content: '❌ Das ist kein Ticket-Thread.', flags: MessageFlags.Ephemeral });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closeTicket(interaction.channelId);
|
||||||
|
await interaction.reply(`🔒 Ticket geschlossen von **${interaction.member?.displayName ?? interaction.user.username}**.`);
|
||||||
|
await interaction.channel.setLocked(true).catch(() => {});
|
||||||
|
await interaction.channel.setArchived(true).catch(() => {});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ticket] Schließen fehlgeschlagen:', error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 🎉-Button: Giveaway-Teilnahme togglen
|
// 🎉-Button: Giveaway-Teilnahme togglen
|
||||||
if (interaction.isButton() && interaction.customId === 'giveaway_enter') {
|
if (interaction.isButton() && interaction.customId === 'giveaway_enter') {
|
||||||
const g = getGiveaway(interaction.message.id);
|
const g = getGiveaway(interaction.message.id);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// /ticket-setup — postet das Ticket-Embed mit 🎫-Button in den aktuellen Kanal (Admin)
|
||||||
|
import {
|
||||||
|
SlashCommandBuilder, PermissionFlagsBits, MessageFlags,
|
||||||
|
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
|
||||||
|
} from 'discord.js';
|
||||||
|
import { ticketChannelId } from '../../runtime-settings.js';
|
||||||
|
|
||||||
|
export const data = new SlashCommandBuilder()
|
||||||
|
.setName('ticket-setup')
|
||||||
|
.setDescription('Postet das Ticket-Embed mit Button in diesen Kanal')
|
||||||
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator);
|
||||||
|
|
||||||
|
export async function execute(interaction) {
|
||||||
|
if (!ticketChannelId()) {
|
||||||
|
await interaction.reply({
|
||||||
|
content: '❌ Erst auf der Setup-Seite einen Ticket-Kanal wählen.',
|
||||||
|
flags: MessageFlags.Ephemeral,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embed = new EmbedBuilder()
|
||||||
|
.setColor(0xf5c518)
|
||||||
|
.setTitle('🎫 Support-Ticket')
|
||||||
|
.setDescription(
|
||||||
|
'Du hast eine Frage, ein Problem oder ein privates Anliegen?\n\n' +
|
||||||
|
'Klick auf den Button — es öffnet sich ein **privater Thread**, ' +
|
||||||
|
'den nur du und das Team sehen können.'
|
||||||
|
)
|
||||||
|
.setFooter({ text: 'D4RKST3R // SUPPORT' });
|
||||||
|
|
||||||
|
const row = new ActionRowBuilder().addComponents(
|
||||||
|
new ButtonBuilder()
|
||||||
|
.setCustomId('ticket_create')
|
||||||
|
.setStyle(ButtonStyle.Primary)
|
||||||
|
.setLabel('Ticket erstellen')
|
||||||
|
.setEmoji('🎫')
|
||||||
|
);
|
||||||
|
|
||||||
|
await interaction.channel.send({ embeds: [embed], components: [row] });
|
||||||
|
await interaction.reply({ content: '✅ Ticket-Post erstellt.', flags: MessageFlags.Ephemeral });
|
||||||
|
}
|
||||||
@@ -252,6 +252,26 @@ export function commitHeatmap() {
|
|||||||
.all();
|
.all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ticket-System (Button → privater Thread)
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
thread_id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
status TEXT NOT NULL DEFAULT 'open',
|
||||||
|
opened_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
const insertTicket = db.prepare('INSERT INTO tickets (thread_id, user_id) VALUES (?, ?)');
|
||||||
|
const getTicketStmt = db.prepare('SELECT * FROM tickets WHERE thread_id = ?');
|
||||||
|
const closeTicketStmt = db.prepare(`UPDATE tickets SET status = 'closed' WHERE thread_id = ?`);
|
||||||
|
const openTicketByUserStmt = db.prepare(
|
||||||
|
`SELECT * FROM tickets WHERE user_id = ? AND status = 'open'`
|
||||||
|
);
|
||||||
|
export const saveTicket = (threadId, userId) => insertTicket.run(threadId, userId);
|
||||||
|
export const getTicket = (threadId) => getTicketStmt.get(threadId) ?? null;
|
||||||
|
export const closeTicket = (threadId) => closeTicketStmt.run(threadId);
|
||||||
|
export const openTicketOf = (userId) => openTicketByUserStmt.get(userId) ?? null;
|
||||||
|
|
||||||
// Modmail: DM-Konversationen ↔ Staff-Threads
|
// Modmail: DM-Konversationen ↔ Staff-Threads
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS modmail (
|
CREATE TABLE IF NOT EXISTS modmail (
|
||||||
|
|||||||
@@ -52,6 +52,11 @@ export function votingChannelId() {
|
|||||||
return getSetting('voting_channel_id') || null;
|
return getSetting('voting_channel_id') || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ticket-Kanal (dort entstehen die privaten Ticket-Threads) — leer = Feature aus */
|
||||||
|
export function ticketChannelId() {
|
||||||
|
return getSetting('ticket_channel_id') || null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Modmail-Staff-Kanal (privat!) — leer = Feature aus */
|
/** Modmail-Staff-Kanal (privat!) — leer = Feature aus */
|
||||||
export function modmailChannelId() {
|
export function modmailChannelId() {
|
||||||
return getSetting('modmail_channel_id') || null;
|
return getSetting('modmail_channel_id') || null;
|
||||||
|
|||||||
@@ -58,6 +58,31 @@ export function registerApiV1(app, client) {
|
|||||||
return { ok: true, message_id: message.id };
|
return { ok: true, message_id: message.id };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Eigene Bot-Nachricht nachträglich bearbeiten (z. B. Status-Posts von Skripten)
|
||||||
|
app.patch('/api/v1/message', async (request, reply) => {
|
||||||
|
if (!requireScope(request, reply, 'message')) return;
|
||||||
|
|
||||||
|
const { channel_id: channelId, message_id: messageId, content, embed } = request.body ?? {};
|
||||||
|
const cleanEmbed = sanitizeEmbed(embed);
|
||||||
|
if (!channelId || !messageId || (!content && !cleanEmbed)) {
|
||||||
|
return reply.code(400).send({ error: 'channel_id, message_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' });
|
||||||
|
}
|
||||||
|
const message = await channel.messages.fetch(String(messageId)).catch(() => null);
|
||||||
|
if (!message) return reply.code(404).send({ error: 'Nachricht nicht gefunden' });
|
||||||
|
if (message.author?.id !== client.user?.id) {
|
||||||
|
return reply.code(403).send({ error: 'Nur eigene Bot-Nachrichten sind editierbar' });
|
||||||
|
}
|
||||||
|
await message.edit({
|
||||||
|
...(content !== undefined ? { content: String(content).slice(0, 2000) } : {}),
|
||||||
|
...(cleanEmbed ? { embeds: [cleanEmbed] } : {}),
|
||||||
|
});
|
||||||
|
return { ok: true, message_id: messageId };
|
||||||
|
});
|
||||||
|
|
||||||
// Direktnachricht an einen User
|
// Direktnachricht an einen User
|
||||||
app.post('/api/v1/dm', async (request, reply) => {
|
app.post('/api/v1/dm', async (request, reply) => {
|
||||||
if (!requireScope(request, reply, 'dm')) return;
|
if (!requireScope(request, reply, 'dm')) return;
|
||||||
|
|||||||
+47
-1
@@ -212,6 +212,7 @@ ${rssItems}
|
|||||||
modlog_channel_id: getSetting('modlog_channel_id') ?? '',
|
modlog_channel_id: getSetting('modlog_channel_id') ?? '',
|
||||||
status_channel_id: getSetting('status_channel_id') ?? '',
|
status_channel_id: getSetting('status_channel_id') ?? '',
|
||||||
voting_channel_id: getSetting('voting_channel_id') ?? '',
|
voting_channel_id: getSetting('voting_channel_id') ?? '',
|
||||||
|
ticket_channel_id: getSetting('ticket_channel_id') ?? '',
|
||||||
gameservers: getSetting('gameservers') ?? '',
|
gameservers: getSetting('gameservers') ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -244,7 +245,7 @@ ${rssItems}
|
|||||||
const OPTIONAL_CHANNELS = [
|
const OPTIONAL_CHANNELS = [
|
||||||
'release_channel_id', 'backup_channel_id', 'starboard_channel_id',
|
'release_channel_id', 'backup_channel_id', 'starboard_channel_id',
|
||||||
'screenshot_channel_id', 'modmail_channel_id', 'welcome_channel_id',
|
'screenshot_channel_id', 'modmail_channel_id', 'welcome_channel_id',
|
||||||
'modlog_channel_id', 'status_channel_id', 'voting_channel_id',
|
'modlog_channel_id', 'status_channel_id', 'voting_channel_id', 'ticket_channel_id',
|
||||||
];
|
];
|
||||||
for (const key of ['commit_channel_id', 'devlog_channel_id', ...OPTIONAL_CHANNELS]) {
|
for (const key of ['commit_channel_id', 'devlog_channel_id', ...OPTIONAL_CHANNELS]) {
|
||||||
if (body[key] === undefined) continue;
|
if (body[key] === undefined) continue;
|
||||||
@@ -312,6 +313,51 @@ ${rssItems}
|
|||||||
return { ok: true, settings: currentSettings() };
|
return { ok: true, settings: currentSettings() };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Composer (Admin): Nachricht/Embed als Bot senden oder eigene Posts bearbeiten
|
||||||
|
app.post('/api/compose', async (request, reply) => {
|
||||||
|
if (requireAdmin(request, reply)) return;
|
||||||
|
|
||||||
|
const { channel_id: channelId, message_id: messageId, content, embed_title: embedTitle } = request.body ?? {};
|
||||||
|
const text = String(content ?? '').trim();
|
||||||
|
if (!channelId || !text) {
|
||||||
|
return reply.code(400).send({ error: 'channel_id und content 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()
|
||||||
|
? {
|
||||||
|
content: null,
|
||||||
|
embeds: [
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setColor(0xf5c518)
|
||||||
|
.setTitle(String(embedTitle).slice(0, 200))
|
||||||
|
.setDescription(text.slice(0, 4000))
|
||||||
|
.setFooter({ text: 'D4RKST3R' })
|
||||||
|
.setTimestamp(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: { content: text.slice(0, 2000), embeds: [] };
|
||||||
|
|
||||||
|
if (messageId) {
|
||||||
|
const message = await channel.messages.fetch(String(messageId)).catch(() => null);
|
||||||
|
if (!message) return reply.code(404).send({ error: 'Nachricht nicht gefunden' });
|
||||||
|
if (message.author?.id !== client.user?.id) {
|
||||||
|
return reply.code(403).send({ error: 'Nur eigene Bot-Nachrichten sind editierbar' });
|
||||||
|
}
|
||||||
|
await message.edit(payload);
|
||||||
|
request.log.info(`Composer: Nachricht ${messageId} bearbeitet`);
|
||||||
|
return { ok: true, edited: true, message_id: String(messageId) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await channel.send(payload);
|
||||||
|
request.log.info(`Composer: Nachricht ${message.id} gesendet`);
|
||||||
|
return { ok: true, edited: false, message_id: message.id };
|
||||||
|
});
|
||||||
|
|
||||||
// --- API-Keys (Admin) — für /api/v1/* ---
|
// --- API-Keys (Admin) — für /api/v1/* ---
|
||||||
|
|
||||||
const VALID_SCOPES = ['message', 'dm', 'roles', 'read'];
|
const VALID_SCOPES = ['message', 'dm', 'roles', 'read'];
|
||||||
|
|||||||
Reference in New Issue
Block a user