initial commit
This commit is contained in:
63
server/notifications.mjs
Normal file
63
server/notifications.mjs
Normal file
@@ -0,0 +1,63 @@
|
||||
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; },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user