1import IntegrationData from '../models/IntegrationData.js';
2import integrationModules from '../integrations/index.js';
3
4const integrations = {};
5
6const events = {};
7
8const lastPings = {};
9
10const registerEvent = (module) => (name, callback) => {
11 if (!events[name]) events[name] = [];
12 events[name].push({module, callback});
13}
14
15const shouldThrottlePing = (eventName, integration) => {
16 if (eventName !== "minutePassed") return false;
17
18 const intervalRaw = integration.data?.interval;
19 const interval = Number.isInteger(intervalRaw) && intervalRaw > 0 ? intervalRaw : 1;
20 if (interval <= 1) return false;
21
22 const now = Date.now();
23 const last = lastPings[integration.id];
24 if (last !== undefined && now - last < interval * 60 * 1000 - 30 * 1000) return true;
25
26 lastPings[integration.id] = now;
27 return false;
28}
29
30const getActiveByName = async (name) => {
31 const data = await IntegrationData.findAll({where: {name: name}});
32 if (!data) return null;
33
34 return data.map((item) => ({...item, data: JSON.parse(item.data)}));
35}
36
37const triggerActivity = async (id, error = false) => {
38 await IntegrationData.update({lastActivity: new Date().toISOString(), activityFailed: error}, {where: {id: id}});
39}
40
41export const triggerEvent = async (name, data) => {
42 if (!events[name]) return;
43
44 for (const module of events[name]) {
45 const active = await getActiveByName(module.module);
46 for (const integration of active) {
47 if (shouldThrottlePing(name, integration)) continue;
48 await module.callback(integration, data, (error = false) => triggerActivity(integration.id, error));
49 }
50 }
51}
52
53export const clearPingState = (id) => {
54 delete lastPings[id];
55}
56
57export const initialize = async () => {
58 for (const { name, setup } of integrationModules) {
59 integrations[name] = setup(registerEvent(name));
60 console.log(`Integration "${name}" loaded successfully`);
61 }
62};
63
64export const getActive = async () => {
65 const data = await IntegrationData.findAll();
66 if (!data) return null;
67
68 return data.map((item) => ({...item, data: JSON.parse(item.data)}));
69}
70
71export const getIntegrationById = (id) => IntegrationData.findOne({where: {id: id}});
72
73export const deleteIntegration = async (id) => {
74 const data = await IntegrationData.findOne({where: {id}});
75 if (!data) return null;
76
77 await IntegrationData.destroy({where: {id}});
78 clearPingState(id);
79 return true;
80}
81
82export const create = async (name, data) => {
83 const integration = integrations[name];
84 if (!integration) return null;
85
86 const displayName = data.integration_name;
87 delete data.integration_name;
88
89 const created = await IntegrationData.create({name: name, data: data, displayName});
90
91 return created.id;
92}
93
94export const patch = async (id, data) => {
95 const item = await IntegrationData.findOne({where: {id: id}});
96 if (!item) return null;
97
98 const displayName = data.integration_name;
99 delete data.integration_name;
100
101 IntegrationData.update({data: {...JSON.parse(item.data), ...data}, displayName}, {where: {id: id}});
102 clearPingState(id);
103 return true;
104}
105
106export const getIntegrations = () => {
107 const result = {};
108
109 for (const [name, integration] of Object.entries(integrations)) {
110 const updatedIntegration = {...integration};
111
112 updatedIntegration.fields = updatedIntegration.fields.map((field) => ({
113 ...field, regex: field.regex ? field.regex.source : undefined
114 }));
115
116 result[name] = {name, ...updatedIntegration};
117 }
118
119 return result;
120};
121
122export const getIntegration = (name) => integrations[name];
123
124export const validateInput = (module, data) => {
125 const integration = integrations[module];
126 if (!integration) return false;
127
128 for (const field of integration.fields) {
129 if (field.required && (data[field.name] === undefined || data[field.name] === null || data[field.name] === "")) return false;
130
131 if (data[field.name] !== undefined && data[field.name] !== null && data[field.name] !== "") {
132 if (field.regex && !new RegExp(field.regex).test(data[field.name])) return false;
133 if (field.type === "text" && data[field.name].length > 250) return false;
134 if (field.type === "textarea" && data[field.name].length > 2000) return false;
135 if (field.type === "boolean" && typeof data[field.name] !== "boolean") return false;
136 if (field.type === "number") {
137 const num = Number(data[field.name]);
138 if (!Number.isInteger(num)) return false;
139 if (field.min !== undefined && num < field.min) return false;
140 if (field.max !== undefined && num > field.max) return false;
141 data[field.name] = num;
142 }
143 }
144 }
145
146 const result = {};
147 for (const field of integration.fields) result[field.name] = data[field.name];
148 result["integration_name"] = data["integration_name"];
149
150 return result;
151}