Expand settings for notifications.

This commit is contained in:
James Cole
2024-12-11 07:23:46 +01:00
parent c35ff3174a
commit c920070ce2
24 changed files with 476 additions and 252 deletions

View File

@@ -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

View File

@@ -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;
} }
} }

View File

@@ -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')) {

View File

@@ -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]));
} }

View File

@@ -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';

View File

@@ -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'];

View 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,
};
}
}

View 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;
}
}

View 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;
}
}

View File

@@ -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'];
} }

View File

@@ -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];
} }

View File

@@ -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];
} }

View File

@@ -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'];
} }

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 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);
}
}
}

View File

@@ -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);
} }

View File

@@ -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.

View File

@@ -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;
} }

View File

@@ -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;
} }
/** /**

View File

@@ -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,],
], ],

View File

@@ -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' => '',
], ],
]; ];

View File

@@ -183,6 +183,7 @@ return [
'file', 'file',
'staticText', 'staticText',
'password', 'password',
'passwordWithValue',
'nonSelectableAmount', 'nonSelectableAmount',
'number', 'number',
'amountNoCurrency', 'amountNoCurrency',

View File

@@ -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',

View File

@@ -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

View File

@@ -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">