-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1ec5b74
Showing
7 changed files
with
359 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules/ | ||
package-lock.json | ||
config.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright (c) 2022-2023 Oleksandr Nemesh | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# Moodle Ping Bot | ||
Перевіряє доступність сайту Moodle та відправляє повідомлення в Telegram, при зміні статусу. | ||
Бота було створено у листопаді 2022 року, під час відключень світла, щоб відслідковувати доступність сайту НУБіП. | ||
|
||
 | ||
*[Elearn Status](https://t.me/elearn_nubip_status) - канал зі статусом сайту НУБіП* | ||
|
||
## Встановлення | ||
1. Створити бота в Telegram за допомогою [@BotFather](https://t.me/BotFather) | ||
2. Створити публічний канал та додати бота до нього. Надати боту права адміністратора. | ||
3. Перейменувати `config.example.json` у `config.json` та заповнити його | ||
4. Встановити залежності `npm install` | ||
5. Запустити бота `npm start` | ||
|
||
## Особливості | ||
- Не потребує бази даних, так як використовує закріплені повідомлення для збереження стану. | ||
- Відображає повну статистику доступності сайту (час відповіді, код статусу, час з останньої зміни статусу). | ||
- Не орієнтується на код 200, а перевіряє контент сторінки на наявність ключових слів. Може відрізнити технічні роботи від справної сторінки. | ||
- Відновлення стану бота після перезапуску, тому можна використовувати абсолютно будь-який хостинг, навіть якщо він робить регулярні рестарти. | ||
- Нове повідомлення в каналі буде створюватись лише при зміні статусу доступності сайту. В інших випадках буде оновлюватись існуюче повідомлення. | ||
- Повна підтримка локалізації. Зміна тексту повідомлень відбувається в `config.json`. | ||
- Для меншої кількості неправдивих сповіщень, при виявленні помилки, бот повторить спробу ще через 10 секунд (`'RecheckTime'` в `config.json`). Якщо помилка повториться, то сповіщення буде відправлено. | ||
- Повідомляє про такі статуси: Працює нормально, Працює повільно, Не працює, Технічні роботи, Помилка. | ||
|
||
## Ліцензія | ||
Код розповсюджується під [MIT License](LICENSE) і вільний для використання, зміни та розповсюдження. | ||
Велике прохання поважати автора і залишити посилання на цей репозиторій, якщо ви використовуєте цей код. | ||
|
||
## Підтримка | ||
Якщо ви знайшли помилку, або у вас є ідеї щодо покращення бота, будь ласка, створіть [нову проблему](https://github.com/prevter/moodle-ping-bot/issues/new) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"TOKEN": "<TELEGRAM_BOT_TOKEN>", | ||
"CHANNEL_ID": "<CHANNEL FOR POSTS (@example)>", | ||
"WEBSITE_URL": "https://elearn.nubip.edu.ua/", | ||
|
||
"WorkingMessage": "🟢 Elearn працює нормально", | ||
"SlowMessage": "🟡 Elearn працює повільно", | ||
"DownMessage": "🔴 Elearn не працює", | ||
"TechWorkMessage": "⚙️ Технічні роботи на Elearn", | ||
"ErrorMessage": "❌ Elearn повернув невідому помилку\nМожливо помилка бази даних, зазвичай зникає миттєво.", | ||
"SpecialErrorMessage": "❌ Elearn повернув помилку", | ||
"StatusCodeMessage": "Код помилки: {0}", | ||
"PingMessage": "Пінг: {0} мс", | ||
"TimeMessage": "Оновлено: {0}", | ||
"PassedTime": "Пройшло часу: {0}", | ||
|
||
"WorkingContent": "href=\"https://elearn.nubip.edu.ua/login/index.php\"", | ||
"TechnicalWorksContent": "<img src=\"/custompix/techworks.png\">", | ||
"CriticalPing": 6000, | ||
"CheckInterval": 60000, | ||
"RecheckTime": 10000, | ||
"Timeout": 60000 | ||
} |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
const config = require('./config.json'); | ||
const { Telegraf } = require('telegraf') | ||
const bot = new Telegraf(config.TOKEN) | ||
|
||
// wrapper for http and https modules to get correct module depending on url | ||
const get_http = (url) => { | ||
return url.startsWith('https') ? require('https') : require('http'); | ||
} | ||
|
||
let last_message = { | ||
status: 'unknown', | ||
ping: 0, | ||
message_id: -1, | ||
last_time: 0 | ||
} | ||
|
||
// syncronize last message with pinned message in channel | ||
const syncronizeLastMessage = () => { | ||
// load last message from channel | ||
return new Promise((resolve, reject) => { | ||
bot.telegram.getChat(config.CHANNEL_ID).then((chat) => { | ||
// get pinned message | ||
if (chat.pinned_message) { | ||
// try to find status in message | ||
last_message.message_id = chat.pinned_message.message_id; | ||
last_message.last_time = chat.pinned_message.date; | ||
if (chat.pinned_message.text.includes(config.WorkingMessage)) { | ||
last_message.status = 'online'; | ||
} | ||
else if (chat.pinned_message.text.includes(config.SlowMessage)) { | ||
last_message.status = 'slow'; | ||
} | ||
else if (chat.pinned_message.text.includes(config.TechWorkMessage)) { | ||
last_message.status = 'techwork'; | ||
} | ||
else if (chat.pinned_message.text.includes(config.ErrorMessage)) { | ||
last_message.status = 'error'; | ||
} | ||
else if (chat.pinned_message.text.includes(config.DownMessage)) { | ||
last_message.status = 'offline'; | ||
} | ||
else if (chat.pinned_message.text.includes(config.SpecialErrorMessage)) { | ||
last_message.status = 'special'; | ||
} | ||
if (last_message.status !== 'unknown') | ||
console.log(`Recovered last message: "${last_message.status}"`, "ID:", last_message.message_id, "Time:", last_message.last_time); | ||
} | ||
resolve(); | ||
}).catch((error) => { | ||
console.log(error); | ||
reject(error); | ||
}); | ||
}); | ||
} | ||
|
||
// wrapper for sending message to channel. also pins message and deletes the `message pinned` notification | ||
const send = async (msg, callback) => { | ||
bot.telegram.sendMessage(config.CHANNEL_ID, msg).then((message) => { | ||
callback(message.message_id); | ||
bot.telegram.pinChatMessage(config.CHANNEL_ID, message.message_id, { disable_notification: true }) | ||
.then(() => { | ||
bot.telegram.deleteMessage(config.CHANNEL_ID, message.message_id + 1); | ||
}); | ||
}); | ||
}; | ||
|
||
// get day or days depending on number (1 день, 2 дні, 5 днів) | ||
const getDayOrDays = (days) => { | ||
if (days % 10 === 1 && days % 100 !== 11) | ||
return 'день'; | ||
else if (days % 10 >= 2 && days % 10 <= 4 && (days % 100 < 10 || days % 100 >= 20)) | ||
return 'дні'; | ||
else | ||
return 'днів'; | ||
} | ||
|
||
// create string with passed time (1 день 2:30:15) | ||
const calculatePassedTime = (timestamp1, timestamp2) => { | ||
let timePassed = timestamp2 - timestamp1; | ||
let seconds = Math.floor(timePassed % 60); | ||
let minutes = Math.floor(timePassed / 60) % 60; | ||
let hours = Math.floor(timePassed / 3600) % 24; | ||
let days = Math.floor(timePassed / 86400); | ||
let result = ''; | ||
if (days > 0) | ||
result += `${days} ${getDayOrDays(days)} `; | ||
if (hours > 0 || result.length > 0) | ||
result += `${hours}:`; | ||
if (minutes > 0 || result.length > 0) | ||
result += `${minutes < 10 ? '0' : ''}${minutes}:`; | ||
else | ||
result += `${minutes}:`; | ||
result += `${seconds < 10 ? '0' : ''}${seconds}`; | ||
return result; | ||
}; | ||
|
||
// builds message depending on status | ||
const createMessage = (result) => { | ||
let pingMessage = config.PingMessage.replace('{0}', result.ping); | ||
let changedTimeMessage = config.TimeMessage.replace('{0}', new Date().toLocaleString('uk-UA', { timeZone: 'Europe/Kiev' })); | ||
let timePassedMessage = config.PassedTime.replace('{0}', calculatePassedTime(last_message.last_time, Date.now() / 1000)); | ||
switch (result.status) { | ||
case 'online': | ||
return `${config.WorkingMessage}\n${pingMessage}\n${changedTimeMessage}\n${timePassedMessage}`; | ||
case 'slow': | ||
return `${config.SlowMessage}\n${pingMessage}\n${changedTimeMessage}\n${timePassedMessage}`; | ||
case 'techwork': | ||
return `${config.TechWorkMessage}\n${pingMessage}\n${changedTimeMessage}\n${timePassedMessage}`; | ||
case 'error': | ||
return `${config.ErrorMessage}\n${pingMessage}\n${changedTimeMessage}\n${timePassedMessage}`; | ||
case 'offline': | ||
return `${config.DownMessage}\n${changedTimeMessage}\n${timePassedMessage}`; | ||
case 'special': | ||
var message = config.StatusCodeMessage.replace('{0}', result.statusCode + ' ' + result.statusMessage); | ||
return `${config.SpecialErrorMessage}\n${message}\n${pingMessage}\n${changedTimeMessage}\n${timePassedMessage}`; | ||
} | ||
} | ||
|
||
// triggered when status changed. decides whether to send new message or edit old one | ||
const statusChanged = (result) => { | ||
// bot haven't sent any message yet | ||
if (last_message.message_id === -1) { | ||
last_message.status = result.status; | ||
last_message.ping = result.ping; | ||
last_message.last_time = Date.now() / 1000; | ||
send(createMessage(result), (message_id) => { | ||
last_message.message_id = message_id; | ||
}); | ||
} | ||
else { | ||
if (last_message.status !== result.status) { | ||
// before changing status, edit old message to show how much time passed | ||
bot.telegram.editMessageText(config.CHANNEL_ID, last_message.message_id, null, createMessage(result)); | ||
|
||
// then send new message | ||
last_message.status = result.status; | ||
last_message.ping = result.ping; | ||
last_message.last_time = Date.now() / 1000; | ||
send(createMessage(result), (message_id) => { | ||
last_message.message_id = message_id; | ||
}); | ||
} | ||
else { | ||
// status didn't change, edit old message | ||
last_message.ping = result.ping; | ||
bot.telegram.editMessageText(config.CHANNEL_ID, last_message.message_id, null, createMessage(result)); | ||
} | ||
} | ||
} | ||
|
||
const check = () => { | ||
return new Promise((resolve, reject) => { | ||
let start = Date.now(); | ||
const request = get_http(config.WEBSITE_URL).get(config.WEBSITE_URL, { | ||
timeout: config.Timeout | ||
}, (response) => { | ||
let data = ''; | ||
response.on('data', (chunk) => { | ||
data = data + chunk.toString(); | ||
}); | ||
|
||
response.on('end', () => { | ||
let time = Date.now() - start; | ||
let returnObject = { | ||
status: 'online', | ||
ping: time, | ||
statusCode: response.statusCode | ||
}; | ||
|
||
if (response.statusCode === 200 && data.includes(config.WorkingContent)) { | ||
if (time > config.CriticalPing) { | ||
returnObject.status = 'slow'; | ||
} | ||
} | ||
else if (data.includes(config.TechnicalWorksContent) || response.statusMessage === 'Moodle under maintenance') { | ||
returnObject.status = 'techwork'; | ||
} | ||
else { | ||
switch (response.statusCode) { | ||
case 200: | ||
returnObject.status = 'error'; | ||
break; | ||
default: | ||
returnObject.status = 'special'; | ||
returnObject.statusMessage = response.statusMessage; | ||
break; | ||
} | ||
} | ||
|
||
resolve(returnObject); | ||
}); | ||
}); | ||
|
||
request.on('error', error => { | ||
resolve({ | ||
status: 'offline', | ||
statusCode: -1, | ||
ping: 0 | ||
}); | ||
}); | ||
}); | ||
}; | ||
|
||
// logs status to console | ||
const logStatus = (result) => { | ||
switch (result.status) { | ||
case 'online': | ||
console.log(`\x1b[32mOnline\x1b[37m, Ping: ${result.ping} ms`); | ||
break; | ||
case 'offline': | ||
console.log(`\x1b[31mOffline\x1b[37m`); | ||
break; | ||
case 'slow': | ||
console.log(`\x1b[33mSlow\x1b[37m, Ping: ${result.ping} ms`); | ||
break; | ||
case 'error': | ||
case 'special': | ||
console.log(`\x1b[36mError\x1b[37m, \x1b[35mStatus:\x1b[37m \x1b[4m${result.statusCode}\x1b[0m, Ping: ${result.ping} ms`); | ||
break; | ||
case 'techwork': | ||
console.log(`\x1b[35mTechwork\x1b[37m, Ping: ${result.ping} ms`); | ||
break; | ||
} | ||
} | ||
|
||
// called when status changes to anything except online. | ||
// double checks status to make sure it's not a false alarm | ||
const retry = (result) => { | ||
return new Promise((resolve, reject) => { | ||
setTimeout(() => { | ||
check().then((new_result) => { | ||
if (result.status === new_result.status) { | ||
var average_time = (result.ping + new_result.ping) / 2; | ||
result.ping = average_time; | ||
logStatus(result); | ||
statusChanged(result); | ||
} | ||
resolve(); | ||
}); | ||
}, config.RetryInterval); | ||
}); | ||
}; | ||
|
||
// start checking loop. if status is not online, retry after some time | ||
const start = () => { | ||
check().then((result) => { | ||
if (result.status !== 'online' && result.status !== 'techwork') { | ||
retry(result).then(() => { | ||
setTimeout(start, config.CheckInterval); | ||
}); | ||
} else { | ||
logStatus(result); | ||
statusChanged(result); | ||
setTimeout(start, config.CheckInterval); | ||
} | ||
}); | ||
} | ||
|
||
bot.launch(); | ||
syncronizeLastMessage().then(() => start()); | ||
|
||
// Enable graceful stop | ||
process.once('SIGINT', () => bot.stop('SIGINT')); | ||
process.once('SIGTERM', () => bot.stop('SIGTERM')); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"name": "moodle-ping-bot", | ||
"version": "1.0.0", | ||
"description": "A simple telegram bot that pings a websites and sends a message to a telegram chat if the website is down.", | ||
"main": "index.js", | ||
"scripts": { | ||
"start": "node index.js", | ||
"dev": "nodemon index.js" | ||
}, | ||
"author": "Prevter", | ||
"license": "MIT", | ||
"dependencies": { | ||
"telegraf": "^4.11.2" | ||
}, | ||
"devDependencies": { | ||
"nodemon": "^3.0.1" | ||
} | ||
} |