feat: button-driven Telegram menu (reply keyboard + commands), reachability diagnostic, How-To update
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
+59
-17
@@ -1,6 +1,6 @@
|
||||
// Telegram-Bot (grammY, Webhook) — vollwertiger Eingangs- und Bedien-Kanal.
|
||||
// Kopplung, Bündelung weitergeleiteter Bilder, Rezeptwahl, Verarbeitung, Rückmeldung.
|
||||
import { Bot, InputFile, InlineKeyboard } from 'grammy';
|
||||
import { Bot, InputFile, InlineKeyboard, Keyboard } from 'grammy';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { one, query } from './db';
|
||||
import { decrypt } from './crypto';
|
||||
@@ -14,6 +14,21 @@ let bot: Bot | null = null;
|
||||
let webhookSecret = '';
|
||||
// Zwischenspeicher: Freitext, aus dem der Nutzer per Knopf ein Bild erzeugen kann.
|
||||
const pendingGenerate = new Map<number, string>();
|
||||
// Chats, die gerade eine Bildbeschreibung eintippen (nach Tippen auf „Neues Bild").
|
||||
const awaitingGenerate = new Set<number>();
|
||||
|
||||
// Menü-Beschriftungen (Dauertastatur) — alles per Fingertipp, ohne Befehle.
|
||||
const BTN_NEW = '✨ Neues Bild', BTN_RECIPES = '📁 Rezept wählen', BTN_HELP = '❓ Hilfe';
|
||||
function mainKeyboard() {
|
||||
return new Keyboard().text(BTN_NEW).row().text(BTN_RECIPES).text(BTN_HELP).resized().persistent();
|
||||
}
|
||||
const HELP_TEXT =
|
||||
'So funktioniert’s — alles per Knopf, ohne Befehle:\n\n' +
|
||||
'🖼 *Bearbeiten:* Bild(er) hierher schicken (am besten als *Datei*), dann unten das Rezept antippen.\n' +
|
||||
'🔀 *Kombinieren:* 2+ Bilder schicken, ins Bild-Textfeld eine Beschreibung wie „mit unserer Hündin Frieda" — danach „🔀 Kombinieren" antippen.\n' +
|
||||
'✨ *Neues Bild:* unten „✨ Neues Bild" tippen und beschreiben, was entstehen soll.\n' +
|
||||
'♻️ *Weiterbearbeiten:* ein fertiges Bild von mir einfach wieder zurückschicken.\n\n' +
|
||||
'Ich melde mich einmal, wenn alles fertig ist — mit Ergebnis und Link.';
|
||||
|
||||
export async function getToken(): Promise<string> {
|
||||
const s = await one<{ telegram_bot_token_enc: string | null }>('SELECT telegram_bot_token_enc FROM settings WHERE id=1');
|
||||
@@ -48,6 +63,16 @@ export async function setupTelegram(): Promise<void> {
|
||||
console.log('[telegram] Webhook gesetzt:', `${BASE}/api/telegram/webhook`);
|
||||
} catch (e) { console.error('[telegram] setWebhook fehlgeschlagen:', e); }
|
||||
}
|
||||
// Befehlsmenü (das „/"-Menü in Telegram) — als zusätzliche Abkürzung.
|
||||
try {
|
||||
await b.api.setMyCommands([
|
||||
{ command: 'neu', description: '✨ Neues Bild aus Text erzeugen' },
|
||||
{ command: 'rezepte', description: '📁 Standard-Rezept wählen' },
|
||||
{ command: 'status', description: '⏳ Letzten Auftrag anzeigen' },
|
||||
{ command: 'hilfe', description: '❓ Hilfe & Anleitung' },
|
||||
{ command: 'start', description: '🏠 Menü zeigen' },
|
||||
]);
|
||||
} catch (e) { console.error('[telegram] setMyCommands:', e); }
|
||||
setInterval(() => { sweepDrafts().catch((e) => console.error('[telegram] sweep', e)); }, 3000);
|
||||
}
|
||||
|
||||
@@ -247,34 +272,39 @@ export async function notifyJobDone(chatId: number, jobId: string): Promise<void
|
||||
let msg = `✅ Fertig: ${done.length} Bild(er)${failed.length ? `, ${failed.length} fehlgeschlagen` : ''}.`;
|
||||
if (delivered) msg += `\n📁 ${delivered} in Picdrop-Galerie ausgeliefert.`;
|
||||
if (BASE) msg += `\n🔗 Bibliothek: ${BASE}/bibliothek`;
|
||||
await b.api.sendMessage(chatId, msg);
|
||||
if (done.length) msg += `\n♻️ Weiterbearbeiten? Schick ein Ergebnis einfach zurück.`;
|
||||
await b.api.sendMessage(chatId, msg, { reply_markup: mainKeyboard() });
|
||||
}
|
||||
|
||||
// --- Handler ----------------------------------------------------------------
|
||||
function register(b: Bot) {
|
||||
const sendRecipeMenu = async (ctx: any) => {
|
||||
const recipes = await query<any>('SELECT id, name FROM recipes ORDER BY is_default DESC, name LIMIT 10');
|
||||
const kb = new InlineKeyboard();
|
||||
recipes.forEach((r, i) => { kb.text(r.name, `d:${r.id}`); if (i % 2 === 1) kb.row(); });
|
||||
return ctx.reply('Welches Rezept soll für neue Bilder standardmäßig gelten?', { reply_markup: kb });
|
||||
};
|
||||
|
||||
b.command('start', async (ctx) => {
|
||||
const link = await linkedUser(ctx.chat.id);
|
||||
if (link) return ctx.reply('👋 Klarbild-Bot ist verbunden. Leite mir Bilder weiter (als *Datei* für volle Qualität), dann wähle ein Rezept. /help für mehr.', { parse_mode: 'Markdown' });
|
||||
if (link) return ctx.reply('👋 Klarbild ist verbunden. Schick mir Bilder zum Bearbeiten/Kombinieren, oder tippe unten auf „✨ Neues Bild".', { reply_markup: mainKeyboard() });
|
||||
return ctx.reply('👋 Willkommen bei Klarbild. Zum Koppeln bitte den *Kopplungscode* aus dem Admin-Bereich hier eingeben.', { parse_mode: 'Markdown' });
|
||||
});
|
||||
|
||||
b.command('help', (ctx) => ctx.reply(
|
||||
'So geht’s:\n• *Bearbeiten:* Bilder weiterleiten (am besten als *Datei*) → Rezept wählen.\n• *Kombinieren:* 2+ Bilder senden, dazu eine Bildunterschrift wie „mit unserer Hündin Frieda" → „🔀 Kombinieren".\n• *Neu erzeugen:* `/neu <Beschreibung>` — z. B. `/neu ein Olivenzweig auf Sand`.\n\nIch melde mich einmal, wenn alles fertig ist — mit den Ergebnissen und Links.\nBefehle: /neu · /rezepte (Standard setzen) · /status · /start',
|
||||
{ parse_mode: 'Markdown' }));
|
||||
const sendHelp = (ctx: any) => ctx.reply(HELP_TEXT, { parse_mode: 'Markdown', reply_markup: mainKeyboard() });
|
||||
b.command('help', sendHelp);
|
||||
b.command('hilfe', sendHelp);
|
||||
|
||||
b.command('neu', async (ctx) => {
|
||||
if (!(await linkedUser(ctx.chat.id))) return ctx.reply('Bitte zuerst koppeln (Code aus dem Admin).');
|
||||
const text = (ctx.match || '').toString().trim();
|
||||
if (!text) return ctx.reply('Schreib z. B.: /neu ein minimalistisches Poster mit einem Olivenzweig auf Sand');
|
||||
if (!text) { awaitingGenerate.add(ctx.chat.id); return ctx.reply('Beschreibe kurz dein Bild — was soll entstehen?'); }
|
||||
const b2 = await getBot(); if (b2) await dispatchGenerate(ctx.chat.id, text, b2);
|
||||
});
|
||||
|
||||
b.command('rezepte', async (ctx) => {
|
||||
if (!(await linkedUser(ctx.chat.id))) return ctx.reply('Bitte zuerst koppeln (Code aus dem Admin).');
|
||||
const recipes = await query<any>('SELECT id, name FROM recipes ORDER BY is_default DESC, name LIMIT 10');
|
||||
const kb = new InlineKeyboard();
|
||||
recipes.forEach((r, i) => { kb.text(r.name, `d:${r.id}`); if (i % 2 === 1) kb.row(); });
|
||||
return ctx.reply('Standard-Rezept für diesen Chat wählen:', { reply_markup: kb });
|
||||
return sendRecipeMenu(ctx);
|
||||
});
|
||||
|
||||
b.command('status', async (ctx) => {
|
||||
@@ -302,23 +332,35 @@ function register(b: Bot) {
|
||||
|
||||
b.on('message:text', async (ctx) => {
|
||||
if (ctx.message.text.startsWith('/')) return;
|
||||
const text = ctx.message.text.trim();
|
||||
const linked = await linkedUser(ctx.chat.id);
|
||||
if (linked) {
|
||||
// Dauertastatur-Knöpfe
|
||||
if (text === BTN_HELP) return sendHelp(ctx);
|
||||
if (text === BTN_RECIPES) return sendRecipeMenu(ctx);
|
||||
if (text === BTN_NEW) { awaitingGenerate.add(ctx.chat.id); return ctx.reply('Beschreibe kurz dein Bild — was soll entstehen?'); }
|
||||
|
||||
// Wartet ein Kombinieren-Draft auf die Beschreibung? → Text als Beschreibung nehmen.
|
||||
const waiting = await one<{ id: string }>(
|
||||
`SELECT id FROM telegram_drafts WHERE chat_id=$1 AND status='awaiting_compose_text'
|
||||
ORDER BY last_received_at DESC LIMIT 1`, [ctx.chat.id]);
|
||||
if (waiting) {
|
||||
const b2 = await getBot(); if (b2) await dispatchCompose(ctx.chat.id, waiting.id, ctx.message.text.trim(), b2);
|
||||
const b2 = await getBot(); if (b2) await dispatchCompose(ctx.chat.id, waiting.id, text, b2);
|
||||
return;
|
||||
}
|
||||
// Nutzer hat „Neues Bild" angetippt und tippt jetzt die Beschreibung.
|
||||
if (awaitingGenerate.has(ctx.chat.id)) {
|
||||
awaitingGenerate.delete(ctx.chat.id);
|
||||
const b2 = await getBot(); if (b2) await dispatchGenerate(ctx.chat.id, text, b2);
|
||||
return;
|
||||
}
|
||||
// Sonst: Freitext als Angebot zum Neu-Erzeugen anbieten.
|
||||
const kb = new InlineKeyboard().text('🎨 Neues Bild erzeugen', 'g:x');
|
||||
pendingGenerate.set(ctx.chat.id, ctx.message.text.trim());
|
||||
return ctx.reply('Soll ich daraus ein neues Bild erzeugen? (oder /help)', { reply_markup: kb });
|
||||
const kb = new InlineKeyboard().text('✨ Ja, Bild daraus erzeugen', 'g:x');
|
||||
pendingGenerate.set(ctx.chat.id, text);
|
||||
return ctx.reply('Soll ich daraus ein neues Bild erzeugen?', { reply_markup: kb });
|
||||
}
|
||||
if (await tryPair(ctx.chat.id, ctx.message.text)) {
|
||||
return ctx.reply('✅ Verbunden! Leite mir Bilder weiter, oder erzeuge mit /neu ein neues Bild. /help für mehr.');
|
||||
if (await tryPair(ctx.chat.id, text)) {
|
||||
return ctx.reply('✅ Verbunden! Schick mir Bilder oder tippe unten auf „✨ Neues Bild".', { reply_markup: mainKeyboard() });
|
||||
}
|
||||
return ctx.reply('Code ungültig oder abgelaufen. Neuen Code im Admin erzeugen.');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user