Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple events in single request #13

Open
ashleyhood opened this issue Oct 24, 2022 · 7 comments · May be fixed by #26
Open

Multiple events in single request #13

ashleyhood opened this issue Oct 24, 2022 · 7 comments · May be fixed by #26

Comments

@ashleyhood
Copy link

I have been evaluating if this package would work with Xero webhooks.

One thing I would like to know is if there is a way in a custom provider to handle multiple events in the one request. This is how Xero handles webhooks by sending an array of events that you then loop through.

Example payload:

{
   "events": [
      {
         "resourceUrl": "https://api.xero.com/api.xro/2.0/Contacts/717f2bfc-c6d4-41fd-b238-3f2f0c0cf777",
         "resourceId": "717f2bfc-c6d4-41fd-b238-3f2f0c0cf777",
         "eventDateUtc": "2017-06-21T01:15:39.902",
         "eventType": "Update",
         "eventCategory": "CONTACT",
         "tenantId": "c2cc9b6e-9458-4c7d-93cc-f02b81b0594f",
         "tenantType": "ORGANISATION"
      }
   ],
   "lastEventSequence": 1,
   "firstEventSequence": 1,
   "entropy": "S0m3r4Nd0mt3xt"

}
@hotmeteor
Copy link
Owner

Interesting! Out of the box I'd say you could hack it by returning something generic for the event value and then all of your events for the data value, but your handler would have to just process the lot of them... which seems counter-productive.

It could be an option to allow for a collection of events instead of a single event (as a new feature). Is that something that would interest you?

@ashleyhood
Copy link
Author

ashleyhood commented Oct 25, 2022

The issue with Xero, is that the events are not all the same. So in one webhook request, events could be a mixture of contacts.update, contacts.create, invoice.update or invoice.create.

I have got around it so far by overriding the provider handle method which essentially ignores the getEvent and getData methods of the provider.

Here is my custom XeroProvider:

<?php

namespace App\Http\Receivers;

use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Receiver\Providers\AbstractProvider;

class XeroProvider extends AbstractProvider
{
    public function verify(Request $request): bool
    {
        if (! is_string($this->secret) || empty($this->secret)) {
            throw new \Exception('Xero webhook secret not found');
        }

        // Compute the payload with HMACSHA256 with base64 encoding
        $computedSignatureKey = base64_encode(
            hash_hmac('sha256', (string) $request->getContent(), $this->secret, true)
        );

        return hash_equals($computedSignatureKey, $request->header('X_XERO_SIGNATURE') ?? '');
    }

    public function getEvent(Request $request): string
    {
        $events = $request->collect('events');

        /**
         * For Xeros' 'Intent to receive' validation.
         * @see https://developer.xero.com/documentation/guides/webhooks/configuring-your-server/#intent-to-receive
         */
        if ($events->isEmpty()) {
            return '';
        }

        /** @var array $firstEvent */
        $firstEvent = $request->collect('events')->first();

        // We never use this but we have to return a string
        return Str::lower("{$firstEvent['eventCategory']}.{$firstEvent['eventType']}");
    }

    protected function handle(): void
    {
        /** @var Collection<string, Collection> $events */
        $events = $this->request
            ->collect('events')
            ->mapToGroups(fn ($item, $key) => [Str::lower("{$item['eventCategory']}.{$item['eventType']}") => $item]);

        foreach ($events as $eventName => $data) {
            $class = $this->getClass($eventName);

            if (class_exists($class)) {
                $instance = new $class($eventName, $data->toArray());

                $this->dispatched = dispatch($instance);
            }
        }
    }
}

I have not looked at how to implement this in the package but think there would need to be a step that transforms the data into collections of events and then each collection is handled as an event.

The other way I thought of handling this was to transform the data in the webhook controller and then loop through the event collections and call the provider multiple times.

Would be interested in your thoughts and think it would be a great addition if this package could handle this situation.

@jeffreyvanhees
Copy link

jeffreyvanhees commented Oct 19, 2023

Currently I'm building a receiver for Soketi websockets that can send webhooks to my server. Soketi also passes multiple events as an array to the webhook, like:

{
    "time_ms": 1697717045179,
    "events": [
        {
            "name": "channel_occupied",
            "channel": "admin",
            "data": {}
        },
        {
            "name": "member_added",
            "channel": "admin",
            "data": {}
        }
    ]
}

Maybe a solution is to allow the getEvent()-method also return an array instead of a string only, and if that's the case handle the hook as multiple event?

I've managed with this code now, but this colors a bit outside the lines compared to your implementation.

protected function handle(): static
{
    collect($this->request->collect('events'))->each(function ($event) {
        $data = $event['data'];
        $class = $this->getClass($event = $event['name']);

        if (class_exists($class)) {
            $class::dispatch($event, $data);
            $this->dispatched = true;
        }
    });

    return $this;
}

@hotmeteor hotmeteor linked a pull request Oct 19, 2023 that will close this issue
@hotmeteor
Copy link
Owner

Thanks @ashleyhood and @jeffreyvanhees

I finally took a stab at a change here. Check it out in this PR: #26

If you'd be willing to pull that branch into your project and try it out I'd be grateful!

@jeffreyvanhees
Copy link

Thanks @ashleyhood and @jeffreyvanhees

I finally took a stab at a change here. Check it out in this PR: #26

If you'd be willing to pull that branch into your project and try it out I'd be grateful!

That's fast @hotmeteor! Thanks for that. I'll give it a try and let you know! :)

@hotmeteor
Copy link
Owner

Actually, give me one second... I missed something. I'll update you when it's ready.

@hotmeteor
Copy link
Owner

OK, go for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants