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
{
Log::debug(sprintf('Now in %s', __METHOD__));
// grab asset account(s) from group:
$accounts = [];
/** @var TransactionJournal $journal */

View File

@@ -24,12 +24,18 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Controllers\Webhook;
use FireflyIII\Api\V1\Controllers\Controller;
use FireflyIII\Enums\WebhookTrigger;
use FireflyIII\Events\RequestedSendWebhookMessages;
use FireflyIII\Events\StoredTransactionGroup;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Generator\Webhook\MessageGeneratorInterface;
use FireflyIII\Models\TransactionGroup;
use FireflyIII\Models\Webhook;
use FireflyIII\Repositories\Webhook\WebhookRepositoryInterface;
use FireflyIII\Transformers\WebhookTransformer;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection;
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);
}
/**
* 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:
* https://api-docs.firefly-iii.org/#/webhooks/submitWebook
*
* Remove the specified resource from storage.
*
* @param Webhook $webhook
*
* @return JsonResponse

View File

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

View File

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

View File

@@ -53,6 +53,15 @@ class StandardMessageGenerator implements MessageGeneratorInterface
private int $version = 0;
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__);
// get the webhooks:
$this->webhooks = $this->getWebhooks();
if (0 === $this->webhooks->count()) {
$this->webhooks = $this->getWebhooks();
}
// do some debugging
Log::debug(
@@ -129,7 +140,9 @@ class StandardMessageGenerator implements MessageGeneratorInterface
switch ($class) {
default:
// 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;
case TransactionGroup::class:
@@ -240,4 +253,12 @@ class StandardMessageGenerator implements MessageGeneratorInterface
{
$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
{
Log::debug(sprintf('Now in %s', __METHOD__));
// kick off the job!
$messages = WebhookMessage::where('webhook_messages.sent', 0)
//->where('webhook_messages.errored', 0)
$messages = WebhookMessage::where('webhook_messages.sent',false)
->get(['webhook_messages.*'])
->filter(
function (WebhookMessage $message) {
@@ -48,7 +48,13 @@ class WebhookEventHandler
)->splice(0, 5);
Log::debug(sprintf('Found %d webhook message(s) ready to be send.', $messages->count()));
foreach ($messages as $message) {
SendWebhookMessage::dispatch($message)->afterResponse();
if (false === $message->sent) {
Log::debug(sprintf('Send message #%d', $message->id));
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'])) {
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');
if (('POST' === $method || 'PUT' === $method) && !in_array($submitted, $allowed, true)) {
$error = new BadHttpHeaderException(sprintf('Content-Type cannot be "%s"', $submitted));

View File

@@ -26,6 +26,7 @@ namespace FireflyIII\Models;
use Eloquent;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -117,4 +118,15 @@ class WebhookMessage extends Model
{
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
{
Log::debug(sprintf('Now in %s', __METHOD__));
$service = new TransactionGroupDestroyService();
$service->destroy($group);
}

View File

@@ -42,6 +42,7 @@ class JournalDestroyService
*/
public function destroy(TransactionJournal $journal): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
/** @var Transaction $transaction */
foreach ($journal->transactions()->get() as $transaction) {
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\Models\TransactionGroup;
use Illuminate\Support\Facades\Log;
/**
* Class TransactionGroupDestroyService
@@ -38,6 +39,7 @@ class TransactionGroupDestroyService
*/
public function destroy(TransactionGroup $transactionGroup): void
{
Log::debug(sprintf('Now in %s', __METHOD__));
/** @var JournalDestroyService $service */
$service = app(JournalDestroyService::class);
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
// end up in send() to be caught.
$signatureGenerator = app(SignatureGeneratorInterface::class);
$this->message->sent = true;
$this->message->save();
try {
$signature = $signatureGenerator->generate($this->message);
} catch (FireflyException $e) {
@@ -108,7 +109,6 @@ class StandardWebhookSender implements WebhookSenderInterface
$client = new Client();
try {
$res = $client->request('POST', $this->message->webhook->url, $options);
$this->message->sent = true;
} catch (RequestException $e) {
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
@@ -127,6 +127,7 @@ class StandardWebhookSender implements WebhookSenderInterface
return;
}
$this->message->sent = true;
$this->message->save();
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
- Working beta of the new layout under `/v3/`
- New authentication screens that support dark mode.
- There is a page for webhooks.
### Changed
- Firefly III requires PHP 8.1

View File

@@ -68,7 +68,8 @@
<div class="box-footer">
<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 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') }}
</a>
<a :href=delete_url class="btn btn-danger"><em class="fa fa-trash"></em> {{ $t('firefly.delete') }}</a>
@@ -85,7 +86,7 @@
<table class="table table-hover" aria-label="A table">
<tbody>
<tr>
<th scope="row" style="width:40%;">{{ $t('list.url') }}</th>
<th scope="row" style="width:40%;">{{ $t('list.url') }}</th>
<td><input type="text" readonly class="form-control" :value=url></td>
</tr>
<tr>
@@ -116,124 +117,130 @@
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ $t('firefly.webhook_messages') }}</h3>
</div>
<div class="box-body" v-if="messages.length === 0">
<p>
{{ $t('firefly.no_webhook_messages') }}
</p>
</div>
<div class="box-body no-padding" v-if="messages.length > 0">
<table class="table table-hover" aria-label="A table">
<thead>
<tr>
<th>
Date and time
</th>
<th>
UID
</th>
<th>
Success?
</th>
<th>
More details
</th>
</tr>
</thead>
<tbody>
<tr v-for="message in messages">
<td>
{{ message.created_at }}
</td>
<td>
{{ message.uuid }}
</td>
<td>
<em class="fa fa-check text-success" v-if="message.success"></em>
<em class="fa fa-times text-danger" v-if="!message.success"></em>
</td>
<td>
<a @click="showWebhookMessage(message.id)" class="btn btn-default">
<em class="fa fa-envelope"></em>
{{ $t('firefly.view_message') }}
</a>
<a @click="showWebhookAttempts(message.id)" class="btn btn-default">
<em class="fa fa-cloud-upload"></em>
{{ $t('firefly.view_attempts') }}
</a>
</td>
</tr>
</tbody>
<div class="box-body" v-if="messages.length === 0 && !loading">
<p>
{{ $t('firefly.no_webhook_messages') }}
</p>
</div>
<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">
<thead>
<tr>
<th>
Date and time
</th>
<th>
UID
</th>
<th>
Success?
</th>
<th>
More details
</th>
</tr>
</thead>
<tbody>
<tr v-for="message in messages">
<td>
{{ message.created_at }}
</td>
<td>
{{ message.uuid }}
</td>
<td>
<em class="fa fa-check text-success" v-if="message.success"></em>
<em class="fa fa-times text-danger" v-if="!message.success"></em>
</td>
<td>
<a @click="showWebhookMessage(message.id)" class="btn btn-default">
<em class="fa fa-envelope"></em>
{{ $t('firefly.view_message') }}
</a>
<a @click="showWebhookAttempts(message.id)" class="btn btn-default">
<em class="fa fa-cloud-upload"></em>
{{ $t('firefly.view_attempts') }}
</a>
</td>
</tr>
</tbody>
</table>
</div>
</table>
</div>
</div>
</div>
<!-- modal for message content -->
<div class="modal fade" id="messageModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ $t('firefly.message_content_title') }}</h4>
</div>
<div class="modal-body">
<p>
{{ $t('firefly.message_content_help') }}
</p>
<textarea class="form-control" rows="10" readonly>{{ message_content }}</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ $t('firefly.close') }}</button>
</div>
</div>
<!-- modal for message content -->
<div class="modal fade" id="messageModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ $t('firefly.message_content_title') }}</h4>
</div>
<div class="modal-body">
<p>
{{ $t('firefly.message_content_help') }}
</p>
<textarea class="form-control" rows="10" readonly>{{ message_content }}</textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ $t('firefly.close') }}</button>
</div>
</div>
</div>
</div>
<!-- modal for message attempts -->
<div class="modal fade" id="attemptModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ $t('firefly.attempt_content_title') }}</h4>
</div>
<div class="modal-body">
<!-- modal for message attempts -->
<div class="modal fade" id="attemptModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">{{ $t('firefly.attempt_content_title') }}</h4>
</div>
<div class="modal-body">
<p>
{{ $t('firefly.attempt_content_help') }}
</p>
<p v-if="0===message_attempts.length">
<em>
{{ $t('firefly.no_attempts') }}
</em>
</p>
<div v-for="message in message_attempts" style="border:1px #eee solid;margin-bottom:0.5em;">
<strong>
{{ $t('firefly.webhook_attempt_at', {moment: message.created_at}) }}
<span class="text-danger">({{ message.status_code }})</span>
</strong>
<p>
{{ $t('firefly.attempt_content_help') }}
{{ $t('firefly.logs') }}: <br/>
<textarea class="form-control" rows="5" readonly>{{ message.logs }}</textarea>
</p>
<p v-if="0===message_attempts.length">
<em>
{{ $t('firefly.no_attempts') }}
</em>
<p v-if="null !== message.response">
{{ $t('firefly.response') }}: <br/>
<textarea class="form-control" rows="5" readonly>{{ message.response }}</textarea>
</p>
<div v-for="message in message_attempts" style="border:1px #eee solid;margin-bottom:0.5em;">
<strong>
{{ $t('firefly.webhook_attempt_at', {moment: message.created_at}) }}
<span class="text-danger">({{ message.status_code }})</span>
</strong>
<p>
{{ $t('firefly.logs') }}: <br/>
<textarea class="form-control" rows="5" readonly>{{ message.logs }}</textarea>
</p>
<p v-if="null !== message.response">
{{ $t('firefly.response') }}: <br/>
<textarea class="form-control" rows="5" readonly>{{ message.response }}</textarea>
</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ $t('firefly.close') }}</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ $t('firefly.close') }}</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
@@ -253,6 +260,7 @@ export default {
secret: '',
show_secret: false,
trigger: '',
loading: true,
response: '',
message_content: '',
message_attempts: [],
@@ -266,6 +274,7 @@ export default {
},
methods: {
getWebhook() {
this.loading = true;
const page = window.location.href.split('/');
this.id = page[page.length - 1]
this.downloadWebhook();
@@ -278,11 +287,19 @@ export default {
let journalId = parseInt(prompt('Enter a transaction ID'));
if (journalId !== null && journalId > 0 && journalId <= 2 ^ 24) {
// 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');
// 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) {
@@ -310,6 +327,7 @@ export default {
});
}
}
this.loading = false;
});
},
showWebhookMessage: function (id) {
@@ -357,7 +375,3 @@ export default {
}
}
</script>
<style scoped>
</style>

View File

@@ -253,7 +253,7 @@ return [
'updated_webhook' => 'Updated webhook ":title"',
'edit_webhook_js' => 'Edit 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',
'view_message' => 'View message',
'view_attempts' => 'View failed attempts',

View File

@@ -643,6 +643,7 @@ Route::group(
Route::get('{webhook}', ['uses' => 'ShowController@show', 'as' => 'show']);
Route::put('{webhook}', ['uses' => 'UpdateController@update', 'as' => 'update']);
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']);
// webhook messages