From 9fb8dbf324079071590c92730390ae6238457c4e Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 16 May 2024 19:24:37 +0530 Subject: [PATCH 01/30] chore: WIP Signed-off-by: sarthakjdev --- cmd/example-chat-bot/main.go | 4 +- pkg/client/cient.go | 18 -------- pkg/client/client.go | 57 ++++++++++++++++++++++++ pkg/{manager => client}/media_manager.go | 17 ++++--- pkg/client/message_manager.go | 19 ++++++++ pkg/client/phone_manager.go | 11 +++++ pkg/client/request_client.go | 8 ++-- pkg/manager/base_manager.go | 9 ---- pkg/manager/message_manager.go | 21 --------- pkg/manager/phone_manager.go | 9 ---- pkg/models/base_message.go | 2 +- pkg/models/contact_message.go | 5 +++ pkg/models/interactive_message.go | 8 ++++ pkg/models/location_message.go | 6 +++ pkg/models/media_message.go | 17 +++++++ pkg/models/reaction.go | 5 +++ pkg/models/template_message.go | 7 +++ pkg/models/text_message.go | 2 +- pkg/webhook/handler.go | 3 ++ pkg/webhook/webhook.go | 24 ++++++++-- utils/utils.go | 1 + 21 files changed, 179 insertions(+), 74 deletions(-) delete mode 100644 pkg/client/cient.go create mode 100644 pkg/client/client.go rename pkg/{manager => client}/media_manager.go (50%) create mode 100644 pkg/client/message_manager.go create mode 100644 pkg/client/phone_manager.go delete mode 100644 pkg/manager/base_manager.go delete mode 100644 pkg/manager/message_manager.go delete mode 100644 pkg/manager/phone_manager.go create mode 100644 pkg/models/contact_message.go create mode 100644 pkg/models/interactive_message.go create mode 100644 pkg/models/location_message.go create mode 100644 pkg/models/media_message.go create mode 100644 pkg/models/reaction.go create mode 100644 pkg/models/template_message.go create mode 100644 utils/utils.go diff --git a/cmd/example-chat-bot/main.go b/cmd/example-chat-bot/main.go index cd33a2c..964cfa8 100644 --- a/cmd/example-chat-bot/main.go +++ b/cmd/example-chat-bot/main.go @@ -3,11 +3,11 @@ package main import ( "fmt" - "github.com/sarthakjdev/wapi.go/pkg/client" + wapi "github.com/sarthakjdev/wapi.go/pkg/client" ) func main() { fmt.Println("This is the main package entry point of my golang file") - whatsappClient := client.NewClient() + whatsappClient := wapi.NewWapiClient(wapi.ClientConfig{}) fmt.Print(whatsappClient) } diff --git a/pkg/client/cient.go b/pkg/client/cient.go deleted file mode 100644 index 37c1b90..0000000 --- a/pkg/client/cient.go +++ /dev/null @@ -1,18 +0,0 @@ -package client - -import "github.com/sarthakjdev/wapi.go/pkg/manager" - -type Client struct { - media manager.MediaManager - message manager.MessageManager - requester requestClient -} - -func NewClient() *Client { - return &Client{ - media: *manager.NewMediaManager(), - message: *manager.NewMessageManager(), - requester: *NewRequestClient(), - } -} - diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..c92a3e0 --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,57 @@ +package wapi + +import ( + "github.com/sarthakjdev/wapi.go/pkg/webhook" +) + +// Client represents a WhatsApp client. +type Client struct { + Media MediaManager + message MessageManager + Phone PhoneNumbersManager + webhook webhook.Webhook + phoneNumberId string + apiAccessToken string + host string + businessAccountId string + apiVersion string +} + +type ClientConfig struct { + phoneNumberId string + apiAccessToken string + businessAccountId string + webhookPath string + webhookSecret string + webhookServerPort int +} + +// NewClient creates a new instance of Client. +func NewWapiClient(options ClientConfig) *Client { + requester := *NewRequestClient() + + return &Client{ + Media: *NewMediaManager(requester), + message: *NewMessageManager(requester), + Phone: *NewPhoneNumbersManager(requester), + webhook: *webhook.NewWebhook(webhook.WebhookManagerConfig{Path: options.webhookPath, Secret: options.webhookSecret, Port: options.webhookServerPort}), + phoneNumberId: options.phoneNumberId, + apiAccessToken: options.apiAccessToken, + host: "graph.facebook.com", + businessAccountId: options.businessAccountId, + apiVersion: "v19.0", + } +} + +func (client *Client) GetPhoneNumberId() string { + return client.phoneNumberId +} + +func (client *Client) SetPhoneNumberId(phoneNumberId string) { + client.phoneNumberId = phoneNumberId +} + +func (client *Client) InitiateClient() bool { + client.webhook.ListenToEvents() + return true +} diff --git a/pkg/manager/media_manager.go b/pkg/client/media_manager.go similarity index 50% rename from pkg/manager/media_manager.go rename to pkg/client/media_manager.go index 89c2af2..3c24f4f 100644 --- a/pkg/manager/media_manager.go +++ b/pkg/client/media_manager.go @@ -1,21 +1,24 @@ -package manager +package wapi type MediaManager struct { - BaseManager + requester requestClient } -func NewMediaManager() *MediaManager { - return &MediaManager{} +func NewMediaManager(requester requestClient) *MediaManager { + return &MediaManager{ + requester: requester, + } } func (mm *MediaManager) UploadMedia() { - // Play media + // upload media + } func (mm *MediaManager) GetMediaUrlById() { - // Play media + } func (mm *MediaManager) GetMediaIdByUrl() { - // Play media + } diff --git a/pkg/client/message_manager.go b/pkg/client/message_manager.go new file mode 100644 index 0000000..3446a38 --- /dev/null +++ b/pkg/client/message_manager.go @@ -0,0 +1,19 @@ +package wapi + +type MessageManager struct { + requester requestClient +} + +func NewMessageManager(requester requestClient) *MessageManager { + return &MessageManager{ + requester: requester, + } +} + +func (mm *MessageManager) Send() { + // Send message +} + +func (mm *MessageManager) Reply() { + // Reply to message +} diff --git a/pkg/client/phone_manager.go b/pkg/client/phone_manager.go new file mode 100644 index 0000000..6722fe6 --- /dev/null +++ b/pkg/client/phone_manager.go @@ -0,0 +1,11 @@ +package wapi + +type PhoneNumbersManager struct { + requester requestClient +} + +func NewPhoneNumbersManager(requester requestClient) *PhoneNumbersManager { + return &PhoneNumbersManager{ + requester: requester, + } +} diff --git a/pkg/client/request_client.go b/pkg/client/request_client.go index 014aa59..af4b78f 100644 --- a/pkg/client/request_client.go +++ b/pkg/client/request_client.go @@ -1,6 +1,8 @@ -package client +package wapi -import "fmt" +import ( + "fmt" +) type requestClient struct { } @@ -9,6 +11,6 @@ func NewRequestClient() *requestClient { return &requestClient{} } -func (*requestClient) requestCloudApi() { +func (requestClientInstance *requestClient) requestCloudApi() { fmt.Println("Requesting cloud api") } diff --git a/pkg/manager/base_manager.go b/pkg/manager/base_manager.go deleted file mode 100644 index 8686c15..0000000 --- a/pkg/manager/base_manager.go +++ /dev/null @@ -1,9 +0,0 @@ -package manager - -type BaseManager struct { -} - -func NewBaseManager() *BaseManager { - return &BaseManager{} -} - diff --git a/pkg/manager/message_manager.go b/pkg/manager/message_manager.go deleted file mode 100644 index 0be1e12..0000000 --- a/pkg/manager/message_manager.go +++ /dev/null @@ -1,21 +0,0 @@ -package manager - -import "github.com/sarthakjdev/wapi.go/pkg/client" - -type MessageManager struct { - BaseManager - client client.Client -} - -func NewMessageManager() *MessageManager { - return &MessageManager{} -} - -func (mm *MessageManager) Send() { - - // Send message -} - -func (mm *MessageManager) Reply() { - // Reply to message -} diff --git a/pkg/manager/phone_manager.go b/pkg/manager/phone_manager.go deleted file mode 100644 index 3c36d38..0000000 --- a/pkg/manager/phone_manager.go +++ /dev/null @@ -1,9 +0,0 @@ -package manager - -type PhoneNumbersManager struct { - BaseManager -} - -func (*PhoneNumbersManager) NewPhoneNumbersManager() *PhoneNumbersManager { - return &PhoneNumbersManager{} -} diff --git a/pkg/models/base_message.go b/pkg/models/base_message.go index db26a5a..abe6e98 100644 --- a/pkg/models/base_message.go +++ b/pkg/models/base_message.go @@ -1,4 +1,4 @@ -package types +package models type BaseMessage struct { } diff --git a/pkg/models/contact_message.go b/pkg/models/contact_message.go new file mode 100644 index 0000000..b1358b7 --- /dev/null +++ b/pkg/models/contact_message.go @@ -0,0 +1,5 @@ +package models + +type ContactMessage struct { + BaseMessage +} \ No newline at end of file diff --git a/pkg/models/interactive_message.go b/pkg/models/interactive_message.go new file mode 100644 index 0000000..bb23b72 --- /dev/null +++ b/pkg/models/interactive_message.go @@ -0,0 +1,8 @@ +package models + + + +type BaseInteractiveMessage struct { + BaseMessage +} + diff --git a/pkg/models/location_message.go b/pkg/models/location_message.go new file mode 100644 index 0000000..7c415d4 --- /dev/null +++ b/pkg/models/location_message.go @@ -0,0 +1,6 @@ +package models + + +type LocateMessage struct { + +} \ No newline at end of file diff --git a/pkg/models/media_message.go b/pkg/models/media_message.go new file mode 100644 index 0000000..b92fd03 --- /dev/null +++ b/pkg/models/media_message.go @@ -0,0 +1,17 @@ +package models + +type BaseMediaMessage struct { + BaseMessage +} + +type AudioMessage struct { +} + +type ImageMessage struct { +} + +type VideoMessage struct { +} + +type DocumentMessage struct { +} diff --git a/pkg/models/reaction.go b/pkg/models/reaction.go new file mode 100644 index 0000000..ffedcf4 --- /dev/null +++ b/pkg/models/reaction.go @@ -0,0 +1,5 @@ +package models + +type ReactionMessage struct { + BaseMessage +} diff --git a/pkg/models/template_message.go b/pkg/models/template_message.go new file mode 100644 index 0000000..9ef08c1 --- /dev/null +++ b/pkg/models/template_message.go @@ -0,0 +1,7 @@ +package models + +type TemplateMessage struct { + BaseMessage +} + + diff --git a/pkg/models/text_message.go b/pkg/models/text_message.go index cb8cd08..ab6f207 100644 --- a/pkg/models/text_message.go +++ b/pkg/models/text_message.go @@ -1,4 +1,4 @@ -package types +package models type TextMessage struct { // embedding diff --git a/pkg/webhook/handler.go b/pkg/webhook/handler.go index 1bc4d36..6287512 100644 --- a/pkg/webhook/handler.go +++ b/pkg/webhook/handler.go @@ -2,3 +2,6 @@ package webhook func getRequestHandler() { } + + + diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index 4cb0012..6bfafda 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -1,8 +1,26 @@ package webhook -type webhook struct { +// references for event driven architecture in golang: https://medium.com/@souravchoudhary0306/implementation-of-event-driven-architecture-in-go-golang-28d9a1c01f91 +type Webhook struct { + secret string + path string + port int } -func NewWebhook() *webhook { - return &webhook{} +type WebhookManagerConfig struct { + Secret string + Path string + Port int +} + +func NewWebhook(options WebhookManagerConfig) *Webhook { + return &Webhook{ + secret: options.Secret, + path: options.Path, + port: options.Port, + } +} + +func (wh *Webhook) ListenToEvents() { + } diff --git a/utils/utils.go b/utils/utils.go new file mode 100644 index 0000000..d4b585b --- /dev/null +++ b/utils/utils.go @@ -0,0 +1 @@ +package utils From d936fdef1f91ea0ca56eefc8ba5690778079e622 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 16 May 2024 19:52:43 +0530 Subject: [PATCH 02/30] fix: make requester and manager package internal Signed-off-by: sarthakjdev --- .../manager}/media_manager.go | 2 +- .../manager}/message_manager.go | 2 +- .../manager}/phone_manager.go | 2 +- .../manager}/request_client.go | 2 +- {pkg => internal}/webhook/handler.go | 0 {pkg => internal}/webhook/webhook.go | 0 pkg/client/client.go | 24 ++++++++++++------- 7 files changed, 19 insertions(+), 13 deletions(-) rename {pkg/client => internal/manager}/media_manager.go (95%) rename {pkg/client => internal/manager}/message_manager.go (94%) rename {pkg/client => internal/manager}/phone_manager.go (92%) rename {pkg/client => internal/manager}/request_client.go (93%) rename {pkg => internal}/webhook/handler.go (100%) rename {pkg => internal}/webhook/webhook.go (100%) diff --git a/pkg/client/media_manager.go b/internal/manager/media_manager.go similarity index 95% rename from pkg/client/media_manager.go rename to internal/manager/media_manager.go index 3c24f4f..ccd2530 100644 --- a/pkg/client/media_manager.go +++ b/internal/manager/media_manager.go @@ -1,4 +1,4 @@ -package wapi +package manager type MediaManager struct { requester requestClient diff --git a/pkg/client/message_manager.go b/internal/manager/message_manager.go similarity index 94% rename from pkg/client/message_manager.go rename to internal/manager/message_manager.go index 3446a38..7ae1b10 100644 --- a/pkg/client/message_manager.go +++ b/internal/manager/message_manager.go @@ -1,4 +1,4 @@ -package wapi +package manager type MessageManager struct { requester requestClient diff --git a/pkg/client/phone_manager.go b/internal/manager/phone_manager.go similarity index 92% rename from pkg/client/phone_manager.go rename to internal/manager/phone_manager.go index 6722fe6..a3b4d7d 100644 --- a/pkg/client/phone_manager.go +++ b/internal/manager/phone_manager.go @@ -1,4 +1,4 @@ -package wapi +package manager type PhoneNumbersManager struct { requester requestClient diff --git a/pkg/client/request_client.go b/internal/manager/request_client.go similarity index 93% rename from pkg/client/request_client.go rename to internal/manager/request_client.go index af4b78f..9b307ac 100644 --- a/pkg/client/request_client.go +++ b/internal/manager/request_client.go @@ -1,4 +1,4 @@ -package wapi +package manager import ( "fmt" diff --git a/pkg/webhook/handler.go b/internal/webhook/handler.go similarity index 100% rename from pkg/webhook/handler.go rename to internal/webhook/handler.go diff --git a/pkg/webhook/webhook.go b/internal/webhook/webhook.go similarity index 100% rename from pkg/webhook/webhook.go rename to internal/webhook/webhook.go diff --git a/pkg/client/client.go b/pkg/client/client.go index c92a3e0..e26d488 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,14 +1,15 @@ package wapi import ( - "github.com/sarthakjdev/wapi.go/pkg/webhook" + manager "github.com/sarthakjdev/wapi.go/internal/manager" + "github.com/sarthakjdev/wapi.go/internal/webhook" ) // Client represents a WhatsApp client. type Client struct { - Media MediaManager - message MessageManager - Phone PhoneNumbersManager + Media manager.MediaManager + message manager.MessageManager + Phone manager.PhoneNumbersManager webhook webhook.Webhook phoneNumberId string apiAccessToken string @@ -17,6 +18,7 @@ type Client struct { apiVersion string } +// ClientConfig represents the configuration options for the WhatsApp client. type ClientConfig struct { phoneNumberId string apiAccessToken string @@ -26,14 +28,14 @@ type ClientConfig struct { webhookServerPort int } -// NewClient creates a new instance of Client. +// NewWapiClient creates a new instance of Client. func NewWapiClient(options ClientConfig) *Client { - requester := *NewRequestClient() + requester := *manager.NewRequestClient() return &Client{ - Media: *NewMediaManager(requester), - message: *NewMessageManager(requester), - Phone: *NewPhoneNumbersManager(requester), + Media: *manager.NewMediaManager(requester), + message: *manager.NewMessageManager(requester), + Phone: *manager.NewPhoneNumbersManager(requester), webhook: *webhook.NewWebhook(webhook.WebhookManagerConfig{Path: options.webhookPath, Secret: options.webhookSecret, Port: options.webhookServerPort}), phoneNumberId: options.phoneNumberId, apiAccessToken: options.apiAccessToken, @@ -43,14 +45,18 @@ func NewWapiClient(options ClientConfig) *Client { } } +// GetPhoneNumberId returns the phone number ID associated with the client. func (client *Client) GetPhoneNumberId() string { return client.phoneNumberId } +// SetPhoneNumberId sets the phone number ID for the client. func (client *Client) SetPhoneNumberId(phoneNumberId string) { client.phoneNumberId = phoneNumberId } +// InitiateClient initializes the client and starts listening to events from the webhook. +// It returns true if the client was successfully initiated. func (client *Client) InitiateClient() bool { client.webhook.ListenToEvents() return true From 5d8afd859b4a1e912e40cb8e17017b6ed005bb67 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 16 May 2024 20:18:25 +0530 Subject: [PATCH 03/30] chore: move example-chat-bot in the root Signed-off-by: sarthakjdev --- {cmd/example-chat-bot => example-chat-bot}/main.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {cmd/example-chat-bot => example-chat-bot}/main.go (100%) diff --git a/cmd/example-chat-bot/main.go b/example-chat-bot/main.go similarity index 100% rename from cmd/example-chat-bot/main.go rename to example-chat-bot/main.go From 8e615e9683f8a2921e2c7db7fe0cfdb82c9488a9 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 16 May 2024 21:03:18 +0530 Subject: [PATCH 04/30] feat: add .github dir Signed-off-by: sarthakjdev --- .github/COMMIT_CONVENTION.md | 98 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/bug_report.md | 40 +++++++++ .github/ISSUE_TEMPLATE/config.yaml | 5 ++ .github/ISSUE_TEMPLATE/custom.md | 7 ++ .github/ISSUE_TEMPLATE/feature_request.md | 19 +++++ .github/PULL_REQUEST_TEMPLATE.md | 3 + .github/workflows/auto-comment.yaml | 18 +++++ .github/workflows/formatTag/action.yml | 16 ++++ .github/workflows/formatTag/formatTag.js | 22 +++++ .github/workflows/formatTag/index.js | 11 +++ .github/workflows/validate-pr-title.yml | 30 +++++++ 11 files changed, 269 insertions(+) create mode 100644 .github/COMMIT_CONVENTION.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yaml create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/auto-comment.yaml create mode 100644 .github/workflows/formatTag/action.yml create mode 100644 .github/workflows/formatTag/formatTag.js create mode 100644 .github/workflows/formatTag/index.js create mode 100644 .github/workflows/validate-pr-title.yml diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md new file mode 100644 index 0000000..0b6654b --- /dev/null +++ b/.github/COMMIT_CONVENTION.md @@ -0,0 +1,98 @@ +# ๐Ÿ“˜ Commit and Branch Conventions + +At `wapi.js`, we follow a strict set of conventions for commit messages and branch names to ensure that our repository stays organized, and our commit history remains crystal clear. Here's a guide on our conventions: + +--- + +## ๐Ÿš€ Conventional Commits + +Conventional commits ensure our commit messages are clear and useful. + +๐Ÿ“‹ **Benefits**: + +- ๐Ÿ“œ Automatic changelog generation +- ๐Ÿ”ข Simplified versioning +- ๐Ÿง Improved readability of commit messages + +### ๐Ÿ“„ Commit Message Format + +Each commit message should follow this format: + +``` +(): + + + + +``` + +- **``**: Describes the purpose of the commit: + - ๐Ÿ†• `feat`: Introduces a new feature + - ๐Ÿž `fix`: Addresses a bug + - ๐Ÿ“š `docs`: Updates documentation + - ๐ŸŽจ `style`: Code that relates to styling, not affecting logic + - ๐Ÿ”ง `refactor`: Refactoring existing code + - ๐Ÿš€ `perf`: Improving performance + - ๐Ÿงช `test`: All about tests + - ๐Ÿงฝ `chore`: Maintenance tasks +- **``**: (Optional) Specifies which part of the codebase is affected. + +- **``**: A concise summary of the changes made. + +### ๐Ÿ“ Examples: + +1. Introducing a new feature: + +``` +feat(auth): implement social login +``` + +2. Addressing a bug: + +``` +fix(button): resolve alignment issue +``` + +--- + +## ๐ŸŒฒ Conventional Branching + +A standardized naming system for branches helps everyone quickly understand a branch's purpose. + +### ๐Ÿ“„ Branch Naming Format + +Branch names should adhere to: + +``` +/ +``` + +- **``**: Purpose of the branch, common ones being: + + - ๐Ÿ†• `feature`: Developing a new feature. + - ๐Ÿž `fix`: Addressing a bug. + - ๐Ÿงฝ `chore`: Regular maintenance tasks. + - ๐Ÿ”ฅ `hotfix`: Immediate fixes, often tied to production issues. + - ๐Ÿ“š `docs`: Documentation enhancements. + +- **``**: A brief, kebab-cased (words separated by hyphens) description of the branch's objective. + +### ๐Ÿ“ Examples: + +1. Developing a new user dashboard: + +``` +feature/user-dashboard +``` + +2. Resolving a login issue: + +``` +fix/login-issue +``` + +--- + +๐Ÿ™Œ Thanks for contributing to `wapi.js`! By adhering to these conventions, we're making our repository a better place. If you're new, welcome aboard, and if you've been here, thanks for sticking around! + +--- diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9b77ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: "" +assignees: "" +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yaml b/.github/ISSUE_TEMPLATE/config.yaml new file mode 100644 index 0000000..46be629 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yaml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Discord server + url: https://discord.gg/PytnZqWpeQ + about: Please visit our Discord server for questions and support requests. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..babf9b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,7 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: "" +labels: "" +assignees: "" +--- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..2bc5d5f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: "" +assignees: "" +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2dcc071 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ +**Please describe the changes this PR makes and why it should be merged:** + +**Status and versioning classification:** diff --git a/.github/workflows/auto-comment.yaml b/.github/workflows/auto-comment.yaml new file mode 100644 index 0000000..17b974a --- /dev/null +++ b/.github/workflows/auto-comment.yaml @@ -0,0 +1,18 @@ +name: Auto Comment on New Issue Label +# This workflow is triggered when a label is added to an issue. +on: + issues: + types: [labeled] + +jobs: + processLabelAction: + name: Process Label Action + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Process Label Action + uses: hramos/respond-to-issue-based-on-label@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + configuration-path: ".github/label-actions.yml" diff --git a/.github/workflows/formatTag/action.yml b/.github/workflows/formatTag/action.yml new file mode 100644 index 0000000..baef702 --- /dev/null +++ b/.github/workflows/formatTag/action.yml @@ -0,0 +1,16 @@ +name: 'Format Tag' +description: 'Formats a git tag to remove potentially prefixes' +inputs: + tag: + description: 'The input tag' + required: true +outputs: + subpackage: + description: 'Whether this tag is a subpackage tag' + package: + description: 'The package string that was extracted from this tag' + semver: + description: 'The semver string that was extracted from this tag' +runs: + using: node20 + main: ./index.js diff --git a/.github/workflows/formatTag/formatTag.js b/.github/workflows/formatTag/formatTag.js new file mode 100644 index 0000000..0f847d8 --- /dev/null +++ b/.github/workflows/formatTag/formatTag.js @@ -0,0 +1,22 @@ +export function formatTag(tag) { + const parsed = /(?:^@.*\/(?.*)@v?)?(?\d+.\d+.\d+)-?.*/.exec( + tag + ); + const parsedPackage = /(?.*)@v?-?.*/.exec(tag); + + if (parsed?.groups) { + const isSubPackage = typeof parsed.groups.package === "string"; + const pkg = isSubPackage + ? parsed.groups.package + : parsedPackage?.groups?.package ?? "wapi.js"; + const semver = parsed.groups.semver; + + return { + isSubpackage: isSubPackage, + package: pkg, + semver, + }; + } + + return null; +} diff --git a/.github/workflows/formatTag/index.js b/.github/workflows/formatTag/index.js new file mode 100644 index 0000000..5ddc8d9 --- /dev/null +++ b/.github/workflows/formatTag/index.js @@ -0,0 +1,11 @@ +import { getInput, setOutput } from "@actions/core"; +import { formatTag } from "./formatTag.js"; + +const tag = getInput("tag", { required: true }); +const parsed = formatTag(tag); + +if (parsed) { + setOutput("subpackage", parsed.isSubpackage); + setOutput("package", parsed.package); + setOutput("semver", parsed.semver); +} diff --git a/.github/workflows/validate-pr-title.yml b/.github/workflows/validate-pr-title.yml new file mode 100644 index 0000000..5b99bed --- /dev/null +++ b/.github/workflows/validate-pr-title.yml @@ -0,0 +1,30 @@ +name: Check PR title + +on: + pull_request: + types: + - opened + - reopened + - edited + - synchronize + - review_requested + - auto_merge_enabled + +concurrency: + group: check-pr-title-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + validate-pr-title: + name: Validate PR title + runs-on: ubuntu-latest + permissions: + statuses: write + timeout-minutes: 10 + steps: + - uses: aslafy-z/conventional-pr-title-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + success-state: "PR Title follows the Conventional Commits spec" + failure-state: "PR Title does not follow the Conventional Commits spec" From c559e6b3c645690dce2e27802e9fbe7ebc1fee5e Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Wed, 22 May 2024 16:35:47 +0530 Subject: [PATCH 05/30] feat: request client works fine and Message manager works fine too. Signed-off-by: sarthakjdev --- example-chat-bot/main.go | 27 +++++++++++++--- go.mod | 21 +++++++++++++ go.sum | 43 ++++++++++++++++++++++++++ internal/manager/media_manager.go | 1 - internal/manager/message_manager.go | 32 +++++++++++++++++-- internal/manager/request_client.go | 48 ++++++++++++++++++++++++++--- internal/webhook/handler.go | 7 +++-- internal/webhook/webhook.go | 11 +++++++ pkg/client/client.go | 41 ++++++++++++------------ pkg/models/base_message.go | 17 +++++++++- pkg/models/contact_message.go | 33 ++++++++++++++++++-- pkg/models/interactive_message.go | 4 --- pkg/models/media_message.go | 1 - pkg/models/reaction.go | 1 - pkg/models/template_message.go | 3 -- pkg/models/text_message.go | 43 ++++++++++++++++++++++++-- utils/utils.go | 11 +++++++ 17 files changed, 297 insertions(+), 47 deletions(-) create mode 100644 go.sum diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 964cfa8..2b14362 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -1,13 +1,30 @@ package main import ( - "fmt" - + "github.com/sarthakjdev/wapi.go/internal/manager" wapi "github.com/sarthakjdev/wapi.go/pkg/client" + wapiModels "github.com/sarthakjdev/wapi.go/pkg/models" ) func main() { - fmt.Println("This is the main package entry point of my golang file") - whatsappClient := wapi.NewWapiClient(wapi.ClientConfig{}) - fmt.Print(whatsappClient) + // creating a client + whatsappClient := wapi.New(wapi.ClientConfig{ + PhoneNumberId: "113269274970227", + ApiAccessToken: "EABhCftGVaeIBOZCmTPNEWyAWSq1Bna8ZCs2Rl7YoXtHfjuXZCAZCMVkjUD8pauKlkGHO6NHP5dXLG9zG5wH7qZAm2GBu8ZChKo37TBa2LzFedZA5sAAZBfgKJ7k7sQZB8t5neJ46DiTH4ZAjxFGvBWRJbC2CP30WHZBhES64pcLCSrWAkoitZBCKAwt5NkOZCoYZCymbP1LnK7e7SZC72R60UJZB4h0xOUvZCF99ohbeO5qeSuxou3Y0ZD", + BusinessAccountId: "103043282674158", + WebhookPath: "/webhook", + WebhookSecret: "123456789", + WebhookServerPort: 8080, + }) + // create a message + textMessage := wapiModels.NewTextMessage(wapiModels.TextMessageConfigs{ + Text: "Hello", + AllowPreview: true, + }) + + contactMessage := wapiModels.NewContactMessage(wapiModels.ContactMessageConfigs{}) + + // send the message + whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) + whatsappClient.Message.Send(manager.SendMessageParams{Message: contactMessage, PhoneNumber: "919643500545"}) } diff --git a/go.mod b/go.mod index 25f28dc..195a470 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,24 @@ module github.com/sarthakjdev/wapi.go go 1.21.3 + +require ( + github.com/go-playground/validator/v10 v10.20.0 + github.com/labstack/echo/v4 v4.12.0 +) + +require ( + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8cc48e0 --- /dev/null +++ b/go.sum @@ -0,0 +1,43 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/manager/media_manager.go b/internal/manager/media_manager.go index ccd2530..7a08243 100644 --- a/internal/manager/media_manager.go +++ b/internal/manager/media_manager.go @@ -12,7 +12,6 @@ func NewMediaManager(requester requestClient) *MediaManager { func (mm *MediaManager) UploadMedia() { // upload media - } func (mm *MediaManager) GetMediaUrlById() { diff --git a/internal/manager/message_manager.go b/internal/manager/message_manager.go index 7ae1b10..b8a6d16 100644 --- a/internal/manager/message_manager.go +++ b/internal/manager/message_manager.go @@ -1,5 +1,9 @@ package manager +import ( + "github.com/sarthakjdev/wapi.go/pkg/models" +) + type MessageManager struct { requester requestClient } @@ -10,8 +14,32 @@ func NewMessageManager(requester requestClient) *MessageManager { } } -func (mm *MessageManager) Send() { - // Send message +type SendMessageParams struct { + Message models.BaseMessage + PhoneNumber string +} + +// ! TODO: return the structured response from here +func (mm *MessageManager) Send(params SendMessageParams) { + // pass the phone number in this toJson method of every message + body, err := params.Message.ToJson() + if err != nil { + // emit a error event here + return + } + + mm.requester.requestCloudApi(requestCloudApiParams{ + body: string(body), + path: "/" + mm.requester.phoneNumberId + "/messages", + }) + + // if err != nil { + // // emit a error event here + // return + // } + + // fmt.Println("Response from cloud api is", response) + } func (mm *MessageManager) Reply() { diff --git a/internal/manager/request_client.go b/internal/manager/request_client.go index 9b307ac..707c850 100644 --- a/internal/manager/request_client.go +++ b/internal/manager/request_client.go @@ -2,15 +2,55 @@ package manager import ( "fmt" + "io" + "net/http" + "strings" +) + +const ( + API_VERSION = "v.19" + BASE_URL = "graph.facebook.com" + REQUEST_PROTOCOL = "https" ) type requestClient struct { + apiVersion string + phoneNumberId string + baseUrl string + apiAccessToken string +} + +func NewRequestClient(phoneNumberId string, apiAccessToken string) *requestClient { + return &requestClient{ + apiVersion: API_VERSION, + baseUrl: BASE_URL, + phoneNumberId: phoneNumberId, + apiAccessToken: apiAccessToken, + } } -func NewRequestClient() *requestClient { - return &requestClient{} +type requestCloudApiParams struct { + body string + path string } -func (requestClientInstance *requestClient) requestCloudApi() { - fmt.Println("Requesting cloud api") +func (requestClientInstance *requestClient) requestCloudApi(params requestCloudApiParams) { + httpRequest, err := http.NewRequest("POST", fmt.Sprintf("%s://%s/%s", REQUEST_PROTOCOL, requestClientInstance.baseUrl, params.path), strings.NewReader(params.body)) + if err != nil { + return + } + httpRequest.Header.Set("Content-Type", "application/json") + httpRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", requestClientInstance.apiAccessToken)) + client := &http.Client{} + response, err := client.Do(httpRequest) + if err != nil { + fmt.Println("Error while requesting cloud api", err) + return + } + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + return + } + fmt.Println("Response from cloud api is", string(body)) } diff --git a/internal/webhook/handler.go b/internal/webhook/handler.go index 6287512..3595270 100644 --- a/internal/webhook/handler.go +++ b/internal/webhook/handler.go @@ -1,7 +1,10 @@ package webhook -func getRequestHandler() { -} +import "net/http" +func (wh *Webhook) getRequestHandler(req *http.Request) { +} +func (wh *Webhook) postRequestHandler(req *http.Request) { +} diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go index 6bfafda..a31c13f 100644 --- a/internal/webhook/webhook.go +++ b/internal/webhook/webhook.go @@ -1,5 +1,9 @@ package webhook +import ( + "github.com/labstack/echo/v4" +) + // references for event driven architecture in golang: https://medium.com/@souravchoudhary0306/implementation-of-event-driven-architecture-in-go-golang-28d9a1c01f91 type Webhook struct { secret string @@ -21,6 +25,13 @@ func NewWebhook(options WebhookManagerConfig) *Webhook { } } +// this function is used in case if the client have not provided any custom http server +func (wh *Webhook) createEchoHttpServer() *echo.Echo { + e := echo.New() + return e + +} + func (wh *Webhook) ListenToEvents() { } diff --git a/pkg/client/client.go b/pkg/client/client.go index e26d488..702cbb3 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -1,47 +1,50 @@ package wapi import ( + "fmt" + manager "github.com/sarthakjdev/wapi.go/internal/manager" "github.com/sarthakjdev/wapi.go/internal/webhook" + "github.com/sarthakjdev/wapi.go/utils" ) // Client represents a WhatsApp client. type Client struct { Media manager.MediaManager - message manager.MessageManager + Message manager.MessageManager Phone manager.PhoneNumbersManager webhook webhook.Webhook phoneNumberId string apiAccessToken string - host string businessAccountId string - apiVersion string } // ClientConfig represents the configuration options for the WhatsApp client. type ClientConfig struct { - phoneNumberId string - apiAccessToken string - businessAccountId string - webhookPath string - webhookSecret string - webhookServerPort int + PhoneNumberId string `validate:"required"` + ApiAccessToken string `validate:"required"` + BusinessAccountId string `validate:"required"` + WebhookPath string `validate:"required"` + WebhookSecret string `validate:"required"` + WebhookServerPort int } // NewWapiClient creates a new instance of Client. -func NewWapiClient(options ClientConfig) *Client { - requester := *manager.NewRequestClient() - +func New(configs ClientConfig) *Client { + err := utils.GetValidator().Struct(configs) + if err != nil { + fmt.Println("error validating client config", err) + return nil + } + requester := *manager.NewRequestClient(configs.PhoneNumberId, configs.ApiAccessToken) return &Client{ Media: *manager.NewMediaManager(requester), - message: *manager.NewMessageManager(requester), + Message: *manager.NewMessageManager(requester), Phone: *manager.NewPhoneNumbersManager(requester), - webhook: *webhook.NewWebhook(webhook.WebhookManagerConfig{Path: options.webhookPath, Secret: options.webhookSecret, Port: options.webhookServerPort}), - phoneNumberId: options.phoneNumberId, - apiAccessToken: options.apiAccessToken, - host: "graph.facebook.com", - businessAccountId: options.businessAccountId, - apiVersion: "v19.0", + webhook: *webhook.NewWebhook(webhook.WebhookManagerConfig{Path: configs.WebhookPath, Secret: configs.WebhookSecret, Port: configs.WebhookServerPort}), + phoneNumberId: configs.PhoneNumberId, + apiAccessToken: configs.ApiAccessToken, + businessAccountId: configs.BusinessAccountId, } } diff --git a/pkg/models/base_message.go b/pkg/models/base_message.go index abe6e98..6c00901 100644 --- a/pkg/models/base_message.go +++ b/pkg/models/base_message.go @@ -1,4 +1,19 @@ package models -type BaseMessage struct { +type MessageType string + +const ( + MessageTypeText MessageType = "text" + MessageTypeAudio MessageType = "audio" + MessageTypeDocument MessageType = "document" + MessageTypeContact MessageType = "contact" + MessageTypeLocation MessageType = "location" + MessageTypeImage MessageType = "image" + MessageTypeVideo MessageType = "video" + MessageTypeInteractive MessageType = "interactive" + MessageTypeTemplate MessageType = "template" +) + +type BaseMessage interface { + ToJson() ([]byte, error) } diff --git a/pkg/models/contact_message.go b/pkg/models/contact_message.go index b1358b7..dbb65ee 100644 --- a/pkg/models/contact_message.go +++ b/pkg/models/contact_message.go @@ -1,5 +1,34 @@ package models +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + type ContactMessage struct { - BaseMessage -} \ No newline at end of file +} + +type ContactMessageConfigs struct { +} + +func NewContactMessage(configs ContactMessageConfigs) *ContactMessage { + err := utils.GetValidator().Struct(configs) + fmt.Println("error validating text message config", err) + if err != nil { + return nil + } + return &ContactMessage{} +} + +func (m *ContactMessage) ToJson() ([]byte, error) { + messageJson, err := json.Marshal(m) + if err != nil { + // emit a error event here + return nil, err + } + fmt.Println("text message json is", string(messageJson)) + + return messageJson, nil +} diff --git a/pkg/models/interactive_message.go b/pkg/models/interactive_message.go index bb23b72..4a373dd 100644 --- a/pkg/models/interactive_message.go +++ b/pkg/models/interactive_message.go @@ -1,8 +1,4 @@ package models - - type BaseInteractiveMessage struct { - BaseMessage } - diff --git a/pkg/models/media_message.go b/pkg/models/media_message.go index b92fd03..84c435a 100644 --- a/pkg/models/media_message.go +++ b/pkg/models/media_message.go @@ -1,7 +1,6 @@ package models type BaseMediaMessage struct { - BaseMessage } type AudioMessage struct { diff --git a/pkg/models/reaction.go b/pkg/models/reaction.go index ffedcf4..8af3a8b 100644 --- a/pkg/models/reaction.go +++ b/pkg/models/reaction.go @@ -1,5 +1,4 @@ package models type ReactionMessage struct { - BaseMessage } diff --git a/pkg/models/template_message.go b/pkg/models/template_message.go index 9ef08c1..cec858c 100644 --- a/pkg/models/template_message.go +++ b/pkg/models/template_message.go @@ -1,7 +1,4 @@ package models type TemplateMessage struct { - BaseMessage } - - diff --git a/pkg/models/text_message.go b/pkg/models/text_message.go index ab6f207..0d645c9 100644 --- a/pkg/models/text_message.go +++ b/pkg/models/text_message.go @@ -1,6 +1,45 @@ package models +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + type TextMessage struct { - // embedding - BaseMessage + Text string `json:"text" validate:"required"` + AllowPreview bool `json:"allowPreview"` +} + +type TextMessageConfigs struct { + Text string + AllowPreview bool +} + +func (m *TextMessage) SetText(text string) { + m.Text = text +} + +func NewTextMessage(configs TextMessageConfigs) *TextMessage { + err := utils.GetValidator().Struct(configs) + fmt.Println("error validating text message config", err) + if err != nil { + return nil + } + return &TextMessage{ + Text: configs.Text, + AllowPreview: configs.AllowPreview, + } +} + +func (m *TextMessage) ToJson() ([]byte, error) { + messageJson, err := json.Marshal(m) + if err != nil { + // emit a error event here + return nil, err + } + fmt.Println("text message json is", string(messageJson)) + + return messageJson, nil } diff --git a/utils/utils.go b/utils/utils.go index d4b585b..3cc639a 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1 +1,12 @@ package utils + + + +import ( + "github.com/go-playground/validator/v10" +) + +// return a validator instance +func GetValidator() *validator.Validate { + return validator.New() +} From 98997dcb81e466d94a0970deebc0d67015fecc01 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Wed, 22 May 2024 16:39:17 +0530 Subject: [PATCH 06/30] chore: fmt Signed-off-by: sarthakjdev --- pkg/models/location_message.go | 6 ++---- utils/utils.go | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/pkg/models/location_message.go b/pkg/models/location_message.go index 7c415d4..987d7af 100644 --- a/pkg/models/location_message.go +++ b/pkg/models/location_message.go @@ -1,6 +1,4 @@ -package models - +package models type LocateMessage struct { - -} \ No newline at end of file +} diff --git a/utils/utils.go b/utils/utils.go index 3cc639a..7bab701 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -1,7 +1,5 @@ package utils - - import ( "github.com/go-playground/validator/v10" ) From 938d89c39224b3251314859ac01bf320d846e389 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Wed, 22 May 2024 17:10:05 +0530 Subject: [PATCH 07/30] chore: text message works fine Signed-off-by: sarthakjdev --- example-chat-bot/main.go | 21 +++++++++---- internal/manager/message_manager.go | 4 ++- pkg/client/client.go | 7 ++--- pkg/models/base_message.go | 7 ++++- pkg/models/text_message.go | 49 +++++++++++++++++++++-------- 5 files changed, 63 insertions(+), 25 deletions(-) diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 2b14362..09c1eb2 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -7,24 +7,33 @@ import ( ) func main() { + + // ! TODO: programmatic read the env variables here + // creating a client - whatsappClient := wapi.New(wapi.ClientConfig{ + whatsappClient, err := wapi.New(wapi.ClientConfig{ PhoneNumberId: "113269274970227", - ApiAccessToken: "EABhCftGVaeIBOZCmTPNEWyAWSq1Bna8ZCs2Rl7YoXtHfjuXZCAZCMVkjUD8pauKlkGHO6NHP5dXLG9zG5wH7qZAm2GBu8ZChKo37TBa2LzFedZA5sAAZBfgKJ7k7sQZB8t5neJ46DiTH4ZAjxFGvBWRJbC2CP30WHZBhES64pcLCSrWAkoitZBCKAwt5NkOZCoYZCymbP1LnK7e7SZC72R60UJZB4h0xOUvZCF99ohbeO5qeSuxou3Y0ZD", + ApiAccessToken: "", BusinessAccountId: "103043282674158", WebhookPath: "/webhook", WebhookSecret: "123456789", WebhookServerPort: 8080, }) + + if err != nil { + return + } + // create a message - textMessage := wapiModels.NewTextMessage(wapiModels.TextMessageConfigs{ - Text: "Hello", + textMessage, err := wapiModels.NewTextMessage(wapiModels.TextMessageConfigs{ + Text: "Hello, from wapi.go", AllowPreview: true, }) - contactMessage := wapiModels.NewContactMessage(wapiModels.ContactMessageConfigs{}) + if err != nil { + return + } // send the message whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) - whatsappClient.Message.Send(manager.SendMessageParams{Message: contactMessage, PhoneNumber: "919643500545"}) } diff --git a/internal/manager/message_manager.go b/internal/manager/message_manager.go index b8a6d16..e76e760 100644 --- a/internal/manager/message_manager.go +++ b/internal/manager/message_manager.go @@ -22,7 +22,9 @@ type SendMessageParams struct { // ! TODO: return the structured response from here func (mm *MessageManager) Send(params SendMessageParams) { // pass the phone number in this toJson method of every message - body, err := params.Message.ToJson() + body, err := params.Message.ToJson(models.ApiCompatibleJsonConverterConfigs{ + SendToPhoneNumber: params.PhoneNumber, + }) if err != nil { // emit a error event here return diff --git a/pkg/client/client.go b/pkg/client/client.go index 702cbb3..21f865c 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -30,11 +30,10 @@ type ClientConfig struct { } // NewWapiClient creates a new instance of Client. -func New(configs ClientConfig) *Client { +func New(configs ClientConfig) (*Client, error) { err := utils.GetValidator().Struct(configs) if err != nil { - fmt.Println("error validating client config", err) - return nil + return nil, fmt.Errorf("error validating client config", err) } requester := *manager.NewRequestClient(configs.PhoneNumberId, configs.ApiAccessToken) return &Client{ @@ -45,7 +44,7 @@ func New(configs ClientConfig) *Client { phoneNumberId: configs.PhoneNumberId, apiAccessToken: configs.ApiAccessToken, businessAccountId: configs.BusinessAccountId, - } + }, nil } // GetPhoneNumberId returns the phone number ID associated with the client. diff --git a/pkg/models/base_message.go b/pkg/models/base_message.go index 6c00901..1063437 100644 --- a/pkg/models/base_message.go +++ b/pkg/models/base_message.go @@ -14,6 +14,11 @@ const ( MessageTypeTemplate MessageType = "template" ) +type ApiCompatibleJsonConverterConfigs struct { + ReplyToMessageId string + SendToPhoneNumber string +} + type BaseMessage interface { - ToJson() ([]byte, error) + ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) } diff --git a/pkg/models/text_message.go b/pkg/models/text_message.go index 0d645c9..516fb9b 100644 --- a/pkg/models/text_message.go +++ b/pkg/models/text_message.go @@ -8,38 +8,61 @@ import ( ) type TextMessage struct { - Text string `json:"text" validate:"required"` - AllowPreview bool `json:"allowPreview"` + Text string + AllowPreview bool } type TextMessageConfigs struct { - Text string - AllowPreview bool + Text string `json:"text" validate:"required"` + AllowPreview bool `json:"allowPreview"` } func (m *TextMessage) SetText(text string) { m.Text = text } -func NewTextMessage(configs TextMessageConfigs) *TextMessage { +func NewTextMessage(configs TextMessageConfigs) (*TextMessage, error) { err := utils.GetValidator().Struct(configs) fmt.Println("error validating text message config", err) if err != nil { - return nil + return nil, fmt.Errorf("error validating text message config: %v", err) } return &TextMessage{ Text: configs.Text, AllowPreview: configs.AllowPreview, - } + }, nil } -func (m *TextMessage) ToJson() ([]byte, error) { - messageJson, err := json.Marshal(m) +// This function convert the TextMessage struct to WhatsApp API compatible JSON +func (m *TextMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + // validate the configs + err := utils.GetValidator().Struct(configs) + if err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + data := make(map[string]interface{}) + if configs.ReplyToMessageId != "" { + // add the context key to the json + data["context"] = map[string]interface{}{ + "message_id": configs.ReplyToMessageId, + } + } + + data["type"] = "text" + data["messaging_product"] = "whatsapp" + data["recipient_type"] = "individual" + data["text"] = map[string]interface{}{ + "body": m.Text, + "preview_url": m.AllowPreview, + } + data["to"] = configs.SendToPhoneNumber + + jsonToReturn, err := json.Marshal(data) + if err != nil { - // emit a error event here - return nil, err + return nil, fmt.Errorf("error marshalling json: %v", err) } - fmt.Println("text message json is", string(messageJson)) - return messageJson, nil + return jsonToReturn, nil } From 9b269d3a2d0a7ab6141c866cc2fcdc0052998839 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 01:49:56 +0530 Subject: [PATCH 08/30] feat: add contact message Signed-off-by: sarthakjdev --- example-chat-bot/main.go | 27 ++++- internal/manager/message_manager.go | 18 +-- pkg/models/base_message.go | 47 +++++--- pkg/models/contact_message.go | 165 +++++++++++++++++++++++++--- pkg/models/text_message.go | 37 +++---- 5 files changed, 232 insertions(+), 62 deletions(-) diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 09c1eb2..71ac923 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/sarthakjdev/wapi.go/internal/manager" wapi "github.com/sarthakjdev/wapi.go/pkg/client" wapiModels "github.com/sarthakjdev/wapi.go/pkg/models" @@ -13,7 +15,7 @@ func main() { // creating a client whatsappClient, err := wapi.New(wapi.ClientConfig{ PhoneNumberId: "113269274970227", - ApiAccessToken: "", + ApiAccessToken: "EABhCftGVaeIBOZCgZCHPf8eF7ZBayGCyVLvpGVbZC8oqjgZCzmhqVXn7TMiQ3JTQ77WxOE4K7DVIgFC8ZA7qSG2ANHQ3BbG09iXezHDHnu2iiC0K5VVcITzHZCMoy5aKkLhILxLNsOQ5s9nQg3dRj1VewJ1PuMJY2n9tcIP29qn0Ht30fpUirvG6tgE9CVdRlMHuHU54U4hFjqcNfbO4Q8jW1QvhKCZBv95do3YFd71v1ucZD", BusinessAccountId: "103043282674158", WebhookPath: "/webhook", WebhookSecret: "123456789", @@ -21,19 +23,38 @@ func main() { }) if err != nil { + fmt.Println("error creating client", err) return } // create a message textMessage, err := wapiModels.NewTextMessage(wapiModels.TextMessageConfigs{ - Text: "Hello, from wapi.go", - AllowPreview: true, + Text: "Hello, from wapi.go", + }) + + if err != nil { + fmt.Println("error creating text message", err) + return + } + + contact := wapiModels.NewContact(wapiModels.ContactName{ + FormattedName: "Sarthak Jain", + FirstName: "Sarthak", }) + contactMessage, err := wapiModels.NewContactMessage([]wapiModels.Contact{*contact}) + + if err != nil { + fmt.Println("error creating contact message", err) + return + } + if err != nil { + fmt.Println("error creating text message", err) return } // send the message whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) + whatsappClient.Message.Send(manager.SendMessageParams{Message: contactMessage, PhoneNumber: "919643500545"}) } diff --git a/internal/manager/message_manager.go b/internal/manager/message_manager.go index e76e760..9b8c1a1 100644 --- a/internal/manager/message_manager.go +++ b/internal/manager/message_manager.go @@ -1,6 +1,8 @@ package manager import ( + "fmt" + "github.com/sarthakjdev/wapi.go/pkg/models" ) @@ -20,28 +22,20 @@ type SendMessageParams struct { } // ! TODO: return the structured response from here -func (mm *MessageManager) Send(params SendMessageParams) { - // pass the phone number in this toJson method of every message +func (mm *MessageManager) Send(params SendMessageParams) (string, error) { body, err := params.Message.ToJson(models.ApiCompatibleJsonConverterConfigs{ SendToPhoneNumber: params.PhoneNumber, + // ReplyToMessageId: "wamid.HBgMOTE5NjQzNTAwNTQ1FQIAERgSQzVGOTlFMzExQ0VCQTg0MUFCAA==", }) if err != nil { // emit a error event here - return + return "", fmt.Errorf("error converting message to json: %v", err) } - mm.requester.requestCloudApi(requestCloudApiParams{ body: string(body), path: "/" + mm.requester.phoneNumberId + "/messages", }) - - // if err != nil { - // // emit a error event here - // return - // } - - // fmt.Println("Response from cloud api is", response) - + return "ok", nil } func (mm *MessageManager) Reply() { diff --git a/pkg/models/base_message.go b/pkg/models/base_message.go index 1063437..c4c72e4 100644 --- a/pkg/models/base_message.go +++ b/pkg/models/base_message.go @@ -1,24 +1,43 @@ package models -type MessageType string - -const ( - MessageTypeText MessageType = "text" - MessageTypeAudio MessageType = "audio" - MessageTypeDocument MessageType = "document" - MessageTypeContact MessageType = "contact" - MessageTypeLocation MessageType = "location" - MessageTypeImage MessageType = "image" - MessageTypeVideo MessageType = "video" - MessageTypeInteractive MessageType = "interactive" - MessageTypeTemplate MessageType = "template" -) - type ApiCompatibleJsonConverterConfigs struct { ReplyToMessageId string SendToPhoneNumber string } +type Context struct { + MessageId string `json:"message_id,omitempty"` +} + +// Base API Payload to send messages +type BaseMessagePayload struct { + Context *Context `json:"context,omitempty"` + To string `json:"to"` + Type string `json:"type"` + MessagingProduct string `json:"messaging_product"` + RecipientType string `json:"recipient_type"` +} + +func NewBaseMessagePayload(to, messageType string) BaseMessagePayload { + return BaseMessagePayload{ + To: to, + Type: messageType, + MessagingProduct: "whatsapp", + RecipientType: "individual", + } +} + +type MediaMessagePayload struct { +} + +type AudioMessagePayload struct { +} + +type TextMessageApiPayload struct { + BaseMessagePayload `json:",inline"` + Text TextMessageApiPayloadText `json:"text" validate:"required"` +} + type BaseMessage interface { ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) } diff --git a/pkg/models/contact_message.go b/pkg/models/contact_message.go index dbb65ee..0ebba24 100644 --- a/pkg/models/contact_message.go +++ b/pkg/models/contact_message.go @@ -7,28 +7,165 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +type AddressType string + +const ( + HomeAddress AddressType = "HOME" + WorkAddress AddressType = "WORK" +) + +type UrlType string + +const ( + HomeUrl AddressType = "HOME" + WorkUrl AddressType = "WORK" +) + +type EmailType string + +const ( + HomeEmail EmailType = "HOME" + WorkEmail EmailType = "WORK" +) + +type PhoneType string + +const ( + CellPhone PhoneType = "CELL" + MainPhone PhoneType = "MAIN" + IphonePhone PhoneType = "IPHONE" + HomePhone PhoneType = "HOME" + WorkPhone PhoneType = "WORK" +) + +type ContactAddress struct { + Street string `json:"street,omitempty"` + City string `json:"city,omitempty"` + State string `json:"state,omitempty"` + Zip string `json:"zip,omitempty"` + Country string `json:"country,omitempty"` + CountryCode string `json:"countryCode,omitempty"` + Type AddressType `json:"type" validate:"required"` +} + +type ContactName struct { + FormattedName string `json:"formatted_name" validate:"required"` + FirstName string `json:"first_name,omitempty"` + LastName string `json:"last_name,omitempty"` + MiddleName string `json:"middle_name,omitempty"` + Suffix string `json:"suffix,omitempty"` + Prefix string `json:"prefix,omitempty"` +} + +type ContactOrg struct { + Company string `json:"company,omitempty"` + Title string `json:"title,omitempty"` + Department string `json:"department,omitempty"` +} + +type ContactEmail struct { + Email string `json:"email,omitempty"` + Type EmailType `json:"type,omitempty"` +} + +type ContactPhone struct { + Phone string `json:"phone,omitempty"` + WaId string `json:"wa_id,omitempty"` + Type PhoneType `json:"type" validate:"required"` +} + +type ContactUrl struct { + Url string `json:"url" validate:"required"` + Type UrlType `json:"type" validate:"required"` +} + +type Contact struct { + Name ContactName `json:"name" validate:"required"` + Org ContactOrg `json:"org,omitempty"` + Addresses []ContactAddress `json:"addresses,omitempty"` + Urls []ContactUrl `json:"urls,omitempty"` + Emails []ContactEmail `json:"emails,omitempty"` + Phones []ContactPhone `json:"phones,omitempty"` + Birthday string `json:"birthday,omitempty"` +} + +func NewContact(name ContactName) *Contact { + return &Contact{ + Name: name, + } +} + +func (contact *ContactMessage) AddContact(params Contact) { + contact.Contacts = append(contact.Contacts, params) +} + +func (contact *Contact) SetFirstName() { +} + +func (contact *Contact) SetLastName() { +} + +func (contact *Contact) SetMiddleName() { +} + +func (contact *Contact) SetOrg(params ContactOrg) { + contact.Org = params +} + +func (contact *Contact) AddPhone(params ContactPhone) { + contact.Phones = append(contact.Phones, params) +} + +func (contact *Contact) AddEmail(params ContactEmail) { + contact.Emails = append(contact.Emails, params) +} + +func (contact *Contact) AddUrl(params ContactUrl) { + contact.Urls = append(contact.Urls, params) +} + +// ! TODO: add regex check here in the params +func (contact *Contact) SetBirthday(params string) { + contact.Birthday = params +} + type ContactMessage struct { + Contacts []Contact `json:"contacts" validate:"required"` } type ContactMessageConfigs struct { + Name string `json:"name" validate:"required"` } -func NewContactMessage(configs ContactMessageConfigs) *ContactMessage { - err := utils.GetValidator().Struct(configs) - fmt.Println("error validating text message config", err) - if err != nil { - return nil - } - return &ContactMessage{} +type ContactMessageApiPayload struct { + BaseMessagePayload + Contacts []Contact `json:"contacts" validate:"required"` } -func (m *ContactMessage) ToJson() ([]byte, error) { - messageJson, err := json.Marshal(m) - if err != nil { - // emit a error event here - return nil, err +func NewContactMessage(configs []Contact) (*ContactMessage, error) { + return &ContactMessage{ + Contacts: configs, + }, nil +} + +func (m *ContactMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + jsonData := ContactMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, "contacts"), + Contacts: m.Contacts, } - fmt.Println("text message json is", string(messageJson)) - return messageJson, nil + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + return jsonToReturn, nil } diff --git a/pkg/models/text_message.go b/pkg/models/text_message.go index 516fb9b..fdbbee4 100644 --- a/pkg/models/text_message.go +++ b/pkg/models/text_message.go @@ -14,7 +14,12 @@ type TextMessage struct { type TextMessageConfigs struct { Text string `json:"text" validate:"required"` - AllowPreview bool `json:"allowPreview"` + AllowPreview bool `json:"allowPreview,omitempty"` +} + +type TextMessageApiPayloadText struct { + Body string `json:"body" validate:"required"` + AllowPreview bool `json:"preview_url,omitempty"` } func (m *TextMessage) SetText(text string) { @@ -23,7 +28,6 @@ func (m *TextMessage) SetText(text string) { func NewTextMessage(configs TextMessageConfigs) (*TextMessage, error) { err := utils.GetValidator().Struct(configs) - fmt.Println("error validating text message config", err) if err != nil { return nil, fmt.Errorf("error validating text message config: %v", err) } @@ -35,30 +39,25 @@ func NewTextMessage(configs TextMessageConfigs) (*TextMessage, error) { // This function convert the TextMessage struct to WhatsApp API compatible JSON func (m *TextMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { - // validate the configs - err := utils.GetValidator().Struct(configs) - if err != nil { + if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) } - data := make(map[string]interface{}) - if configs.ReplyToMessageId != "" { - // add the context key to the json - data["context"] = map[string]interface{}{ - "message_id": configs.ReplyToMessageId, - } + jsonData := TextMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, "text"), + Text: TextMessageApiPayloadText{ + Body: m.Text, + AllowPreview: m.AllowPreview, + }, } - data["type"] = "text" - data["messaging_product"] = "whatsapp" - data["recipient_type"] = "individual" - data["text"] = map[string]interface{}{ - "body": m.Text, - "preview_url": m.AllowPreview, + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } } - data["to"] = configs.SendToPhoneNumber - jsonToReturn, err := json.Marshal(data) + jsonToReturn, err := json.Marshal(jsonData) if err != nil { return nil, fmt.Errorf("error marshalling json: %v", err) From d2e29167c050ebd0a61e09e5bee4cd138b7672e3 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 12:18:39 +0530 Subject: [PATCH 09/30] feat: add location message Signed-off-by: sarthakjdev --- example-chat-bot/main.go | 14 +++-- internal/manager/message_manager.go | 8 +-- pkg/components/base_message.go | 47 ++++++++++++++ pkg/{models => components}/contact_message.go | 13 ++-- .../interactive_message.go | 2 +- pkg/components/location_message.go | 63 +++++++++++++++++++ pkg/{models => components}/media_message.go | 2 +- pkg/{models => components}/reaction.go | 2 +- pkg/components/template_message.go | 15 +++++ pkg/{models => components}/text_message.go | 9 ++- pkg/models/base_message.go | 43 ------------- pkg/models/location_message.go | 4 -- pkg/models/template_message.go | 4 -- 13 files changed, 156 insertions(+), 70 deletions(-) create mode 100644 pkg/components/base_message.go rename pkg/{models => components}/contact_message.go (92%) rename pkg/{models => components}/interactive_message.go (67%) create mode 100644 pkg/components/location_message.go rename pkg/{models => components}/media_message.go (89%) rename pkg/{models => components}/reaction.go (63%) create mode 100644 pkg/components/template_message.go rename pkg/{models => components}/text_message.go (88%) delete mode 100644 pkg/models/base_message.go delete mode 100644 pkg/models/location_message.go delete mode 100644 pkg/models/template_message.go diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 71ac923..6c14c0e 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -5,7 +5,7 @@ import ( "github.com/sarthakjdev/wapi.go/internal/manager" wapi "github.com/sarthakjdev/wapi.go/pkg/client" - wapiModels "github.com/sarthakjdev/wapi.go/pkg/models" + wapiComponents "github.com/sarthakjdev/wapi.go/pkg/components" ) func main() { @@ -28,7 +28,7 @@ func main() { } // create a message - textMessage, err := wapiModels.NewTextMessage(wapiModels.TextMessageConfigs{ + textMessage, err := wapiComponents.NewTextMessage(wapiComponents.TextMessageConfigs{ Text: "Hello, from wapi.go", }) @@ -37,24 +37,28 @@ func main() { return } - contact := wapiModels.NewContact(wapiModels.ContactName{ + contact := wapiComponents.NewContact(wapiComponents.ContactName{ FormattedName: "Sarthak Jain", FirstName: "Sarthak", }) - contactMessage, err := wapiModels.NewContactMessage([]wapiModels.Contact{*contact}) + contactMessage, err := wapiComponents.NewContactMessage([]wapiComponents.Contact{*contact}) if err != nil { fmt.Println("error creating contact message", err) return } + locationMessage, err := wapiComponents.NewLocationMessage(28.7041, 77.1025) + if err != nil { - fmt.Println("error creating text message", err) + fmt.Println("error creating location message", err) return } // send the message whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) whatsappClient.Message.Send(manager.SendMessageParams{Message: contactMessage, PhoneNumber: "919643500545"}) + whatsappClient.Message.Send(manager.SendMessageParams{Message: locationMessage, PhoneNumber: "919643500545"}) + } diff --git a/internal/manager/message_manager.go b/internal/manager/message_manager.go index 9b8c1a1..8eeb5dd 100644 --- a/internal/manager/message_manager.go +++ b/internal/manager/message_manager.go @@ -3,7 +3,7 @@ package manager import ( "fmt" - "github.com/sarthakjdev/wapi.go/pkg/models" + "github.com/sarthakjdev/wapi.go/pkg/components" ) type MessageManager struct { @@ -17,18 +17,18 @@ func NewMessageManager(requester requestClient) *MessageManager { } type SendMessageParams struct { - Message models.BaseMessage + Message components.BaseMessage PhoneNumber string } // ! TODO: return the structured response from here func (mm *MessageManager) Send(params SendMessageParams) (string, error) { - body, err := params.Message.ToJson(models.ApiCompatibleJsonConverterConfigs{ + body, err := params.Message.ToJson(components.ApiCompatibleJsonConverterConfigs{ SendToPhoneNumber: params.PhoneNumber, // ReplyToMessageId: "wamid.HBgMOTE5NjQzNTAwNTQ1FQIAERgSQzVGOTlFMzExQ0VCQTg0MUFCAA==", }) if err != nil { - // emit a error event here + // ! TODO: emit a error event here return "", fmt.Errorf("error converting message to json: %v", err) } mm.requester.requestCloudApi(requestCloudApiParams{ diff --git a/pkg/components/base_message.go b/pkg/components/base_message.go new file mode 100644 index 0000000..bbedc94 --- /dev/null +++ b/pkg/components/base_message.go @@ -0,0 +1,47 @@ +package components + +type MessageType string + +const ( + LocationMessageType MessageType = "location" + AudioMessageType MessageType = "audio" + VideoMessageType MessageType = "video" + DocumentMessageType MessageType = "document" + TextMessageType MessageType = "text" + ContactMessageType MessageType = "contacts" + InteractiveMessageType MessageType = "interactive" + TemplateMessageType MessageType = "template" + ReactionMessageType MessageType = "reaction" + StickerMessageType MessageType = "sticker" +) + +type ApiCompatibleJsonConverterConfigs struct { + ReplyToMessageId string + SendToPhoneNumber string +} + +type Context struct { + MessageId string `json:"message_id,omitempty"` +} + +// Base API Payload to send messages +type BaseMessagePayload struct { + Context *Context `json:"context,omitempty"` + To string `json:"to"` + Type MessageType `json:"type"` + MessagingProduct string `json:"messaging_product"` + RecipientType string `json:"recipient_type"` +} + +func NewBaseMessagePayload(to string, messageType MessageType) BaseMessagePayload { + return BaseMessagePayload{ + To: to, + Type: messageType, + MessagingProduct: "whatsapp", + RecipientType: "individual", + } +} + +type BaseMessage interface { + ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) +} diff --git a/pkg/models/contact_message.go b/pkg/components/contact_message.go similarity index 92% rename from pkg/models/contact_message.go rename to pkg/components/contact_message.go index 0ebba24..3afdcef 100644 --- a/pkg/models/contact_message.go +++ b/pkg/components/contact_message.go @@ -1,4 +1,4 @@ -package models +package components import ( "encoding/json" @@ -99,13 +99,16 @@ func (contact *ContactMessage) AddContact(params Contact) { contact.Contacts = append(contact.Contacts, params) } -func (contact *Contact) SetFirstName() { +func (contact *Contact) SetFirstName(firstName string) { + contact.Name.FirstName = firstName } -func (contact *Contact) SetLastName() { +func (contact *Contact) SetLastName(lastName string) { + contact.Name.LastName = lastName } -func (contact *Contact) SetMiddleName() { +func (contact *Contact) SetMiddleName(middleName string) { + contact.Name.MiddleName = middleName } func (contact *Contact) SetOrg(params ContactOrg) { @@ -153,7 +156,7 @@ func (m *ContactMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]by return nil, fmt.Errorf("error validating configs: %v", err) } jsonData := ContactMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, "contacts"), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, ContactMessageType), Contacts: m.Contacts, } diff --git a/pkg/models/interactive_message.go b/pkg/components/interactive_message.go similarity index 67% rename from pkg/models/interactive_message.go rename to pkg/components/interactive_message.go index 4a373dd..fcc0faf 100644 --- a/pkg/models/interactive_message.go +++ b/pkg/components/interactive_message.go @@ -1,4 +1,4 @@ -package models +package components type BaseInteractiveMessage struct { } diff --git a/pkg/components/location_message.go b/pkg/components/location_message.go new file mode 100644 index 0000000..77222d3 --- /dev/null +++ b/pkg/components/location_message.go @@ -0,0 +1,63 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type LocationMessage struct { + Latitude float64 `json:"latitude" validate:"required"` + Longitude float64 `json:"longitude" validate:"required"` + Address string `json:"address,omitempty"` + Name string `json:"name,omitempty"` +} + +type LocationMessageApiPayload struct { + BaseMessagePayload + Location LocationMessage `json:"location" validate:"required"` +} + +func NewLocationMessage(latitude float64, longitude float64) (*LocationMessage, error) { + + return &LocationMessage{ + Latitude: latitude, + Longitude: longitude, + }, nil + +} + +func (location *LocationMessage) SetAddress(params string) { + location.Address = params +} + +func (location *LocationMessage) SetName(params string) { + location.Name = params +} + +func (location *LocationMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := LocationMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, LocationMessageType), + Location: *location, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} diff --git a/pkg/models/media_message.go b/pkg/components/media_message.go similarity index 89% rename from pkg/models/media_message.go rename to pkg/components/media_message.go index 84c435a..d7fb095 100644 --- a/pkg/models/media_message.go +++ b/pkg/components/media_message.go @@ -1,4 +1,4 @@ -package models +package components type BaseMediaMessage struct { } diff --git a/pkg/models/reaction.go b/pkg/components/reaction.go similarity index 63% rename from pkg/models/reaction.go rename to pkg/components/reaction.go index 8af3a8b..0e1b755 100644 --- a/pkg/models/reaction.go +++ b/pkg/components/reaction.go @@ -1,4 +1,4 @@ -package models +package components type ReactionMessage struct { } diff --git a/pkg/components/template_message.go b/pkg/components/template_message.go new file mode 100644 index 0000000..4a38347 --- /dev/null +++ b/pkg/components/template_message.go @@ -0,0 +1,15 @@ +package components + +type TemplateMessage struct { +} + +type TemplateMessageApiPayload struct { + BaseMessagePayload + Template TemplateMessage `json:"template" validate:"required"` +} + +func NewTemplateMessage() (*TemplateMessage, error) { + + return &TemplateMessage{}, nil + +} diff --git a/pkg/models/text_message.go b/pkg/components/text_message.go similarity index 88% rename from pkg/models/text_message.go rename to pkg/components/text_message.go index fdbbee4..22cfc4a 100644 --- a/pkg/models/text_message.go +++ b/pkg/components/text_message.go @@ -1,4 +1,4 @@ -package models +package components import ( "encoding/json" @@ -26,6 +26,11 @@ func (m *TextMessage) SetText(text string) { m.Text = text } +type TextMessageApiPayload struct { + BaseMessagePayload `json:",inline"` + Text TextMessageApiPayloadText `json:"text" validate:"required"` +} + func NewTextMessage(configs TextMessageConfigs) (*TextMessage, error) { err := utils.GetValidator().Struct(configs) if err != nil { @@ -44,7 +49,7 @@ func (m *TextMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, } jsonData := TextMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, "text"), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, TextMessageType), Text: TextMessageApiPayloadText{ Body: m.Text, AllowPreview: m.AllowPreview, diff --git a/pkg/models/base_message.go b/pkg/models/base_message.go deleted file mode 100644 index c4c72e4..0000000 --- a/pkg/models/base_message.go +++ /dev/null @@ -1,43 +0,0 @@ -package models - -type ApiCompatibleJsonConverterConfigs struct { - ReplyToMessageId string - SendToPhoneNumber string -} - -type Context struct { - MessageId string `json:"message_id,omitempty"` -} - -// Base API Payload to send messages -type BaseMessagePayload struct { - Context *Context `json:"context,omitempty"` - To string `json:"to"` - Type string `json:"type"` - MessagingProduct string `json:"messaging_product"` - RecipientType string `json:"recipient_type"` -} - -func NewBaseMessagePayload(to, messageType string) BaseMessagePayload { - return BaseMessagePayload{ - To: to, - Type: messageType, - MessagingProduct: "whatsapp", - RecipientType: "individual", - } -} - -type MediaMessagePayload struct { -} - -type AudioMessagePayload struct { -} - -type TextMessageApiPayload struct { - BaseMessagePayload `json:",inline"` - Text TextMessageApiPayloadText `json:"text" validate:"required"` -} - -type BaseMessage interface { - ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) -} diff --git a/pkg/models/location_message.go b/pkg/models/location_message.go deleted file mode 100644 index 987d7af..0000000 --- a/pkg/models/location_message.go +++ /dev/null @@ -1,4 +0,0 @@ -package models - -type LocateMessage struct { -} diff --git a/pkg/models/template_message.go b/pkg/models/template_message.go deleted file mode 100644 index cec858c..0000000 --- a/pkg/models/template_message.go +++ /dev/null @@ -1,4 +0,0 @@ -package models - -type TemplateMessage struct { -} From 915b09fa9e033b1ddc500ed76d8278843d055947 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 12:21:42 +0530 Subject: [PATCH 10/30] chore: wapi.js -> wapi.go Signed-off-by: sarthakjdev --- .github/COMMIT_CONVENTION.md | 4 ++-- .github/workflows/formatTag/formatTag.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/COMMIT_CONVENTION.md b/.github/COMMIT_CONVENTION.md index 0b6654b..21461c3 100644 --- a/.github/COMMIT_CONVENTION.md +++ b/.github/COMMIT_CONVENTION.md @@ -1,6 +1,6 @@ # ๐Ÿ“˜ Commit and Branch Conventions -At `wapi.js`, we follow a strict set of conventions for commit messages and branch names to ensure that our repository stays organized, and our commit history remains crystal clear. Here's a guide on our conventions: +At `wapi.go`, we follow a strict set of conventions for commit messages and branch names to ensure that our repository stays organized, and our commit history remains crystal clear. Here's a guide on our conventions: --- @@ -93,6 +93,6 @@ fix/login-issue --- -๐Ÿ™Œ Thanks for contributing to `wapi.js`! By adhering to these conventions, we're making our repository a better place. If you're new, welcome aboard, and if you've been here, thanks for sticking around! +๐Ÿ™Œ Thanks for contributing to `wapi.go`! By adhering to these conventions, we're making our repository a better place. If you're new, welcome aboard, and if you've been here, thanks for sticking around! --- diff --git a/.github/workflows/formatTag/formatTag.js b/.github/workflows/formatTag/formatTag.js index 0f847d8..0472645 100644 --- a/.github/workflows/formatTag/formatTag.js +++ b/.github/workflows/formatTag/formatTag.js @@ -8,7 +8,7 @@ export function formatTag(tag) { const isSubPackage = typeof parsed.groups.package === "string"; const pkg = isSubPackage ? parsed.groups.package - : parsedPackage?.groups?.package ?? "wapi.js"; + : parsedPackage?.groups?.package ?? "wapi.go"; const semver = parsed.groups.semver; return { From 8172ceff05acc515cc8a42f764dd4b5c749e3cfa Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 13:01:13 +0530 Subject: [PATCH 11/30] docs: add readme Signed-off-by: sarthakjdev --- README.md | 98 +++++++++++++++++++++++++++++++++++++++++++- pkg/client/client.go | 2 +- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3649845..9cd72dc 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,99 @@ +
+
+

+@wapijs/wapi.js +

+
+
+ +## ๐Ÿ“Œ Status + +Beta Version - This library is not stable right now. It is currently in beta version. Report issues [here](https://github.com/sarthakjdev/wapi.go/issues). + ## ๐Ÿ“– About -a Golang module, designed to interact with the WhatsApp cloud API in a user-friendly manner. +Wapi.js is a JavaScript module, written in TypeScript, designed to interact with the WhatsApp cloud API in a user-friendly manner. + +## โœจ Features + +- Single Client Model +- Send Messages with the least configuration +- Event Listener for Notifications (support both User and System Notifications) +- Upload Media to WhatsApp servers +- Reply and React to incoming messages. + +## ๐Ÿ’ป Installation + +This assumes you already have a working Go environment, if not please see +[this page](https://golang.org/doc/install) first. + +`go get` _will always pull the latest tagged release from the master branch._ + +```sh +go get github.com/sarthakjdev/wapi.go +``` + +> Note: This library is not affiliated with the official WhatsApp Cloud API or does not act as any official solution provided the the Meta Inclusive Private Limited, this is just a open source library built for developers to support them in building whatsapp cloud api based chat bots easily. + +## ๐Ÿš€ Usage + +You can check out the example WhatsApp bot here. [Example Chatbot](./example-chat-bot/) + +# Example Usage + +Import the package into your project. +This repository has three packages exported: + +- github.com/sarthakjdev/wapi.go/components +- github.com/sarthakjdev/wapi.go/wapi/wapi +- github.com/sarthakjdev/wapi.go/wapi/events + +```go +import "github.com/sarthakjdev/wapi.go/wapi/wapi" +``` + +Construct a new Wapi Client to access the managers in order to send messages and listen to incoming notifications. + +```go +whatsappClient, err := wapi.New(wapi.ClientConfig{ + PhoneNumberId: "", + ApiAccessToken: "", + BusinessAccountId: "", + WebhookPath: "/webhook", + WebhookSecret: "", + WebhookServerPort: 8080, + }) +``` + +## ๐Ÿ”— References + +- **Message Structures**: Refer to the WhatsApp Docs [here](https://developers.facebook.com/docs/whatsapp/cloud-api/reference/messages). + +- **Notification Payloads**: Details can be found [here](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/components). + + + +## ๐Ÿค Contribution Guidelines + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +For detailed guidelines, check [Contributing.md](./CONTRIBUTING.md). + +## ๐Ÿ“œ License + +Distributed under the Apache 2.0 License. View [LICENSE](./LICENSE). + +## ๐Ÿ“ž Contact + +- [Sarthak Jain](https://sarthakjdev.com) +- Email: sarthak@softlancer.co +- [Twitter](https://twitter.com/sarthakjdev) | [LinkedIn](https://www.linkedin.com/in/sarthakjdev) + +Note: This library is part of an open-source product-building initiative by [Softlancer](https://github.com/softlancerhq), and this repository will soon be moved under the same organization. diff --git a/pkg/client/client.go b/pkg/client/client.go index 21f865c..460c458 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -3,7 +3,7 @@ package wapi import ( "fmt" - manager "github.com/sarthakjdev/wapi.go/internal/manager" + "github.com/sarthakjdev/wapi.go/internal/manager" "github.com/sarthakjdev/wapi.go/internal/webhook" "github.com/sarthakjdev/wapi.go/utils" ) From 0c55131be2b69f5e209b3a1ed3c194dcf726b0e9 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 14:33:09 +0530 Subject: [PATCH 12/30] feat: add media messages Signed-off-by: sarthakjdev --- pkg/components/audio_message.go | 64 +++++++++++++++++++++++++++ pkg/components/base_message.go | 1 + pkg/components/document_message.go | 53 +++++++++++++++++++++++ pkg/components/image_message.go | 69 ++++++++++++++++++++++++++++++ pkg/components/media_message.go | 16 ------- pkg/components/reaction.go | 4 -- pkg/components/reaction_message.go | 15 +++++++ pkg/components/sticker_message.go | 69 ++++++++++++++++++++++++++++++ pkg/components/video_message.go | 69 ++++++++++++++++++++++++++++++ 9 files changed, 340 insertions(+), 20 deletions(-) create mode 100644 pkg/components/audio_message.go create mode 100644 pkg/components/document_message.go create mode 100644 pkg/components/image_message.go delete mode 100644 pkg/components/media_message.go delete mode 100644 pkg/components/reaction.go create mode 100644 pkg/components/reaction_message.go create mode 100644 pkg/components/sticker_message.go create mode 100644 pkg/components/video_message.go diff --git a/pkg/components/audio_message.go b/pkg/components/audio_message.go new file mode 100644 index 0000000..2158c86 --- /dev/null +++ b/pkg/components/audio_message.go @@ -0,0 +1,64 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type AudioMessage struct { + Id string `json:"id,omitempty"` + Link string `json:"link,omitempty"` +} + +type AudioMessageApiPayload struct { + BaseMessagePayload + Audio AudioMessage `json:"audio" validate:"required"` +} + +type AudioMessageConfigs = AudioMessage + +func NewAudioMessage(params AudioMessageConfigs) (*AudioMessage, error) { + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + idSet := params.Id != "" + linkSet := params.Link != "" + + if idSet && linkSet { + return nil, fmt.Errorf("only one of ID or Link can be provided") + } + if !idSet && !linkSet { + return nil, fmt.Errorf("either ID or Link must be provided") + } + + return &AudioMessage{}, nil +} + +func (audio *AudioMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := AudioMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, AudioMessageType), + Audio: *audio, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} diff --git a/pkg/components/base_message.go b/pkg/components/base_message.go index bbedc94..6e2607a 100644 --- a/pkg/components/base_message.go +++ b/pkg/components/base_message.go @@ -13,6 +13,7 @@ const ( TemplateMessageType MessageType = "template" ReactionMessageType MessageType = "reaction" StickerMessageType MessageType = "sticker" + ImageMessageType MessageType = "image" ) type ApiCompatibleJsonConverterConfigs struct { diff --git a/pkg/components/document_message.go b/pkg/components/document_message.go new file mode 100644 index 0000000..713c4aa --- /dev/null +++ b/pkg/components/document_message.go @@ -0,0 +1,53 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type DocumentMessage struct { +} + +type DocumentMessageApiPayload struct { + BaseMessagePayload + Document DocumentMessage `json:"document" validate:"required"` +} + +type DocumentMessageConfigs struct { +} + +func NewDocumentMessage(params DocumentMessageConfigs) (*DocumentMessage, error) { + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + return &DocumentMessage{}, nil +} + +func (dm *DocumentMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := DocumentMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, DocumentMessageType), + Document: *dm, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} diff --git a/pkg/components/image_message.go b/pkg/components/image_message.go new file mode 100644 index 0000000..04f9927 --- /dev/null +++ b/pkg/components/image_message.go @@ -0,0 +1,69 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type ImageMessage struct { + Id string `json:"id,omitempty"` + Link string `json:"link,omitempty"` + Caption string `json:"caption,omitempty"` +} + +type ImageMessageApiPayload struct { + BaseMessagePayload + Image ImageMessage `json:"image" validate:"required"` +} + +type ImageMessageConfigs = ImageMessage + +func (image *ImageMessage) SetCaption(params string) { + image.Caption = params +} + +func NewImageMessage(params ImageMessageConfigs) (*ImageMessage, error) { + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + idSet := params.Id != "" + linkSet := params.Link != "" + + if idSet && linkSet { + return nil, fmt.Errorf("only one of ID or Link can be provided") + } + if !idSet && !linkSet { + return nil, fmt.Errorf("either ID or Link must be provided") + } + + return &ImageMessage{}, nil +} + +func (image *ImageMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := ImageMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, ImageMessageType), + Image: *image, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} diff --git a/pkg/components/media_message.go b/pkg/components/media_message.go deleted file mode 100644 index d7fb095..0000000 --- a/pkg/components/media_message.go +++ /dev/null @@ -1,16 +0,0 @@ -package components - -type BaseMediaMessage struct { -} - -type AudioMessage struct { -} - -type ImageMessage struct { -} - -type VideoMessage struct { -} - -type DocumentMessage struct { -} diff --git a/pkg/components/reaction.go b/pkg/components/reaction.go deleted file mode 100644 index 0e1b755..0000000 --- a/pkg/components/reaction.go +++ /dev/null @@ -1,4 +0,0 @@ -package components - -type ReactionMessage struct { -} diff --git a/pkg/components/reaction_message.go b/pkg/components/reaction_message.go new file mode 100644 index 0000000..a14e467 --- /dev/null +++ b/pkg/components/reaction_message.go @@ -0,0 +1,15 @@ +package components + +type ReactionMessage struct { +} + +func NewReactionMessage() (*ReactionMessage, error) { + return &ReactionMessage{}, nil +} + +func (*ReactionMessage) ToJson() { + +} + + + diff --git a/pkg/components/sticker_message.go b/pkg/components/sticker_message.go new file mode 100644 index 0000000..25c3532 --- /dev/null +++ b/pkg/components/sticker_message.go @@ -0,0 +1,69 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type StickerMessage struct { + Id string `json:"id,omitempty"` + Link string `json:"link,omitempty"` +} + +type StickerMessageApiPayload struct { + BaseMessagePayload + Sticker StickerMessage `json:"sticker" validate:"required"` +} + +type StickerMessageConfigs struct { + Id string `json:"id,omitempty"` + Link string `json:"link,omitempty"` +} + +func NewStickerMessage(params *StickerMessageConfigs) (*StickerMessage, error) { + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + idSet := params.Id != "" + linkSet := params.Link != "" + + if idSet && linkSet { + return nil, fmt.Errorf("only one of ID or Link can be provided") + } + if !idSet && !linkSet { + return nil, fmt.Errorf("either ID or Link must be provided") + } + + return &StickerMessage{ + Id: params.Id, + Link: params.Link, + }, nil +} + +func (sticker *StickerMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := StickerMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, StickerMessageType), + Sticker: *sticker, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} diff --git a/pkg/components/video_message.go b/pkg/components/video_message.go new file mode 100644 index 0000000..1c960ac --- /dev/null +++ b/pkg/components/video_message.go @@ -0,0 +1,69 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type VideoMessage struct { + Id string `json:"id,omitempty"` + Link string `json:"link,omitempty"` + Caption string `json:"caption,omitempty"` +} + +type VideoMessageApiPayload struct { + BaseMessagePayload + Video VideoMessage `json:"video" validate:"required"` +} + +type VideoMessageConfigs = VideoMessage + +func NewVideoMessage(params VideoMessageConfigs) (*VideoMessage, error) { + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + idSet := params.Id != "" + linkSet := params.Link != "" + + if idSet && linkSet { + return nil, fmt.Errorf("only one of ID or Link can be provided") + } + if !idSet && !linkSet { + return nil, fmt.Errorf("either ID or Link must be provided") + } + + return &VideoMessage{}, nil +} + +func (video *VideoMessage) SetCaption(params string) { + video.Caption = params +} + +func (video *VideoMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := VideoMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, VideoMessageType), + Video: *video, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} From fc823964caa317ce0dccddac43ee16a346af3424 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 14:45:57 +0530 Subject: [PATCH 13/30] feat: add reaction message Signed-off-by: sarthakjdev --- example-chat-bot/main.go | 11 +++++++ pkg/components/reaction_message.go | 48 ++++++++++++++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 6c14c0e..4b4fd61 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -56,9 +56,20 @@ func main() { return } + reactionMessage, err := wapiComponents.NewReactionMessage(wapiComponents.ReactionMessageParams{ + MessageId: "wamid.HBgMOTE5NjQzNTAwNTQ1FQIAERgSQzVGOTlFMzExQ0VCQTg0MUFCAA==", + Emoji: "๐Ÿ˜", + }) + + if err != nil { + fmt.Println("error creating reaction message", err) + return + } + // send the message whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) whatsappClient.Message.Send(manager.SendMessageParams{Message: contactMessage, PhoneNumber: "919643500545"}) whatsappClient.Message.Send(manager.SendMessageParams{Message: locationMessage, PhoneNumber: "919643500545"}) + whatsappClient.Message.Send(manager.SendMessageParams{Message: reactionMessage, PhoneNumber: "919643500545"}) } diff --git a/pkg/components/reaction_message.go b/pkg/components/reaction_message.go index a14e467..dd0645f 100644 --- a/pkg/components/reaction_message.go +++ b/pkg/components/reaction_message.go @@ -1,15 +1,57 @@ package components +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + type ReactionMessage struct { + MessageId string `json:"message_id" validate:"required"` + Emoji string `json:"emoji" validate:"required"` } -func NewReactionMessage() (*ReactionMessage, error) { - return &ReactionMessage{}, nil +type ReactionMessageParams = ReactionMessage + +type ReactionMessageApiPayload struct { + BaseMessagePayload + Reaction ReactionMessage `json:"reaction" validate:"required"` } -func (*ReactionMessage) ToJson() { +func NewReactionMessage(params ReactionMessageParams) (*ReactionMessage, error) { + + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + return &ReactionMessage{ + MessageId: params.MessageId, + Emoji: params.Emoji, + }, nil } +func (reaction *ReactionMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + jsonData := ReactionMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, ReactionMessageType), + Reaction: *reaction, + } + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil +} From 8f1d5393f9832c9192165bad369fbd31c875d777 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 16:50:47 +0530 Subject: [PATCH 14/30] feat: list interative message Signed-off-by: sarthakjdev --- example-chat-bot/main.go | 76 ++++++++--- pkg/components/audio_message.go | 2 +- pkg/components/base_message.go | 22 ++-- pkg/components/contact_message.go | 2 +- pkg/components/document_message.go | 2 +- pkg/components/image_message.go | 2 +- pkg/components/interactive_message.go | 10 +- pkg/components/list_message.go | 128 +++++++++++++++++++ pkg/components/location_message.go | 2 +- pkg/components/product_list_message.go | 53 ++++++++ pkg/components/product_message.go | 52 ++++++++ pkg/components/quick_reply_button_message.go | 44 +++++++ pkg/components/reaction_message.go | 2 +- pkg/components/sticker_message.go | 2 +- pkg/components/text_message.go | 12 +- pkg/components/video_message.go | 2 +- 16 files changed, 366 insertions(+), 47 deletions(-) create mode 100644 pkg/components/list_message.go create mode 100644 pkg/components/product_list_message.go create mode 100644 pkg/components/product_message.go create mode 100644 pkg/components/quick_reply_button_message.go diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 4b4fd61..6e545f6 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -32,44 +32,80 @@ func main() { Text: "Hello, from wapi.go", }) + // if err != nil { + // fmt.Println("error creating text message", err) + // return + // } + + // contactMessage, err := wapiComponents.NewContactMessage([]wapiComponents.Contact{ + // *wapiComponents.NewContact(wapiComponents.ContactName{ + // FormattedName: "Sarthak Jain", + // FirstName: "Sarthak", + // })}) + + // if err != nil { + // fmt.Println("error creating contact message", err) + // return + // } + + // locationMessage, err := wapiComponents.NewLocationMessage(28.7041, 77.1025) + + // if err != nil { + // fmt.Println("error creating location message", err) + // return + // } + + // reactionMessage, err := wapiComponents.NewReactionMessage(wapiComponents.ReactionMessageParams{ + // MessageId: "wamid.HBgMOTE5NjQzNTAwNTQ1FQIAERgSQzVGOTlFMzExQ0VCQTg0MUFCAA==", + // Emoji: "๐Ÿ˜", + // }) + + // if err != nil { + // fmt.Println("error creating reaction message", err) + // return + // } + + // send the message + whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) + // whatsappClient.Message.Send(manager.SendMessageParams{Message: contactMessage, PhoneNumber: "919643500545"}) + // whatsappClient.Message.Send(manager.SendMessageParams{Message: locationMessage, PhoneNumber: "919643500545"}) + // whatsappClient.Message.Send(manager.SendMessageParams{Message: reactionMessage, PhoneNumber: "919643500545"}) + + listMessage, err := wapiComponents.NewListMessage(wapiComponents.ListMessageParams{ + ButtonText: "Button 1", + BodyText: "Body 1", + }) + if err != nil { - fmt.Println("error creating text message", err) + fmt.Println("error creating list message", err) return } - contact := wapiComponents.NewContact(wapiComponents.ContactName{ - FormattedName: "Sarthak Jain", - FirstName: "Sarthak", - }) - - contactMessage, err := wapiComponents.NewContactMessage([]wapiComponents.Contact{*contact}) + listSectionRow, err := wapiComponents.NewListSectionRow("1", "Title 1", "Description 1") if err != nil { - fmt.Println("error creating contact message", err) + fmt.Println("error creating list section row", err) return } - locationMessage, err := wapiComponents.NewLocationMessage(28.7041, 77.1025) + listSection, err := wapiComponents.NewListSection("Section1") if err != nil { - fmt.Println("error creating location message", err) + fmt.Println("error creating list section row", err) return } - reactionMessage, err := wapiComponents.NewReactionMessage(wapiComponents.ReactionMessageParams{ - MessageId: "wamid.HBgMOTE5NjQzNTAwNTQ1FQIAERgSQzVGOTlFMzExQ0VCQTg0MUFCAA==", - Emoji: "๐Ÿ˜", - }) + listSection.AddRow(listSectionRow) + listMessage.AddSection(listSection) + jsonData, err := listMessage.ToJson(wapiComponents.ApiCompatibleJsonConverterConfigs{SendToPhoneNumber: "919643500545"}) if err != nil { - fmt.Println("error creating reaction message", err) + fmt.Println("error converting message to json", err) return } - // send the message - whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) - whatsappClient.Message.Send(manager.SendMessageParams{Message: contactMessage, PhoneNumber: "919643500545"}) - whatsappClient.Message.Send(manager.SendMessageParams{Message: locationMessage, PhoneNumber: "919643500545"}) - whatsappClient.Message.Send(manager.SendMessageParams{Message: reactionMessage, PhoneNumber: "919643500545"}) + fmt.Println(string(jsonData)) + + whatsappClient.Message.Send(manager.SendMessageParams{Message: listMessage, PhoneNumber: "919643500545"}) } diff --git a/pkg/components/audio_message.go b/pkg/components/audio_message.go index 2158c86..557bcb8 100644 --- a/pkg/components/audio_message.go +++ b/pkg/components/audio_message.go @@ -43,7 +43,7 @@ func (audio *AudioMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([] } jsonData := AudioMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, AudioMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeAudio), Audio: *audio, } diff --git a/pkg/components/base_message.go b/pkg/components/base_message.go index 6e2607a..80688b9 100644 --- a/pkg/components/base_message.go +++ b/pkg/components/base_message.go @@ -3,17 +3,17 @@ package components type MessageType string const ( - LocationMessageType MessageType = "location" - AudioMessageType MessageType = "audio" - VideoMessageType MessageType = "video" - DocumentMessageType MessageType = "document" - TextMessageType MessageType = "text" - ContactMessageType MessageType = "contacts" - InteractiveMessageType MessageType = "interactive" - TemplateMessageType MessageType = "template" - ReactionMessageType MessageType = "reaction" - StickerMessageType MessageType = "sticker" - ImageMessageType MessageType = "image" + MessageTypeLocation MessageType = "location" + MessageTypeAudio MessageType = "audio" + MessageTypeVideo MessageType = "video" + MessageTypeDocument MessageType = "document" + MessageTypeText MessageType = "text" + MessageTypeContact MessageType = "contacts" + MessageTypeInteractive MessageType = "interactive" + MessageTypeTemplate MessageType = "template" + MessageTypeReaction MessageType = "reaction" + MessageTypeSticker MessageType = "sticker" + MessageTypeImage MessageType = "image" ) type ApiCompatibleJsonConverterConfigs struct { diff --git a/pkg/components/contact_message.go b/pkg/components/contact_message.go index 3afdcef..ec4920f 100644 --- a/pkg/components/contact_message.go +++ b/pkg/components/contact_message.go @@ -156,7 +156,7 @@ func (m *ContactMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]by return nil, fmt.Errorf("error validating configs: %v", err) } jsonData := ContactMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, ContactMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeContact), Contacts: m.Contacts, } diff --git a/pkg/components/document_message.go b/pkg/components/document_message.go index 713c4aa..da8eac0 100644 --- a/pkg/components/document_message.go +++ b/pkg/components/document_message.go @@ -32,7 +32,7 @@ func (dm *DocumentMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([] } jsonData := DocumentMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, DocumentMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeDocument), Document: *dm, } diff --git a/pkg/components/image_message.go b/pkg/components/image_message.go index 04f9927..8b0185a 100644 --- a/pkg/components/image_message.go +++ b/pkg/components/image_message.go @@ -48,7 +48,7 @@ func (image *ImageMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([] } jsonData := ImageMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, ImageMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeImage), Image: *image, } diff --git a/pkg/components/interactive_message.go b/pkg/components/interactive_message.go index fcc0faf..4cd8a0a 100644 --- a/pkg/components/interactive_message.go +++ b/pkg/components/interactive_message.go @@ -1,4 +1,10 @@ package components -type BaseInteractiveMessage struct { -} +type InteractiveMessageType string + +const ( + InteractiveMessageTypeButton InteractiveMessageType = "button" + InteractiveMessageTypeProduct InteractiveMessageType = "product" + InteractiveMessageTypeProductList InteractiveMessageType = "product_list" + InteractiveMessageTypeList InteractiveMessageType = "list" +) diff --git a/pkg/components/list_message.go b/pkg/components/list_message.go new file mode 100644 index 0000000..f376923 --- /dev/null +++ b/pkg/components/list_message.go @@ -0,0 +1,128 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type listSection struct { + Title string `json:"title" validate:"required"` + Rows []listSectionRow `json:"rows" validate:"required"` +} + +func NewListSection(title string) (*listSection, error) { + return &listSection{ + Title: title, + }, nil +} + +type listSectionRow struct { + Id string `json:"id" validate:"required"` + Description string `json:"description" validate:"required"` + Title string `json:"title" validate:"required"` +} + +func (section *listSection) AddRow(row *listSectionRow) { + section.Rows = append(section.Rows, *row) +} + +func (section *listSection) SetTitle(title string) { + section.Title = title +} + +func NewListSectionRow(id, title, description string) (*listSectionRow, error) { + return &listSectionRow{ + Id: id, + Description: description, + Title: title, + }, nil +} + +func (row *listSectionRow) SetTitle(title string) { + row.Title = title +} + +func (row *listSectionRow) SetDescription(description string) { + row.Description = description +} + +func (row *listSectionRow) SetId(id string) { + row.Id = id +} + +type listMessageAction struct { + ButtonText string `json:"button" validate:"required"` + Sections []listSection `json:"sections" validate:"required"` +} + +type ListMessageBody struct { + Text string `json:"text" validate:"required"` +} + +type listMessage struct { + Type InteractiveMessageType `json:"type" validate:"required"` + Action listMessageAction `json:"action" validate:"required"` + Body ListMessageBody `json:"body,omitempty"` +} + +type ListMessageParams struct { + ButtonText string `json:"-" validate:"required"` + BodyText string `json:"-" validate:"required` +} + +func NewListMessage(params ListMessageParams) (*listMessage, error) { + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + return &listMessage{ + Type: InteractiveMessageTypeList, + Body: ListMessageBody{ + Text: params.BodyText, + }, + Action: listMessageAction{ + ButtonText: params.ButtonText, + }, + }, nil +} + +type ListMessageApiPayload struct { + BaseMessagePayload + Interactive listMessage `json:"interactive" validate:"required"` +} + +func (m *listMessage) AddSection(section *listSection) { + m.Action.Sections = append(m.Action.Sections, *section) +} + +func (m *listMessage) SetBodyText(section *listSection) { + m.Body.Text = section.Title +} + +func (m *listMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := ListMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeInteractive), + Interactive: *m, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} diff --git a/pkg/components/location_message.go b/pkg/components/location_message.go index 77222d3..8c9f2ef 100644 --- a/pkg/components/location_message.go +++ b/pkg/components/location_message.go @@ -42,7 +42,7 @@ func (location *LocationMessage) ToJson(configs ApiCompatibleJsonConverterConfig } jsonData := LocationMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, LocationMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeLocation), Location: *location, } diff --git a/pkg/components/product_list_message.go b/pkg/components/product_list_message.go new file mode 100644 index 0000000..dbb2836 --- /dev/null +++ b/pkg/components/product_list_message.go @@ -0,0 +1,53 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type ProductListMessage struct { +} + +type ProductListMessageParams struct { +} + +type ProductListMessageApiPayload struct { + BaseMessagePayload + Interactive ProductListMessage `json:"interactive" validate:"required"` +} + +func NewProductListMessage(params ProductListMessageParams) (*ProductListMessage, error) { + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + return &ProductListMessage{}, nil +} + +func (m *ProductListMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := ProductListMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeInteractive), + Interactive: *m, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} diff --git a/pkg/components/product_message.go b/pkg/components/product_message.go new file mode 100644 index 0000000..db18c72 --- /dev/null +++ b/pkg/components/product_message.go @@ -0,0 +1,52 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type ProductMessage struct { +} + +type ProductMessageParams struct { +} + +type ProductMessageApiPayload struct { + BaseMessagePayload + Interactive ProductMessage `json:"interactive" validate:"required"` +} + +func NewProductMessage(params ProductMessageParams) (*ProductMessage, error) { + if err := utils.GetValidator().Struct(params); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + return &ProductMessage{}, nil +} + +func (m *ProductMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := ProductMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeInteractive), + Interactive: *m, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil + +} diff --git a/pkg/components/quick_reply_button_message.go b/pkg/components/quick_reply_button_message.go new file mode 100644 index 0000000..b6b6c61 --- /dev/null +++ b/pkg/components/quick_reply_button_message.go @@ -0,0 +1,44 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/sarthakjdev/wapi.go/utils" +) + +type QuickReplyButtonMessage struct { +} + +type QuickReplyButtonMessageParams struct { +} + +type QuickReplyButtonMessageApiPayload struct { + BaseMessagePayload + Interactive QuickReplyButtonMessage `json:"interactive" validate:"required"` +} + +func (m *QuickReplyButtonMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { + if err := utils.GetValidator().Struct(configs); err != nil { + return nil, fmt.Errorf("error validating configs: %v", err) + } + + jsonData := QuickReplyButtonMessageApiPayload{ + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeInteractive), + Interactive: *m, + } + + if configs.ReplyToMessageId != "" { + jsonData.Context = &Context{ + MessageId: configs.ReplyToMessageId, + } + } + + jsonToReturn, err := json.Marshal(jsonData) + + if err != nil { + return nil, fmt.Errorf("error marshalling json: %v", err) + } + + return jsonToReturn, nil +} diff --git a/pkg/components/reaction_message.go b/pkg/components/reaction_message.go index dd0645f..129d87d 100644 --- a/pkg/components/reaction_message.go +++ b/pkg/components/reaction_message.go @@ -37,7 +37,7 @@ func (reaction *ReactionMessage) ToJson(configs ApiCompatibleJsonConverterConfig } jsonData := ReactionMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, ReactionMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeReaction), Reaction: *reaction, } diff --git a/pkg/components/sticker_message.go b/pkg/components/sticker_message.go index 25c3532..23c9427 100644 --- a/pkg/components/sticker_message.go +++ b/pkg/components/sticker_message.go @@ -48,7 +48,7 @@ func (sticker *StickerMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) } jsonData := StickerMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, StickerMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeSticker), Sticker: *sticker, } diff --git a/pkg/components/text_message.go b/pkg/components/text_message.go index 22cfc4a..8440363 100644 --- a/pkg/components/text_message.go +++ b/pkg/components/text_message.go @@ -7,7 +7,7 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) -type TextMessage struct { +type textMessage struct { Text string AllowPreview bool } @@ -22,7 +22,7 @@ type TextMessageApiPayloadText struct { AllowPreview bool `json:"preview_url,omitempty"` } -func (m *TextMessage) SetText(text string) { +func (m *textMessage) SetText(text string) { m.Text = text } @@ -31,25 +31,25 @@ type TextMessageApiPayload struct { Text TextMessageApiPayloadText `json:"text" validate:"required"` } -func NewTextMessage(configs TextMessageConfigs) (*TextMessage, error) { +func NewTextMessage(configs TextMessageConfigs) (*textMessage, error) { err := utils.GetValidator().Struct(configs) if err != nil { return nil, fmt.Errorf("error validating text message config: %v", err) } - return &TextMessage{ + return &textMessage{ Text: configs.Text, AllowPreview: configs.AllowPreview, }, nil } // This function convert the TextMessage struct to WhatsApp API compatible JSON -func (m *TextMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { +func (m *textMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) } jsonData := TextMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, TextMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeText), Text: TextMessageApiPayloadText{ Body: m.Text, AllowPreview: m.AllowPreview, diff --git a/pkg/components/video_message.go b/pkg/components/video_message.go index 1c960ac..9bbdae2 100644 --- a/pkg/components/video_message.go +++ b/pkg/components/video_message.go @@ -48,7 +48,7 @@ func (video *VideoMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([] } jsonData := VideoMessageApiPayload{ - BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, VideoMessageType), + BaseMessagePayload: NewBaseMessagePayload(configs.SendToPhoneNumber, MessageTypeVideo), Video: *video, } From db835525fae71ef85d62222d5bb914f83c24b42e Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 16:51:24 +0530 Subject: [PATCH 15/30] fix: syntax error Signed-off-by: sarthakjdev --- pkg/components/list_message.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/components/list_message.go b/pkg/components/list_message.go index f376923..52055d6 100644 --- a/pkg/components/list_message.go +++ b/pkg/components/list_message.go @@ -69,7 +69,7 @@ type listMessage struct { type ListMessageParams struct { ButtonText string `json:"-" validate:"required"` - BodyText string `json:"-" validate:"required` + BodyText string `json:"-" validate:"required"` } func NewListMessage(params ListMessageParams) (*listMessage, error) { From 7c2b493961969f71a6e13fe5243583d8f9bceca7 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 16:53:22 +0530 Subject: [PATCH 16/30] docs: add docs for list interactive message Signed-off-by: sarthakjdev --- pkg/components/list_message.go | 54 ++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/pkg/components/list_message.go b/pkg/components/list_message.go index 52055d6..918346b 100644 --- a/pkg/components/list_message.go +++ b/pkg/components/list_message.go @@ -1,3 +1,4 @@ +// Package components provides components for creating interactive list messages. package components import ( @@ -7,31 +8,37 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// listSection represents a section in the list message. type listSection struct { - Title string `json:"title" validate:"required"` - Rows []listSectionRow `json:"rows" validate:"required"` + Title string `json:"title" validate:"required"` // Title of the section. + Rows []listSectionRow `json:"rows" validate:"required"` // Rows in the section. } +// NewListSection creates a new list section with the given title. func NewListSection(title string) (*listSection, error) { return &listSection{ Title: title, }, nil } -type listSectionRow struct { - Id string `json:"id" validate:"required"` - Description string `json:"description" validate:"required"` - Title string `json:"title" validate:"required"` -} - +// AddRow adds a new row to the list section. func (section *listSection) AddRow(row *listSectionRow) { section.Rows = append(section.Rows, *row) } +// SetTitle sets the title of the list section. func (section *listSection) SetTitle(title string) { section.Title = title } +// listSectionRow represents a row in the list section. +type listSectionRow struct { + Id string `json:"id" validate:"required"` // ID of the row. + Description string `json:"description" validate:"required"` // Description of the row. + Title string `json:"title" validate:"required"` // Title of the row. +} + +// NewListSectionRow creates a new list section row with the given ID, title, and description. func NewListSectionRow(id, title, description string) (*listSectionRow, error) { return &listSectionRow{ Id: id, @@ -40,38 +47,46 @@ func NewListSectionRow(id, title, description string) (*listSectionRow, error) { }, nil } +// SetTitle sets the title of the list section row. func (row *listSectionRow) SetTitle(title string) { row.Title = title } +// SetDescription sets the description of the list section row. func (row *listSectionRow) SetDescription(description string) { row.Description = description } +// SetId sets the ID of the list section row. func (row *listSectionRow) SetId(id string) { row.Id = id } +// listMessageAction represents the action of the list message. type listMessageAction struct { - ButtonText string `json:"button" validate:"required"` - Sections []listSection `json:"sections" validate:"required"` + ButtonText string `json:"button" validate:"required"` // Text of the button. + Sections []listSection `json:"sections" validate:"required"` // Sections in the list message. } +// ListMessageBody represents the body of the list message. type ListMessageBody struct { - Text string `json:"text" validate:"required"` + Text string `json:"text" validate:"required"` // Text of the body. } +// listMessage represents an interactive list message. type listMessage struct { - Type InteractiveMessageType `json:"type" validate:"required"` - Action listMessageAction `json:"action" validate:"required"` - Body ListMessageBody `json:"body,omitempty"` + Type InteractiveMessageType `json:"type" validate:"required"` // Type of the message. + Action listMessageAction `json:"action" validate:"required"` // Action of the message. + Body ListMessageBody `json:"body,omitempty"` // Body of the message. } +// ListMessageParams represents the parameters for creating a list message. type ListMessageParams struct { - ButtonText string `json:"-" validate:"required"` - BodyText string `json:"-" validate:"required"` + ButtonText string `json:"-" validate:"required"` // Text of the button. + BodyText string `json:"-" validate:"required"` // Text of the body. } +// NewListMessage creates a new list message with the given parameters. func NewListMessage(params ListMessageParams) (*listMessage, error) { if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -88,19 +103,23 @@ func NewListMessage(params ListMessageParams) (*listMessage, error) { }, nil } +// ListMessageApiPayload represents the API payload for the list message. type ListMessageApiPayload struct { BaseMessagePayload - Interactive listMessage `json:"interactive" validate:"required"` + Interactive listMessage `json:"interactive" validate:"required"` // Interactive message. } +// AddSection adds a new section to the list message. func (m *listMessage) AddSection(section *listSection) { m.Action.Sections = append(m.Action.Sections, *section) } +// SetBodyText sets the body text of the list message. func (m *listMessage) SetBodyText(section *listSection) { m.Body.Text = section.Title } +// ToJson converts the list message to JSON. func (m *listMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -124,5 +143,4 @@ func (m *listMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, } return jsonToReturn, nil - } From 764a68f087f0008fad769311f1d8dceef55c78cb Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 16:59:44 +0530 Subject: [PATCH 17/30] fix: audio message Signed-off-by: sarthakjdev --- pkg/components/audio_message.go | 5 ++++- pkg/components/document_message.go | 4 +++- pkg/components/image_message.go | 6 +++++- pkg/components/video_message.go | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/pkg/components/audio_message.go b/pkg/components/audio_message.go index 557bcb8..d460d9c 100644 --- a/pkg/components/audio_message.go +++ b/pkg/components/audio_message.go @@ -34,7 +34,10 @@ func NewAudioMessage(params AudioMessageConfigs) (*AudioMessage, error) { return nil, fmt.Errorf("either ID or Link must be provided") } - return &AudioMessage{}, nil + return &AudioMessage{ + Id: params.Id, + Link: params.Link, + }, nil } func (audio *AudioMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { diff --git a/pkg/components/document_message.go b/pkg/components/document_message.go index da8eac0..9704f31 100644 --- a/pkg/components/document_message.go +++ b/pkg/components/document_message.go @@ -23,7 +23,9 @@ func NewDocumentMessage(params DocumentMessageConfigs) (*DocumentMessage, error) return nil, fmt.Errorf("error validating configs: %v", err) } - return &DocumentMessage{}, nil + return &DocumentMessage{ + + }, nil } func (dm *DocumentMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { diff --git a/pkg/components/image_message.go b/pkg/components/image_message.go index 8b0185a..984de69 100644 --- a/pkg/components/image_message.go +++ b/pkg/components/image_message.go @@ -39,7 +39,11 @@ func NewImageMessage(params ImageMessageConfigs) (*ImageMessage, error) { return nil, fmt.Errorf("either ID or Link must be provided") } - return &ImageMessage{}, nil + return &ImageMessage{ + Id: params.Id, + Link: params.Link, + Caption: params.Caption, + }, nil } func (image *ImageMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { diff --git a/pkg/components/video_message.go b/pkg/components/video_message.go index 9bbdae2..3aa2959 100644 --- a/pkg/components/video_message.go +++ b/pkg/components/video_message.go @@ -35,7 +35,10 @@ func NewVideoMessage(params VideoMessageConfigs) (*VideoMessage, error) { return nil, fmt.Errorf("either ID or Link must be provided") } - return &VideoMessage{}, nil + return &VideoMessage{ + Id: params.Id, + Link: params.Link, + }, nil } func (video *VideoMessage) SetCaption(params string) { From 12dd5fec36b826cf944001f171a26886e0c76dd7 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 17:00:13 +0530 Subject: [PATCH 18/30] fix: format Signed-off-by: sarthakjdev --- pkg/components/document_message.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/components/document_message.go b/pkg/components/document_message.go index 9704f31..da8eac0 100644 --- a/pkg/components/document_message.go +++ b/pkg/components/document_message.go @@ -23,9 +23,7 @@ func NewDocumentMessage(params DocumentMessageConfigs) (*DocumentMessage, error) return nil, fmt.Errorf("error validating configs: %v", err) } - return &DocumentMessage{ - - }, nil + return &DocumentMessage{}, nil } func (dm *DocumentMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { From 570a1aba0959e677d917c25f1ad82a3576387fe1 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Thu, 23 May 2024 17:36:53 +0530 Subject: [PATCH 19/30] feat: add quick reply button component Signed-off-by: sarthakjdev --- example-chat-bot/main.go | 12 ++++ pkg/components/quick_reply_button_message.go | 65 +++++++++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 6e545f6..7498180 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -108,4 +108,16 @@ func main() { whatsappClient.Message.Send(manager.SendMessageParams{Message: listMessage, PhoneNumber: "919643500545"}) + buttonMessage, err := wapiComponents.NewQuickReplyButtonMessage("Body 1") + + if err != nil { + fmt.Println("error creating button message", err) + return + } + + buttonMessage.AddButton("1", "Button 1") + buttonMessage.AddButton("2", "Button 2") + + whatsappClient.Message.Send(manager.SendMessageParams{Message: buttonMessage, PhoneNumber: "919643500545"}) + } diff --git a/pkg/components/quick_reply_button_message.go b/pkg/components/quick_reply_button_message.go index b6b6c61..c2757fd 100644 --- a/pkg/components/quick_reply_button_message.go +++ b/pkg/components/quick_reply_button_message.go @@ -7,17 +7,76 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) -type QuickReplyButtonMessage struct { +// quickReplyButtonMessageButtonReply represents the reply structure of a quick reply button. +type quickReplyButtonMessageButtonReply struct { + Title string `json:"title" validate:"required"` // Title of the quick reply button. + Id string `json:"id" validate:"required"` // ID of the quick reply button. +} + +// quickReplyButtonMessageButton represents a quick reply button. +type quickReplyButtonMessageButton struct { + Type string `json:"type" validate:"required"` // Type of the quick reply button. + Reply quickReplyButtonMessageButtonReply `json:"reply" validate:"required"` // Reply structure of the quick reply button. +} + +// NewQuickReplyButton creates a new quick reply button with the given ID and title. +func NewQuickReplyButton(id, title string) (*quickReplyButtonMessageButton, error) { + return &quickReplyButtonMessageButton{ + Type: "reply", + Reply: quickReplyButtonMessageButtonReply{ + Title: title, + Id: id, + }, + }, nil } -type QuickReplyButtonMessageParams struct { +// QuickReplyButtonMessageAction represents the action of a quick reply button message. +type QuickReplyButtonMessageAction struct { + Buttons []quickReplyButtonMessageButton `json:"buttons" validate:"required"` // List of quick reply buttons. } +// QuickReplyButtonMessageBody represents the body of a quick reply button message. +type QuickReplyButtonMessageBody struct { + Text string `json:"text" validate:"required"` // Text of the quick reply button message. +} + +// QuickReplyButtonMessage represents a quick reply button message. +type QuickReplyButtonMessage struct { + Type InteractiveMessageType `json:"type" validate:"required"` // Type of the quick reply button message. + Body QuickReplyButtonMessageBody `json:"body" validate:"required"` // Body of the quick reply button message. + Action QuickReplyButtonMessageAction `json:"action" validate:"required"` // Action of the quick reply button message. +} + +// QuickReplyButtonMessageApiPayload represents the API payload for a quick reply button message. type QuickReplyButtonMessageApiPayload struct { BaseMessagePayload - Interactive QuickReplyButtonMessage `json:"interactive" validate:"required"` + Interactive QuickReplyButtonMessage `json:"interactive" validate:"required"` // Interactive part of the API payload. +} + +// NewQuickReplyButtonMessage creates a new quick reply button message with the given body text. +func NewQuickReplyButtonMessage(bodyText string) (*QuickReplyButtonMessage, error) { + return &QuickReplyButtonMessage{ + Type: InteractiveMessageTypeButton, + Body: QuickReplyButtonMessageBody{ + Text: bodyText, + }, + Action: QuickReplyButtonMessageAction{ + Buttons: []quickReplyButtonMessageButton{}, + }, + }, nil +} + +func (m *QuickReplyButtonMessage) AddButton(id, title string) error { + button, err := NewQuickReplyButton(id, title) + if err != nil { + return fmt.Errorf("error creating quick reply button: %v", err) + } + m.Action.Buttons = append(m.Action.Buttons, *button) + + return nil } +// ToJson converts the quick reply button message to JSON. func (m *QuickReplyButtonMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) From b34d46e358856366a81c3b80a0d8fc0dcb817cbe Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Tue, 28 May 2024 17:57:32 +0530 Subject: [PATCH 20/30] feat: add event emit and listen arch, user end is still left Signed-off-by: sarthakjdev --- example-chat-bot/main.go | 86 +++++++++++----------- internal/manager/event_manager.go | 107 ++++++++++++++++++++++++++++ internal/manager/media_manager.go | 4 +- internal/manager/message_manager.go | 4 +- internal/manager/phone_manager.go | 4 +- internal/manager/request_client.go | 8 +-- internal/manager/webhook_manager.go | 90 +++++++++++++++++++++++ internal/webhook/handler.go | 10 --- internal/webhook/webhook.go | 37 ---------- pkg/client/client.go | 13 +++- pkg/events/base_event.go | 45 ++++++++++-- pkg/events/list_message_event.go | 5 -- pkg/events/ready_event.go | 9 +++ pkg/events/text_message_event.go | 13 ++++ 14 files changed, 325 insertions(+), 110 deletions(-) create mode 100644 internal/manager/event_manager.go create mode 100644 internal/manager/webhook_manager.go delete mode 100644 internal/webhook/handler.go delete mode 100644 internal/webhook/webhook.go delete mode 100644 pkg/events/list_message_event.go create mode 100644 pkg/events/ready_event.go diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 7498180..40497c2 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -5,7 +5,7 @@ import ( "github.com/sarthakjdev/wapi.go/internal/manager" wapi "github.com/sarthakjdev/wapi.go/pkg/client" - wapiComponents "github.com/sarthakjdev/wapi.go/pkg/components" + "github.com/sarthakjdev/wapi.go/pkg/events" ) func main() { @@ -28,9 +28,9 @@ func main() { } // create a message - textMessage, err := wapiComponents.NewTextMessage(wapiComponents.TextMessageConfigs{ - Text: "Hello, from wapi.go", - }) + // textMessage, err := wapiComponents.NewTextMessage(wapiComponents.TextMessageConfigs{ + // Text: "Hello, from wapi.go", + // }) // if err != nil { // fmt.Println("error creating text message", err) @@ -66,58 +66,64 @@ func main() { // } // send the message - whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) + // whatsappClient.Message.Send(manager.SendMessageParams{Message: textMessage, PhoneNumber: "919643500545"}) // whatsappClient.Message.Send(manager.SendMessageParams{Message: contactMessage, PhoneNumber: "919643500545"}) // whatsappClient.Message.Send(manager.SendMessageParams{Message: locationMessage, PhoneNumber: "919643500545"}) // whatsappClient.Message.Send(manager.SendMessageParams{Message: reactionMessage, PhoneNumber: "919643500545"}) - listMessage, err := wapiComponents.NewListMessage(wapiComponents.ListMessageParams{ - ButtonText: "Button 1", - BodyText: "Body 1", - }) + // listMessage, err := wapiComponents.NewListMessage(wapiComponents.ListMessageParams{ + // ButtonText: "Button 1", + // BodyText: "Body 1", + // }) - if err != nil { - fmt.Println("error creating list message", err) - return - } + // if err != nil { + // fmt.Println("error creating list message", err) + // return + // } - listSectionRow, err := wapiComponents.NewListSectionRow("1", "Title 1", "Description 1") + // listSectionRow, err := wapiComponents.NewListSectionRow("1", "Title 1", "Description 1") - if err != nil { - fmt.Println("error creating list section row", err) - return - } + // if err != nil { + // fmt.Println("error creating list section row", err) + // return + // } - listSection, err := wapiComponents.NewListSection("Section1") + // listSection, err := wapiComponents.NewListSection("Section1") - if err != nil { - fmt.Println("error creating list section row", err) - return - } + // if err != nil { + // fmt.Println("error creating list section row", err) + // return + // } - listSection.AddRow(listSectionRow) - listMessage.AddSection(listSection) - jsonData, err := listMessage.ToJson(wapiComponents.ApiCompatibleJsonConverterConfigs{SendToPhoneNumber: "919643500545"}) + // listSection.AddRow(listSectionRow) + // listMessage.AddSection(listSection) + // jsonData, err := listMessage.ToJson(wapiComponents.ApiCompatibleJsonConverterConfigs{SendToPhoneNumber: "919643500545"}) - if err != nil { - fmt.Println("error converting message to json", err) - return - } + // if err != nil { + // fmt.Println("error converting message to json", err) + // return + // } - fmt.Println(string(jsonData)) + // fmt.Println(string(jsonData)) - whatsappClient.Message.Send(manager.SendMessageParams{Message: listMessage, PhoneNumber: "919643500545"}) + // whatsappClient.Message.Send(manager.SendMessageParams{Message: listMessage, PhoneNumber: "919643500545"}) - buttonMessage, err := wapiComponents.NewQuickReplyButtonMessage("Body 1") + // buttonMessage, err := wapiComponents.NewQuickReplyButtonMessage("Body 1") - if err != nil { - fmt.Println("error creating button message", err) - return - } + // if err != nil { + // fmt.Println("error creating button message", err) + // return + // } + + // buttonMessage.AddButton("1", "Button 1") + // buttonMessage.AddButton("2", "Button 2") - buttonMessage.AddButton("1", "Button 1") - buttonMessage.AddButton("2", "Button 2") + // whatsappClient.Message.Send(manager.SendMessageParams{Message: buttonMessage, PhoneNumber: "919643500545"}) + + whatsappClient.On(manager.ReadyEvent, func(event events.BaseEvent) { + fmt.Println("client is ready") + }) - whatsappClient.Message.Send(manager.SendMessageParams{Message: buttonMessage, PhoneNumber: "919643500545"}) + whatsappClient.InitiateClient() } diff --git a/internal/manager/event_manager.go b/internal/manager/event_manager.go new file mode 100644 index 0000000..37f8ce3 --- /dev/null +++ b/internal/manager/event_manager.go @@ -0,0 +1,107 @@ +package manager + +import ( + "fmt" + "sync" + + "github.com/sarthakjdev/wapi.go/pkg/events" +) + +type EventType string + +const ( + TextMessageEvent EventType = "text_message" + AudioMessageEvent EventType = "audio_message" + VideoMessageEvent EventType = "video_message" + ImageMessageEvent EventType = "image_message" + ContactMessageEvent EventType = "contact_message" + DocumentMessageEvent EventType = "document_message" + LocationMessageEvent EventType = "location_message" + ReactionMessageEvent EventType = "reaction_message" + ListInteractionMessageEvent EventType = "list_interaction_message" + TemplateMessageEvent EventType = "template_message" + QuickReplyMessageEvent EventType = "quick_reply_message" + ReplyButtonInteractionEvent EventType = "reply_button_interaction" + StickerMessageEvent EventType = "sticker_message" + AdInteractionEvent EventType = "ad_interaction_message" + CustomerIdentityChangedEvent EventType = "customer_identity_changed" + CustomerNumberChangedEvent EventType = "customer_number_changed" + MessageDeliveredEvent EventType = "message_delivered" + MessageFailedEvent EventType = "message_failed" + MessageReadEvent EventType = "message_read" + MessageSentEvent EventType = "message_sent" + MessageUndeliveredEvent EventType = "message_undelivered" + OrderReceivedEvent EventType = "order_received" + ProductInquiryEvent EventType = "product_inquiry" + UnknownEvent EventType = "unknown" + ErrorEvent EventType = "error" + WarnEvent EventType = "warn" + ReadyEvent EventType = "ready" +) + +type ChannelEvent struct { + Type EventType + Data events.BaseEvent +} + +type EventManger struct { + subscribers map[string]chan ChannelEvent + sync.RWMutex +} + +func NewEventManager() *EventManger { + return &EventManger{ + subscribers: make(map[string]chan ChannelEvent), + } +} + +// subscriber to this event listener will be notified when the event is published +func (em *EventManger) Subscribe(eventName string) (chan ChannelEvent, error) { + em.Lock() + defer em.Unlock() + if ch, ok := em.subscribers[eventName]; ok { + return ch, nil + } + em.subscribers[eventName] = make(chan ChannelEvent, 100) + return em.subscribers[eventName], nil + +} + +// subscriber to this event listener will be notified when the event is published +func (em *EventManger) Unsubscribe(id string) { + em.Lock() + defer em.Unlock() + delete(em.subscribers, id) +} + +// publish event to this events system and let all the subscriber consume them +func (em *EventManger) Publish(eventType EventType, data events.BaseEvent) error { + fmt.Println("Publishing event: ", eventType) + em.Lock() + defer em.Unlock() + + for _, ch := range em.subscribers { + select { + case ch <- ChannelEvent{ + Type: eventType, + Data: data, + }: + default: + return fmt.Errorf("event queue full for type: %s", eventType) + } + } + return nil +} + +func (em *EventManger) On(name EventType, handler func(events.BaseEvent)) string { + ch, _ := em.Subscribe(string(name)) + go func() { + for { + select { + case event := <-ch: + handler(event.Data) + } + } + }() + return string(name) +} diff --git a/internal/manager/media_manager.go b/internal/manager/media_manager.go index 7a08243..bd56cc3 100644 --- a/internal/manager/media_manager.go +++ b/internal/manager/media_manager.go @@ -1,10 +1,10 @@ package manager type MediaManager struct { - requester requestClient + requester RequestClient } -func NewMediaManager(requester requestClient) *MediaManager { +func NewMediaManager(requester RequestClient) *MediaManager { return &MediaManager{ requester: requester, } diff --git a/internal/manager/message_manager.go b/internal/manager/message_manager.go index 8eeb5dd..b1699b8 100644 --- a/internal/manager/message_manager.go +++ b/internal/manager/message_manager.go @@ -7,10 +7,10 @@ import ( ) type MessageManager struct { - requester requestClient + requester RequestClient } -func NewMessageManager(requester requestClient) *MessageManager { +func NewMessageManager(requester RequestClient) *MessageManager { return &MessageManager{ requester: requester, } diff --git a/internal/manager/phone_manager.go b/internal/manager/phone_manager.go index a3b4d7d..af2c583 100644 --- a/internal/manager/phone_manager.go +++ b/internal/manager/phone_manager.go @@ -1,10 +1,10 @@ package manager type PhoneNumbersManager struct { - requester requestClient + requester RequestClient } -func NewPhoneNumbersManager(requester requestClient) *PhoneNumbersManager { +func NewPhoneNumbersManager(requester RequestClient) *PhoneNumbersManager { return &PhoneNumbersManager{ requester: requester, } diff --git a/internal/manager/request_client.go b/internal/manager/request_client.go index 707c850..60f1c17 100644 --- a/internal/manager/request_client.go +++ b/internal/manager/request_client.go @@ -13,15 +13,15 @@ const ( REQUEST_PROTOCOL = "https" ) -type requestClient struct { +type RequestClient struct { apiVersion string phoneNumberId string baseUrl string apiAccessToken string } -func NewRequestClient(phoneNumberId string, apiAccessToken string) *requestClient { - return &requestClient{ +func NewRequestClient(phoneNumberId string, apiAccessToken string) *RequestClient { + return &RequestClient{ apiVersion: API_VERSION, baseUrl: BASE_URL, phoneNumberId: phoneNumberId, @@ -34,7 +34,7 @@ type requestCloudApiParams struct { path string } -func (requestClientInstance *requestClient) requestCloudApi(params requestCloudApiParams) { +func (requestClientInstance *RequestClient) requestCloudApi(params requestCloudApiParams) { httpRequest, err := http.NewRequest("POST", fmt.Sprintf("%s://%s/%s", REQUEST_PROTOCOL, requestClientInstance.baseUrl, params.path), strings.NewReader(params.body)) if err != nil { return diff --git a/internal/manager/webhook_manager.go b/internal/manager/webhook_manager.go new file mode 100644 index 0000000..5e78e5c --- /dev/null +++ b/internal/manager/webhook_manager.go @@ -0,0 +1,90 @@ +package manager + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "time" + + "github.com/labstack/echo/v4" + "github.com/sarthakjdev/wapi.go/pkg/events" +) + +// references for event driven architecture in golang: https://medium.com/@souravchoudhary0306/implementation-of-event-driven-architecture-in-go-golang-28d9a1c01f91 +type WebhookManager struct { + secret string + path string + port int + EventManager EventManger + Requester RequestClient +} + +type WebhookManagerConfig struct { + Secret string + Path string + Port int + EventManager EventManger + Requester RequestClient +} + +func NewWebhook(options *WebhookManagerConfig) *WebhookManager { + return &WebhookManager{ + secret: options.Secret, + path: options.Path, + port: options.Port, + EventManager: options.EventManager, + Requester: options.Requester, + } +} + +// this function is used in case if the client have not provided any custom http server +func (wh *WebhookManager) createEchoHttpServer() *echo.Echo { + e := echo.New() + return e + +} + +func (wh *WebhookManager) getRequestHandler(req *http.Request) { +} + +func (wh *WebhookManager) postRequestHandler(req *http.Request) { + // emits events based on the payload of the request + + wh.EventManager.Publish(TextMessageEvent, events.NewTextMessageEvent( + "wiuhbiueqwdqwd", + "2134141414", + "hello", + )) + +} + +func (wh *WebhookManager) ListenToEvents() { + + fmt.Println("Listening to events") + server := wh.createEchoHttpServer() + + // Start server in a goroutine + go func() { + if err := server.Start(":8080"); err != nil { + return + } + }() + + wh.EventManager.Publish(ReadyEvent, events.NewReadyEvent()) + + // Wait for an interrupt signal (e.g., Ctrl+C) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) // Capture SIGINT (Ctrl+C) + <-quit // Wait for the signal + + // Gracefully shut down the server (optional) + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := server.Shutdown(ctx); err != nil { + log.Fatal(err) // Handle shutdown errors gracefully + } + +} diff --git a/internal/webhook/handler.go b/internal/webhook/handler.go deleted file mode 100644 index 3595270..0000000 --- a/internal/webhook/handler.go +++ /dev/null @@ -1,10 +0,0 @@ -package webhook - -import "net/http" - -func (wh *Webhook) getRequestHandler(req *http.Request) { -} - -func (wh *Webhook) postRequestHandler(req *http.Request) { - -} diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go deleted file mode 100644 index a31c13f..0000000 --- a/internal/webhook/webhook.go +++ /dev/null @@ -1,37 +0,0 @@ -package webhook - -import ( - "github.com/labstack/echo/v4" -) - -// references for event driven architecture in golang: https://medium.com/@souravchoudhary0306/implementation-of-event-driven-architecture-in-go-golang-28d9a1c01f91 -type Webhook struct { - secret string - path string - port int -} - -type WebhookManagerConfig struct { - Secret string - Path string - Port int -} - -func NewWebhook(options WebhookManagerConfig) *Webhook { - return &Webhook{ - secret: options.Secret, - path: options.Path, - port: options.Port, - } -} - -// this function is used in case if the client have not provided any custom http server -func (wh *Webhook) createEchoHttpServer() *echo.Echo { - e := echo.New() - return e - -} - -func (wh *Webhook) ListenToEvents() { - -} diff --git a/pkg/client/client.go b/pkg/client/client.go index 460c458..6a43837 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/sarthakjdev/wapi.go/internal/manager" - "github.com/sarthakjdev/wapi.go/internal/webhook" + "github.com/sarthakjdev/wapi.go/pkg/events" "github.com/sarthakjdev/wapi.go/utils" ) @@ -13,7 +13,7 @@ type Client struct { Media manager.MediaManager Message manager.MessageManager Phone manager.PhoneNumbersManager - webhook webhook.Webhook + webhook manager.WebhookManager phoneNumberId string apiAccessToken string businessAccountId string @@ -36,11 +36,12 @@ func New(configs ClientConfig) (*Client, error) { return nil, fmt.Errorf("error validating client config", err) } requester := *manager.NewRequestClient(configs.PhoneNumberId, configs.ApiAccessToken) + eventManager := *manager.NewEventManager() return &Client{ Media: *manager.NewMediaManager(requester), Message: *manager.NewMessageManager(requester), Phone: *manager.NewPhoneNumbersManager(requester), - webhook: *webhook.NewWebhook(webhook.WebhookManagerConfig{Path: configs.WebhookPath, Secret: configs.WebhookSecret, Port: configs.WebhookServerPort}), + webhook: *manager.NewWebhook(&manager.WebhookManagerConfig{Path: configs.WebhookPath, Secret: configs.WebhookSecret, Port: configs.WebhookServerPort, EventManager: eventManager, Requester: requester}), phoneNumberId: configs.PhoneNumberId, apiAccessToken: configs.ApiAccessToken, businessAccountId: configs.BusinessAccountId, @@ -60,6 +61,12 @@ func (client *Client) SetPhoneNumberId(phoneNumberId string) { // InitiateClient initializes the client and starts listening to events from the webhook. // It returns true if the client was successfully initiated. func (client *Client) InitiateClient() bool { + client.webhook.ListenToEvents() return true } + +// OnMessage registers a handler for a specific event type. +func (client *Client) On(eventType manager.EventType, handler func(events.BaseEvent)) { + client.webhook.EventManager.On(eventType, handler) +} diff --git a/pkg/events/base_event.go b/pkg/events/base_event.go index 2cd9bb9..e4e26f7 100644 --- a/pkg/events/base_event.go +++ b/pkg/events/base_event.go @@ -1,20 +1,55 @@ package events -type BaseEvent struct { +type MessageContext struct { + From string `json:"from"` } -type BaseMessageEvent struct { +type BaseEvent interface { + GetEventType() string +} + +type BaseMessageEventInterface interface { BaseEvent + Reply() (string, error) + React() (string, error) } -type BaseSystemEvent struct { +type BaseSystemEventInterface interface { BaseEvent } -func (*BaseMessageEvent) Reply() { +type BaseMessageEvent struct { + MessageId string `json:"message_id"` + Context MessageContext `json:"context"` +} + +func NewBaseMessageEvent(messageId, from string) BaseMessageEvent { + return BaseMessageEvent{ + MessageId: messageId, + Context: MessageContext{ + From: from, + }, + } +} +func (bme BaseMessageEvent) GetEventType() string { + return "message" } -func (*BaseMessageEvent) React() { +func (baseMessageEvent *BaseMessageEvent) Reply() (string, error) { + // we need requester here + return "", nil + +} + +func (baseMessageEvent *BaseMessageEvent) React() (string, error) { + // we need requester here + return "", nil +} + +type BaseSystemEvent struct { +} +func (bme BaseSystemEvent) GetEventType() string { + return "system" } diff --git a/pkg/events/list_message_event.go b/pkg/events/list_message_event.go deleted file mode 100644 index 46cf52a..0000000 --- a/pkg/events/list_message_event.go +++ /dev/null @@ -1,5 +0,0 @@ -package events - -type ListMessageEvent struct { - BaseMessageEvent -} diff --git a/pkg/events/ready_event.go b/pkg/events/ready_event.go new file mode 100644 index 0000000..87102c9 --- /dev/null +++ b/pkg/events/ready_event.go @@ -0,0 +1,9 @@ +package events + +type ReadyEvent struct { + BaseSystemEvent +} + +func NewReadyEvent() *ReadyEvent { + return &ReadyEvent{} +} diff --git a/pkg/events/text_message_event.go b/pkg/events/text_message_event.go index 42771ca..dd85d59 100644 --- a/pkg/events/text_message_event.go +++ b/pkg/events/text_message_event.go @@ -2,4 +2,17 @@ package events type TextMessageEvent struct { BaseMessageEvent + Text string `json:"text"` +} + +func NewTextMessageEvent(messageId, from, text string) *TextMessageEvent { + return &TextMessageEvent{ + BaseMessageEvent: BaseMessageEvent{ + MessageId: messageId, + Context: MessageContext{ + From: from, + }, + }, + Text: text, + } } From a00b4459caebf69be4eb5cda1fc989839726314c Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Tue, 28 May 2024 22:00:03 +0530 Subject: [PATCH 21/30] feat: add reply and react functions Signed-off-by: sarthakjdev --- internal/manager/media_manager.go | 6 ++- internal/manager/message_manager.go | 11 ++--- internal/manager/phone_manager.go | 6 ++- internal/manager/webhook_manager.go | 35 +++++++++++++--- .../request_client.go | 18 ++++----- pkg/client/client.go | 5 ++- pkg/events/base_event.go | 40 ++++++++++++++++--- pkg/events/text_message_event.go | 13 +++--- 8 files changed, 96 insertions(+), 38 deletions(-) rename internal/{manager => request_client}/request_client.go (73%) diff --git a/internal/manager/media_manager.go b/internal/manager/media_manager.go index bd56cc3..58ee11d 100644 --- a/internal/manager/media_manager.go +++ b/internal/manager/media_manager.go @@ -1,10 +1,12 @@ package manager +import requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" + type MediaManager struct { - requester RequestClient + requester requestclient.RequestClient } -func NewMediaManager(requester RequestClient) *MediaManager { +func NewMediaManager(requester requestclient.RequestClient) *MediaManager { return &MediaManager{ requester: requester, } diff --git a/internal/manager/message_manager.go b/internal/manager/message_manager.go index b1699b8..bf78d80 100644 --- a/internal/manager/message_manager.go +++ b/internal/manager/message_manager.go @@ -3,14 +3,15 @@ package manager import ( "fmt" + requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" "github.com/sarthakjdev/wapi.go/pkg/components" ) type MessageManager struct { - requester RequestClient + requester requestclient.RequestClient } -func NewMessageManager(requester RequestClient) *MessageManager { +func NewMessageManager(requester requestclient.RequestClient) *MessageManager { return &MessageManager{ requester: requester, } @@ -31,9 +32,9 @@ func (mm *MessageManager) Send(params SendMessageParams) (string, error) { // ! TODO: emit a error event here return "", fmt.Errorf("error converting message to json: %v", err) } - mm.requester.requestCloudApi(requestCloudApiParams{ - body: string(body), - path: "/" + mm.requester.phoneNumberId + "/messages", + mm.requester.RequestCloudApi(requestclient.RequestCloudApiParams{ + Body: string(body), + Path: "/" + mm.requester.PhoneNumberId + "/messages", }) return "ok", nil } diff --git a/internal/manager/phone_manager.go b/internal/manager/phone_manager.go index af2c583..c32c2b0 100644 --- a/internal/manager/phone_manager.go +++ b/internal/manager/phone_manager.go @@ -1,10 +1,12 @@ package manager +import requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" + type PhoneNumbersManager struct { - requester RequestClient + requester requestclient.RequestClient } -func NewPhoneNumbersManager(requester RequestClient) *PhoneNumbersManager { +func NewPhoneNumbersManager(requester requestclient.RequestClient) *PhoneNumbersManager { return &PhoneNumbersManager{ requester: requester, } diff --git a/internal/manager/webhook_manager.go b/internal/manager/webhook_manager.go index 5e78e5c..b8ca1e9 100644 --- a/internal/manager/webhook_manager.go +++ b/internal/manager/webhook_manager.go @@ -4,12 +4,12 @@ import ( "context" "fmt" "log" - "net/http" "os" "os/signal" "time" "github.com/labstack/echo/v4" + requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" "github.com/sarthakjdev/wapi.go/pkg/events" ) @@ -19,7 +19,7 @@ type WebhookManager struct { path string port int EventManager EventManger - Requester RequestClient + Requester requestclient.RequestClient } type WebhookManagerConfig struct { @@ -27,7 +27,7 @@ type WebhookManagerConfig struct { Path string Port int EventManager EventManger - Requester RequestClient + Requester requestclient.RequestClient } func NewWebhook(options *WebhookManagerConfig) *WebhookManager { @@ -47,16 +47,31 @@ func (wh *WebhookManager) createEchoHttpServer() *echo.Echo { } -func (wh *WebhookManager) getRequestHandler(req *http.Request) { +func (wh *WebhookManager) getRequestHandler(c echo.Context) { + + // this endpoint is used to verify the webhook + + request := c.Request() + fmt.Println(request) + } -func (wh *WebhookManager) postRequestHandler(req *http.Request) { +func (wh *WebhookManager) postRequestHandler(c echo.Context) { // emits events based on the payload of the request + request := c.Request() + + fmt.Println(request) + + // parse the request here + // get the type of message + // emit the event based on the type of message + wh.EventManager.Publish(TextMessageEvent, events.NewTextMessageEvent( "wiuhbiueqwdqwd", "2134141414", "hello", + wh.Requester, )) } @@ -66,6 +81,16 @@ func (wh *WebhookManager) ListenToEvents() { fmt.Println("Listening to events") server := wh.createEchoHttpServer() + server.GET(wh.path, func(c echo.Context) error { + wh.getRequestHandler(c) + return c.String(200, "ok") + }) + + server.POST(wh.path, func(c echo.Context) error { + wh.postRequestHandler(c) + return c.String(200, "ok") + }) + // Start server in a goroutine go func() { if err := server.Start(":8080"); err != nil { diff --git a/internal/manager/request_client.go b/internal/request_client/request_client.go similarity index 73% rename from internal/manager/request_client.go rename to internal/request_client/request_client.go index 60f1c17..6f7cc66 100644 --- a/internal/manager/request_client.go +++ b/internal/request_client/request_client.go @@ -1,4 +1,4 @@ -package manager +package requestclient import ( "fmt" @@ -8,14 +8,14 @@ import ( ) const ( - API_VERSION = "v.19" + API_VERSION = "v19.0" BASE_URL = "graph.facebook.com" REQUEST_PROTOCOL = "https" ) type RequestClient struct { apiVersion string - phoneNumberId string + PhoneNumberId string baseUrl string apiAccessToken string } @@ -24,18 +24,18 @@ func NewRequestClient(phoneNumberId string, apiAccessToken string) *RequestClien return &RequestClient{ apiVersion: API_VERSION, baseUrl: BASE_URL, - phoneNumberId: phoneNumberId, + PhoneNumberId: phoneNumberId, apiAccessToken: apiAccessToken, } } -type requestCloudApiParams struct { - body string - path string +type RequestCloudApiParams struct { + Body string + Path string } -func (requestClientInstance *RequestClient) requestCloudApi(params requestCloudApiParams) { - httpRequest, err := http.NewRequest("POST", fmt.Sprintf("%s://%s/%s", REQUEST_PROTOCOL, requestClientInstance.baseUrl, params.path), strings.NewReader(params.body)) +func (requestClientInstance *RequestClient) RequestCloudApi(params RequestCloudApiParams) { + httpRequest, err := http.NewRequest("POST", fmt.Sprintf("%s://%s/%s", REQUEST_PROTOCOL, requestClientInstance.baseUrl, params.Path), strings.NewReader(params.Body)) if err != nil { return } diff --git a/pkg/client/client.go b/pkg/client/client.go index 6a43837..d3f9fb0 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/sarthakjdev/wapi.go/internal/manager" + requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" "github.com/sarthakjdev/wapi.go/pkg/events" "github.com/sarthakjdev/wapi.go/utils" ) @@ -33,9 +34,9 @@ type ClientConfig struct { func New(configs ClientConfig) (*Client, error) { err := utils.GetValidator().Struct(configs) if err != nil { - return nil, fmt.Errorf("error validating client config", err) + return nil, fmt.Errorf("error validating client config: %w", err) } - requester := *manager.NewRequestClient(configs.PhoneNumberId, configs.ApiAccessToken) + requester := *requestclient.NewRequestClient(configs.PhoneNumberId, configs.ApiAccessToken) eventManager := *manager.NewEventManager() return &Client{ Media: *manager.NewMediaManager(requester), diff --git a/pkg/events/base_event.go b/pkg/events/base_event.go index e4e26f7..790b548 100644 --- a/pkg/events/base_event.go +++ b/pkg/events/base_event.go @@ -1,5 +1,10 @@ package events +import ( + requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" + "github.com/sarthakjdev/wapi.go/pkg/components" +) + type MessageContext struct { From string `json:"from"` } @@ -19,16 +24,18 @@ type BaseSystemEventInterface interface { } type BaseMessageEvent struct { + requester requestclient.RequestClient MessageId string `json:"message_id"` Context MessageContext `json:"context"` } -func NewBaseMessageEvent(messageId, from string) BaseMessageEvent { +func NewBaseMessageEvent(messageId, from string, requester requestclient.RequestClient) BaseMessageEvent { return BaseMessageEvent{ MessageId: messageId, Context: MessageContext{ From: from, }, + requester: requester, } } @@ -36,14 +43,37 @@ func (bme BaseMessageEvent) GetEventType() string { return "message" } -func (baseMessageEvent *BaseMessageEvent) Reply() (string, error) { - // we need requester here +// Reply to the message +func (baseMessageEvent *BaseMessageEvent) Reply(Message components.BaseMessage) (string, error) { + + body, err := Message.ToJson(components.ApiCompatibleJsonConverterConfigs{ + SendToPhoneNumber: baseMessageEvent.Context.From, + ReplyToMessageId: baseMessageEvent.MessageId, + }) + + if err != nil { + return "", err + } + + baseMessageEvent.requester.RequestCloudApi(requestclient.RequestCloudApiParams{ + Body: string(body), + Path: "/" + baseMessageEvent.requester.PhoneNumberId + "/messages", + }) + return "", nil } -func (baseMessageEvent *BaseMessageEvent) React() (string, error) { - // we need requester here +// React to the message +func (baseMessageEvent *BaseMessageEvent) React(emoji string) (string, error) { + reactionMessage, err := components.NewReactionMessage(components.ReactionMessageParams{ + Emoji: emoji, + MessageId: baseMessageEvent.MessageId, + }) + if err != nil { + return "", err + } + baseMessageEvent.Reply(reactionMessage) return "", nil } diff --git a/pkg/events/text_message_event.go b/pkg/events/text_message_event.go index dd85d59..4b4f9d4 100644 --- a/pkg/events/text_message_event.go +++ b/pkg/events/text_message_event.go @@ -1,18 +1,15 @@ package events +import requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" + type TextMessageEvent struct { BaseMessageEvent Text string `json:"text"` } -func NewTextMessageEvent(messageId, from, text string) *TextMessageEvent { +func NewTextMessageEvent(messageId, from, text string, requester requestclient.RequestClient) *TextMessageEvent { return &TextMessageEvent{ - BaseMessageEvent: BaseMessageEvent{ - MessageId: messageId, - Context: MessageContext{ - From: from, - }, - }, - Text: text, + BaseMessageEvent: NewBaseMessageEvent(messageId, from, requester), + Text: text, } } From 841b7b819c97fa8048005aaafeeab931c59263b9 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Wed, 29 May 2024 00:12:58 +0530 Subject: [PATCH 22/30] chore: Wip Signed-off-by: sarthakjdev --- internal/manager/event_manager.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/internal/manager/event_manager.go b/internal/manager/event_manager.go index 37f8ce3..35180bc 100644 --- a/internal/manager/event_manager.go +++ b/internal/manager/event_manager.go @@ -45,18 +45,18 @@ type ChannelEvent struct { } type EventManger struct { - subscribers map[string]chan ChannelEvent + subscribers map[EventType]chan ChannelEvent sync.RWMutex } func NewEventManager() *EventManger { return &EventManger{ - subscribers: make(map[string]chan ChannelEvent), + subscribers: make(map[EventType]chan ChannelEvent), } } // subscriber to this event listener will be notified when the event is published -func (em *EventManger) Subscribe(eventName string) (chan ChannelEvent, error) { +func (em *EventManger) Subscribe(eventName EventType) (chan ChannelEvent, error) { em.Lock() defer em.Unlock() if ch, ok := em.subscribers[eventName]; ok { @@ -64,11 +64,10 @@ func (em *EventManger) Subscribe(eventName string) (chan ChannelEvent, error) { } em.subscribers[eventName] = make(chan ChannelEvent, 100) return em.subscribers[eventName], nil - } // subscriber to this event listener will be notified when the event is published -func (em *EventManger) Unsubscribe(id string) { +func (em *EventManger) Unsubscribe(id EventType) { em.Lock() defer em.Unlock() delete(em.subscribers, id) @@ -93,8 +92,8 @@ func (em *EventManger) Publish(eventType EventType, data events.BaseEvent) error return nil } -func (em *EventManger) On(name EventType, handler func(events.BaseEvent)) string { - ch, _ := em.Subscribe(string(name)) +func (em *EventManger) On(eventName EventType, handler func(events.BaseEvent)) EventType { + ch, _ := em.Subscribe(eventName) go func() { for { select { @@ -103,5 +102,5 @@ func (em *EventManger) On(name EventType, handler func(events.BaseEvent)) string } } }() - return string(name) + return eventName } From 2ae7b384e5eb451fc9159267bc4531aa2a57981b Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Wed, 29 May 2024 00:40:05 +0530 Subject: [PATCH 23/30] chore: Wip Signed-off-by: sarthakjdev --- internal/manager/webhook_manager.go | 10 ++++------ pkg/client/client.go | 9 +++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/manager/webhook_manager.go b/internal/manager/webhook_manager.go index b8ca1e9..5d15ffc 100644 --- a/internal/manager/webhook_manager.go +++ b/internal/manager/webhook_manager.go @@ -47,16 +47,14 @@ func (wh *WebhookManager) createEchoHttpServer() *echo.Echo { } -func (wh *WebhookManager) getRequestHandler(c echo.Context) { - +func (wh *WebhookManager) GetRequestHandler(c echo.Context) { // this endpoint is used to verify the webhook - request := c.Request() fmt.Println(request) } -func (wh *WebhookManager) postRequestHandler(c echo.Context) { +func (wh *WebhookManager) PostRequestHandler(c echo.Context) { // emits events based on the payload of the request request := c.Request() @@ -82,12 +80,12 @@ func (wh *WebhookManager) ListenToEvents() { server := wh.createEchoHttpServer() server.GET(wh.path, func(c echo.Context) error { - wh.getRequestHandler(c) + wh.GetRequestHandler(c) return c.String(200, "ok") }) server.POST(wh.path, func(c echo.Context) error { - wh.postRequestHandler(c) + wh.PostRequestHandler(c) return c.String(200, "ok") }) diff --git a/pkg/client/client.go b/pkg/client/client.go index d3f9fb0..e90a2da 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -3,6 +3,7 @@ package wapi import ( "fmt" + "github.com/labstack/echo/v4" "github.com/sarthakjdev/wapi.go/internal/manager" requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" "github.com/sarthakjdev/wapi.go/pkg/events" @@ -67,6 +68,14 @@ func (client *Client) InitiateClient() bool { return true } +func (client *Client) GetWebhookGetRequestHandler() func(c echo.Context) { + return client.webhook.GetRequestHandler +} + +func (client *Client) GetWebhookPostRequestHandler() func(c echo.Context) { + return client.webhook.PostRequestHandler +} + // OnMessage registers a handler for a specific event type. func (client *Client) On(eventType manager.EventType, handler func(events.BaseEvent)) { client.webhook.EventManager.On(eventType, handler) From 2536efefbfafcde3464c1c1b6c22623778451818 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Sat, 1 Jun 2024 01:13:04 +0530 Subject: [PATCH 24/30] feat: event listening works now Signed-off-by: sarthakjdev --- a.json | 76 ++++ example-chat-bot/main.go | 28 +- internal/manager/event_manager.go | 34 +- internal/manager/types.go | 334 ++++++++++++++++++ internal/manager/webhook_manager.go | 251 +++++++++++-- pkg/client/client.go | 4 +- pkg/events/audio_message_event.go | 23 ++ pkg/events/base_event.go | 15 +- pkg/events/button_interaction_event.go | 11 + pkg/events/contacts_message_event.go | 11 + pkg/events/customer_identity_changed_event.go | 8 + pkg/events/customer_number_change_event.go | 8 + pkg/events/document_message_event.go | 11 + pkg/events/image_message_event.go | 11 + pkg/events/list_interaction_event.go | 11 + pkg/events/location_message_event.go | 12 + pkg/events/message_delivered_event.go | 1 + pkg/events/message_failed_event.go | 1 + pkg/events/message_read_event.go | 1 + pkg/events/message_sent_event.go | 1 + pkg/events/message_undlivered_event.go | 1 + pkg/events/reaction_event.go | 11 + pkg/events/text_message_event.go | 6 +- pkg/events/video_message_event.go | 1 + 24 files changed, 787 insertions(+), 84 deletions(-) create mode 100644 a.json create mode 100644 internal/manager/types.go create mode 100644 pkg/events/audio_message_event.go create mode 100644 pkg/events/button_interaction_event.go create mode 100644 pkg/events/contacts_message_event.go create mode 100644 pkg/events/customer_identity_changed_event.go create mode 100644 pkg/events/customer_number_change_event.go create mode 100644 pkg/events/document_message_event.go create mode 100644 pkg/events/image_message_event.go create mode 100644 pkg/events/list_interaction_event.go create mode 100644 pkg/events/location_message_event.go create mode 100644 pkg/events/message_delivered_event.go create mode 100644 pkg/events/message_failed_event.go create mode 100644 pkg/events/message_read_event.go create mode 100644 pkg/events/message_sent_event.go create mode 100644 pkg/events/message_undlivered_event.go create mode 100644 pkg/events/reaction_event.go create mode 100644 pkg/events/video_message_event.go diff --git a/a.json b/a.json new file mode 100644 index 0000000..6e53db8 --- /dev/null +++ b/a.json @@ -0,0 +1,76 @@ +{ + "object": "whatsapp_business_account", + "entry": [ + { + "id": "103043282674158", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "15550078267", + "phone_number_id": "113269274970227" + }, + "contacts": [ + { "wa_id": "919643500545", "profile": { "name": "Sarthak Jain" } } + ], + "messages": [ + { + "id": "wamid.HBgMOTE5NjQzNTAwNTQ1FQIAEhgWM0VCMDkyNjJBM0ZGODYzQTA1NzdDMwA=", + "from": "919643500545", + "timestamp": "1717180074", + "type": "text", + "context": { + "id": "", + "referred_product": { + "catalog_id": "", + "product_retailer_id": "" + } + }, + "Errors": null, + "referral": { + "source_url": "", + "source_type": "", + "source_id": "", + "headline": "", + "body": "", + "thumbnail_url": "", + "ctwa_clid": "", + "media_type": "" + }, + "audio": {}, + "image": { "id": "", "mime_type": "", "sha256": "" }, + "button": { "payload": "", "text": "" }, + "document": { "id": "", "mime_type": "", "sha256": "" }, + "order": { "catalog_id": "", "product_items": null }, + "sticker": { + "id": "", + "mime_type": "", + "sha256": "", + "animated": false + }, + "system": { + "identity": "", + "body": "", + "customer": "", + "type": "", + "wa_id": "" + }, + "identity": { + "acknowledged": "", + "created_timestamp": "", + "hash": "" + }, + "video": { "id": "", "mime_type": "", "sha256": "" }, + "reaction": { "message_id": "", "emoji": "" }, + "location": { "latitude": 0, "longitude": 0 }, + "contacts": null + } + ] + }, + "field": "messages" + } + ] + } + ] +} diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index 40497c2..a7ca3ad 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -5,6 +5,7 @@ import ( "github.com/sarthakjdev/wapi.go/internal/manager" wapi "github.com/sarthakjdev/wapi.go/pkg/client" + wapiComponents "github.com/sarthakjdev/wapi.go/pkg/components" "github.com/sarthakjdev/wapi.go/pkg/events" ) @@ -15,10 +16,10 @@ func main() { // creating a client whatsappClient, err := wapi.New(wapi.ClientConfig{ PhoneNumberId: "113269274970227", - ApiAccessToken: "EABhCftGVaeIBOZCgZCHPf8eF7ZBayGCyVLvpGVbZC8oqjgZCzmhqVXn7TMiQ3JTQ77WxOE4K7DVIgFC8ZA7qSG2ANHQ3BbG09iXezHDHnu2iiC0K5VVcITzHZCMoy5aKkLhILxLNsOQ5s9nQg3dRj1VewJ1PuMJY2n9tcIP29qn0Ht30fpUirvG6tgE9CVdRlMHuHU54U4hFjqcNfbO4Q8jW1QvhKCZBv95do3YFd71v1ucZD", + ApiAccessToken: "EABhCftGVaeIBO27nfwWbjXQWIxFbXFbcRLNbtavOmdGQSZBq7cQ0L6pqACzW2VZBEy53fUohFJQNLdVqeS4hnthQrKS2X9d0Wm1rXih7ej8nkEUjxI0odIq3VLCjlD2RaPK7ZA0BA0lBhkDVmoDbQX7cUIObu6yqUvY2rsDiKZCZA6qNQocjU40e0z6a97kjbt3t7Inkp5R55BSF8uzVEv5zKN0ZBU71Rap7aEoZBplAsjy", BusinessAccountId: "103043282674158", WebhookPath: "/webhook", - WebhookSecret: "123456789", + WebhookSecret: "1234567890", WebhookServerPort: 8080, }) @@ -28,14 +29,14 @@ func main() { } // create a message - // textMessage, err := wapiComponents.NewTextMessage(wapiComponents.TextMessageConfigs{ - // Text: "Hello, from wapi.go", - // }) + textMessage, err := wapiComponents.NewTextMessage(wapiComponents.TextMessageConfigs{ + Text: "Hello, from wapi.go", + }) - // if err != nil { - // fmt.Println("error creating text message", err) - // return - // } + if err != nil { + fmt.Println("error creating text message", err) + return + } // contactMessage, err := wapiComponents.NewContactMessage([]wapiComponents.Contact{ // *wapiComponents.NewContact(wapiComponents.ContactName{ @@ -124,6 +125,15 @@ func main() { fmt.Println("client is ready") }) + whatsappClient.On(manager.TextMessageEvent, func(event events.BaseEvent) { + fmt.Println("text message event received") + + textMessageEvent := event.(*events.TextMessageEvent) + fmt.Println(textMessageEvent.Context.From) + textMessageEvent.Reply(textMessage) + + }) + whatsappClient.InitiateClient() } diff --git a/internal/manager/event_manager.go b/internal/manager/event_manager.go index 35180bc..ef2d222 100644 --- a/internal/manager/event_manager.go +++ b/internal/manager/event_manager.go @@ -7,38 +7,6 @@ import ( "github.com/sarthakjdev/wapi.go/pkg/events" ) -type EventType string - -const ( - TextMessageEvent EventType = "text_message" - AudioMessageEvent EventType = "audio_message" - VideoMessageEvent EventType = "video_message" - ImageMessageEvent EventType = "image_message" - ContactMessageEvent EventType = "contact_message" - DocumentMessageEvent EventType = "document_message" - LocationMessageEvent EventType = "location_message" - ReactionMessageEvent EventType = "reaction_message" - ListInteractionMessageEvent EventType = "list_interaction_message" - TemplateMessageEvent EventType = "template_message" - QuickReplyMessageEvent EventType = "quick_reply_message" - ReplyButtonInteractionEvent EventType = "reply_button_interaction" - StickerMessageEvent EventType = "sticker_message" - AdInteractionEvent EventType = "ad_interaction_message" - CustomerIdentityChangedEvent EventType = "customer_identity_changed" - CustomerNumberChangedEvent EventType = "customer_number_changed" - MessageDeliveredEvent EventType = "message_delivered" - MessageFailedEvent EventType = "message_failed" - MessageReadEvent EventType = "message_read" - MessageSentEvent EventType = "message_sent" - MessageUndeliveredEvent EventType = "message_undelivered" - OrderReceivedEvent EventType = "order_received" - ProductInquiryEvent EventType = "product_inquiry" - UnknownEvent EventType = "unknown" - ErrorEvent EventType = "error" - WarnEvent EventType = "warn" - ReadyEvent EventType = "ready" -) - type ChannelEvent struct { Type EventType Data events.BaseEvent @@ -79,7 +47,7 @@ func (em *EventManger) Publish(eventType EventType, data events.BaseEvent) error em.Lock() defer em.Unlock() - for _, ch := range em.subscribers { + if ch, ok := em.subscribers[eventType]; ok { select { case ch <- ChannelEvent{ Type: eventType, diff --git a/internal/manager/types.go b/internal/manager/types.go new file mode 100644 index 0000000..0639ec3 --- /dev/null +++ b/internal/manager/types.go @@ -0,0 +1,334 @@ +package manager + +type EventType string + +const ( + TextMessageEvent EventType = "text_message" + AudioMessageEvent EventType = "audio_message" + VideoMessageEvent EventType = "video_message" + ImageMessageEvent EventType = "image_message" + ContactMessageEvent EventType = "contact_message" + DocumentMessageEvent EventType = "document_message" + LocationMessageEvent EventType = "location_message" + ReactionMessageEvent EventType = "reaction_message" + ListInteractionMessageEvent EventType = "list_interaction_message" + TemplateMessageEvent EventType = "template_message" + QuickReplyMessageEvent EventType = "quick_reply_message" + ReplyButtonInteractionEvent EventType = "reply_button_interaction" + StickerMessageEvent EventType = "sticker_message" + AdInteractionEvent EventType = "ad_interaction_message" + CustomerIdentityChangedEvent EventType = "customer_identity_changed" + CustomerNumberChangedEvent EventType = "customer_number_changed" + MessageDeliveredEvent EventType = "message_delivered" + MessageFailedEvent EventType = "message_failed" + MessageReadEvent EventType = "message_read" + MessageSentEvent EventType = "message_sent" + MessageUndeliveredEvent EventType = "message_undelivered" + OrderReceivedEvent EventType = "order_received" + ProductInquiryEvent EventType = "product_inquiry" + UnknownEvent EventType = "unknown" + ErrorEvent EventType = "error" + WarnEvent EventType = "warn" + ReadyEvent EventType = "ready" +) + +type NotificationReasonEnum string + +const ( + NotificationReasonMessage NotificationReasonEnum = "message" +) + +type NotificationPayloadErrorSchemaType struct { + Code int `json:"code"` + Title string `json:"title"` + Message string `json:"message"` + ErrorData struct { + Details string `json:"details"` + } `json:"error_data,omitempty"` +} + +type NotificationPayloadMessageContextSchemaType struct { + Forwarded bool `json:"forwarded,omitempty"` + FrequentlyForwarded bool `json:"frequently_forwarded,omitempty"` + From string `json:"from,omitempty"` + Id string `json:"id"` + ReferredProduct struct { + CatalogId string `json:"catalog_id"` + ProductRetailerId string `json:"product_retailer_id"` + } `json:"referred_product,omitempty"` +} + +type NotificationPayloadTextMessageSchemaType struct { + Text struct { + Body string `json:"body"` + } `json:"text,omitempty"` + Referral struct { + SourceUrl string `json:"source_url"` + SourceType AdInteractionSourceTypeEnum `json:"source_type"` + SourceId string `json:"source_id"` + Headline string `json:"headline"` + Body string `json:"body"` + ImageUrl string `json:"image_url,omitempty"` + VideoUrl string `json:"video_url,omitempty"` + ThumbnailUrl string `json:"thumbnail_url"` + CtwaCLId string `json:"ctwa_clid"` + MediaType AdInteractionSourceMediaTypeEnum `json:"media_type"` + } `json:"referral,omitempty"` +} + +type NotificationPayloadAudioMessageSchemaType struct { + Audio struct { + Id string `json:"id,omitempty"` + MIMEType string `json:"mime_type,omitempty"` + SHA256 string `json:"sha256,omitempty"` + } `json:"audio,omitempty"` +} + +type NotificationPayloadImageMessageSchemaType struct { + Image struct { + Id string `json:"id"` + MIMEType string `json:"mime_type"` + SHA256 string `json:"sha256"` + Caption string `json:"caption,omitempty"` + } `json:"image,omitempty"` +} + +type NotificationPayloadButtonMessageSchemaType struct { + Button struct { + Payload string `json:"payload"` + Text string `json:"text"` + } `json:"button,omitempty"` +} + +type NotificationPayloadDocumentMessageSchemaType struct { + Document struct { + Id string `json:"id"` + MIMEType string `json:"mime_type"` + SHA256 string `json:"sha256"` + Caption string `json:"caption,omitempty"` + Filename string `json:"filename,omitempty"` + } `json:"document,omitempty"` +} + +type NotificationPayloadOrderMessageSchemaType struct { + // OrderText string `json:"text"` + Order struct { + CatalogId string `json:"catalog_id"` + ProductItems []struct { + ProductRetailerId string `json:"product_retailer_id"` + Quantity string `json:"quantity"` + ItemPrice string `json:"item_price"` + Currency string `json:"currency"` + } `json:"product_items"` + } `json:"order,omitempty"` +} + +type NotificationPayloadStickerMessageSchemaType struct { + Sticker struct { + Id string `json:"id"` + MIMEType string `json:"mime_type"` + SHA256 string `json:"sha256"` + Animated bool `json:"animated"` + } `json:"sticker,omitempty"` +} + +type NotificationPayloadSystemMessageSchemaType struct { + System struct { + Identity string `json:"identity"` + Body string `json:"body"` + Customer string `json:"customer"` + Type SystemNotificationTypeEnum `json:"type"` + WaId string `json:"wa_id"` + } `json:"system,omitempty"` + Identity struct { + Acknowledged string `json:"acknowledged"` + CreatedTimestamp string `json:"created_timestamp"` + Hash string `json:"hash"` + } `json:"identity,omitempty"` +} + +type NotificationPayloadVideoMessageSchemaType struct { + Video struct { + Id string `json:"id"` + MIMEType string `json:"mime_type"` + SHA256 string `json:"sha256"` + Caption string `json:"caption,omitempty"` + Filename string `json:"filename,omitempty"` + } `json:"video,omitempty"` +} + +type NotificationPayloadReactionMessageSchemaType struct { + Reaction struct { + MessageId string `json:"message_id"` + Emoji string `json:"emoji"` + } `json:"reaction,omitempty"` +} + +type NotificationPayloadInteractionMessageSchemaType struct { + Interactive struct { + Type InteractiveNotificationTypeEnum `json:"type"` + } `json:"interactive,omitempty"` +} + +type NotificationPayloadLocationMessageSchemaType struct { + Location struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Name string `json:"name,omitempty"` + Address string `json:"address,omitempty"` + } `json:"location,omitempty"` +} + +type NotificationPayloadContactMessageSchemaType struct { + Contacts []Contact `json:"contacts"` +} + +type NotificationMessageTypeEnum string + +const ( + NotificationMessageTypeText NotificationMessageTypeEnum = "text" + NotificationMessageTypeAudio NotificationMessageTypeEnum = "audio" + NotificationMessageTypeImage NotificationMessageTypeEnum = "image" + NotificationMessageTypeButton NotificationMessageTypeEnum = "button" + NotificationMessageTypeDocument NotificationMessageTypeEnum = "document" + NotificationMessageTypeOrder NotificationMessageTypeEnum = "order" + NotificationMessageTypeSticker NotificationMessageTypeEnum = "sticker" + NotificationMessageTypeSystem NotificationMessageTypeEnum = "system" + NotificationMessageTypeVideo NotificationMessageTypeEnum = "video" + NotificationMessageTypeReaction NotificationMessageTypeEnum = "reaction" + NotificationMessageTypeInteractive NotificationMessageTypeEnum = "interactive" + NotificationMessageTypeUnknown NotificationMessageTypeEnum = "unknown" + NotificationMessageTypeLocation NotificationMessageTypeEnum = "location" + NotificationMessageTypeContacts NotificationMessageTypeEnum = "contacts" +) + +type InteractiveNotificationTypeEnum string + +const ( + NotificationTypeButtonReply InteractiveNotificationTypeEnum = "button_reply" + NotificationTypeListReply InteractiveNotificationTypeEnum = "list_reply" +) + +type AdInteractionSourceTypeEnum string + +const ( + AdInteractionSourceTypeUnknown AdInteractionSourceTypeEnum = "unknown" + // Add other ad interaction source types +) + +type AdInteractionSourceMediaTypeEnum string + +const ( + AdInteractionSourceMediaTypeImage AdInteractionSourceMediaTypeEnum = "image" + AdInteractionSourceMediaTypeVideo AdInteractionSourceMediaTypeEnum = "video" + // Add other ad interaction source media types +) + +type SystemNotificationTypeEnum string + +const ( + SystemNotificationTypeUnknown SystemNotificationTypeEnum = "unknown" + // Add other system notification types +) + +type Contact struct { + WaId string `json:"wa_id"` + Profile Profile `json:"profile"` +} + +type Profile struct { + Name string `json:"name"` +} + +type WhatsappApiNotificationPayloadSchemaType struct { + Object string `json:"object"` + Entry []Entry `json:"entry"` +} + +type Entry struct { + Id string `json:"id"` + Changes []Change `json:"changes"` +} + +type Change struct { + Value Value `json:"value"` + Field string `json:"field"` +} + +type Value struct { + MessagingProduct string `json:"messaging_product"` + Metadata Metadata `json:"metadata"` + Contacts []Contact `json:"contacts,omitempty"` + Statuses []Status `json:"statuses,omitempty"` + Messages []Message `json:"messages,omitempty"` + Errors []Error `json:"errors,omitempty"` +} + +type Metadata struct { + DisplayPhoneNumber string `json:"display_phone_number"` + PhoneNumberId string `json:"phone_number_id"` +} + +type Status struct { + Conversation Conversation `json:"conversation,omitempty"` + Errors []Error `json:"errors,omitempty"` + Status string `json:"status"` + Timestamp string `json:"timestamp"` + RecipientId string `json:"recipient_id"` + Pricing Pricing `json:"pricing,omitempty"` +} + +type Conversation struct { + Id string `json:"id"` + Origin Origin `json:"origin,omitempty"` +} + +type Origin struct { + Type MessageStatusCategoryEnum `json:"type"` + ExpirationTimestamp string `json:"expiration_timestamp,omitempty"` +} + +type Pricing struct { + PricingModel string `json:"pricing_model"` + Category MessageStatusCategoryEnum `json:"category"` +} + +type Message struct { + Id string `json:"id"` + From string `json:"from"` + Timestamp string `json:"timestamp"` + Type NotificationMessageTypeEnum `json:"type"` + Context NotificationPayloadMessageContextSchemaType `json:"context"` + Errors []Error `json:",inline"` + NotificationPayloadTextMessageSchemaType `json:",inline"` + NotificationPayloadAudioMessageSchemaType `json:",inline"` + NotificationPayloadImageMessageSchemaType `json:",inline"` + NotificationPayloadButtonMessageSchemaType `json:",inline"` + NotificationPayloadDocumentMessageSchemaType `json:",inline"` + NotificationPayloadOrderMessageSchemaType `json:",inline"` + NotificationPayloadStickerMessageSchemaType `json:",inline"` + NotificationPayloadSystemMessageSchemaType `json:",inline"` + NotificationPayloadVideoMessageSchemaType `json:",inline"` + NotificationPayloadReactionMessageSchemaType `json:",inline"` + NotificationPayloadLocationMessageSchemaType `json:",inline"` + NotificationPayloadContactMessageSchemaType `json:",inline"` +} + +type Error struct { + // Add fields for error details +} + +type MessageStatusCategoryEnum string + +const ( + MessageStatusCategorySent MessageStatusCategoryEnum = "sent" +) + +// Add other message status categories + +type MessageStatusEnum string + +const ( + MessageStatusDelivered MessageStatusEnum = "delivered" + // Add other message statuses +) diff --git a/internal/manager/webhook_manager.go b/internal/manager/webhook_manager.go index 5d15ffc..a76efa7 100644 --- a/internal/manager/webhook_manager.go +++ b/internal/manager/webhook_manager.go @@ -2,7 +2,9 @@ package manager import ( "context" + "encoding/json" "fmt" + "io" "log" "os" "os/signal" @@ -11,9 +13,9 @@ import ( "github.com/labstack/echo/v4" requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" "github.com/sarthakjdev/wapi.go/pkg/events" + "github.com/sarthakjdev/wapi.go/utils" ) -// references for event driven architecture in golang: https://medium.com/@souravchoudhary0306/implementation-of-event-driven-architecture-in-go-golang-28d9a1c01f91 type WebhookManager struct { secret string path string @@ -47,47 +49,231 @@ func (wh *WebhookManager) createEchoHttpServer() *echo.Echo { } -func (wh *WebhookManager) GetRequestHandler(c echo.Context) { - // this endpoint is used to verify the webhook - request := c.Request() - fmt.Println(request) - +func (wh *WebhookManager) GetRequestHandler(c echo.Context) error { + hubVerificationToken := c.QueryParam("hub.verify_token") + hubChallenge := c.QueryParam("hub.challenge") + fmt.Println(hubVerificationToken, hubChallenge) + if hubVerificationToken == wh.secret { + return c.String(200, hubChallenge) + } else { + return c.String(400, "invalid token") + } } -func (wh *WebhookManager) PostRequestHandler(c echo.Context) { - // emits events based on the payload of the request +func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { + body, err := io.ReadAll(c.Request().Body) + if err != nil { + fmt.Println("Error reading request body:", err) + c.String(400, "error reading request body") + } + + var payload WhatsappApiNotificationPayloadSchemaType - request := c.Request() + if err := json.Unmarshal(body, &payload); err != nil { + fmt.Println("Error unmarshaling JSON:", err) + c.String(400, "Invalid JSON data") + } - fmt.Println(request) + if err := utils.GetValidator().Struct(payload); err != nil { + fmt.Println("Error validating JSON:", err) + c.String(400, "Invalid JSON data") + } - // parse the request here - // get the type of message - // emit the event based on the type of message + for _, entry := range payload.Entry { + for _, change := range entry.Changes { + for _, message := range change.Value.Messages { + switch message.Type { + case NotificationMessageTypeText: + { + wh.EventManager.Publish(TextMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeImage: + { - wh.EventManager.Publish(TextMessageEvent, events.NewTextMessageEvent( - "wiuhbiueqwdqwd", - "2134141414", - "hello", - wh.Requester, - )) + wh.EventManager.Publish(ImageMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeVideo: + { + + wh.EventManager.Publish(AudioMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + + } + case NotificationMessageTypeDocument: + { + + wh.EventManager.Publish(DocumentMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeAudio: + { + wh.EventManager.Publish(AudioMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeLocation: + { + wh.EventManager.Publish(LocationMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeContacts: + { + wh.EventManager.Publish(ContactMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeSticker: + { + wh.EventManager.Publish(StickerMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeSystem: + { + wh.EventManager.Publish(TextMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeButton: + { + wh.EventManager.Publish(ReplyButtonInteractionEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeInteractive: + { + wh.EventManager.Publish(ListInteractionMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeReaction: + { + wh.EventManager.Publish(ReactionMessageEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeOrder: + { + wh.EventManager.Publish(OrderReceivedEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + case NotificationMessageTypeUnknown: + { + wh.EventManager.Publish(UnknownEvent, events.NewReactionMessageEvent( + events.NewBaseMessageEvent( + message.Id, + "", + message.From, + message.Context.Forwarded, + wh.Requester), + message.Text.Body), + ) + } + } + + } + } + } + + c.String(200, "Message received") + + fmt.Println("Received valid payload:", payload.Entry[0].Changes[0].Value.Messages[0].Type) + + return nil } func (wh *WebhookManager) ListenToEvents() { - fmt.Println("Listening to events") server := wh.createEchoHttpServer() - - server.GET(wh.path, func(c echo.Context) error { - wh.GetRequestHandler(c) - return c.String(200, "ok") - }) - - server.POST(wh.path, func(c echo.Context) error { - wh.PostRequestHandler(c) - return c.String(200, "ok") - }) + server.GET(wh.path, wh.GetRequestHandler) + server.POST(wh.path, wh.PostRequestHandler) // Start server in a goroutine go func() { @@ -97,7 +283,6 @@ func (wh *WebhookManager) ListenToEvents() { }() wh.EventManager.Publish(ReadyEvent, events.NewReadyEvent()) - // Wait for an interrupt signal (e.g., Ctrl+C) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) // Capture SIGINT (Ctrl+C) @@ -111,3 +296,7 @@ func (wh *WebhookManager) ListenToEvents() { } } + +func (wh *WebhookManager) determineEventType() EventType { + return TextMessageEvent +} diff --git a/pkg/client/client.go b/pkg/client/client.go index e90a2da..e5aa211 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -68,11 +68,11 @@ func (client *Client) InitiateClient() bool { return true } -func (client *Client) GetWebhookGetRequestHandler() func(c echo.Context) { +func (client *Client) GetWebhookGetRequestHandler() func(c echo.Context) error { return client.webhook.GetRequestHandler } -func (client *Client) GetWebhookPostRequestHandler() func(c echo.Context) { +func (client *Client) GetWebhookPostRequestHandler() func(c echo.Context) error { return client.webhook.PostRequestHandler } diff --git a/pkg/events/audio_message_event.go b/pkg/events/audio_message_event.go new file mode 100644 index 0000000..136a5e0 --- /dev/null +++ b/pkg/events/audio_message_event.go @@ -0,0 +1,23 @@ +package events + +import ( + "github.com/sarthakjdev/wapi.go/pkg/components" +) + +type AudioMessageEvent struct { + BaseMessageEvent + Audio components.AudioMessage + MimeType string `json:"mimetype"` + SHA256 string `json:"sha256"` + MediaId string `json:"media_id"` +} + +func NewAudioMessageEvent(baseMessageEvent BaseMessageEvent, mediaId string, audio components.AudioMessage, mime_type, sha256 string) *AudioMessageEvent { + return &AudioMessageEvent{ + BaseMessageEvent: baseMessageEvent, + Audio: audio, + MimeType: mime_type, + SHA256: sha256, + MediaId: mediaId, + } +} diff --git a/pkg/events/base_event.go b/pkg/events/base_event.go index 790b548..b104cb7 100644 --- a/pkg/events/base_event.go +++ b/pkg/events/base_event.go @@ -24,18 +24,22 @@ type BaseSystemEventInterface interface { } type BaseMessageEvent struct { - requester requestclient.RequestClient - MessageId string `json:"message_id"` - Context MessageContext `json:"context"` + requester requestclient.RequestClient + MessageId string `json:"message_id"` + Context MessageContext `json:"context"` + Timestamp string `json:"timestamp"` + IsForwarded bool `json:"is_forwarded"` } -func NewBaseMessageEvent(messageId, from string, requester requestclient.RequestClient) BaseMessageEvent { +func NewBaseMessageEvent(messageId string, timestamp string, from string, isForwarded bool, requester requestclient.RequestClient) BaseMessageEvent { return BaseMessageEvent{ MessageId: messageId, Context: MessageContext{ From: from, }, - requester: requester, + requester: requester, + Timestamp: timestamp, + IsForwarded: isForwarded, } } @@ -83,3 +87,4 @@ type BaseSystemEvent struct { func (bme BaseSystemEvent) GetEventType() string { return "system" } + diff --git a/pkg/events/button_interaction_event.go b/pkg/events/button_interaction_event.go new file mode 100644 index 0000000..135263c --- /dev/null +++ b/pkg/events/button_interaction_event.go @@ -0,0 +1,11 @@ +package events + +type ButtonInteractionMessageEvent struct { + BaseMessageEvent +} + +func NewButtonInteractionEvent(baseMessageEvent BaseMessageEvent, text string) *ButtonInteractionMessageEvent { + return &ButtonInteractionMessageEvent{ + BaseMessageEvent: baseMessageEvent, + } +} diff --git a/pkg/events/contacts_message_event.go b/pkg/events/contacts_message_event.go new file mode 100644 index 0000000..d46b9f3 --- /dev/null +++ b/pkg/events/contacts_message_event.go @@ -0,0 +1,11 @@ +package events + +type ContactsMessageEvent struct { + BaseMessageEvent +} + +func NewContactsMessageEvent(baseMessageEvent BaseMessageEvent, text string) *ContactsMessageEvent { + return &ContactsMessageEvent{ + BaseMessageEvent: baseMessageEvent, + } +} diff --git a/pkg/events/customer_identity_changed_event.go b/pkg/events/customer_identity_changed_event.go new file mode 100644 index 0000000..ece1833 --- /dev/null +++ b/pkg/events/customer_identity_changed_event.go @@ -0,0 +1,8 @@ +package events + +type CustomerIdentityChangedEvent struct { +} + +func NewCustomerIdentityChangedEvent(baseMessageEvent BaseMessageEvent, text string) *CustomerIdentityChangedEvent { + return &CustomerIdentityChangedEvent{} +} diff --git a/pkg/events/customer_number_change_event.go b/pkg/events/customer_number_change_event.go new file mode 100644 index 0000000..b81d36e --- /dev/null +++ b/pkg/events/customer_number_change_event.go @@ -0,0 +1,8 @@ +package events + +type CustomerNumberChangedEvent struct { +} + +func NewCustomerNumberChangedEvent(baseMessageEvent BaseMessageEvent, text string) *CustomerNumberChangedEvent { + return &CustomerNumberChangedEvent{} +} diff --git a/pkg/events/document_message_event.go b/pkg/events/document_message_event.go new file mode 100644 index 0000000..ed4eab2 --- /dev/null +++ b/pkg/events/document_message_event.go @@ -0,0 +1,11 @@ +package events + +type DocumentMessageEvent struct { + BaseMessageEvent +} + +func NewDocumentMessageEvent(baseMessageEvent BaseMessageEvent, text string) *DocumentMessageEvent { + return &DocumentMessageEvent{ + BaseMessageEvent: baseMessageEvent, + } +} diff --git a/pkg/events/image_message_event.go b/pkg/events/image_message_event.go new file mode 100644 index 0000000..fc8ccf6 --- /dev/null +++ b/pkg/events/image_message_event.go @@ -0,0 +1,11 @@ +package events + +type ImageMessageEvent struct { + BaseMessageEvent +} + +func NewImageMessageEvent(baseMessageEvent BaseMessageEvent, text string) *ImageMessageEvent { + return &ImageMessageEvent{ + BaseMessageEvent: baseMessageEvent, + } +} diff --git a/pkg/events/list_interaction_event.go b/pkg/events/list_interaction_event.go new file mode 100644 index 0000000..ec0727f --- /dev/null +++ b/pkg/events/list_interaction_event.go @@ -0,0 +1,11 @@ +package events + +type ListInteractionEvent struct { + BaseMessageEvent +} + +func NewListInteractionEvent(baseMessageEvent BaseMessageEvent, text string) *ListInteractionEvent { + return &ListInteractionEvent{ + BaseMessageEvent: baseMessageEvent, + } +} diff --git a/pkg/events/location_message_event.go b/pkg/events/location_message_event.go new file mode 100644 index 0000000..f711ed8 --- /dev/null +++ b/pkg/events/location_message_event.go @@ -0,0 +1,12 @@ +package events + + +type LocationMessageEvent struct { + BaseMessageEvent +} + +func NewLocationMessageEvent(baseMessageEvent BaseMessageEvent, text string) *LocationMessageEvent { + return &LocationMessageEvent{ + BaseMessageEvent: baseMessageEvent, + } +} diff --git a/pkg/events/message_delivered_event.go b/pkg/events/message_delivered_event.go new file mode 100644 index 0000000..b3adf69 --- /dev/null +++ b/pkg/events/message_delivered_event.go @@ -0,0 +1 @@ +package events diff --git a/pkg/events/message_failed_event.go b/pkg/events/message_failed_event.go new file mode 100644 index 0000000..f6f37a0 --- /dev/null +++ b/pkg/events/message_failed_event.go @@ -0,0 +1 @@ +package events \ No newline at end of file diff --git a/pkg/events/message_read_event.go b/pkg/events/message_read_event.go new file mode 100644 index 0000000..b3adf69 --- /dev/null +++ b/pkg/events/message_read_event.go @@ -0,0 +1 @@ +package events diff --git a/pkg/events/message_sent_event.go b/pkg/events/message_sent_event.go new file mode 100644 index 0000000..b3adf69 --- /dev/null +++ b/pkg/events/message_sent_event.go @@ -0,0 +1 @@ +package events diff --git a/pkg/events/message_undlivered_event.go b/pkg/events/message_undlivered_event.go new file mode 100644 index 0000000..b3adf69 --- /dev/null +++ b/pkg/events/message_undlivered_event.go @@ -0,0 +1 @@ +package events diff --git a/pkg/events/reaction_event.go b/pkg/events/reaction_event.go new file mode 100644 index 0000000..68f5da5 --- /dev/null +++ b/pkg/events/reaction_event.go @@ -0,0 +1,11 @@ +package events + +type ReactionMessageEvent struct { + BaseMessageEvent +} + +func NewReactionMessageEvent(baseMessageEvent BaseMessageEvent, text string) *ReactionMessageEvent { + return &ReactionMessageEvent{ + BaseMessageEvent: baseMessageEvent, + } +} diff --git a/pkg/events/text_message_event.go b/pkg/events/text_message_event.go index 4b4f9d4..32ef5bb 100644 --- a/pkg/events/text_message_event.go +++ b/pkg/events/text_message_event.go @@ -1,15 +1,13 @@ package events -import requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" - type TextMessageEvent struct { BaseMessageEvent Text string `json:"text"` } -func NewTextMessageEvent(messageId, from, text string, requester requestclient.RequestClient) *TextMessageEvent { +func NewTextMessageEvent(baseMessageEvent BaseMessageEvent, text string) *TextMessageEvent { return &TextMessageEvent{ - BaseMessageEvent: NewBaseMessageEvent(messageId, from, requester), + BaseMessageEvent: baseMessageEvent, Text: text, } } diff --git a/pkg/events/video_message_event.go b/pkg/events/video_message_event.go new file mode 100644 index 0000000..f6f37a0 --- /dev/null +++ b/pkg/events/video_message_event.go @@ -0,0 +1 @@ +package events \ No newline at end of file From af9bf604755078fcd9784405252b9f1a45185502 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Sat, 1 Jun 2024 01:32:08 +0530 Subject: [PATCH 25/30] fix: formatting Signed-off-by: sarthakjdev --- pkg/events/base_event.go | 1 - pkg/events/location_message_event.go | 1 - pkg/events/message_failed_event.go | 2 +- pkg/events/video_message_event.go | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/events/base_event.go b/pkg/events/base_event.go index b104cb7..0b0dfa8 100644 --- a/pkg/events/base_event.go +++ b/pkg/events/base_event.go @@ -87,4 +87,3 @@ type BaseSystemEvent struct { func (bme BaseSystemEvent) GetEventType() string { return "system" } - diff --git a/pkg/events/location_message_event.go b/pkg/events/location_message_event.go index f711ed8..73c3987 100644 --- a/pkg/events/location_message_event.go +++ b/pkg/events/location_message_event.go @@ -1,6 +1,5 @@ package events - type LocationMessageEvent struct { BaseMessageEvent } diff --git a/pkg/events/message_failed_event.go b/pkg/events/message_failed_event.go index f6f37a0..b3adf69 100644 --- a/pkg/events/message_failed_event.go +++ b/pkg/events/message_failed_event.go @@ -1 +1 @@ -package events \ No newline at end of file +package events diff --git a/pkg/events/video_message_event.go b/pkg/events/video_message_event.go index f6f37a0..b3adf69 100644 --- a/pkg/events/video_message_event.go +++ b/pkg/events/video_message_event.go @@ -1 +1 @@ -package events \ No newline at end of file +package events From 82a3fdac339bc3c08d92f4fef7227ddad66502f9 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Sat, 1 Jun 2024 17:50:54 +0530 Subject: [PATCH 26/30] feat: add all event structures Signed-off-by: sarthakjdev --- .gitignore | 2 - a.json | 76 ------------------- example-chat-bot/main.go | 17 ++++- internal/manager/event_manager.go | 21 +++-- internal/manager/media_manager.go | 8 +- internal/manager/message_manager.go | 9 ++- internal/manager/phone_manager.go | 13 ---- internal/manager/webhook_manager.go | 41 +++++----- pkg/client/client.go | 12 ++- pkg/components/audio_message.go | 6 +- pkg/components/base_message.go | 8 +- pkg/components/document_message.go | 6 +- pkg/components/image_message.go | 7 +- pkg/components/location_message.go | 19 +++-- pkg/components/order.go | 16 ++++ pkg/components/product_list_message.go | 6 +- pkg/components/product_message.go | 6 +- pkg/components/reaction_message.go | 12 ++- pkg/components/sticker_message.go | 6 +- pkg/components/template_message.go | 5 +- pkg/components/text_message.go | 22 ++++-- pkg/components/video_message.go | 7 +- pkg/events/ad_interaction_event.go | 50 ++++++++++++ pkg/events/audio_message_event.go | 23 +++--- pkg/events/base_event.go | 9 +++ pkg/events/button_interaction_event.go | 11 --- pkg/events/contacts_message_event.go | 10 ++- pkg/events/customer_identity_changed_event.go | 8 +- pkg/events/customer_number_change_event.go | 8 +- pkg/events/document_message_event.go | 17 ++++- pkg/events/image_message_event.go | 17 ++++- pkg/events/list_interaction_event.go | 12 ++- pkg/events/location_message_event.go | 10 ++- pkg/events/message_delivered_event.go | 16 ++++ pkg/events/message_failed_event.go | 17 +++++ pkg/events/message_read_event.go | 16 ++++ pkg/events/message_sent_event.go | 16 ++++ pkg/events/message_undelivered_event.go | 17 +++++ pkg/events/message_undlivered_event.go | 1 - pkg/events/order_event.go | 17 +++++ pkg/events/product_enquiry_event.go | 19 +++++ .../quick_reply_button_interaction_event.go | 17 +++++ pkg/events/reaction_event.go | 10 ++- pkg/events/ready_event.go | 4 +- pkg/events/reply_button_interaction_event.go | 17 +++++ pkg/events/sticker_message_event.go | 22 ++++++ pkg/events/text_message_event.go | 6 +- pkg/events/video_message_event.go | 21 +++++ 48 files changed, 513 insertions(+), 203 deletions(-) delete mode 100644 a.json delete mode 100644 internal/manager/phone_manager.go create mode 100644 pkg/components/order.go create mode 100644 pkg/events/ad_interaction_event.go delete mode 100644 pkg/events/button_interaction_event.go create mode 100644 pkg/events/message_undelivered_event.go delete mode 100644 pkg/events/message_undlivered_event.go create mode 100644 pkg/events/order_event.go create mode 100644 pkg/events/product_enquiry_event.go create mode 100644 pkg/events/quick_reply_button_interaction_event.go create mode 100644 pkg/events/reply_button_interaction_event.go create mode 100644 pkg/events/sticker_message_event.go diff --git a/.gitignore b/.gitignore index 5086c50..077bbdb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ node_modules .vscode/ -config.toml -node_modules dist/* \ No newline at end of file diff --git a/a.json b/a.json deleted file mode 100644 index 6e53db8..0000000 --- a/a.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "object": "whatsapp_business_account", - "entry": [ - { - "id": "103043282674158", - "changes": [ - { - "value": { - "messaging_product": "whatsapp", - "metadata": { - "display_phone_number": "15550078267", - "phone_number_id": "113269274970227" - }, - "contacts": [ - { "wa_id": "919643500545", "profile": { "name": "Sarthak Jain" } } - ], - "messages": [ - { - "id": "wamid.HBgMOTE5NjQzNTAwNTQ1FQIAEhgWM0VCMDkyNjJBM0ZGODYzQTA1NzdDMwA=", - "from": "919643500545", - "timestamp": "1717180074", - "type": "text", - "context": { - "id": "", - "referred_product": { - "catalog_id": "", - "product_retailer_id": "" - } - }, - "Errors": null, - "referral": { - "source_url": "", - "source_type": "", - "source_id": "", - "headline": "", - "body": "", - "thumbnail_url": "", - "ctwa_clid": "", - "media_type": "" - }, - "audio": {}, - "image": { "id": "", "mime_type": "", "sha256": "" }, - "button": { "payload": "", "text": "" }, - "document": { "id": "", "mime_type": "", "sha256": "" }, - "order": { "catalog_id": "", "product_items": null }, - "sticker": { - "id": "", - "mime_type": "", - "sha256": "", - "animated": false - }, - "system": { - "identity": "", - "body": "", - "customer": "", - "type": "", - "wa_id": "" - }, - "identity": { - "acknowledged": "", - "created_timestamp": "", - "hash": "" - }, - "video": { "id": "", "mime_type": "", "sha256": "" }, - "reaction": { "message_id": "", "emoji": "" }, - "location": { "latitude": 0, "longitude": 0 }, - "contacts": null - } - ] - }, - "field": "messages" - } - ] - } - ] -} diff --git a/example-chat-bot/main.go b/example-chat-bot/main.go index a7ca3ad..2d619dd 100644 --- a/example-chat-bot/main.go +++ b/example-chat-bot/main.go @@ -134,6 +134,21 @@ func main() { }) - whatsappClient.InitiateClient() + whatsappClient.On(manager.AudioMessageEvent, func(be events.BaseEvent) { + fmt.Println("audio message event received") + }) + whatsappClient.On(manager.VideoMessageEvent, func(be events.BaseEvent) { + fmt.Println("video message event received") + }) + + whatsappClient.On(manager.DocumentMessageEvent, func(be events.BaseEvent) { + fmt.Println("document message event received") + }) + + whatsappClient.On(manager.ImageMessageEvent, func(be events.BaseEvent) { + fmt.Println("image message event received") + }) + + whatsappClient.InitiateClient() } diff --git a/internal/manager/event_manager.go b/internal/manager/event_manager.go index ef2d222..1bdeec4 100644 --- a/internal/manager/event_manager.go +++ b/internal/manager/event_manager.go @@ -7,23 +7,27 @@ import ( "github.com/sarthakjdev/wapi.go/pkg/events" ) +// ChannelEvent represents an event that can be published and subscribed to. type ChannelEvent struct { - Type EventType - Data events.BaseEvent + Type EventType // Type is the type of the event. + Data events.BaseEvent // Data is the data associated with the event. } +// EventManger is responsible for managing events and their subscribers. type EventManger struct { - subscribers map[EventType]chan ChannelEvent - sync.RWMutex + subscribers map[EventType]chan ChannelEvent // subscribers is a map of event types to channels of ChannelEvent. + sync.RWMutex // RWMutex is used to synchronize access to the subscribers map. } +// NewEventManager creates a new instance of EventManger. func NewEventManager() *EventManger { return &EventManger{ subscribers: make(map[EventType]chan ChannelEvent), } } -// subscriber to this event listener will be notified when the event is published +// Subscribe adds a new subscriber to the specified event type. +// The subscriber will be notified when the event is published. func (em *EventManger) Subscribe(eventName EventType) (chan ChannelEvent, error) { em.Lock() defer em.Unlock() @@ -34,14 +38,14 @@ func (em *EventManger) Subscribe(eventName EventType) (chan ChannelEvent, error) return em.subscribers[eventName], nil } -// subscriber to this event listener will be notified when the event is published +// Unsubscribe removes a subscriber from the specified event type. func (em *EventManger) Unsubscribe(id EventType) { em.Lock() defer em.Unlock() delete(em.subscribers, id) } -// publish event to this events system and let all the subscriber consume them +// Publish publishes an event to the event system and notifies all the subscribers. func (em *EventManger) Publish(eventType EventType, data events.BaseEvent) error { fmt.Println("Publishing event: ", eventType) em.Lock() @@ -60,6 +64,9 @@ func (em *EventManger) Publish(eventType EventType, data events.BaseEvent) error return nil } +// On registers a handler function for the specified event type. +// The handler function will be called whenever the event is published. +// It returns the event type that the handler is registered for. func (em *EventManger) On(eventName EventType, handler func(events.BaseEvent)) EventType { ch, _ := em.Subscribe(eventName) go func() { diff --git a/internal/manager/media_manager.go b/internal/manager/media_manager.go index 58ee11d..556eb66 100644 --- a/internal/manager/media_manager.go +++ b/internal/manager/media_manager.go @@ -2,24 +2,24 @@ package manager import requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" +// MediaManager is responsible for managing media related operations. type MediaManager struct { requester requestclient.RequestClient } +// NewMediaManager creates a new instance of MediaManager. func NewMediaManager(requester requestclient.RequestClient) *MediaManager { return &MediaManager{ requester: requester, } } -func (mm *MediaManager) UploadMedia() { - // upload media -} - +// GetMediaUrlById retrieves the media URL by its ID. func (mm *MediaManager) GetMediaUrlById() { } +// GetMediaIdByUrl retrieves the media ID by its URL. func (mm *MediaManager) GetMediaIdByUrl() { } diff --git a/internal/manager/message_manager.go b/internal/manager/message_manager.go index bf78d80..7c9eee5 100644 --- a/internal/manager/message_manager.go +++ b/internal/manager/message_manager.go @@ -7,29 +7,33 @@ import ( "github.com/sarthakjdev/wapi.go/pkg/components" ) +// MessageManager is responsible for managing messages. type MessageManager struct { requester requestclient.RequestClient } +// NewMessageManager creates a new instance of MessageManager. func NewMessageManager(requester requestclient.RequestClient) *MessageManager { return &MessageManager{ requester: requester, } } +// SendMessageParams represents the parameters for sending a message. type SendMessageParams struct { Message components.BaseMessage PhoneNumber string } -// ! TODO: return the structured response from here +// Send sends a message with the given parameters and returns the response. +// TODO: return the structured response from here func (mm *MessageManager) Send(params SendMessageParams) (string, error) { body, err := params.Message.ToJson(components.ApiCompatibleJsonConverterConfigs{ SendToPhoneNumber: params.PhoneNumber, // ReplyToMessageId: "wamid.HBgMOTE5NjQzNTAwNTQ1FQIAERgSQzVGOTlFMzExQ0VCQTg0MUFCAA==", }) if err != nil { - // ! TODO: emit a error event here + // TODO: emit an error event here return "", fmt.Errorf("error converting message to json: %v", err) } mm.requester.RequestCloudApi(requestclient.RequestCloudApiParams{ @@ -39,6 +43,7 @@ func (mm *MessageManager) Send(params SendMessageParams) (string, error) { return "ok", nil } +// Reply replies to a message. func (mm *MessageManager) Reply() { // Reply to message } diff --git a/internal/manager/phone_manager.go b/internal/manager/phone_manager.go deleted file mode 100644 index c32c2b0..0000000 --- a/internal/manager/phone_manager.go +++ /dev/null @@ -1,13 +0,0 @@ -package manager - -import requestclient "github.com/sarthakjdev/wapi.go/internal/request_client" - -type PhoneNumbersManager struct { - requester requestclient.RequestClient -} - -func NewPhoneNumbersManager(requester requestclient.RequestClient) *PhoneNumbersManager { - return &PhoneNumbersManager{ - requester: requester, - } -} diff --git a/internal/manager/webhook_manager.go b/internal/manager/webhook_manager.go index a76efa7..c31ebba 100644 --- a/internal/manager/webhook_manager.go +++ b/internal/manager/webhook_manager.go @@ -1,3 +1,4 @@ +// Package manager provides functionality for managing webhooks. package manager import ( @@ -16,6 +17,7 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// WebhookManager represents a manager for handling webhooks. type WebhookManager struct { secret string path string @@ -24,6 +26,7 @@ type WebhookManager struct { Requester requestclient.RequestClient } +// WebhookManagerConfig represents the configuration options for creating a new WebhookManager. type WebhookManagerConfig struct { Secret string Path string @@ -32,6 +35,7 @@ type WebhookManagerConfig struct { Requester requestclient.RequestClient } +// NewWebhook creates a new WebhookManager with the given options. func NewWebhook(options *WebhookManagerConfig) *WebhookManager { return &WebhookManager{ secret: options.Secret, @@ -42,13 +46,14 @@ func NewWebhook(options *WebhookManagerConfig) *WebhookManager { } } -// this function is used in case if the client have not provided any custom http server +// createEchoHttpServer creates a new instance of Echo HTTP server. +// This function is used in case the client has not provided any custom HTTP server. func (wh *WebhookManager) createEchoHttpServer() *echo.Echo { e := echo.New() return e - } +// GetRequestHandler handles GET requests to the webhook endpoint. func (wh *WebhookManager) GetRequestHandler(c echo.Context) error { hubVerificationToken := c.QueryParam("hub.verify_token") hubChallenge := c.QueryParam("hub.challenge") @@ -60,6 +65,7 @@ func (wh *WebhookManager) GetRequestHandler(c echo.Context) error { } } +// PostRequestHandler handles POST requests to the webhook endpoint. func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { body, err := io.ReadAll(c.Request().Body) if err != nil { @@ -85,7 +91,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { switch message.Type { case NotificationMessageTypeText: { - wh.EventManager.Publish(TextMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(TextMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -98,7 +104,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { case NotificationMessageTypeImage: { - wh.EventManager.Publish(ImageMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(ImageMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -112,7 +118,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { case NotificationMessageTypeVideo: { - wh.EventManager.Publish(AudioMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(AudioMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -126,7 +132,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { case NotificationMessageTypeDocument: { - wh.EventManager.Publish(DocumentMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(DocumentMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -138,7 +144,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeAudio: { - wh.EventManager.Publish(AudioMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(AudioMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -150,7 +156,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeLocation: { - wh.EventManager.Publish(LocationMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(LocationMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -162,7 +168,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeContacts: { - wh.EventManager.Publish(ContactMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(ContactMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -174,7 +180,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeSticker: { - wh.EventManager.Publish(StickerMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(StickerMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -186,7 +192,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeSystem: { - wh.EventManager.Publish(TextMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(TextMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -198,7 +204,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeButton: { - wh.EventManager.Publish(ReplyButtonInteractionEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(ReplyButtonInteractionEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -210,7 +216,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeInteractive: { - wh.EventManager.Publish(ListInteractionMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(ListInteractionMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -222,7 +228,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeReaction: { - wh.EventManager.Publish(ReactionMessageEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(ReactionMessageEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -234,7 +240,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeOrder: { - wh.EventManager.Publish(OrderReceivedEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(OrderReceivedEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -246,7 +252,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { } case NotificationMessageTypeUnknown: { - wh.EventManager.Publish(UnknownEvent, events.NewReactionMessageEvent( + wh.EventManager.Publish(UnknownEvent, events.NewTextMessageEvent( events.NewBaseMessageEvent( message.Id, "", @@ -269,6 +275,7 @@ func (wh *WebhookManager) PostRequestHandler(c echo.Context) error { return nil } +// ListenToEvents starts listening to events and handles incoming requests. func (wh *WebhookManager) ListenToEvents() { fmt.Println("Listening to events") server := wh.createEchoHttpServer() @@ -294,9 +301,9 @@ func (wh *WebhookManager) ListenToEvents() { if err := server.Shutdown(ctx); err != nil { log.Fatal(err) // Handle shutdown errors gracefully } - } +// determineEventType determines the event type. func (wh *WebhookManager) determineEventType() EventType { return TextMessageEvent } diff --git a/pkg/client/client.go b/pkg/client/client.go index e5aa211..1806e38 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -14,7 +14,6 @@ import ( type Client struct { Media manager.MediaManager Message manager.MessageManager - Phone manager.PhoneNumbersManager webhook manager.WebhookManager phoneNumberId string apiAccessToken string @@ -33,16 +32,22 @@ type ClientConfig struct { // NewWapiClient creates a new instance of Client. func New(configs ClientConfig) (*Client, error) { + // Validate the client configuration options err := utils.GetValidator().Struct(configs) if err != nil { return nil, fmt.Errorf("error validating client config: %w", err) } + + // Create a new request client requester := *requestclient.NewRequestClient(configs.PhoneNumberId, configs.ApiAccessToken) + + // Create a new event manager eventManager := *manager.NewEventManager() + + // Create a new Client instance with the provided configurations return &Client{ Media: *manager.NewMediaManager(requester), Message: *manager.NewMessageManager(requester), - Phone: *manager.NewPhoneNumbersManager(requester), webhook: *manager.NewWebhook(&manager.WebhookManagerConfig{Path: configs.WebhookPath, Secret: configs.WebhookSecret, Port: configs.WebhookServerPort, EventManager: eventManager, Requester: requester}), phoneNumberId: configs.PhoneNumberId, apiAccessToken: configs.ApiAccessToken, @@ -63,15 +68,16 @@ func (client *Client) SetPhoneNumberId(phoneNumberId string) { // InitiateClient initializes the client and starts listening to events from the webhook. // It returns true if the client was successfully initiated. func (client *Client) InitiateClient() bool { - client.webhook.ListenToEvents() return true } +// GetWebhookGetRequestHandler returns the handler function for handling GET requests to the webhook. func (client *Client) GetWebhookGetRequestHandler() func(c echo.Context) error { return client.webhook.GetRequestHandler } +// GetWebhookPostRequestHandler returns the handler function for handling POST requests to the webhook. func (client *Client) GetWebhookPostRequestHandler() func(c echo.Context) error { return client.webhook.PostRequestHandler } diff --git a/pkg/components/audio_message.go b/pkg/components/audio_message.go index d460d9c..9f733f1 100644 --- a/pkg/components/audio_message.go +++ b/pkg/components/audio_message.go @@ -7,18 +7,22 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// AudioMessage represents an audio message. type AudioMessage struct { Id string `json:"id,omitempty"` Link string `json:"link,omitempty"` } +// AudioMessageApiPayload represents the payload for an audio message API request. type AudioMessageApiPayload struct { BaseMessagePayload Audio AudioMessage `json:"audio" validate:"required"` } +// AudioMessageConfigs is an alias for AudioMessage. type AudioMessageConfigs = AudioMessage +// NewAudioMessage creates a new AudioMessage object. func NewAudioMessage(params AudioMessageConfigs) (*AudioMessage, error) { if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -40,6 +44,7 @@ func NewAudioMessage(params AudioMessageConfigs) (*AudioMessage, error) { }, nil } +// ToJson converts the AudioMessage object to JSON. func (audio *AudioMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -63,5 +68,4 @@ func (audio *AudioMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([] } return jsonToReturn, nil - } diff --git a/pkg/components/base_message.go b/pkg/components/base_message.go index 80688b9..b4b9a1a 100644 --- a/pkg/components/base_message.go +++ b/pkg/components/base_message.go @@ -1,7 +1,9 @@ package components +// MessageType represents the type of message. type MessageType string +// Constants for different message types. const ( MessageTypeLocation MessageType = "location" MessageTypeAudio MessageType = "audio" @@ -16,16 +18,18 @@ const ( MessageTypeImage MessageType = "image" ) +// ApiCompatibleJsonConverterConfigs represents the configuration for converting to JSON. type ApiCompatibleJsonConverterConfigs struct { ReplyToMessageId string SendToPhoneNumber string } +// Context represents the context of the message. type Context struct { MessageId string `json:"message_id,omitempty"` } -// Base API Payload to send messages +// BaseMessagePayload represents the base payload to send messages. type BaseMessagePayload struct { Context *Context `json:"context,omitempty"` To string `json:"to"` @@ -34,6 +38,7 @@ type BaseMessagePayload struct { RecipientType string `json:"recipient_type"` } +// NewBaseMessagePayload creates a new instance of BaseMessagePayload. func NewBaseMessagePayload(to string, messageType MessageType) BaseMessagePayload { return BaseMessagePayload{ To: to, @@ -43,6 +48,7 @@ func NewBaseMessagePayload(to string, messageType MessageType) BaseMessagePayloa } } +// BaseMessage is an interface for sending messages. type BaseMessage interface { ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) } diff --git a/pkg/components/document_message.go b/pkg/components/document_message.go index da8eac0..9dc771c 100644 --- a/pkg/components/document_message.go +++ b/pkg/components/document_message.go @@ -7,17 +7,21 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// DocumentMessage represents a document message. type DocumentMessage struct { } +// DocumentMessageApiPayload represents the API payload for a document message. type DocumentMessageApiPayload struct { BaseMessagePayload Document DocumentMessage `json:"document" validate:"required"` } +// DocumentMessageConfigs represents the configurations for a document message. type DocumentMessageConfigs struct { } +// NewDocumentMessage creates a new DocumentMessage instance. func NewDocumentMessage(params DocumentMessageConfigs) (*DocumentMessage, error) { if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -26,6 +30,7 @@ func NewDocumentMessage(params DocumentMessageConfigs) (*DocumentMessage, error) return &DocumentMessage{}, nil } +// ToJson converts the DocumentMessage instance to JSON. func (dm *DocumentMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -49,5 +54,4 @@ func (dm *DocumentMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([] } return jsonToReturn, nil - } diff --git a/pkg/components/image_message.go b/pkg/components/image_message.go index 984de69..780cb92 100644 --- a/pkg/components/image_message.go +++ b/pkg/components/image_message.go @@ -7,23 +7,28 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// ImageMessage represents a message with an image. type ImageMessage struct { Id string `json:"id,omitempty"` Link string `json:"link,omitempty"` Caption string `json:"caption,omitempty"` } +// ImageMessageApiPayload represents the API payload for an image message. type ImageMessageApiPayload struct { BaseMessagePayload Image ImageMessage `json:"image" validate:"required"` } +// ImageMessageConfigs is an alias for ImageMessage. type ImageMessageConfigs = ImageMessage +// SetCaption sets the caption for the image message. func (image *ImageMessage) SetCaption(params string) { image.Caption = params } +// NewImageMessage creates a new ImageMessage instance. func NewImageMessage(params ImageMessageConfigs) (*ImageMessage, error) { if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -46,6 +51,7 @@ func NewImageMessage(params ImageMessageConfigs) (*ImageMessage, error) { }, nil } +// ToJson converts the ImageMessage to JSON. func (image *ImageMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -69,5 +75,4 @@ func (image *ImageMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([] } return jsonToReturn, nil - } diff --git a/pkg/components/location_message.go b/pkg/components/location_message.go index 8c9f2ef..6accc5d 100644 --- a/pkg/components/location_message.go +++ b/pkg/components/location_message.go @@ -7,35 +7,39 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// LocationMessage represents a location message with latitude, longitude, address, and name. type LocationMessage struct { - Latitude float64 `json:"latitude" validate:"required"` - Longitude float64 `json:"longitude" validate:"required"` - Address string `json:"address,omitempty"` - Name string `json:"name,omitempty"` + Latitude float64 `json:"latitude" validate:"required"` // Latitude of the location + Longitude float64 `json:"longitude" validate:"required"` // Longitude of the location + Address string `json:"address,omitempty"` // Address of the location (optional) + Name string `json:"name,omitempty"` // Name of the location (optional) } +// LocationMessageApiPayload represents the API payload for a location message. type LocationMessageApiPayload struct { BaseMessagePayload - Location LocationMessage `json:"location" validate:"required"` + Location LocationMessage `json:"location" validate:"required"` // Location message } +// NewLocationMessage creates a new LocationMessage with the given latitude and longitude. func NewLocationMessage(latitude float64, longitude float64) (*LocationMessage, error) { - return &LocationMessage{ Latitude: latitude, Longitude: longitude, }, nil - } +// SetAddress sets the address of the location. func (location *LocationMessage) SetAddress(params string) { location.Address = params } +// SetName sets the name of the location. func (location *LocationMessage) SetName(params string) { location.Name = params } +// ToJson converts the LocationMessage to JSON with the given configurations. func (location *LocationMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -59,5 +63,4 @@ func (location *LocationMessage) ToJson(configs ApiCompatibleJsonConverterConfig } return jsonToReturn, nil - } diff --git a/pkg/components/order.go b/pkg/components/order.go new file mode 100644 index 0000000..cbbdf1b --- /dev/null +++ b/pkg/components/order.go @@ -0,0 +1,16 @@ +package components + +// Order represents an order in the system. +type Order struct { + CatalogID string `json:"catalog_id"` // CatalogID is the ID of the catalog associated with the order. + ProductItems []ProductItem `json:"product_items"` // ProductItems is a list of product items in the order. + Text string `json:"text"` // Text is an additional text associated with the order. +} + +// ProductItem represents a product item in an order. +type ProductItem struct { + Currency string `json:"currency"` // Currency is the currency of the product item. + ItemPrice string `json:"item_price"` // ItemPrice is the price of the product item. + ProductRetailerID string `json:"product_retailer_id"` // ProductRetailerID is the ID of the retailer associated with the product item. + Quantity string `json:"quantity"` // Quantity is the quantity of the product item. +} diff --git a/pkg/components/product_list_message.go b/pkg/components/product_list_message.go index dbb2836..3aefb5a 100644 --- a/pkg/components/product_list_message.go +++ b/pkg/components/product_list_message.go @@ -7,17 +7,21 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// ProductListMessage represents a product list message. type ProductListMessage struct { } +// ProductListMessageParams represents the parameters for creating a product list message. type ProductListMessageParams struct { } +// ProductListMessageApiPayload represents the API payload for a product list message. type ProductListMessageApiPayload struct { BaseMessagePayload Interactive ProductListMessage `json:"interactive" validate:"required"` } +// NewProductListMessage creates a new product list message. func NewProductListMessage(params ProductListMessageParams) (*ProductListMessage, error) { if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -26,6 +30,7 @@ func NewProductListMessage(params ProductListMessageParams) (*ProductListMessage return &ProductListMessage{}, nil } +// ToJson converts the product list message to JSON. func (m *ProductListMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -49,5 +54,4 @@ func (m *ProductListMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ( } return jsonToReturn, nil - } diff --git a/pkg/components/product_message.go b/pkg/components/product_message.go index db18c72..0d98e54 100644 --- a/pkg/components/product_message.go +++ b/pkg/components/product_message.go @@ -7,17 +7,21 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// ProductMessage represents a product message. type ProductMessage struct { } +// ProductMessageParams represents the parameters for creating a product message. type ProductMessageParams struct { } +// ProductMessageApiPayload represents the API payload for a product message. type ProductMessageApiPayload struct { BaseMessagePayload Interactive ProductMessage `json:"interactive" validate:"required"` } +// NewProductMessage creates a new product message with the given parameters. func NewProductMessage(params ProductMessageParams) (*ProductMessage, error) { if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -25,6 +29,7 @@ func NewProductMessage(params ProductMessageParams) (*ProductMessage, error) { return &ProductMessage{}, nil } +// ToJson converts the product message to JSON with the given configurations. func (m *ProductMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -48,5 +53,4 @@ func (m *ProductMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]by } return jsonToReturn, nil - } diff --git a/pkg/components/reaction_message.go b/pkg/components/reaction_message.go index 129d87d..0e88c57 100644 --- a/pkg/components/reaction_message.go +++ b/pkg/components/reaction_message.go @@ -7,20 +7,23 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// ReactionMessage represents a reaction to a message. type ReactionMessage struct { - MessageId string `json:"message_id" validate:"required"` - Emoji string `json:"emoji" validate:"required"` + MessageId string `json:"message_id" validate:"required"` // The ID of the message to react to. + Emoji string `json:"emoji" validate:"required"` // The emoji representing the reaction. } +// ReactionMessageParams is an alias for ReactionMessage. type ReactionMessageParams = ReactionMessage +// ReactionMessageApiPayload represents the API payload for a reaction message. type ReactionMessageApiPayload struct { BaseMessagePayload - Reaction ReactionMessage `json:"reaction" validate:"required"` + Reaction ReactionMessage `json:"reaction" validate:"required"` // The reaction message. } +// NewReactionMessage creates a new ReactionMessage instance. func NewReactionMessage(params ReactionMessageParams) (*ReactionMessage, error) { - if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) } @@ -31,6 +34,7 @@ func NewReactionMessage(params ReactionMessageParams) (*ReactionMessage, error) }, nil } +// ToJson converts the ReactionMessage to JSON. func (reaction *ReactionMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) diff --git a/pkg/components/sticker_message.go b/pkg/components/sticker_message.go index 23c9427..9b9ed03 100644 --- a/pkg/components/sticker_message.go +++ b/pkg/components/sticker_message.go @@ -7,21 +7,25 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// StickerMessage represents a sticker message. type StickerMessage struct { Id string `json:"id,omitempty"` Link string `json:"link,omitempty"` } +// StickerMessageApiPayload represents the API payload for a sticker message. type StickerMessageApiPayload struct { BaseMessagePayload Sticker StickerMessage `json:"sticker" validate:"required"` } +// StickerMessageConfigs represents the configurations for a sticker message. type StickerMessageConfigs struct { Id string `json:"id,omitempty"` Link string `json:"link,omitempty"` } +// NewStickerMessage creates a new sticker message based on the provided configurations. func NewStickerMessage(params *StickerMessageConfigs) (*StickerMessage, error) { if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -42,6 +46,7 @@ func NewStickerMessage(params *StickerMessageConfigs) (*StickerMessage, error) { }, nil } +// ToJson converts the sticker message to JSON based on the provided configurations. func (sticker *StickerMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -65,5 +70,4 @@ func (sticker *StickerMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) } return jsonToReturn, nil - } diff --git a/pkg/components/template_message.go b/pkg/components/template_message.go index 4a38347..293c96a 100644 --- a/pkg/components/template_message.go +++ b/pkg/components/template_message.go @@ -1,15 +1,16 @@ package components +// TemplateMessage represents a template message. type TemplateMessage struct { } +// TemplateMessageApiPayload represents the API payload for a template message. type TemplateMessageApiPayload struct { BaseMessagePayload Template TemplateMessage `json:"template" validate:"required"` } +// NewTemplateMessage creates a new instance of TemplateMessage. func NewTemplateMessage() (*TemplateMessage, error) { - return &TemplateMessage{}, nil - } diff --git a/pkg/components/text_message.go b/pkg/components/text_message.go index 8440363..b8913d3 100644 --- a/pkg/components/text_message.go +++ b/pkg/components/text_message.go @@ -7,30 +7,36 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// textMessage represents a text message. type textMessage struct { - Text string - AllowPreview bool + Text string // The text content of the message. + AllowPreview bool // Whether to allow preview of the message. } +// TextMessageConfigs represents the configuration options for a text message. type TextMessageConfigs struct { - Text string `json:"text" validate:"required"` - AllowPreview bool `json:"allowPreview,omitempty"` + Text string `json:"text" validate:"required"` // The text content of the message. + AllowPreview bool `json:"allowPreview,omitempty"` // Whether to allow preview of the message. } +// TextMessageApiPayloadText represents the text payload for the WhatsApp API. type TextMessageApiPayloadText struct { - Body string `json:"body" validate:"required"` - AllowPreview bool `json:"preview_url,omitempty"` + Body string `json:"body" validate:"required"` // The text content of the message. + AllowPreview bool `json:"preview_url,omitempty"` // Whether to allow preview of the message. } +// SetText sets the text content of the message. func (m *textMessage) SetText(text string) { m.Text = text } +// TextMessageApiPayload represents the payload for the WhatsApp API. type TextMessageApiPayload struct { BaseMessagePayload `json:",inline"` - Text TextMessageApiPayloadText `json:"text" validate:"required"` + Text TextMessageApiPayloadText `json:"text" validate:"required"` // The text content of the message. } +// NewTextMessage creates a new text message with the given configurations. func NewTextMessage(configs TextMessageConfigs) (*textMessage, error) { err := utils.GetValidator().Struct(configs) if err != nil { @@ -42,7 +48,7 @@ func NewTextMessage(configs TextMessageConfigs) (*textMessage, error) { }, nil } -// This function convert the TextMessage struct to WhatsApp API compatible JSON +// ToJson converts the text message struct to WhatsApp API compatible JSON. func (m *textMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) diff --git a/pkg/components/video_message.go b/pkg/components/video_message.go index 3aa2959..9278ef4 100644 --- a/pkg/components/video_message.go +++ b/pkg/components/video_message.go @@ -7,19 +7,23 @@ import ( "github.com/sarthakjdev/wapi.go/utils" ) +// VideoMessage represents a video message. type VideoMessage struct { Id string `json:"id,omitempty"` Link string `json:"link,omitempty"` Caption string `json:"caption,omitempty"` } +// VideoMessageApiPayload represents the API payload for a video message. type VideoMessageApiPayload struct { BaseMessagePayload Video VideoMessage `json:"video" validate:"required"` } +// VideoMessageConfigs is an alias for VideoMessage. type VideoMessageConfigs = VideoMessage +// NewVideoMessage creates a new VideoMessage instance. func NewVideoMessage(params VideoMessageConfigs) (*VideoMessage, error) { if err := utils.GetValidator().Struct(params); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -41,10 +45,12 @@ func NewVideoMessage(params VideoMessageConfigs) (*VideoMessage, error) { }, nil } +// SetCaption sets the caption for the video message. func (video *VideoMessage) SetCaption(params string) { video.Caption = params } +// ToJson converts the video message to JSON. func (video *VideoMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([]byte, error) { if err := utils.GetValidator().Struct(configs); err != nil { return nil, fmt.Errorf("error validating configs: %v", err) @@ -68,5 +74,4 @@ func (video *VideoMessage) ToJson(configs ApiCompatibleJsonConverterConfigs) ([] } return jsonToReturn, nil - } diff --git a/pkg/events/ad_interaction_event.go b/pkg/events/ad_interaction_event.go new file mode 100644 index 0000000..9944156 --- /dev/null +++ b/pkg/events/ad_interaction_event.go @@ -0,0 +1,50 @@ +package events + +// AdInteractionSourceType represents the source type of an ad interaction. +type AdInteractionSourceType string + +const ( + // AdInteractionSourceTypePost indicates that the ad interaction is from a post. + AdInteractionSourceTypePost AdInteractionSourceType = "post" + // AdInteractionSourceTypeAd indicates that the ad interaction is from an ad. + AdInteractionSourceTypeAd AdInteractionSourceType = "ad" +) + +// AdInteractionSourceMediaType represents the media type of an ad interaction source. +type AdInteractionSourceMediaType string + +const ( + // AdInteractionSourceMediaTypeImage indicates that the ad interaction source is an image. + AdInteractionSourceMediaTypeImage AdInteractionSourceMediaType = "image" + // AdInteractionSourceMediaTypeVideo indicates that the ad interaction source is a video. + AdInteractionSourceMediaTypeVideo AdInteractionSourceMediaType = "video" +) + +// AdSource represents the source of an ad. +type AdSource struct { + Url string `json:"url"` + Id string `json:"id"` + Type AdInteractionSourceType `json:"type"` + Title string `json:"title"` + Description string `json:"description"` + MediaUrl string `json:"mediaUrl"` + MediaType AdInteractionSourceMediaType `json:"mediaType"` + ThumbnailUrl string `json:"thumbnailUrl"` + CtwaClid string `json:"ctwaClid"` +} + +// AdInteractionEvent represents an ad interaction event. +type AdInteractionEvent struct { + BaseMessageEvent `json:",inline"` + AdSource AdSource `json:"adSource"` + Text string `json:"text"` +} + +// NewAdInteractionEvent creates a new instance of AdInteractionEvent. +func NewAdInteractionEvent(baseMessageEvent BaseMessageEvent, adSource AdSource, text string) *AdInteractionEvent { + return &AdInteractionEvent{ + BaseMessageEvent: baseMessageEvent, + AdSource: adSource, + Text: text, + } +} diff --git a/pkg/events/audio_message_event.go b/pkg/events/audio_message_event.go index 136a5e0..71ed1f5 100644 --- a/pkg/events/audio_message_event.go +++ b/pkg/events/audio_message_event.go @@ -4,20 +4,21 @@ import ( "github.com/sarthakjdev/wapi.go/pkg/components" ) +// AudioMessageEvent represents an event for an audio message. type AudioMessageEvent struct { - BaseMessageEvent - Audio components.AudioMessage - MimeType string `json:"mimetype"` - SHA256 string `json:"sha256"` - MediaId string `json:"media_id"` + BaseMediaMessageEvent `json:",inline"` + Audio components.AudioMessage `json:"audio"` } -func NewAudioMessageEvent(baseMessageEvent BaseMessageEvent, mediaId string, audio components.AudioMessage, mime_type, sha256 string) *AudioMessageEvent { +// NewAudioMessageEvent creates a new AudioMessageEvent instance. +func NewAudioMessageEvent(baseMessageEvent BaseMessageEvent, mediaId string, audio components.AudioMessage, mime_type, sha256, mimeType string) *AudioMessageEvent { return &AudioMessageEvent{ - BaseMessageEvent: baseMessageEvent, - Audio: audio, - MimeType: mime_type, - SHA256: sha256, - MediaId: mediaId, + BaseMediaMessageEvent: BaseMediaMessageEvent{ + BaseMessageEvent: baseMessageEvent, + MediaId: mediaId, + Sha256: sha256, + MimeType: mimeType, + }, + Audio: audio, } } diff --git a/pkg/events/base_event.go b/pkg/events/base_event.go index 0b0dfa8..fd6b60e 100644 --- a/pkg/events/base_event.go +++ b/pkg/events/base_event.go @@ -81,7 +81,16 @@ func (baseMessageEvent *BaseMessageEvent) React(emoji string) (string, error) { return "", nil } +// BaseMediaMessageEvent represents a base media message event which contains media information. +type BaseMediaMessageEvent struct { + BaseMessageEvent `json:",inline"` + MediaId string `json:"media_id"` + MimeType string `json:"mime_type"` + Sha256 string `json:"sha256"` +} + type BaseSystemEvent struct { + Timestamp string `json:"timestamp"` } func (bme BaseSystemEvent) GetEventType() string { diff --git a/pkg/events/button_interaction_event.go b/pkg/events/button_interaction_event.go deleted file mode 100644 index 135263c..0000000 --- a/pkg/events/button_interaction_event.go +++ /dev/null @@ -1,11 +0,0 @@ -package events - -type ButtonInteractionMessageEvent struct { - BaseMessageEvent -} - -func NewButtonInteractionEvent(baseMessageEvent BaseMessageEvent, text string) *ButtonInteractionMessageEvent { - return &ButtonInteractionMessageEvent{ - BaseMessageEvent: baseMessageEvent, - } -} diff --git a/pkg/events/contacts_message_event.go b/pkg/events/contacts_message_event.go index d46b9f3..c096833 100644 --- a/pkg/events/contacts_message_event.go +++ b/pkg/events/contacts_message_event.go @@ -1,11 +1,17 @@ package events +import "github.com/sarthakjdev/wapi.go/pkg/components" + +// ContactsMessageEvent represents an event that occurs when a message with contacts is received. type ContactsMessageEvent struct { - BaseMessageEvent + BaseMessageEvent `json:",inline"` + Contacts components.ContactMessage `json:"contacts"` } -func NewContactsMessageEvent(baseMessageEvent BaseMessageEvent, text string) *ContactsMessageEvent { +// NewContactsMessageEvent creates a new ContactsMessageEvent instance. +func NewContactsMessageEvent(baseMessageEvent BaseMessageEvent, contacts components.ContactMessage) *ContactsMessageEvent { return &ContactsMessageEvent{ BaseMessageEvent: baseMessageEvent, + Contacts: contacts, } } diff --git a/pkg/events/customer_identity_changed_event.go b/pkg/events/customer_identity_changed_event.go index ece1833..f93b1bc 100644 --- a/pkg/events/customer_identity_changed_event.go +++ b/pkg/events/customer_identity_changed_event.go @@ -1,8 +1,8 @@ package events type CustomerIdentityChangedEvent struct { -} - -func NewCustomerIdentityChangedEvent(baseMessageEvent BaseMessageEvent, text string) *CustomerIdentityChangedEvent { - return &CustomerIdentityChangedEvent{} + BaseSystemEvent `json:",inline"` + Acknowledged string `json:"acknowledged"` + CreationTimestamp string `json:"creationTime"` + Hash string `json:"hash"` } diff --git a/pkg/events/customer_number_change_event.go b/pkg/events/customer_number_change_event.go index b81d36e..43fc931 100644 --- a/pkg/events/customer_number_change_event.go +++ b/pkg/events/customer_number_change_event.go @@ -1,8 +1,8 @@ package events type CustomerNumberChangedEvent struct { -} - -func NewCustomerNumberChangedEvent(baseMessageEvent BaseMessageEvent, text string) *CustomerNumberChangedEvent { - return &CustomerNumberChangedEvent{} + BaseMessageEvent `json:",inline"` + ChangeDescription string `json:"changeDescription"` + NewWaId string `json:"newWaId"` + OldWaId string `json:"oldWaId"` } diff --git a/pkg/events/document_message_event.go b/pkg/events/document_message_event.go index ed4eab2..67848ad 100644 --- a/pkg/events/document_message_event.go +++ b/pkg/events/document_message_event.go @@ -1,11 +1,22 @@ package events +import "github.com/sarthakjdev/wapi.go/pkg/components" + +// DocumentMessageEvent represents an event that occurs when a document message is received. type DocumentMessageEvent struct { - BaseMessageEvent + BaseMediaMessageEvent + Document components.DocumentMessage } -func NewDocumentMessageEvent(baseMessageEvent BaseMessageEvent, text string) *DocumentMessageEvent { +// NewDocumentMessageEvent creates a new DocumentMessageEvent instance. +func NewDocumentMessageEvent(baseMessageEvent BaseMessageEvent, document components.DocumentMessage, mediaId, sha256, mimeType string) *DocumentMessageEvent { return &DocumentMessageEvent{ - BaseMessageEvent: baseMessageEvent, + BaseMediaMessageEvent: BaseMediaMessageEvent{ + MediaId: mediaId, + Sha256: sha256, + MimeType: mimeType, + BaseMessageEvent: baseMessageEvent, + }, + Document: document, } } diff --git a/pkg/events/image_message_event.go b/pkg/events/image_message_event.go index fc8ccf6..276bfd6 100644 --- a/pkg/events/image_message_event.go +++ b/pkg/events/image_message_event.go @@ -1,11 +1,22 @@ package events +import "github.com/sarthakjdev/wapi.go/pkg/components" + +// ImageMessageEvent represents an event for an image message. type ImageMessageEvent struct { - BaseMessageEvent + BaseMediaMessageEvent `json:",inline"` + Image components.ImageMessage `json:"image"` } -func NewImageMessageEvent(baseMessageEvent BaseMessageEvent, text string) *ImageMessageEvent { +// NewImageMessageEvent creates a new ImageMessageEvent instance. +func NewImageMessageEvent(baseMessageEvent BaseMessageEvent, image components.ImageMessage, mimeType, sha256, mediaId string) *ImageMessageEvent { return &ImageMessageEvent{ - BaseMessageEvent: baseMessageEvent, + BaseMediaMessageEvent: BaseMediaMessageEvent{ + MediaId: mediaId, + Sha256: sha256, + MimeType: mimeType, + BaseMessageEvent: baseMessageEvent, + }, + Image: image, } } diff --git a/pkg/events/list_interaction_event.go b/pkg/events/list_interaction_event.go index ec0727f..e70422f 100644 --- a/pkg/events/list_interaction_event.go +++ b/pkg/events/list_interaction_event.go @@ -1,11 +1,19 @@ package events +// ListInteractionEvent represents an interaction event related to a list. type ListInteractionEvent struct { - BaseMessageEvent + BaseMessageEvent `json:",inline"` + Title string `json:"title"` + ListId string `json:"list_id"` + Description string `json:"description"` } -func NewListInteractionEvent(baseMessageEvent BaseMessageEvent, text string) *ListInteractionEvent { +// NewListInteractionEvent creates a new ListInteractionEvent instance. +func NewListInteractionEvent(baseMessageEvent BaseMessageEvent, title, listId, description string) *ListInteractionEvent { return &ListInteractionEvent{ BaseMessageEvent: baseMessageEvent, + Title: title, + ListId: listId, + Description: description, } } diff --git a/pkg/events/location_message_event.go b/pkg/events/location_message_event.go index 73c3987..4697f7f 100644 --- a/pkg/events/location_message_event.go +++ b/pkg/events/location_message_event.go @@ -1,11 +1,17 @@ package events +import "github.com/sarthakjdev/wapi.go/pkg/components" + +// LocationMessageEvent represents an event that contains a location message. type LocationMessageEvent struct { - BaseMessageEvent + BaseMessageEvent `json:",inline"` + Location components.LocationMessage `json:"location"` } -func NewLocationMessageEvent(baseMessageEvent BaseMessageEvent, text string) *LocationMessageEvent { +// NewLocationMessageEvent creates a new LocationMessageEvent instance. +func NewLocationMessageEvent(baseMessageEvent BaseMessageEvent, location components.LocationMessage) *LocationMessageEvent { return &LocationMessageEvent{ BaseMessageEvent: baseMessageEvent, + Location: location, } } diff --git a/pkg/events/message_delivered_event.go b/pkg/events/message_delivered_event.go index b3adf69..444bf49 100644 --- a/pkg/events/message_delivered_event.go +++ b/pkg/events/message_delivered_event.go @@ -1 +1,17 @@ package events + +// MessageDeliveredEvent represents an event related to an undelivered message. +type MessageDeliveredEvent struct { + BaseSystemEvent `json:",inline"` + MessageId string `json:"messageId"` + SentTo string `json:"sentTo"` +} + +// MessageDeliveredEvent creates a new instance of MessageUndeliveredEvent. +func NewMessageDeliveredEvent(baseSystemEvent BaseSystemEvent, messageId, sendTo string) *MessageDeliveredEvent { + return &MessageDeliveredEvent{ + BaseSystemEvent: baseSystemEvent, + MessageId: messageId, + SentTo: sendTo, + } +} diff --git a/pkg/events/message_failed_event.go b/pkg/events/message_failed_event.go index b3adf69..83a2b6b 100644 --- a/pkg/events/message_failed_event.go +++ b/pkg/events/message_failed_event.go @@ -1 +1,18 @@ package events + +type MessageFailedEvent struct { + BaseSystemEvent `json:",inline"` + MessageId string `json:"messageId"` + SentTo string `json:"sentTo"` + FailReason string `json:"failReason"` +} + +func NewMessageFailedEvent(baseSystemEvent BaseSystemEvent, messageId, sendTo, failReason string) *MessageFailedEvent { + return &MessageFailedEvent{ + BaseSystemEvent: baseSystemEvent, + MessageId: messageId, + SentTo: sendTo, + FailReason: failReason, + } + +} diff --git a/pkg/events/message_read_event.go b/pkg/events/message_read_event.go index b3adf69..849ac37 100644 --- a/pkg/events/message_read_event.go +++ b/pkg/events/message_read_event.go @@ -1 +1,17 @@ package events + +// MessageReadEvent represents an event indicating that a message has been read. +type MessageReadEvent struct { + BaseSystemEvent `json:",inline"` + MessageId string `json:"messageId"` + SentTo string `json:"sentTo"` +} + +// NewMessageReadEvent creates a new instance of MessageReadEvent. +func NewMessageReadEvent(baseSystemEvent BaseSystemEvent, messageId, sendTo string) *MessageReadEvent { + return &MessageReadEvent{ + BaseSystemEvent: baseSystemEvent, + MessageId: messageId, + SentTo: sendTo, + } +} diff --git a/pkg/events/message_sent_event.go b/pkg/events/message_sent_event.go index b3adf69..2833d1b 100644 --- a/pkg/events/message_sent_event.go +++ b/pkg/events/message_sent_event.go @@ -1 +1,17 @@ package events + +// MessageSentEvent represents an event indicating that a message has been sent. +type MessageSentEvent struct { + BaseSystemEvent `json:",inline"` + MessageId string `json:"messageId"` + SentTo string `json:"sentTo"` +} + +// NewMessageSentEvent creates a new instance of MessageSentEvent. +func NewMessageSentEvent(baseSystemEvent BaseSystemEvent, messageId, sendTo string) *MessageSentEvent { + return &MessageSentEvent{ + BaseSystemEvent: baseSystemEvent, + MessageId: messageId, + SentTo: sendTo, + } +} diff --git a/pkg/events/message_undelivered_event.go b/pkg/events/message_undelivered_event.go new file mode 100644 index 0000000..0ecdca7 --- /dev/null +++ b/pkg/events/message_undelivered_event.go @@ -0,0 +1,17 @@ +package events + +// MessageUndeliveredEvent represents an event related to an undelivered message. +type MessageUndeliveredEvent struct { + BaseSystemEvent `json:",inline"` + MessageId string `json:"messageId"` + SentTo string `json:"sentTo"` +} + +// NewMessageUndeliveredEvent creates a new instance of MessageUndeliveredEvent. +func NewMessageUndeliveredEvent(baseSystemEvent BaseSystemEvent, messageId, sendTo string) *MessageUndeliveredEvent { + return &MessageUndeliveredEvent{ + BaseSystemEvent: baseSystemEvent, + MessageId: messageId, + SentTo: sendTo, + } +} diff --git a/pkg/events/message_undlivered_event.go b/pkg/events/message_undlivered_event.go deleted file mode 100644 index b3adf69..0000000 --- a/pkg/events/message_undlivered_event.go +++ /dev/null @@ -1 +0,0 @@ -package events diff --git a/pkg/events/order_event.go b/pkg/events/order_event.go new file mode 100644 index 0000000..7b28f9a --- /dev/null +++ b/pkg/events/order_event.go @@ -0,0 +1,17 @@ +package events + +import "github.com/sarthakjdev/wapi.go/pkg/components" + +// OrderEvent represents an event related to an order. +type OrderEvent struct { + BaseMessageEvent `json:",inline"` + Order components.Order `json:"order"` +} + +// NewOrderEvent creates a new OrderEvent instance. +func NewOrderEvent(baseMessageEvent BaseMessageEvent, order components.Order) *OrderEvent { + return &OrderEvent{ + BaseMessageEvent: baseMessageEvent, + Order: order, + } +} diff --git a/pkg/events/product_enquiry_event.go b/pkg/events/product_enquiry_event.go new file mode 100644 index 0000000..b9c7659 --- /dev/null +++ b/pkg/events/product_enquiry_event.go @@ -0,0 +1,19 @@ +package events + +// ProductInquiryEvent represents an event related to a product inquiry. +type ProductInquiryEvent struct { + BaseMessageEvent `json:",inline"` + ProductId string `json:"productId"` + CatalogId string `json:"catalogId"` + Text string `json:"text"` +} + +// NewProductInquiryEvent creates a new instance of ProductInquiryEvent. +func NewProductInquiryEvent(baseMessageEvent BaseMessageEvent, productId, catalogId, text string) *ProductInquiryEvent { + return &ProductInquiryEvent{ + BaseMessageEvent: baseMessageEvent, + ProductId: productId, + CatalogId: catalogId, + Text: text, + } +} diff --git a/pkg/events/quick_reply_button_interaction_event.go b/pkg/events/quick_reply_button_interaction_event.go new file mode 100644 index 0000000..994f8d9 --- /dev/null +++ b/pkg/events/quick_reply_button_interaction_event.go @@ -0,0 +1,17 @@ +package events + +// QuickReplyButtonInteractionEvent represents an event triggered when a user interacts with a quick reply button. +type QuickReplyButtonInteractionEvent struct { + BaseMessageEvent `json:",inline"` + ButtonText string `json:"button_text"` + ButtonPayload string `json:"button_payload"` +} + +// NewQuickReplyButtonInteractionEvent creates a new instance of QuickReplyButtonInteractionEvent. +func NewQuickReplyButtonInteractionEvent(baseMessageEvent BaseMessageEvent, buttonText, buttonPayload string) *QuickReplyButtonInteractionEvent { + return &QuickReplyButtonInteractionEvent{ + BaseMessageEvent: baseMessageEvent, + ButtonText: buttonText, + ButtonPayload: buttonPayload, + } +} diff --git a/pkg/events/reaction_event.go b/pkg/events/reaction_event.go index 68f5da5..de5ec89 100644 --- a/pkg/events/reaction_event.go +++ b/pkg/events/reaction_event.go @@ -1,11 +1,17 @@ package events +import "github.com/sarthakjdev/wapi.go/pkg/components" + +// ReactionMessageEvent represents an event that occurs when a reaction is added to a message. type ReactionMessageEvent struct { - BaseMessageEvent + BaseMessageEvent `json:",inline"` + Reaction components.ReactionMessage } -func NewReactionMessageEvent(baseMessageEvent BaseMessageEvent, text string) *ReactionMessageEvent { +// NewReactionMessageEvent creates a new ReactionMessageEvent instance. +func NewReactionMessageEvent(baseMessageEvent BaseMessageEvent, reaction components.ReactionMessage) *ReactionMessageEvent { return &ReactionMessageEvent{ BaseMessageEvent: baseMessageEvent, + Reaction: reaction, } } diff --git a/pkg/events/ready_event.go b/pkg/events/ready_event.go index 87102c9..feb97f1 100644 --- a/pkg/events/ready_event.go +++ b/pkg/events/ready_event.go @@ -1,9 +1,11 @@ package events +// ReadyEvent represents an event that is triggered when the system is ready. type ReadyEvent struct { - BaseSystemEvent + BaseSystemEvent `json:",inline"` } +// NewReadyEvent creates a new instance of ReadyEvent. func NewReadyEvent() *ReadyEvent { return &ReadyEvent{} } diff --git a/pkg/events/reply_button_interaction_event.go b/pkg/events/reply_button_interaction_event.go new file mode 100644 index 0000000..10e9ee3 --- /dev/null +++ b/pkg/events/reply_button_interaction_event.go @@ -0,0 +1,17 @@ +package events + +// ReplyButtonInteractionEvent represents an interaction event triggered by a reply button. +type ReplyButtonInteractionEvent struct { + BaseMessageEvent `json:",inline"` + Title string `json:"title"` + ButtonId string `json:"button_id"` +} + +// NewReplyButtonInteractionEvent creates a new instance of ReplyButtonInteractionEvent. +func NewReplyButtonInteractionEvent(baseMessageEvent BaseMessageEvent, title, buttonId string) *ReplyButtonInteractionEvent { + return &ReplyButtonInteractionEvent{ + BaseMessageEvent: baseMessageEvent, + Title: title, + ButtonId: buttonId, + } +} diff --git a/pkg/events/sticker_message_event.go b/pkg/events/sticker_message_event.go new file mode 100644 index 0000000..678da6b --- /dev/null +++ b/pkg/events/sticker_message_event.go @@ -0,0 +1,22 @@ +package events + +import "github.com/sarthakjdev/wapi.go/pkg/components" + +// StickerMessageEvent represents an event for a sticker message. +type StickerMessageEvent struct { + BaseMediaMessageEvent `json:",inline"` + Sticker components.StickerMessage +} + +// NewStickerMessageEvent creates a new StickerMessageEvent instance. +func NewStickerMessageEvent(baseMessageEvent BaseMessageEvent, sticker components.StickerMessage, mediaId, sha256, mimeType string) *StickerMessageEvent { + return &StickerMessageEvent{ + BaseMediaMessageEvent: BaseMediaMessageEvent{ + MediaId: mediaId, + Sha256: sha256, + MimeType: mimeType, + BaseMessageEvent: baseMessageEvent, + }, + Sticker: sticker, + } +} diff --git a/pkg/events/text_message_event.go b/pkg/events/text_message_event.go index 32ef5bb..030e79c 100644 --- a/pkg/events/text_message_event.go +++ b/pkg/events/text_message_event.go @@ -1,10 +1,12 @@ package events +// TextMessageEvent represents an event for a text message. type TextMessageEvent struct { - BaseMessageEvent - Text string `json:"text"` + BaseMessageEvent `json:",inline"` + Text string `json:"text"` } +// NewTextMessageEvent creates a new TextMessageEvent instance. func NewTextMessageEvent(baseMessageEvent BaseMessageEvent, text string) *TextMessageEvent { return &TextMessageEvent{ BaseMessageEvent: baseMessageEvent, diff --git a/pkg/events/video_message_event.go b/pkg/events/video_message_event.go index b3adf69..527abf2 100644 --- a/pkg/events/video_message_event.go +++ b/pkg/events/video_message_event.go @@ -1 +1,22 @@ package events + +import "github.com/sarthakjdev/wapi.go/pkg/components" + +// VideoMessageEvent represents a WhatsApp video message event. +type VideoMessageEvent struct { + BaseMediaMessageEvent `json:",inline"` + Video components.VideoMessage `json:"video"` +} + +// NewVideoMessageEvent creates a new VideoMessageEvent instance. +func NewVideoMessageEvent(baseMessageEvent BaseMessageEvent, video components.VideoMessage, mimeType, sha256, mediaId string) *VideoMessageEvent { + return &VideoMessageEvent{ + BaseMediaMessageEvent: BaseMediaMessageEvent{ + MediaId: mediaId, + Sha256: sha256, + MimeType: mimeType, + BaseMessageEvent: baseMessageEvent, + }, + Video: video, + } +} From 22873f5667d00e226d2128f8e7c6f5ab7d672ceb Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Sat, 1 Jun 2024 17:58:12 +0530 Subject: [PATCH 27/30] chore: WIP Signed-off-by: sarthakjdev --- .github/workflows/release.yaml | 20 ++++++++++++++++++++ internal/request_client/request_client.go | 16 ++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..4c31984 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,20 @@ +name: Release + +on: + push: + branches: + - master + +concurrency: + group: check-pr-title-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + release: + name: Release + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v3 + + diff --git a/internal/request_client/request_client.go b/internal/request_client/request_client.go index 6f7cc66..a333205 100644 --- a/internal/request_client/request_client.go +++ b/internal/request_client/request_client.go @@ -13,6 +13,7 @@ const ( REQUEST_PROTOCOL = "https" ) +// RequestClient represents a client for making requests to a cloud API. type RequestClient struct { apiVersion string PhoneNumberId string @@ -20,6 +21,7 @@ type RequestClient struct { apiAccessToken string } +// NewRequestClient creates a new instance of RequestClient. func NewRequestClient(phoneNumberId string, apiAccessToken string) *RequestClient { return &RequestClient{ apiVersion: API_VERSION, @@ -29,15 +31,18 @@ func NewRequestClient(phoneNumberId string, apiAccessToken string) *RequestClien } } +// RequestCloudApiParams represents the parameters for making a request to the cloud API. type RequestCloudApiParams struct { Body string Path string } -func (requestClientInstance *RequestClient) RequestCloudApi(params RequestCloudApiParams) { +// RequestCloudApi makes a request to the cloud API with the given parameters. +// It returns the response body as a string and any error encountered. +func (requestClientInstance *RequestClient) RequestCloudApi(params RequestCloudApiParams) (string, error) { httpRequest, err := http.NewRequest("POST", fmt.Sprintf("%s://%s/%s", REQUEST_PROTOCOL, requestClientInstance.baseUrl, params.Path), strings.NewReader(params.Body)) if err != nil { - return + return "", err } httpRequest.Header.Set("Content-Type", "application/json") httpRequest.Header.Set("Authorization", fmt.Sprintf("Bearer %s", requestClientInstance.apiAccessToken)) @@ -45,12 +50,15 @@ func (requestClientInstance *RequestClient) RequestCloudApi(params RequestCloudA response, err := client.Do(httpRequest) if err != nil { fmt.Println("Error while requesting cloud api", err) - return + return "", err } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { - return + return "", err } + fmt.Println("Response from cloud api is", string(body)) + + return string(body), nil } From 0cdfca4103ffac48a503a88acda0b65f540587d1 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Sat, 1 Jun 2024 18:21:24 +0530 Subject: [PATCH 28/30] chore: remove release workflow Signed-off-by: sarthakjdev --- .github/workflows/release.yaml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index 4c31984..0000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: Release - -on: - push: - branches: - - master - -concurrency: - group: check-pr-title-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - release: - name: Release - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v3 - - From 1e8f98b961e370302ba95f617ed9a7de3132b28a Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Sat, 1 Jun 2024 18:39:20 +0530 Subject: [PATCH 29/30] test: hooks Signed-off-by: sarthakjdev --- .husky/hooks/commit-msg | 26 ++++++++++++++++++++++++++ .husky/hooks/pre-commit | 11 +++++++++++ .husky/hooks/pre-push | 9 +++++++++ 3 files changed, 46 insertions(+) create mode 100755 .husky/hooks/commit-msg create mode 100755 .husky/hooks/pre-commit create mode 100755 .husky/hooks/pre-push diff --git a/.husky/hooks/commit-msg b/.husky/hooks/commit-msg new file mode 100755 index 0000000..db0a7b4 --- /dev/null +++ b/.husky/hooks/commit-msg @@ -0,0 +1,26 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + + +# check for conventional commit format +commit_msg=$(cat "$1") +# Regex to match Conventional Commit format +conventional_commit_regex='^(feat|fix|chore|docs|test|style|refactor|perf|build|ci|revert)(\(.+?\))?: .{1,}$' + +if ! [[ "$commit_msg" =~ $conventional_commit_regex ]]; then + echo "ERROR: Commit message does not adhere to Conventional Commits format:" + echo "$commit_msg" + exit 1 +fi + + +# If not a merge commit, require commit to be signed off +if [ ! -e .git/MERGE_MSG ] && ! grep -q "^Signed-off-by: " "$1"; then + echo >&2 "Commit message must be signed off with your user name and email." + echo >&2 "To sign off your commit, add the -s flag to the git commit command." + exit 1 +fi + + + + diff --git a/.husky/hooks/pre-commit b/.husky/hooks/pre-commit new file mode 100755 index 0000000..ffc3b34 --- /dev/null +++ b/.husky/hooks/pre-commit @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +branch=`git symbolic-ref HEAD` + +if [ "$branch" = "refs/heads/master" ]; then + echo "\\033[31mDirect commit to master is not allowed.\\033[0m" + exit 1 +fi + +go fmt ./... diff --git a/.husky/hooks/pre-push b/.husky/hooks/pre-push new file mode 100755 index 0000000..2a4657e --- /dev/null +++ b/.husky/hooks/pre-push @@ -0,0 +1,9 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +branch=`git symbolic-ref HEAD` + +if [ "$branch" = "refs/heads/master" ]; then + echo "\\033[31mDirect push to master is not allowed.\\033[0m" + exit 1 +fi From 064b39ff22f2269e6341bec2f10dc0133edfc5c8 Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Sat, 1 Jun 2024 19:03:58 +0530 Subject: [PATCH 30/30] feat: add husky Signed-off-by: sarthakjdev --- .husky/hooks/commit-msg | 32 ++++++++++++++------------------ .husky/hooks/pre-commit | 7 +++---- .husky/hooks/pre-push | 3 +-- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/.husky/hooks/commit-msg b/.husky/hooks/commit-msg index db0a7b4..f37eba3 100755 --- a/.husky/hooks/commit-msg +++ b/.husky/hooks/commit-msg @@ -1,26 +1,22 @@ #!/bin/sh -. "$(dirname "$0")/_/husky.sh" +# # Check for conventional commit format +# commit_msg=$(cat "$1") +# # Modified regex to allow for more valid formats +# conventional_commit_regex='^(feat|fix|chore|docs|test|style|refactor|perf|build|ci|revert)(\(.+\))?(!)?(:)(\s.*)?$' -# check for conventional commit format -commit_msg=$(cat "$1") -# Regex to match Conventional Commit format -conventional_commit_regex='^(feat|fix|chore|docs|test|style|refactor|perf|build|ci|revert)(\(.+?\))?: .{1,}$' - -if ! [[ "$commit_msg" =~ $conventional_commit_regex ]]; then - echo "ERROR: Commit message does not adhere to Conventional Commits format:" - echo "$commit_msg" - exit 1 -fi +# echo "$conventional_commit_regex" +# if ! [[ "$commit_msg" =~ $conventional_commit_regex ]]; then +# echo "ERROR: Commit message does not adhere to Conventional Commits format:" +# echo "$commit_msg" +# echo "$($commit_msg =~ $conventional_commit_regex )" +# exit 1 +# fi # If not a merge commit, require commit to be signed off if [ ! -e .git/MERGE_MSG ] && ! grep -q "^Signed-off-by: " "$1"; then - echo >&2 "Commit message must be signed off with your user name and email." - echo >&2 "To sign off your commit, add the -s flag to the git commit command." - exit 1 + echo >&2 "Commit message must be signed off with your user name and email." + echo >&2 "To sign off your commit, add the -s flag to the git commit command." + exit 1 fi - - - - diff --git a/.husky/hooks/pre-commit b/.husky/hooks/pre-commit index ffc3b34..35d2694 100755 --- a/.husky/hooks/pre-commit +++ b/.husky/hooks/pre-commit @@ -1,11 +1,10 @@ #!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" -branch=`git symbolic-ref HEAD` +branch=$(git symbolic-ref HEAD) if [ "$branch" = "refs/heads/master" ]; then - echo "\\033[31mDirect commit to master is not allowed.\\033[0m" - exit 1 + echo "\\033[31mDirect commit to master is not allowed.\\033[0m" + exit 1 fi go fmt ./... diff --git a/.husky/hooks/pre-push b/.husky/hooks/pre-push index 2a4657e..864d02f 100755 --- a/.husky/hooks/pre-push +++ b/.husky/hooks/pre-push @@ -1,7 +1,6 @@ #!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" -branch=`git symbolic-ref HEAD` +branch=$(git symbolic-ref HEAD) if [ "$branch" = "refs/heads/master" ]; then echo "\\033[31mDirect push to master is not allowed.\\033[0m"