···11+export const intersect = (a: Array<any>, b: Array<any>) => {
22+ const setA = new Set(a);
33+ const setB = new Set(b);
44+ const intersection = new Set([...setA].filter(x => setB.has(x)));
55+ return Array.from(intersection);
66+}
77+88+99+export function sleep(ms: any) {
1010+ return new Promise(resolve => setTimeout(resolve, ms));
1111+}
+69
notifiers/common/notifierUtils.ts
···11+import { setTimeout, clearTimeout } from "node:timers";
22+import { alertResolvedAllowed, CommonOptions } from "./options.ts";
33+import { Types } from "npm:komodo_client";
44+import { isAlertType, resolveableAlertIdentifier } from "./alertUtils.ts";
55+66+const alertFriendly = (alert: Types.Alert): string => `Alert ${alert._id} for ${alert.target.id}-${alert.data.type}`;
77+88+export const createNotifierPipe = (opts: CommonOptions) => {
99+1010+ const {
1111+ resolverTimeout,
1212+ resolverTypes = [],
1313+ allowedResolveTypes
1414+ } = opts;
1515+1616+ const resolverTimeouts = new Map<string,NodeJS.Timeout>();
1717+1818+ return async (alert: Types.Alert, pushFunc: () => Promise<void>) => {
1919+2020+ if(!alertResolvedAllowed(allowedResolveTypes, alert.resolved)) {
2121+ console.debug(`Not pushing ${alertFriendly(alert)} becuase it is ${alert.resolved ? 'resolved' : 'unresolved'} which is not included in allowed resolved types of '${allowedResolveTypes}'`);
2222+ return;
2323+ }
2424+2525+ if(resolverTimeout === undefined || (resolverTypes.length > 0 && !isAlertType(resolverTypes, alert))) {
2626+ console.debug(`Pushing ${alertFriendly(alert)}`);
2727+ return await pushFunc();
2828+ }
2929+3030+ const id = resolveableAlertIdentifier(alert);
3131+3232+ if(alert.resolved) {
3333+ // if we had a waiting unresolved then the assumption is we *do not* want to notify of the resolved status
3434+ // since it was transient
3535+ if(resolverTimeouts.has(id)) {
3636+ console.debug(`Not pushing ${alertFriendly(alert)} because it is RESOVLED and had an UNRESOLVED notification that was wait waiting to be pushed (and has now been canceled).`)
3737+ clearTimeout(resolverTimeouts.get(id));
3838+ resolverTimeouts.delete(id);
3939+ return;
4040+ } else {
4141+ // if there was no unresolved then the unresolved was either already sent or there was never an unresolved to begin with
4242+ // in either case, notify of resolved
4343+ console.debug(`Pushing ${alertFriendly(alert)}`);
4444+ return await pushFunc();
4545+ }
4646+ } else {
4747+ // at this point we know the alert is unresolved
4848+ // and alert was either in resolverTypes or resolverTypes was empty (wait on all unresolved)
4949+5050+ if(resolverTimeouts.has(id)) {
5151+ // if alert was produced again, before previous timeout has ended, clear the previous
5252+ console.debug(`${alertFriendly(alert)} -- replacing previously waiting notification of same type (reset timeout)`);
5353+ clearTimeout(resolverTimeouts.get(id));
5454+ resolverTimeouts.delete(id);
5555+ }
5656+5757+ const timeout = setTimeout(async () => {
5858+ console.debug(`${alertFriendly(alert)} -- timeout of ${resolverTimeout} lapsed, pushing`);
5959+ await pushFunc();
6060+ resolverTimeouts.delete(id);
6161+ }, resolverTimeout);
6262+6363+ console.debug(`${alertFriendly(alert)} -- set timeout for ${resolverTimeout} ms`);
6464+ resolverTimeouts.set(id, timeout);
6565+ }
6666+6767+ return;
6868+ };
6969+}
+49-1
notifiers/common/options.ts
···11+import { Types } from "npm:komodo_client";
12import { valToBoolean } from "../../common/utils.ts";
2334export type ResolvedType = "resolved" | "unresolved";
55+export type AlertTypes = Types.Alert['data']['type'];
4657export interface CommonOptions {
68 levelInTitle?: boolean;
79 resolvedIndicator?: boolean;
810 allowedResolveTypes?: ResolvedType[];
1111+ resolverTimeout?: number
1212+ resolverTypes?: AlertTypes[]
913}
10141115export const asResolvedType = (val: string): val is ResolvedType => {
···1721 levelInTitle: lit,
1822 resolvedIndicator: ri,
1923 allowedResolveTypes: fr,
2424+ resolverTypes: rt,
2525+ resolverTimeout: rtime,
2026 } = data;
21272228 let levelInTitle = lit;
···5662 }
5763 }
5864 }
6565+6666+ let resolverTypes = rt;
6767+ if (resolverTypes === undefined) {
6868+ const types = Deno.env.get("UNRESOLVED_TIMEOUT_TYPES");
6969+ if(types === undefined || types.trim() === '') {
7070+ resolverTypes = undefined;
7171+ } else {
7272+ try {
7373+ resolverTypes = parseAlertTypesString(types);
7474+ } catch (e) {
7575+ throw new Error(`Could not parse 'UNRESOLVED_TIMEOUT_TYPES' ENV`, {cause: e});
7676+ }
7777+ }
7878+ }
7979+8080+ let resolverTimeout = rtime;
8181+ if (resolverTimeout === undefined) {
8282+ const timeVal = Deno.env.get("UNRESOLVED_TIMEOUT");
8383+ if(timeVal !== undefined) {
8484+ const time = Number.parseInt(timeVal);
8585+ if(Number.isNaN(time)) {
8686+ throw new Error(`Could not parse 'UNRESOLVED_TIMEOUT' ENV as a number`);
8787+ }
8888+ resolverTimeout = time;
8989+ }
9090+ }
9191+5992 return {
6093 levelInTitle,
6194 resolvedIndicator,
6262- allowedResolveTypes
9595+ allowedResolveTypes,
9696+ resolverTypes,
9797+ resolverTimeout
6398 }
6499};
65100···92127 }
93128 const allowedTruthyVals = resolvedTypesToVal(types);
94129 return allowedTruthyVals.includes(alertResolved);
130130+}
131131+132132+export const parseAlertTypesString = (str: string): AlertTypes[] => {
133133+ let types: AlertTypes[] = [];
134134+135135+ const splitTypes = str.split(',').map(x => x.trim().toLocaleLowerCase());
136136+ // TODO don't have a good way to generate types from TS and don't want to handcode
137137+ // const badTypes = splitTypes.filter(x => !asResolvedType(x));
138138+ // if(badTypes.length > 0) {
139139+ // throw new Error(`Invalid resolve types found, values must be either 'resolved' or 'unresolved'. Invalid found: ${badTypes.join(',')}`);
140140+ // }
141141+ types = Array.from(new Set(splitTypes)) as AlertTypes[];
142142+ return types;
95143}
+16-14
notifiers/discord/program.ts
···22import { EmbedBuilder, WebhookClient } from "npm:discord.js";
33import { CommonAlert, parseAlert } from "../common/alertParser.ts";
44import path from "node:path";
55-import { alertResolvedAllowed, parseOptions } from "../common/options.ts";
55+import { parseOptions } from "../common/options.ts";
66+import { createNotifierPipe } from "../common/notifierUtils.ts";
6778const program = () => {
89 const DISCORD_WEBHOOK: string = Deno.env.get("DISCORD_WEBHOOK") as string;
···6970 }
7071 };
71727373+ const doAlert = async (data: CommonAlert, alert: Types.Alert) => {
7474+ try {
7575+ await pushAlert(data, alert.level);
7676+ } catch (e) {
7777+ console.debug("Komodo Alert Payload:", alert);
7878+ console.error(
7979+ new Error("Failed to push Alert to Discord", { cause: e }),
8080+ );
8181+ }
8282+ }
8383+8484+ const notifierPipe = createNotifierPipe(commonOpts);
8585+7286 const server = Deno.serve({ port: 7000 }, async (req) => {
7387 const alert: Types.Alert = await req.json();
7488 console.log(`Recieved data from ${req.headers.get("host")}...`);
···8397 return new Response();
8498 }
85998686- if(!alertResolvedAllowed(commonOpts.allowedResolveTypes, alert.resolved)) {
8787- console.debug(`Not pushing alert because Alert is ${alert.resolved ? 'resolved' : 'unresolved'} which is not included in allowed resolved types of '${commonOpts.allowedResolveTypes}'`);
8888- return new Response();
8989- }
9090-9191- try {
9292- await pushAlert(data, alert.level);
9393- } catch (e) {
9494- console.debug("Komodo Alert Payload:", alert);
9595- console.error(
9696- new Error("Failed to push Alert to Gotify", { cause: e }),
9797- );
9898- }
100100+ await notifierPipe(alert, () => doAlert(data, alert))
99101100102 return new Response();
101103 });
+21-18
notifiers/gotify/program.ts
···22import { gotify } from "npm:gotify@1.1.0";
33import { CommonAlert, parseAlert } from "../common/alertParser.ts";
44import { titleAndSubtitle } from "../common/notifierBuilder.ts";
55-import { alertResolvedAllowed, parseOptions } from "../common/options.ts";
55+import { parseOptions } from "../common/options.ts";
66+import { createNotifierPipe } from "../common/notifierUtils.ts";
6778const program = () => {
89 const GOTIFY_URL: string = Deno.env.get("GOTIFY_URL") as string;
···7677 }
7778 };
78798080+ const doAlert = async (data: CommonAlert, alert: Types.Alert) => {
8181+ try {
8282+ await pushAlert(
8383+ titleAndSubtitle(data),
8484+ data.message ?? "",
8585+ severityLevelPriority[alert.level],
8686+ );
8787+ } catch (e) {
8888+ console.debug("Komodo Alert Payload:", alert);
8989+ console.error(
9090+ new Error("Failed to push Alert to Gotify", { cause: e }),
9191+ );
9292+ }
9393+9494+ }
9595+9696+ const notifierPipe = createNotifierPipe(commonOpts);
9797+7998 return Deno.serve({ port: 7000 }, async (req) => {
8099 const alert: Types.Alert = await req.json();
81100 console.log(`Recieved data from ${req.headers.get("host")}...`);
···90109 return new Response();
91110 }
921119393- if(!alertResolvedAllowed(commonOpts.allowedResolveTypes, alert.resolved)) {
9494- console.debug(`Not pushing alert because Alert is ${alert.resolved ? 'resolved' : 'unresolved'} which is not included in allowed resolved types of '${commonOpts.allowedResolveTypes}'`);
9595- return new Response();
9696- }
9797-9898- try {
9999- await pushAlert(
100100- titleAndSubtitle(data),
101101- data.message ?? "",
102102- severityLevelPriority[alert.level],
103103- );
104104- } catch (e) {
105105- console.debug("Komodo Alert Payload:", alert);
106106- console.error(
107107- new Error("Failed to push Alert to Gotify", { cause: e }),
108108- );
109109- }
112112+ await notifierPipe(alert, () => doAlert(data, alert))
110113111114 return new Response();
112115 });
+21-18
notifiers/ntfy/program.ts
···22import { Config, publish } from "npm:ntfy@1.7.0";
33import { CommonAlert, parseAlert } from "../common/alertParser.ts";
44import { titleAndSubtitle } from "../common/notifierBuilder.ts";
55-import { alertResolvedAllowed, parseOptions } from "../common/options.ts";
55+import { parseOptions } from "../common/options.ts";
66+import { createNotifierPipe } from "../common/notifierUtils.ts";
6778const program = () => {
89···112113 }
113114 };
114115115115- return Deno.serve({ port: 7000 }, async (req) => {
116116- const alert: Types.Alert = await req.json();
117117- console.log(`Recieved data from ${req.headers.get("host")}...`);
118116119119- let data: CommonAlert;
120120-121121- try {
122122- data = parseAlert(alert, commonOpts);
123123- } catch (e) {
124124- console.error(e);
125125- return new Response();
126126- }
127127-128128- if(!alertResolvedAllowed(commonOpts.allowedResolveTypes, alert.resolved)) {
129129- console.debug(`Not pushing alert because Alert is ${alert.resolved ? 'resolved' : 'unresolved'} which is not included in allowed resolved types of '${commonOpts.allowedResolveTypes}'`);
130130- return new Response();
131131- }
132132-117117+ const doAlert = async (data: CommonAlert, alert: Types.Alert) => {
133118 try {
134119 await pushAlert(
135120 titleAndSubtitle(data),
···144129 new Error("Failed to push Alert to ntfy", { cause: e }),
145130 );
146131 }
132132+ }
133133+134134+ const notifierPipe = createNotifierPipe(commonOpts);
135135+136136+ return Deno.serve({ port: 7000 }, async (req) => {
137137+ const alert: Types.Alert = await req.json();
138138+ console.log(`Recieved data from ${req.headers.get("host")}...`);
139139+140140+ let data: CommonAlert;
141141+142142+ try {
143143+ data = parseAlert(alert, commonOpts);
144144+ } catch (e) {
145145+ console.error(e);
146146+ return new Response();
147147+ }
148148+149149+ await notifierPipe(alert, () => doAlert(data, alert))
147150148151 return new Response();
149152 });