Expand webhook options, allow for budgets.

This commit is contained in:
James Cole
2025-08-17 07:40:19 +02:00
parent 52abe3bbc2
commit 7771b0311c
20 changed files with 3279 additions and 3046 deletions

View File

@@ -24,15 +24,18 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\System; namespace FireflyIII\Api\V1\Controllers\System;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Api\V1\Requests\System\UpdateRequest; use FireflyIII\Api\V1\Requests\System\UpdateRequest;
use FireflyIII\Enums\WebhookDelivery;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Support\Binder\EitherConfigKey; use FireflyIII\Support\Binder\EitherConfigKey;
use FireflyIII\Support\Facades\FireflyConfig;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
/** /**
@@ -107,8 +110,8 @@ class ConfigurationController extends Controller
return [ return [
'is_demo_site' => $isDemoSite?->data, 'is_demo_site' => $isDemoSite?->data,
'permission_update_check' => null === $updateCheck ? null : (int) $updateCheck->data, 'permission_update_check' => null === $updateCheck ? null : (int)$updateCheck->data,
'last_update_check' => null === $lastCheck ? null : (int) $lastCheck->data, 'last_update_check' => null === $lastCheck ? null : (int)$lastCheck->data,
'single_user_mode' => $singleUser?->data, 'single_user_mode' => $singleUser?->data,
]; ];
} }
@@ -139,7 +142,18 @@ class ConfigurationController extends Controller
'value' => $dynamic[$shortKey], 'value' => $dynamic[$shortKey],
'editable' => true, 'editable' => true,
]; ];
return response()->api(['data' => $data])->header('Content-Type', self::JSON_CONTENT_TYPE);
} }
if (str_starts_with($configKey, 'webhook.')) {
$data = [
'title' => $configKey,
'value' => $this->getWebhookConfiguration($configKey),
'editable' => false,
];
return response()->api(['data' => $data])->header('Content-Type', self::JSON_CONTENT_TYPE);
}
// fallback
if (!str_starts_with($configKey, 'configuration.')) { if (!str_starts_with($configKey, 'configuration.')) {
$data = [ $data = [
'title' => $configKey, 'title' => $configKey,
@@ -162,7 +176,7 @@ class ConfigurationController extends Controller
*/ */
public function update(UpdateRequest $request, string $name): JsonResponse public function update(UpdateRequest $request, string $name): JsonResponse
{ {
$rules = ['value' => 'required']; $rules = ['value' => 'required'];
if (!$this->repository->hasRole(auth()->user(), 'owner')) { if (!$this->repository->hasRole(auth()->user(), 'owner')) {
$messages = ['value' => '200005: You need the "owner" role to do this.']; $messages = ['value' => '200005: You need the "owner" role to do this.'];
Validator::make([], $rules, $messages)->validate(); Validator::make([], $rules, $messages)->validate();
@@ -182,4 +196,33 @@ class ConfigurationController extends Controller
return response()->api(['data' => $data])->header('Content-Type', self::CONTENT_TYPE); return response()->api(['data' => $data])->header('Content-Type', self::CONTENT_TYPE);
} }
private function getWebhookConfiguration(string $configKey): array
{
switch ($configKey) {
case 'webhook.triggers':
$cases = WebhookTrigger::cases();
$data = [];
foreach ($cases as $c) {
$data[$c->name] = $c->value;
}
return $data;
case 'webhook.responses':
$cases = WebhookResponse::cases();
$data = [];
foreach ($cases as $c) {
$data[$c->name] = $c->value;
}
return $data;
case 'webhook.deliveries':
$cases = WebhookDelivery::cases();
$data = [];
foreach ($cases as $c) {
$data[$c->name] = $c->value;
}
return $data;
default:
throw new FireflyException(sprintf('Unknown webhook configuration key "%s".', $configKey));
}
}
} }

View File

@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Webhook; namespace FireflyIII\Api\V1\Controllers\Webhook;
use FireflyIII\Api\V1\Controllers\Controller; use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\RequestedSendWebhookMessages; use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface; use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
@@ -146,7 +147,7 @@ class ShowController extends Controller
$engine->setUser(auth()->user()); $engine->setUser(auth()->user());
// tell the generator which trigger it should look for // tell the generator which trigger it should look for
$engine->setTrigger($webhook->trigger); $engine->setTrigger(WebhookTrigger::tryFrom($webhook->trigger));
// tell the generator which objects to process // tell the generator which objects to process
$engine->setObjects(new Collection([$group])); $engine->setObjects(new Collection([$group]));
// set the webhook to trigger // set the webhook to trigger

View File

@@ -24,11 +24,15 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Webhook; namespace FireflyIII\Api\V1\Requests\Models\Webhook;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Models\Webhook; use FireflyIII\Models\Webhook;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log;
/** /**
* Class CreateRequest * Class CreateRequest
@@ -40,11 +44,11 @@ class CreateRequest extends FormRequest
public function getData(): array public function getData(): array
{ {
$triggers = Webhook::getTriggersForValidation(); $triggers = Webhook::getTriggersForValidation();
$responses = Webhook::getResponsesForValidation(); $responses = Webhook::getResponsesForValidation();
$deliveries = Webhook::getDeliveriesForValidation(); $deliveries = Webhook::getDeliveriesForValidation();
$fields = [ $fields = [
'title' => ['title', 'convertString'], 'title' => ['title', 'convertString'],
'active' => ['active', 'boolean'], 'active' => ['active', 'boolean'],
'trigger' => ['trigger', 'convertString'], 'trigger' => ['trigger', 'convertString'],
@@ -55,9 +59,9 @@ class CreateRequest extends FormRequest
// this is the way. // this is the way.
$return = $this->getAllData($fields); $return = $this->getAllData($fields);
$return['trigger'] = $triggers[$return['trigger']] ?? (int) $return['trigger']; $return['trigger'] = $triggers[$return['trigger']] ?? (int)$return['trigger'];
$return['response'] = $responses[$return['response']] ?? (int) $return['response']; $return['response'] = $responses[$return['response']] ?? (int)$return['response'];
$return['delivery'] = $deliveries[$return['delivery']] ?? (int) $return['delivery']; $return['delivery'] = $deliveries[$return['delivery']] ?? (int)$return['delivery'];
return $return; return $return;
} }
@@ -81,4 +85,44 @@ class CreateRequest extends FormRequest
'url' => ['required', sprintf('url:%s', $validProtocols), 'uniqueWebhook'], 'url' => ['required', sprintf('url:%s', $validProtocols), 'uniqueWebhook'],
]; ];
} }
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
Log::debug('Validating webhook');
$data = $validator->getData();
$trigger = $data['trigger'] ?? null;
$response = $data['response'] ?? null;
if (null === $trigger || null === $response) {
Log::debug('No trigger or response, return.');
return;
}
$triggers = array_keys(Webhook::getTriggersForValidation());
$responses = array_keys(Webhook::getResponsesForValidation());
if (!in_array($trigger, $triggers) || !in_array($response, $responses)) {
return;
}
// cannot deliver budget info.
if (is_int($trigger)) {
Log::debug(sprintf('Trigger was integer (%d).', $trigger));
$trigger = WebhookTrigger::from($trigger)->name;
}
if (is_int($response)) {
Log::debug(sprintf('Response was integer (%d).', $response));
$response = WebhookResponse::from($response)->name;
}
Log::debug(sprintf('Trigger is %s, response is %s', $trigger, $response));
if (str_contains($trigger, 'TRANSACTION') && str_contains($response, 'BUDGET')) {
$validator->errors()->add('response', trans('validation.webhook_budget_info'));
}
if (str_contains($trigger, 'BUDGET') && str_contains($response, 'ACCOUNT')) {
$validator->errors()->add('response', trans('validation.webhook_account_info'));
}
if (str_contains($trigger, 'BUDGET') && str_contains($response, 'TRANSACTION')) {
$validator->errors()->add('response', trans('validation.webhook_transaction_info'));
}
}
);
}
} }

View File

@@ -24,11 +24,15 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests\Models\Webhook; namespace FireflyIII\Api\V1\Requests\Models\Webhook;
use FireflyIII\Enums\WebhookResponse;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Models\Webhook; use FireflyIII\Models\Webhook;
use FireflyIII\Rules\IsBoolean; use FireflyIII\Rules\IsBoolean;
use FireflyIII\Support\Request\ChecksLogin; use FireflyIII\Support\Request\ChecksLogin;
use FireflyIII\Support\Request\ConvertsDataTypes; use FireflyIII\Support\Request\ConvertsDataTypes;
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Log;
/** /**
* Class UpdateRequest * Class UpdateRequest
@@ -94,4 +98,43 @@ class UpdateRequest extends FormRequest
'url' => [sprintf('url:%s', $validProtocols), sprintf('uniqueExistingWebhook:%d', $webhook->id)], 'url' => [sprintf('url:%s', $validProtocols), sprintf('uniqueExistingWebhook:%d', $webhook->id)],
]; ];
} }
public function withValidator(Validator $validator): void
{
$validator->after(
function (Validator $validator): void {
Log::debug('Validating webhook');
$data = $validator->getData();
$trigger = $data['trigger'] ?? null;
$response = $data['response'] ?? null;
if (null === $trigger || null === $response) {
Log::debug('No trigger or response, return.');
return;
}
$triggers = array_keys(Webhook::getTriggersForValidation());
$responses = array_keys(Webhook::getResponsesForValidation());
if (!in_array($trigger, $triggers) || !in_array($response, $responses)) {
return;
}
// cannot deliver budget info.
if (is_int($trigger)) {
Log::debug(sprintf('Trigger was integer (%d).', $trigger));
$trigger = WebhookTrigger::from($trigger)->name;
}
if (is_int($response)) {
Log::debug(sprintf('Response was integer (%d).', $response));
$response = WebhookResponse::from($response)->name;
}
Log::debug(sprintf('Trigger is %s, response is %s', $trigger, $response));
if (str_contains($trigger, 'TRANSACTION') && str_contains($response, 'BUDGET')) {
$validator->errors()->add('response', trans('validation.webhook_budget_info'));
}
if (str_contains($trigger, 'BUDGET') && str_contains($response, 'ACCOUNT')) {
$validator->errors()->add('response', trans('validation.webhook_account_info'));
}
if (str_contains($trigger, 'BUDGET') && str_contains($response, 'TRANSACTION')) {
$validator->errors()->add('response', trans('validation.webhook_transaction_info'));
}
}
);
}
} }

View File

@@ -31,5 +31,6 @@ enum WebhookResponse: int
{ {
case TRANSACTIONS = 200; case TRANSACTIONS = 200;
case ACCOUNTS = 210; case ACCOUNTS = 210;
case BUDGET = 230;
case NONE = 220; case NONE = 220;
} }

View File

@@ -29,10 +29,11 @@ namespace FireflyIII\Enums;
*/ */
enum WebhookTrigger: int enum WebhookTrigger: int
{ {
case STORE_TRANSACTION = 100; case STORE_TRANSACTION = 100;
// case BEFORE_STORE_TRANSACTION = 101; case UPDATE_TRANSACTION = 110;
case UPDATE_TRANSACTION = 110; case DESTROY_TRANSACTION = 120;
// case BEFORE_UPDATE_TRANSACTION = 111; case STORE_BUDGET = 200;
case DESTROY_TRANSACTION = 120; case UPDATE_BUDGET = 210;
// case BEFORE_DESTROY_TRANSACTION = 121; case DESTROY_BUDGET = 220;
case STORE_UPDATE_BUDGET_LIMIT = 230;
} }

View File

@@ -24,6 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Generator\Webhook; namespace FireflyIII\Generator\Webhook;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -38,7 +39,7 @@ interface MessageGeneratorInterface
public function setObjects(Collection $objects): void; public function setObjects(Collection $objects): void;
public function setTrigger(int $trigger): void; public function setTrigger(WebhookTrigger $trigger): void;
public function setUser(User $user): void; public function setUser(User $user): void;

View File

@@ -47,11 +47,11 @@ use Symfony\Component\HttpFoundation\ParameterBag;
*/ */
class StandardMessageGenerator implements MessageGeneratorInterface class StandardMessageGenerator implements MessageGeneratorInterface
{ {
private Collection $objects; private Collection $objects;
private int $trigger; private WebhookTrigger $trigger;
private User $user; private User $user;
private int $version = 0; private int $version = 0;
private Collection $webhooks; private Collection $webhooks;
public function __construct() public function __construct()
{ {
@@ -68,9 +68,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
} }
// do some debugging // do some debugging
Log::debug( Log::debug(sprintf('StandardMessageGenerator will generate messages for %d object(s) and %d webhook(s).', $this->objects->count(), $this->webhooks->count()));
sprintf('StandardMessageGenerator will generate messages for %d object(s) and %d webhook(s).', $this->objects->count(), $this->webhooks->count())
);
$this->run(); $this->run();
} }
@@ -79,6 +77,9 @@ class StandardMessageGenerator implements MessageGeneratorInterface
return $this->user->webhooks()->where('active', true)->where('trigger', $this->trigger)->get(['webhooks.*']); return $this->user->webhooks()->where('active', true)->where('trigger', $this->trigger)->get(['webhooks.*']);
} }
/**
* @throws FireflyException
*/
private function run(): void private function run(): void
{ {
Log::debug('Now in StandardMessageGenerator::run'); Log::debug('Now in StandardMessageGenerator::run');
@@ -108,7 +109,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
*/ */
private function generateMessage(Webhook $webhook, Model $model): void private function generateMessage(Webhook $webhook, Model $model): void
{ {
$class = $model::class; $class = $model::class;
// Line is ignored because all of Firefly III's Models have an id property. // Line is ignored because all of Firefly III's Models have an id property.
Log::debug(sprintf('Now in generateMessage(#%d, %s#%d)', $webhook->id, $class, $model->id)); Log::debug(sprintf('Now in generateMessage(#%d, %s#%d)', $webhook->id, $class, $model->id));
@@ -116,7 +117,8 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$basicMessage = [ $basicMessage = [
'uuid' => $uuid->toString(), 'uuid' => $uuid->toString(),
'user_id' => 0, 'user_id' => 0,
'trigger' => WebhookTrigger::from($webhook->trigger)->name, 'user_group_id' => 0,
'trigger' => $webhook->trigger->name,
'response' => WebhookResponse::from($webhook->response)->name, 'response' => WebhookResponse::from($webhook->response)->name,
'url' => $webhook->url, 'url' => $webhook->url,
'version' => sprintf('v%d', $this->getVersion()), 'version' => sprintf('v%d', $this->getVersion()),
@@ -127,15 +129,14 @@ class StandardMessageGenerator implements MessageGeneratorInterface
switch ($class) { switch ($class) {
default: default:
// Line is ignored because all of Firefly III's Models have an id property. // Line is ignored because all of Firefly III's Models have an id property.
Log::error( Log::error(sprintf('Webhook #%d was given %s#%d to deal with but can\'t extract user ID from it.', $webhook->id, $class, $model->id));
sprintf('Webhook #%d was given %s#%d to deal with but can\'t extract user ID from it.', $webhook->id, $class, $model->id)
);
return; return;
case TransactionGroup::class: case TransactionGroup::class:
/** @var TransactionGroup $model */ /** @var TransactionGroup $model */
$basicMessage['user_id'] = $model->user->id; $basicMessage['user_id'] = $model->user_id;
$basicMessage['user_group_id'] = $model->user_group_id;
break; break;
} }
@@ -143,9 +144,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
// then depends on the response what to put in the message: // then depends on the response what to put in the message:
switch ($webhook->response) { switch ($webhook->response) {
default: default:
Log::error( Log::error(sprintf('The response code for webhook #%d is "%d" and the message generator cant handle it. Soft fail.', $webhook->id, $webhook->response));
sprintf('The response code for webhook #%d is "%d" and the message generator cant handle it. Soft fail.', $webhook->id, $webhook->response)
);
return; return;
@@ -156,7 +155,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
case WebhookResponse::TRANSACTIONS->value: case WebhookResponse::TRANSACTIONS->value:
/** @var TransactionGroup $model */ /** @var TransactionGroup $model */
$transformer = new TransactionGroupTransformer(); $transformer = new TransactionGroupTransformer();
try { try {
$basicMessage['content'] = $transformer->transformObject($model); $basicMessage['content'] = $transformer->transformObject($model);
@@ -173,13 +172,13 @@ class StandardMessageGenerator implements MessageGeneratorInterface
case WebhookResponse::ACCOUNTS->value: case WebhookResponse::ACCOUNTS->value:
/** @var TransactionGroup $model */ /** @var TransactionGroup $model */
$accounts = $this->collectAccounts($model); $accounts = $this->collectAccounts($model);
$enrichment = new AccountEnrichment(); $enrichment = new AccountEnrichment();
$enrichment->setDate(null); $enrichment->setDate(null);
$enrichment->setUser($model->user); $enrichment->setUser($model->user);
$accounts = $enrichment->enrich($accounts); $accounts = $enrichment->enrich($accounts);
foreach ($accounts as $account) { foreach ($accounts as $account) {
$transformer = new AccountTransformer(); $transformer = new AccountTransformer();
$transformer->setParameters(new ParameterBag()); $transformer->setParameters(new ParameterBag());
$basicMessage['content'][] = $transformer->transform($account); $basicMessage['content'][] = $transformer->transform($account);
} }
@@ -209,7 +208,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
private function storeMessage(Webhook $webhook, array $message): void private function storeMessage(Webhook $webhook, array $message): void
{ {
$webhookMessage = new WebhookMessage(); $webhookMessage = new WebhookMessage();
$webhookMessage->webhook()->associate($webhook); $webhookMessage->webhook()->associate($webhook);
$webhookMessage->sent = false; $webhookMessage->sent = false;
$webhookMessage->errored = false; $webhookMessage->errored = false;
@@ -224,7 +223,7 @@ class StandardMessageGenerator implements MessageGeneratorInterface
$this->objects = $objects; $this->objects = $objects;
} }
public function setTrigger(int $trigger): void public function setTrigger(WebhookTrigger $trigger): void
{ {
$this->trigger = $trigger; $this->trigger = $trigger;
} }

View File

@@ -53,7 +53,7 @@ class DestroyedGroupEventHandler
$engine = app(MessageGeneratorInterface::class); $engine = app(MessageGeneratorInterface::class);
$engine->setUser($user); $engine->setUser($user);
$engine->setObjects(new Collection([$group])); $engine->setObjects(new Collection([$group]));
$engine->setTrigger(WebhookTrigger::DESTROY_TRANSACTION->value); $engine->setTrigger(WebhookTrigger::DESTROY_TRANSACTION);
$engine->generateMessages(); $engine->generateMessages();
event(new RequestedSendWebhookMessages()); event(new RequestedSendWebhookMessages());

View File

@@ -32,6 +32,7 @@ use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\Services\Internal\Support\CreditRecalculateService; use FireflyIII\Services\Internal\Support\CreditRecalculateService;
use FireflyIII\TransactionRules\Engine\RuleEngineInterface; use FireflyIII\TransactionRules\Engine\RuleEngineInterface;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
/** /**
* Class StoredGroupEventHandler * Class StoredGroupEventHandler
@@ -51,11 +52,11 @@ class StoredGroupEventHandler
private function processRules(StoredTransactionGroup $storedGroupEvent): void private function processRules(StoredTransactionGroup $storedGroupEvent): void
{ {
if (false === $storedGroupEvent->applyRules) { if (false === $storedGroupEvent->applyRules) {
app('log')->info(sprintf('Will not run rules on group #%d', $storedGroupEvent->transactionGroup->id)); Log::info(sprintf('Will not run rules on group #%d', $storedGroupEvent->transactionGroup->id));
return; return;
} }
app('log')->debug('Now in StoredGroupEventHandler::processRules()'); Log::debug('Now in StoredGroupEventHandler::processRules()');
$journals = $storedGroupEvent->transactionGroup->transactionJournals; $journals = $storedGroupEvent->transactionGroup->transactionJournals;
$array = []; $array = [];
@@ -65,7 +66,7 @@ class StoredGroupEventHandler
$array[] = $journal->id; $array[] = $journal->id;
} }
$journalIds = implode(',', $array); $journalIds = implode(',', $array);
app('log')->debug(sprintf('Add local operator for journal(s): %s', $journalIds)); Log::debug(sprintf('Add local operator for journal(s): %s', $journalIds));
// collect rules: // collect rules:
$ruleGroupRepository = app(RuleGroupRepositoryInterface::class); $ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
@@ -98,10 +99,10 @@ class StoredGroupEventHandler
*/ */
private function triggerWebhooks(StoredTransactionGroup $storedGroupEvent): void private function triggerWebhooks(StoredTransactionGroup $storedGroupEvent): void
{ {
app('log')->debug(__METHOD__); Log::debug(__METHOD__);
$group = $storedGroupEvent->transactionGroup; $group = $storedGroupEvent->transactionGroup;
if (false === $storedGroupEvent->fireWebhooks) { if (false === $storedGroupEvent->fireWebhooks) {
app('log')->info(sprintf('Will not fire webhooks for transaction group #%d', $group->id)); Log::info(sprintf('Will not fire webhooks for transaction group #%d', $group->id));
return; return;
} }
@@ -113,7 +114,7 @@ class StoredGroupEventHandler
$engine->setUser($user); $engine->setUser($user);
// tell the generator which trigger it should look for // tell the generator which trigger it should look for
$engine->setTrigger(WebhookTrigger::STORE_TRANSACTION->value); $engine->setTrigger(WebhookTrigger::STORE_TRANSACTION);
// tell the generator which objects to process // tell the generator which objects to process
$engine->setObjects(new Collection([$group])); $engine->setObjects(new Collection([$group]));
// tell the generator to generate the messages // tell the generator to generate the messages

View File

@@ -164,7 +164,7 @@ class UpdatedGroupEventHandler
$engine = app(MessageGeneratorInterface::class); $engine = app(MessageGeneratorInterface::class);
$engine->setUser($user); $engine->setUser($user);
$engine->setObjects(new Collection([$group])); $engine->setObjects(new Collection([$group]));
$engine->setTrigger(WebhookTrigger::UPDATE_TRANSACTION->value); $engine->setTrigger(WebhookTrigger::UPDATE_TRANSACTION);
$engine->generateMessages(); $engine->generateMessages();
event(new RequestedSendWebhookMessages()); event(new RequestedSendWebhookMessages());

View File

@@ -57,6 +57,11 @@ class EitherConfigKey
'firefly.rule-actions', 'firefly.rule-actions',
'firefly.context-rule-actions', 'firefly.context-rule-actions',
'search.operators', 'search.operators',
// webhooks
'webhook.triggers',
'webhook.responses',
'webhook.deliveries',
]; ];
/** /**

View File

@@ -57,6 +57,7 @@ return [
'liability_direction_credit_short', 'liability_direction_credit_short',
'liability_direction_null_short', 'liability_direction_null_short',
'interest_calc_yearly', 'interest_calc_yearly',
'loading',
'interest_calc_', 'interest_calc_',
'interest_calc_null', 'interest_calc_null',
'interest_calc_daily', 'interest_calc_daily',
@@ -246,12 +247,19 @@ return [
'multi_account_warning_withdrawal', 'multi_account_warning_withdrawal',
'multi_account_warning_deposit', 'multi_account_warning_deposit',
'multi_account_warning_transfer', 'multi_account_warning_transfer',
'webhook_trigger_STORE_TRANSACTION', 'webhook_trigger_STORE_TRANSACTION',
'webhook_trigger_UPDATE_TRANSACTION', 'webhook_trigger_UPDATE_TRANSACTION',
'webhook_trigger_DESTROY_TRANSACTION', 'webhook_trigger_DESTROY_TRANSACTION',
'webhook_trigger_STORE_BUDGET',
'webhook_trigger_UPDATE_BUDGET',
'webhook_trigger_DESTROY_BUDGET',
'webhook_trigger_STORE_UPDATE_BUDGET_LIMIT',
'webhook_response_TRANSACTIONS', 'webhook_response_TRANSACTIONS',
'webhook_response_ACCOUNTS', 'webhook_response_ACCOUNTS',
'webhook_response_none_NONE', 'webhook_response_NONE',
'webhook_delivery_JSON', 'webhook_delivery_JSON',
'actions', 'actions',
'meta_data', 'meta_data',

0
public/v1/js/.gitkeep Normal file → Executable file
View File

View File

@@ -24,7 +24,10 @@
{{ $t('form.webhook_delivery') }} {{ $t('form.webhook_delivery') }}
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<select <div v-if="loading" class="form-control-static">
<em class="fa fa-spinner fa-spin"></em> {{ $t('firefly.loading') }}
</div>
<select v-if="!loading"
ref="bill" ref="bill"
v-model="delivery" v-model="delivery"
:title="$t('form.webhook_delivery')" :title="$t('form.webhook_delivery')"
@@ -49,6 +52,7 @@ export default {
name: "WebhookDelivery", name: "WebhookDelivery",
data() { data() {
return { return {
loading: true,
delivery : 0, delivery : 0,
deliveries: [ deliveries: [
@@ -71,8 +75,24 @@ export default {
mounted() { mounted() {
this.delivery = this.value; this.delivery = this.value;
this.deliveries = [ this.deliveries = [
{id: 300, name: this.$t('firefly.webhook_delivery_JSON')}, //{id: 300, name: this.$t('firefly.webhook_delivery_JSON')},
]; ];
axios.get('./api/v1/configuration/webhook.deliveries').then((response) => {
for (let key in response.data.data.value) {
if (!response.data.data.value.hasOwnProperty(key)) {
continue;
}
this.deliveries.push(
{
id: response.data.data.value[key],
name: this.$t('firefly.webhook_delivery_' + key),
}
);
}
this.loading = false;
}).catch((error) => {
this.loading = false;
});
}, },
watch: { watch: {
value() { value() {

View File

@@ -19,73 +19,89 @@
--> -->
<template> <template>
<div class="form-group" v-bind:class="{ 'has-error': hasError()}"> <div class="form-group" v-bind:class="{ 'has-error': hasError()}">
<label class="col-sm-4 control-label"> <label class="col-sm-4 control-label">
{{ $t('form.webhook_response') }} {{ $t('form.webhook_response') }}
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<select <div v-if="loading" class="form-control-static">
ref="bill" <em class="fa fa-spinner fa-spin"></em> {{ $t('firefly.loading') }}
v-model="response" </div>
:title="$t('form.webhook_response')" <select v-if="!loading"
class="form-control" ref="response"
name="webhook_response" v-model="response"
> :title="$t('form.webhook_response')"
<option v-for="response in this.responses" class="form-control"
:label="response.name" name="webhook_response"
:value="response.id">{{ response.name }} >
</option> <option v-for="response in this.responses"
</select> :label="response.name"
<p class="help-block" v-text="$t('firefly.webhook_response_form_help')"></p> :value="response.id">{{ response.name }}
<ul v-for="error in this.error" class="list-unstyled"> </option>
<li class="text-danger">{{ error }}</li> </select>
</ul> <p class="help-block" v-text="$t('firefly.webhook_response_form_help')"></p>
<ul v-for="error in this.error" class="list-unstyled">
<li class="text-danger">{{ error }}</li>
</ul>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: "WebhookResponse", name: "WebhookResponse",
data() { data() {
return { return {
response: 0, loading: true,
responses: [], response: 0,
}; responses: [],
}, };
props: {
error: {
type: Array,
required: true,
default() {
return []
}
}, },
value: { props: {
type: Number, error: {
required: true, type: Array,
} required: true,
}, default() {
watch: { return []
value() { }
this.response = this.value; },
value: {
type: Number,
required: true,
}
},
watch: {
value() {
this.response = this.value;
},
response(newValue) {
this.$emit('input', newValue);
}
},
mounted() {
this.response = this.value;
this.responses = [];
axios.get('./api/v1/configuration/webhook.responses').then((response) => {
for (let key in response.data.data.value) {
if (!response.data.data.value.hasOwnProperty(key)) {
continue;
}
this.responses.push(
{
id: response.data.data.value[key],
name: this.$t('firefly.webhook_response_' + key),
}
);
}
this.loading = false;
}).catch((error) => {
this.loading = false;
});
},
methods: {
hasError() {
return this.error?.length > 0;
}
}, },
response(newValue) {
this.$emit('input', newValue);
}
},
mounted() {
this.response = this.value;
this.responses = [
{id: 200, name: this.$t('firefly.webhook_response_TRANSACTIONS')},
{id: 210, name: this.$t('firefly.webhook_response_ACCOUNTS')},
{id: 220, name: this.$t('firefly.webhook_response_none_NONE')},
];
},
methods: {
hasError() {
return this.error?.length > 0;
}
},
} }
</script> </script>

View File

@@ -19,73 +19,90 @@
--> -->
<template> <template>
<div class="form-group" v-bind:class="{ 'has-error': hasError()}"> <div class="form-group" v-bind:class="{ 'has-error': hasError()}">
<label class="col-sm-4 control-label"> <label class="col-sm-4 control-label">
{{ $t('form.webhook_trigger') }} {{ $t('form.webhook_trigger') }}
</label> </label>
<div class="col-sm-8"> <div class="col-sm-8">
<select <div v-if="loading" class="form-control-static">
ref="bill" <em class="fa fa-spinner fa-spin"></em> {{ $t('firefly.loading') }}
v-model="trigger" </div>
:title="$t('form.webhook_trigger')" <select v-if="!loading"
class="form-control" ref="trigger"
name="webhook_trigger" v-model="trigger"
> :title="$t('form.webhook_trigger')"
<option v-for="trigger in this.triggers" class="form-control"
:label="trigger.name" name="webhook_trigger"
:value="trigger.id">{{ trigger.name }} >
</option> <option v-for="trigger in this.triggers"
</select> :label="trigger.name"
<p class="help-block" v-text="$t('firefly.webhook_trigger_form_help')"></p> :value="trigger.id">{{ trigger.name }}
<ul v-for="error in this.error" class="list-unstyled"> </option>
<li class="text-danger">{{ error }}</li> </select>
</ul> <p class="help-block" v-text="$t('firefly.webhook_trigger_form_help')"></p>
<ul v-for="error in this.error" class="list-unstyled">
<li class="text-danger">{{ error }}</li>
</ul>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: "WebhookTrigger", name: "WebhookTrigger",
data() { data() {
return { return {
trigger: 0, trigger: 0,
triggers: [], loading: true,
}; triggers: [],
}, };
props: {
error: {
type: Array,
required: true,
default() {
return []
}
}, },
value: { props: {
type: Number, error: {
required: true, type: Array,
} required: true,
}, default() {
mounted() { return []
this.trigger = this.value; }
this.triggers = [ },
{id: 100, name: this.$t('firefly.webhook_trigger_STORE_TRANSACTION')}, value: {
{id: 110, name: this.$t('firefly.webhook_trigger_UPDATE_TRANSACTION')}, type: Number,
{id: 120, name: this.$t('firefly.webhook_trigger_DESTROY_TRANSACTION')}, required: true,
]; }
}, },
watch: { mounted() {
value() { this.trigger = this.value;
this.trigger = this.value; this.triggers = [];
axios.get('./api/v1/configuration/webhook.triggers').then((response) => {
for (let key in response.data.data.value) {
if (!response.data.data.value.hasOwnProperty(key)) {
continue;
}
this.triggers.push(
{
id: response.data.data.value[key],
name: this.$t('firefly.webhook_trigger_' + key),
}
);
console.log('webhook trigger: id=' + response.data.data.value[key] + ', name=' + key);
}
this.loading = false;
}).catch((error) => {
this.loading = false;
});
},
watch: {
value() {
this.trigger = this.value;
},
trigger(newValue) {
this.$emit('input', newValue);
}
},
methods: {
hasError() {
return this.error?.length > 0;
}
}, },
trigger(newValue) {
this.$emit('input', newValue);
}
},
methods: {
hasError() {
return this.error?.length > 0;
}
},
} }
</script> </script>

View File

@@ -19,138 +19,172 @@
--> -->
<template> <template>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12"> <div class="col-lg-12 col-md-12 col-sm-12">
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title"> <h3 class="box-title">
{{ $t('firefly.webhooks') }} {{ $t('firefly.webhooks') }}
</h3> </h3>
</div>
<div class="box-body no-padding">
<div style="padding:8px;">
<a href="webhooks/create" class="btn btn-success"><span class="fa fa-plus fa-fw"></span>{{ $t('firefly.create_new_webhook') }}</a>
</div>
<table class="table table-responsive table-hover" v-if="webhooks.length > 0" aria-label="A table.">
<thead>
<tr>
<th>Title</th>
<th>Responds when</th>
<th>Responds with (delivery)</th>
<th style="width:20%;">Secret (show / hide)</th>
<th>URL</th>
<th class="hidden-sm hidden-xs">&nbsp;</th>
</tr>
</thead>
<tbody>
<tr v-for="webhook in webhooks" :key="webhook.id">
<td>
<a :href="'webhooks/show/' + webhook.id">{{ webhook.title }}</a>
</td>
<td>
<span v-if="webhook.active">{{ triggers[webhook.trigger] }}</span>
<span v-if="!webhook.active" class="text-muted"><s>{{ triggers[webhook.trigger] }}</s> ({{ $t('firefly.inactive') }})</span>
</td>
<td>{{ responses[webhook.response] }} ({{ deliveries[webhook.delivery] }})</td>
<td>
<em style="cursor:pointer"
v-if="webhook.show_secret" class="fa fa-eye" @click="toggleSecret(webhook)"></em>
<em style="cursor:pointer"
v-if="!webhook.show_secret" class="fa fa-eye-slash" @click="toggleSecret(webhook)"></em>
<code v-if="webhook.show_secret">{{ webhook.secret }}</code>
<code v-if="!webhook.show_secret">********</code>
</td>
<td>
<code :title="webhook.full_url">{{ webhook.url }}</code>
</td>
<td class="hidden-sm hidden-xs">
<div class="btn-group btn-group-xs pull-right">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ $t('firefly.actions') }} <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a :href="'webhooks/show/' + webhook.id"><span class="fa fa-fw fa-search"></span> {{ $t('firefly.inspect') }}</a></li>
<li><a :href="'webhooks/edit/' + webhook.id"><span class="fa fa-fw fa-pencil"></span> {{$t( 'firefly.edit') }}</a></li>
<li><a :href="'webhooks/delete/' + webhook.id"><span class="fa fa-fw fa-trash"></span> {{ $t('firefly.delete') }}</a></li>
</ul>
</div> </div>
</td> <div class="box-body no-padding">
</tr> <div style="padding:8px;">
</tbody> <a href="webhooks/create" class="btn btn-success"><span
</table> class="fa fa-plus fa-fw"></span>{{ $t('firefly.create_new_webhook') }}</a>
</div>
<div v-if="webhooks.length > 0" style="padding:8px;"> <table class="table table-responsive table-hover" v-if="webhooks.length > 0" aria-label="A table.">
<a href="webhooks/create" class="btn btn-success"><span class="fa fa-plus fa-fw"></span>{{ $t('firefly.create_new_webhook') }}</a> <thead>
</div> <tr>
<th>Title</th>
<th>Responds when</th>
<th>Responds with (delivery)</th>
<th style="width:20%;">Secret (show / hide)</th>
<th>URL</th>
<th class="hidden-sm hidden-xs">&nbsp;</th>
</tr>
</thead>
<tbody>
<tr v-for="webhook in webhooks" :key="webhook.id">
<td>
<a :href="'webhooks/show/' + webhook.id">{{ webhook.title }}</a>
</td>
<td>
<span v-if="webhook.active">{{ triggers[webhook.trigger] }}</span>
<span v-if="!webhook.active" class="text-muted"><s>{{ triggers[webhook.trigger] }}</s> ({{
$t('firefly.inactive')
}})</span>
</td>
<td>{{ responses[webhook.response] }} ({{ deliveries[webhook.delivery] }})</td>
<td>
<em style="cursor:pointer"
v-if="webhook.show_secret" class="fa fa-eye" @click="toggleSecret(webhook)"></em>
<em style="cursor:pointer"
v-if="!webhook.show_secret" class="fa fa-eye-slash"
@click="toggleSecret(webhook)"></em>
<code v-if="webhook.show_secret">{{ webhook.secret }}</code>
<code v-if="!webhook.show_secret">********</code>
</td>
<td>
<code :title="webhook.full_url">{{ webhook.url }}</code>
</td>
<td class="hidden-sm hidden-xs">
<div class="btn-group btn-group-xs pull-right">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
{{ $t('firefly.actions') }} <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right" role="menu">
<li><a :href="'webhooks/show/' + webhook.id"><span
class="fa fa-fw fa-search"></span> {{ $t('firefly.inspect') }}</a></li>
<li><a :href="'webhooks/edit/' + webhook.id"><span
class="fa fa-fw fa-pencil"></span> {{ $t('firefly.edit') }}</a></li>
<li><a :href="'webhooks/delete/' + webhook.id"><span
class="fa fa-fw fa-trash"></span> {{ $t('firefly.delete') }}</a></li>
</ul>
</div>
</td>
</tr>
</tbody>
</table>
<div v-if="webhooks.length > 0" style="padding:8px;">
<a href="webhooks/create" class="btn btn-success"><span
class="fa fa-plus fa-fw"></span>{{ $t('firefly.create_new_webhook') }}</a>
</div>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div>
</template> </template>
<script> <script>
export default { export default {
name: "Index", name: "Index",
data() { data() {
return { return {
webhooks: [], webhooks: [],
triggers: { triggers: {
STORE_TRANSACTION: this.$t('firefly.webhook_trigger_STORE_TRANSACTION'), },
UPDATE_TRANSACTION: this.$t('firefly.webhook_trigger_UPDATE_TRANSACTION'), responses: {
DESTROY_TRANSACTION: this.$t('firefly.webhook_trigger_DESTROY_TRANSACTION'), },
}, deliveries: {
responses: { },
TRANSACTIONS: this.$t('firefly.webhook_response_TRANSACTIONS'), };
ACCOUNTS: this.$t('firefly.webhook_response_ACCOUNTS'),
NONE: this.$t('firefly.webhook_response_none_NONE'),
},
deliveries: {
JSON: this.$t('firefly.webhook_delivery_JSON'),
},
};
},
mounted() {
this.getWebhooks();
},
methods: {
getWebhooks: function () {
this.webhooks = [];
this.downloadWebhooks(1);
}, },
toggleSecret: function (webhook) { mounted() {
webhook.show_secret = !webhook.show_secret; this.getOptions();
}, },
downloadWebhooks: function (page) { methods: {
axios.get("./api/v1/webhooks?page=" + page).then((response) => { getOptions: function () {
for (let i in response.data.data) { // get triggers
if (response.data.data.hasOwnProperty(i)) { axios.get('./api/v1/configuration/webhook.triggers').then((response) => {
let current = response.data.data[i]; for (let key in response.data.data.value) {
let webhook = { if (!response.data.data.value.hasOwnProperty(key)) {
id: current.id, continue;
title: current.attributes.title, }
url: current.attributes.url, this.triggers[key] = this.$t('firefly.webhook_trigger_' + key);
active: current.attributes.active, }
full_url: current.attributes.url,
secret: current.attributes.secret,
trigger: current.attributes.trigger,
response: current.attributes.response,
delivery: current.attributes.delivery,
show_secret: false,
};
if(current.attributes.url.length > 20) {
webhook.url = current.attributes.url.slice(0, 20) + '...';
}
this.webhooks.push(webhook);
}
}
if (response.data.meta.pagination.current_page < response.data.meta.pagination.total_pages) { // get responses
this.downloadWebhooks(response.data.meta.pagination.current_page + 1); axios.get('./api/v1/configuration/webhook.responses').then((response) => {
} for (let key in response.data.data.value) {
}); if (!response.data.data.value.hasOwnProperty(key)) {
}, continue;
} }
this.responses[key] = this.$t('firefly.webhook_response_' + key);
}
// get deliveries
axios.get('./api/v1/configuration/webhook.deliveries').then((response) => {
for (let key in response.data.data.value) {
if (!response.data.data.value.hasOwnProperty(key)) {
continue;
}
this.deliveries[key] = this.$t('firefly.webhook_delivery_' + key);
}
// get webhooks
this.getWebhooks();
})
})
});
},
getWebhooks: function () {
this.webhooks = [];
this.downloadWebhooks(1);
},
toggleSecret: function (webhook) {
webhook.show_secret = !webhook.show_secret;
},
downloadWebhooks: function (page) {
axios.get("./api/v1/webhooks?page=" + page).then((response) => {
for (let i in response.data.data) {
if (response.data.data.hasOwnProperty(i)) {
let current = response.data.data[i];
let webhook = {
id: current.id,
title: current.attributes.title,
url: current.attributes.url,
active: current.attributes.active,
full_url: current.attributes.url,
secret: current.attributes.secret,
trigger: current.attributes.trigger,
response: current.attributes.response,
delivery: current.attributes.delivery,
show_secret: false,
};
if (current.attributes.url.length > 20) {
webhook.url = current.attributes.url.slice(0, 20) + '...';
}
this.webhooks.push(webhook);
}
}
if (response.data.meta.pagination.current_page < response.data.meta.pagination.total_pages) {
this.downloadWebhooks(response.data.meta.pagination.current_page + 1);
}
});
},
}
} }
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -24,6 +24,9 @@
declare(strict_types=1); declare(strict_types=1);
return [ return [
'webhook_budget_info' => 'Cannot deliver budget information for transaction related webhooks.',
'webhook_account_info' => 'Cannot deliver account information for budget related webhooks.',
'webhook_transaction_info' => 'Cannot deliver transaction information for budget related webhooks.',
'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities', 'invalid_account_type' => 'A piggy bank can only be linked to asset accounts and liabilities',
'invalid_account_currency' => 'This account does not use the currency you have selected', 'invalid_account_currency' => 'This account does not use the currency you have selected',
'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".', 'current_amount_too_much' => 'The combined amount in "current_amount" cannot exceed the "target_amount".',