From b34d46e358856366a81c3b80a0d8fc0dcb817cbe Mon Sep 17 00:00:00 2001 From: sarthakjdev Date: Tue, 28 May 2024 17:57:32 +0530 Subject: [PATCH] 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, + } }