diff --git a/wunsch-bot.js b/wunsch-bot.js index 305b0bb..65577cc 100644 --- a/wunsch-bot.js +++ b/wunsch-bot.js @@ -1,523 +1,600 @@ require('dotenv').config(); const { Telegraf } = require('telegraf'); -const fs = require('fs'); +const express = require('express'); const path = require('path'); +const fs = require('fs').promises; const archiver = require('archiver'); const schedule = require('node-schedule'); -const express = require('express'); -const bodyParser = require('body-parser'); -const axios = require('axios'); + +// ==================== KONFIGURATION ==================== + +const validateEnv = () => { + const required = ['TELEGRAM_TOKEN', 'ALLOWED_CHAT_ID']; + const missing = required.filter(key => !process.env[key]); + if (missing.length > 0) { + console.error(`Fehlende Umgebungsvariablen: ${missing.join(', ')}`); + process.exit(1); + } +}; +validateEnv(); + +const CONFIG = { + token: process.env.TELEGRAM_TOKEN, + allowedChatId: process.env.ALLOWED_CHAT_ID, + allowedThreadId: parseInt(process.env.ALLOWED_THREAD_ID, 10) || null, + adminIds: (process.env.ADM_IDS || '').split(',').map(id => id.trim()).filter(Boolean), + + // Blacklists + blacklistChatIds: (process.env.BLACKLIST_CHAT_IDS || '').split(',').map(id => id.trim()).filter(Boolean), + // WICHTIG: IDs wie 1, 646 müssen HIER stehen, damit Topics geblockt werden + blacklistThreadIds: (process.env.BLACKLIST_THREAD_IDS || '').split(',').map(id => parseInt(id.trim(), 10)).filter(Boolean), + + logDir: path.join(__dirname, 'Log'), + port: process.env.PORT || 3005, + itemsPerPage: 10 +}; + +const PATHS = { + errorLog: path.join(CONFIG.logDir, 'error.log'), + wishLog: path.join(CONFIG.logDir, 'wish.log'), + notFoundLog: path.join(CONFIG.logDir, 'not_found.json') +}; + +const CATEGORIES = { + 'category_film': '🎬 Film', + 'category_4k_filme': '📀 4K Filme', + 'category_serie': '📺 Serie', + 'category_anime': '📖 Anime', + 'category_disney': '✨ Disney', + 'category_bollywood': '🎬 Bollywood', + 'category_medizin': '⚕️ Medizin', + 'category_survival': '🏕️ Survival', + 'category_wwe': '🏆 WWE', + 'category_musik': '🎵 Musik', + 'category_hoerspiele_comics': '🎧 Hörspiele & Comics', + 'category_pc_games': '💻 PC Games' +}; + +// ==================== HELPER KLASSEN ==================== + +class Logger { + static async error(error) { + const timestamp = new Date().toISOString(); + const message = `${timestamp} - ${error.message}\n${error.stack}\n\n`; + console.error(message); + try { + if (!await this.fileExists(PATHS.errorLog)) await fs.writeFile(PATHS.errorLog, ''); + await fs.appendFile(PATHS.errorLog, message); + } catch (err) { + console.error('Kritisch: Konnte Error-Log nicht schreiben:', err.message); + } + } + + static async wish(wish, category, link) { + const timestamp = new Date().toISOString(); + const message = `${timestamp} - Kategorie: ${category} - Wunsch: ${wish} - Link: ${link || 'Kein Link'}\n\n`; + try { + if (!await this.fileExists(PATHS.wishLog)) await fs.writeFile(PATHS.wishLog, ''); + await fs.appendFile(PATHS.wishLog, message); + } catch (err) { + console.error('Fehler beim Schreiben in wish.log:', err.message); + } + } + + static async fileExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } + } +} + +class NotFoundManager { + static async get() { + try { + const data = await fs.readFile(PATHS.notFoundLog, 'utf8'); + return JSON.parse(data); + } catch (error) { + if (error.code !== 'ENOENT') console.error('Fehler Lesen not_found.json:', error); + return []; + } + } + + static async add(wish, category) { + try { + const wishes = await this.get(); + wishes.push({ wish, category, timestamp: new Date().toISOString() }); + await fs.writeFile(PATHS.notFoundLog, JSON.stringify(wishes, null, 2)); + } catch (error) { + console.error('Fehler Speichern not_found:', error); + throw error; + } + } + + static async delete(index) { + try { + const wishes = await this.get(); + if (index >= 0 && index < wishes.length) { + wishes.splice(index, 1); + await fs.writeFile(PATHS.notFoundLog, JSON.stringify(wishes, null, 2)); + return true; + } + return false; + } catch (error) { + console.error('Fehler Löschen not_found:', error); + return false; + } + } +} + +class SessionManager { + constructor() { + this.states = {}; + } + + set(chatId, data) { + this.states[chatId] = data; + } + + get(chatId) { + return this.states[chatId]; + } + + delete(chatId) { + delete this.states[chatId]; + } + + async clearChat(ctx, chatId) { + const state = this.get(chatId); + if (!state) return; + + const idsToDelete = [ + state.commandMessageId, + state.uiMessageId, + state.wishCommandMessageId, + state.titleRequestMessageId + ].filter(id => id); + + for (const msgId of idsToDelete) { + try { await ctx.deleteMessage(msgId); } catch (err) {} + } + this.delete(chatId); + } +} + +// ==================== EXPRESS & BOT SETUP ==================== const app = express(); -app.use(bodyParser.json()); -const bot = new Telegraf(process.env.TELEGRAM_TOKEN); +app.use(express.json()); +app.use(express.static(path.join(__dirname, 'public'))); -// Erlaubte Gruppen-ID und Themen-ID aus der .env-Datei -const allowedChatId = process.env.ALLOWED_CHAT_ID; -const allowedThreadId = parseInt(process.env.ALLOWED_THREAD_ID, 10); +const bot = new Telegraf(CONFIG.token); +const sessions = new SessionManager(); -// Benutzerstatus-Management -const userStates = {}; +// ==================== MIDDLEWARES & HELPER ==================== -// Log-Ordner und Datei-Pfade -const logDir = path.join(__dirname, 'Log'); -const errorLogFilePath = path.join(logDir, 'error.log'); -const wishLogFilePath = path.join(logDir, 'wish.log'); -const notFoundLogFilePath = path.join(logDir, 'not_found.json'); // Pfad zur JSON-Datei für nicht gefundene Wünsche +// 1. GLOBALE BLACKLIST (Läuft für JEDE Nachricht und JEDES Update) +const globalRestrictionCheck = (ctx, next) => { + // Wir müssen Chat und Thread ID aus verschiedenen Quellen holen + // (Message, Edited Message, Callback Query) + const chatId = ctx.chat?.id.toString(); + + // Thread ID kann in message.message_thread_id oder callbackQuery.message.message_thread_id stehen + const threadId = ctx.message?.message_thread_id || ctx.callbackQuery?.message?.message_thread_id; -// Stelle sicher, dass der Log-Ordner existiert -if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir); -} + // 1. Chat Blacklist + if (chatId && CONFIG.blacklistChatIds.includes(chatId)) { + console.log(`🛑 GLOBALE BLOCKADE: Chat ID ${chatId} ist gesperrt.`); + return; // Sofort stoppen + } -// Funktion zum Schreiben von Fehlern in die Log-Datei -function logError(error) { - const errorMessage = `${new Date().toISOString()} - ${error.message}\n${error.stack}\n\n`; - fs.appendFile(errorLogFilePath, errorMessage, (err) => { - if (err) { - console.error(`Error writing to error log file: ${err.message}`); + // 2. Thread Blacklist (Fix für ID 1, 646, etc.) + // Wir prüfen nur, wenn eine threadId existiert (Topic) + if (threadId && CONFIG.blacklistThreadIds.includes(threadId)) { + console.log(`🛑 GLOBALE BLOCKADE: Thread ID ${threadId} ist auf der Blacklist.`); + return; // Sofort stoppen + } + + // Wenn alles okay ist, geht es weiter + return next(); +}; + +// Middleware global anwenden +bot.use(globalRestrictionCheck); + +const getReplyOptions = (ctx, extra = {}) => { + const threadId = ctx.message?.message_thread_id || ctx.callbackQuery?.message?.message_thread_id; + if (threadId) { + return { ...extra, message_thread_id: threadId }; + } + return extra; +}; + +// 3. STRICT MODE (Wenn ALLOWED_THREAD_ID gesetzt ist) +const restrictToConfiguredThread = (ctx, next) => { + const chatId = ctx.chat?.id.toString(); + const threadId = ctx.message?.message_thread_id; + + // Wenn wir nicht im erlaubten Chat sind, machen wir nix (außer Blacklist fängt das schon ab) + if (chatId !== CONFIG.allowedChatId) { + return; + } + + // Wenn eine explizite ALLOWED_THREAD_ID gesetzt ist, muss sie stimmen + if (CONFIG.allowedThreadId && threadId !== CONFIG.allowedThreadId) { + console.log(`⚠️ IGNORIERT (Strict Mode): Falscher Thread. Ziel=${CONFIG.allowedThreadId}, Ist=${threadId}`); + return; + } + + console.log(`✅ AKZEPTIERT: Cmd=${ctx.message.text} Thread=${threadId}`); + return next(); +}; + +const isAdmin = (ctx, next) => { + const userId = ctx.from?.id.toString(); + if (CONFIG.adminIds.includes(userId)) { + return next(); + } + return; +}; + +// ==================== BOT LOGIK ==================== + +const deleteMessageSafe = async (ctx, msgId) => { + try { await ctx.deleteMessage(msgId); } catch (e) {} +}; + +const sendWishToGroup = async (wish, category, link) => { + const msg = `🎬 *Ein neuer Wunsch ist eingegangen!*\n\n🔹 Kategorie: ${category}\n\n🔸 Titel: ${wish}\n\n🔗 Link: ${link || 'Kein Link'}`; + await Logger.wish(wish, category, link); + + await bot.telegram.sendMessage(CONFIG.allowedChatId, msg, { + message_thread_id: CONFIG.allowedThreadId, + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: '✅ Erledigt', callback_data: 'wish_fulfilled' }], + [{ text: '❌ Nicht gefunden', callback_data: 'wish_not_found' }] + ] } }); -} +}; -// Funktion zum Schreiben von Wünschen in die Wunsch-Log-Datei -function logWish(wish, category, link) { - const wishMessage = `${new Date().toISOString()} - Kategorie: ${category} - Wunsch: ${wish} - Link: ${link || 'Kein Link'}\n\n`; - fs.appendFile(wishLogFilePath, wishMessage, (err) => { - if (err) { - console.error(`Error writing to wish log file: ${err.message}`); - } +const getCategoryKeyboard = () => { + const buttons = Object.entries(CATEGORIES).map(([key, label]) => ({ + text: label, + callback_data: key + })); + + const chunks = []; + for (let i = 0; i < buttons.length; i += 2) { + chunks.push(buttons.slice(i, i + 2)); + } + + return { reply_markup: { inline_keyboard: chunks } }; +}; + +// --- HILFSFUNKTION PAGINATION --- + +async function sendNotFoundPage(ctx, page = 0) { + const userId = ctx.from.id; + const list = await NotFoundManager.get(); + const totalItems = list.length; + const totalPages = Math.ceil(totalItems / CONFIG.itemsPerPage); + + if (page >= totalPages && totalPages > 0) page = totalPages - 1; + + const start = page * CONFIG.itemsPerPage; + const end = start + CONFIG.itemsPerPage; + const currentItems = list.slice(start, end); + + let text = `🔍 *Liste der nicht gefundenen Wünsche (${totalItems} Einträge)*\n`; + text += `📄 Seite ${page + 1} / ${totalPages || 1}\n\n`; + + const keyboard = []; + + currentItems.forEach((item, index) => { + const globalIndex = start + index; + const displayTitle = item.wish.length > 25 ? item.wish.substring(0, 22) + '...' : item.wish; + text += `${globalIndex + 1}. [${item.category}] ${displayTitle}\n`; }); -} -// Funktion zum Speichern von „Nicht gefunden“-Wünschen in der JSON-Datei -function saveNotFoundWish(wish, category) { - let notFoundWishes = []; - - if (fs.existsSync(notFoundLogFilePath)) { - notFoundWishes = JSON.parse(fs.readFileSync(notFoundLogFilePath, 'utf8')); - } - - notFoundWishes.push({ wish, category, timestamp: new Date().toISOString() }); - fs.writeFileSync(notFoundLogFilePath, JSON.stringify(notFoundWishes, null, 2), 'utf8'); -} - -// Funktion zum Löschen von „Nicht gefunden“-Wünschen aus der JSON-Datei -function deleteNotFoundWish(index) { - let notFoundWishes = []; - - if (fs.existsSync(notFoundLogFilePath)) { - notFoundWishes = JSON.parse(fs.readFileSync(notFoundLogFilePath, 'utf8')); - } - - if (index >= 0 && index < notFoundWishes.length) { - notFoundWishes.splice(index, 1); - fs.writeFileSync(notFoundLogFilePath, JSON.stringify(notFoundWishes, null, 2), 'utf8'); - return true; - } - return false; -} - -// Funktion zum Erstellen des Inline-Keyboards für die Auswahl der Wunschkategorie -function getCategoryKeyboard() { - return { - reply_markup: JSON.stringify({ - inline_keyboard: [ - [{ text: 'Film', callback_data: 'category_film' }], - [{ text: 'Serie', callback_data: 'category_serie' }], - [{ text: 'Anime', callback_data: 'category_anime' }], - [{ text: 'Disney', callback_data: 'category_disney' }], - [{ text: 'Medizin', callback_data: 'category_medizin' }], - [{ text: 'Survival', callback_data: 'category_survival' }], - [{ text: 'WWE', callback_data: 'category_wwe' }], - [{ text: 'Musik', callback_data: 'category_musik' }], - [{ text: 'Bollywood', callback_data: 'category_bollywood' }], - [{ text: 'Hörspiele & Comics', callback_data: 'category_hoerspiele_comics' }], - [{ text: 'PC Games', callback_data: 'category_pc_games' }] // Neue Kategorie hinzugefügt - ] - }) - }; -} - -// Funktion zum Erstellen der Inline-Keyboards für "Erledigt" und "Nicht gefunden" nebeneinander -function getWishActionKeyboard() { - return { - reply_markup: JSON.stringify({ - inline_keyboard: [ - [ - { text: '✅ Erledigt', callback_data: 'wish_fulfilled' }, - { text: '❌ Nicht gefunden', callback_data: 'wish_not_found' } - ] - ] - }) - }; -} - -// Funktion zum Erstellen der Inline-Keyboards für das Löschen von „Nicht gefunden“-Wünschen -function getDeleteNotFoundWishKeyboard() { - return { - reply_markup: JSON.stringify({ - inline_keyboard: [ - [{ text: '🗑️ Löschen', callback_data: 'delete_not_found' }] - ] - }) - }; -} - -// Funktion zum Senden des Wunsches an die Gruppe -async function sendWish(wish, category, chatId, userId, link) { - const message = `🎬 *Ein neuer Wunsch ist eingegangen!*\n\n🔹 Kategorie: ${category}\n\n🔸 Titel: ${wish}\n\n🔗 Link: ${link || 'Kein Link'}`; - - try { - // Logge den Wunsch - logWish(wish, category, link); - - // Sende die Nachricht an die Gruppe mit "Erledigt"-Button und "Nicht gefunden"-Button nebeneinander - await bot.telegram.sendMessage(allowedChatId, message, { - message_thread_id: allowedThreadId, // Sende in das erlaubte Thema - reply_markup: getWishActionKeyboard().reply_markup, // Füge die Buttons hinzu + const deleteButtons = []; + currentItems.forEach((item, index) => { + const globalIndex = start + index; + deleteButtons.push({ + text: `🗑️ #${globalIndex + 1}`, + callback_data: `del_${globalIndex}_${page}` }); + }); - } catch (error) { - logError(new Error(`Error sending ${category} wish: ${error.message}`)); + const chunks = []; + for (let i = 0; i < deleteButtons.length; i += 3) { + chunks.push(deleteButtons.slice(i, i + 3)); } -} + keyboard.push(...chunks); -// Funktion zur Archivierung der Logs -function archiveLogs() { - const dateStr = new Date().toISOString().split('T')[0]; - const zipFilePath = path.join(logDir, `logs_${dateStr}.zip`); - const output = fs.createWriteStream(zipFilePath); - const archive = archiver('zip', { zlib: { level: 9 } }); + const navRow = []; + if (page > 0) navRow.push({ text: '⬅️ Zurück', callback_data: `page_${page - 1}` }); + navRow.push({ text: `📄 ${page + 1}/${totalPages || 1}`, callback_data: 'ignore' }); + if (page < totalPages - 1) navRow.push({ text: '➡️ Weiter', callback_data: `page_${page + 1}` }); + if (navRow.length > 0) keyboard.push(navRow); - output.on('close', () => { - console.log(`Logs archived to ${zipFilePath} (${archive.pointer()} total bytes)`); - - // Lösche die ursprünglichen Log-Dateien - if (fs.existsSync(errorLogFilePath)) fs.unlinkSync(errorLogFilePath); - if (fs.existsSync(wishLogFilePath)) fs.unlinkSync(wishLogFilePath); - - // Neue Log-Dateien erstellen - fs.writeFileSync(errorLogFilePath, ''); - fs.writeFileSync(wishLogFilePath, ''); - - // Maximal 4 Zip-Archive behalten - const files = fs.readdirSync(logDir).filter(file => file.endsWith('.zip')); - if (files.length > 4) { - const sortedFiles = files.sort(); - for (let i = 0; i < sortedFiles.length - 4; i++) { - fs.unlinkSync(path.join(logDir, sortedFiles[i])); + try { + if (ctx.callbackQuery) { + await ctx.editMessageText(text, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + } else { + await bot.telegram.sendMessage(userId, text, { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: keyboard } + }); + if (ctx.chat && ctx.chat.id.toString() !== userId.toString()) { + await ctx.reply('📬 Liste per PN geschickt.', getReplyOptions(ctx)); } } - }); - - archive.on('error', (err) => { - logError(new Error(`Error creating log archive: ${err.message}`)); - }); - - archive.pipe(output); - archive.file(errorLogFilePath, { name: 'error.log' }); - archive.file(wishLogFilePath, { name: 'wish.log' }); - archive.finalize(); + } catch (err) { + console.error('Error rendering page:', err); + } } -// Scheduler für die tägliche Archivierung um Mitternacht -schedule.scheduleJob('0 0 * * *', archiveLogs); +// --- COMMANDS --- -// /help Befehl für alle Kanäle -bot.command('help', async (ctx) => { - const helpMessage = `📋 *Hilfemenü*\n\nHier ist eine kurze Anleitung, wie du einen Wunsch äußern kannst:\n\n` + - `1️⃣ Verwende den Befehl /wunsch, um den Wunschprozess zu starten.\n\n` + - `2️⃣ Wähle eine Kategorie aus, die deinem Wunsch entspricht. 🗂️\n\n` + - `3️⃣ Du wirst aufgefordert, einen Link zum Cover oder Spotify anzugeben (dies ist optional). 📎\n\n` + - `4️⃣ Gib den Titel deines Wunsches ein. ✍️\n\n` + - `5️⃣ Dein Wunsch wird an die Gruppe weitergeleitet und du erhältst eine Bestätigung. ✅\n\n` + - `Für weitere Informationen, besuche bitte unsere Anleitung:`; +// /start, /help, /info haben KEIN restrictToConfiguredThread, funktionieren also überall +// AUCH nicht in geblockten Threads, weil globalRestrictionCheck davor sitzt. - const inlineKeyboard = { - reply_markup: JSON.stringify({ - inline_keyboard: [ - [{ text: 'Anleitung', url: 'https://git.viper.ipv64.net/M_Viper/telegram-film-wunsch-bot' }] // Hier den Link zur Webseite anpassen - ] - }) - }; +bot.command('start', (ctx) => ctx.reply('Willkommen! Nutze /wunsch um einen zu starten.', getReplyOptions(ctx))); +bot.command('help', (ctx) => ctx.reply( + `📋 *Hilfemenü*\n\n` + + `1️⃣ /wunsch - Starte einen Wunsch\n` + + `2️⃣ Kategorie wählen\n` + + `3️⃣ Optional: Link senden (oder "x")\n` + + `4️⃣ Titel eingeben`, + { ...getReplyOptions(ctx), parse_mode: 'Markdown' } +)); - await ctx.reply(helpMessage, inlineKeyboard); +bot.command('info', (ctx) => ctx.reply( + `🤖 *Bot-Informationen*\n\n` + + `Version: 2.0 (Strict Global Blacklist)\n` + + `Status: Laufend`, + { ...getReplyOptions(ctx), parse_mode: 'Markdown' } +)); + +// Wichtig: /wunsch, /cancel, /notfound laufen NUR im erlaubten Thread +bot.command('wunsch', restrictToConfiguredThread, async (ctx) => { + const chatId = ctx.chat.id.toString(); + await sessions.clearChat(ctx, chatId); + + const msg = await ctx.reply('Möchtest du etwas wünschen? Wähle eine Kategorie.', { + ...getCategoryKeyboard(), + ...getReplyOptions(ctx), + disable_notification: true + }); + + sessions.set(chatId, { + waitingFor: 'category', + commandMessageId: ctx.message.message_id, + uiMessageId: msg.message_id + }); }); -// /info Befehl für alle Kanäle -bot.command('info', async (ctx) => { - const botInfo = `🤖 *Bot-Informationen*\n\n` + - `🔢 **Version:** 1.3.9\n\n` + - `👨‍💻 **Ersteller:** M_Viper\n\n` + - `📝 **Lizenz:** MIT Lizenz\n\n` + - `📅 **Erstellt am:** 2024-07-15\n\n` + // Füge hier das Erstellungsdatum hinzu - `🛠️ **Letztes Update:** 2024-09-19\n\n\n` + // Füge hier das letzte Update hinzu - `📈 **Funktionen:**\n\n` + - `- Wunschliste verwalten\n` + - `- Bot-Anleitungen bereitstellen\n` + - `- Benutzeranfragen bearbeiten\n\n` + - `🔧 **Wartung:** Regelmäßig aktualisiert und gewartet`; +bot.command('cancel', restrictToConfiguredThread, async (ctx) => { + const chatId = ctx.chat.id.toString(); + const session = sessions.get(chatId); - const inlineKeyboard = { - reply_markup: JSON.stringify({ - inline_keyboard: [ - [ - { text: '🌐 Webseite besuchen', url: 'https://m-viper.de' }, - { text: '📧 Kontakt', url: 'https://t.me/M_Viper04' } - ] - ] - }) - }; - - await ctx.reply(botInfo, inlineKeyboard); + if (session) { + await sessions.clearChat(ctx, chatId); + await deleteMessageSafe(ctx, ctx.message.message_id); + + const abortMsg = await ctx.reply('🔴 Vorgang abgebrochen.', { + ...getReplyOptions(ctx), + disable_notification: true + }); + + setTimeout(() => deleteMessageSafe(ctx, abortMsg.message_id), 3000); + } else { + await deleteMessageSafe(ctx, ctx.message.message_id); + } }); +bot.command('notfound', restrictToConfiguredThread, isAdmin, async (ctx) => { + await sendNotFoundPage(ctx, 0); +}); + +// --- TEXT HANDLER --- + +bot.on('text', restrictToConfiguredThread, async (ctx) => { + const chatId = ctx.chat.id.toString(); + const session = sessions.get(chatId); + const text = ctx.message.text.trim(); + + if (!session) return; + + try { + if (session.waitingFor === 'link') { + const link = text.toLowerCase() === 'x' ? null : text; + + if (session.uiMessageId) await deleteMessageSafe(ctx, session.uiMessageId); + await deleteMessageSafe(ctx, ctx.message.message_id); + + sessions.set(chatId, { ...session, link, waitingFor: 'title' }); + + const newMsg = await ctx.reply(`Bitte gib den Titel für ${session.category} ein:`, { ...getReplyOptions(ctx), disable_notification: true }); + sessions.set(chatId, { ...session, link, waitingFor: 'title', uiMessageId: newMsg.message_id }); + + } else if (session.waitingFor === 'title') { + const wish = text; + if (!wish) return ctx.reply('Bitte einen Titel eingeben.', getReplyOptions(ctx)); + + await sendWishToGroup(wish, session.category, session.link); + + await sessions.clearChat(ctx, chatId); + await deleteMessageSafe(ctx, ctx.message.message_id); + + const successMsg = `✅ *Dein Wunsch wurde erfolgreich weitergeleitet!*\n\n🔹 Kategorie: ${session.category}\n🔸 Titel: ${wish}`; + + const confirm = await ctx.reply(successMsg, { + ...getReplyOptions(ctx), + parse_mode: 'Markdown', + disable_notification: true + }); + setTimeout(() => deleteMessageSafe(ctx, confirm.message_id), 5000); + } + } catch (error) { + await Logger.error(error); + ctx.reply('❌ Ein Fehler ist aufgetreten.', getReplyOptions(ctx)); + await sessions.clearChat(ctx, chatId); + } +}); + +// --- CALLBACK HANDLER --- -// Callback-Query-Handler bot.on('callback_query', async (ctx) => { const data = ctx.callbackQuery.data; - const messageId = ctx.callbackQuery.message.message_id; // Speichere die ID der Nachricht, um sie eindeutig zu identifizieren - const chatId = ctx.chat.id.toString(); - const userId = ctx.callbackQuery.from.id; - - try { - if (chatId !== allowedChatId) { - console.log(`Callback-Query aus nicht erlaubtem Chat: ${chatId}`); - return; - } - - if (data === 'wish_fulfilled') { - const category = userStates[chatId]?.category || 'Kanal'; - const message = `✨ *Wunsch wurde erfüllt!* ✨\n\n🎉 Der gewünschte Inhalt ist jetzt verfügbar.\n\n📺 Bitte schaue im Kanal "${category}" rein.`; - - // Sende die Nachricht, dass der Wunsch erfüllt wurde - await bot.telegram.sendMessage(allowedChatId, message, { - message_thread_id: allowedThreadId, // In das richtige Thema posten - }); - - // Lösche nur die Nachricht, die diesem Wunsch zugeordnet ist - await ctx.deleteMessage(messageId); - - // Beantworte die Callback-Abfrage, um den Ladekreis zu entfernen - await ctx.answerCbQuery(); - } else if (data === 'wish_not_found') { - // Überprüfe, ob der Benutzer ein Admin ist - const admins = await bot.telegram.getChatAdministrators(chatId); - const isAdmin = admins.some(admin => admin.user.id === userId); - - if (isAdmin) { - const wishTitle = userStates[chatId]?.wishTitle; - const category = userStates[chatId]?.category; - - // Füge den Wunsch in die "Nicht gefunden"-Liste ein - if (wishTitle && category) { - saveNotFoundWish(wishTitle, category); - } - - // Bestätige die Speicherung und entferne die spezifische Nachricht - await bot.telegram.sendMessage(allowedChatId, `📽️ *Sorry*,\n\nZum ${category} *"${wishTitle}"* wurde leider nichts gefunden. Keine Sorge, der Wunsch wurde auf unsere Liste der nicht gefundenen Titel gesetzt.`, { - message_thread_id: allowedThreadId, - }); - - await ctx.deleteMessage(messageId); - - // Beantworte die Callback-Abfrage - await ctx.answerCbQuery(); - } else { - await ctx.answerCbQuery('Nur Admins können diese Funktion nutzen.'); - } - } else if (data.startsWith('delete_not_found_')) { - const index = parseInt(data.split('_')[3], 10); - if (deleteNotFoundWish(index)) { - await ctx.reply('Der Eintrag wurde erfolgreich gelöscht.'); - } else { - await ctx.reply('Ungültige Nummer. Bitte versuche es erneut.'); - } - // Beantworte die Callback-Abfrage - await ctx.answerCbQuery(); - } else if (data === 'delete_not_found') { - await ctx.reply('Bitte gib die Nummer des Eintrags ein, den du löschen möchtest.', { - reply_markup: JSON.stringify({ - force_reply: true - }) - }); - userStates[chatId] = { ...userStates[chatId], waitingForDeleteIndex: true }; - } else if (data.startsWith('category_')) { - const categoryMap = { - 'category_film': 'Film', - 'category_serie': 'Serie', - 'category_anime': 'Anime', - 'category_disney': 'Disney', - 'category_medizin': 'Medizin', - 'category_survival': 'Survival', - 'category_wwe': 'WWE', - 'category_musik': 'Musik', - 'category_bollywood': 'Bollywood', - 'category_hoerspiele_comics': 'Hörspiele & Comics', - 'category_pc_games': 'PC Games' - }; - const category = categoryMap[data]; - const categoryMessage = await ctx.reply(`Du hast die Kategorie ${category} ausgewählt. Bitte gib einen Link zum Cover oder zu Spotify ein. Falls du keinen Link angeben möchtest, trage einfach ein X ein (optional).`, { disable_notification: true }); + const userId = ctx.from.id; + + if (data.startsWith('category_')) { + const category = CATEGORIES[data]; + const chatId = ctx.chat?.id.toString(); + if (category && chatId) { + const session = sessions.get(chatId) || {}; + if (session.uiMessageId) await deleteMessageSafe(ctx, session.uiMessageId); - userStates[chatId] = { - category, - waitingForLink: true, - categoryMessageId: categoryMessage.message_id - }; + const msg = await ctx.reply( + `Kategorie: *${category}*\n\nBitte sende mir einen Link (Cover/Spotify) oder "x":`, + { ...getReplyOptions(ctx), parse_mode: 'Markdown', disable_notification: true } + ); - await ctx.deleteMessage(messageId); + sessions.set(chatId, { + ...session, + category, + waitingFor: 'link', + uiMessageId: msg.message_id + }); } - } catch (error) { - logError(new Error(`Error handling callback query: ${error.message}`)); + await ctx.answerCbQuery(); + return; } - // Beantworte die Callback-Abfrage als abgeschlossen - ctx.answerCbQuery().catch(error => { - logError(new Error(`Error answering callback query: ${error.message}`)); - }); -}); + if (data.startsWith('page_')) { + if (!CONFIG.adminIds.includes(userId.toString())) return ctx.answerCbQuery('Keine Berechtigung'); + const page = parseInt(data.split('_')[1]); + await sendNotFoundPage(ctx, page); + await ctx.answerCbQuery(); + return; + } + + if (data === 'ignore') { + await ctx.answerCbQuery(); + return; + } -// Nachrichten-Handler -bot.on('text', async (ctx) => { - const chatId = ctx.chat.id.toString(); - const userId = ctx.message.from.id; - const text = ctx.message.text.trim(); - const threadId = ctx.message?.message_thread_id; // Thema-ID (falls vorhanden) + if (data.startsWith('del_')) { + if (!CONFIG.adminIds.includes(userId.toString())) return ctx.answerCbQuery('Keine Berechtigung'); + const parts = data.split('_'); + const indexToDelete = parseInt(parts[1]); + const currentPage = parseInt(parts[2]); - console.log(`Received message in chat ID ${chatId} and thread ID ${threadId}: ${text}`); // Logging zur Diagnose - - if (chatId !== allowedChatId || threadId !== allowedThreadId) { - console.log(`Ignoring message in chat ID ${chatId} and thread ID ${threadId} as it's not allowed.`); - if (text.startsWith('/wunsch')) { - await ctx.reply('❌ Dieser Befehl ist in diesem Kanal nicht erlaubt. Bitte benutze den Befehl in den Serien- und Filmwünsche Kanal.', { disable_notification: true }); + const success = await NotFoundManager.delete(indexToDelete); + if (success) { + await ctx.answerCbQuery(`Eintrag #${indexToDelete + 1} gelöscht`); + await sendNotFoundPage(ctx, currentPage); + } else { + await ctx.answerCbQuery('Fehler beim Löschen'); } return; } - // Prüfe, ob der Benutzer den Vorgang abbrechen möchte - if (text === '/cancel') { - if (userStates[chatId]) { - const categoryMessageId = userStates[chatId].categoryMessageId; - const wishCommandMessageId = userStates[chatId].wishCommandMessageId; - const commandMessageId = userStates[chatId].commandMessageId; + if (data === 'wish_fulfilled' || data === 'wish_not_found') { + if (!CONFIG.adminIds.includes(userId.toString())) return ctx.answerCbQuery('Keine Berechtigung'); - // Lösche alle relevanten Nachrichten - if (categoryMessageId) await ctx.deleteMessage(categoryMessageId); - if (wishCommandMessageId) await ctx.deleteMessage(wishCommandMessageId); - if (commandMessageId) await ctx.deleteMessage(commandMessageId); + if (data === 'wish_fulfilled') { + await ctx.editMessageText('✨ *Wunsch wurde erfüllt!* ✨\n🎉 Der Inhalt ist verfügbar.', { parse_mode: 'Markdown' }); + } else if (data === 'wish_not_found') { + const msgText = ctx.callbackQuery.message.text; + const wishTitleMatch = msgText.match(/🔸 Titel: (.*)/); + const title = wishTitleMatch ? wishTitleMatch[1] : 'Unbekannt'; + const catMatch = msgText.match(/🔹 Kategorie: (.*)/); + const category = catMatch ? catMatch[1] : 'Allgemein'; - // Setze den Benutzerstatus zurück - delete userStates[chatId]; - await ctx.reply('🔴 Der Wunschvorgang wurde abgebrochen.', { disable_notification: true }); - } else { - await ctx.reply('🔴 Es gibt keinen laufenden Vorgang zum Abbrechen.', { disable_notification: true }); - } - return; // Beende die Verarbeitung - } - - // Der Rest des Codes bleibt gleich - if (userStates[chatId]) { - if (userStates[chatId].waitingForLink) { - const link = text; - userStates[chatId].wishLink = link; - await ctx.reply(`Bitte gib den Titel des ${userStates[chatId].category} ein.`, { disable_notification: true }); - userStates[chatId].waitingForLink = false; - userStates[chatId].waitingForWish = true; - return; - } - - if (userStates[chatId].waitingForWish) { - const wish = text; - if (wish) { - const category = userStates[chatId].category; - const link = userStates[chatId].wishLink; - const categoryMessageId = userStates[chatId].categoryMessageId; - const titleMessageId = ctx.message.message_id; - const commandMessageId = userStates[chatId].commandMessageId; - const wishCommandMessageId = userStates[chatId].wishCommandMessageId; - - try { - userStates[chatId].wishTitle = wish; - await sendWish(wish, category, chatId, userId, link); - if (categoryMessageId) await ctx.deleteMessage(categoryMessageId); - if (titleMessageId) await ctx.deleteMessage(titleMessageId); - if (commandMessageId) await ctx.deleteMessage(commandMessageId); - if (wishCommandMessageId) await ctx.deleteMessage(wishCommandMessageId); - - userStates[chatId].waitingForWish = false; - } catch (error) { - logError(new Error(`Error processing wish: ${error.message}`)); - } - } else { - await ctx.reply(`Bitte gib den Titel des ${userStates[chatId].category} ein.`, { disable_notification: true }); - } - return; - } - - if (userStates[chatId].waitingForDeleteIndex) { - const index = parseInt(text, 10) - 1; - if (deleteNotFoundWish(index)) { - await ctx.reply('Der Eintrag wurde erfolgreich gelöscht.'); - } else { - await ctx.reply('Ungültige Nummer. Bitte versuche es erneut.'); - } - userStates[chatId].waitingForDeleteIndex = false; - return; - } - } - - if (text.startsWith('/wunsch')) { - const commandMessage = await ctx.reply('Möchtest du etwas wünschen? Wähle bitte eine Kategorie. Du kannst den Vorgang jederzeit mit dem Befehl /cancel abbrechen.', { ...getCategoryKeyboard(), disable_notification: true }); - userStates[chatId] = { - waitingForCategory: true, - commandMessageId: commandMessage.message_id, - wishCommandMessageId: ctx.message.message_id - }; - } else if (text.startsWith('/notfound')) { - const admins = await bot.telegram.getChatAdministrators(chatId); - const isAdmin = admins.some(admin => admin.user.id === userId); - - if (isAdmin) { - if (fs.existsSync(notFoundLogFilePath)) { - const notFoundWishes = JSON.parse(fs.readFileSync(notFoundLogFilePath, 'utf8')); - let replyMessage = '🔍 Liste der nicht gefundenen Wünsche:\n\n'; - - if (notFoundWishes.length === 0) { - replyMessage += 'Keine nicht gefundenen Wünsche.'; - } else { - notFoundWishes.forEach((entry, index) => { - replyMessage += `${index + 1}. Kategorie: ${entry.category} - Wunsch: ${entry.wish}\n`; - }); - } - - // Füge die Löschen-Buttons für jeden Eintrag hinzu - const inlineKeyboard = { - reply_markup: JSON.stringify({ - inline_keyboard: notFoundWishes.map((_, index) => [ - { text: `🗑️ Löschen ${index + 1}`, callback_data: `delete_not_found_${index}` } - ]) - }) - }; - - await ctx.reply(replyMessage, inlineKeyboard); - } else { - await ctx.reply('Es gibt keine nicht gefundenen Wünsche.'); - } + await NotFoundManager.add(title, category); + await ctx.editMessageText(`☹️ Zu "${title}" wurde nichts gefunden.\nAuf die "Not Found" Liste gesetzt.`); } + await ctx.answerCbQuery(); + return; } }); -//start Frontend +bot.catch((err, ctx) => { + Logger.error(err); + console.error(`Error for ${ctx.updateType}`, err); +}); -// Middleware, um statische Dateien aus dem "public"-Ordner zu servieren -app.use(express.static(path.join(__dirname, 'public'))); +// ==================== EXPRESS ROUTES ==================== -// API-Route zum Senden von Wünschen app.post('/api/sendWish', async (req, res) => { const { category, link, title } = req.body; - - // Überprüfen, ob die erforderlichen Daten vorhanden sind - if (!category || !title) { - return res.status(400).send('Kategorie und Titel sind erforderlich.'); - } - - // Erstelle die Nachricht, die an die Telegram-Gruppe gesendet wird - const message = `🎬 *Ein neuer Wunsch ist eingegangen!*\n\n🔹 Kategorie: ${category}\n\n🔸 Titel: ${title}\n\n🔗 Link: ${link || 'Kein Link'}`; - - // Inline-Tastatur mit den Buttons - const replyMarkup = { - reply_markup: JSON.stringify({ - inline_keyboard: [ - [ - { text: '✅ Erledigt', callback_data: 'wish_fulfilled' }, - { text: '❌ Nicht gefunden', callback_data: 'wish_not_found' } - ] - ] - }) - }; - + if (!category || !title) return res.status(400).json({ error: 'Kategorie und Titel sind erforderlich.' }); try { - // Sende die Nachricht an die Telegram-Gruppe im richtigen Thema mit den Buttons - await bot.telegram.sendMessage(allowedChatId, message, { - message_thread_id: allowedThreadId, // Sende in das erlaubte Thema - reply_markup: replyMarkup.reply_markup // Füge die Buttons hinzu - }); - res.status(200).send('Wunsch gesendet'); + await sendWishToGroup(title, category, link || null); + res.status(200).json({ success: true }); } catch (error) { - console.error('Error sending wish:', error); - res.status(500).send('Fehler beim Senden des Wunsches'); + Logger.error(error); + res.status(500).json({ error: 'Interner Serverfehler' }); } }); -//End Frontend +// ==================== INIT & START ==================== -// Server starten -const PORT = process.env.PORT || 3005; -app.listen(PORT, () => { - console.log(`Server läuft auf http://localhost:${PORT}`); -}); +async function init() { + try { + await fs.mkdir(CONFIG.logDir, { recursive: true }); + const files = [PATHS.errorLog, PATHS.wishLog, PATHS.notFoundLog]; + for (const file of files) { + try { await fs.access(file); } catch { await fs.writeFile(file, file.endsWith('.json') ? '[]' : ''); } + } + } catch (error) { + console.error('Init Error:', error); + process.exit(1); + } -bot.launch(); + schedule.scheduleJob('0 0 * * *', async () => { + const dateStr = new Date().toISOString().split('T')[0]; + const zipPath = path.join(CONFIG.logDir, `logs_${dateStr}.zip`); + const output = require('fs').createWriteStream(zipPath); + const archive = archiver('zip', { zlib: { level: 9 } }); -console.log('Bot is running...'); \ No newline at end of file + output.on('close', async () => { + console.log(`Logs archiviert: ${zipPath}`); + try { + const files = await fs.readdir(CONFIG.logDir); + const zips = files.filter(f => f.endsWith('.zip')).sort(); + for (let i = 0; i < zips.length - 4; i++) { + await fs.unlink(path.join(CONFIG.logDir, zips[i])); + } + } catch (err) { Logger.error(err); } + }); + + archive.on('error', (err) => Logger.error(err)); + archive.pipe(output); + + [PATHS.errorLog, PATHS.wishLog].forEach(async (filePath) => { + try { await fs.access(filePath); archive.file(filePath, { name: path.basename(filePath) }); } catch (e) {} + }); + + archive.finalize(); + }); + + app.listen(CONFIG.port, () => console.log(`🌐 Server läuft auf Port ${CONFIG.port}`)); + await bot.launch(); + console.log('✅ Bot gestartet'); + + process.once('SIGINT', () => bot.stop('SIGINT')); + process.once('SIGTERM', () => bot.stop('SIGTERM')); +} + +init(); \ No newline at end of file