Implement Telegram bot

This commit is contained in:
Matteo Gheza 2023-08-29 16:18:26 +02:00
parent da20cf7bf6
commit df762afca5
15 changed files with 1107 additions and 972 deletions

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\TelegramBotLogins;
use DefStudio\Telegraph\Models\TelegraphBot;
use Illuminate\Support\Str;
class TelegramController extends Controller
{
public function loginToken(Request $request)
{
//Get telegramBotUsername from the name of the first bot (first row)
$telegramBotUsername = TelegraphBot::first()->name;
$telegramBotStartParameter = (string) Str::uuid();
$row = new TelegramBotLogins();
$row->chat_id = null;
$row->tmp_login_code = $telegramBotStartParameter;
$row->user = $request->user()->id;
$row->save();
return [
"start_link" => "https://t.me/$telegramBotUsername?start=$telegramBotStartParameter"
];
}
}

View File

@ -37,7 +37,7 @@ class ScheduleSlots extends Model
];
/**
* Get the user that owns the phone.
* Get the user that owns the schedule slot.
*/
public function user(): BelongsTo
{

View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TelegramBotLogins extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'chat_id',
'tmp_login_code'
];
/**
* Get the user that owns the Telegram chat.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -1,35 +0,0 @@
<?php
namespace App\Telegram\Commands;
use Telegram\Bot\Commands\Command;
use Throwable;
class Start extends Command
{
protected string $description = 'Start command to process initial request!';
/**
* Execute the bot command.
*/
public function handle()
{
$message = $this->getUpdate()->message;
$firstName = $message->from->first_name;
$text = "Hello, $firstName! Welcome to our bot!\nType /help to get a list of available commands.";
$this->bot->sendMessage([
'chat_id' => $message->chat->id,
'text' => $text,
]);
}
/**
* Triggered on failure.
*/
public function failed(array $arguments, Throwable $exception): void
{
//
}
}

View File

@ -0,0 +1,198 @@
<?php
namespace App\Telegram;
use App\Models\TelegramBotLogins;
use App\Models\User;
class WebhookController extends
\DefStudio\Telegraph\Handlers\WebhookHandler
{
private $publicCommandsDict = [
"info" => "Ottieni informazioni sul profilo connesso",
"help" => "Ottieni informazioni sui comandi",
"attiva" => "Modifica la tua disponibilità in \"reperibile\"",
"disattiva" => "Modifica la tua disponibilità in \"non reperibile\"",
"programma" => "Abilita programmazione oraria",
"disponibili" => "Mostra un elenco dei vigili attualmente disponibili",
"stato" => "Mostra lo stato della disponibilità della squadra"
];
private $user = null;
private function user(): User|null {
if($this->user) return $this->user;
$this->user = $this->message->from()->storage()->get('user');
return $this->user;
}
/**
* Helper and core commands
*/
public function help(): void
{
$text = " Elenco dei comandi disponibili:";
foreach ($this->publicCommandsDict as $command => $description) {
$text .= "\n/$command - $description";
}
$this->reply($text);
}
public function registerCommands()
{
$response = $this->bot->registerCommands($this->publicCommandsDict)->send();
$this->reply(json_encode(($response)));
}
public function start(string $loginCode)
{
if($this->user()) {
$username = $this->user()->username;
$this->chat->html(
"⚠️ Il tuo account è già collegato con Telegram (username: <i>$username</i>).\n".
"Per scollegarlo, eseguire il comando <strong><i>/logout</i></strong>"
)->send();
return;
}
if(!$loginCode || $loginCode == "/start") {
$this->chat->html(
"Questo Bot Telegram permette di interfacciarsi con il sistema di gestione delle disponibilità <b>AllertaVVF</b>\n".
"Per iniziare, è necessario collegare l'account di Allerta con Telegram.\n".
"Per farlo, accedere alla WebApp e premere su <strong>\"Collega l'account al bot Telegram\"</strong>."
)->send();
return;
}
$row = TelegramBotLogins::where('tmp_login_code', $loginCode)->first();
if(!$row) {
$this->chat->html(
"⚠️ Il codice di login non è valido.\n".
"Per favore, riprovare."
)->send();
return;
}
$row->chat_id = $this->message->chat()->id();
$row->tmp_login_code = null;
$row->save();
$this->reply("✅ Il tuo account è stato collegato con successo.");
$user = User::find($row->user);
$this->message->from()->storage()->set("user", $user);
}
public function logout()
{
$this->message->from()->storage()->forget('user');
TelegramBotLogins::where('chat_id', $this->message->chat()->id())->delete();
$this->reply("✅ Il tuo account è stato scollegato con successo.");
}
/**
* Generic commands
*/
public function info()
{
$user = $this->user();
if(is_null($user)) {
$this->reply("⚠️ Il tuo account Allerta non è collegato con Telegram.\nPer favore, eseguire il comando <strong><i>/start</i></strong>.");
return;
}
$this->reply(
" Informazioni sul profilo:".
"\n<i>Nome:</i> <b>".$user["name"]."</b>".
"\n<i>Disponibile:</i> ".($user["available"] ? "<b>SI</b>" : "<b>NO</b>").
"\n<i>Caposquadra:</i> ".($user["chief"] === 1 ? "<b>SI</b>" : "<b>NO</b>").
"\n<i>Autista:</i> ".($user["driver"] === 1 ? "<b>SI</b>" : "<b>NO</b>").
"\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>"
);
}
public function attiva() {
$user = $this->user();
if(is_null($user)) {
$this->reply("⚠️ Il tuo account Allerta non è collegato con Telegram.\nPer favore, eseguire il comando <strong><i>/start</i></strong>.");
return;
}
$user->available = true;
$user->availability_manual_mode = true;
$user->save();
$this->reply("Disponibilità aggiornata con successo.\nOra sei <b>operativo</b>.");
}
public function disattiva() {
$user = $this->user();
if(is_null($user)) {
$this->reply("⚠️ Il tuo account Allerta non è collegato con Telegram.\nPer favore, eseguire il comando <strong><i>/start</i></strong>.");
return;
}
$user->available = false;
$user->availability_manual_mode = true;
$user->save();
$this->reply("Disponibilità aggiornata con successo.\nOra sei <b>non operativo</b>.");
}
public function programma() {
$user = $this->user();
if(is_null($user)) {
$this->reply("⚠️ Il tuo account Allerta non è collegato con Telegram.\nPer favore, eseguire il comando <strong><i>/start</i></strong>.");
return;
}
$user->availability_manual_mode = false;
$user->save();
$this->reply("Programmazione oraria <b>abilitata</b>.\nPer disabilitarla (e tornare in modalità manuale), cambiare la disponbilità usando i comandi \"/attiva\" e \"/disattiva\"");
}
public function disponibili()
{
$user = $this->user();
if(is_null($user)) {
$this->reply("⚠️ Il tuo account Allerta non è collegato con Telegram.\nPer favore, eseguire il comando <strong><i>/start</i></strong>.");
return;
}
//Get all users with availability true
$users = User::where('available', true)->where('hidden', false)->get();
if(count($users) == 0) {
$text = "⚠️ Nessun vigile attualmente disponibile.";
} else {
$text = "👨‍🚒 Elenco dei vigili attualmente disponibili:";
foreach ($users as $user) {
$text .= "\n- <i>".$user->name."</i>";
if($user->chief) $text .= " CS";
if($user->driver) $text .= " 🚒";
}
}
$this->reply($text);
}
public function stato()
{
$user = $this->user();
if(is_null($user)) {
$this->reply("⚠️ Il tuo account Allerta non è collegato con Telegram.\nPer favore, eseguire il comando <strong><i>/start</i></strong>.");
return;
}
//Get all users with availability true
$available_users_count = User::where('available', true)->where('hidden', false)->count();
if($available_users_count >= 5) {
$text = "🚒 Distaccamento operativo con squadra completa";
} else if($available_users_count >= 2) {
$text = "🧯 Distaccamento operativo per supporto";
} else {
$text = "⚠️ Distaccamento non operativo";
}
$this->reply($text);
}
/**
* TODOs:
* - Notification when availability changes (send "stato" response again ONLY IF state changes)
* - Notification when availability is changed by the system (send "stato" response again)
* - At 7:00 AM, send a notification to all users with availability in manual mode, asking them to confirm their availability or dismiss this notification
* - Everything related to alerts, ask the client what to do with that since currently unused in prod
*/
}

View File

@ -6,8 +6,8 @@
"license": "MIT",
"require": {
"php": "^8.1",
"defstudio/telegraph": "^1.38",
"guzzlehttp/guzzle": "^7.2",
"telegram-bot-sdk/laravel": "^4.0",
"lab404/laravel-impersonate": "^1.7",
"laravel/framework": "^10.0",
"laravel/sanctum": "^3.2",

1332
backend/composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,165 +0,0 @@
<?php
return [
/**
*--------------------------------------------------------------------------
* Default Bot Name
*--------------------------------------------------------------------------
*
* Here you may specify which of the bots below you wish to use as
* your default bot for regular use. Of course, you may use many
* bots at once using the manager class.
*/
'use' => 'default',
/**
*--------------------------------------------------------------------------
* Your Telegram Bots
*--------------------------------------------------------------------------
*
* You may use multiple bots at once using the manager class. Each bot
* that you own should be configured here.
*
* Here are each of the telegram bots config parameters.
*
* Supported Params:
*
* - name: The *personal* name you would like to refer to your bot as.
*
* - token: Your Telegram Bot Token.
* Refer for more details: https://core.telegram.org/bots#botfather
* Example: (string) '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'.
*
* - commands: (Optional) Commands to register for this bot,
* Supported Values: "Command Group Name", "Command Repository Name", "Command => Full Path to Class".
* Default: Registers Global Commands.
* Example: (array) [
* 'admin', // Command Group Name.
* 'status', // Command Repository Name.
* 'hello' => App\Telegram\Commands\HelloCommand::class,
* 'bye' => App\Telegram\Commands\ByeCommand::class,
* ]
*/
'bots' => [
'default' => [
'token' => env('TELEGRAM_BOT_TOKEN', 'YOUR-BOT-TOKEN'),
'commands' => [
'start' => App\Telegram\Commands\Start::class,
],
'listen' => [
'update' => [],
'webhook.failed' => [],
// Example of various events fired.
'message' => [],
//'message.photo' => [ \App\Listeners\ProcessInboundPhoto::class ],
'poll' => [],
'message.left_chat_member' => [],
'inline_query.location' => [],
],
]
],
/**
*--------------------------------------------------------------------------
* Webhook [Optional]
*--------------------------------------------------------------------------
*
* Domain: If you want to set a custom domain for your webhook.
* Path: Path is used to construct a webhook route. Default: /telegram
* Controller: Responsible to listen to updates and acknowledge to Telegram.
*
* Default path: telegram
* Webhook path: /telegram/{bot}/webhook
*/
'webhook' => [
'domain' => env('TELEGRAM_WEBHOOK_DOMAIN', null),
'path' => env('TELEGRAM_WEBHOOK_PATH', 'telegram'),
'controller' => \Telegram\Bot\Laravel\Http\Controllers\WebhookController::class,
],
/**
*--------------------------------------------------------------------------
* HTTP [Optional]
*--------------------------------------------------------------------------
*
* - config: To set HTTP Client config (Ex: proxy).
* - async: When set to True, All the requests would be made non-blocking (Async).
* - api_url: To set the Base API URL.
* - client: To set HTTP Client. Should be an instance of @see \Telegram\Bot\Contracts\HttpClientInterface::class
*/
'http' => [
'config' => [],
'async' => env('TELEGRAM_ASYNC_REQUESTS', false),
'api_url' => 'https://api.telegram.org',
'client' => \Telegram\Bot\Http\GuzzleHttpClient::class,
],
/**
*--------------------------------------------------------------------------
* Register Global Commands [Optional]
*--------------------------------------------------------------------------
*
* If you'd like to use the SDK's built in command handler system,
* You can register all the global commands here.
*
* Global commands will apply to all the bots in system and are always active.
*
* The command class should extend the \Telegram\Bot\Commands\Command class.
*
* Default: The SDK registers, a help command which when a user sends /help
* will respond with a list of available commands and description.
*/
'commands' => [
'help' => Telegram\Bot\Commands\HelpCommand::class,
],
/**
*--------------------------------------------------------------------------
* Command Groups [Optional]
*--------------------------------------------------------------------------
*
* You can organize a set of commands into groups which can later,
* be re-used across all your bots.
*
* You can create [4] types of groups!
*
* 1. Group using full path to command classes.
*
* 2. Group using command repository: Provide the key name of the command from the command repository
* and the system will automatically resolve to the appropriate command.
*
* 3. Group using other groups of commands: You can create a group which uses other
* groups of commands to bundle them into one group.
*
* 4. You can create a group with a combination of 1, 2 and 3 all together in one group.
*
* Examples shown below are by the group type for you to understand each of them.
*/
'command_groups' => [
],
/**
*--------------------------------------------------------------------------
* Command Repository [Optional]
*--------------------------------------------------------------------------
*
* Command Repository lets you register commands that can be shared between,
* one or more bots across the project.
*
* This will help you prevent from having to register same set of commands,
* for each bot over and over again and make it easier to maintain them.
*
* Command Repository are not active by default, You need to use the key name to register them,
* individually in a group of commands or in bot commands.
*
* Think of this as a central storage, to register, reuse and maintain them across all bots.
*/
'command_repository' => [
// 'start' => App\Telegram\Commands\StartCommand::class,
// 'stop' => App\Telegram\Commands\StopCommand::class,
],
];

View File

@ -0,0 +1,120 @@
<?php
use DefStudio\Telegraph\Telegraph;
return [
/*
* Telegram api base url, it can be overridden
* for self-hosted servers
*/
'telegram_api_url' => 'https://api.telegram.org/',
/*
* Sets Telegraph messages default parse mode
* allowed values: html|markdown|MarkdownV2
*/
'default_parse_mode' => Telegraph::PARSE_HTML,
/*
* Sets the handler to be used when Telegraph
* receives a new webhook call.
*
* For reference, see https://defstudio.github.io/telegraph/webhooks/overview
*/
'webhook_handler' => App\Telegram\WebhookController::class,
/*
* Sets a custom domain when registering a webhook. This will allow a local telegram bot api server
* to reach the webhook. Disabled by default
*
* For reference, see https://core.telegram.org/bots/api#using-a-local-bot-api-server
*/
// 'custom_webhook_domain' => 'http://my.custom.domain',
/*
* If enabled, Telegraph dumps received
* webhook messages to logs
*/
'debug_mode' => false,
/*
* If enabled, unknown webhook commands are
* reported as exception in application logs
*/
'report_unknown_webhook_commands' => false,
'security' => [
/*
* if enabled, allows callback queries from unregistered chats
*/
'allow_callback_queries_from_unknown_chats' => true,
/*
* if enabled, allows messages and commands from unregistered chats
*/
'allow_messages_from_unknown_chats' => true,
/*
* if enabled, store unknown chats as new TelegraphChat models
*/
'store_unknown_chats_in_db' => false,
],
/*
* Set model class for both TelegraphBot and TelegraphChat,
* to allow more customization.
*
* Bot model must be or extend `DefStudio\Telegraph\Models\TelegraphBot::class`
* Chat model must be or extend `DefStudio\Telegraph\Models\TelegraphChat::class`
*/
'models' => [
'bot' => DefStudio\Telegraph\Models\TelegraphBot::class,
'chat' => DefStudio\Telegraph\Models\TelegraphChat::class,
],
'storage' => [
/**
* Default storage driver to be used for Telegraph data
*/
'default' => 'file',
'stores' => [
'file' => [
/**
* Telegraph cache driver to be used, must implement
* DefStudio\Telegraph\Contracts\StorageDriver contract
*/
'driver' => \DefStudio\Telegraph\Storage\FileStorageDriver::class,
/*
* Laravel Storage disk to use. See /config/filesystems/disks for available disks
* If 'null', Laravel default store will be used,
*/
'disk' => 'local',
/**
* Folder inside filesystem to be used as root for Telegraph storage
*/
'root' => 'telegraph',
],
'cache' => [
/**
* Telegraph cache driver to be used, must implement
* DefStudio\Telegraph\Contracts\StorageDriver contract
*/
'driver' => \DefStudio\Telegraph\Storage\CacheStorageDriver::class,
/*
* Laravel Cache store to use. See /config/cache/stores for available stores
* If 'null', Laravel default store will be used,
*/
'store' => null,
/*
* Prefix to be prepended to cache keys
*/
'key_prefix' => 'tgph',
],
],
],
];

View File

@ -1,85 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class LaratrustSetupTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Create table for storing roles
Schema::create('roles', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name')->unique();
$table->string('display_name')->nullable();
$table->string('description')->nullable();
$table->timestamps();
});
// Create table for storing permissions
Schema::create('permissions', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name')->unique();
$table->string('display_name')->nullable();
$table->string('description')->nullable();
$table->timestamps();
});
// Create table for associating roles to users and teams (Many To Many Polymorphic)
Schema::create('role_user', function (Blueprint $table) {
$table->unsignedBigInteger('role_id');
$table->unsignedBigInteger('user_id');
$table->string('user_type');
$table->foreign('role_id')->references('id')->on('roles')
->onUpdate('cascade')->onDelete('cascade');
$table->primary(['user_id', 'role_id', 'user_type']);
});
// Create table for associating permissions to users (Many To Many Polymorphic)
Schema::create('permission_user', function (Blueprint $table) {
$table->unsignedBigInteger('permission_id');
$table->unsignedBigInteger('user_id');
$table->string('user_type');
$table->foreign('permission_id')->references('id')->on('permissions')
->onUpdate('cascade')->onDelete('cascade');
$table->primary(['user_id', 'permission_id', 'user_type']);
});
// Create table for associating permissions to roles (Many-to-Many)
Schema::create('permission_role', function (Blueprint $table) {
$table->unsignedBigInteger('permission_id');
$table->unsignedBigInteger('role_id');
$table->foreign('permission_id')->references('id')->on('permissions')
->onUpdate('cascade')->onDelete('cascade');
$table->foreign('role_id')->references('id')->on('roles')
->onUpdate('cascade')->onDelete('cascade');
$table->primary(['permission_id', 'role_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('permission_user');
Schema::dropIfExists('permission_role');
Schema::dropIfExists('permissions');
Schema::dropIfExists('role_user');
Schema::dropIfExists('roles');
}
}

View File

@ -0,0 +1,18 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
public function up(): void
{
Schema::create('telegraph_bots', function (Blueprint $table) {
$table->id();
$table->string('token')->unique();
$table->string('name')->nullable();
$table->timestamps();
});
}
};

View File

@ -0,0 +1,21 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
public function up(): void
{
Schema::create('telegraph_chats', function (Blueprint $table) {
$table->id();
$table->string('chat_id');
$table->string('name')->nullable();
$table->foreignId('telegraph_bot_id')->constrained('telegraph_bots')->cascadeOnDelete();
$table->timestamps();
$table->unique(['chat_id', 'telegraph_bot_id']);
});
}
};

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('telegram_bot_logins', function (Blueprint $table) {
$table->id();
$table->string('chat_id')->unique()->nullable();
$table->string('tmp_login_code')->unique()->nullable();
$table->unsignedBigInteger('user')->unsigned();
$table->foreign('user')->references('id')->on('users')->cascadeOnDelete();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('telegram_bot_logins');
}
};

View File

@ -5,10 +5,9 @@ use App\Http\Controllers\AuthController;
use App\Http\Controllers\UserController;
use App\Http\Controllers\ScheduleSlotsController;
use App\Http\Controllers\AvailabilityController;
use App\Http\Controllers\TelegramController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Telegram\Bot\Laravel\Http\Middleware\ValidateWebhook;
use Telegram\Bot\Laravel\Http\Controllers\WebhookController;
/*
|--------------------------------------------------------------------------
@ -40,6 +39,8 @@ Route::middleware('auth:web')->group( function () {
Route::post('/availability', [AvailabilityController::class, 'updateAvailability']);
Route::post('/manual_mode', [AvailabilityController::class, 'updateAvailabilityManualMode']);
Route::post('/telegram_login_token', [TelegramController::class, 'loginToken']);
Route::post('/logout', [AuthController::class, 'logout']);
});
@ -59,8 +60,3 @@ Route::post('/cron/execute', function(Request $request) {
return response('Access Denied', 403);
}
});
//TODO: remove this and open issue on https://github.com/telegram-bot-sdk/laravel since named route not working
Route::group(['middleware' => ValidateWebhook::class], function (): void {
Route::post('/telegram/{bot}/webhook', Telegram\Bot\Laravel\Http\Controllers\WebhookController::class)->name('telegram.bot.webhook');
});

View File

@ -29,6 +29,6 @@
</div>
</div>
<app-table [sourceType]="'list'" (changeAvailability)="changeAvailibility($event.newState, $event.user)" #table></app-table>
<div class="text-center" *ngIf="false">
<div class="text-center">
<button (click)="requestTelegramToken()" class="btn btn-md btn-success mt-3">{{ 'list.connect_telegram_bot'|translate }}</button>
</div>