Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add Telegram notifications. #27

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,7 @@ VITE_REVERB_SCHEME="${REVERB_SCHEME}"

### ENABLE/DISABLE NEW USER REGISTRATION ###
USER_REGISTRATION_ENABLED=true

### TELEGRAM CREDENTIALS ###
TELEGRAM_BOT_ID=
TELEGRAM_BOT_TOKEN=
1 change: 1 addition & 0 deletions app/Http/Controllers/Api/NotificationStreamController.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ private function validateNotificationStream(Request $request): array
NotificationStream::TYPE_SLACK,
NotificationStream::TYPE_TEAMS,
NotificationStream::TYPE_PUSHOVER,
NotificationStream::TYPE_TELEGRAM,
])],
'value' => ['required', 'string', 'max:255'],
'notifications' => ['required', 'array'],
Expand Down
52 changes: 52 additions & 0 deletions app/Jobs/BackupTasks/SendTelegramNotificationJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace App\Jobs\BackupTasks;

use App\Models\BackupTask;
use App\Models\BackupTaskLog;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

/**
* Job to send Telegram notifications for backup tasks.
*
* This job is responsible for dispatching Telegram notifications
* related to backup tasks and their logs.
*/
class SendTelegramNotificationJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;

/**
* Create a new job instance.
*
* @param BackupTask $backupTask The backup task associated with the notification
* @param BackupTaskLog $backupTaskLog The log entry for the backup task
* @param string $notificationStreamValue The value of the notification stream
*/
public function __construct(
public BackupTask $backupTask,
public BackupTaskLog $backupTaskLog,
public string $notificationStreamValue
) {
//
}

/**
* Execute the job.
*
* Sends a Telegram notification for the backup task.
*/
public function handle(): void
{
$this->backupTask->sendTelegramNotification($this->backupTaskLog, $this->notificationStreamValue);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Livewire\NotificationStreams\Forms;

use App\Livewire\NotificationStreams\Forms\Traits\LogsJsErrors;
use App\Models\NotificationStream;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
Expand All @@ -22,6 +23,8 @@
*/
class CreateNotificationStream extends Component
{
use LogsJsErrors;

/** @var NotificationStreamForm The form object for creating a notification stream */
public NotificationStreamForm $form;

Expand Down
11 changes: 11 additions & 0 deletions app/Livewire/NotificationStreams/Forms/NotificationStreamForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class NotificationStreamForm extends Form
'label' => 'API Token',
'input_type' => 'text',
],
NotificationStream::TYPE_TELEGRAM => [
'label' => 'ID',
'input_type' => 'text',
],
];

/**
Expand Down Expand Up @@ -133,6 +137,11 @@ public function initialize(): void
NotificationStream::TYPE_EMAIL => __('Email'),
NotificationStream::TYPE_PUSHOVER => __('Pushover'),
]);

$config = config('services.telegram');
if ($config['bot_id']) {
$this->availableTypes = $this->availableTypes->merge([NotificationStream::TYPE_TELEGRAM => __('Telegram')]);
}
}

public function setNotificationStream(NotificationStream $notificationStream): void
Expand Down Expand Up @@ -177,6 +186,7 @@ protected function getValueValidationRule(): array|string
NotificationStream::TYPE_TEAMS => ['url', 'regex:/^https:\/\/.*\.webhook\.office\.com\/webhookb2\/.+/i'],
NotificationStream::TYPE_PUSHOVER => ['required', 'string'],
NotificationStream::TYPE_EMAIL => ['email'],
NotificationStream::TYPE_TELEGRAM => ['required', 'string', 'regex:/^\d+$/'],
default => 'string',
};
}
Expand All @@ -189,6 +199,7 @@ protected function getValueErrorMessage(): string
NotificationStream::TYPE_TEAMS => __('Please enter a valid Microsoft Teams Webhook URL.'),
NotificationStream::TYPE_EMAIL => __('Please enter a valid email address.'),
NotificationStream::TYPE_PUSHOVER => __('Please enter a valid Pushover API Token.'),
NotificationStream::TYPE_TELEGRAM => __('Please enter a valid Telegram ID.'),
default => __('Please enter a valid value for the selected notification type.'),
};
}
Expand Down
20 changes: 20 additions & 0 deletions app/Livewire/NotificationStreams/Forms/Traits/LogsJsErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace App\Livewire\NotificationStreams\Forms\Traits;

use Illuminate\Support\Facades\Log;
use Livewire\Attributes\On;

trait LogsJsErrors
{
/**
* Listen for js error events from frontend and log them.
*/
#[On('jsError')]
public function logJsError(string $message): void
{
Log::error('Error from js script for Telegram authentication.', ['error' => $message]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace App\Livewire\NotificationStreams\Forms;

use App\Livewire\NotificationStreams\Forms\Traits\LogsJsErrors;
use App\Models\NotificationStream;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\RedirectResponse;
Expand All @@ -22,7 +23,7 @@
class UpdateNotificationStream extends Component
{
use AuthorizesRequests;

use LogsJsErrors;
/** @var NotificationStream The notification stream being updated */
public NotificationStream $notificationStream;

Expand Down
39 changes: 39 additions & 0 deletions app/Models/BackupTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
use App\Jobs\BackupTasks\SendPushoverNotificationJob;
use App\Jobs\BackupTasks\SendSlackNotificationJob;
use App\Jobs\BackupTasks\SendTeamsNotificationJob;
use App\Jobs\BackupTasks\SendTelegramNotificationJob;
use App\Jobs\RunDatabaseBackupTaskJob;
use App\Jobs\RunFileBackupTaskJob;
use App\Mail\BackupTasks\OutputMail;
use App\Models\Traits\ComposesTelegramNotification;
use App\Traits\HasTags;
use Carbon\CarbonInterface;
use Cron\CronExpression;
Expand Down Expand Up @@ -43,6 +45,7 @@
class BackupTask extends Model
{
use AuditableModel;
use ComposesTelegramNotification;
/** @use HasFactory<BackupTaskFactory> */
use HasFactory;
use HasTags;
Expand Down Expand Up @@ -662,6 +665,16 @@ public function hasPushoverNotification(): bool
->exists();
}

/**
* Check if the task has Telegram notifications enabled.
*/
public function hasTelegramNotification(): bool
{
return $this->notificationStreams()
->where('type', NotificationStream::TYPE_TELEGRAM)
->exists();
}

/**
* Send notifications for the latest backup task log.
*
Expand Down Expand Up @@ -727,6 +740,7 @@ public function dispatchNotification(NotificationStream $notificationStream, Bac
NotificationStream::TYPE_SLACK => SendSlackNotificationJob::dispatch($this, $backupTaskLog, $streamValue)->onQueue($queue),
NotificationStream::TYPE_TEAMS => SendTeamsNotificationJob::dispatch($this, $backupTaskLog, $streamValue)->onQueue($queue),
NotificationStream::TYPE_PUSHOVER => SendPushoverNotificationJob::dispatch($this, $backupTaskLog, $streamValue, $additionalStreamValueOne)->onQueue($queue),
NotificationStream::TYPE_TELEGRAM => SendTelegramNotificationJob::dispatch($this, $backupTaskLog, $streamValue)->onQueue($queue),
default => throw new InvalidArgumentException("Unsupported notification type: {$notificationStream->getAttribute('type')}"),
};
}
Expand Down Expand Up @@ -958,6 +972,31 @@ public function sendPushoverNotification(BackupTaskLog $backupTaskLog, string $p
}
}

/**
* Send a Telegram notification for the backup task.
*
* @param BackupTaskLog $backupTaskLog The log entry for the backup task
* @param string $chatID The target Telegram chat ID
*
* @throws RuntimeException|ConnectionException If the Telegram request fails.
*/
public function sendTelegramNotification(BackupTaskLog $backupTaskLog, string $chatID): void
{
$url = $this->getTelegramUrl();
$message = $this->composeTelegramNotificationText($this, $backupTaskLog);
$payload = [
'chat_id' => $chatID,
'text' => $message,
'parse_mode' => 'HTML',
];

$response = Http::post($url, $payload);

if (! $response->successful()) {
throw new RuntimeException('Telegram notification failed: ' . $response->body());
}
}

/**
* Check if the task has a custom store path.
*/
Expand Down
11 changes: 11 additions & 0 deletions app/Models/NotificationStream.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class NotificationStream extends Model
public const string TYPE_SLACK = 'slack_webhook';
public const string TYPE_TEAMS = 'teams_webhook';
public const string TYPE_PUSHOVER = 'pushover';
public const string TYPE_TELEGRAM = 'telegram';

/**
* The attributes that aren't mass assignable.
Expand Down Expand Up @@ -108,6 +109,14 @@ public function isPushover(): bool
return $this->type === self::TYPE_PUSHOVER;
}

/**
* Check if the notification stream type is Telegram.
*/
public function isTelegram(): bool
{
return $this->type === self::TYPE_TELEGRAM;
}

/**
* Returns whether this stream will send backup notifications on success.
*/
Expand Down Expand Up @@ -139,6 +148,7 @@ protected function formattedType(): Attribute
self::TYPE_SLACK => (string) __('Slack Webhook'),
self::TYPE_TEAMS => (string) __('Teams Webhook'),
self::TYPE_PUSHOVER => (string) __('Pushover'),
self::TYPE_TELEGRAM => (string) __('Telegram'),
default => null,
};
}
Expand All @@ -160,6 +170,7 @@ protected function typeIcon(): Attribute
self::TYPE_SLACK => 'M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z',
self::TYPE_TEAMS => 'M 12.5 2 A 3 3 0 0 0 9.7089844 6.09375 C 9.4804148 6.0378189 9.2455412 6 9 6 L 4 6 C 2.346 6 1 7.346 1 9 L 1 14 C 1 15.654 2.346 17 4 17 L 9 17 C 10.654 17 12 15.654 12 14 L 12 9 C 12 8.6159715 11.921192 8.2518913 11.789062 7.9140625 A 3 3 0 0 0 12.5 8 A 3 3 0 0 0 12.5 2 z M 19 4 A 2 2 0 0 0 19 8 A 2 2 0 0 0 19 4 z M 4.5 9 L 8.5 9 C 8.776 9 9 9.224 9 9.5 C 9 9.776 8.776 10 8.5 10 L 7 10 L 7 14 C 7 14.276 6.776 14.5 6.5 14.5 C 6.224 14.5 6 14.276 6 14 L 6 10 L 4.5 10 C 4.224 10 4 9.776 4 9.5 C 4 9.224 4.224 9 4.5 9 z M 15 9 C 14.448 9 14 9.448 14 10 L 14 14 C 14 16.761 11.761 19 9 19 C 8.369 19 8.0339375 19.755703 8.4609375 20.220703 C 9.4649375 21.313703 10.903 22 12.5 22 C 15.24 22 17.529453 20.040312 17.939453 17.320312 C 17.979453 17.050312 18 16.78 18 16.5 L 18 11 C 18 9.9 17.1 9 16 9 L 15 9 z M 20.888672 9 C 20.322672 9 19.870625 9.46625 19.890625 10.03125 C 19.963625 12.09325 20 16.5 20 16.5 C 20 16.618 19.974547 16.859438 19.935547 17.148438 C 19.812547 18.048438 20.859594 18.653266 21.558594 18.072266 C 22.439594 17.340266 23 16.237 23 15 L 23 11 C 23 9.9 22.1 9 21 9 L 20.888672 9 z',
self::TYPE_PUSHOVER => 'M11.6685 21.0473c5.2435.1831 9.6426-3.9191 9.8257-9.1627.1831-5.24355-3.9191-9.64267-9.1626-9.82578-5.24355-.18311-9.64265 3.91918-9.82576 9.16268-.18311 5.2435 3.91916 9.6427 9.16266 9.8258zM11.8206 8.47095l1.9374-.1867-2.0265 4.17345c.331-.0144.6576-.1144.9816-.3018.324-.1873.6257-.4274.9014-.7186.2775-.291.5191-.6168.7267-.9791.2075-.3603.3593-.7189.457-1.0701.0577-.2189.0892-.4295.0926-.6317s-.0442-.3822-.1409-.5378c-.0967-.1556-.2463-.2834-.4508-.3833-.2044-.1-.4828-.1561-.8389-.1686-.4153-.0145-.8256.038-1.2309.1594-.4071.1213-.7848.3049-1.1369.5507-.352.2458-.66.5562-.9274.933-.2676.3769-.4646.8082-.5911 1.2939-.0483.1598-.0768.2869-.0895.3849s-.0174.1776-.014.2408c.0015.0632.009.1136.0208.1474.0119.0339.0218.0676.028.1031-.4321-.015-.7443-.1132-.9366-.2926-.1924-.1794-.23-.4852-.1149-.9119.1177-.4452.3625-.8637.7364-1.2572.374-.3936.8129-.73655 1.3188-1.02705.5059-.29055 1.0559-.51825 1.6501-.67945.5942-.1612 1.1704-.2321 1.7285-.2126.4914.0172.9006.102 1.2295.2527.329.1508.5839.3453.7614.5799.1775.2346.2849.5057.3207.8114.0358.3057.0099.6223-.0777.9497-.1066.3936-.2967.7879-.5685 1.18135s-.609.7455-1.0079 1.0565c-.4008.3128-.8551.5605-1.3666.7469-.5096.1846-1.049.2679-1.6164.248l-.0631-.0022-1.7377 3.5597-1.82655-.0638z',
self::TYPE_TELEGRAM => 'M 18.632812 1.714844 L 0.519531 8.722656 C 0.507812 8.726562 0.5 8.730469 0.488281 8.738281 C 0.34375 8.820312 -0.683594 9.445312 0.761719 10.007812 L 0.777344 10.015625 L 5.089844 11.40625 C 5.15625 11.429688 5.230469 11.421875 5.289062 11.382812 L 15.984375 4.710938 C 16.011719 4.695312 16.042969 4.683594 16.070312 4.675781 C 16.222656 4.652344 16.648438 4.605469 16.378906 4.949219 C 16.070312 5.339844 8.765625 11.890625 7.953125 12.617188 C 7.90625 12.65625 7.878906 12.714844 7.875 12.777344 L 7.519531 16.996094 C 7.519531 17.085938 7.558594 17.167969 7.628906 17.21875 C 7.730469 17.28125 7.859375 17.273438 7.949219 17.195312 L 10.511719 14.902344 C 10.59375 14.828125 10.71875 14.824219 10.808594 14.890625 L 15.28125 18.132812 L 15.292969 18.144531 C 15.402344 18.210938 16.570312 18.890625 16.910156 17.371094 L 19.996094 2.699219 C 20 2.652344 20.042969 2.140625 19.675781 1.839844 C 19.292969 1.523438 18.75 1.683594 18.667969 1.699219 C 18.65625 1.703125 18.644531 1.707031 18.632812 1.714844 Z M 18.632812 1.714844',
default => 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z',
};
}
Expand Down
47 changes: 47 additions & 0 deletions app/Models/Traits/ComposesTelegramNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace App\Models\Traits;

use App\Models\BackupTask;
use App\Models\BackupTaskLog;
use Carbon\Carbon;
use RuntimeException;

trait ComposesTelegramNotification
{
/**
* Build URL for Telegram notification.
*
* @throws RuntimeException
*/
public function getTelegramUrl(): string
{
$config = config()->get('services.telegram');

if ($config['bot_token'] === null) {
throw new RuntimeException('Telegram bot token is not configured.');
}

return 'https://api.telegram.org/bot' . $config['bot_token'] . '/sendMessage';
}

/**
* Compose message for Telegram notification based on the backup task and its log.
*/
public function composeTelegramNotificationText(BackupTask $backupTask, BackupTaskLog $backupTaskLog): string
{
$isSuccessful = $backupTaskLog->getAttribute('successful_at') !== null;
$message = $isSuccessful
? 'The backup task was <b>SUCCESSFUL</b>. 👌'
: 'The backup task <b>FAILED</b>. 😭';

return $message . "\nDetails:\n" .
'Backup Type: ' . ucfirst($backupTask->getAttribute('type')) . "\n" .
'Remote Server: ' . ($backupTask->getAttribute('remoteServer')?->label ?? 'N/A') . "\n" .
'Backup Destination: ' . ($backupTask->getAttribute('backupDestination')?->label ?? 'N/A') .
' (' . ($backupTask->getAttribute('backupDestination')?->type() ?? 'N/A') . ")\n" .
'Ran at: ' . Carbon::parse($backupTaskLog->getAttribute('created_at'))->format('jS F Y, H:i:s');
}
}
5 changes: 5 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,9 @@
'client_secret' => env('BITBUCKET_CLIENT_SECRET'),
'redirect' => config('app.url') . '/auth/bitbucket/callback',
],

'telegram' => [
'bot_id' => env('TELEGRAM_BOT_ID'),
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
],
];
21 changes: 21 additions & 0 deletions database/factories/NotificationStreamFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function definition(): array
NotificationStream::TYPE_DISCORD,
NotificationStream::TYPE_TEAMS,
NotificationStream::TYPE_PUSHOVER,
NotificationStream::TYPE_TELEGRAM,
]);

return [
Expand Down Expand Up @@ -88,6 +89,17 @@ public function pushover(): static
]);
}

/**
* Indicate that the notification stream is for Telegram.
*/
public function telegram(): static
{
return $this->state([
'type' => NotificationStream::TYPE_TELEGRAM,
'value' => $this->generateTelegram(),
]);
}

/**
* Indicate that the notification stream will send successful notifications.
*/
Expand Down Expand Up @@ -138,6 +150,7 @@ private function getValueForType(string $type): string
NotificationStream::TYPE_SLACK => $this->generateSlackWebhook(),
NotificationStream::TYPE_DISCORD => $this->generateDiscordWebhook(),
NotificationStream::TYPE_TEAMS => $this->generateTeamsWebhook(),
NotificationStream::TYPE_TELEGRAM => $this->generateTelegram(),
default => $this->faker->url,
};
}
Expand Down Expand Up @@ -186,4 +199,12 @@ private function generatePushover(): string
{
return $this->faker->regexify('[a-zA-Z0-9]{30}');
}

/**
* Generate a realistic Telegram chatID.
*/
private function generateTelegram(): string
{
return $this->faker->regexify('[0-9]{10}');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ class="mt-1 block w-full"
<x-input-error :messages="$message" class="mt-2" />
@enderror
</div>

<template x-if="$wire.form.type === 'telegram'">
<x-telegram-form />
</template>
@foreach ($form->getAdditionalFieldsConfig() as $field => $config)
<div class="mt-4">
<x-input-label for="form.{{ $field }}" :value="__($config['label'])" />
Expand Down
Loading
Loading