Merge pull request #421 from allerta-vvf/master

Implement Telegram bot
This commit is contained in:
Matteo Gheza 2022-01-05 21:29:59 +01:00 committed by GitHub
commit f1dec9b862
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 280 additions and 53 deletions

View File

@ -87,9 +87,9 @@ function apiRouter (FastRoute\RouteCollector $r) {
requireLogin() || accessDenied();
$users->online_time_update();
if($users->hasRole(Role::FULL_VIEWER)) {
$response = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, availability_minutes ASC, name ASC");
$response = $db->select("SELECT * FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC");
} else {
$response = $db->select("SELECT `id`, `chief`, `online_time`, `available`, `name` FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, availability_minutes ASC, name ASC");
$response = $db->select("SELECT `id`, `chief`, `online_time`, `available`, `name` FROM `".DB_PREFIX."_profiles` ORDER BY available DESC, chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC");
}
apiResponse(
!is_null($response) ? $response : []
@ -219,24 +219,15 @@ function apiRouter (FastRoute\RouteCollector $r) {
['POST'],
'/availability',
function ($vars) {
global $users, $db;
global $users, $availability;
requireLogin() || accessDenied();
$users->online_time_update();
if(!$users->hasRole(Role::FULL_VIEWER) && $_POST["id"] !== $users->auth->getUserId()){
exit;
}
$user_id = is_numeric($_POST["id"]) ? $_POST["id"] : $users->auth->getUserId();
logger("Disponibilità cambiata in ".($_POST["available"] ? '"disponibile"' : '"non disponibile"'), $user_id, $users->auth->getUserId());
apiResponse([
"response" => $db->update(
DB_PREFIX.'_profiles',
[
'available' => $_POST['available'], 'availability_last_change' => 'manual'
],
[
'id' => $user_id
]
),
"response" => $availability->change($_POST["available"], $user_id),
"updated_user" => $user_id,
"updated_user_name" => $users->getName($user_id)
]);
@ -267,6 +258,28 @@ function apiRouter (FastRoute\RouteCollector $r) {
}
);
$r->addRoute(
['POST'],
'/telegram_login_token',
function ($vars) {
global $users, $db;
requireLogin() || accessDenied();
$users->online_time_update();
$token = bin2hex(random_bytes(16));
apiResponse([
"response" => $db->insert(
DB_PREFIX.'_bot_telegram',
[
'user' => $users->auth->getUserId(),
'tmp_login_token' => $token
]
),
"start_link" => "https://t.me/".BOT_TELEGRAM_USERNAME."?start=".$token,
"token" => $token
]);
}
);
$r->addRoute(
['POST'],
'/login',

View File

@ -11,7 +11,7 @@
],
"require": {
"delight-im/auth": "dev-master",
"ulrichsg/getopt-php": "4.0.0",
"ulrichsg/getopt-php": "4.0.1",
"nikic/fast-route": "^2.0@dev",
"spatie/array-to-xml": "3.1.0",
"ezyang/htmlpurifier": "4.14.0",

18
backend/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c24f492bcd977f01ae44c4ee0c7ae1e4",
"content-hash": "9193956804bd765f7fe3f29d0c61472e",
"packages": [
{
"name": "azuyalabs/yasumi",
@ -3107,16 +3107,16 @@
},
{
"name": "ulrichsg/getopt-php",
"version": "v4.0.0",
"version": "v4.0.1",
"source": {
"type": "git",
"url": "https://github.com/getopt-php/getopt-php.git",
"reference": "073e809a5a4811b0caec43a2151b0e8ccbe29e4b"
"reference": "ad9e8cdf1b709caf84ca545ff90bcc7298f0fa5a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/getopt-php/getopt-php/zipball/073e809a5a4811b0caec43a2151b0e8ccbe29e4b",
"reference": "073e809a5a4811b0caec43a2151b0e8ccbe29e4b",
"url": "https://api.github.com/repos/getopt-php/getopt-php/zipball/ad9e8cdf1b709caf84ca545ff90bcc7298f0fa5a",
"reference": "ad9e8cdf1b709caf84ca545ff90bcc7298f0fa5a",
"shasum": ""
},
"require": {
@ -3147,13 +3147,13 @@
"email": "thflori@gmail.com"
}
],
"description": "Command line arguments parser for PHP 5.4 - 7.3",
"description": "Command line arguments parser for PHP 7.1 and above",
"homepage": "http://getopt-php.github.io/getopt-php",
"support": {
"issues": "https://github.com/getopt-php/getopt-php/issues",
"source": "https://github.com/getopt-php/getopt-php/tree/v4.0.0"
"source": "https://github.com/getopt-php/getopt-php/tree/v4.0.1"
},
"time": "2021-05-02T13:05:10+00:00"
"time": "2022-01-04T11:01:04+00:00"
}
],
"packages-dev": [
@ -3265,5 +3265,5 @@
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.1.0"
"plugin-api-version": "2.2.0"
}

View File

@ -22,6 +22,7 @@ define('JWT_PRIVATE_KEY', '@@private_key@@');
/* Telegram bot options */
define('BOT_TELEGRAM_API_KEY', '');
define('BOT_TELEGRAM_USERNAME', '');
define('BOT_TELEGRAM_DEBUG_USER', null);
/* Sentry options */
define('SENTRY_CSP_REPORT_URI', '');

View File

@ -4,16 +4,81 @@ use skrtdev\Telegram\Message;
require_once 'utils.php';
function telegramBotRouter() {
if(defined("BASE_PATH")){
$base_path = "/".BASE_PATH."api/bot/telegram";
} else {
$base_path = "/api/bot/telegram";
}
$_SERVER['SCRIPT_URL'] = $base_path;
define('NONE', 0);
define('WEBHOOK', 1);
$Bot = null;
//TODO: fix NovaGram ip check and enable it again
$Bot = new Bot(BOT_TELEGRAM_API_KEY, ["disable_ip_check" => true]);
function initializeBot($mode = WEBHOOK) {
global $Bot;
if (is_null($Bot)) {
if(defined("BASE_PATH")){
$base_path = "/".BASE_PATH."api/bot/telegram";
} else {
$base_path = "/api/bot/telegram";
}
$_SERVER['SCRIPT_URL'] = $base_path;
$NovagramConfig = [
"disable_ip_check" => true, //TODO: fix NovaGram ip check and enable it again
"parse_mode" => "HTML",
"mode" => $mode
];
if(defined("BOT_TELEGRAM_DEBUG_USER")){
$NovagramConfig["debug"] = BOT_TELEGRAM_DEBUG_USER;
}
$Bot = new Bot(BOT_TELEGRAM_API_KEY, $NovagramConfig);
}
}
function getUserIdByMessage(Message $message)
{
global $db;
return $db->selectValue("SELECT user FROM `".DB_PREFIX."_bot_telegram` WHERE `chat_id` = ?", [$message->from->id]);
}
function requireBotLogin(Message $message)
{
$userId = getUserIdByMessage($message);
if ($userId === null) {
$message->reply(
"Non hai ancora collegato il tuo account Allerta al bot.".
"\nPer farlo, premere su <strong>\"Collega l'account al bot Telegram\"</strong>."
);
exit();
}
}
function sendTelegramNotification($message)
{
global $Bot, $db;
if(is_null($Bot)) initializeBot(NONE);
//TODO: implement different types of notifications
//TODO: add command for subscribing to notifications
$chats = $db->select("SELECT `chat_id` FROM `".DB_PREFIX."_bot_telegram_notifications`");
if(!is_null($chats)) {
foreach ($chats as $chat) {
$chat = $chat['chat_id'];
$Bot->sendMessage([
"chat_id" => $chat,
"text" => $message
]);
}
}
}
function yesOrNo($value)
{
return ($value === 1 || $value) ? '<b>SI</b>' : '<b>NO</b>';
}
function telegramBotRouter() {
global $Bot;
initializeBot();
$Bot->addErrorHandler(function ($e) {
print('Caught '.get_class($e).' exception from general handler'.PHP_EOL);
@ -21,12 +86,113 @@ function telegramBotRouter() {
});
$Bot->onCommand('start', function (Message $message, array $args = []) {
var_dump($message, $args);
$message->reply('Hey! Nice to meet you. Use /info to know more about me.');
global $db;
if(isset($args[0])) {
$registered_chats = $db->select("SELECT * FROM `".DB_PREFIX."_bot_telegram` WHERE `chat_id` = ?", [$message->from->id]);
if(!is_null($registered_chats) && count($registered_chats) > 1) {
$message->chat->sendMessage(
"⚠️ Questo account Allerta è già associato ad un'altro utente Telegram.".
"\nContattare un amministratore."
);
return;
}
$response = $db->update(
DB_PREFIX.'_bot_telegram',
['chat_id' => $message->from->id],
['tmp_login_token' => $args[0]]
);
if($response === 1) {
$message->chat->sendMessage(
"✅ Login avvenuto con successo!".
"\nPer ottenere informazioni sul profilo, utilizzare il comando /info".
"\nPer ricevere informazioni sui comandi, utilizzare il comando /help o visualizzare il menu dei comandi da Telegram"
);
} else {
$message->chat->sendMessage(
"⚠️ Chiave di accesso non valida, impossibile eseguire il login.".
"\nRiprovare o contattare un amministratore."
);
}
} else {
$message->chat->sendMessage(
"Per iniziare, è necessario collegare l'account di Allerta con Telegram.".
"\nPer farlo, premere su <strong>\"Collega l'account al bot Telegram\"</strong>."
);
}
});
$Bot->onCommand('help', function (Message $message, array $args = []) {
$message->chat->sendMessage(
" Elenco dei comandi disponibili:".
"\n/info - Ottieni informazioni sul profilo connesso".
"\n/help - Ottieni informazioni sui comandi".
"\n/attiva - Modifica la tua disponibilità in \"reperibile\"".
"\n/disattiva - Modifica la tua disponibilità in \"non reperibile\"".
"\n/elenco_disponibili - Mostra un elenco dei vigili attualmente disponibili"
);
});
$Bot->onCommand('info', function (Message $message) {
$message->reply('Well, I\'m just an example, but you can learn more about NovaGram at docs.novagram.ga');
global $users;
$user_id = getUserIdByMessage($message);
if(is_null($user_id)) {
$message->chat->sendMessage('⚠️ Questo account Telegram non è associato a nessun utente di Allerta.');
} else {
$user = $users->get_user($user_id);
$message->chat->sendMessage(
" Informazioni sul profilo:".
"\n<i>Nome:</i> <b>".$user["name"]."</b>".
"\n<i>Disponibile:</i> ".yesOrNo($user["available"]).
"\n<i>Caposquadra:</i> ".yesOrNo($user["chief"] === 1).
"\n<i>Autista:</i> ".yesOrNo($user["driver"] === 1).
"\n<i>Interventi svolti:</i> <b>".$user["services"]."</b>".
"\n<i>Esercitazioni svolte:</i> <b>".$user["trainings"]."</b>".
"\n<i>Minuti di disponibilità:</i> <b>".$user["availability_minutes"]."</b>"
);
}
});
//Too difficult and "spaghetti to explain it here in comments, please use https://regexr.com/
//Jokes apart, checks if text contains something like "Attiva", "attiva", "Disponibile", "disponibile" but not "Non ", "non ", "Non_", "non_", "Dis" or "dis"
$Bot->onText("/\/?(Sono |sono |Io sono |Io sono )?(?<!non( |_))(?<!dis)(?<!Non( |_))(?<!Dis)(Attiva|Attivami|Attivo|Disponibile|Operativo|attiva|attivami|attivo|disponibile|operativo)/", function (Message $message, $matches = []) {
global $availability;
requireBotLogin($message);
if(count(explode(" ", $message->text)) > 3) return;
$user_id = getUserIdByMessage($message);
$availability->change(0, $user_id);
$message->reply("Disponibilità aggiorata con successo.\nOra sei <b>operativo</b>.");
});
$Bot->onText("/\/?(Io |Io sono )?(Disattiva|Disattivami|Non( |_)attivo|Non( |_)(Sono |sono )?disponibile|Non( |_)(Sono |sono )?operativo|disattiva|sisattivami|non( |_)(Sono |sono )?attivo|non( |_)(Sono |sono )?disponibile|non( |_)(Sono |sono )?operativo)/", function (Message $message, $matches = []) {
global $availability;
requireBotLogin($message);
if(count(explode(" ", $message->text)) > 4) return;
$user_id = getUserIdByMessage($message);
$availability->change(0, $user_id);
$message->reply("Disponibilità aggiorata con successo.\nOra sei <b>non operativo</b>.");
});
$Bot->onText("/\/?(Elenco|elenco|Elenca|elenca)(_| )(Disponibili|disponibili)/", function (Message $message, $matches = []) {
global $db, $users;
requireBotLogin($message);
if(count(explode(" ", $message->text)) > 2) return;
$result = $db->select("SELECT `chief`, `driver`, `available`, `name` FROM `".DB_PREFIX."_profiles` WHERE available = 1 and hidden = 0 ORDER BY chief DESC, services ASC, trainings DESC, availability_minutes ASC, name ASC");
var_dump($result);
if(!is_null($result) && count($result) > 0) {
$msg = " Vigili attualmente disponibili:";
foreach($result as $user) {
$msg .= "\n<b>".$user["name"]."</b>";
if($user["driver"]) $msg .= " 🚒";
if($user["chief"]) {
$msg .= " 🟥";
} else {
$msg .= "";
}
}
} else {
$msg = "⚠️ Nessun vigile disponibile.";
}
$message->reply($msg);
});
$Bot->start();

View File

@ -16,10 +16,14 @@ $db = \Delight\Db\PdoDatabase::fromDsn(
)
);
CacheManager::setDefaultConfig(new ConfigurationOption([
'path' => realpath(dirname(__FILE__).'/tmp')
]));
$cache = CacheManager::getInstance('files');
try {
CacheManager::setDefaultConfig(new ConfigurationOption([
'path' => realpath(dirname(__FILE__).'/tmp')
]));
$cache = CacheManager::getInstance('files');
} catch(Exception $e) {
$cache = null;
}
$options = new Options($db, $cache);
function get_option($name, $default=null) {
global $options;
@ -110,12 +114,16 @@ class options
$this->db = $db;
$this->cache = $cache;
if(!$bypassCache){
$this->optionsCache = $this->cache->getItem("options");
if (is_null($this->optionsCache->get())) {
$this->optionsCache->set($db->select("SELECT * FROM `".DB_PREFIX."_options` WHERE `enabled` = 1"))->expiresAfter(60);
$this->cache->save($this->optionsCache);
try {
$this->optionsCache = $this->cache->getItem("options");
if (is_null($this->optionsCache->get())) {
$this->optionsCache->set($db->select("SELECT * FROM `".DB_PREFIX."_options` WHERE `enabled` = 1"))->expiresAfter(60);
$this->cache->save($this->optionsCache);
}
$this->options = $this->optionsCache->get();
} catch(Exception $e) {
$this->options = $db->select("SELECT * FROM `".DB_PREFIX."_options` WHERE `enabled` = 1");
}
$this->options = $this->optionsCache->get();
} else {
$this->options = $db->select("SELECT * FROM `".DB_PREFIX."_options` WHERE `enabled` = 1");
}
@ -222,6 +230,7 @@ class Users
public function isHidden($id=null)
{
if(is_null($id)) $id = $this->auth->getUserId();
if(is_null($id)) return true;
$user = $this->db->selectRow("SELECT * FROM `".DB_PREFIX."_profiles` WHERE `id` = ?", [$id]);
return $user["hidden"];
}
@ -239,6 +248,37 @@ class Users
}
}
class Availability {
private $db = null;
private $users = null;
public function __construct($db, $users)
{
$this->db = $db;
$this->users = $users;
}
public function change($availability, $user_id)
{
logger("Disponibilità cambiata in ".($availability ? '"disponibile"' : '"non disponibile"'), $user_id, $this->users->auth->getUserId());
$available_users_count = $this->db->selectValue("SELECT COUNT(id) FROM `".DB_PREFIX."_profiles` WHERE `available` = 1 AND `hidden` = 0");
if($available_users_count >= 5) {
sendTelegramNotification("✅ Distaccamento operativo con squadra completa");
} else if($available_users_count >= 2) {
sendTelegramNotification("🚒 Distaccamento operativo per supporto");
} else {
sendTelegramNotification("⚠️ Distaccamento non operativo");
}
return $this->db->update(
DB_PREFIX."_profiles",
["available" => $availability, 'availability_last_change' => 'manual'],
["id" => $user_id]
);
}
}
class Services {
private $db = null;
@ -328,5 +368,6 @@ class Schedules {
}
$users = new Users($db, $auth);
$availability = new Availability($db, $users);
$services = new Services($db);
$schedules = new Schedules($db, $users);

View File

@ -9,4 +9,7 @@
</button>
</div>
<owner-image></owner-image>
<app-table [sourceType]="'list'" (changeAvailability)="changeAvailibility($event.newState, $event.user)" #table></app-table>
<app-table [sourceType]="'list'" (changeAvailability)="changeAvailibility($event.newState, $event.user)" #table></app-table>
<div class="text-center">
<button (click)="requestTelegramToken()" class="btn btn-md btn-success mt-3">Collega l'account al bot Telegram</button>
</div>

View File

@ -41,4 +41,11 @@ export class ListComponent implements OnInit {
ngOnInit(): void {
}
requestTelegramToken() {
this.api.post("telegram_login_token", {}).then((response) => {
console.log(response);
window.open(response.start_link, "_blank");
});
}
}

View File

@ -7,7 +7,6 @@
<ng-container *ngIf="auth.profile.full_viewer">
<th>Autista</th>
<th>Chiama</th>
<th>Scrivi</th>
<th>Interventi</th>
<th>Minuti disponibilità</th>
</ng-container>
@ -32,9 +31,6 @@
<td>
<a href="tel:{{row.phone_number}}"><i class="fa fa-phone"></i></a>
</td>
<td>
<a target="_blank" href="https://wa.me/{{row.phone_number}}?text=Allerta%20in%20corso.%20Mettiti%20in%20contatto%20al%20più%20presto%20con%20me."><i class="fab fa-whatsapp"></i></a>
</td>
<td>{{ row.services }}</td>
<td>{{ row.availability_minutes }}</td>
</ng-container>

View File

@ -77,14 +77,14 @@ export class AuthService {
});
}).catch((err) => {
let error_message = "";
if(err.status == 401) {
if(err.status === 401) {
error_message = err.error.message;
} else if (err.status = 400) {
} else if (err.status === 400) {
let error_messages = err.error.errors;
error_message = error_messages.map((val: any) => {
return `${val.msg} in ${val.param}`;
}).join(" & ");
} else if (err.status = 500) {
} else if (err.status === 500) {
error_message = "Server error";
} else {
error_message = "Unknown error";
@ -105,4 +105,4 @@ export class AuthService {
}
this.router.navigate(routerDestination);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 419 B