Clear up webhooks

This commit is contained in:
James Cole
2023-01-05 19:05:23 +01:00
parent 7bd824e8cb
commit 1fee2092d6
17 changed files with 219 additions and 115 deletions

View File

@@ -80,6 +80,7 @@ class DestroyController extends Controller
*/ */
public function destroy(TransactionGroup $transactionGroup): JsonResponse public function destroy(TransactionGroup $transactionGroup): JsonResponse
{ {
Log::debug(sprintf('Now in %s', __METHOD__));
// grab asset account(s) from group: // grab asset account(s) from group:
$accounts = []; $accounts = [];
/** @var TransactionJournal $journal */ /** @var TransactionJournal $journal */

View File

@@ -24,12 +24,18 @@ 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\StoredTransactionGroup;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\Webhook; use FireflyIII\Models\Webhook;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface; use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Transformers\WebhookTransformer; use FireflyIII\Transformers\WebhookTransformer;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use League\Fractal\Pagination\IlluminatePaginatorAdapter; use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection; use League\Fractal\Resource\Collection as FractalCollection;
use League\Fractal\Resource\Item; use League\Fractal\Resource\Item;
@@ -111,4 +117,35 @@ class ShowController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE); return response()->json($manager->createData($resource)->toArray())->header('Content-Type', self::CONTENT_TYPE);
} }
/**
* This endpoint is documented at:
* https://api-docs.firefly-iii.org/#/webhooks/triggerWebhookTransaction
*
* This method recycles part of the code of the StoredGroupEventHandler.
*
* @param Webhook $webhook
* @param TransactionGroup $group
* @return JsonResponse
*/
public function triggerTransaction(Webhook $webhook, TransactionGroup $group): JsonResponse
{
/** @var MessageGeneratorInterface $engine */
$engine = app(MessageGeneratorInterface::class);
$engine->setUser(auth()->user());
// tell the generator which trigger it should look for
$engine->setTrigger($webhook->trigger);
// tell the generator which objects to process
$engine->setObjects(new Collection([$group]));
// set the webhook to trigger
$engine->setWebhooks(new Collection([$webhook]));
// tell the generator to generate the messages
$engine->generateMessages();
// trigger event to send them:
event(new RequestedSendWebhookMessages());
return response()->json([], 204);
}
} }

View File

@@ -56,8 +56,6 @@ class SubmitController extends Controller
* This endpoint is documented at: * This endpoint is documented at:
* https://api-docs.firefly-iii.org/#/webhooks/submitWebook * https://api-docs.firefly-iii.org/#/webhooks/submitWebook
* *
* Remove the specified resource from storage.
*
* @param Webhook $webhook * @param Webhook $webhook
* *
* @return JsonResponse * @return JsonResponse

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Events;
use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionGroup;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
/** /**
* Class DestroyedTransactionGroup. * Class DestroyedTransactionGroup.
@@ -45,6 +46,7 @@ class DestroyedTransactionGroup extends Event
*/ */
public function __construct(TransactionGroup $transactionGroup) public function __construct(TransactionGroup $transactionGroup)
{ {
Log::debug(sprintf('Now in %s', __METHOD__));
$this->transactionGroup = $transactionGroup; $this->transactionGroup = $transactionGroup;
} }
} }

View File

@@ -46,6 +46,12 @@ interface MessageGeneratorInterface
*/ */
public function setObjects(Collection $objects): void; public function setObjects(Collection $objects): void;
/**
* @param Collection $webhooks
* @return void
*/
public function setWebhooks(Collection $webhooks): void;
/** /**
* @param int $trigger * @param int $trigger
*/ */

View File

@@ -53,6 +53,15 @@ class StandardMessageGenerator implements MessageGeneratorInterface
private int $version = 0; private int $version = 0;
private Collection $webhooks; private Collection $webhooks;
/**
*
*/
public function __construct()
{
$this->objects = new Collection();
$this->webhooks = new Collection();
}
/** /**
* *
*/ */
@@ -60,7 +69,9 @@ class StandardMessageGenerator implements MessageGeneratorInterface
{ {
Log::debug(__METHOD__); Log::debug(__METHOD__);
// get the webhooks: // get the webhooks:
if (0 === $this->webhooks->count()) {
$this->webhooks = $this->getWebhooks(); $this->webhooks = $this->getWebhooks();
}
// do some debugging // do some debugging
Log::debug( Log::debug(
@@ -129,7 +140,9 @@ 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(sprintf('Webhook #%d was given %s#%d to deal with but can\'t extract user ID from it.', $webhook->id, $class, $model->id)); // @phpstan-ignore-line 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)
); // @phpstan-ignore-line
return; return;
case TransactionGroup::class: case TransactionGroup::class:
@@ -240,4 +253,12 @@ class StandardMessageGenerator implements MessageGeneratorInterface
{ {
$this->user = $user; $this->user = $user;
} }
/**
* @inheritDoc
*/
public function setWebhooks(Collection $webhooks): void
{
$this->webhooks = $webhooks;
}
} }

View File

@@ -37,9 +37,9 @@ class WebhookEventHandler
*/ */
public function sendWebhookMessages(): void public function sendWebhookMessages(): void
{ {
Log::debug(sprintf('Now in %s', __METHOD__));
// kick off the job! // kick off the job!
$messages = WebhookMessage::where('webhook_messages.sent', 0) $messages = WebhookMessage::where('webhook_messages.sent',false)
//->where('webhook_messages.errored', 0)
->get(['webhook_messages.*']) ->get(['webhook_messages.*'])
->filter( ->filter(
function (WebhookMessage $message) { function (WebhookMessage $message) {
@@ -48,7 +48,13 @@ class WebhookEventHandler
)->splice(0, 5); )->splice(0, 5);
Log::debug(sprintf('Found %d webhook message(s) ready to be send.', $messages->count())); Log::debug(sprintf('Found %d webhook message(s) ready to be send.', $messages->count()));
foreach ($messages as $message) { foreach ($messages as $message) {
if (false === $message->sent) {
Log::debug(sprintf('Send message #%d', $message->id));
SendWebhookMessage::dispatch($message)->afterResponse(); SendWebhookMessage::dispatch($message)->afterResponse();
} }
if (false !== $message->sent) {
Log::debug(sprintf('Skip message #%d', $message->id));
}
}
} }
} }

View File

@@ -49,7 +49,7 @@ class AcceptHeaders
if ('GET' === $method && !$request->accepts(['application/json', 'application/vdn.api+json'])) { if ('GET' === $method && !$request->accepts(['application/json', 'application/vdn.api+json'])) {
throw new BadHttpHeaderException('Your request must accept either application/json or application/vdn.api+json.'); throw new BadHttpHeaderException('Your request must accept either application/json or application/vdn.api+json.');
} }
$allowed = ['application/x-www-form-urlencoded', 'application/json']; $allowed = ['application/x-www-form-urlencoded', 'application/json',''];
$submitted = (string)$request->header('Content-Type'); $submitted = (string)$request->header('Content-Type');
if (('POST' === $method || 'PUT' === $method) && !in_array($submitted, $allowed, true)) { if (('POST' === $method || 'PUT' === $method) && !in_array($submitted, $allowed, true)) {
$error = new BadHttpHeaderException(sprintf('Content-Type cannot be "%s"', $submitted)); $error = new BadHttpHeaderException(sprintf('Content-Type cannot be "%s"', $submitted));

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Models;
use Eloquent; use Eloquent;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -117,4 +118,15 @@ class WebhookMessage extends Model
{ {
return $this->hasMany(WebhookAttempt::class); return $this->hasMany(WebhookAttempt::class);
} }
/**
* Get the amount
*
* @return Attribute
*/
protected function sent(): Attribute
{
return Attribute::make(
get: fn ($value) => (bool)$value,
);
}
} }

View File

@@ -85,6 +85,7 @@ class TransactionGroupRepository implements TransactionGroupRepositoryInterface
*/ */
public function destroy(TransactionGroup $group): void public function destroy(TransactionGroup $group): void
{ {
Log::debug(sprintf('Now in %s', __METHOD__));
$service = new TransactionGroupDestroyService(); $service = new TransactionGroupDestroyService();
$service->destroy($group); $service->destroy($group);
} }

View File

@@ -42,6 +42,7 @@ class JournalDestroyService
*/ */
public function destroy(TransactionJournal $journal): void public function destroy(TransactionJournal $journal): void
{ {
Log::debug(sprintf('Now in %s', __METHOD__));
/** @var Transaction $transaction */ /** @var Transaction $transaction */
foreach ($journal->transactions()->get() as $transaction) { foreach ($journal->transactions()->get() as $transaction) {
Log::debug(sprintf('Will now delete transaction #%d', $transaction->id)); Log::debug(sprintf('Will now delete transaction #%d', $transaction->id));

View File

@@ -25,6 +25,7 @@ namespace FireflyIII\Services\Internal\Destroy;
use FireflyIII\Events\DestroyedTransactionGroup; use FireflyIII\Events\DestroyedTransactionGroup;
use FireflyIII\Models\TransactionGroup; use FireflyIII\Models\TransactionGroup;
use Illuminate\Support\Facades\Log;
/** /**
* Class TransactionGroupDestroyService * Class TransactionGroupDestroyService
@@ -38,6 +39,7 @@ class TransactionGroupDestroyService
*/ */
public function destroy(TransactionGroup $transactionGroup): void public function destroy(TransactionGroup $transactionGroup): void
{ {
Log::debug(sprintf('Now in %s', __METHOD__));
/** @var JournalDestroyService $service */ /** @var JournalDestroyService $service */
$service = app(JournalDestroyService::class); $service = app(JournalDestroyService::class);
foreach ($transactionGroup->transactionJournals as $journal) { foreach ($transactionGroup->transactionJournals as $journal) {

View File

@@ -56,7 +56,8 @@ class StandardWebhookSender implements WebhookSenderInterface
// have the signature generator generate a signature. If it fails, the error thrown will // have the signature generator generate a signature. If it fails, the error thrown will
// end up in send() to be caught. // end up in send() to be caught.
$signatureGenerator = app(SignatureGeneratorInterface::class); $signatureGenerator = app(SignatureGeneratorInterface::class);
$this->message->sent = true;
$this->message->save();
try { try {
$signature = $signatureGenerator->generate($this->message); $signature = $signatureGenerator->generate($this->message);
} catch (FireflyException $e) { } catch (FireflyException $e) {
@@ -108,7 +109,6 @@ class StandardWebhookSender implements WebhookSenderInterface
$client = new Client(); $client = new Client();
try { try {
$res = $client->request('POST', $this->message->webhook->url, $options); $res = $client->request('POST', $this->message->webhook->url, $options);
$this->message->sent = true;
} catch (RequestException $e) { } catch (RequestException $e) {
Log::error($e->getMessage()); Log::error($e->getMessage());
Log::error($e->getTraceAsString()); Log::error($e->getTraceAsString());
@@ -127,6 +127,7 @@ class StandardWebhookSender implements WebhookSenderInterface
return; return;
} }
$this->message->sent = true;
$this->message->save(); $this->message->save();
Log::debug(sprintf('Webhook message #%d was sent. Status code %d', $this->message->id, $res->getStatusCode())); Log::debug(sprintf('Webhook message #%d was sent. Status code %d', $this->message->id, $res->getStatusCode()));

View File

@@ -30,6 +30,7 @@ Lots of new stuff that I invite you to test and break.
- #6605 You can search for external ID values - #6605 You can search for external ID values
- Working beta of the new layout under `/v3/` - Working beta of the new layout under `/v3/`
- New authentication screens that support dark mode. - New authentication screens that support dark mode.
- There is a page for webhooks.
### Changed ### Changed
- Firefly III requires PHP 8.1 - Firefly III requires PHP 8.1

View File

@@ -68,7 +68,8 @@
<div class="box-footer"> <div class="box-footer">
<div class="btn-group pull-right"> <div class="btn-group pull-right">
<a :href=edit_url class="btn btn-default"><em class="fa fa-pencil"></em> {{ $t('firefly.edit') }}</a> <a :href=edit_url class="btn btn-default"><em class="fa fa-pencil"></em> {{ $t('firefly.edit') }}</a>
<a id="triggerButton" href="#" @click="submitTest" class="btn btn-default"><em class="fa fa-bolt"></em> <a id="triggerButton" v-if="active" href="#" @click="submitTest" class="btn btn-default"><em
class="fa fa-bolt"></em>
{{ $t('list.trigger') }} {{ $t('list.trigger') }}
</a> </a>
<a :href=delete_url class="btn btn-danger"><em class="fa fa-trash"></em> {{ $t('firefly.delete') }}</a> <a :href=delete_url class="btn btn-danger"><em class="fa fa-trash"></em> {{ $t('firefly.delete') }}</a>
@@ -116,18 +117,24 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">{{ $t('firefly.webhook_messages') }}</h3> <h3 class="box-title">{{ $t('firefly.webhook_messages') }}</h3>
</div> </div>
<div class="box-body" v-if="messages.length === 0"> <div class="box-body" v-if="messages.length === 0 && !loading">
<p> <p>
{{ $t('firefly.no_webhook_messages') }} {{ $t('firefly.no_webhook_messages') }}
</p> </p>
</div> </div>
<div class="box-body no-padding" v-if="messages.length > 0"> <div class="box-body" v-if="loading">
<p class="text-center">
<em class="fa fa-spin fa-spinner"></em>
</p>
</div>
<div class="box-body no-padding" v-if="messages.length > 0 && !loading">
<table class="table table-hover" aria-label="A table"> <table class="table table-hover" aria-label="A table">
<thead> <thead>
<tr> <tr>
@@ -253,6 +260,7 @@ export default {
secret: '', secret: '',
show_secret: false, show_secret: false,
trigger: '', trigger: '',
loading: true,
response: '', response: '',
message_content: '', message_content: '',
message_attempts: [], message_attempts: [],
@@ -266,6 +274,7 @@ export default {
}, },
methods: { methods: {
getWebhook() { getWebhook() {
this.loading = true;
const page = window.location.href.split('/'); const page = window.location.href.split('/');
this.id = page[page.length - 1] this.id = page[page.length - 1]
this.downloadWebhook(); this.downloadWebhook();
@@ -278,11 +287,19 @@ export default {
let journalId = parseInt(prompt('Enter a transaction ID')); let journalId = parseInt(prompt('Enter a transaction ID'));
if (journalId !== null && journalId > 0 && journalId <= 2 ^ 24) { if (journalId !== null && journalId > 0 && journalId <= 2 ^ 24) {
// disable button. Add informative message. // disable button. Add informative message.
$('#triggerButton').prop('disabled', true).addClass('disabled'); let button = $('#triggerButton');
button.prop('disabled', true).addClass('disabled');
this.success_message = this.$t('firefly.webhook_was_triggered'); this.success_message = this.$t('firefly.webhook_was_triggered');
// TODO actually trigger the webhook. // TODO actually trigger the webhook.
axios.post('./api/v1/webhooks/' + this.id + '/trigger', {id: journalId}); axios.post('./api/v1/webhooks/' + this.id + '/trigger-transaction/' + journalId, {});
button.prop('disabled', false).removeClass('disabled');
// set a time-outs.
this.loading = true;
setTimeout(() => {
this.getWebhook();
}, 2000);
} }
if (e) { if (e) {
@@ -310,6 +327,7 @@ export default {
}); });
} }
} }
this.loading = false;
}); });
}, },
showWebhookMessage: function (id) { showWebhookMessage: function (id) {
@@ -357,7 +375,3 @@ export default {
} }
} }
</script> </script>
<style scoped>
</style>

View File

@@ -253,7 +253,7 @@ return [
'updated_webhook' => 'Updated webhook ":title"', 'updated_webhook' => 'Updated webhook ":title"',
'edit_webhook_js' => 'Edit webhook "{title}"', 'edit_webhook_js' => 'Edit webhook "{title}"',
'show_webhook' => 'Webhook ":title"', 'show_webhook' => 'Webhook ":title"',
'webhook_was_triggered' => 'The webhook was triggered on the indicated transaction. You can refresh this page to see the results.', 'webhook_was_triggered' => 'The webhook was triggered on the indicated transaction. Please wait for results to appear.',
'webhook_messages' => 'Webhook message', 'webhook_messages' => 'Webhook message',
'view_message' => 'View message', 'view_message' => 'View message',
'view_attempts' => 'View failed attempts', 'view_attempts' => 'View failed attempts',

View File

@@ -643,6 +643,7 @@ Route::group(
Route::get('{webhook}', ['uses' => 'ShowController@show', 'as' => 'show']); Route::get('{webhook}', ['uses' => 'ShowController@show', 'as' => 'show']);
Route::put('{webhook}', ['uses' => 'UpdateController@update', 'as' => 'update']); Route::put('{webhook}', ['uses' => 'UpdateController@update', 'as' => 'update']);
Route::post('{webhook}/submit', ['uses' => 'SubmitController@submit', 'as' => 'submit']); Route::post('{webhook}/submit', ['uses' => 'SubmitController@submit', 'as' => 'submit']);
Route::post('{webhook}/trigger-transaction/{transactionGroup}', ['uses' => 'ShowController@triggerTransaction', 'as' => 'trigger-transaction']);
Route::delete('{webhook}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']); Route::delete('{webhook}', ['uses' => 'DestroyController@destroy', 'as' => 'destroy']);
// webhook messages // webhook messages