├── .github └── workflows │ └── php.yml ├── .gitignore ├── README.md ├── composer.json ├── composer.lock ├── config └── cloudapi.php ├── database └── migrations │ ├── 2024_02_10_174929_create_wabas_table.php │ ├── 2024_02_10_174934_create_waba_phones_table.php │ ├── 2024_02_10_220808_create_templates_table.php │ ├── 2024_02_11_220104_create_chat_categories_table.php │ ├── 2024_02_11_220404_create_chats_table.php │ ├── 2024_02_11_220525_create_messages_table.php │ └── 2024_03_06_141423_create_campaigns_table.php ├── docker-compose.yml ├── dockerfile ├── phpunit.xml ├── routes ├── api.php └── channels.php ├── src ├── Console │ └── Commands │ │ ├── CopyMessengerCommand.php │ │ └── InstallCommand.php ├── Events │ └── NewWhatsappMessageHook.php ├── Facade.php ├── Factories │ ├── CampaignFactory.php │ ├── ChatFactory.php │ ├── MessageFactory.php │ ├── TemplateFactory.php │ ├── WabaFactory.php │ └── WabaPhoneFactory.php ├── Http │ ├── Controllers │ │ ├── APIResourceController.php │ │ ├── CampaignController.php │ │ ├── ChatController.php │ │ ├── MessageController.php │ │ ├── TemplateController.php │ │ ├── WabaController.php │ │ ├── WabaPhoneController.php │ │ └── WebhookController.php │ ├── Requests │ │ ├── SendTemplateRequest.php │ │ ├── StoreCampaignRequest.php │ │ └── StoreTemplateRequest.php │ └── Resources │ │ ├── MessageResource.php │ │ └── TemplateResource.php ├── Jobs │ └── SendToOpenApi.php ├── Lib │ ├── Message │ │ ├── BussinessProfile.php │ │ ├── ProcessMessageWebhook.php │ │ ├── ReceivedMessage.php │ │ ├── ReceivedMessageStatus.php │ │ └── SendMessage.php │ └── Template │ │ ├── Adapter │ │ ├── AppToMeta.php │ │ ├── AppToResponse.php │ │ ├── MetaToApp.php │ │ └── RequestToMeta.php │ │ ├── CreateTemplate.php │ │ ├── ProcessStatusWebhook.php │ │ └── SendTemplate.php ├── Listeners │ └── ResponseMessageReady.php ├── Models │ ├── Campaign.php │ ├── Chat.php │ ├── ChatCategory.php │ ├── Message.php │ ├── Template.php │ ├── Waba.php │ └── WabaPhone.php ├── ServiceProvider.php ├── Services │ ├── FacebookService.php │ ├── FileManager.php │ ├── MediaManagerService.php │ ├── MessageService.php │ ├── ResumableUploadAPI.php │ ├── TemplateManagerService.php │ └── WabaManagerService.php └── WhatsappCloudApi.php ├── stubs ├── 10 │ └── app │ │ └── Exceptions │ │ └── Handler.php ├── 11 │ └── bootstrap │ │ └── app.php ├── common │ └── docker-compose.yml └── interface │ ├── postcss.config.js │ ├── resources │ ├── css │ │ └── app.css │ ├── js │ │ ├── app.js │ │ └── components │ │ │ └── Messenger │ │ │ ├── Chat │ │ │ ├── ChatBubble.vue │ │ │ ├── Messenger.vue │ │ │ ├── NewCampaign.vue │ │ │ ├── NewTemplateMessage.vue │ │ │ ├── SendMediaMessage.vue │ │ │ ├── SendNew.vue │ │ │ └── Template.vue │ │ │ ├── Template │ │ │ ├── Create.vue │ │ │ ├── Detail.vue │ │ │ ├── Forms │ │ │ │ ├── Body.vue │ │ │ │ ├── Buttons.vue │ │ │ │ ├── Buttons │ │ │ │ │ ├── DisableMarketing.vue │ │ │ │ │ ├── PhoneNumber.vue │ │ │ │ │ ├── QuickReply.vue │ │ │ │ │ └── Url.vue │ │ │ │ ├── Card.vue │ │ │ │ ├── Cards.vue │ │ │ │ ├── Footer.vue │ │ │ │ ├── Form.vue │ │ │ │ ├── Header.vue │ │ │ │ └── Waba.vue │ │ │ ├── Index.vue │ │ │ ├── Preview │ │ │ │ └── Index.vue │ │ │ └── Update.vue │ │ │ └── Ui │ │ │ └── Breadcrumb.vue │ └── views │ │ ├── messenger.blade.php │ │ └── template │ │ ├── create.blade.php │ │ ├── edit.blade.php │ │ ├── index.blade.php │ │ └── show.blade.php │ ├── tailwind.config.js │ └── vite.config.js └── tests ├── Fake ├── Message │ ├── FakeMessageCreteResponse.php │ ├── FakeMessageHook.php │ └── FakeMessageRequests.php ├── Template │ ├── FakeCreateTemplate.php │ └── FakeTemplateToMetaFormat.php ├── Waba │ └── FakeWabaResponses.php └── Webhook │ ├── FakeReceivedMessage.php │ ├── FakeTemplateStatusWebhook.php │ └── FakeWebhook.php ├── Feature ├── Campaign │ └── CreateCampaignTest.php ├── Chat │ └── ChatTest.php ├── Commands │ ├── CopyMessengerCommandTest.php │ └── InstallTest.php ├── Message │ ├── MessageStatusWebhookTest.php │ ├── MessageTest.php │ ├── ReceivedMessageTest.php │ ├── SendMessagesTest.php │ └── WabaPhoneTest.php ├── Services │ ├── FileManagerTest.php │ ├── MediaManagerServiceTest.php │ └── files │ │ └── .gitignore ├── Template │ ├── CreateTemplateTest.php │ ├── SendTemplateTest.php │ ├── TemplateStatusWebhookTest.php │ └── TemplateTest.php └── Waba │ ├── WabaManagerTest.php │ ├── WabaTest.php │ └── WebhookSubscribeTest.php └── TestCase.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | pull_request: 7 | branches: [ "develop" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: '8.2' 24 | 25 | - name: Validate composer.json and composer.lock 26 | run: composer validate --strict 27 | 28 | - name: Cache Composer packages 29 | id: composer-cache 30 | uses: actions/cache@v4 31 | with: 32 | path: vendor 33 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-php- 36 | 37 | - name: Install dependencies 38 | run: composer install --prefer-dist --no-progress 39 | 40 | - name: Check code style 41 | run: ./vendor/bin/pint --test 42 | 43 | - name: Run phpunit Tests 44 | run: | 45 | vendor/bin/phpunit --coverage-clover build/logs/clover.xml 46 | 47 | - name: Upload coverage results to Coveralls 48 | env: 49 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | run: | 51 | vendor/bin/coveralls.php --coverage_clover=build/logs/clover.xml -v -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | html 3 | .phpunit.result.cache 4 | .phpunit.cache 5 | tests/files 6 | tests/logs -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SDK Consultoría - Whatsapp Cloud API 2 | ==== 3 | 4 | Descripción 5 | ------------ 6 | ##### ¿Qué puedo esperar de esta librería? 7 | - Conexión WhatsApp cloud API 8 | - Conexión WhatsApp bussines manager 9 | - Chat en tiempo real con WhatsApp 10 | - Envíos y recepción de mensajes de video, imagen, texto, pdf, stickers y reacciones. 11 | - Envíos masivos de whatsapp 12 | - Envíos de plantillas 13 | 14 | Este paquete está en desarrollo, todavía no es una versión estable ni optimizada. 15 | 16 | Video Demo 17 | - https://youtu.be/Tw5X-AVTMa0 18 | 19 | Video envíos masivos de whatsapp 20 | - https://youtu.be/x2nPfEt6HYw 21 | 22 | Video envíos y recepción de mensajes de video, imagen, texto, pdf, stickers y reacciones. 23 | - https://youtu.be/BZUQSeo7yz0 24 | 25 | Tutorial de instalación 26 | - https://youtu.be/EUyvuYIFRz8 27 | 28 | ##### Coverage 29 | 30 | https://coveralls.io/github/sdkconsultoria/whatsapp-cloud-api 31 | 32 | [![Coverage Status](https://coveralls.io/repos/github/sdkconsultoria/whatsapp-cloud-api/badge.svg?branch=develop)](https://coveralls.io/github/sdkconsultoria/whatsapp-cloud-api?branch=develop) 33 | 34 | Instalación 35 | ------------ 36 | Ejecuta el comando para instalar la librería en tu proyecto Laravel 37 | 38 | ``` 39 | composer require sdkconsultoria/whatsapp-cloud-api 40 | ``` 41 | 42 | Ejecutar las migraciones, para generar las tablas donde se guardaran los chats 43 | ``` 44 | php artisan migrate 45 | ``` 46 | 47 | Linkear el storage público para poder obtener la URL de los archivos recibidos y enviados 48 | 49 | ``` 50 | php artisan storage:link 51 | ``` 52 | 53 | Si quieres usar el Messenger en VUE (opcional), también puedes usar los endpoints 54 | ``` 55 | php artisan sdk:whatsapp-messenger-install 56 | ``` 57 | 58 | Configuración con Laravel sail y soketi 59 | ------------ 60 | 61 | socketi es una opción Open-source para notificaciones push totalmente compatible con pusher y Laravel echo https://docs.soketi.app/ 62 | 63 | la configuración por defecto para soketi es esta: 64 | ``` 65 | PUSHER_APP_ID=app-id 66 | PUSHER_APP_KEY=app-key 67 | PUSHER_APP_SECRET=app-secret 68 | PUSHER_HOST=soketi 69 | PUSHER_PORT=6001 70 | PUSHER_SCHEME=http 71 | PUSHER_APP_CLUSTER=mt1 72 | 73 | VITE_APP_NAME="${APP_NAME}" 74 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 75 | VITE_PUSHER_HOST=localhost 76 | VITE_PUSHER_PORT="${PUSHER_PORT}" 77 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 78 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 79 | 80 | META_WEBHOOK_TOKEN= 81 | META_TOKEN= 82 | META_APP_ID= 83 | ``` 84 | 85 | Configuración de Whatsapp Business Account WABA 86 | ------------ 87 | 88 | Puedes obtener la información desde 89 | 90 | https://developers.facebook.com/apps/ 91 | 92 | Si no tienes una APP debes crear una en la página de facebook developers. 93 | https://developers.facebook.com/apps 94 | 95 | ###### Registrar el webhook en meta 96 | {{HOST}}/api/v1/whatsapp-webhook 97 | 98 | ###### Obtener waba, plantillas, números telefónicos y bussiness profile desde Meta 99 | {{HOST}}/api/v1/waba/{{wabaId}}/init -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sdkconsultoria/whatsapp-cloud-api", 3 | "type": "library", 4 | "description": "Comunicacion rapida y segura con la API de Whatsapp Cloud", 5 | "keywords": [ 6 | "laravel", 7 | "package", 8 | "whatsapp cloud api", 9 | "whatsapp" 10 | ], 11 | "license": "MIT", 12 | "authors": [{ 13 | "name": "Camilo Antonio Rodríguez Cruz", 14 | "email": "camilo@sdkconsultoria.com", 15 | "homepage": "https://sdkconsultoria.com", 16 | "role": "Developer" 17 | }], 18 | "require": { 19 | "php": "^8.0", 20 | "guzzlehttp/guzzle": "^7.8", 21 | "pusher/pusher-php-server": "^7.2", 22 | "sdkconsultoria/open-ai-api": "dev-develop" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^11.0.6", 26 | "orchestra/testbench": "^9.0", 27 | "fakerphp/faker": "^1.23", 28 | "laravel/pint": "^1.14", 29 | "php-coveralls/php-coveralls": "^0.1.0" 30 | }, 31 | "autoload": { 32 | "files": [], 33 | "psr-4": { 34 | "Sdkconsultoria\\WhatsappCloudApi\\": "src/", 35 | "Sdkconsultoria\\WhatsappCloudApi\\Tests\\": "tests/" 36 | } 37 | }, 38 | "scripts": { 39 | "format": "vendor/bin/pint --config pint.json", 40 | "test": "vendor/bin/phpunit", 41 | "coverage": "vendor/bin/phpunit --coverage-html html" 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": ["Sdkconsultoria\\WhatsappCloudApi\\ServiceProvider"], 46 | "aliases": { 47 | "WhatsappCloudApi": "Sdkconsultoria\\WhatsappCloudApi\\Facade" 48 | } 49 | } 50 | }, 51 | "minimum-stability": "dev", 52 | "prefer-stable": true 53 | } 54 | -------------------------------------------------------------------------------- /config/cloudapi.php: -------------------------------------------------------------------------------- 1 | 2 | env('META_TOKEN'), 12 | 'webhook_token' => env('META_WEBHOOK_TOKEN'), 13 | 'webhook_redirect' => env('META_WEBHOOK_REDIRECT'), 14 | 'api_version' => env('WHATSAPP_API_VERSION', 'v19.0'), 15 | 'app_id' => env('META_APP_ID'), 16 | 'app_secret' => env('META_APP_SECRET'), 17 | 'webhook_verify_signature' => env('META_WEBHOOK_VERIFY_SIGNATURE', false), 18 | ]; 19 | -------------------------------------------------------------------------------- /database/migrations/2024_02_10_174929_create_wabas_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->timestamps(); 19 | $table->softDeletes(); 20 | $table->foreignId('created_by')->nullable()->constrained('users'); 21 | $table->foreignId('updated_by')->nullable()->constrained('users'); 22 | $table->string('waba_id'); 23 | $table->string('name'); 24 | $table->string('timezone_id'); 25 | $table->string('message_template_namespace'); 26 | $table->string('currency'); 27 | $table->smallInteger('status')->default('20'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('wabas'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /database/migrations/2024_02_10_174934_create_waba_phones_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->timestamps(); 19 | $table->softDeletes(); 20 | $table->foreignId('created_by')->nullable()->constrained('users'); 21 | $table->foreignId('updated_by')->nullable()->constrained('users'); 22 | $table->foreignId('waba_id'); 23 | 24 | $table->string('address')->nullable(); 25 | $table->text('description')->nullable(); 26 | $table->string('vertical')->nullable(); 27 | $table->text('about')->nullable(); 28 | $table->string('email')->nullable(); 29 | $table->string('websites')->nullable(); 30 | $table->string('profile_picture_url')->nullable(); 31 | $table->string('messaging_product')->nullable(); 32 | 33 | $table->string('name'); 34 | $table->string('code_verification_status')->nullable(); 35 | $table->string('display_phone_number'); 36 | $table->string('phone_number_clean'); 37 | $table->string('quality_rating'); 38 | $table->string('phone_id'); 39 | 40 | $table->string('pin')->nullable(); 41 | $table->smallInteger('status')->default('20'); 42 | }); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | * 48 | * @return void 49 | */ 50 | public function down() 51 | { 52 | Schema::dropIfExists('waba_phones'); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /database/migrations/2024_02_10_220808_create_templates_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->timestamps(); 19 | $table->softDeletes(); 20 | $table->foreignId('created_by')->nullable()->constrained('users'); 21 | $table->foreignId('updated_by')->nullable()->constrained('users'); 22 | $table->foreignId('waba_id'); 23 | $table->string('name'); 24 | $table->string('status'); 25 | $table->string('category'); 26 | $table->string('language'); 27 | $table->string('type')->default('GENERIC'); 28 | $table->string('template_id'); 29 | // $table->smallInteger('status')->default('20'); 30 | $table->json('content'); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('templates'); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /database/migrations/2024_02_11_220104_create_chat_categories_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->timestamps(); 19 | $table->softDeletes(); 20 | $table->foreignId('created_by')->nullable()->constrained('users'); 21 | $table->foreignId('updated_by')->nullable()->constrained('users'); 22 | $table->string('name'); 23 | $table->smallInteger('status')->default('20'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | * @return void 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists('chat_categories'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2024_02_11_220404_create_chats_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->timestamps(); 19 | $table->softDeletes(); 20 | $table->timestamp('last_message')->nullable(); 21 | $table->foreignId('created_by')->nullable()->constrained('users'); 22 | $table->foreignId('updated_by')->nullable()->constrained('users'); 23 | $table->foreignId('category_id')->nullable(); 24 | $table->smallInteger('unread_messages')->nullable()->default('0'); 25 | $table->string('order')->nullable(); 26 | $table->string('color')->nullable(); 27 | $table->foreignId('waba_phone_id'); 28 | $table->string('waba_phone'); 29 | $table->string('client_phone'); 30 | $table->smallInteger('status')->default('20'); 31 | $table->string('expiration_timestamp')->nullable(); 32 | $table->string('origin')->nullable(); 33 | $table->boolean('bot')->default(false); 34 | }); 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | * 40 | * @return void 41 | */ 42 | public function down() 43 | { 44 | Schema::dropIfExists('chats'); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /database/migrations/2024_02_11_220525_create_messages_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->foreignId('created_by')->nullable()->constrained('users'); 19 | $table->foreignId('chat_id'); 20 | $table->string('timestamp')->nullable(); 21 | $table->string('sent_at')->nullable(); 22 | $table->string('delivered_at')->nullable(); 23 | $table->string('read_at')->nullable(); 24 | $table->string('message_id')->nullable(); 25 | $table->string('type'); 26 | $table->string('direction'); 27 | $table->string('sended_by')->nullable(); 28 | $table->json('body')->nullable(); 29 | $table->smallInteger('status')->default('20'); 30 | $table->foreignId('response_to')->nullable()->constrained('messages'); 31 | $table->string('reaction')->nullable(); 32 | }); 33 | } 34 | 35 | /** 36 | * Reverse the migrations. 37 | * 38 | * @return void 39 | */ 40 | public function down() 41 | { 42 | Schema::dropIfExists('messages'); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /database/migrations/2024_03_06_141423_create_campaigns_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->timestamps(); 17 | $table->smallInteger('status')->default('20'); 18 | $table->foreignId('template_id'); 19 | $table->foreignId('waba_phone_id'); 20 | $table->string('name'); 21 | $table->integer('total_messages'); 22 | $table->integer('total_sent')->default(0); 23 | $table->integer('total_delivered')->default(0); 24 | $table->integer('total_read')->default(0); 25 | $table->integer('total_error')->default(0); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists('campaigns'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | php: 3 | build: 4 | context: ./ 5 | dockerfile: Dockerfile 6 | image: hellsythe/php-composer 7 | volumes: 8 | - '.:/app' 9 | networks: 10 | - services 11 | networks: 12 | services: 13 | driver: bridge 14 | -------------------------------------------------------------------------------- /dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2-cli 2 | 3 | RUN apt-get update && apt-get install -y \ 4 | libfreetype-dev \ 5 | libjpeg62-turbo-dev \ 6 | libpng-dev \ 7 | && docker-php-ext-configure gd --with-freetype --with-jpeg \ 8 | && docker-php-ext-install -j$(nproc) gd 9 | 10 | RUN curl -sS https://getcomposer.org/installer -o composer-setup.php && \ 11 | php composer-setup.php --install-dir=/usr/local/bin --filename=composer && \ 12 | composer clear-cache 13 | 14 | Run apt install zip -y 15 | 16 | WORKDIR /app 17 | 18 | ENTRYPOINT ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./tests/ 18 | ./tests/TestCase.php 19 | ./tests/Fake/ 20 | 21 | 22 | 23 | 24 | src 25 | 26 | 27 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | prefix('api/v1') 18 | ->group(function () { 19 | Route::get('whatsapp-webhook', 'WebhookController@subscribe')->name('meta.webhook.subscribe'); 20 | Route::post('whatsapp-webhook', 'WebhookController@webhook')->name('meta.webhook'); 21 | }); 22 | 23 | Route::namespace('Sdkconsultoria\WhatsappCloudApi\Http\Controllers') 24 | ->prefix('api/v1') 25 | ->group(WhatsappCloudApi::routes()); 26 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | confirm('Esto sobrescribira el messenger del paquete usando el messenger del proyecto principal, estas seguro de continuar?')) { 42 | $this->copyMessenger(); 43 | $this->comment('SDK Whatsapp Messenger se copio correctamente.'); 44 | } 45 | } 46 | 47 | /** 48 | * Cuando estamos desarrollando nueva funcionalidad en el messenger, necesitamos copiar el messenger del proyecto principal al paquete. 49 | */ 50 | private function copyMessenger(): void 51 | { 52 | $this->info('Copiando messenger a los stubs del packate...'); 53 | $proyectPath = base_path('resources/js/components/Messenger'); 54 | $packagePath = __DIR__.'/../../../stubs/interface/resources/js/components/Messenger'; 55 | resolve(Filesystem::class)->deleteDirectory($packagePath); 56 | resolve(Filesystem::class)->copyDirectory($proyectPath, $packagePath); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Events/NewWhatsappMessageHook.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function broadcastOn(): array 31 | { 32 | return [ 33 | new Channel('new_whatsapp_message'), 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Facade.php: -------------------------------------------------------------------------------- 1 | WabaPhone::factory(), 28 | 'waba_phone' => $this->faker->numberBetween(1111111), 29 | 'client_phone' => $this->faker->numberBetween(1111111), 30 | 31 | 'status' => 20, 32 | 'template_id' => Template::factory(), 33 | 'name' => $this->faker->word, 34 | 'total_messages' => $this->faker->numberBetween(1111111), 35 | 'total_sent' => 0, 36 | 'total_delivered' => 0, 37 | 'total_read' => 0, 38 | 'total_error' => 0, 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Factories/ChatFactory.php: -------------------------------------------------------------------------------- 1 | WabaPhone::factory(), 27 | 'waba_phone' => $this->faker->numberBetween(1111111), 28 | 'client_phone' => $this->faker->numberBetween(1111111), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Factories/MessageFactory.php: -------------------------------------------------------------------------------- 1 | Chat::factory(), 27 | 'timestamp' => time(), 28 | 'sent_at' => null, 29 | 'delivered_at' => null, 30 | 'read_at' => null, 31 | 'message_id' => 'wamid.'.$this->faker->numberBetween(1111111), 32 | 'type' => 'text', 33 | 'direction' => 'toApp', 34 | 'body' => '{"text": {"body": "Todo bien", "preview_url": false}, "type": "text"}', 35 | 'status' => 20, 36 | 'response_to' => null, 37 | 'reaction' => null, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Factories/TemplateFactory.php: -------------------------------------------------------------------------------- 1 | [ 27 | 'text' => 'Welcome', 28 | ], 29 | ]; 30 | 31 | return [ 32 | 'waba_id' => Waba::factory(), 33 | 'name' => $this->faker->word, 34 | 'status' => 'APPROVED', 35 | 'category' => 'UTILITY', 36 | 'language' => 'es_MX', 37 | 'template_id' => $this->faker->numberBetween(111111111), 38 | 'content' => json_encode(['components' => $content]), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Factories/WabaFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word, 26 | 'timezone_id' => $this->faker->word, 27 | 'currency' => $this->faker->word, 28 | 'message_template_namespace' => $this->faker->word, 29 | 'waba_id' => $this->faker->numberBetween(111111111), 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Factories/WabaPhoneFactory.php: -------------------------------------------------------------------------------- 1 | Waba::factory(), 27 | 'address' => $this->faker->word, 28 | 'description' => $this->faker->word, 29 | 'vertical' => $this->faker->word, 30 | 'about' => $this->faker->word, 31 | 'email' => $this->faker->word, 32 | 'websites' => $this->faker->word, 33 | 'profile_picture_url' => $this->faker->word, 34 | 'name' => $this->faker->word, 35 | 'code_verification_status' => $this->faker->word, 36 | 'display_phone_number' => $this->faker->word, 37 | 'phone_number_clean' => $this->faker->word, 38 | 'quality_rating' => $this->faker->word, 39 | 'phone_id' => $this->faker->word, 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Controllers/APIResourceController.php: -------------------------------------------------------------------------------- 1 | filters() as $index => $filter) { 31 | $search_value = $request->input($index); 32 | if ($search_value) { 33 | $query = $filter($query, $search_value); 34 | } 35 | } 36 | 37 | return $query; 38 | } 39 | 40 | public function index(Request $request) 41 | { 42 | $models = new $this->resource; 43 | $models = $this->applyFilters($models, $request); 44 | $models = $this->defaultOptions($models, $request); 45 | $models = $models->simplePaginate($this->perPage)->appends(request()->except('page')); 46 | 47 | if ($this->transformer) { 48 | $transformer = $this->transformer; 49 | 50 | return $transformer::collection($this->reverseElements($models)); 51 | } 52 | 53 | return response()->json($models); 54 | } 55 | 56 | protected function reverseElements($models) 57 | { 58 | if ($this->isReverseElements) { 59 | return $models->reverse(); 60 | } 61 | 62 | return $models; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Http/Controllers/CampaignController.php: -------------------------------------------------------------------------------- 1 | file->getRealPath()); 16 | $phones = explode(',', $file); 17 | 18 | $campaign = new Campaign(); 19 | $campaign->name = $request->name; 20 | $campaign->template_id = $request->template_id; 21 | $campaign->waba_phone_id = $request->waba_phone_id; 22 | $campaign->total_messages = count($phones); 23 | $campaign->save(); 24 | 25 | foreach ($phones as $phone) { 26 | resolve(SendTemplate::class)->send($campaign->wabaPhone, $campaign->template, $phone, $request->vars ?? []); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Http/Controllers/ChatController.php: -------------------------------------------------------------------------------- 1 | orderBy('last_message', 'desc'); 15 | 16 | return $models; 17 | } 18 | 19 | protected function filters(): array 20 | { 21 | return [ 22 | 'client_phone' => function ($query, $value) { 23 | return $query->where('client_phone', 'like', "%$value%"); 24 | }, 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Http/Controllers/MessageController.php: -------------------------------------------------------------------------------- 1 | where('type', '!=', 'reaction'); 22 | $models->orderBy('timestamp', 'desc'); 23 | 24 | return $models; 25 | } 26 | 27 | protected function filters(): array 28 | { 29 | return [ 30 | 'chat_id' => function ($query, $value) { 31 | return $query->where('chat_id', "$value"); 32 | }, 33 | ]; 34 | } 35 | 36 | public function index(Request $request) 37 | { 38 | $models = new $this->resource; 39 | $models = $this->applyFilters($models, $request); 40 | $models = $this->defaultOptions($models, $request); 41 | $models = $models->simplePaginate()->appends(request()->except('page')); 42 | 43 | $chat = Chat::where('id', $request->input('chat_id'))->first(); 44 | $chat->unread_messages = 0; 45 | $chat->save(); 46 | 47 | return $this->transformer::collection($this->reverseElements($models)); 48 | } 49 | 50 | public function sendMessage(Request $request) 51 | { 52 | $message = resolve(SendMessage::class)->send($request->all()); 53 | 54 | return response()->json($message); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Http/Controllers/TemplateController.php: -------------------------------------------------------------------------------- 1 | function ($query, $value) { 23 | return $query->where('status', "$value"); 24 | }, 25 | 'name' => function ($query, $value) { 26 | return $query->where('name', 'like', "%$value%"); 27 | }, 28 | ]; 29 | } 30 | 31 | public function store(StoreTemplateRequest $request) 32 | { 33 | $template = resolve(CreateTemplate::class)->create($request); 34 | 35 | return response()->json([ 36 | 'message' => 'Template created successfully', 37 | 'template' => new TemplateResource($template), 38 | ], 201); 39 | } 40 | 41 | public function sendTemplate(SendTemplateRequest $request) 42 | { 43 | $template = Template::find($request->template); 44 | $wabaPhone = WabaPhone::find($request->waba_phone); 45 | 46 | try { 47 | $message = resolve(SendTemplate::class)->send($wabaPhone, $template, $request->to, $request->vars ?? []); 48 | } catch (\Exception $e) { 49 | return response()->json($e->getMessage(), 500); 50 | } 51 | 52 | return response()->json($message); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Http/Controllers/WabaController.php: -------------------------------------------------------------------------------- 1 | getWabaInfo($wabaId); 16 | resolve(WabaManagerService::class)->getPhoneNumbers($wabaId); 17 | $templates = resolve(WabaManagerService::class)->getAllTemplates($wabaId); 18 | $this->saveTemplates($templates, $wabaId); 19 | 20 | return response()->json($templates); 21 | } 22 | 23 | public function loadTemplatesFromWaba(string $wabaId) 24 | { 25 | $templates = resolve(WabaManagerService::class)->getAllTemplates($wabaId); 26 | $this->saveTemplates($templates, $wabaId); 27 | 28 | return response()->json($templates); 29 | } 30 | 31 | private function saveTemplates($templates, $wabaId) 32 | { 33 | foreach ($templates['data'] as $template) { 34 | resolve(MetaToApp::class)->process($template, $wabaId); 35 | } 36 | } 37 | 38 | public function getWabaInfoFromMeta(string $wabaId) 39 | { 40 | return resolve(WabaManagerService::class)->getWabaInfo($wabaId); 41 | } 42 | 43 | public function getWabaPhonesFromMeta(string $wabaId) 44 | { 45 | return resolve(WabaManagerService::class)->getPhoneNumbers($wabaId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Http/Controllers/WabaPhoneController.php: -------------------------------------------------------------------------------- 1 | function ($query, $value) { 17 | return $query->where('waba_id', "$value"); 18 | }, 19 | ]; 20 | } 21 | 22 | public function getBussinesProfile(string $phoneId) 23 | { 24 | return resolve(BussinessProfile::class)->process($phoneId); 25 | } 26 | 27 | public function setBussinesProfile(string $phoneId, Request $request) 28 | { 29 | return resolve(BussinessProfile::class)->update($phoneId, $request->all()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Http/Controllers/WebhookController.php: -------------------------------------------------------------------------------- 1 | hub_verify_token === config('meta.webhook_token')) { 16 | return $request->hub_challenge; 17 | } 18 | 19 | return response()->json(['error' => 'Invalid verify token'], 400); 20 | } 21 | 22 | public function webhook(Request $request) 23 | { 24 | $this->verifySignature($request); 25 | $this->redirectRequestIfNeeded($request); 26 | $data = $request->all()['entry'][0]['changes'][0]; 27 | 28 | switch ($data['field']) { 29 | case 'messages': 30 | resolve(ProcessMessageWebhook::class)->process($data['value']); 31 | break; 32 | case 'message_template_status_update': 33 | resolve(ProcessStatusWebhook::class)->process($data['value']); 34 | break; 35 | } 36 | } 37 | 38 | private function redirectRequestIfNeeded(Request $request) 39 | { 40 | if (! config('meta.webhook_redirect')) { 41 | return; 42 | } 43 | 44 | Http::post(config('meta.webhook_redirect'), $request->all()); 45 | } 46 | 47 | private function verifySignature(Request $request) 48 | { 49 | if (! config('meta.webhook_verify_signature')) { 50 | return; 51 | } 52 | 53 | $secret = config('meta.app_secret'); 54 | $signature = $request->header('x-hub-signature') ?? ''; 55 | $payload = $request->getContent(); 56 | $expected = 'sha1='.hash_hmac('sha1', $payload, $secret); 57 | 58 | if (! hash_equals($expected, $signature)) { 59 | abort(403, 'Invalid signature'); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Http/Requests/SendTemplateRequest.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function rules(): array 24 | { 25 | $this->getValidationsComponents(); 26 | 27 | return array_merge([ 28 | 'waba_phone' => 'required', 29 | 'to' => 'required', 30 | 'template' => 'required|integer|exists:Sdkconsultoria\WhatsappCloudApi\Models\Template,id', 31 | 'vars' => 'nullable|array', 32 | ], $this->getValidationsComponents()); 33 | } 34 | 35 | private function getValidationsComponents(): array|false 36 | { 37 | $validations = []; 38 | $template = Template::find($this->template); 39 | 40 | if (! $template) { 41 | return []; 42 | } 43 | 44 | foreach ($template->getComponents() as $index => $component) { 45 | switch (strtoupper($index)) { 46 | case 'BODY': 47 | $this->getComponentVarsValidations($component, $validations, 'body'); 48 | break; 49 | case 'HEADER': 50 | $this->getHeaderValidations($component, $validations); 51 | break; 52 | 53 | default: 54 | break; 55 | } 56 | } 57 | 58 | return $validations; 59 | } 60 | 61 | private function getComponentVarsValidations(array $component, array &$validations, $type): void 62 | { 63 | $uniques = $this->countUniqueVars($component['text']); 64 | 65 | if ($uniques === 0) { 66 | $validations["vars.$type.parameters"] = 'nullable'; 67 | 68 | return; 69 | } 70 | 71 | $validations["vars.$type.parameters"] = 'required|array|size:'.$uniques; 72 | 73 | for ($i = 0; $i < $uniques; $i++) { 74 | $validations["vars.$type.parameters.$i.text"] = 'required|string'; 75 | $validations["vars.$type.parameters.$i.type"] = 'required|string'; 76 | } 77 | } 78 | 79 | private function countUniqueVars(string $text): int 80 | { 81 | preg_match_all('/{{([0-9]{1,2})}}/', $text, $matches); 82 | $uniques = array_unique(array_map('intval', $matches[1])); 83 | 84 | return count($uniques); 85 | } 86 | 87 | private function getHeaderValidations(array $component, array &$validations): void 88 | { 89 | switch ($component['format']) { 90 | case 'text': 91 | $this->getComponentVarsValidations($component, $validations, 'header'); 92 | break; 93 | case 'document': 94 | case 'video': 95 | case 'image': 96 | $validations['vars.header.parameters.0.type'] = 'required|string'; 97 | $validations['vars.header.parameters.0.'.$component['format'].'.link'] = 'required|string'; 98 | break; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Http/Requests/StoreCampaignRequest.php: -------------------------------------------------------------------------------- 1 | |string> 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'name' => 'required|string|max:255', 26 | 'template_id' => 'required|integer|exists:Sdkconsultoria\WhatsappCloudApi\Models\Template,id', 27 | 'waba_phone_id' => 'required|integer|exists:Sdkconsultoria\WhatsappCloudApi\Models\WabaPhone,id', 28 | 'file' => 'required|extensions:csv', 29 | 'vars' => 'nullable|array', 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Http/Requests/StoreTemplateRequest.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function rules(): array 23 | { 24 | $rules = []; 25 | $rules = array_merge($rules, $this->basicRules()); 26 | 27 | return $rules; 28 | } 29 | 30 | private function basicRules(): array 31 | { 32 | return [ 33 | 'waba_id' => 'required', 34 | 'name' => 'required|string|alpha_dash:ascii', 35 | 'category' => ['required', 'string'], 36 | 'language' => 'required|string', 37 | // 'allow_category_change' => 'required|boolean', 38 | 'components' => 'required|array', 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Http/Resources/MessageResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 20 | 'chat_id' => $this->chat_id, 21 | 'type' => $this->type, 22 | 'direction' => $this->direction, 23 | 'content' => $this->getContent(), 24 | 'timestamp' => $this->timestamp, 25 | 'sended_by' => $this->sended_by, 26 | 'reaction' => $this->reaction, 27 | 'sent_at' => $this->sent_at, 28 | 'read_at' => $this->read_at, 29 | 'delivered_at' => $this->delivered_at, 30 | 'response_to' => $this->getResponseTo(), 31 | ]; 32 | } 33 | 34 | private function getContent() 35 | { 36 | $body = json_decode($this->body); 37 | 38 | switch ($this->type) { 39 | case 'image': 40 | case 'video': 41 | case 'audio': 42 | case 'sticker': 43 | case 'document': 44 | return [ 45 | 'url' => Url::to($body->{$this->type}->url ?? ''), 46 | 'caption' => $body->{$this->type}->caption ?? '', 47 | ]; 48 | case 'contacts': 49 | return $body->contacts; 50 | case 'text': 51 | return $body->text->body; 52 | case 'template': 53 | return $body; 54 | } 55 | } 56 | 57 | private function getResponseTo() 58 | { 59 | $body = json_decode($this->body); 60 | 61 | if ($body->context ?? false) { 62 | return $body->context; 63 | } 64 | 65 | return null; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Http/Resources/TemplateResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 19 | 'name' => $this->name, 20 | 'status' => $this->status, 21 | 'category' => $this->category, 22 | 'language' => $this->language, 23 | 'components' => json_decode($this->content)->components, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Jobs/SendToOpenApi.php: -------------------------------------------------------------------------------- 1 | chat->bot) { 34 | $assistant = Assistant::first(); 35 | $thread = Thread::where('identifier', $this->content['from'])->first(); 36 | if (! $thread) { 37 | resolve(ThreadManager::class)->createConversationWithAssistant($this->content['from'], $this->content['text']['body'], $assistant); 38 | } else { 39 | resolve(ThreadManager::class)->addMessage($thread, $this->content['text']['body']); 40 | resolve(ThreadManager::class)->addRunToThread($thread); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Lib/Message/BussinessProfile.php: -------------------------------------------------------------------------------- 1 | getBussinesProfile($phoneId); 14 | 15 | return $this->saveWabaPhone($phoneId, $bussinesProfile['data'][0]); 16 | } 17 | 18 | private function saveWabaPhone(string $phoneId, array $profile): WabaPhone 19 | { 20 | $wabaPhone = WabaPhone::where('phone_id', $phoneId)->first(); 21 | $wabaPhone->about = $profile['about']; 22 | $wabaPhone->address = $profile['address']; 23 | $wabaPhone->description = $profile['description']; 24 | $wabaPhone->email = $profile['email']; 25 | $wabaPhone->profile_picture_url = $profile['profile_picture_url']; 26 | $wabaPhone->websites = json_encode($profile['websites']); 27 | $wabaPhone->vertical = $profile['vertical']; 28 | $wabaPhone->messaging_product = $profile['messaging_product']; 29 | $wabaPhone->save(); 30 | 31 | return $wabaPhone; 32 | } 33 | 34 | public function update(string $phoneId, array $profile) 35 | { 36 | if (isset($profile['picture_profile'])) { 37 | $filePath = $profile['picture_profile']->getRealPath(); 38 | $handler = resolve(ResumableUploadAPI::class)->uploadFile($filePath); 39 | unset($profile['picture_profile']); 40 | $profile['profile_picture_handle'] = $handler->handler; 41 | } 42 | 43 | resolve(WabaManagerService::class)->setBussinesProfile($phoneId, $profile); 44 | 45 | return $this->process($phoneId); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Lib/Message/ProcessMessageWebhook.php: -------------------------------------------------------------------------------- 1 | process($messageEvent); 12 | } 13 | 14 | if (isset($messageEvent['statuses'])) { 15 | resolve(ReceivedMessageStatus::class)->process($messageEvent); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Lib/Message/ReceivedMessage.php: -------------------------------------------------------------------------------- 1 | first(); 18 | 19 | $content = $messageEvent['messages'][0]; 20 | $chat = Chat::findOrCreateChat($content['from'], $wabaPhoneNumber); 21 | 22 | switch ($content['type']) { 23 | case 'unsupported': 24 | case 'contacts': 25 | case 'text': 26 | $this->processIfIsResponse($content); 27 | $this->processTextMessage($chat, $content); 28 | SendToOpenApi::dispatch($chat, $content); 29 | break; 30 | case 'document': 31 | case 'sticker': 32 | case 'video': 33 | case 'audio': 34 | case 'image': 35 | $this->processIfIsResponse($content); 36 | $content[$content['type']]['url'] = $this->saveFile($content[$content['type']], $phoneNumberId, $chat); 37 | $this->processTextMessage($chat, $content); 38 | break; 39 | case 'reaction': 40 | Message::where('message_id', $content['reaction']['message_id']) 41 | ->update(['reaction' => $content['reaction']['emoji']]); 42 | break; 43 | } 44 | 45 | NewWhatsappMessageHook::dispatch(['chat_id' => $chat->id]); 46 | } 47 | 48 | private function processIfIsResponse(array &$content): void 49 | { 50 | if (isset($content['context']) && isset($content['context']['id'])) { 51 | $message = Message::where('message_id', $content['context']['id'])->firstOrFail(); 52 | $content['context']['message'] = $message->body; 53 | } 54 | } 55 | 56 | private function processTextMessage(Chat $chat, array $content): void 57 | { 58 | $messageModel = new Message(); 59 | $messageModel->chat_id = $chat->id; 60 | $messageModel->message_id = $content['id']; 61 | $messageModel->timestamp = $content['timestamp']; 62 | $messageModel->status = Message::STATUS_DELIVERED; 63 | $messageModel->type = $content['type']; 64 | $messageModel->body = json_encode($content); 65 | $messageModel->direction = 'toApp'; 66 | $messageModel->save(); 67 | } 68 | 69 | private function saveFile(array $file, string $phoneNumberId, Chat $chat): string 70 | { 71 | $service = resolve(MediaManagerService::class); 72 | 73 | return $service->download($file['id'], $phoneNumberId, "received/$chat->id/{$file['id']}", 'public'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Lib/Message/ReceivedMessageStatus.php: -------------------------------------------------------------------------------- 1 | processSent($messageEvent); 14 | break; 15 | case 'delivered': 16 | $this->processDelivered($messageEvent); 17 | break; 18 | case 'read': 19 | $this->processRead($messageEvent); 20 | break; 21 | } 22 | } 23 | 24 | private function processSent($messageEvent): void 25 | { 26 | $message = Message::where('message_id', $messageEvent['statuses'][0]['id'])->firstOrFail(); 27 | $message->sent_at = $messageEvent['statuses'][0]['timestamp']; 28 | $message->save(); 29 | 30 | $chat = $message->chat; 31 | $chat->expiration_timestamp = $messageEvent['statuses'][0]['conversation']['expiration_timestamp']; 32 | $chat->save(); 33 | } 34 | 35 | private function processDelivered($messageEvent): void 36 | { 37 | $message = Message::where('message_id', $messageEvent['statuses'][0]['id'])->firstOrFail(); 38 | $message->delivered_at = $messageEvent['statuses'][0]['timestamp']; 39 | $message->save(); 40 | } 41 | 42 | private function processRead($messageEvent): void 43 | { 44 | $message = Message::where('message_id', $messageEvent['statuses'][0]['id'])->firstOrFail(); 45 | $message->read_at = $messageEvent['statuses'][0]['timestamp']; 46 | $message->save(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Lib/Message/SendMessage.php: -------------------------------------------------------------------------------- 1 | createNewMessage($request, $sentBy); 19 | $processedMessage = $this->processMessage($messageModel, $request['message']); 20 | $messageResponse = resolve(MessageService::class) 21 | ->sendMessage($messageModel->chat->wabaPhone->phone_id, $request['to'], $processedMessage); 22 | $this->finishMessage($messageModel, $messageResponse, $processedMessage); 23 | } 24 | 25 | private function createNewMessage(array $request, $sentBy = null): Message 26 | { 27 | $phoneNumber = WabaPhone::find($request['waba_phone_id']); 28 | $message = new Message(); 29 | $message->status = Message::STATUS_PENDING; 30 | $message->direction = 'toClient'; 31 | $message->timestamp = time(); 32 | $message->type = $request['message']['type']; 33 | $message->chat_id = $this->getChatId($phoneNumber, $request['to'])->id; 34 | $message->sended_by = $sentBy; 35 | $message->save(); 36 | 37 | return $message; 38 | } 39 | 40 | private function processMessage(Message $message, array $request): array 41 | { 42 | $this->processedMessage = $request; 43 | 44 | switch ($request['type']) { 45 | case 'audio': 46 | case 'video': 47 | case 'document': 48 | case 'image': 49 | $url = "sended/{$message->chat->id}/$message->id.{$request[$request['type']]->extension()}"; 50 | $request[$request['type']]->storeAs('public', $url); 51 | $media = resolve(MediaManagerService::class)->upload($message->chat->wabaPhone->phone_id, $request[$request['type']]); 52 | $request[$request['type']] = ['id' => $media['id']]; 53 | 54 | $this->processedMessage[$request['type']] = ['url' => Storage::disk()->url($url)]; 55 | break; 56 | } 57 | 58 | return $request; 59 | } 60 | 61 | private function getChatId($phoneNumber, $to): Chat 62 | { 63 | $chat = Chat::firstOrCreate([ 64 | 'waba_phone' => $phoneNumber->phone_number_clean, 65 | 'waba_phone_id' => $phoneNumber->id, 66 | 'client_phone' => $to, 67 | ]); 68 | $chat->last_message = date('Y-m-d H:i:s'); 69 | $chat->save(); 70 | 71 | return $chat; 72 | } 73 | 74 | private function finishMessage(Message $messageModel, array $messageResponse): void 75 | { 76 | $messageModel->message_id = $messageResponse['messages'][0]['id']; 77 | $messageModel->body = json_encode($this->processedMessage); 78 | $messageModel->save(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Lib/Template/Adapter/AppToMeta.php: -------------------------------------------------------------------------------- 1 | fixComponents($metaTemplate['components']); 12 | $template = $this->saveTemplate($metaTemplate, $wabaId); 13 | 14 | return $template; 15 | } 16 | 17 | private function fixComponents(array $components): array 18 | { 19 | $fixedComponents = []; 20 | 21 | foreach ($components as $component) { 22 | $fixedComponents[$component['type']] = $component; 23 | unset($fixedComponents[$component['type']]['type']); 24 | 25 | $this->fixSubcomponents($component['type'], $fixedComponents[$component['type']]); 26 | } 27 | 28 | return $fixedComponents; 29 | } 30 | 31 | private function fixSubcomponents(string $type, array &$subcomponents): void 32 | { 33 | switch ($type) { 34 | case 'CAROUSEL': 35 | foreach ($subcomponents['cards'] as $key => $item) { 36 | $subcomponents['cards'][$key]['components'] = $this->fixComponents($item['components']); 37 | } 38 | break; 39 | } 40 | } 41 | 42 | private function saveTemplate(array $metaTemplate, ?string $wabaId = null): Template 43 | { 44 | $template = $this->findOrCreateTemplate($metaTemplate['id'], $wabaId); 45 | $template->status = $metaTemplate['status']; 46 | $template->category = $metaTemplate['category']; 47 | $template->name = $metaTemplate['name']; 48 | $template->language = $metaTemplate['language']; 49 | $template->content = json_encode($metaTemplate); 50 | $template->save(); 51 | 52 | return $template; 53 | } 54 | 55 | private function findOrCreateTemplate(string $templateId, ?string $wabaId = null): Template 56 | { 57 | $template = Template::where('template_id', $templateId)->first(); 58 | 59 | if (! $template) { 60 | $template = new Template(); 61 | $template->template_id = $templateId; 62 | $template->waba_id = $wabaId; 63 | } 64 | 65 | return $template; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Lib/Template/Adapter/RequestToMeta.php: -------------------------------------------------------------------------------- 1 | getRealPath(); 20 | $handler = resolve(ResumableUploadAPI::class)->uploadFile($filePath); 21 | $component['example']['header_handle'] = [$handler->handler]; 22 | } 23 | 24 | if ($component['type'] === 'CAROUSEL') { 25 | foreach ($component['cards'] as $cardIndex => $card) { 26 | $subComponents = []; 27 | foreach ($card['components'] as $componentIndex => $subcomponent) { 28 | $subcomponent['type'] = strtoupper($componentIndex); 29 | if ($subcomponent['type'] === 'HEADER'){ 30 | $filePath = $subcomponent['example']['header_handle'][0]->getRealPath(); 31 | $handler = resolve(ResumableUploadAPI::class)->uploadFile($filePath); 32 | $subcomponent['example']['header_handle'] = [$handler->handler]; 33 | } 34 | $subComponents[] = $subcomponent; 35 | } 36 | $component['cards'][$cardIndex]['components'] = $subComponents; 37 | } 38 | } 39 | 40 | return $component; 41 | }, $components, array_keys($components)); 42 | $processed['components'] = $components; 43 | 44 | return $processed; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Lib/Template/CreateTemplate.php: -------------------------------------------------------------------------------- 1 | waba_id); 16 | $templateInMetaFormat = resolve(RequestToMeta::class)->process($request->all()); 17 | $metaTemplateResponse = resolve(TemplateManagerService::class)->createTemplate($waba->waba_id, $templateInMetaFormat); 18 | $templateFromMeta = resolve(TemplateManagerService::class)->getTemplate($metaTemplateResponse['id']); 19 | 20 | return resolve(MetaToApp::class)->process($templateFromMeta, $waba->id); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Lib/Template/ProcessStatusWebhook.php: -------------------------------------------------------------------------------- 1 | firstOrFail(); 13 | $template->status = $messageEvent['event']; 14 | $template->save(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Lib/Template/SendTemplate.php: -------------------------------------------------------------------------------- 1 | setVars($vars); 16 | $message = resolve(MessageService::class) 17 | ->sendTemplate($wabaPhone, $to, $template); 18 | 19 | $messageModel = new Message(); 20 | $messageModel->direction = 'toClient'; 21 | $messageModel->body = json_encode($template->componentsWithVars); 22 | $messageModel->timestamp = time(); 23 | $messageModel->message_id = $message['messages'][0]['id']; 24 | $messageModel->type = 'template'; 25 | $messageModel->chat_id = $this->getChatId($wabaPhone, $to); 26 | $messageModel->sended_by = $sentBy; 27 | $messageModel->save(); 28 | } 29 | 30 | private function getChatId($wabaPhone, $to) 31 | { 32 | $chat = Chat::firstOrCreate([ 33 | 'waba_phone' => $wabaPhone->phone_number_clean, 34 | 'waba_phone_id' => $wabaPhone->id, 35 | 'client_phone' => $to, 36 | ]); 37 | 38 | return $chat->id; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Listeners/ResponseMessageReady.php: -------------------------------------------------------------------------------- 1 | run->thread->identifier; 24 | $message = $event->messages['data'][0]['content'][0]['text']['value']; 25 | 26 | resolve(SendMessage::class)->send([ 27 | 'waba_phone_id' => 1, 28 | 'to' => $identifier, 29 | 'message' => [ 30 | 'type' => 'text', 31 | 'text' => [ 32 | 'body' => $message, 33 | 'preview_url' => false, 34 | ], 35 | ], 36 | ], 'BOT'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Models/Campaign.php: -------------------------------------------------------------------------------- 1 | belongsTo(WabaPhone::class); 23 | } 24 | 25 | public function template() 26 | { 27 | return $this->belongsTo(Template::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Models/Chat.php: -------------------------------------------------------------------------------- 1 | $wabaPhoneNumber->id, 20 | 'waba_phone' => $wabaPhoneNumber->phone_number_clean, 21 | 'client_phone' => $from, 22 | ]); 23 | 24 | $chat->unread_messages += 1; 25 | $chat->last_message = date('Y-m-d H:i:s'); 26 | $chat->status = Chat::STATUS_UNREAD; 27 | $chat->save(); 28 | 29 | return $chat; 30 | } 31 | 32 | public function wabaPhone() 33 | { 34 | return $this->belongsTo(WabaPhone::class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Models/ChatCategory.php: -------------------------------------------------------------------------------- 1 | belongsTo(Chat::class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Models/Template.php: -------------------------------------------------------------------------------- 1 | content, true)['components']; 25 | } 26 | 27 | public function setVars(array $vars): void 28 | { 29 | $components = $this->getComponents(); 30 | $fixedVars = []; 31 | foreach ($vars as $key => $var) { 32 | $key = strtoupper($key); 33 | $fixedVars[] = array_merge($var, ['type' => $key]); 34 | $components[$key] = $components[$key] ?? []; 35 | 36 | switch ($key) { 37 | case 'BODY': 38 | $components[$key] = $this->replaceVars($components[$key], $var); 39 | break; 40 | case 'BUTTON': 41 | $components['BUTTON'] = $components['BUTTON'] ?? []; 42 | $components['BUTTON'][] = $var; 43 | break; 44 | 45 | default: 46 | $components[$key]['parameters'] = $var; 47 | break; 48 | } 49 | } 50 | 51 | $this->componentsWithVars = $components; 52 | $this->vars = $fixedVars; 53 | } 54 | 55 | private function replaceVars(array $component, array $var): array 56 | { 57 | $text = $component['text']; 58 | foreach ($var['parameters'] as $index => $value) { 59 | $text = str_replace('{{'.($index + 1).'}}', $value['text'], $text); 60 | } 61 | $component['text'] = $text; 62 | 63 | return $component; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Models/Waba.php: -------------------------------------------------------------------------------- 1 | first(); 15 | 16 | if (! $waba) { 17 | $waba = new Waba(); 18 | $waba->waba_id = $wabaInfo['id']; 19 | } 20 | 21 | $waba->waba_id = $wabaInfo['id']; 22 | $waba->name = $wabaInfo['name']; 23 | $waba->timezone_id = $wabaInfo['timezone_id']; 24 | $waba->message_template_namespace = $wabaInfo['message_template_namespace']; 25 | $waba->currency = $wabaInfo['currency'] ?? 'USD'; 26 | $waba->save(); 27 | } 28 | 29 | public function templates() 30 | { 31 | return $this->hasMany(Template::class, 'waba_id', 'waba_id'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Models/WabaPhone.php: -------------------------------------------------------------------------------- 1 | first(); 17 | $wabaPhone = WabaPhone::where('phone_id', $phone['id'])->first(); 18 | 19 | if (! $wabaPhone) { 20 | $wabaPhone = new WabaPhone(); 21 | $wabaPhone->phone_id = $phone['id']; 22 | $wabaPhone->waba_id = $waba->id; 23 | } 24 | 25 | $wabaPhone->name = $phone['verified_name']; 26 | $wabaPhone->display_phone_number = $phone['display_phone_number']; 27 | $wabaPhone->quality_rating = $phone['quality_rating']; 28 | $wabaPhone->phone_number_clean = str_replace(['-', ' ', '+'], '', $phone['display_phone_number']); 29 | $wabaPhone->save(); 30 | 31 | try { 32 | resolve(BussinessProfile::class)->process($phone['id']); 33 | } catch (\Exception $e) { 34 | \Log::error($e->getMessage()); 35 | } 36 | 37 | } 38 | } 39 | 40 | /** 41 | * @codeCoverageIgnore 42 | */ 43 | public function waba() 44 | { 45 | return $this->belongsTo(Waba::class); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__.'/../routes/api.php'); 21 | $this->registerBroadcasting(); 22 | $this->registerMigrations(); 23 | $this->registerCustomFactory(); 24 | $this->registerCommands(); 25 | $this->registerRouteMacro(); 26 | $this->registerEvents(); 27 | } 28 | 29 | private function registerBroadcasting(): void 30 | { 31 | Broadcast::routes(); 32 | 33 | require __DIR__.'/../routes/channels.php'; 34 | } 35 | 36 | private function registerMigrations() 37 | { 38 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 39 | } 40 | 41 | /** 42 | * @codeCoverageIgnore 43 | */ 44 | private function registerCustomFactory(): void 45 | { 46 | Factory::guessFactoryNamesUsing(function (string $model_name) { 47 | $sdk = Str::startsWith($model_name, 'Sdkconsultoria'); 48 | 49 | if ($sdk) { 50 | return Str::of($model_name)->replace('Models', 'Factories').'Factory'; 51 | } 52 | 53 | $namespace = 'Database\\Factories\\'; 54 | 55 | $model_name = str_replace('App\\Models\\', '', $model_name); 56 | 57 | return $namespace.$model_name.'Factory'; 58 | }); 59 | } 60 | 61 | private function registerCommands(): void 62 | { 63 | if ($this->app->runningInConsole()) { 64 | $this->commands([ 65 | \Sdkconsultoria\WhatsappCloudApi\Console\Commands\InstallCommand::class, 66 | \Sdkconsultoria\WhatsappCloudApi\Console\Commands\CopyMessengerCommand::class, 67 | ]); 68 | } 69 | } 70 | 71 | /** 72 | * @codeCoverageIgnore 73 | */ 74 | private function registerRouteMacro(): void 75 | { 76 | Route::macro('ResourceView', function ($uri) { 77 | Route::get("{$uri}", fn () => view("{$uri}.index"))->name("{$uri}_view.index"); 78 | Route::get("{$uri}/create", fn () => view("{$uri}.create"))->name("{$uri}_view.create"); 79 | Route::get("{$uri}/update/{id}", fn () => view("{$uri}.edit"))->name("{$uri}_view.edit"); 80 | Route::get("{$uri}/{id}", fn () => view("{$uri}.show"))->name("{$uri}_view.show"); 81 | }); 82 | 83 | Route::macro('ApiResource', function ($uri, $controller) { 84 | Route::get("{$uri}", "{$controller}@index")->name("{$uri}.index"); 85 | Route::post("{$uri}/create", "{$controller}@storage")->name("{$uri}.storage"); 86 | Route::put("{$uri}/update/{id}", "{$controller}@update")->name("{$uri}.update"); 87 | Route::get("{$uri}/{id}", "{$controller}@show")->name("{$uri}.show"); 88 | Route::delete("{$uri}/{id}", "{$controller}@destroy")->name("{$uri}.destroy"); 89 | }); 90 | } 91 | 92 | private function registerEvents(): void 93 | { 94 | Event::listen( 95 | MessageReady::class, 96 | ResponseMessageReady::class, 97 | ); 98 | } 99 | 100 | /** 101 | * Register any application services. 102 | * 103 | * @codeCoverageIgnore 104 | */ 105 | public function register(): void 106 | { 107 | $this->mergeConfigFrom( 108 | __DIR__.'/../config/cloudapi.php', 'meta' 109 | ); 110 | 111 | $this->app->bind('WhatsappCloudApi', function () { 112 | return new WhatsappCloudApi(); 113 | }); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Services/FacebookService.php: -------------------------------------------------------------------------------- 1 | graph_url .= config('meta.api_version').'/'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Services/FileManager.php: -------------------------------------------------------------------------------- 1 | graph_url}{$phoneNumberId}/media"; 13 | 14 | $response = Http::withToken(config('meta.token')) 15 | ->attach('file', file_get_contents($file->getRealPath()), $file->getClientOriginalName(), ['Content-Type' => $file->getClientMimeType()]) 16 | ->post($url, [ 17 | 'filename' => $filename, 18 | 'messaging_product' => 'whatsapp', 19 | ]); 20 | 21 | return $response->json(); 22 | } 23 | 24 | public function download(string $mediaId, string $phoneNumberId, string $filename, string $disk = 'local'): string 25 | { 26 | $media = $this->getMediaUrl($mediaId, $phoneNumberId); 27 | 28 | return $this->downloadMedia($media['url'], $filename, $disk); 29 | } 30 | 31 | private function getMediaUrl(string $mediaId, string $phoneNumberId): array 32 | { 33 | $url = "{$this->graph_url}{$mediaId}?phone_number_id={$phoneNumberId}"; 34 | $response = Http::withToken(config('meta.token'))->get($url); 35 | 36 | return $response->json(); 37 | } 38 | 39 | private function downloadMedia(string $mediaUrl, string $filename, string $disk): string 40 | { 41 | $response = Http::withToken(config('meta.token'))->get($mediaUrl); 42 | 43 | $extension = str_replace('inline;filename=File', '', $response->getHeaders()['Content-Disposition'][0]); 44 | 45 | Storage::disk($disk)->put("$filename$extension", $response->getBody()->getContents()); 46 | 47 | return Storage::disk($disk)->url("$filename$extension"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Services/MessageService.php: -------------------------------------------------------------------------------- 1 | graph_url .= "$phoneId/messages"; 14 | 15 | $response = Http::withToken(config('meta.token'))->post($this->graph_url, array_merge([ 16 | 'messaging_product' => 'whatsapp', 17 | 'recipient_type' => 'individual', 18 | 'to' => $to, 19 | ], $message))->throw(); 20 | 21 | return $response->json(); 22 | } 23 | 24 | public function sendTemplate(WabaPhone $phone, string $to, Template $template): array 25 | { 26 | $this->graph_url .= "$phone->phone_id/messages"; 27 | $response = Http::withToken(config('meta.token'))->post($this->graph_url, [ 28 | 'messaging_product' => 'whatsapp', 29 | 'recipient_type' => 'individual', 30 | 'to' => $to, 31 | 'type' => 'template', 32 | 'template' => [ 33 | 'name' => $template->name, 34 | 'language' => [ 35 | 'code' => $template->language, 36 | ], 37 | 'components' => $template->vars, 38 | ], 39 | ])->throw(); 40 | 41 | return $response->json(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Services/ResumableUploadAPI.php: -------------------------------------------------------------------------------- 1 | validate($file); 23 | $this->createUploadSession(); 24 | $this->uploadOrFail(); 25 | 26 | return $this; 27 | } 28 | 29 | private function validate(string $file) 30 | { 31 | if (file_exists($file)) { 32 | $this->file = $file; 33 | $this->mime_type = mime_content_type($file); 34 | $this->file_size = filesize($file); 35 | } else { 36 | throw new Exception("El archivo {$file} no existe."); 37 | } 38 | } 39 | 40 | private function createUploadSession(): void 41 | { 42 | $appId = config('meta.app_id'); 43 | $url = $this->graph_url."{$appId}/uploads"; 44 | $url .= '?file_length='.$this->file_size; 45 | $url .= '&file_type='.$this->mime_type; 46 | $url .= '&file_name=myprofile.jpg'; 47 | $url .= '&access_token='.config('meta.token'); 48 | 49 | $response = Http::post($url)->throw()->json(); 50 | $this->session_id = $response['id']; 51 | } 52 | 53 | private function uploadOrFail(string $offset = '0') 54 | { 55 | $url = $this->graph_url.$this->session_id; 56 | 57 | $response = Http::withHeaders([ 58 | 'Authorization' => 'OAuth '.config('meta.token'), 59 | 'file_offset' => $offset, 60 | ])->withBody(file_get_contents($this->file), $this->mime_type) 61 | ->post($url)->throw()->json(); 62 | 63 | if (! isset($response['h'])) { 64 | throw new Exception(json_encode($response)); 65 | } 66 | 67 | $this->handler = $response['h']; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Services/TemplateManagerService.php: -------------------------------------------------------------------------------- 1 | graph_url .= $wabaId.'/message_templates'; 12 | $response = Http::withToken(config('meta.token'))->post($this->graph_url, $template); 13 | 14 | return $response->json(); 15 | } 16 | 17 | public function getTemplate(string $templateId): array 18 | { 19 | $this->graph_url .= $templateId; 20 | $response = Http::withToken(config('meta.token'))->get($this->graph_url); 21 | 22 | return $response->json(); 23 | } 24 | 25 | // public function getAllTemplates(string $wabaId): array 26 | // { 27 | // $this->graph_url .= $wabaId; 28 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url); 29 | 30 | // return $response->json(); 31 | // } 32 | 33 | // public function setTemplate(string $phoneId): array 34 | // { 35 | // $this->graph_url .= $phoneId.'/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical'; 36 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url); 37 | 38 | // return $response->json(); 39 | // } 40 | 41 | // public function deleteTemplate(string $phoneId): array 42 | // { 43 | // $this->graph_url .= $phoneId.'/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical'; 44 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url); 45 | 46 | // return $response->json(); 47 | // } 48 | } 49 | -------------------------------------------------------------------------------- /src/Services/WabaManagerService.php: -------------------------------------------------------------------------------- 1 | graph_url .= $wabaId; 15 | $response = Http::withToken(config('meta.token'))->get($this->graph_url); 16 | 17 | Waba::saveWaba($response->json()); 18 | 19 | return $response->json(); 20 | } 21 | 22 | public function getPhoneNumbers(string $wabaId): array 23 | { 24 | $this->graph_url .= $wabaId.'/phone_numbers'; 25 | $response = Http::withToken(config('meta.token'))->get($this->graph_url); 26 | 27 | WabaPhone::savePhones($response->json(), $wabaId); 28 | 29 | return $response->json(); 30 | } 31 | 32 | public function getBussinesProfile(string $phoneId): array 33 | { 34 | $this->graph_url .= $phoneId.'/whatsapp_business_profile?fields=about,address,description,email,profile_picture_url,websites,vertical'; 35 | $response = Http::withToken(config('meta.token'))->get($this->graph_url); 36 | 37 | return $response->json(); 38 | } 39 | 40 | public function setBussinesProfile(string $phoneId, $data): array 41 | { 42 | $this->graph_url .= $phoneId.'/whatsapp_business_profile'; 43 | 44 | $response = Http::withToken(config('meta.token')) 45 | ->post($this->graph_url, array_merge($data, ['messaging_product' => 'whatsapp'])); 46 | 47 | return $response->json(); 48 | } 49 | 50 | public function getAllTemplates(string $wabaId): array 51 | { 52 | $this->graph_url .= $wabaId.'/message_templates'; 53 | $response = Http::withToken(config('meta.token'))->get($this->graph_url); 54 | 55 | return $response->json(); 56 | } 57 | 58 | // public function getAllWabas(): Response 59 | // { 60 | // $this->graph_url .= config('meta.app_id') . '/owned_whatsapp_business_accounts'; 61 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url); 62 | // return $response->json(); 63 | // } 64 | 65 | // public function subscribeWaba(string $wabaId): Response 66 | // { 67 | // $this->graph_url .= $wabaId.'/phone_numbers'; 68 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url); 69 | // return $response->json(); 70 | // } 71 | 72 | // public function getAllPhoneNumberFromWaba(string $wabaId): Response 73 | // { 74 | // $this->graph_url .= $wabaId.'/phone_numbers'; 75 | // $response = Http::withToken(config('meta.token'))->get($this->graph_url); 76 | // return $response->json(); 77 | // } 78 | 79 | // public function getPin2faToNumber(string $phoneId): Response 80 | // { 81 | // $this->graph_url .= $phoneId.'/register'; 82 | // $response = Http::withToken(config('meta.token'))->post($this->graph_url, [ 83 | // 'messaging_product' => 'whatsapp', 84 | // ]); 85 | // return $response->json(); 86 | // } 87 | // public function enable2faToNumber(string $phoneId, string $pin): Response 88 | // { 89 | // $this->graph_url .= $phoneId.'/register'; 90 | // $response = Http::withToken(config('meta.token'))->post($this->graph_url, [ 91 | // 'messaging_product' => 'whatsapp', 92 | // 'pin' => $pin 93 | // ]); 94 | // return $response->json(); 95 | // } 96 | } 97 | -------------------------------------------------------------------------------- /src/WhatsappCloudApi.php: -------------------------------------------------------------------------------- 1 | name('waba.init'); 13 | Route::get('waba/{wabaId}/get-templates', 'WabaController@loadTemplatesFromWaba')->name('waba.getTemplatesFromMeta'); 14 | Route::get('waba/{wabaId}/get-info', 'WabaController@getWabaInfoFromMeta')->name('waba.getInfoFromMeta'); 15 | Route::get('waba/{wabaId}/get-phones', 'WabaController@getWabaPhonesFromMeta')->name('waba.getPhonesFromMeta'); 16 | 17 | Route::ApiResource('template', 'TemplateController'); 18 | Route::get('waba', 'WabaController@index')->name('waba.index'); 19 | 20 | Route::post('message/send', 'MessageController@sendMessage')->name('message.send'); 21 | Route::post('message/template/send', 'TemplateController@sendTemplate')->name('message.template.send'); 22 | Route::get('message', 'MessageController@index')->name('message.index'); 23 | 24 | Route::get('waba-phone', 'WabaPhoneController@index')->name('waba_phone.waba_number'); 25 | Route::get('waba-phone/{phoneId}/bussines_profile', 'WabaPhoneController@getBussinesProfile')->name('waba_phone.bussines_profile'); 26 | Route::post('waba-phone/{phoneId}/bussines_profile', 'WabaPhoneController@setBussinesProfile')->name('waba_phone.storage_bussines_profile'); 27 | 28 | Route::get('chat', 'ChatController@index')->name('chat.index'); 29 | 30 | Route::post('campaign', 'CampaignController@store')->name('campaign.store'); 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stubs/10/app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $dontFlash = [ 17 | 'current_password', 18 | 'password', 19 | 'password_confirmation', 20 | ]; 21 | 22 | /** 23 | * Register the exception handling callbacks for the application. 24 | */ 25 | public function register(): void 26 | { 27 | $this->reportable(function (Throwable $e) { 28 | // 29 | }); 30 | } 31 | 32 | /** 33 | * Render an exception into an HTTP response. 34 | * 35 | * @param \Illuminate\Http\Request $request 36 | * @return \Symfony\Component\HttpFoundation\Response 37 | * 38 | * @throws \Throwable 39 | */ 40 | public function render($request, Throwable $e) 41 | { 42 | 43 | if ($e instanceof ValidationException && ($request->ajax() || $request->wantsJson() || $request->is('api/*'))) { 44 | return response()->json($e->errors(), 422); 45 | } 46 | 47 | return parent::render($request, $e); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /stubs/11/bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withRouting( 9 | web: __DIR__.'/../routes/web.php', 10 | commands: __DIR__.'/../routes/console.php', 11 | health: '/up', 12 | ) 13 | ->withMiddleware(function (Middleware $middleware) { 14 | // 15 | }) 16 | ->withExceptions(function (Exceptions $exceptions) { 17 | // 18 | })->create(); 19 | -------------------------------------------------------------------------------- /stubs/common/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | laravel.test: 3 | build: 4 | context: ./vendor/laravel/sail/runtimes/8.3 5 | dockerfile: Dockerfile 6 | args: 7 | WWWGROUP: '${WWWGROUP}' 8 | image: sail-8.3/app 9 | extra_hosts: 10 | - 'host.docker.internal:host-gateway' 11 | ports: 12 | - '${APP_PORT:-80}:80' 13 | - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' 14 | environment: 15 | WWWUSER: '${WWWUSER}' 16 | LARAVEL_SAIL: 1 17 | XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' 18 | XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' 19 | IGNITION_LOCAL_SITES_PATH: '${PWD}' 20 | volumes: 21 | - '.:/var/www/html' 22 | - './..:/var/www/packages' 23 | networks: 24 | - sail 25 | depends_on: 26 | - mysql 27 | - redis 28 | - meilisearch 29 | - mailpit 30 | - selenium 31 | mysql: 32 | image: 'mysql/mysql-server:8.0' 33 | ports: 34 | - '${FORWARD_DB_PORT:-3306}:3306' 35 | environment: 36 | MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}' 37 | MYSQL_ROOT_HOST: '%' 38 | MYSQL_DATABASE: '${DB_DATABASE}' 39 | MYSQL_USER: '${DB_USERNAME}' 40 | MYSQL_PASSWORD: '${DB_PASSWORD}' 41 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 42 | volumes: 43 | - 'sail-mysql:/var/lib/mysql' 44 | - './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' 45 | networks: 46 | - sail 47 | healthcheck: 48 | test: 49 | - CMD 50 | - mysqladmin 51 | - ping 52 | - '-p${DB_PASSWORD}' 53 | retries: 3 54 | timeout: 5s 55 | redis: 56 | image: 'redis:alpine' 57 | ports: 58 | - '${FORWARD_REDIS_PORT:-6379}:6379' 59 | volumes: 60 | - 'sail-redis:/data' 61 | networks: 62 | - sail 63 | healthcheck: 64 | test: 65 | - CMD 66 | - redis-cli 67 | - ping 68 | retries: 3 69 | timeout: 5s 70 | meilisearch: 71 | image: 'getmeili/meilisearch:latest' 72 | ports: 73 | - '${FORWARD_MEILISEARCH_PORT:-7700}:7700' 74 | environment: 75 | MEILI_NO_ANALYTICS: '${MEILISEARCH_NO_ANALYTICS:-false}' 76 | volumes: 77 | - 'sail-meilisearch:/meili_data' 78 | networks: 79 | - sail 80 | healthcheck: 81 | test: 82 | - CMD 83 | - wget 84 | - '--no-verbose' 85 | - '--spider' 86 | - 'http://localhost:7700/health' 87 | retries: 3 88 | timeout: 5s 89 | mailpit: 90 | image: 'axllent/mailpit:latest' 91 | ports: 92 | - '${FORWARD_MAILPIT_PORT:-1025}:1025' 93 | - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025' 94 | networks: 95 | - sail 96 | selenium: 97 | image: selenium/standalone-chrome 98 | extra_hosts: 99 | - 'host.docker.internal:host-gateway' 100 | volumes: 101 | - '/dev/shm:/dev/shm' 102 | networks: 103 | - sail 104 | soketi: 105 | image: 'quay.io/soketi/soketi:latest-16-alpine' 106 | environment: 107 | SOKETI_DEBUG: '1' 108 | SOKETI_METRICS_SERVER_PORT: '9601' 109 | ports: 110 | - '${SOKETI_PORT:-6001}:6001' 111 | - '${SOKETI_METRICS_SERVER_PORT:-9601}:9601' 112 | networks: 113 | - sail 114 | networks: 115 | sail: 116 | driver: bridge 117 | volumes: 118 | sail-mysql: 119 | driver: local 120 | sail-redis: 121 | driver: local 122 | sail-meilisearch: 123 | driver: local 124 | -------------------------------------------------------------------------------- /stubs/interface/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /stubs/interface/resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /stubs/interface/resources/js/app.js: -------------------------------------------------------------------------------- 1 | import Echo from 'laravel-echo'; 2 | import Pusher from 'pusher-js'; 3 | import { createApp } from 'vue' 4 | import MessengerComponent from "./components/Messenger/Chat/Messenger.vue"; 5 | import TemplateIndex from "./components/Messenger/Template/Index.vue"; 6 | import TemplateCreate from "./components/Messenger/Template/Create.vue"; 7 | import TemplateUpdate from "./components/Messenger/Template/Update.vue"; 8 | import TemplateDetail from "./components/Messenger/Template/Detail.vue"; 9 | 10 | window.Pusher = Pusher; 11 | 12 | window.Echo = new Echo({ 13 | broadcaster: 'pusher', 14 | key: import.meta.env.VITE_PUSHER_APP_KEY, 15 | cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1', 16 | wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, 17 | wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, 18 | wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, 19 | forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', 20 | enabledTransports: ['ws', 'wss'], 21 | }); 22 | 23 | let element = document.getElementById('messenger') 24 | if (element !== null) { 25 | const app = createApp({}); 26 | app.component('MessengerComponent', MessengerComponent) 27 | app.component('TemplateIndex', TemplateIndex) 28 | app.component('TemplateCreate', TemplateCreate) 29 | app.component('TemplateUpdate', TemplateUpdate) 30 | app.component('TemplateDetail', TemplateDetail) 31 | 32 | app.mount('#messenger'); 33 | } 34 | 35 | // const files = require.context('../myFolder', true, /(Module|Utils)\.js$/) 36 | -------------------------------------------------------------------------------- /stubs/interface/resources/js/components/Messenger/Chat/ChatBubble.vue: -------------------------------------------------------------------------------- 1 |