Expand test notification framework.

This commit is contained in:
James Cole 2024-12-08 16:28:22 +01:00
parent c06fb8daf6
commit 2f7a1c941e
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
15 changed files with 509 additions and 70 deletions

View File

@ -1,8 +1,7 @@
<?php
/**
* AdminRequestedTestMessage.php
* Copyright (c) 2019 james@firefly-iii.org
/*
* TestEmailChannel.php
* Copyright (c) 2024 james@firefly-iii.org.
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
@ -17,31 +16,30 @@
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
declare(strict_types=1);
namespace FireflyIII\Events;
namespace FireflyIII\Events\Test;
use FireflyIII\User;
use Illuminate\Queue\SerializesModels;
/**
* Class AdminRequestedTestMessage.
*/
class AdminRequestedTestMessage extends Event
class TestNotificationChannel
{
use SerializesModels;
public User $user;
public string $channel;
/**
* Create a new event instance.
*/
public function __construct(User $user)
public function __construct(string $channel, User $user)
{
app('log')->debug(sprintf('Triggered AdminRequestedTestMessage for user #%d (%s)', $user->id, $user->email));
app('log')->debug(sprintf('Triggered TestNotificationChannel("%s") for user #%d (%s)', $channel, $user->id, $user->email));
$this->user = $user;
$this->channel = $channel;
}
}

View File

@ -26,10 +26,15 @@ namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\Admin\InvitationCreated;
use FireflyIII\Events\AdminRequestedTestMessage;
use FireflyIII\Events\NewVersionAvailable;
use FireflyIII\Events\Test\TestNotificationChannel;
use FireflyIII\Notifications\Admin\TestNotification;
use FireflyIII\Notifications\Admin\UserInvitation;
use FireflyIII\Notifications\Admin\VersionCheckResult;
use FireflyIII\Notifications\Test\TestNotificationDiscord;
use FireflyIII\Notifications\Test\TestNotificationEmail;
use FireflyIII\Notifications\Test\TestNotificationSlack;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Notification;
/**
@ -109,17 +114,34 @@ class AdminEventHandler
/**
* Sends a test message to an administrator.
*/
public function sendTestMessage(AdminRequestedTestMessage $event): void
public function sendTestNotification(TestNotificationChannel $event): void
{
Log::debug(sprintf('Now in sendTestNotification(#%d, "%s")', $event->user->id, $event->channel));
/** @var UserRepositoryInterface $repository */
$repository = app(UserRepositoryInterface::class);
if (!$repository->hasRole($event->user, 'owner')) {
Log::error(sprintf('User #%d is not an owner.', $event->user->id));
return;
}
switch($event->channel) {
case 'email':
$class = TestNotificationEmail::class;
break;
case 'slack':
$class = TestNotificationSlack::class;
break;
case 'discord':
$class = TestNotificationDiscord::class;
break;
default:
app('log')->error(sprintf('Unknown channel "%s" in sendTestNotification method.', $event->channel));
return;
}
Log::debug(sprintf('Will send %s as a notification.', $class));
try {
Notification::send($event->user, new TestNotification($event->user->email));
Notification::send($event->user, new $class($event->user->email));
} catch (\Exception $e) { // @phpstan-ignore-line
$message = $e->getMessage();
if (str_contains($message, 'Bcc')) {
@ -135,5 +157,6 @@ class AdminEventHandler
app('log')->error($e->getMessage());
app('log')->error($e->getTraceAsString());
}
Log::debug(sprintf('If you see no errors above this line, test notification was sent over channel "%s"', $event->channel));
}
}

View File

@ -67,23 +67,4 @@ class HomeController extends Controller
return view('admin.index', compact('title', 'mainTitleIcon', 'email'));
}
/**
* Send a test message to the admin.
*
* @return Redirector|RedirectResponse
*/
public function testMessage()
{
die('disabled.');
Log::channel('audit')->info('User sends test message.');
/** @var User $user */
$user = auth()->user();
app('log')->debug('Now in testMessage() controller.');
event(new AdminRequestedTestMessage($user));
session()->flash('info', (string)trans('firefly.send_test_triggered'));
return redirect(route('admin.index'));
}
}

View File

@ -23,9 +23,12 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Admin;
use FireflyIII\Events\Test\TestNotificationChannel;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\NotificationRequest;
use FireflyIII\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class NotificationController extends Controller
@ -45,23 +48,62 @@ class NotificationController extends Controller
// admin notification settings:
$notifications = [];
foreach (config('notifications.notifications.owner') as $key => $info) {
if($info['enabled']) {
if ($info['enabled']) {
$notifications[$key] = app('fireflyconfig')->get(sprintf('notification_%s', $key), true)->data;
}
}
return view('admin.notifications.index', compact('title', 'subTitle', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl','discordUrl','notifications'));
return view('admin.notifications.index', compact('title', 'subTitle', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl', 'discordUrl', 'notifications'));
}
public function postIndex(NotificationRequest $request): RedirectResponse {
public function postIndex(NotificationRequest $request): RedirectResponse
{
$all = $request->getAll();
var_dump($request->getAll());
exit;
// app('fireflyconfig')->set(sprintf('notification_%s', $key), $value);;
foreach (config('notifications.notifications.owner') as $key => $info) {
if (array_key_exists($key, $all)) {
app('fireflyconfig')->set(sprintf('notification_%s', $key), $all[$key]);
}
}
if ('' === $all['slack_url']) {
app('fireflyconfig')->delete('slack_webhook_url');
}
if ('' === $all['discord_url']) {
app('fireflyconfig')->delete('discord_webhook_url');
}
if ('' !== $all['slack_url']) {
app('fireflyconfig')->set('slack_webhook_url', $all['slack_url']);
}
if ('' !== $all['discord_url']) {
app('fireflyconfig')->set('discord_webhook_url', $all['discord_url']);
}
session()->flash('success', (string)trans('firefly.notification_settings_saved'));
session()->flash('success', (string) trans('firefly.notification_settings_saved'));
return redirect(route('admin.index'));
return redirect(route('admin.notification.index'));
}
public function testNotification(Request $request): RedirectResponse
{
$all = $request->all();
$channel = $all['test_submit'] ?? '';
switch ($channel) {
default:
session()->flash('error', (string) trans('firefly.notification_test_failed', ['channel' => $channel]));
break;
case 'email':
case 'discord':
case 'slack':
/** @var User $user */
$user = auth()->user();
app('log')->debug(sprintf('Now in testNotification("%s") controller.', $channel));
event(new TestNotificationChannel($channel, $user));
session()->flash('success', (string) trans('firefly.notification_test_executed', ['channel' => $channel]));
}
return redirect(route('admin.notification.index'));
}
}

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace FireflyIII\Http\Requests;
use FireflyIII\Rules\Admin\IsValidDiscordUrl;
use FireflyIII\Rules\Admin\IsValidSlackUrl;
use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Foundation\Http\FormRequest;
@ -42,18 +44,9 @@ class NotificationRequest extends FormRequest
}
$return[$key] = $value;
}
$return['discord_url'] = $this->convertString('discordUrl');
$return['slack_url'] = $this->convertString('slackUrl');
$return['discord_url'] = $this->convertString('discord_url');
$return['slack_url'] = $this->convertString('slack_url');
return $return;
// if (UrlValidator::isValidWebhookURL($url)) {
// app('fireflyconfig')->set('slack_webhook_url', $url);
// }
// }
//
//
// var_dump($this->all());
// exit;
// return [];
}
/**
@ -61,10 +54,14 @@ class NotificationRequest extends FormRequest
*/
public function rules(): array
{
// fixed
return [
//'password' => 'required',
$rules = [
'discord_url' => ['nullable', 'url', 'min:1', new IsValidDiscordUrl()],
'slack_url' => ['nullable', 'url', 'min:1', new IsValidSlackUrl()],
];
foreach (config('notifications.notifications.owner') as $key => $info) {
$rules[sprintf('notification_%s', $key)] = 'in:0,1';
}
return $rules;
}
}

View File

@ -170,15 +170,11 @@ class TransactionJournal extends Model
public function scopeAfter(EloquentBuilder $query, Carbon $date): EloquentBuilder
{
Log::debug(sprintf('scopeAfter("%s")', $date->format('Y-m-d H:i:s')));
return $query->where('transaction_journals.date', '>=', $date->format('Y-m-d H:i:s'));
}
public function scopeBefore(EloquentBuilder $query, Carbon $date): EloquentBuilder
{
Log::debug(sprintf('scopeBefore("%s")', $date->format('Y-m-d H:i:s')));
return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d H:i:s'));
}

View File

@ -0,0 +1,108 @@
<?php
/*
* TestNotification.php
* Copyright (c) 2022 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Notifications\Test;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
/**
* Class TestNotification
*/
class TestNotificationDiscord extends Notification
{
use Queueable;
private string $address;
/**
* Create a new notification instance.
*/
public function __construct(string $address)
{
$this->address = $address;
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @return array
*/
public function toArray($notifiable)
{
return [
];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @return MailMessage
*/
public function toMail($notifiable)
{
}
/**
* Get the Slack representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
*/
public function toSlack($notifiable) {
// since it's an admin notificaiton, grab the URL from fireflyconfig
$url = app('fireflyconfig')->get('discord_webhook_url', '')->data;
return (new SlackMessage())
->content((string)trans('email.admin_test_subject'))
->to($url);
}
/**
* Get the notification's delivery channels.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @param mixed $notifiable
*
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
}

View File

@ -0,0 +1,102 @@
<?php
/*
* TestNotification.php
* Copyright (c) 2022 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Notifications\Test;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
/**
* Class TestNotification
*/
class TestNotificationEmail extends Notification
{
use Queueable;
private string $address;
/**
* Create a new notification instance.
*/
public function __construct(string $address)
{
$this->address = $address;
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @return array
*/
public function toArray($notifiable)
{
return [
];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @return MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage())
->markdown('emails.admin-test', ['email' => $this->address])
->subject((string) trans('email.admin_test_subject'));
}
/**
* Get the Slack representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
*/
public function toSlack($notifiable) {}
/**
* Get the notification's delivery channels.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @param mixed $notifiable
*
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
}

View File

@ -0,0 +1,113 @@
<?php
/*
* TestNotification.php
* Copyright (c) 2022 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace FireflyIII\Notifications\Test;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
//use Illuminate\Notifications\Slack\SlackMessage;
/**
* Class TestNotification
*/
class TestNotificationSlack extends Notification
{
use Queueable;
private string $address;
/**
* Create a new notification instance.
*/
public function __construct(string $address)
{
$this->address = $address;
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @return array
*/
public function toArray($notifiable)
{
return [
];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @return MailMessage
*/
public function toMail($notifiable)
{
}
/**
* Get the Slack representation of the notification.
*
* @param mixed $notifiable
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
*/
public function toSlack($notifiable) {
// since it's an admin notification, grab the URL from fireflyconfig
$url = app('fireflyconfig')->get('slack_webhook_url', '')->data;
// return (new SlackMessage)
// ->text((string)trans('email.admin_test_subject'))
// ->to($url);
return (new SlackMessage())
->content((string)trans('email.admin_test_subject'))
->to($url);
}
/**
* Get the notification's delivery channels.
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @param mixed $notifiable
*
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
}

View File

@ -49,6 +49,7 @@ use FireflyIII\Events\Security\MFANewBackupCodes;
use FireflyIII\Events\Security\MFAUsedBackupCode;
use FireflyIII\Events\StoredAccount;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Events\Test\TestNotificationChannel;
use FireflyIII\Events\TriggeredAuditLog;
use FireflyIII\Events\UpdatedAccount;
use FireflyIII\Events\UpdatedTransactionGroup;
@ -135,8 +136,8 @@ class EventServiceProvider extends ServiceProvider
'FireflyIII\Handlers\Events\UserEventHandler@sendEmailChangeUndoMail',
],
// admin related
AdminRequestedTestMessage::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendTestMessage',
TestNotificationChannel::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendTestNotification',
],
NewVersionAvailable::class => [
'FireflyIII\Handlers\Events\AdminEventHandler@sendNewVersion',

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Rules\Admin;
use FireflyIII\Support\Validation\ValidatesAmountsTrait;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Log;
class IsValidDiscordUrl implements ValidationRule
{
use ValidatesAmountsTrait;
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
$value = (string)$value;
if('' === $value) {
return;
}
if(!str_starts_with($value, 'https://discord.com/api/webhooks/')) {
$fail('validation.active_url')->translate();
$message = sprintf('IsValidDiscordUrl: "%s" is not a discord URL.', substr($value, 0, 255));
Log::debug($message);
Log::channel('audit')->info($message);
}
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace FireflyIII\Rules\Admin;
use FireflyIII\Support\Validation\ValidatesAmountsTrait;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\Log;
class IsValidSlackUrl implements ValidationRule
{
use ValidatesAmountsTrait;
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function validate(string $attribute, mixed $value, \Closure $fail): void
{
$value = (string)$value;
if('' === $value) {
return;
}
if(!str_starts_with($value, 'https://hooks.slack.com/services/')) {
$fail('validation.active_url')->translate();
$message = sprintf('IsValidSlackUrl: "%s" is not a discord URL.', substr($value, 0, 255));
Log::debug($message);
Log::channel('audit')->info($message);
}
}
}

View File

@ -54,6 +54,8 @@ use FireflyIII\Notifications\Admin\TestNotification;
use FireflyIII\Notifications\Admin\UserInvitation;
use FireflyIII\Notifications\Admin\UserRegistration;
use FireflyIII\Notifications\Admin\VersionCheckResult;
use FireflyIII\Notifications\Test\TestNotificationDiscord;
use FireflyIII\Notifications\Test\TestNotificationSlack;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@ -399,7 +401,7 @@ class User extends Authenticatable
/**
* Route notifications for the Slack channel.
*/
public function routeNotificationForSlack(Notification $notification): string
public function routeNotificationForSlack(Notification $notification): ?string
{
// this check does not validate if the user is owner, Should be done by notification itself.
$res = app('fireflyconfig')->get('slack_webhook_url', '')->data;
@ -407,9 +409,19 @@ class User extends Authenticatable
$res = '';
}
$res = (string)$res;
if ($notification instanceof TestNotification) {
// not the best way to do this, but alas.
if ($notification instanceof TestNotificationSlack) {
return $res;
}
if ($notification instanceof TestNotificationDiscord) {
$res = app('fireflyconfig')->get('discord_webhook_url', '')->data;
if (is_array($res)) {
$res = '';
}
return (string)$res;
}
if ($notification instanceof UserInvitation) {
return $res;
}

View File

@ -2483,6 +2483,8 @@ return [
'owner_notifications' => 'Admin notifications',
'owner_notifications_expl' => 'The following notifications can be enabled or disabled by the administrator. It will be sent over ALL configured channels. Some channels are configured in your environment variables, others can be set here.',
'channel_settings' => 'Settings for notification channels',
'notification_test_failed' => 'Notification test for channel ":channel" failed. The logs will have more details.',
'notification_test_executed' => 'Notification test for channel ":channel" executed. Check your logs for details.',
'settings_notifications' => 'Settings for notifications',
'title_owner_notifications' => 'Owner notifications',
'owner_notification_check_user_new_reg' => 'User gets post-registration welcome message',

View File

@ -24,8 +24,8 @@
</div>
{% endfor %}
<p style="margin-top:2em;">{{ 'channel_settings'|_ }}</p>
{{ ExpandedForm.text('slackUrl', slackUrl, {'label' : 'slack_url_label'|_}) }}
{{ ExpandedForm.text('discordUrl', discordUrl, {'label' : 'discord_url_label'|_}) }}
{{ ExpandedForm.text('slack_url', slackUrl, {'label' : 'slack_url_label'|_}) }}
{{ ExpandedForm.text('discord_url', discordUrl, {'label' : 'discord_url_label'|_}) }}
</div>
<div class="box-footer">
<button type="submit" class="btn btn-success">