-
Notifications
You must be signed in to change notification settings - Fork 30
feat(integration-whatsapp): core ingest do coletor de WhatsApp #273
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
base: 4.x
Are you sure you want to change the base?
Changes from all commits
8397b1b
f75506b
e1354b4
80c374d
6fb8897
f6ff5a2
c264605
3955aa1
8677ff6
a0755eb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Integration WhatsApp Context | ||
|
|
||
| Ingest and integration layer for the WhatsApp platform. Receives normalized events from an | ||
| external collector service (`wpp-tui`, a separate Node/Baileys repo) via authenticated webhook, | ||
| and persists them as a raw event store (data lake) for later analysis. | ||
|
|
||
| > **Status:** Core ingest implemented (migrations, models, HMAC webhook, raw event store + Job). | ||
| > The Collector sends every event and Laravel stores everything raw — there is **no collection policy / | ||
| > filtering** in this phase (the per-group `collection_policy` idea was dropped; revisit if/when a | ||
| > hardening phase needs it). Design in `docs/spec.md`; data-modeling decision in | ||
| > `docs/adr/0001-data-lake-approach.md`; implementation plan in `docs/plans/0001-ingest-implementation.md`. | ||
| > Also deferred: Filament admin, identity-link flow, `wpp-tui` webhook wiring, deploy/retention/onboarding. | ||
|
|
||
| ## Glossary | ||
|
|
||
| | Term | Definition | Not to be confused with | | ||
| | ---------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | ||
| | **Collector** | The external Node/Baileys service (`wpp-tui` repo) that holds the WhatsApp WebSocket session and emits events. | This module (which is the Laravel-side ingest, not the WhatsApp runtime). | | ||
| | **Webhook ingest** | The HTTP endpoint that receives events from the Collector, validates HMAC, and enqueues a Job. | The Collector's own optional `WEBHOOK_URL` proxy (raw forwarding). | | ||
| | **Event store** | The `whatsapp_events` table holding raw Baileys payloads (jsonb), one row per event. | Aggregated metrics (which do not exist yet — deferred until data team). | | ||
| | **Identity link** | The future flow that associates a `whatsapp_participants` row with an `identity.users` record. | OAuth (Discord uses OAuth; WhatsApp uses a DM verification code flow). | | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| WhatsApp servers | ||
| ↓ WebSocket (Baileys) | ||
| [ wpp-tui — external Node repo ] ← runtime, holds session, sends every event (no filtering) | ||
| ↓ POST /api/integrations/whatsapp/events (HMAC + X-Event-Id) | ||
| [ integration-whatsapp — THIS module ] | ||
| ├─ Ingest/Http/Middleware/VerifyWhatsAppSignature ← validate HMAC + X-Event-Id | ||
| ├─ Ingest/Http/Controllers/WhatsAppWebhookController ← idempotency check, dispatch Job → 202 | ||
| ├─ Ingest/Jobs/ProcessWhatsAppEvent ← upsert group/participant, insert raw event | ||
| └─ Models/{WhatsAppGroup, WhatsAppParticipant, WhatsAppEvent} | ||
| ↓ (future) | ||
| [ identity ] ← resolve participant → user (DM verification flow) | ||
| [ activity ] ← (future) aggregate engagement once metrics are defined | ||
| ``` | ||
|
|
||
| ## Module Boundaries | ||
|
|
||
| ### This module owns: | ||
|
|
||
| - The webhook ingest endpoint and its HMAC/idempotency validation | ||
| - The raw event store (`whatsapp_groups`, `whatsapp_participants`, `whatsapp_events`) | ||
| - Upsert logic for groups and participants from incoming events | ||
| - (Future) the identity-link verification endpoint | ||
|
|
||
| ### This module does NOT own: | ||
|
|
||
| - The WhatsApp WebSocket connection / Baileys runtime (lives in the `wpp-tui` repo) | ||
| - Metric aggregation / dashboards (deferred — will likely live in `activity` + `panel-admin`) | ||
| - Identity/user records (belongs to `identity`) | ||
|
|
||
| ## Dependencies | ||
|
|
||
| - **Identity** — participant ↔ user linking (future) | ||
| - **No dependency on** `bot-discord`, `moderation`, `community`, etc. | ||
|
|
||
| ## Key decisions (see docs/adr/0001) | ||
|
|
||
| - **Data lake first**: store raw Baileys payloads in `whatsapp_events.payload` (jsonb). Decide what to | ||
| measure later, once the data team has material to work with. | ||
| - **No phone hashing / no encryption (for now)**: `participants.external_jid` stores the real WhatsApp | ||
| JID (real phone number). Conscious decision for the exploration phase. | ||
| - **No TTL (for now)**: events are retained indefinitely until the exploration phase ends and a | ||
| retention policy is defined. | ||
| - **Collect all event types**: including `presence.update`. Volume is accepted as the cost of mapping. | ||
| - **Only `type` is materialized top-level** on `whatsapp_events`; everything else lives in `payload`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| { | ||
| "name": "he4rt/integration-whatsapp", | ||
| "description": "", | ||
| "type": "library", | ||
| "version": "1.0", | ||
| "license": "proprietary", | ||
| "require": {}, | ||
| "autoload": { | ||
| "psr-4": { | ||
| "He4rt\\IntegrationWhatsapp\\": "src/", | ||
| "He4rt\\IntegrationWhatsapp\\Database\\Factories\\": "database/factories/", | ||
| "He4rt\\IntegrationWhatsapp\\Database\\Seeders\\": "database/seeders/" | ||
| } | ||
| }, | ||
| "autoload-dev": { | ||
| "psr-4": { | ||
| "He4rt\\IntegrationWhatsapp\\Tests\\": "tests/" | ||
| } | ||
| }, | ||
| "minimum-stability": "stable", | ||
| "extra": { | ||
| "laravel": { | ||
| "providers": [ | ||
| "He4rt\\IntegrationWhatsapp\\IntegrationWhatsappServiceProvider" | ||
| ] | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| return [ | ||
| 'webhook_secret' => env('WHATSAPP_WEBHOOK_SECRET', ''), | ||
| ]; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\IntegrationWhatsapp\Database\Factories; | ||
|
|
||
| use He4rt\IntegrationWhatsapp\Models\WhatsAppEvent; | ||
| use He4rt\IntegrationWhatsapp\Models\WhatsAppGroup; | ||
| use He4rt\IntegrationWhatsapp\Models\WhatsAppParticipant; | ||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||
|
|
||
| /** | ||
| * @extends Factory<WhatsAppEvent> | ||
| */ | ||
| final class WhatsAppEventFactory extends Factory | ||
| { | ||
| protected $model = WhatsAppEvent::class; | ||
|
|
||
| public function definition(): array | ||
| { | ||
| return [ | ||
| 'event_id' => fake()->uuid(), | ||
| 'type' => 'messages.upsert', | ||
| 'group_id' => WhatsAppGroup::factory(), | ||
| 'participant_id' => WhatsAppParticipant::factory(), | ||
| 'participant_alt' => fake()->numerify('################').'@lid', | ||
| 'occurred_at' => now(), | ||
| 'occurred_at_source' => 'whatsapp', | ||
| 'received_at' => now(), | ||
| 'payload' => [ | ||
| 'key' => ['id' => fake()->uuid()], | ||
| 'message' => ['conversation' => fake()->sentence()], | ||
| ], | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\IntegrationWhatsapp\Database\Factories; | ||
|
|
||
| use He4rt\IntegrationWhatsapp\Models\WhatsAppGroup; | ||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||
|
|
||
| /** | ||
| * @extends Factory<WhatsAppGroup> | ||
| */ | ||
| final class WhatsAppGroupFactory extends Factory | ||
| { | ||
| protected $model = WhatsAppGroup::class; | ||
|
|
||
| public function definition(): array | ||
| { | ||
| return [ | ||
| 'external_jid' => fake()->unique()->numerify('1203#########').'@g.us', | ||
| 'display_name' => fake()->words(2, true), | ||
| 'internal_name' => fake()->randomElement(['geral', 'delas', 'vagas']), | ||
| 'payload' => [], | ||
| 'first_seen_at' => now(), | ||
| 'last_seen_at' => now(), | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\IntegrationWhatsapp\Database\Factories; | ||
|
|
||
| use He4rt\IntegrationWhatsapp\Models\WhatsAppGroup; | ||
| use He4rt\IntegrationWhatsapp\Models\WhatsAppGroupParticipant; | ||
| use He4rt\IntegrationWhatsapp\Models\WhatsAppParticipant; | ||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||
|
|
||
| /** | ||
| * @extends Factory<WhatsAppGroupParticipant> | ||
| */ | ||
| final class WhatsAppGroupParticipantFactory extends Factory | ||
| { | ||
| protected $model = WhatsAppGroupParticipant::class; | ||
|
|
||
| public function definition(): array | ||
| { | ||
| return [ | ||
| 'group_id' => WhatsAppGroup::factory(), | ||
| 'participant_id' => WhatsAppParticipant::factory(), | ||
| 'admin_role' => null, | ||
| 'joined_at' => now(), | ||
| 'left_at' => null, | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace He4rt\IntegrationWhatsapp\Database\Factories; | ||
|
|
||
| use He4rt\IntegrationWhatsapp\Models\WhatsAppParticipant; | ||
| use Illuminate\Database\Eloquent\Factories\Factory; | ||
|
|
||
| /** | ||
| * @extends Factory<WhatsAppParticipant> | ||
| */ | ||
| final class WhatsAppParticipantFactory extends Factory | ||
| { | ||
| protected $model = WhatsAppParticipant::class; | ||
|
|
||
| public function definition(): array | ||
| { | ||
| return [ | ||
| 'external_jid' => fake()->unique()->numerify('5511#########').'@s.whatsapp.net', | ||
| 'push_name' => fake()->name(), | ||
| 'payload' => [], | ||
| 'identity_id' => null, | ||
| 'first_seen_at' => now(), | ||
| 'last_seen_at' => now(), | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Illuminate\Database\Migrations\Migration; | ||
| use Illuminate\Database\Schema\Blueprint; | ||
| use Illuminate\Support\Facades\Schema; | ||
|
|
||
| return new class extends Migration | ||
| { | ||
| public function up(): void | ||
| { | ||
| Schema::create('whatsapp_groups', function (Blueprint $table): void { | ||
| $table->uuid('id')->primary(); | ||
| $table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete(); | ||
| $table->string('external_jid')->unique(); | ||
| $table->string('display_name')->nullable(); | ||
| $table->string('internal_name')->nullable(); | ||
| $table->jsonb('payload')->nullable(); | ||
| $table->timestamp('first_seen_at')->nullable(); | ||
| $table->timestamp('last_seen_at')->nullable(); | ||
| $table->timestamps(); | ||
| }); | ||
| } | ||
|
|
||
| public function down(): void | ||
| { | ||
| Schema::dropIfExists('whatsapp_groups'); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,29 @@ | ||||||
| <?php | ||||||
|
|
||||||
| declare(strict_types=1); | ||||||
|
|
||||||
| use Illuminate\Database\Migrations\Migration; | ||||||
| use Illuminate\Database\Schema\Blueprint; | ||||||
| use Illuminate\Support\Facades\Schema; | ||||||
|
|
||||||
| return new class extends Migration | ||||||
| { | ||||||
| public function up(): void | ||||||
| { | ||||||
| Schema::create('whatsapp_participants', function (Blueprint $table): void { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| $table->uuid('id')->primary(); | ||||||
| $table->string('external_jid')->unique(); | ||||||
| $table->string('push_name')->nullable(); | ||||||
| $table->jsonb('payload')->nullable(); | ||||||
| $table->foreignUuid('identity_id')->nullable()->constrained('users')->nullOnDelete(); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove |
||||||
| $table->timestamp('first_seen_at')->nullable(); | ||||||
| $table->timestamp('last_seen_at')->nullable(); | ||||||
| $table->timestamps(); | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| public function down(): void | ||||||
| { | ||||||
| Schema::dropIfExists('whatsapp_participants'); | ||||||
| } | ||||||
| }; | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Illuminate\Database\Migrations\Migration; | ||
| use Illuminate\Database\Schema\Blueprint; | ||
| use Illuminate\Support\Facades\Schema; | ||
|
|
||
| return new class extends Migration | ||
| { | ||
| public function up(): void | ||
| { | ||
| Schema::create('whatsapp_events', function (Blueprint $table): void { | ||
| $table->uuid('id')->primary(); | ||
| $table->foreignId('tenant_id')->nullable()->constrained('tenants')->nullOnDelete(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remover. |
||
| $table->uuid('event_id')->unique(); | ||
| $table->string('type')->index(); | ||
| $table->foreignUuid('group_id')->nullable()->constrained('whatsapp_groups')->nullOnDelete(); | ||
| $table->foreignUuid('participant_id')->nullable()->constrained('whatsapp_participants')->nullOnDelete(); | ||
|
Comment on lines
+18
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. atualizar. |
||
| $table->string('participant_alt')->nullable(); | ||
| $table->timestamp('occurred_at')->index(); | ||
| $table->string('occurred_at_source')->nullable(); | ||
| $table->timestamp('received_at')->nullable(); | ||
| $table->jsonb('payload'); | ||
| $table->timestamps(); | ||
|
|
||
| $table->index(['type', 'occurred_at']); | ||
| $table->index(['group_id', 'occurred_at']); | ||
| $table->index(['participant_id', 'occurred_at']); | ||
| }); | ||
| } | ||
|
|
||
| public function down(): void | ||
| { | ||
| Schema::dropIfExists('whatsapp_events'); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| use Illuminate\Database\Migrations\Migration; | ||
| use Illuminate\Database\Schema\Blueprint; | ||
| use Illuminate\Support\Facades\Schema; | ||
|
|
||
| return new class extends Migration | ||
| { | ||
| public function up(): void | ||
| { | ||
| Schema::create('whatsapp_group_participants', function (Blueprint $table): void { | ||
| $table->uuid('id')->primary(); | ||
| $table->foreignUuid('group_id')->constrained('whatsapp_groups')->cascadeOnDelete(); | ||
| $table->foreignUuid('participant_id')->constrained('whatsapp_participants')->cascadeOnDelete(); | ||
| $table->string('admin_role')->nullable(); | ||
| $table->timestamp('joined_at')->nullable(); | ||
| $table->timestamp('left_at')->nullable(); | ||
| $table->timestamps(); | ||
|
Comment on lines
+14
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. missing "tag" attribute. role: |
||
|
|
||
| $table->unique(['group_id', 'participant_id']); | ||
| $table->index(['group_id', 'left_at']); | ||
| }); | ||
| } | ||
|
|
||
| public function down(): void | ||
| { | ||
| Schema::dropIfExists('whatsapp_group_participants'); | ||
| } | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trocar WhatsappGroup pra WhatsappConversation.
Mude em todo o projeto.