mirror of
https://github.com/complexcaresolutions/telegram-media-bot.git
synced 2026-03-17 16:13:42 +00:00
feat: comprehensive user-friendly German error messages
- Add centralized friendlyError() helper mapping technical errors to clear German messages (rate limit, auth, network, timeout, etc.) - Improve /status to handle rate limits gracefully - Improve album upload: abort early on rate limit/auth, show partial results with failure reason - Better file-type rejection: show the rejected extension, suggest sending as photo instead - Friendlier Telegram download errors (file too big, getFile failures) - Add global bot.catch() handler for unhandled Grammy errors - Truncate long unknown errors to 150 chars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0d9d7a2971
commit
a2ee6d8cbe
2 changed files with 149 additions and 32 deletions
12
src/index.ts
12
src/index.ts
|
|
@ -17,6 +17,18 @@ async function main(): Promise<void> {
|
|||
const bot = createBot();
|
||||
registerHandlers(bot);
|
||||
|
||||
// Global error handler — catch unhandled errors so bot doesn't crash
|
||||
bot.catch((err) => {
|
||||
const ctx = err.ctx;
|
||||
const e = err.error;
|
||||
log.error(`Unhandled error for update ${ctx.update.update_id}:`, e);
|
||||
|
||||
// Try to notify the user
|
||||
ctx.reply('\u274c Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut.').catch(() => {
|
||||
// Ignore if we can't even send a message
|
||||
});
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const shutdown = async (signal: string) => {
|
||||
log.info(`Received ${signal}, shutting down...`);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,66 @@ function escapeMarkdown(text: string): string {
|
|||
return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
// Map technical errors to user-friendly German messages
|
||||
function friendlyError(error: unknown): string {
|
||||
const msg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Rate limit
|
||||
if (msg.includes('429') || msg.includes('RATE_LIMITED')) {
|
||||
const match = msg.match(/"retryAfter":(\d+)/);
|
||||
const secs = match ? match[1] : '60';
|
||||
return `Server-Rate-Limit erreicht. Bitte warte ${secs} Sekunden und versuche es dann erneut.`;
|
||||
}
|
||||
|
||||
// Auth
|
||||
if (msg.includes('401') || msg.includes('incorrect')) {
|
||||
return 'Anmeldung am CMS fehlgeschlagen. Bitte den Admin kontaktieren.';
|
||||
}
|
||||
|
||||
// Forbidden
|
||||
if (msg.includes('403') || msg.includes('Berechtigung')) {
|
||||
return 'Keine Berechtigung für diese Aktion. Bitte den Admin kontaktieren.';
|
||||
}
|
||||
|
||||
// Validation (400 from Payload)
|
||||
if (msg.includes('400') && msg.includes('nicht korrekt')) {
|
||||
return 'Upload-Validierung fehlgeschlagen. Möglicherweise fehlen Pflichtfelder (Alt-Text, Tenant).';
|
||||
}
|
||||
|
||||
// Server error
|
||||
if (msg.includes('500') || msg.includes('Internal Server')) {
|
||||
return 'CMS-Serverfehler. Bitte versuche es später erneut.';
|
||||
}
|
||||
|
||||
// Network / DNS
|
||||
if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND') || msg.includes('fetch failed')) {
|
||||
return 'CMS-Server nicht erreichbar. Bitte versuche es später erneut.';
|
||||
}
|
||||
|
||||
// Timeout
|
||||
if (msg.includes('Timeout') || msg.includes('AbortError') || msg.includes('abort')) {
|
||||
return 'Zeitüberschreitung beim Server. Bitte versuche es erneut.';
|
||||
}
|
||||
|
||||
// Telegram file errors
|
||||
if (msg.includes('file_path') || msg.includes('getFile')) {
|
||||
return 'Telegram konnte die Datei nicht bereitstellen. Bitte sende das Bild erneut.';
|
||||
}
|
||||
|
||||
// File too large
|
||||
if (msg.includes('zu groß') || msg.includes('too large')) {
|
||||
return 'Datei ist zu groß (max. 20 MB). Bitte eine kleinere Datei senden.';
|
||||
}
|
||||
|
||||
// Download error
|
||||
if (msg.includes('Download failed')) {
|
||||
return 'Datei konnte nicht heruntergeladen werden. Bitte versuche es erneut.';
|
||||
}
|
||||
|
||||
// Fallback: shorten raw errors
|
||||
return msg.length > 150 ? msg.slice(0, 150) + '...' : msg;
|
||||
}
|
||||
|
||||
// Allowed image extensions for document uploads
|
||||
const ALLOWED_EXTENSIONS = new Set(['jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'svg']);
|
||||
const ALLOWED_MIME_PREFIXES = ['image/'];
|
||||
|
|
@ -62,10 +122,19 @@ async function downloadTelegramFile(
|
|||
ctx: BotContext,
|
||||
fileId: string,
|
||||
): Promise<{ buffer: Buffer; mimeType: string; filePath: string }> {
|
||||
const file = await ctx.api.getFile(fileId);
|
||||
let file;
|
||||
try {
|
||||
file = await ctx.api.getFile(fileId);
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : '';
|
||||
if (msg.includes('file is too big')) {
|
||||
throw new Error('Datei ist zu groß für den Telegram-Download (max. 20 MB). Bitte verkleinere die Datei.');
|
||||
}
|
||||
throw new Error('Telegram konnte die Datei nicht bereitstellen. Bitte sende das Bild erneut.');
|
||||
}
|
||||
const filePath = file.file_path;
|
||||
if (!filePath) {
|
||||
throw new Error('Telegram returned no file_path');
|
||||
throw new Error('Telegram konnte die Datei nicht bereitstellen. Bitte sende das Bild erneut.');
|
||||
}
|
||||
const fileUrl = `https://api.telegram.org/file/bot${config.telegram.botToken}/${filePath}`;
|
||||
const { buffer, mimeType } = await downloadFile(fileUrl);
|
||||
|
|
@ -127,23 +196,14 @@ async function processUpload(
|
|||
parse_mode: 'MarkdownV2',
|
||||
});
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : 'Unbekannter Fehler';
|
||||
log.error(`Upload failed for ${filename}`, error);
|
||||
|
||||
let userMsg: string;
|
||||
if (errMsg.includes('429') || errMsg.includes('RATE_LIMITED')) {
|
||||
const match = errMsg.match(/"retryAfter":(\d+)/);
|
||||
const secs = match ? match[1] : '60';
|
||||
userMsg = `\u23f3 Server\\-Rate\\-Limit erreicht\\. Bitte warte ${secs} Sekunden und versuche es dann erneut\\.`;
|
||||
} else if (errMsg.includes('401') || errMsg.includes('incorrect')) {
|
||||
userMsg = `\u274c Anmeldung am CMS fehlgeschlagen\\. Bitte den Admin kontaktieren\\.`;
|
||||
} else {
|
||||
userMsg = `\u274c Upload fehlgeschlagen: ${escapeMarkdown(errMsg)}`;
|
||||
}
|
||||
|
||||
await ctx.api.editMessageText(ctx.chat!.id, statusMessageId, userMsg, {
|
||||
parse_mode: 'MarkdownV2',
|
||||
});
|
||||
const userMsg = friendlyError(error);
|
||||
await ctx.api.editMessageText(
|
||||
ctx.chat!.id,
|
||||
statusMessageId,
|
||||
`\u274c ${escapeMarkdown(userMsg)}`,
|
||||
{ parse_mode: 'MarkdownV2' },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,16 +221,27 @@ async function processAlbum(
|
|||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
let lastError = '';
|
||||
let rateLimited = false;
|
||||
|
||||
for (let i = 0; i < total; i++) {
|
||||
const msg = messages[i];
|
||||
const userId = ctx.from?.id;
|
||||
if (!userId) continue;
|
||||
|
||||
// Skip remaining uploads if rate-limited
|
||||
if (rateLimited) {
|
||||
failCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const rateCheck = checkRateLimit(userId);
|
||||
if (!rateCheck.allowed) {
|
||||
failCount++;
|
||||
failCount += total - i;
|
||||
rateLimited = true;
|
||||
lastError = `Bot-Rate-Limit nach ${i} Bildern erreicht`;
|
||||
log.warn(`Rate limit hit during album upload at image ${i + 1}`);
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -182,7 +253,16 @@ async function processAlbum(
|
|||
successCount++;
|
||||
} catch (error) {
|
||||
failCount++;
|
||||
lastError = friendlyError(error);
|
||||
log.error(`Album upload failed for ${msg.filename}`, error);
|
||||
|
||||
// Abort album on rate limit or auth errors
|
||||
const errMsg = error instanceof Error ? error.message : '';
|
||||
if (errMsg.includes('429') || errMsg.includes('401')) {
|
||||
rateLimited = true;
|
||||
failCount += total - i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Progress update every 3 images
|
||||
|
|
@ -199,10 +279,17 @@ async function processAlbum(
|
|||
}
|
||||
}
|
||||
|
||||
const resultText =
|
||||
`\u2705 Album hochgeladen\n\n` +
|
||||
`Erfolgreich: ${successCount}/${total}\n` +
|
||||
(failCount > 0 ? `Fehlgeschlagen: ${failCount}` : '');
|
||||
let resultText: string;
|
||||
if (successCount === total) {
|
||||
resultText = `\u2705 Album hochgeladen (${successCount} Bilder)`;
|
||||
} else if (successCount === 0) {
|
||||
resultText = `\u274c Album-Upload fehlgeschlagen\n\n${lastError}`;
|
||||
} else {
|
||||
resultText =
|
||||
`\u26a0\ufe0f Album teilweise hochgeladen\n\n` +
|
||||
`Erfolgreich: ${successCount}/${total}\n` +
|
||||
(lastError ? `Grund: ${lastError}` : '');
|
||||
}
|
||||
|
||||
try {
|
||||
await ctx.api.editMessageText(ctx.chat!.id, statusMsg.message_id, resultText);
|
||||
|
|
@ -256,7 +343,7 @@ export function registerHandlers(bot: Bot<BotContext>): void {
|
|||
await ctx.reply('\ud83c\udfe2 Tenant auswählen:', { reply_markup: keyboard });
|
||||
} catch (error) {
|
||||
log.error('Failed to build tenant keyboard', error);
|
||||
await ctx.reply('\u274c Fehler beim Laden der Tenants. Versuche es später erneut.');
|
||||
await ctx.reply(`\u274c ${friendlyError(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -291,21 +378,37 @@ export function registerHandlers(bot: Bot<BotContext>): void {
|
|||
await ctx.reply(text, { parse_mode: 'MarkdownV2' });
|
||||
} catch (error) {
|
||||
log.error('Failed to list media', error);
|
||||
await ctx.reply('\u274c Fehler beim Laden der Medien.');
|
||||
await ctx.reply(`\u274c ${friendlyError(error)}`);
|
||||
}
|
||||
});
|
||||
|
||||
// /status command
|
||||
bot.command('status', async (ctx) => {
|
||||
const healthy = await payloadClient.checkHealth();
|
||||
let apiStatus: string;
|
||||
try {
|
||||
const healthy = await payloadClient.checkHealth();
|
||||
apiStatus = healthy ? 'Erreichbar' : 'Nicht erreichbar';
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : '';
|
||||
if (msg.includes('429') || msg.includes('RATE_LIMITED')) {
|
||||
apiStatus = 'Rate\\-Limit \\(bitte warten\\)';
|
||||
} else {
|
||||
apiStatus = 'Nicht erreichbar';
|
||||
}
|
||||
}
|
||||
|
||||
const expiry = payloadClient.getTokenExpiry();
|
||||
const expiryStr = expiry ? expiry.toLocaleString('de-DE') : 'Unbekannt';
|
||||
const expiryStr = expiry ? expiry.toLocaleString('de-DE') : 'Kein Token';
|
||||
|
||||
const tenantStr = ctx.session.selectedTenantId !== null
|
||||
? `${escapeMarkdown(ctx.session.selectedTenantName)} \\(ID ${ctx.session.selectedTenantId}\\)`
|
||||
: 'Nicht gewählt — nutze /tenant';
|
||||
|
||||
const text =
|
||||
`\ud83d\udcca *Bot Status*\n\n` +
|
||||
`\u23f1 Uptime: ${escapeMarkdown(formatUptime())}\n` +
|
||||
`\ud83d\udfe2 API: ${healthy ? 'Erreichbar' : 'Nicht erreichbar'}\n` +
|
||||
`\ud83c\udfe2 Tenant: ${ctx.session.selectedTenantId !== null ? `${escapeMarkdown(ctx.session.selectedTenantName)} \\(ID ${ctx.session.selectedTenantId}\\)` : 'Nicht gewählt'}\n` +
|
||||
`\ud83d\udfe2 API: ${apiStatus}\n` +
|
||||
`\ud83c\udfe2 Tenant: ${tenantStr}\n` +
|
||||
`\ud83d\udd11 Token läuft ab: ${escapeMarkdown(expiryStr)}`;
|
||||
|
||||
await ctx.reply(text, { parse_mode: 'MarkdownV2' });
|
||||
|
|
@ -382,9 +485,11 @@ export function registerHandlers(bot: Bot<BotContext>): void {
|
|||
|
||||
// Validate file extension and MIME type
|
||||
if (!isAllowedFile(filename, mimeType)) {
|
||||
const allowedList = Array.from(ALLOWED_EXTENSIONS).join(', ');
|
||||
const ext = filename.split('.').pop()?.toLowerCase() ?? '?';
|
||||
await ctx.reply(
|
||||
`\u274c Nicht unterstütztes Format\\. Erlaubte Formate: ${escapeMarkdown(allowedList)}`,
|
||||
`\u274c *${escapeMarkdown(ext)}*\\-Dateien werden nicht unterstützt\\.\n\n` +
|
||||
`Erlaubte Bildformate: jpg, png, webp, avif, gif, svg\n` +
|
||||
`Tipp: Bilder einfach als Foto \\(nicht Dokument\\) senden\\.`,
|
||||
{ parse_mode: 'MarkdownV2' },
|
||||
);
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue