mirror of
https://github.com/complexcaresolutions/telegram-media-bot.git
synced 2026-03-17 15:03:42 +00:00
feat: show rate-limit countdown in /status
Track rate limit expiry from 429 responses. /status now displays remaining time (e.g. "Rate-Limit (frei in ~7m 30s)") instead of just "Rate-Limit (bitte warten)". Also skip unnecessary login attempts while rate limit is known to be active. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
86d3eeb51f
commit
6bd531e5fc
2 changed files with 44 additions and 15 deletions
|
|
@ -33,6 +33,11 @@ interface TenantDoc {
|
|||
slug: string;
|
||||
}
|
||||
|
||||
interface HealthResult {
|
||||
status: 'ok' | 'rate_limited' | 'auth_error' | 'unreachable';
|
||||
retryAfter?: number; // seconds until rate limit clears
|
||||
}
|
||||
|
||||
function decodeJwtExpiry(token: string): number {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) return 0;
|
||||
|
|
@ -43,6 +48,7 @@ function decodeJwtExpiry(token: string): number {
|
|||
class PayloadClient {
|
||||
private token: string | null = null;
|
||||
private tokenExpiry: number = 0;
|
||||
private rateLimitClearsAt: number = 0; // epoch ms when rate limit expires
|
||||
private readonly apiUrl: string;
|
||||
|
||||
constructor() {
|
||||
|
|
@ -62,6 +68,12 @@ class PayloadClient {
|
|||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
// Track rate limit expiry
|
||||
if (response.status === 429) {
|
||||
const match = text.match(/"retryAfter":(\d+)/);
|
||||
const secs = match ? Number(match[1]) : 60;
|
||||
this.rateLimitClearsAt = Date.now() + secs * 1000;
|
||||
}
|
||||
throw new Error(`Payload login failed (${response.status}): ${text}`);
|
||||
}
|
||||
|
||||
|
|
@ -168,18 +180,27 @@ class PayloadClient {
|
|||
return data.docs;
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<'ok' | 'rate_limited' | 'auth_error' | 'unreachable'> {
|
||||
async checkHealth(): Promise<HealthResult> {
|
||||
// If we know we're rate-limited, don't even try (avoid wasting attempts)
|
||||
const remaining = Math.ceil((this.rateLimitClearsAt - Date.now()) / 1000);
|
||||
if (remaining > 0) {
|
||||
return { status: 'rate_limited', retryAfter: remaining };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.authFetch(`${this.apiUrl}/users/me`);
|
||||
if (response.ok) return 'ok';
|
||||
if (response.status === 401) return 'auth_error';
|
||||
return 'unreachable';
|
||||
if (response.ok) return { status: 'ok' };
|
||||
if (response.status === 401) return { status: 'auth_error' };
|
||||
return { status: 'unreachable' };
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : '';
|
||||
log.error('Health check failed', error);
|
||||
if (msg.includes('429') || msg.includes('RATE_LIMITED')) return 'rate_limited';
|
||||
if (msg.includes('401') || msg.includes('incorrect')) return 'auth_error';
|
||||
return 'unreachable';
|
||||
if (msg.includes('429') || msg.includes('RATE_LIMITED')) {
|
||||
const secs = Math.ceil((this.rateLimitClearsAt - Date.now()) / 1000);
|
||||
return { status: 'rate_limited', retryAfter: Math.max(secs, 0) };
|
||||
}
|
||||
if (msg.includes('401') || msg.includes('incorrect')) return { status: 'auth_error' };
|
||||
return { status: 'unreachable' };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,4 +210,4 @@ class PayloadClient {
|
|||
}
|
||||
|
||||
export const payloadClient = new PayloadClient();
|
||||
export type { MediaDoc, MediaUploadOptions, MediaListResponse, TenantDoc };
|
||||
export type { MediaDoc, MediaUploadOptions, MediaListResponse, TenantDoc, HealthResult };
|
||||
|
|
|
|||
|
|
@ -385,13 +385,21 @@ export function registerHandlers(bot: Bot<BotContext>): void {
|
|||
// /status command
|
||||
bot.command('status', async (ctx) => {
|
||||
const health = await payloadClient.checkHealth();
|
||||
const apiLabels: Record<string, string> = {
|
||||
ok: '\ud83d\udfe2 Erreichbar',
|
||||
rate_limited: '\ud83d\udfe1 Rate\\-Limit \\(bitte warten\\)',
|
||||
auth_error: '\ud83d\udd34 Anmeldefehler',
|
||||
unreachable: '\ud83d\udd34 Nicht erreichbar',
|
||||
};
|
||||
const apiStatus = apiLabels[health] ?? '\ud83d\udd34 Unbekannt';
|
||||
|
||||
let apiStatus: string;
|
||||
if (health.status === 'ok') {
|
||||
apiStatus = '\ud83d\udfe2 Erreichbar';
|
||||
} else if (health.status === 'rate_limited') {
|
||||
const secs = health.retryAfter ?? 0;
|
||||
const mins = Math.floor(secs / 60);
|
||||
const rest = secs % 60;
|
||||
const timeStr = mins > 0 ? `${mins}m ${rest}s` : `${secs}s`;
|
||||
apiStatus = `\ud83d\udfe1 Rate\\-Limit \\(frei in ~${escapeMarkdown(timeStr)}\\)`;
|
||||
} else if (health.status === 'auth_error') {
|
||||
apiStatus = '\ud83d\udd34 Anmeldefehler';
|
||||
} else {
|
||||
apiStatus = '\ud83d\udd34 Nicht erreichbar';
|
||||
}
|
||||
|
||||
const expiry = payloadClient.getTokenExpiry();
|
||||
const expiryStr = expiry ? expiry.toLocaleString('de-DE') : 'Kein Token';
|
||||
|
|
|
|||
Loading…
Reference in a new issue