-
Notifications
You must be signed in to change notification settings - Fork 1
/
avalon-alerts-bot.js
282 lines (230 loc) · 8.07 KB
/
avalon-alerts-bot.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
const fs = require('fs');
const fetch = require('node-fetch');
const formatDistance = require('date-fns/formatDistance');
const config = require('./config.json');
var currentAPI = 0;
var retries = 0;
var db = {
down: [],
missers: {},
leaders: []
};
const watcher = async () => {
try {
// Save old leaders to compare
const old = db.leaders;
// Get new leaders data
await update_db_leaders();
// Alert leaders that unregistered
old.filter(o => (db.leaders.find(l => l.name === o.name) === undefined)).map(async leader => await telegram(`Leader${candidate} \`${leader.name}\` unregistered`));
// Actual missers
const missers = Object.keys(db.missers);
// Compare new leaders from db with old
db.leaders.map(async (leader, index) => {
// Check if this leader is producing or just a candidate
const candidate = index < 13 ? '' : ' candidate';
// Find the old leader
const oldLeader = old.find(l => l.name === leader.name);
// Leader not found in old leaders db?
if (oldLeader === undefined) {
await telegram(`Leader${candidate} \`${leader.name}\` registered`);
return;
}
// Is this leader an actual misser?
if (missers.includes(leader.name)) {
// Get misser data from db
const misser = db.missers[leader.name];
// Calc total and new misses
const total = leader.missed - misser.start + 1;
const misses = leader.missed - misser.last;
// First, check if started producing again or got out of schedule
if (leader.missed === oldLeader.missed) {
// Sometimes the API is still not updated when this watcher is run causing
// false 'back producing' messages, so ignore if on schedule and still not produced
// blocks yet
if (candidate || leader.produced > oldLeader.produced) {
const action = !candidate ? 'started producing again' : 'is out of schedule';
await telegram(`Leader${candidate} \`${leader.name}\` ${action}, after missing *${total}* block(s), total blocks missed now is *${leader.missed}*`);
// Remove misser from db
delete db.missers[leader.name];
savedb();
}
return;
}
// Get triggers from config
const repeater = config.watcher.triggers[0];
const triggers = config.watcher.triggers.slice(1);
var message = false;
// Total misses are less than repeater trigger?
if (total < repeater) {
// Message if found one that fits, through all triggers that didn't fire yet
message = (triggers.find(t => (t >= (total - misses) && t <= misses)) !== undefined);
} else {
// Message if new misses greater or equal than repeater
message = (misses >= repeater);
}
// Send message?
if (message) {
await telegram(`Leader${candidate} \`${leader.name}\` continues missing, now with *${total}* block(s) missed`);
// Update last message missed in db
misser.last = leader.missed;
savedb();
}
} else {
// Calc the misses
const misses = leader.missed - (oldLeader.missed || 0);
// Are there any misses?
if (misses > 0) {
// Add to missers in db
db.missers[leader.name] = {
produced: leader.produced,
start: oldLeader.missed + 1,
last: leader.missed
};
savedb();
await telegram(`Leader${candidate} \`${leader.name}\` missed *${misses}* block(s)`);
}
}
});
} catch (e) {
console.error('API node', config.apis[currentAPI], 'failed to retrieve leader data, reason:', e);
// Retry the watcher because this might happen due to communication errors with the node...
console.log('Retrying the watcher in a bit...');
scheduleRetry(watcher);
return;
}
savedb();
}
const APIwatcher = async () => {
// Save old leaders to compare
const old = db.down || [];
const nodes = await get_api_nodes_down();
// If lost contact with all nodes, maybe it's a network issue?
if (nodes.length === config.apiwatcher.nodes.length) {
console.log('Lost contact with all API nodes at once, maybe network issue? Skipping...');
return;
}
// Get current timestamp
const now = Date.now();
// Alert api nodes back up
old.filter(api => !nodes.includes(api.node)).map(async api => await telegram(`API node ${api.node} is back up, it was down for ${formatDistance(new Date(api.timestamp), new Date())}`));
// Process api nodes down
const down = nodes.map(node => {
// Find if this node was already down
const oldDown = old.find(api => api.node === node);
const timestamp = oldDown ? oldDown.timestamp : now;
return { node, timestamp };
});
// Save api nodes down to db
db.down = down;
savedb();
// Send alerts for down nodes
down.map(async api => {
// Was this node already down?
if (api.timestamp !== now) {
// Get the seconds down
const secs = Math.round((now - api.timestamp) / 1000);
// Find a trigger that fits if any
const message = (config.apiwatcher.triggers.find(t => Math.abs(secs - t) < 30) !== undefined) || ((secs % config.apiwatcher.triggers[0]) < 30);
// Send message?
if (message) {
await telegram(`API node ${api.node} has been down for ${formatDistance(new Date(api.timestamp), new Date())}`);
}
} else {
await telegram(`API node ${api.node} went down`);
}
});
}
// helpers
const nextAPI = () => currentAPI < (config.apis.length - 1) ? currentAPI + 1 : 0;
const scheduleRetry = (action) => {
currentAPI = nextAPI();
if (retries++ < config.watcher.retries) {
setTimeout(action, config.intervals.retry);
} else {
// Reached retries limit
console.log('Reached the retries limit, giving up...');
retries = 0;
}
}
const update_db_leaders = async () => {
return fetch(`${config.apis[currentAPI]}/rank/leaders`)
.then(res => res.json())
.then(leaders => {
if (!leaders || !Array.isArray(leaders)) {
console.log('Failed updating leaders data:', leaders);
return;
}
db.leaders = leaders;
})
.catch (err => {
console.error('Error updating leaders data');
console.error(err);
});
}
const get_api_nodes_down = async () => {
const down = await Promise.all(config.apiwatcher.nodes.map(async api => {
try {
const res = await fetch(`${api}/count`, { timeout: 5000 });
return (!res.ok);
} catch (e) {
console.error('API watcher node', api, 'fetch failed, reason:', e);
return true;
}
}));
return config.apiwatcher.nodes.filter((_v, index) => down[index]);
}
const telegram = async (msg) => {
if (config.telegram && config.telegram.apiurl && config.telegram.apikey && config.telegram.apikey !== '') {
const body = {
chat_id: config.telegram.chat,
text: msg,
parse_mode: 'markdown'
};
return fetch(`${config.telegram.apiurl}${config.telegram.apikey}/sendMessage`, {
method: 'post',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
}
})
.then(res => res.json())
.then(json => {
if (!json.ok) {
console.log('Failed sending telegram message:', json);
}
})
.catch (err => {
console.error('Error sending telegram message');
console.error(err)
});
} else {
console.log('TM Message:', msg);
}
}
const loaddb = () => {
try {
db = JSON.parse(fs.readFileSync(config.db));
}
catch (e) {
console.log('Error loading DB:', e.message);
}
}
const savedb = () => {
try {
fs.writeFileSync(config.db, JSON.stringify(db, null, 2));
}
catch (e) {
console.log('Error saving DB:', e.message);
}
}
// boot up the bot
// telegram('Avalon alerts bot starting...');
// load the database
loaddb();
// start watcher
setInterval(watcher, config.intervals.watcher);
// start API watcher
setInterval(APIwatcher, config.intervals.apiwatcher);
// do 1st watcher round now
setImmediate(watcher);