feat: telegram bot (grammy webhook) — full parity channel

- pairing (admin code -> chat link), bundling of forwarded images via drafts+quiet-window sweeper
- recipe pick via inline keyboard, job (origin=telegram) creation, one summary reply with result files + links
- compressed-photo detection/flag; /start /help /rezepte /status
- webhook route (secret token), admin pairing-code + links endpoints, AdminApp telegram section
- worker notifies chat on completion; migration 002 adds jobs.telegram_chat_id; setupTelegram on init
This commit is contained in:
2026-07-23 17:39:49 +00:00
parent 26c3e0ef07
commit d680b3f67a
8 changed files with 342 additions and 4 deletions
+32 -3
View File
@@ -6,19 +6,31 @@ export default function AdminApp() {
const [models, setModels] = useState<any[]>([]);
const [avail, setAvail] = useState<any[]>([]);
const [users, setUsers] = useState<any[]>([]);
const [tgLinks, setTgLinks] = useState<any[]>([]);
const [toast, setToast] = useState<string | null>(null);
const [orKey, setOrKey] = useState(''); const [pdPw, setPdPw] = useState('');
const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2600); };
const load = useCallback(async () => {
const [a, b, c, d] = await Promise.all([
const [a, b, c, d, e] = await Promise.all([
fetch('/api/admin/settings').then((r) => r.json()),
fetch('/api/admin/stats').then((r) => r.json()),
fetch('/api/admin/models?available=1').then((r) => r.json()),
fetch('/api/admin/users').then((r) => r.json()),
fetch('/api/admin/telegram/links').then((r) => r.json()).catch(() => ({ links: [] })),
]);
setS(a.settings || {}); setStats(b); setModels(c.models || []); setAvail(c.available || []); setUsers(d.users || []);
setS(a.settings || {}); setStats(b); setModels(c.models || []); setAvail(c.available || []);
setUsers(d.users || []); setTgLinks(e.links || []);
}, []);
const pairCode = async (userId: string) => {
const r = await fetch('/api/admin/telegram/pairing-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId }) }).then((x) => x.json());
if (r.code) alert(`Kopplungscode: ${r.code}\n\n15 Minuten gültig. Im Klarbild-Telegram-Chat eingeben.`);
};
const unlink = async (chat_id: number) => {
await fetch('/api/admin/telegram/links', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id }) });
load();
};
useEffect(() => { load(); }, [load]);
const field = (k: string, v: any) => setS((p: any) => ({ ...p, [k]: v }));
@@ -147,12 +159,29 @@ export default function AdminApp() {
{users.map((u) => (
<div key={u.id} className="zeile">
<div><b>{u.display_name || u.username}</b> <span className="fein">{u.username} · {u.role}</span></div>
<button className="mini" onClick={() => setPw(u.id)}>Passwort setzen</button>
<div className="reihe">
<button className="mini" onClick={() => pairCode(u.id)}>Telegram koppeln</button>
<button className="mini" onClick={() => setPw(u.id)}>Passwort setzen</button>
</div>
</div>
))}
</div>
</section>
<section className="karte">
<div className="kopfzeile"><span className="mono-label">Telegram-Bot</span></div>
<div className="liste">
<div className="fein">Über Telegram koppeln" bei einem Nutzer einen Code erzeugen und im Chat mit dem Bot eingeben. Danach dort Bilder weiterleiten → Rezept wählen → Ergebnisse kommen zurück.</div>
{tgLinks.map((l) => (
<div key={l.chat_id} className="zeile">
<div><b>Chat {l.chat_id}</b> <span className="fein">{l.user_name || ''} · seit {new Date(l.linked_at).toLocaleDateString('de-DE')}</span></div>
<button className="mini" onClick={() => unlink(l.chat_id)}>Trennen</button>
</div>
))}
{tgLinks.length === 0 && <div className="fein">Noch keine gekoppelten Chats.</div>}
</div>
</section>
{toast && <div className="toast">{toast}</div>}
<AdminStyles />
</div>