64 lines
4.0 KiB
JavaScript
64 lines
4.0 KiB
JavaScript
export function notificationText(event, service, config) {
|
|
const en = config.locale === 'en';
|
|
const name = service.name[en ? 'en' : 'ru'];
|
|
const scope = event.scope === 'scenario' ? (en ? 'User journey' : 'Пользовательский сценарий') : (en ? 'Availability' : 'Доступность');
|
|
const recovered = event.kind === 'recovery';
|
|
const title = recovered ? (en ? 'Recovered' : 'Восстановлено') : (en ? 'Outage confirmed' : 'Сбой подтверждён');
|
|
const date = new Intl.DateTimeFormat(en ? 'en' : 'ru', { dateStyle: 'short', timeStyle: 'short', timeZone: config.timezone }).format(event.created_at);
|
|
const duration = recovered && event.resolved_at ? Math.max(1, Math.round((event.resolved_at - event.started_at) / 60000)) : null;
|
|
return [`RideBus Status · ${title}`, name, scope, date,
|
|
...(duration === null ? [] : [`${en ? 'Confirmed incident duration' : 'Длительность подтверждённого инцидента'}: ${duration} ${en ? 'min' : 'мин'}`]),
|
|
...(config.publicUrl ? [config.publicUrl] : []),
|
|
].join('\n').slice(0, 3500);
|
|
}
|
|
|
|
export async function sendTelegram(config, text, { fetchImpl = fetch } = {}) {
|
|
try {
|
|
const response = await fetchImpl(`https://api.telegram.org/bot${config.token}/sendMessage`, {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, redirect: 'error',
|
|
body: JSON.stringify({ chat_id: config.chatId, text, link_preview_options: { is_disabled: true } }),
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
let body;
|
|
try { body = await response.json(); } catch { body = null; }
|
|
if (response.ok && body?.ok === true) return { ok: true };
|
|
const code = body?.error_code || response.status;
|
|
const retryAfter = Number(body?.parameters?.retry_after);
|
|
return { ok: false, retryable: code === 429 || code >= 500, reason: code === 429 ? 'rate_limited' : code === 401 || code === 403 ? 'access_denied' : 'telegram_error', retryAfterSeconds: code === 429 && Number.isFinite(retryAfter) ? Math.max(1, Math.min(3600, retryAfter)) : null };
|
|
} catch { return { ok: false, retryable: true, reason: 'network', retryAfterSeconds: null }; }
|
|
}
|
|
|
|
export function createNotifier(store, config, { send = sendTelegram, now = Date.now } = {}) {
|
|
let active = null, stopped = true, timer;
|
|
async function flush() {
|
|
if (!config.telegram.enabled) return;
|
|
if (active) return active;
|
|
active = (async () => {
|
|
for (const event of store.dueNotifications(now())) {
|
|
const service = config.services.find(service => service.id === event.service_id);
|
|
const signature = event.scope === 'scenario' ? service?.scenario?.signature : service?.signature;
|
|
if (!service || signature !== event.signature || now() - event.created_at > 86400000) {
|
|
store.notificationResult(event.id, { status: 'discarded', reason: 'obsolete' }, now()); continue;
|
|
}
|
|
const result = await send(config.telegram, notificationText(event, service, config.telegram));
|
|
if (result.ok) store.notificationResult(event.id, { status: 'sent' }, now());
|
|
else {
|
|
const retry = result.retryable && event.attempts < 7;
|
|
const delaySeconds = result.retryAfterSeconds || Math.min(3600, 30 * 2 ** event.attempts);
|
|
store.notificationResult(event.id, { status: retry ? 'pending' : 'failed', retryAt: now() + delaySeconds * 1000, reason: result.reason }, now());
|
|
// The database holds this incident's recovery until its outage is sent.
|
|
}
|
|
}
|
|
})();
|
|
try { await active; } finally { active = null; }
|
|
}
|
|
async function loop() {
|
|
try { await flush(); } catch { console.error('Notification delivery failed; pending events remain in the database.'); }
|
|
if (!stopped) timer = setTimeout(loop, 15000);
|
|
}
|
|
return { flush,
|
|
start() { if (!stopped || !config.telegram.enabled) return; stopped = false; void loop(); },
|
|
async stop() { stopped = true; clearTimeout(timer); if (active) await active; },
|
|
};
|
|
}
|