mirror of
https://github.com/firefly-iii/firefly-iii.git
synced 2025-09-22 03:56:42 +00:00
Expand settings for notifications.
This commit is contained in:
19
.env.example
19
.env.example
@@ -178,25 +178,6 @@ MANDRILL_SECRET=
|
|||||||
SPARKPOST_SECRET=
|
SPARKPOST_SECRET=
|
||||||
MAILERSEND_API_KEY=
|
MAILERSEND_API_KEY=
|
||||||
|
|
||||||
#
|
|
||||||
# Ntfy notification settings.
|
|
||||||
# defaults to "https://ntfy.sh", but needs a topic or it won't work.
|
|
||||||
# authentication is recommended but not required.
|
|
||||||
#
|
|
||||||
NTFY_SERVER=
|
|
||||||
NTFY_TOPIC=
|
|
||||||
NTFY_AUTH_ENABLED=false
|
|
||||||
NTFY_AUTH_USERNAME=
|
|
||||||
NTFY_AUTH_PASSWORD=
|
|
||||||
|
|
||||||
#
|
|
||||||
# Pushover notification Application/API Token and User token.
|
|
||||||
# Used if you want to receive notifications over pushover.
|
|
||||||
# Both must be configured for this channel to work.
|
|
||||||
#
|
|
||||||
PUSHOVER_APP_TOKEN=
|
|
||||||
PUSHOVER_USER_TOKEN=
|
|
||||||
|
|
||||||
# Firefly III can send you the following messages.
|
# Firefly III can send you the following messages.
|
||||||
SEND_ERROR_MESSAGE=true
|
SEND_ERROR_MESSAGE=true
|
||||||
|
|
||||||
|
@@ -23,23 +23,23 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Events\Test;
|
namespace FireflyIII\Events\Test;
|
||||||
|
|
||||||
use FireflyIII\User;
|
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
|
||||||
use Illuminate\Queue\SerializesModels;
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
class TestNotificationChannel
|
class TestNotificationChannel
|
||||||
{
|
{
|
||||||
use SerializesModels;
|
use SerializesModels;
|
||||||
|
|
||||||
public User $user;
|
public OwnerNotifiable $owner;
|
||||||
public string $channel;
|
public string $channel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new event instance.
|
* Create a new event instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(string $channel, User $user)
|
public function __construct(string $channel, OwnerNotifiable $owner)
|
||||||
{
|
{
|
||||||
app('log')->debug(sprintf('Triggered TestNotificationChannel("%s") for user #%d (%s)', $channel, $user->id, $user->email));
|
app('log')->debug(sprintf('Triggered TestNotificationChannel("%s")', $channel));
|
||||||
$this->user = $user;
|
$this->owner = $owner;
|
||||||
$this->channel = $channel;
|
$this->channel = $channel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -117,14 +117,8 @@ class AdminEventHandler
|
|||||||
*/
|
*/
|
||||||
public function sendTestNotification(TestNotificationChannel $event): void
|
public function sendTestNotification(TestNotificationChannel $event): void
|
||||||
{
|
{
|
||||||
Log::debug(sprintf('Now in sendTestNotification(#%d, "%s")', $event->user->id, $event->channel));
|
Log::debug(sprintf('Now in sendTestNotification("%s")', $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) {
|
switch($event->channel) {
|
||||||
case 'email':
|
case 'email':
|
||||||
$class = TestNotificationEmail::class;
|
$class = TestNotificationEmail::class;
|
||||||
@@ -145,7 +139,7 @@ class AdminEventHandler
|
|||||||
Log::debug(sprintf('Will send %s as a notification.', $class));
|
Log::debug(sprintf('Will send %s as a notification.', $class));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Notification::send($event->user, new $class($event->user->email));
|
Notification::send($event->owner, new $class($event->owner));
|
||||||
} catch (\Exception $e) { // @phpstan-ignore-line
|
} catch (\Exception $e) { // @phpstan-ignore-line
|
||||||
$message = $e->getMessage();
|
$message = $e->getMessage();
|
||||||
if (str_contains($message, 'Bcc')) {
|
if (str_contains($message, 'Bcc')) {
|
||||||
|
@@ -26,6 +26,7 @@ namespace FireflyIII\Http\Controllers\Admin;
|
|||||||
use FireflyIII\Events\Test\TestNotificationChannel;
|
use FireflyIII\Events\Test\TestNotificationChannel;
|
||||||
use FireflyIII\Http\Controllers\Controller;
|
use FireflyIII\Http\Controllers\Controller;
|
||||||
use FireflyIII\Http\Requests\NotificationRequest;
|
use FireflyIII\Http\Requests\NotificationRequest;
|
||||||
|
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
|
||||||
use FireflyIII\User;
|
use FireflyIII\User;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -40,12 +41,21 @@ class NotificationController extends Controller
|
|||||||
$mainTitleIcon = 'fa-hand-spock-o';
|
$mainTitleIcon = 'fa-hand-spock-o';
|
||||||
$subTitle = (string) trans('firefly.title_owner_notifications');
|
$subTitle = (string) trans('firefly.title_owner_notifications');
|
||||||
$subTitleIcon = 'envelope-o';
|
$subTitleIcon = 'envelope-o';
|
||||||
$slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data;
|
|
||||||
$channels = config('notifications.channels');
|
// notification settings:
|
||||||
|
$slackUrl = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data;
|
||||||
|
$pushoverAppToken = app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data;
|
||||||
|
$pushoverUserToken = app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data;
|
||||||
|
|
||||||
|
$ntfyServer = app('fireflyconfig')->getEncrypted('ntfy_server', 'https://ntfy.sh')->data;
|
||||||
|
$ntfyTopic = app('fireflyconfig')->getEncrypted('ntfy_topic', '')->data;
|
||||||
|
$ntfyAuth = app('fireflyconfig')->get('ntfy_auth', false)->data;
|
||||||
|
$ntfyUser = app('fireflyconfig')->getEncrypted('ntfy_user', '')->data;
|
||||||
|
$ntfyPass = app('fireflyconfig')->getEncrypted('ntfy_pass', '')->data;
|
||||||
|
|
||||||
|
$channels = config('notifications.channels');
|
||||||
$forcedAvailability = [];
|
$forcedAvailability = [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// admin notification settings:
|
// admin notification settings:
|
||||||
$notifications = [];
|
$notifications = [];
|
||||||
foreach (config('notifications.notifications.owner') as $key => $info) {
|
foreach (config('notifications.notifications.owner') as $key => $info) {
|
||||||
@@ -60,18 +70,24 @@ class NotificationController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate presence of of Ntfy settings.
|
// validate presence of of Ntfy settings.
|
||||||
if('' === (string)config('ntfy-notification-channel.topic')) {
|
if ('' === $ntfyTopic) {
|
||||||
Log::warning('No topic name for Ntfy, channel is disabled.');
|
Log::warning('No topic name for Ntfy, channel is disabled.');
|
||||||
$forcedAvailability['ntfy'] = false;
|
$forcedAvailability['ntfy'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate pushover
|
// validate pushover
|
||||||
if('' === (string)config('services.pushover.token') || '' === (string)config('services.pushover.user_token')) {
|
if ('' === $pushoverAppToken || '' === $pushoverUserToken) {
|
||||||
Log::warning('No Pushover token, channel is disabled.');
|
Log::warning('No Pushover token, channel is disabled.');
|
||||||
$forcedAvailability['pushover'] = false;
|
$forcedAvailability['pushover'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('admin.notifications.index', compact('title', 'subTitle', 'forcedAvailability', 'mainTitleIcon', 'subTitleIcon', 'channels', 'slackUrl', 'notifications'));
|
return view('admin.notifications.index',
|
||||||
|
compact(
|
||||||
|
'title', 'subTitle', 'forcedAvailability', 'mainTitleIcon', 'subTitleIcon', 'channels',
|
||||||
|
'slackUrl', 'notifications',
|
||||||
|
'pushoverAppToken', 'pushoverUserToken',
|
||||||
|
'ntfyServer', 'ntfyTopic', 'ntfyAuth', 'ntfyUser', 'ntfyPass'
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function postIndex(NotificationRequest $request): RedirectResponse
|
public function postIndex(NotificationRequest $request): RedirectResponse
|
||||||
@@ -83,12 +99,17 @@ class NotificationController extends Controller
|
|||||||
app('fireflyconfig')->set(sprintf('notification_%s', $key), $all[$key]);
|
app('fireflyconfig')->set(sprintf('notification_%s', $key), $all[$key]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ('' === $all['slack_url']) {
|
$variables = ['slack_webhook_url', 'pushover_app_token', 'pushover_user_token', 'ntfy_server', 'ntfy_topic', 'ntfy_user', 'ntfy_pass'];
|
||||||
app('fireflyconfig')->delete('slack_webhook_url');
|
foreach ($variables as $variable) {
|
||||||
}
|
if ('' === $all[$variable]) {
|
||||||
if ('' !== $all['slack_url']) {
|
app('fireflyconfig')->delete($variable);
|
||||||
app('fireflyconfig')->set('slack_webhook_url', $all['slack_url']);
|
}
|
||||||
|
if ('' !== $all[$variable]) {
|
||||||
|
app('fireflyconfig')->setEncrypted($variable, $all[$variable]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
app('fireflyconfig')->set('ntfy_auth', $all['ntfy_auth'] ?? false);
|
||||||
|
|
||||||
|
|
||||||
session()->flash('success', (string) trans('firefly.notification_settings_saved'));
|
session()->flash('success', (string) trans('firefly.notification_settings_saved'));
|
||||||
|
|
||||||
@@ -109,10 +130,9 @@ class NotificationController extends Controller
|
|||||||
case 'slack':
|
case 'slack':
|
||||||
case 'pushover':
|
case 'pushover':
|
||||||
case 'ntfy':
|
case 'ntfy':
|
||||||
/** @var User $user */
|
$owner = new OwnerNotifiable();
|
||||||
$user = auth()->user();
|
|
||||||
app('log')->debug(sprintf('Now in testNotification("%s") controller.', $channel));
|
app('log')->debug(sprintf('Now in testNotification("%s") controller.', $channel));
|
||||||
event(new TestNotificationChannel($channel, $user));
|
event(new TestNotificationChannel($channel, $owner));
|
||||||
session()->flash('success', (string) trans('firefly.notification_test_executed', ['channel' => $channel]));
|
session()->flash('success', (string) trans('firefly.notification_test_executed', ['channel' => $channel]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,8 +23,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Http\Requests;
|
namespace FireflyIII\Http\Requests;
|
||||||
|
|
||||||
use FireflyIII\Rules\Admin\IsValidDiscordUrl;
|
use FireflyIII\Rules\Admin\IsValidSlackOrDiscordUrl;
|
||||||
use FireflyIII\Rules\Admin\IsValidSlackUrl;
|
|
||||||
use FireflyIII\Support\Request\ChecksLogin;
|
use FireflyIII\Support\Request\ChecksLogin;
|
||||||
use FireflyIII\Support\Request\ConvertsDataTypes;
|
use FireflyIII\Support\Request\ConvertsDataTypes;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
@@ -44,7 +43,16 @@ class NotificationRequest extends FormRequest
|
|||||||
}
|
}
|
||||||
$return[$key] = $value;
|
$return[$key] = $value;
|
||||||
}
|
}
|
||||||
$return['slack_url'] = $this->convertString('slack_url');
|
$return['slack_webhook_url'] = $this->convertString('slack_webhook_url');
|
||||||
|
|
||||||
|
$return['pushover_app_token'] = $this->convertString('pushover_app_token');
|
||||||
|
$return['pushover_user_token'] = $this->convertString('pushover_user_token');
|
||||||
|
|
||||||
|
$return['ntfy_server'] = $this->convertString('ntfy_server');
|
||||||
|
$return['ntfy_topic'] = $this->convertString('ntfy_topic');
|
||||||
|
$return['ntfy_auth'] = $this->convertBoolean($this->get('ntfy_auth'));
|
||||||
|
$return['ntfy_user'] = $this->convertString('ntfy_user');
|
||||||
|
$return['ntfy_pass'] = $this->convertString('ntfy_pass');
|
||||||
return $return;
|
return $return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +62,10 @@ class NotificationRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
$rules = [
|
$rules = [
|
||||||
'slack_url' => ['nullable', 'url', 'min:1', new IsValidSlackUrl()],
|
'slack_webhook_url' => ['nullable', 'url', 'min:1', new IsValidSlackOrDiscordUrl()],
|
||||||
|
'ntfy_server' => ['nullable', 'url', 'min:1'],
|
||||||
|
'ntfy_user' => ['required_with:ntfy_pass,ntfy_auth', 'nullable', 'string', 'min:1'],
|
||||||
|
'ntfy_pass' => ['required_with:ntfy_user,ntfy_auth', 'nullable', 'string', 'min:1'],
|
||||||
];
|
];
|
||||||
foreach (config('notifications.notifications.owner') as $key => $info) {
|
foreach (config('notifications.notifications.owner') as $key => $info) {
|
||||||
$rules[sprintf('notification_%s', $key)] = 'in:0,1';
|
$rules[sprintf('notification_%s', $key)] = 'in:0,1';
|
||||||
|
@@ -107,6 +107,9 @@ class UserInvitation extends Notification
|
|||||||
*/
|
*/
|
||||||
public function via($notifiable)
|
public function via($notifiable)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
$slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data;
|
$slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data;
|
||||||
if (UrlValidator::isValidWebhookURL($slackUrl)) {
|
if (UrlValidator::isValidWebhookURL($slackUrl)) {
|
||||||
return ['mail', 'slack'];
|
return ['mail', 'slack'];
|
||||||
|
70
app/Notifications/Notifiables/OwnerNotifiable.php
Normal file
70
app/Notifications/Notifiables/OwnerNotifiable.php
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* AdminNotifiable.php
|
||||||
|
* Copyright (c) 2024 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\Notifiables;
|
||||||
|
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use NotificationChannels\Pushover\PushoverReceiver;
|
||||||
|
|
||||||
|
class OwnerNotifiable
|
||||||
|
{
|
||||||
|
|
||||||
|
public function routeNotificationForSlack(): string
|
||||||
|
{
|
||||||
|
$res = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data;
|
||||||
|
if (is_array($res)) {
|
||||||
|
$res = '';
|
||||||
|
}
|
||||||
|
return (string) $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function routeNotificationForPushover()
|
||||||
|
{
|
||||||
|
$pushoverAppToken = (string) app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data;
|
||||||
|
$pushoverUserToken = (string) app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data;
|
||||||
|
return PushoverReceiver::withUserKey($pushoverUserToken)
|
||||||
|
->withApplicationToken($pushoverAppToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the notification routing information for the given driver.
|
||||||
|
*
|
||||||
|
* @param string $driver
|
||||||
|
* @param null|Notification $notification
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function routeNotificationFor($driver, $notification = null)
|
||||||
|
{
|
||||||
|
$method = 'routeNotificationFor' . Str::studly($driver);
|
||||||
|
if (method_exists($this, $method)) {
|
||||||
|
return $this->{$method}($notification); // @phpstan-ignore-line
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($driver) {
|
||||||
|
'mail' => (string) config('firefly.site_owner'),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
46
app/Notifications/ReturnsAvailableChannels.php
Normal file
46
app/Notifications/ReturnsAvailableChannels.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* ReturnsAvailableChannels.php
|
||||||
|
* Copyright (c) 2024 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;
|
||||||
|
|
||||||
|
use FireflyIII\Support\Notifications\UrlValidator;
|
||||||
|
|
||||||
|
class ReturnsAvailableChannels
|
||||||
|
{
|
||||||
|
public static function returnChannels(string $type): array {
|
||||||
|
$channels = ['mail'];
|
||||||
|
|
||||||
|
if('owner' === $type) {
|
||||||
|
$slackUrl = app('fireflyconfig')->get('slack_webhook_url', '')->data;
|
||||||
|
if (UrlValidator::isValidWebhookURL($slackUrl)) {
|
||||||
|
$channels[] = 'slack';
|
||||||
|
}
|
||||||
|
// only the owner can get notifications over
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return $channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
60
app/Notifications/ReturnsSettings.php
Normal file
60
app/Notifications/ReturnsSettings.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* ReturnsSettings.php
|
||||||
|
* Copyright (c) 2024 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;
|
||||||
|
|
||||||
|
use FireflyIII\Exceptions\FireflyException;
|
||||||
|
use FireflyIII\Support\Facades\FireflyConfig;
|
||||||
|
use FireflyIII\User;
|
||||||
|
|
||||||
|
class ReturnsSettings
|
||||||
|
{
|
||||||
|
public static function getSettings(string $channel, string $type, ?User $user): array
|
||||||
|
{
|
||||||
|
if ('ntfy' === $channel) {
|
||||||
|
return self::getNtfySettings($type, $user);
|
||||||
|
}
|
||||||
|
throw new FireflyException(sprintf('Cannot handle channel "%s"', $channel));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function getNtfySettings(string $type, ?User $user)
|
||||||
|
{
|
||||||
|
$settings = [
|
||||||
|
'ntfy_server' => 'https://ntfy.sh',
|
||||||
|
'ntfy_topic' => '',
|
||||||
|
'ntfy_auth' => false,
|
||||||
|
'ntfy_user' => '',
|
||||||
|
'ntfy_pass' => '',
|
||||||
|
|
||||||
|
];
|
||||||
|
if ('owner' === $type) {
|
||||||
|
$settings['ntfy_server'] = FireflyConfig::getEncrypted('ntfy_server', 'https://ntfy.sh')->data;
|
||||||
|
$settings['ntfy_topic'] = FireflyConfig::getEncrypted('ntfy_topic', '')->data;
|
||||||
|
$settings['ntfy_auth'] = FireflyConfig::get('ntfy_auth', false)->data;
|
||||||
|
$settings['ntfy_user'] = FireflyConfig::getEncrypted('ntfy_user', '')->data;
|
||||||
|
$settings['ntfy_pass'] = FireflyConfig::getEncrypted('ntfy_pass', '')->data;
|
||||||
|
}
|
||||||
|
return $settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -24,6 +24,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Notifications\Test;
|
namespace FireflyIII\Notifications\Test;
|
||||||
|
|
||||||
|
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
@@ -35,26 +36,26 @@ class TestNotificationEmail extends Notification
|
|||||||
{
|
{
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
private string $address;
|
private OwnerNotifiable $owner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new notification instance.
|
* Create a new notification instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(string $address)
|
public function __construct(OwnerNotifiable $owner)
|
||||||
{
|
{
|
||||||
$this->address = $address;
|
$this->owner = $owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the array representation of the notification.
|
* Get the array representation of the notification.
|
||||||
*
|
*
|
||||||
* @param mixed $notifiable
|
* @param OwnerNotifiable $notifiable
|
||||||
*
|
*
|
||||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function toArray($notifiable)
|
public function toArray(OwnerNotifiable $notifiable)
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
];
|
];
|
||||||
@@ -69,23 +70,14 @@ class TestNotificationEmail extends Notification
|
|||||||
*
|
*
|
||||||
* @return MailMessage
|
* @return MailMessage
|
||||||
*/
|
*/
|
||||||
public function toMail($notifiable)
|
public function toMail(OwnerNotifiable $notifiable)
|
||||||
{
|
{
|
||||||
|
$address = (string) config('firefly.site_owner');
|
||||||
return (new MailMessage())
|
return (new MailMessage())
|
||||||
->markdown('emails.admin-test', ['email' => $this->address])
|
->markdown('emails.admin-test', ['email' => $address])
|
||||||
->subject((string) trans('email.admin_test_subject'));
|
->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.
|
* Get the notification's delivery channels.
|
||||||
*
|
*
|
||||||
@@ -95,7 +87,7 @@ class TestNotificationEmail extends Notification
|
|||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function via($notifiable)
|
public function via(OwnerNotifiable $notifiable)
|
||||||
{
|
{
|
||||||
return ['mail'];
|
return ['mail'];
|
||||||
}
|
}
|
||||||
|
@@ -24,9 +24,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Notifications\Test;
|
namespace FireflyIII\Notifications\Test;
|
||||||
|
|
||||||
|
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
|
||||||
|
use FireflyIII\Notifications\ReturnsSettings;
|
||||||
|
use FireflyIII\User;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
|
||||||
use Illuminate\Notifications\Messages\SlackMessage;
|
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
use Ntfy\Message;
|
use Ntfy\Message;
|
||||||
use Wijourdil\NtfyNotificationChannel\Channels\NtfyChannel;
|
use Wijourdil\NtfyNotificationChannel\Channels\NtfyChannel;
|
||||||
@@ -40,14 +41,14 @@ class TestNotificationNtfy extends Notification
|
|||||||
{
|
{
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
private string $address;
|
public OwnerNotifiable $owner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new notification instance.
|
* Create a new notification instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(string $address)
|
public function __construct(OwnerNotifiable $owner)
|
||||||
{
|
{
|
||||||
$this->address = $address;
|
$this->owner = $owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,51 +67,34 @@ class TestNotificationNtfy extends Notification
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function toNtfy(mixed $notifiable): Message
|
public function toNtfy(OwnerNotifiable $notifiable): Message
|
||||||
{
|
{
|
||||||
|
$settings = ReturnsSettings::getSettings('ntfy', 'owner', null);
|
||||||
|
|
||||||
|
// overrule config.
|
||||||
|
config(['ntfy-notification-channel.server' => $settings['ntfy_server']]);
|
||||||
|
config(['ntfy-notification-channel.topic' => $settings['ntfy_topic']]);
|
||||||
|
|
||||||
|
if ($settings['ntfy_auth']) {
|
||||||
|
// overrule auth as well.
|
||||||
|
config(['ntfy-notification-channel.authentication.enabled' => true]);
|
||||||
|
config(['ntfy-notification-channel.authentication.username' => $settings['ntfy_user']]);
|
||||||
|
config(['ntfy-notification-channel.authentication.password' => $settings['ntfy_pass']]);
|
||||||
|
}
|
||||||
|
|
||||||
$message = new Message();
|
$message = new Message();
|
||||||
$message->topic(config('ntfy-notification-channel.topic'));
|
$message->topic($settings['ntfy_topic']);
|
||||||
$message->title((string)trans('email.admin_test_subject'));
|
$message->title((string) trans('email.admin_test_subject'));
|
||||||
$message->body((string)trans('email.admin_test_message', ['channel' => 'ntfy']));
|
$message->body((string) trans('email.admin_test_message', ['channel' => 'ntfy']));
|
||||||
$message->tags(['white_check_mark', 'ok_hand']);
|
$message->tags(['white_check_mark']);
|
||||||
|
|
||||||
return $message;
|
return $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the mail representation of the notification.
|
|
||||||
*
|
|
||||||
* @param mixed $notifiable
|
|
||||||
*
|
|
||||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
*
|
|
||||||
* @return MailMessage
|
|
||||||
*/
|
*/
|
||||||
public function toMail($notifiable)
|
public function via(OwnerNotifiable $notifiable)
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 [NtfyChannel::class];
|
return [NtfyChannel::class];
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Notifications\Test;
|
namespace FireflyIII\Notifications\Test;
|
||||||
|
|
||||||
|
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
|
||||||
|
use FireflyIII\User;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
@@ -42,73 +44,45 @@ class TestNotificationPushover extends Notification
|
|||||||
{
|
{
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
private string $address;
|
private OwnerNotifiable $owner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new notification instance.
|
* Create a new notification instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(string $address)
|
public function __construct(OwnerNotifiable $owner)
|
||||||
{
|
{
|
||||||
$this->address = $address;
|
$this->owner = $owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the array representation of the notification.
|
* Get the array representation of the notification.
|
||||||
*
|
*
|
||||||
* @param mixed $notifiable
|
* @param OwnerNotifiable $notifiable
|
||||||
*
|
*
|
||||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function toArray($notifiable)
|
public function toArray(OwnerNotifiable $notifiable)
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function toPushover(mixed $notifiable): PushoverMessage
|
public function toPushover(OwnerNotifiable $notifiable): PushoverMessage
|
||||||
{
|
{
|
||||||
Log::debug('Now in toPushover()');
|
Log::debug('Now in toPushover()');
|
||||||
|
|
||||||
return PushoverMessage::create((string)trans('email.admin_test_message', ['channel' => 'Pushover']))
|
return PushoverMessage::create((string)trans('email.admin_test_message', ['channel' => 'Pushover']))
|
||||||
->title((string)trans('email.admin_test_subject'));
|
->title((string)trans('email.admin_test_subject'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public function toSlack($notifiable) {
|
public function via(OwnerNotifiable $notifiable)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the notification's delivery channels.
|
|
||||||
*
|
|
||||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
|
||||||
*
|
|
||||||
* @param mixed $notifiable
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function via($notifiable)
|
|
||||||
{
|
{
|
||||||
return [PushoverChannel::class];
|
return [PushoverChannel::class];
|
||||||
}
|
}
|
||||||
|
@@ -24,11 +24,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace FireflyIII\Notifications\Test;
|
namespace FireflyIII\Notifications\Test;
|
||||||
|
|
||||||
|
use FireflyIII\Notifications\Notifiables\OwnerNotifiable;
|
||||||
|
use FireflyIII\User;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
|
||||||
use Illuminate\Notifications\Messages\SlackMessage;
|
|
||||||
use Illuminate\Notifications\Notification;
|
use Illuminate\Notifications\Notification;
|
||||||
//use Illuminate\Notifications\Slack\SlackMessage;
|
//use Illuminate\Notifications\Slack\SlackMessage;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
use Illuminate\Notifications\Messages\SlackMessage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class TestNotification
|
* Class TestNotification
|
||||||
@@ -37,64 +40,49 @@ class TestNotificationSlack extends Notification
|
|||||||
{
|
{
|
||||||
use Queueable;
|
use Queueable;
|
||||||
|
|
||||||
private string $address;
|
private OwnerNotifiable $owner;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new notification instance.
|
* Create a new notification instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(string $address)
|
public function __construct(OwnerNotifiable $owner)
|
||||||
{
|
{
|
||||||
$this->address = $address;
|
$this->owner =$owner;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the array representation of the notification.
|
* Get the array representation of the notification.
|
||||||
*
|
*
|
||||||
* @param mixed $notifiable
|
* @param OwnerNotifiable $notifiable
|
||||||
*
|
*
|
||||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function toArray($notifiable)
|
public function toArray(OwnerNotifiable $notifiable)
|
||||||
{
|
{
|
||||||
return [
|
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.
|
* Get the Slack representation of the notification.
|
||||||
*
|
*
|
||||||
* @param mixed $notifiable
|
* @param OwnerNotifiable $notifiable
|
||||||
*
|
*
|
||||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
public function toSlack($notifiable) {
|
public function toSlack(OwnerNotifiable $notifiable)
|
||||||
|
{
|
||||||
// since it's an admin notification, grab the URL from fireflyconfig
|
// since it's an admin notification, grab the URL from fireflyconfig
|
||||||
$url = app('fireflyconfig')->get('slack_webhook_url', '')->data;
|
$url = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data;
|
||||||
|
if ('' !== $url) {
|
||||||
// return (new SlackMessage)
|
return new SlackMessage()->content((string)trans('email.admin_test_subject'))->to($url);
|
||||||
// ->text((string)trans('email.admin_test_subject'))
|
//return new SlackMessage()->text((string) trans('email.admin_test_subject'))->to($url);
|
||||||
// ->to($url);
|
|
||||||
return (new SlackMessage())
|
|
||||||
->content((string)trans('email.admin_test_subject'))
|
|
||||||
->to($url);
|
|
||||||
|
|
||||||
|
}
|
||||||
|
Log::error('Empty slack URL, cannot send notification.');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,11 +90,11 @@ class TestNotificationSlack extends Notification
|
|||||||
*
|
*
|
||||||
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
|
||||||
*
|
*
|
||||||
* @param mixed $notifiable
|
* @param OwnerNotifiable $notifiable
|
||||||
*
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public function via($notifiable)
|
public function via(OwnerNotifiable $notifiable)
|
||||||
{
|
{
|
||||||
return ['slack'];
|
return ['slack'];
|
||||||
}
|
}
|
||||||
|
32
app/Rules/Admin/IsValidSlackOrDiscordUrl.php
Normal file
32
app/Rules/Admin/IsValidSlackOrDiscordUrl.php
Normal 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 IsValidSlackOrDiscordUrl 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/') && !str_starts_with($value, 'https://discord.com/api/webhooks/')) {
|
||||||
|
$fail('validation.active_url')->translate();
|
||||||
|
$message = sprintf('IsValidSlackUrl: "%s" is not a discord or slack URL.', substr($value, 0, 255));
|
||||||
|
Log::debug($message);
|
||||||
|
Log::channel('audit')->info($message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -24,7 +24,7 @@ class IsValidSlackUrl implements ValidationRule
|
|||||||
|
|
||||||
if(!str_starts_with($value, 'https://hooks.slack.com/services/')) {
|
if(!str_starts_with($value, 'https://hooks.slack.com/services/')) {
|
||||||
$fail('validation.active_url')->translate();
|
$fail('validation.active_url')->translate();
|
||||||
$message = sprintf('IsValidSlackUrl: "%s" is not a discord URL.', substr($value, 0, 255));
|
$message = sprintf('IsValidSlackUrl: "%s" is not a slack URL.', substr($value, 0, 255));
|
||||||
Log::debug($message);
|
Log::debug($message);
|
||||||
Log::channel('audit')->info($message);
|
Log::channel('audit')->info($message);
|
||||||
}
|
}
|
||||||
|
@@ -289,6 +289,26 @@ class ExpandedForm
|
|||||||
|
|
||||||
return $html;
|
return $html;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @throws FireflyException
|
||||||
|
*/
|
||||||
|
public function passwordWithValue(string $name, string $value, ?array $options = null): string
|
||||||
|
{
|
||||||
|
$label = $this->label($name, $options);
|
||||||
|
$options = $this->expandOptionArray($name, $label, $options);
|
||||||
|
$classes = $this->getHolderClasses($name);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$html = view('form.password', compact('classes', 'value','name', 'label', 'options'))->render();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
app('log')->debug(sprintf('Could not render passwordWithValue(): %s', $e->getMessage()));
|
||||||
|
$html = 'Could not render passwordWithValue.';
|
||||||
|
|
||||||
|
throw new FireflyException($html, 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function to render a percentage.
|
* Function to render a percentage.
|
||||||
|
@@ -25,7 +25,10 @@ namespace FireflyIII\Support;
|
|||||||
|
|
||||||
use FireflyIII\Exceptions\FireflyException;
|
use FireflyIII\Exceptions\FireflyException;
|
||||||
use FireflyIII\Models\Configuration;
|
use FireflyIII\Models\Configuration;
|
||||||
|
use Illuminate\Contracts\Encryption\DecryptException;
|
||||||
|
use Illuminate\Contracts\Encryption\EncryptException;
|
||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class FireflyConfig.
|
* Class FireflyConfig.
|
||||||
@@ -34,7 +37,7 @@ class FireflyConfig
|
|||||||
{
|
{
|
||||||
public function delete(string $name): void
|
public function delete(string $name): void
|
||||||
{
|
{
|
||||||
$fullName = 'ff-config-'.$name;
|
$fullName = 'ff-config-' . $name;
|
||||||
if (\Cache::has($fullName)) {
|
if (\Cache::has($fullName)) {
|
||||||
\Cache::forget($fullName);
|
\Cache::forget($fullName);
|
||||||
}
|
}
|
||||||
@@ -46,6 +49,25 @@ class FireflyConfig
|
|||||||
return 1 === Configuration::where('name', $name)->count();
|
return 1 === Configuration::where('name', $name)->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getEncrypted(string $name, $default = null): ?Configuration
|
||||||
|
{
|
||||||
|
$result = $this->get($name, $default);
|
||||||
|
if (null === $result) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if ('' === $result->data) {
|
||||||
|
Log::warning(sprintf('Empty encrypted preference found: "%s"', $name));
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$result->data = decrypt($result->data);
|
||||||
|
} catch (DecryptException $e) {
|
||||||
|
Log::error(sprintf('Could not decrypt preference "%s": %s', $name, $e->getMessage()));
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param null|bool|int|string $default
|
* @param null|bool|int|string $default
|
||||||
*
|
*
|
||||||
@@ -53,7 +75,7 @@ class FireflyConfig
|
|||||||
*/
|
*/
|
||||||
public function get(string $name, $default = null): ?Configuration
|
public function get(string $name, $default = null): ?Configuration
|
||||||
{
|
{
|
||||||
$fullName = 'ff-config-'.$name;
|
$fullName = 'ff-config-' . $name;
|
||||||
if (\Cache::has($fullName)) {
|
if (\Cache::has($fullName)) {
|
||||||
return \Cache::get($fullName);
|
return \Cache::get($fullName);
|
||||||
}
|
}
|
||||||
@@ -61,7 +83,7 @@ class FireflyConfig
|
|||||||
try {
|
try {
|
||||||
/** @var null|Configuration $config */
|
/** @var null|Configuration $config */
|
||||||
$config = Configuration::where('name', $name)->first(['id', 'name', 'data']);
|
$config = Configuration::where('name', $name)->first(['id', 'name', 'data']);
|
||||||
} catch (\Exception|QueryException $e) {
|
} catch (\Exception | QueryException $e) {
|
||||||
throw new FireflyException(sprintf('Could not poll the database: %s', $e->getMessage()), 0, $e);
|
throw new FireflyException(sprintf('Could not poll the database: %s', $e->getMessage()), 0, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,10 +100,18 @@ class FireflyConfig
|
|||||||
return $this->set($name, $default);
|
return $this->set($name, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function setEncrypted(string $name, mixed $value): Configuration
|
||||||
* @param mixed $value
|
{
|
||||||
*/
|
try {
|
||||||
public function set(string $name, $value): Configuration
|
$encrypted = encrypt($value);
|
||||||
|
} catch (EncryptException $e) {
|
||||||
|
Log::error(sprintf('Could not encrypt preference "%s": %s', $name, $e->getMessage()));
|
||||||
|
throw new FireflyException(sprintf('Could not encrypt preference "%s". Cowardly refuse to continue.', $name));
|
||||||
|
}
|
||||||
|
return $this->set($name, $encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $name, mixed $value): Configuration
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$config = Configuration::whereName($name)->whereNull('deleted_at')->first();
|
$config = Configuration::whereName($name)->whereNull('deleted_at')->first();
|
||||||
@@ -99,13 +129,13 @@ class FireflyConfig
|
|||||||
$item->name = $name;
|
$item->name = $name;
|
||||||
$item->data = $value;
|
$item->data = $value;
|
||||||
$item->save();
|
$item->save();
|
||||||
\Cache::forget('ff-config-'.$name);
|
\Cache::forget('ff-config-' . $name);
|
||||||
|
|
||||||
return $item;
|
return $item;
|
||||||
}
|
}
|
||||||
$config->data = $value;
|
$config->data = $value;
|
||||||
$config->save();
|
$config->save();
|
||||||
\Cache::forget('ff-config-'.$name);
|
\Cache::forget('ff-config-' . $name);
|
||||||
|
|
||||||
return $config;
|
return $config;
|
||||||
}
|
}
|
||||||
|
107
app/User.php
107
app/User.php
@@ -36,7 +36,6 @@ use FireflyIII\Models\Category;
|
|||||||
use FireflyIII\Models\CurrencyExchangeRate;
|
use FireflyIII\Models\CurrencyExchangeRate;
|
||||||
use FireflyIII\Models\GroupMembership;
|
use FireflyIII\Models\GroupMembership;
|
||||||
use FireflyIII\Models\ObjectGroup;
|
use FireflyIII\Models\ObjectGroup;
|
||||||
use FireflyIII\Models\PiggyBank;
|
|
||||||
use FireflyIII\Models\Preference;
|
use FireflyIII\Models\Preference;
|
||||||
use FireflyIII\Models\Recurrence;
|
use FireflyIII\Models\Recurrence;
|
||||||
use FireflyIII\Models\Role;
|
use FireflyIII\Models\Role;
|
||||||
@@ -54,7 +53,6 @@ use FireflyIII\Notifications\Admin\UserInvitation;
|
|||||||
use FireflyIII\Notifications\Admin\UserRegistration;
|
use FireflyIII\Notifications\Admin\UserRegistration;
|
||||||
use FireflyIII\Notifications\Admin\VersionCheckResult;
|
use FireflyIII\Notifications\Admin\VersionCheckResult;
|
||||||
use FireflyIII\Notifications\Test\TestNotificationDiscord;
|
use FireflyIII\Notifications\Test\TestNotificationDiscord;
|
||||||
use FireflyIII\Notifications\Test\TestNotificationSlack;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
@@ -65,7 +63,6 @@ use Illuminate\Notifications\Notification;
|
|||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Passport\HasApiTokens;
|
use Laravel\Passport\HasApiTokens;
|
||||||
use Laravel\Passport\Token;
|
|
||||||
use NotificationChannels\Pushover\PushoverReceiver;
|
use NotificationChannels\Pushover\PushoverReceiver;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
|
|
||||||
@@ -93,7 +90,7 @@ class User extends Authenticatable
|
|||||||
public static function routeBinder(string $value): self
|
public static function routeBinder(string $value): self
|
||||||
{
|
{
|
||||||
if (auth()->check()) {
|
if (auth()->check()) {
|
||||||
$userId = (int)$value;
|
$userId = (int) $value;
|
||||||
$user = self::find($userId);
|
$user = self::find($userId);
|
||||||
if (null !== $user) {
|
if (null !== $user) {
|
||||||
return $user;
|
return $user;
|
||||||
@@ -102,12 +99,6 @@ class User extends Authenticatable
|
|||||||
|
|
||||||
throw new NotFoundHttpException();
|
throw new NotFoundHttpException();
|
||||||
}
|
}
|
||||||
public function routeNotificationForPushover()
|
|
||||||
{
|
|
||||||
return PushoverReceiver::withUserKey((string) config('services.pushover.user_token'))
|
|
||||||
->withApplicationToken((string) config('services.pushover.token'));
|
|
||||||
//return (string) config('services.pushover.token');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Link to accounts.
|
* Link to accounts.
|
||||||
@@ -192,7 +183,7 @@ class User extends Authenticatable
|
|||||||
*/
|
*/
|
||||||
public function getAdministrationId(): int
|
public function getAdministrationId(): int
|
||||||
{
|
{
|
||||||
$groupId = (int)$this->user_group_id;
|
$groupId = (int) $this->user_group_id;
|
||||||
if (0 === $groupId) {
|
if (0 === $groupId) {
|
||||||
throw new FireflyException('User has no administration ID.');
|
throw new FireflyException('User has no administration ID.');
|
||||||
}
|
}
|
||||||
@@ -269,38 +260,38 @@ class User extends Authenticatable
|
|||||||
app('log')->debug(sprintf('in hasAnyRoleInGroup(%s)', implode(', ', $roles)));
|
app('log')->debug(sprintf('in hasAnyRoleInGroup(%s)', implode(', ', $roles)));
|
||||||
|
|
||||||
/** @var Collection $dbRoles */
|
/** @var Collection $dbRoles */
|
||||||
$dbRoles = UserRole::whereIn('title', $roles)->get();
|
$dbRoles = UserRole::whereIn('title', $roles)->get();
|
||||||
if (0 === $dbRoles->count()) {
|
if (0 === $dbRoles->count()) {
|
||||||
app('log')->error(sprintf('Could not find role(s): %s. Probably migration mishap.', implode(', ', $roles)));
|
app('log')->error(sprintf('Could not find role(s): %s. Probably migration mishap.', implode(', ', $roles)));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
$dbRolesIds = $dbRoles->pluck('id')->toArray();
|
$dbRolesIds = $dbRoles->pluck('id')->toArray();
|
||||||
$dbRolesTitles = $dbRoles->pluck('title')->toArray();
|
$dbRolesTitles = $dbRoles->pluck('title')->toArray();
|
||||||
|
|
||||||
/** @var Collection $groupMemberships */
|
/** @var Collection $groupMemberships */
|
||||||
$groupMemberships = $this->groupMemberships()->whereIn('user_role_id', $dbRolesIds)->where('user_group_id', $userGroup->id)->get();
|
$groupMemberships = $this->groupMemberships()->whereIn('user_role_id', $dbRolesIds)->where('user_group_id', $userGroup->id)->get();
|
||||||
if (0 === $groupMemberships->count()) {
|
if (0 === $groupMemberships->count()) {
|
||||||
app('log')->error(sprintf(
|
app('log')->error(sprintf(
|
||||||
'User #%d "%s" does not have roles %s in user group #%d "%s"',
|
'User #%d "%s" does not have roles %s in user group #%d "%s"',
|
||||||
$this->id,
|
$this->id,
|
||||||
$this->email,
|
$this->email,
|
||||||
implode(', ', $roles),
|
implode(', ', $roles),
|
||||||
$userGroup->id,
|
$userGroup->id,
|
||||||
$userGroup->title
|
$userGroup->title
|
||||||
));
|
));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
foreach ($groupMemberships as $membership) {
|
foreach ($groupMemberships as $membership) {
|
||||||
app('log')->debug(sprintf(
|
app('log')->debug(sprintf(
|
||||||
'User #%d "%s" has role "%s" in user group #%d "%s"',
|
'User #%d "%s" has role "%s" in user group #%d "%s"',
|
||||||
$this->id,
|
$this->id,
|
||||||
$this->email,
|
$this->email,
|
||||||
$membership->userRole->title,
|
$membership->userRole->title,
|
||||||
$userGroup->id,
|
$userGroup->id,
|
||||||
$userGroup->title
|
$userGroup->title
|
||||||
));
|
));
|
||||||
if (in_array($membership->userRole->title, $dbRolesTitles, true)) {
|
if (in_array($membership->userRole->title, $dbRolesTitles, true)) {
|
||||||
app('log')->debug(sprintf('Return true, found role "%s"', $membership->userRole->title));
|
app('log')->debug(sprintf('Return true, found role "%s"', $membership->userRole->title));
|
||||||
|
|
||||||
@@ -308,13 +299,13 @@ class User extends Authenticatable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
app('log')->error(sprintf(
|
app('log')->error(sprintf(
|
||||||
'User #%d "%s" does not have roles %s in user group #%d "%s"',
|
'User #%d "%s" does not have roles %s in user group #%d "%s"',
|
||||||
$this->id,
|
$this->id,
|
||||||
$this->email,
|
$this->email,
|
||||||
implode(', ', $roles),
|
implode(', ', $roles),
|
||||||
$userGroup->id,
|
$userGroup->id,
|
||||||
$userGroup->title
|
$userGroup->title
|
||||||
));
|
));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -366,13 +357,13 @@ class User extends Authenticatable
|
|||||||
*/
|
*/
|
||||||
public function routeNotificationFor($driver, $notification = null)
|
public function routeNotificationFor($driver, $notification = null)
|
||||||
{
|
{
|
||||||
$method = 'routeNotificationFor'.Str::studly($driver);
|
$method = 'routeNotificationFor' . Str::studly($driver);
|
||||||
if (method_exists($this, $method)) {
|
if (method_exists($this, $method)) {
|
||||||
return $this->{$method}($notification); // @phpstan-ignore-line
|
return $this->{$method}($notification); // @phpstan-ignore-line
|
||||||
}
|
}
|
||||||
$email = $this->email;
|
$email = $this->email;
|
||||||
// see if user has alternative email address:
|
// see if user has alternative email address:
|
||||||
$pref = app('preferences')->getForUser($this, 'remote_guard_alt_email');
|
$pref = app('preferences')->getForUser($this, 'remote_guard_alt_email');
|
||||||
if (null !== $pref) {
|
if (null !== $pref) {
|
||||||
$email = $pref->data;
|
$email = $pref->data;
|
||||||
}
|
}
|
||||||
@@ -382,7 +373,6 @@ class User extends Authenticatable
|
|||||||
}
|
}
|
||||||
|
|
||||||
return match ($driver) {
|
return match ($driver) {
|
||||||
'database' => $this->notifications(),
|
|
||||||
'mail' => $email,
|
'mail' => $email,
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
@@ -404,30 +394,41 @@ class User extends Authenticatable
|
|||||||
return $this->belongsToMany(Role::class);
|
return $this->belongsToMany(Role::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function routeNotificationForPushover(Notification $notification)
|
||||||
|
{
|
||||||
|
// this check does not validate if the user is owner, Should be done by notification itself.
|
||||||
|
$appToken = (string) app('fireflyconfig')->getEncrypted('pushover_app_token', '')->data;
|
||||||
|
$userToken = (string) app('fireflyconfig')->getEncrypted('pushover_user_token', '')->data;
|
||||||
|
|
||||||
|
if (property_exists($notification, 'type') && $notification->type === 'owner') {
|
||||||
|
return PushoverReceiver::withUserKey($userToken)
|
||||||
|
->withApplicationToken($appToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new FireflyException('No pushover token found.');
|
||||||
|
// return PushoverReceiver::withUserKey((string) config('services.pushover.user_token'))
|
||||||
|
// ->withApplicationToken((string) config('services.pushover.token'));
|
||||||
|
//return (string) config('services.pushover.token');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route notifications for the Slack channel.
|
* 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.
|
// this check does not validate if the user is owner, Should be done by notification itself.
|
||||||
$res = app('fireflyconfig')->get('slack_webhook_url', '')->data;
|
$res = app('fireflyconfig')->getEncrypted('slack_webhook_url', '')->data;
|
||||||
if (is_array($res)) {
|
if (is_array($res)) {
|
||||||
$res = '';
|
$res = '';
|
||||||
}
|
}
|
||||||
$res = (string)$res;
|
$res = (string) $res;
|
||||||
|
|
||||||
// not the best way to do this, but alas.
|
if (property_exists($notification, 'type') && $notification->type === 'owner') {
|
||||||
|
|
||||||
if ($notification instanceof TestNotificationSlack) {
|
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
if ($notification instanceof TestNotificationDiscord) {
|
|
||||||
$res = app('fireflyconfig')->get('discord_webhook_url', '')->data;
|
// not the best way to do this, but alas.
|
||||||
if (is_array($res)) {
|
|
||||||
$res = '';
|
|
||||||
}
|
|
||||||
return (string)$res;
|
|
||||||
}
|
|
||||||
if ($notification instanceof UserInvitation) {
|
if ($notification instanceof UserInvitation) {
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
@@ -437,12 +438,12 @@ class User extends Authenticatable
|
|||||||
if ($notification instanceof VersionCheckResult) {
|
if ($notification instanceof VersionCheckResult) {
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
$pref = app('preferences')->getForUser($this, 'slack_webhook_url', '')->data;
|
$pref = app('preferences')->getEncryptedForUser($this, 'slack_webhook_url', '')->data;
|
||||||
if (is_array($pref)) {
|
if (is_array($pref)) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (string)$pref;
|
return (string) $pref;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -24,8 +24,8 @@ return [
|
|||||||
'channels' => [
|
'channels' => [
|
||||||
'email' => ['enabled' => true, 'ui_configurable' => 0,],
|
'email' => ['enabled' => true, 'ui_configurable' => 0,],
|
||||||
'slack' => ['enabled' => true, 'ui_configurable' => 1,],
|
'slack' => ['enabled' => true, 'ui_configurable' => 1,],
|
||||||
'ntfy' => ['enabled' => true, 'ui_configurable' => 0,],
|
'ntfy' => ['enabled' => true, 'ui_configurable' => 1,],
|
||||||
'pushover' => ['enabled' => true, 'ui_configurable' => 0,],
|
'pushover' => ['enabled' => true, 'ui_configurable' => 1,],
|
||||||
'gotify' => ['enabled' => false, 'ui_configurable' => 0,],
|
'gotify' => ['enabled' => false, 'ui_configurable' => 0,],
|
||||||
'pushbullet' => ['enabled' => false, 'ui_configurable' => 0,],
|
'pushbullet' => ['enabled' => false, 'ui_configurable' => 0,],
|
||||||
],
|
],
|
||||||
|
@@ -3,12 +3,12 @@
|
|||||||
// config for Wijourdil/NtfyNotificationChannel
|
// config for Wijourdil/NtfyNotificationChannel
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'server' => env('NTFY_SERVER', 'https://ntfy.sh'),
|
'server' => 'https://ntfy.sh',
|
||||||
'topic' => env('NTFY_TOPIC', ''),
|
'topic' => '',
|
||||||
'authentication' => [
|
'authentication' => [
|
||||||
'enabled' => (bool) env('NTFY_AUTH_ENABLED', false),
|
'enabled' => false,
|
||||||
'username' => env('NTFY_AUTH_USERNAME', ''),
|
'username' => '',
|
||||||
'password' => env('NTFY_AUTH_PASSWORD', ''),
|
'password' => '',
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@@ -183,6 +183,7 @@ return [
|
|||||||
'file',
|
'file',
|
||||||
'staticText',
|
'staticText',
|
||||||
'password',
|
'password',
|
||||||
|
'passwordWithValue',
|
||||||
'nonSelectableAmount',
|
'nonSelectableAmount',
|
||||||
'number',
|
'number',
|
||||||
'amountNoCurrency',
|
'amountNoCurrency',
|
||||||
|
@@ -2497,8 +2497,9 @@ return [
|
|||||||
'notification_settings' => 'Settings for notifications',
|
'notification_settings' => 'Settings for notifications',
|
||||||
'notification_settings_saved' => 'The notification settings have been saved',
|
'notification_settings_saved' => 'The notification settings have been saved',
|
||||||
'available_channels_title' => 'Available channels',
|
'available_channels_title' => 'Available channels',
|
||||||
'available_channels_expl' => 'These channels are available to send notifications over. To test your confiuration, use the buttons below. Please note that the buttons have no spam control.',
|
'available_channels_expl' => 'These channels are available to send notifications over. To test your configuration, use the buttons below. Please note that the buttons have no spam control.',
|
||||||
'notification_channel_name_email' => 'Email',
|
'notification_channel_name_email' => 'Email',
|
||||||
|
'slack_discord_double' => 'The Slack notification channel can also send notifications to Discord.',
|
||||||
'notification_channel_name_slack' => 'Slack',
|
'notification_channel_name_slack' => 'Slack',
|
||||||
'notification_channel_name_ntfy' => 'Ntfy.sh',
|
'notification_channel_name_ntfy' => 'Ntfy.sh',
|
||||||
'notification_channel_name_pushover' => 'Pushover',
|
'notification_channel_name_pushover' => 'Pushover',
|
||||||
|
@@ -264,5 +264,12 @@ return [
|
|||||||
'webhook_delivery' => 'Delivery',
|
'webhook_delivery' => 'Delivery',
|
||||||
'webhook_response' => 'Response',
|
'webhook_response' => 'Response',
|
||||||
'webhook_trigger' => 'Trigger',
|
'webhook_trigger' => 'Trigger',
|
||||||
|
'pushover_app_token' => 'Pushover app token',
|
||||||
|
'pushover_user_token' => 'Pushover user token',
|
||||||
|
'ntfy_server' => 'Ntfy server',
|
||||||
|
'ntfy_topic' => 'Ntfy topic',
|
||||||
|
'ntfy_auth' => 'Ntfy authentication enabled',
|
||||||
|
'ntfy_user' => 'Ntfy username',
|
||||||
|
'ntfy_pass' => 'Ntfy password',
|
||||||
];
|
];
|
||||||
// Ignore this comment
|
// Ignore this comment
|
||||||
|
@@ -24,7 +24,16 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<p style="margin-top:2em;">{{ 'channel_settings'|_ }}</p>
|
<p style="margin-top:2em;">{{ 'channel_settings'|_ }}</p>
|
||||||
{{ ExpandedForm.text('slack_url', slackUrl, {'label' : 'slack_url_label'|_}) }}
|
{{ ExpandedForm.text('slack_webhook_url', slackUrl, {'label' : 'slack_url_label'|_, helpText: trans('firefly.slack_discord_double')}) }}
|
||||||
|
|
||||||
|
{{ ExpandedForm.text('pushover_app_token', pushoverAppToken, {}) }}
|
||||||
|
{{ ExpandedForm.text('pushover_user_token', pushoverUserToken, {}) }}
|
||||||
|
|
||||||
|
{{ ExpandedForm.text('ntfy_server', ntfyServer, {}) }}
|
||||||
|
{{ ExpandedForm.text('ntfy_topic', ntfyTopic, {}) }}
|
||||||
|
{{ ExpandedForm.checkbox('ntfy_auth','1', ntfyAuth, {}) }}
|
||||||
|
{{ ExpandedForm.text('ntfy_user', ntfyUser, {}) }}
|
||||||
|
{{ ExpandedForm.passwordWithValue('ntfy_pass', ntfyPass, {}) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="box-footer">
|
<div class="box-footer">
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
|
Reference in New Issue
Block a user