import {getAlertsOutboxOverview,retryUnknownOutbox} from "../services/alert-engine/outbox.js" import {sendTelegramRequest} from "../services/alerts-telegram-send.js" import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod" import { getAlertsOverview, getTelegramSettingsRow, replaceAlertsConfig, formatAlertRuleTestTelegramText, updateTelegramSettings, } from "../services/alerts-service.js" import { alertsResponseSchema, alertsOutboxOverviewSchema,alertsOutboxRetrySchema,alertsOutboxRetryReceiptSchema, putRulesSchema, normalizeAlertsConfig, putTelegramSchema, testTelegramSchema, telegramSettingsReceiptSchema, } from "@mmapp/contracts/alerts" import {OutboxRetryConflict} from "../services/alerts-outbox-retry.js" interface Dependencies { retryOutbox:typeof retryUnknownOutbox; outbox:()=>Promise;overview:()=>Promise;replace:typeof replaceAlertsConfig;writeTelegram:typeof updateTelegramSettings readTelegram:()=>Promise<{botToken:string;chatId:string;messageThreadId:number|null}> send:(input:{token:string;chatId:string;text:string;messageThreadId?:number})=>Promise<{ok:true}|{ok:false;error:string}> } export function createAlertsRoutes(deps:Dependencies):FastifyPluginAsyncZod {return async(app)=>{ app.get("/alerts", async (_req, reply) => { const result=alertsResponseSchema.safeParse(await deps.overview()) if(!result.success)return reply.status(500).send({error:"Снимок оповещений не подтверждён"}) return reply.header("Cache-Control","no-store").send(result.data) }) app.get("/alerts/outbox",async(_req,reply)=>{ const parsed=alertsOutboxOverviewSchema.safeParse(await deps.outbox()) if(!parsed.success)return reply.status(500).send({error:"Состояние очереди не подтверждено"}) return reply.header("Cache-Control","no-store").send(parsed.data) }) app.post<{Params:{id:string}}>("/alerts/outbox/:id/retry",async(req,reply)=>{ const parsed=alertsOutboxRetrySchema.safeParse(req.body) if(!parsed.success||!req.params.id.trim()||req.params.id.length>200)return reply.status(400).send({error:"Проверьте подтверждение и версию записи очереди"}) try{const receipt=alertsOutboxRetryReceiptSchema.safeParse(await deps.retryOutbox(req.params.id,parsed.data));if(!receipt.success||receipt.data.id!==req.params.id||receipt.data.resolutionVersion!==parsed.data.expectedVersion+1)return reply.status(500).send({error:"Постановка сообщения в очередь не подтверждена"});return reply.header("Cache-Control","no-store").send(receipt.data)}catch(error){if(error instanceof OutboxRetryConflict)return reply.status(409).send({error:error.message});throw error} }) app.put("/alerts/rules", async (req, reply) => { const parsed = putRulesSchema.safeParse(req.body) if (!parsed.success) { return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() }) } try { normalizeAlertsConfig(parsed.data) } catch { return reply.status(400).send({error:"Проверьте объекты, условия и группы правил"}) } const ifMatch=req.headers["if-match"] if(ifMatch===undefined)return reply.status(428).send({error:"Обновите снимок правил перед сохранением"}) if(typeof ifMatch!=="string"||!/^"[a-f0-9]{64}"$/.test(ifMatch))return reply.status(400).send({error:"Некорректная ревизия правил"}) const expectedRevision=ifMatch.slice(1,-1) const groups = parsed.data.groups const rules = parsed.data.rules.map((r) => ({ id: r.id, name: r.name, type: r.type, target: r.target, targets: r.targets, groupId: r.groupId ?? null, condition: r.condition, conditions: r.conditions && r.conditions.length > 0 ? r.conditions.map((c) => c.trim()).filter(Boolean) : r.condition?.trim() ? [r.condition.trim()] : [], severity: r.severity, enabled: r.enabled, cooldown: r.cooldown, confirmStabilitySec: r.confirmStabilitySec ?? null, recoveryMode: r.recoveryMode ?? "always", recoveryStabilitySec: r.recoveryStabilitySec ?? null, chatId: r.chatId, })) try {return reply.send(await deps.replace({ groups, rules,expectedRevision }))} catch(error){if(error instanceof Error&&error.message.includes("Правила изменились"))return reply.status(412).send({error:error.message});if(error instanceof Error&&error.message.includes("уже выполняется"))return reply.status(409).send({error:error.message});throw error} }) app.put("/alerts/telegram", async (req, reply) => { const parsed = putTelegramSchema.strict().safeParse(req.body) if (!parsed.success) { return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() }) } if (Object.keys(parsed.data).length === 0) return reply.status(400).send({error:"Нет изменённых параметров Telegram"}) const next = await deps.writeTelegram({ token: parsed.data.token, chatId: parsed.data.chatId, messageThreadId: parsed.data.messageThreadId, }) const connected = next.tokenConfigured && Boolean(next.chatId?.trim()) const receipt = telegramSettingsReceiptSchema.safeParse({ ok: true, telegram: { ...next, connected } }) if (!receipt.success) return reply.status(500).send({error:"Сохранение параметров Telegram не подтверждено"}) return reply.send(receipt.data) }) app.post("/alerts/telegram/test", async (req, reply) => { const parsed = testTelegramSchema.strict().safeParse(req.body ?? {}) if (!parsed.success) { return reply.status(400).send({ error: "Некорректное тело запроса", details: parsed.error.flatten() }) } const stored = await deps.readTelegram() const token = (parsed.data.token?.trim() || stored.botToken).trim() const chatId = (parsed.data.chatId?.trim() || stored.chatId || "").trim() if (!token) return reply.status(400).send({error:"Не задан Bot Token (сохраните в БД или передайте в запросе)"}) if (!chatId) return reply.status(400).send({error:"Не задан Chat ID"}) const threadId = parsed.data.messageThreadId ?? stored.messageThreadId ?? undefined if (threadId !== undefined && (!Number.isInteger(threadId) || threadId < 1 || threadId > 2147483647)) return reply.status(400).send({error:"ID темы Telegram не подтверждён"}) const text = parsed.data.rulePreview ? formatAlertRuleTestTelegramText(parsed.data.rulePreview) : "MikroTik Manager — тест оповещений (Telegram)." const send = await deps.send({ text, token, chatId, messageThreadId: threadId, }) if (send.ok !== true) { return reply.status(502).send({ ok: false, error: send.error }) } return reply.send({ ok: true }) }) } } export const defaultAlertsDependencies:Dependencies={retryOutbox:retryUnknownOutbox,outbox:getAlertsOutboxOverview,overview:getAlertsOverview,replace:replaceAlertsConfig,writeTelegram:updateTelegramSettings,readTelegram:getTelegramSettingsRow,send:input=>sendTelegramRequest(input.token,{chat_id:input.chatId,text:input.text,...(input.messageThreadId!==undefined?{message_thread_id:input.messageThreadId}:{})})} export default createAlertsRoutes(defaultAlertsDependencies)