Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Prevter committed Oct 29, 2023
0 parents commit 1ec5b74
Show file tree
Hide file tree
Showing 7 changed files with 359 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
package-lock.json
config.json
21 changes: 21 additions & 0 deletions LICENSE
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.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Moodle Ping Bot
Перевіряє доступність сайту Moodle та відправляє повідомлення в Telegram, при зміні статусу.
Бота було створено у листопаді 2022 року, під час відключень світла, щоб відслідковувати доступність сайту НУБіП.

![Screenshot](doc/bot.png)
*[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)
23 changes: 23 additions & 0 deletions config.example.json
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
}
Binary file added doc/bot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
264 changes: 264 additions & 0 deletions index.js
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'));
18 changes: 18 additions & 0 deletions package.json
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"
}
}

0 comments on commit 1ec5b74

Please sign in to comment.