From 9e462831873074c66008ced4e5d53da31011f206 Mon Sep 17 00:00:00 2001 From: Pius Alfred Date: Sun, 24 Dec 2023 00:16:04 +0100 Subject: [PATCH] refactor --- _examples/chatter/chatter.go | 182 ------------------ _examples/chatter/text.go | 72 ------- _examples/listener/main.go | 114 ----------- _examples/messages/messages.go | 339 --------------------------------- base_client.go | 180 +++++++++++++++++ client.go | 306 ++++++++++++++--------------- client_test.go | 10 +- config.go | 59 ++++++ errors/errors.go | 2 +- http/http.go | 35 ++++ http/response.go | 8 +- media.go | 36 ++-- phone_numbers.go | 40 ++-- qr.go | 36 ++-- reply.go | 10 +- templates.go | 12 +- webhooks/webhooks.go | 2 +- whatsapp.go | 184 ++++-------------- 18 files changed, 536 insertions(+), 1091 deletions(-) delete mode 100644 _examples/chatter/chatter.go delete mode 100644 _examples/chatter/text.go delete mode 100644 _examples/listener/main.go delete mode 100644 _examples/messages/messages.go create mode 100644 base_client.go create mode 100644 config.go diff --git a/_examples/chatter/chatter.go b/_examples/chatter/chatter.go deleted file mode 100644 index 9a2bdf6..0000000 --- a/_examples/chatter/chatter.go +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright 2023 Pius Alfred - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the “Software”), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT - * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package main - -import ( - "context" - "errors" - "fmt" - "log/slog" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/julienschmidt/httprouter" - "github.com/piusalfred/whatsapp" - hooks "github.com/piusalfred/whatsapp/webhooks" -) - -var ErrInvalidToken = errors.New("invalid token") - -type Config struct { - BaseURL string - Version string - AccessToken string - PhoneNumberID string - BusinessAccountID string - WebhookSecret string - Port int -} - -type Service struct { - Config *Config - whatsapp *whatsapp.Client - Logger *slog.Logger -} - -func (svc *Service) WebhookSubVerifier() http.HandlerFunc { - verif := hooks.SubscriptionVerifier(func(ctx context.Context, request *hooks.VerificationRequest) error { - if svc.Config.WebhookSecret != request.Token { - return fmt.Errorf("%w: %s", ErrInvalidToken, request.Token) - } - - return nil - }) - - return verif.HandlerFunc -} - -// ListenAndServe starts the server and listens for incoming requests. -func (svc *Service) ListenAndServe(ctx context.Context) error { - listener := hooks.NewEventListener() - listener.OnTextMessage(svc.OnTextMessageHook) - router := httprouter.New() - router.HandlerFunc( - http.MethodGet, - "/webhooks", - svc.WebhookSubVerifier(), - ) - router.Handler( - http.MethodPost, - "/webhooks", - listener.NotificationHandler(), - ) - server := &http.Server{ - Addr: fmt.Sprintf(":%d", svc.Config.Port), - Handler: router, - DisableGeneralOptionsHandler: false, - TLSConfig: nil, - ReadTimeout: 0, - ReadHeaderTimeout: 0, - WriteTimeout: 0, - IdleTimeout: 0, - MaxHeaderBytes: 0, - TLSNextProto: nil, - ConnState: nil, - ErrorLog: slog.NewLogLogger(svc.Logger.Handler(), slog.LevelDebug), - BaseContext: nil, - ConnContext: nil, - } - - go func() { - // Waiting for context cancellation - <-ctx.Done() - - // Create a context for shutdown with a timeout - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer shutdownCancel() - - // Try to gracefully shut down the server - if err := server.Shutdown(shutdownCtx); err != nil { - svc.Logger.ErrorContext(shutdownCtx, "error during server shutdown:", err) - } - }() - - if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - return fmt.Errorf("listen and serve: %w", err) - } - - return nil -} - -func NewService(config *Config) (*Service, error) { - logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ - AddSource: true, - Level: slog.LevelDebug, - })) - - client, err := whatsapp.NewClientWithConfig(&whatsapp.Config{ - BaseURL: config.BaseURL, - Version: config.Version, - AccessToken: config.AccessToken, - PhoneNumberID: config.PhoneNumberID, - BusinessAccountID: config.BusinessAccountID, - }) - if err != nil { - return nil, fmt.Errorf("new client with config: %w", err) - } - service := &Service{ - Config: config, - whatsapp: client, - Logger: logger, - } - - return service, nil -} - -func Run(config *Config) error { - service, err := NewService(config) - if err != nil { - return fmt.Errorf("new service: %w", err) - } - - ctx, cancel := context.WithCancelCause(context.Background()) - - // Capture system signals to trigger a shutdown - go func() { - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) // Capture Ctrl+C and terminated signal - <-sigs - cancel(fmt.Errorf("received signal")) - }() - - defer cancel(nil) - - return service.ListenAndServe(ctx) -} - -func main() { - config := &Config{ - BaseURL: whatsapp.BaseURL, - Version: "v16.0", - AccessToken: "EAALLrT0ok6UBO6cpaRw0iHybYbwLq1xMtyMWXFVXJUJOHRgapUoA4PQwzksAqUQ4zqb3QJPsh9GXo8AACEe7hrFRjtpexY6qG05O9YQr2e1d0orrYIkD0B11mOK0llEls5KhDTsZCDPd1QqZAsbZAxZCX9jC65Aiz3gnzvaLx3nBGyXkA6aNZCPHYpxYryfpTtx7nkLsOvobzyvAZD", - PhoneNumberID: "114425371552711", - BusinessAccountID: "113592508304116", - WebhookSecret: "testtoken", - Port: 8080, - } - - if err := Run(config); err != nil { - fmt.Printf("error running service: %v\n", err) - os.Exit(1) - } -} diff --git a/_examples/chatter/text.go b/_examples/chatter/text.go deleted file mode 100644 index 621900c..0000000 --- a/_examples/chatter/text.go +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2023 Pius Alfred - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the “Software”), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT - * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package main - -import ( - "context" - "fmt" - "log/slog" - - "github.com/piusalfred/whatsapp" - "github.com/piusalfred/whatsapp/models" - hooks "github.com/piusalfred/whatsapp/webhooks" -) - -// OnTextMessageHook is a function that is called when a text message is received. -func (svc *Service) OnTextMessageHook( - ctx context.Context, - nctx *hooks.NotificationContext, - mctx *hooks.MessageContext, - text *hooks.Text, -) error { - svc.Logger.LogAttrs(ctx, slog.LevelInfo, "received text message", - slog.String("message_id", mctx.ID), - slog.String("sender", mctx.From), - slog.String("timestamp", mctx.Timestamp), - slog.String("type", mctx.Type), - ) - - name := nctx.Contacts[0].Profile.Name - message := &models.Text{ - PreviewURL: true, - Body: fmt.Sprintf("Hello %s, I am a bot, did you say %q?", name, text.Body), - } - reply, err := svc.whatsapp.Reply(ctx, &whatsapp.ReplyRequest{ - Recipient: mctx.From, - Context: mctx.ID, - MessageType: "text", - Content: message, - }) - if err != nil { - svc.Logger.LogAttrs(ctx, slog.LevelError, "failed to send reply", - slog.String("message_id", mctx.ID), - slog.String("sender", mctx.From), - slog.String("timestamp", mctx.Timestamp), - slog.String("type", mctx.Type), - ) - - return err - } - - svc.Logger.LogAttrs(ctx, slog.LevelInfo, "sent reply", - slog.Group("reply", reply)) - - return nil -} diff --git a/_examples/listener/main.go b/_examples/listener/main.go deleted file mode 100644 index cb7b5a5..0000000 --- a/_examples/listener/main.go +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2023 Pius Alfred - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of this software - * and associated documentation files (the “Software”), to deal in the Software without restriction, - * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all copies or substantial - * portions of the Software. - * - * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT - * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, - * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -package main - -import ( - "context" - "encoding/json" - "errors" - "io" - "log" - "net/http" - "os" - - "github.com/julienschmidt/httprouter" - hooks "github.com/piusalfred/whatsapp/webhooks" -) - -type verifier struct { - secret string - logger io.Writer - // other places where you pull the secret e.g database - // other fields for tracing and logging etc -} - -// This is first implementation -func (v *verifier) Verify(ctx context.Context, vr *hooks.VerificationRequest) error { - if vr.Token != v.secret { - log.Println("invalid token") - return errors.New("invalid token") - } - - log.Println("valid token") - return nil -} - -// This is second implementation -func VerifyFn(secret string) hooks.SubscriptionVerifier { - return func(ctx context.Context, vr *hooks.VerificationRequest) error { - if vr.Token != secret { - log.Println("invalid token") - return errors.New("invalid token") - } - log.Println("valid token") - return nil - } -} - -// func HandleNotificationError(ctx context.Context, writer http.ResponseWriter, request *http.Request, err error) error { -// if err != nil { -// log.Printf("HandleError: %+v\n", err) -// return err -// } - -// log.Printf("HandleError: NIL") -// return nil -// } - -func HandleGeneralNotification(ctx context.Context, writer http.ResponseWriter, notification *hooks.Notification) error { - os.Stdout.WriteString("HandleEvent") - jsonb, err := json.Marshal(notification) - if err != nil { - return err - } - // print the string representation of the json - // os.Stdout.WriteString(string(jsonb)) - log.Printf("\n%s\n", string(jsonb)) - writer.WriteHeader(http.StatusOK) - return nil -} - -func main() { - router := httprouter.New() - - h := VerifyFn("testtoken").HandlerFunc - router.HandlerFunc(http.MethodGet, "/webhooks", h) - /* - // verifyHandler2 - verifier := &verifier{ - secret: "mytesttoken", - logger: log.Writer(), - } - - verifyHandler2 := hooks.VerifySubscriptionHandler(verifier.Verify) - router.Handler(http.MethodGet, "/webhooks", verifyHandler2) - - */ - - listener := hooks.NewEventListener(hooks.WithGlobalNotificationHandler(HandleGeneralNotification)) - router.Handler(http.MethodPost, "/webhooks", listener.GlobalHandler()) - - //router.Handler(http.MethodPost, "/webhooks", http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // w.WriteHeader(http.StatusOK) - // - //}))) - - log.Fatal(http.ListenAndServe(":8080", router)) -} diff --git a/_examples/messages/messages.go b/_examples/messages/messages.go deleted file mode 100644 index 0c29fd2..0000000 --- a/_examples/messages/messages.go +++ /dev/null @@ -1,339 +0,0 @@ -package main - -import ( - "bufio" - "context" - "fmt" - "log/slog" - "net/http" - "os" - "os/signal" - "strings" - "time" - - "github.com/piusalfred/whatsapp" - whttp "github.com/piusalfred/whatsapp/http" - "github.com/piusalfred/whatsapp/models" -) - -func main() { - err := setup() - if err != nil { - fmt.Printf("error setting up: %v\n", err) //nolint:forbidigo - os.Exit(1) - } -} - -const quitMessage = `Press Ctrl+C to quit at any point to quit` - -var ErrInterrupted = fmt.Errorf("interrupted") - -// setup runs a simple interactive commandline tool to show that you have successfully -// configured your whatsapp business account to send messages. -func setup() error { - writer := os.Stdout - if _, err2 := writer.WriteString(quitMessage); err2 != nil { - return err2 - } - logger := slog.New(slog.NewTextHandler(writer, &slog.HandlerOptions{ - AddSource: true, - Level: slog.LevelDebug, - })) - hook := whttp.LogRequestHook(logger) - respHook := whttp.LogResponseHook(logger) - - ctx, cancel := context.WithCancelCause(context.Background()) - defer cancel(nil) - - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt) - go func() { - sig := <-sigChan - err := fmt.Errorf("%w: received signal: %v", ErrInterrupted, sig) - cancel(err) - }() - configer := &configer{reader: bufio.NewReader(os.Stdin)} - config, err := configer.Read(ctx) - if err != nil { - return err - } - client, err := whatsapp.NewClientWithConfig(config, - whatsapp.WithBaseClient(&whatsapp.BaseClient{Client: whttp.NewClient( - whttp.WithHTTPClient(http.DefaultClient), - whttp.WithRequestHooks(hook), - whttp.WithResponseHooks(respHook), - )})) - if err != nil { - return err - } - - reader := bufio.NewReader(os.Stdin) - fmt.Println("Enter the recipient phone number (this number must be registered in FB portal): ") - recipient, err := reader.ReadString('\n') - if err != nil { - return err - } - - fmt.Println("Sending Template Message (make sure you reply): ") - // Send a Template ( We will use the default template called hello_world) - tmpl := &whatsapp.Template{ - LanguageCode: "en_US", - LanguagePolicy: "", - Name: "hello_world", - Components: nil, - } - - response, err := client.SendTemplate(ctx, recipient, tmpl) - if err != nil { - return err - } - - logger.LogAttrs(ctx, slog.LevelInfo, "send template", slog.Group("response", response)) - - message := &whatsapp.TextMessage{ - Message: "😺Find me at https://github.com/piusalfred/whatsapp 👩🏻‍🦰", - PreviewURL: true, - } - - response, err = client.SendTextMessage(ctx, recipient, message) - if err != nil { - return fmt.Errorf("send message: %w", err) - } - - logger.LogAttrs(ctx, slog.LevelInfo, "send template", - slog.Group("response", response)) - - location := &models.Location{ - Longitude: -3.688344, - Latitude: 40.453053, - Name: "Estadio Santiago Bernabeu", - Address: "Av. de Concha Espina, 1, 28036 Madrid, Spain", - } - // - response, err = client.SendLocationMessage(ctx, recipient, location) - if err != nil { - fmt.Printf("error sending location message: %v\n", err) - os.Exit(1) - } - - fmt.Printf("response: %+v\n", response) - - name := &models.Name{ - FormattedName: "John Doe Jr", - FirstName: "John", - LastName: "Doe", - MiddleName: "Jackson", - Suffix: "Jr", - Prefix: "Dr", - } - - birthday := time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC) - homePhone := &models.Phone{ - Phone: "555-1234", - Type: "home", - WaID: "", - } - - workPhone := &models.Phone{ - Phone: "555-1234", - Type: "work", - } - - email := &models.Email{ - Email: "thejohndoejr@example.dummy", - Type: "work", - } - - organization := &models.Org{ - Company: "John Doe and Sons Co. LTD", - Department: "Serious Stuffs Department", - Title: "Commander In Chief", - } - // - address := &models.Address{ - Street: "123 Main St", - City: "Anytown", - State: "CA", - Zip: "12345", - Country: "United States", - CountryCode: "US", - Type: "home", - } - // - contact1 := models.NewContact("John Doe Jr", - models.WithContactName(name), - models.WithContactBirthdays(birthday), - models.WithContactPhones(homePhone, workPhone), - models.WithContactEmails(email), - models.WithContactOrganization(organization), - models.WithContactAddresses(address), - ) - - contacts := []*models.Contact{contact1, contact1} - - response, err = client.SendContacts(ctx, recipient, contacts) - if err != nil { - fmt.Printf("error sending contacts: %v\n", err) - os.Exit(1) - } - - fmt.Printf("response: %+v\n", response) - - //// Sending an image - media := &whatsapp.MediaMessage{ - Type: whatsapp.MediaTypeImage, - MediaLink: "https://cdn.pixabay.com/photo/2022/12/04/16/17/leaves-7634894_1280.jpg", - } - // - response, err = client.SendMedia(ctx, recipient, media, nil) - // - if err != nil { - fmt.Printf("error sending media: %v\n", err) - os.Exit(1) - } - // - fmt.Printf("response: %+v\n", response) - // - // - header := &models.InteractiveHeader{ - Text: "choose what you want to do", - Type: "image", - Image: &models.Media{ - Link: "https://cdn.pixabay.com/photo/2022/12/04/16/17/leaves-7634894_1280.jpg", - }, - } - - bodyText := ` - Real Madrid is one of the most successful football - clubs in the world, with a rich history and a proud tradition. -Founded in 1902, the club has won countless domestic and international -titles over the years, cementing its place among the greatest teams of all time. -With a squad of some of the most talented and skilled players in the world, -Real Madrid has consistently dominated the sport, winning a record 14 European -Champions League titles and 35 La Liga titles. The club's legendary players, -such as Cristiano Ronaldo, Zinedine Zidane, Alfredo Di Stefano, and Raul, -have left an indelible mark on the history of the game, and their legacy -continues to inspire generations of football fans around the world. With a -loyal and passionate fanbase, state-of-the-art facilities, and an unwavering -ccommitment to excellence, -Real Madrid is truly one of the greatest clubs in the history of football` - - replyButton1 := &models.InteractiveReplyButton{ - ID: "btn0001", - Title: "Real Madrid", - } - - replyButton2 := &models.InteractiveReplyButton{ - ID: "btn0002", - Title: "Barcelona", - } - - replyButton3 := &models.InteractiveReplyButton{ - ID: "btn0003", - Title: "Atletico Madrid", - } - - buttonsList := models.CreateInteractiveRelyButtonList( - replyButton1, replyButton2, replyButton3) - - action := &models.InteractiveAction{ - Button: "", - Buttons: buttonsList, - CatalogID: "", - ProductRetailerID: "", - Sections: nil, - } - - interactive := models.Interactive{ - Type: models.InteractiveMessageButton, - Action: action, - Header: header, - } - - models.WithInteractiveFooter("https://github.com/piusalfred/whatsapp")(&interactive) - models.WithInteractiveBody(bodyText)(&interactive) - - response, err = client.SendInteractiveMessage(ctx, recipient, &interactive) - if err != nil { - fmt.Printf("error sending interactive message: %v\n", err) - os.Exit(1) - } - - fmt.Printf("response: %+v\n", response) - - select { - case <-ctx.Done(): - return fmt.Errorf("interupted: %w", ctx.Err()) - - default: - return nil - } -} - -var _ whatsapp.ConfigReader = (*configer)(nil) - -// configer implements whatsapp.ConfigReaderFunc it basically asks user to enter the required -// configuration values via the commandline. -type configer struct { - reader *bufio.Reader -} - -func (c *configer) Read(ctx context.Context) (*whatsapp.Config, error) { - doneChan := make(chan struct{}, 1) - errChan := make(chan error, 1) - var config whatsapp.Config - - go func() { - fmt.Println("Enter your access token: ") - token, err := c.reader.ReadString('\n') - if err != nil { - errChan <- err - - return - } - config.AccessToken = strings.TrimSpace(token) - - fmt.Println("Enter your phone number ID: ") - phoneID, err := c.reader.ReadString('\n') - if err != nil { - errChan <- err - - return - } - - config.PhoneNumberID = strings.TrimSpace(phoneID) - - fmt.Println("Enter your business account ID: ") - businessID, err := c.reader.ReadString('\n') - if err != nil { - errChan <- err - - return - } - - config.BusinessAccountID = strings.TrimSpace(businessID) - - fmt.Println("Enter API version:(Lowest version is v16.0) ") - version, err := c.reader.ReadString('\n') - if err != nil { - errChan <- err - - return - } - - config.Version = strings.TrimSpace(version) - - doneChan <- struct{}{} - }() - - select { - case <-doneChan: - return &config, nil - - case err := <-errChan: - return nil, err - - case <-ctx.Done(): - return nil, fmt.Errorf("interrupted: %w", ctx.Err()) - } -} diff --git a/base_client.go b/base_client.go new file mode 100644 index 0000000..29a4c40 --- /dev/null +++ b/base_client.go @@ -0,0 +1,180 @@ +/* + * Copyright 2023 Pius Alfred + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package whatsapp + +import ( + "context" + "fmt" + "net/http" + + whttp "github.com/piusalfred/whatsapp/http" + "github.com/piusalfred/whatsapp/models" +) + +type ( + // BaseClient wraps the http client only and is used to make requests to the whatsapp api, + // It does not have the context. This is idealy for making requests to the whatsapp api for + // different users. The Client struct is used to make requests to the whatsapp api for a + // single user. + BaseClient struct { + base *whttp.Client + mw []SendMiddleware + } + + // BaseClientOption is a function that implements the BaseClientOption interface. + BaseClientOption func(*BaseClient) +) + +// WithBaseClientMiddleware adds a middleware to the base client. +func WithBaseClientMiddleware(mw ...SendMiddleware) BaseClientOption { + return func(client *BaseClient) { + client.mw = append(client.mw, mw...) + } +} + +// WithBaseHTTPClient sets the http client for the base client. +func WithBaseHTTPClient(httpClient *whttp.Client) BaseClientOption { + return func(client *BaseClient) { + client.base = httpClient + } +} + +// NewBaseClient creates a new base client. +func NewBaseClient(options ...BaseClientOption) *BaseClient { + b := &BaseClient{base: whttp.NewClient()} + + for _, option := range options { + option(b) + } + + return b +} + +func (c *BaseClient) Send(ctx context.Context, req *whttp.RequestContext, + message *models.Message, +) (*ResponseMessage, error) { + fs := WrapSender(SenderFunc(c.send), c.mw...) + + resp, err := fs.Send(ctx, req, message) + if err != nil { + return nil, fmt.Errorf("base client: %s: %w", req.Name, err) + } + + return resp, nil +} + +func (c *BaseClient) send(ctx context.Context, req *whttp.RequestContext, + msg *models.Message, +) (*ResponseMessage, error) { + request := &whttp.Request{ + Context: req, + Method: http.MethodPost, + Headers: map[string]string{"Content-Type": "application/json"}, + Bearer: req.Bearer, + Payload: msg, + } + + var resp ResponseMessage + err := c.base.Do(ctx, request, &resp) + if err != nil { + return nil, fmt.Errorf("%s: %w", req.Name, err) + } + + return &resp, nil +} + +func (c *BaseClient) MarkMessageRead(ctx context.Context, req *whttp.RequestContext, + messageID string, +) (*StatusResponse, error) { + reqBody := &MessageStatusUpdateRequest{ + MessagingProduct: MessagingProduct, + Status: MessageStatusRead, + MessageID: messageID, + } + + params := &whttp.Request{ + Context: req, + Method: http.MethodPost, + Headers: map[string]string{"Content-Type": "application/json"}, + Bearer: req.Bearer, + Payload: reqBody, + } + + var success StatusResponse + err := c.base.Do(ctx, params, &success) + if err != nil { + return nil, fmt.Errorf("mark message read: %w", err) + } + + return &success, nil +} + +var _ Sender = (*BaseClient)(nil) + +// Sender is an interface that represents a sender of a message. +type Sender interface { + Send(ctx context.Context, req *whttp.RequestContext, message *models.Message) (*ResponseMessage, error) +} + +// SenderFunc is a function that implements the Sender interface. +type SenderFunc func(ctx context.Context, req *whttp.RequestContext, + message *models.Message) (*ResponseMessage, error) + +// Send calls the function that implements the Sender interface. +func (f SenderFunc) Send(ctx context.Context, req *whttp.RequestContext, + message *models.Message) (*ResponseMessage, + error, +) { + return f(ctx, req, message) +} + +// SendMiddleware that takes a Sender and returns a new Sender that will wrap the original +// Sender and execute the middleware function before sending the message. +type SendMiddleware func(Sender) Sender + +// WrapSender wraps a Sender with a SendMiddleware. +func WrapSender(sender Sender, middleware ...SendMiddleware) Sender { + // iterate backwards so that the middleware is executed in the right order + for i := len(middleware) - 1; i >= 0; i-- { + sender = middleware[i](sender) + } + + return sender +} + +// TransparentClient is a client that can send messages to a recipient without knowing the configuration of the client. +// It uses Sender instead of already configured clients. It is ideal for having a client for different environments. +type TransparentClient struct { + Middlewares []SendMiddleware +} + +// Send sends a message to the recipient. +func (client *TransparentClient) Send(ctx context.Context, sender Sender, + req *whttp.RequestContext, message *models.Message, mw ...SendMiddleware, +) (*ResponseMessage, error) { + s := WrapSender(WrapSender(sender, client.Middlewares...), mw...) + + response, err := s.Send(ctx, req, message) + if err != nil { + return nil, fmt.Errorf("transparent client: %w", err) + } + + return response, nil +} diff --git a/client.go b/client.go index b66462c..b459f9d 100644 --- a/client.go +++ b/client.go @@ -22,91 +22,122 @@ package whatsapp import ( "context" "fmt" - "net/http" whttp "github.com/piusalfred/whatsapp/http" "github.com/piusalfred/whatsapp/models" ) -// Config is a struct that holds the configuration for the whatsapp client. -// It is used to create a new whatsapp client. -type Config struct { - BaseURL string - Version string - AccessToken string - PhoneNumberID string - BusinessAccountID string -} +const ( + MessageEndpoint = "messages" +) -// ConfigReader is an interface that can be used to read the configuration -// from a file or any other source. -type ConfigReader interface { - Read(ctx context.Context) (*Config, error) -} +var ErrConfigNil = fmt.Errorf("config is nil") -// ConfigReaderFunc is a function that implements the ConfigReader interface. -type ConfigReaderFunc func(ctx context.Context) (*Config, error) +type ( + // Client is a struct that holds the configuration for the whatsapp client. + // It is used to create a new whatsapp client for a single user. Uses the BaseClient + // to make requests to the whatsapp api. If you want a client that's flexible and can + // make requests to the whatsapp api for different users, use the TransparentClient. + Client struct { + bc *BaseClient + config *Config + } -// Read implements the ConfigReader interface. -func (fn ConfigReaderFunc) Read(ctx context.Context) (*Config, error) { - return fn(ctx) -} + ClientOption func(*Client) -type Client struct { - Base *BaseClient - Config *Config -} + ResponseMessage struct { + Product string `json:"messaging_product,omitempty"` + Contacts []*ResponseContact `json:"contacts,omitempty"` + Messages []*MessageID `json:"messages,omitempty"` + } + MessageID struct { + ID string `json:"id,omitempty"` + } -type ClientOption func(*Client) + ResponseContact struct { + Input string `json:"input"` + WhatsappID string `json:"wa_id"` + } -// NewClient creates a new whatsapp client with the given options. -func NewClient(reader ConfigReader, options ...ClientOption) (*Client, error) { - client := &Client{ - Base: &BaseClient{whttp.NewClient()}, + TextMessage struct { + Message string + PreviewURL bool } - config, err := reader.Read(context.Background()) - if err != nil || config == nil { - return nil, fmt.Errorf("failed to read config: %w", err) + ReactMessage struct { + MessageID string + Emoji string } - client.Config = config + TextTemplateRequest struct { + Name string + LanguageCode string + LanguagePolicy string + Body []*models.TemplateParameter + } - if client.Config.BaseURL == "" { - client.Config.BaseURL = BaseURL + Template struct { + LanguageCode string + LanguagePolicy string + Name string + Components []*models.TemplateComponent } - if client.Config.Version == "" { - client.Config.Version = LowestSupportedVersion + InteractiveTemplateRequest struct { + Name string + LanguageCode string + LanguagePolicy string + Headers []*models.TemplateParameter + Body []*models.TemplateParameter + Buttons []*models.InteractiveButtonTemplate } - for i, option := range options { - if option == nil { - return nil, fmt.Errorf("option at index %d is nil", i) - } - option(client) + MediaMessage struct { + Type MediaType + MediaID string + MediaLink string + Caption string + Filename string + Provider string } +) - return client, nil +func WithBaseClient(base *BaseClient) ClientOption { + return func(client *Client) { + client.bc = base + } +} + +func NewClient(reader ConfigReader, options ...ClientOption) (*Client, error) { + config, err := reader.Read(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to read config: %w", err) + } + + return NewClientWithConfig(config, options...) } func NewClientWithConfig(config *Config, options ...ClientOption) (*Client, error) { + if config == nil { + return nil, ErrConfigNil + } client := &Client{ - Base: &BaseClient{whttp.NewClient()}, - Config: config, + bc: NewBaseClient(), + config: config, } - if client.Config.BaseURL == "" { - client.Config.BaseURL = BaseURL + if client.config.BaseURL == "" { + client.config.BaseURL = BaseURL } - if client.Config.Version == "" { - client.Config.Version = LowestSupportedVersion + if client.config.Version == "" { + client.config.Version = LowestSupportedVersion } - for i, option := range options { + for _, option := range options { if option == nil { - return nil, fmt.Errorf("option at index %d is nil", i) + // skip nil options + continue } option(client) } @@ -114,27 +145,14 @@ func NewClientWithConfig(config *Config, options ...ClientOption) (*Client, erro return client, nil } -func WithBaseClient(base *BaseClient) ClientOption { - return func(client *Client) { - client.Base = base - } -} - -const MessageEndpoint = "messages" - -type TextMessage struct { - Message string - PreviewURL bool -} - -// SendTextMessage sends a text message to a WhatsApp Business Account. -func (client *Client) SendTextMessage(ctx context.Context, recipient string, +// SendText sends a text message to a WhatsApp Business Account. +func (client *Client) SendText(ctx context.Context, recipient string, message *TextMessage, ) (*ResponseMessage, error) { text := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: recipient, - RecipientType: individualRecipientType, + RecipientType: RecipientTypeIndividual, Type: textMessageType, Text: &models.Text{ PreviewURL: message.PreviewURL, @@ -142,34 +160,12 @@ func (client *Client) SendTextMessage(ctx context.Context, recipient string, }, } - req := &whttp.RequestContext{ - Name: "send text message", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, - Bearer: client.Config.AccessToken, - Endpoints: []string{MessageEndpoint}, - } - - return client.Base.Send(ctx, req, text) -} - -// MarkMessageRead sends a read receipt for a message. -func (client *Client) MarkMessageRead(ctx context.Context, messageID string) (*StatusResponse, error) { - req := &whttp.RequestContext{ - Name: "mark message read", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, - Endpoints: []string{MessageEndpoint}, + res, err := client.SendMessage(ctx, "send text", text) + if err != nil { + return nil, fmt.Errorf("failed to send text message: %w", err) } - return client.Base.MarkMessageRead(ctx, req, messageID) -} - -type ReactMessage struct { - MessageID string - Emoji string + return res, nil } // React sends a reaction to a message. @@ -214,7 +210,7 @@ type ReactMessage struct { // } func (client *Client) React(ctx context.Context, recipient string, msg *ReactMessage) (*ResponseMessage, error) { reaction := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: recipient, Type: reactionMessageType, Reaction: &models.Reaction{ @@ -223,15 +219,12 @@ func (client *Client) React(ctx context.Context, recipient string, msg *ReactMes }, } - req := &whttp.RequestContext{ - Name: "react to message", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, - Endpoints: []string{MessageEndpoint}, + res, err := client.SendMessage(ctx, "react", reaction) + if err != nil { + return nil, fmt.Errorf("failed to send reaction message: %w", err) } - return client.Base.Send(ctx, req, reaction) + return res, nil } // SendContacts sends a contact message. Contacts can be easily built using the models.NewContact() function. @@ -239,33 +232,33 @@ func (client *Client) SendContacts(ctx context.Context, recipient string, contac *ResponseMessage, error, ) { contact := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: recipient, - RecipientType: individualRecipientType, + RecipientType: RecipientTypeIndividual, Type: contactsMessageType, Contacts: contacts, } req := &whttp.RequestContext{ Name: "send contacts", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, - Bearer: client.Config.AccessToken, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, + Bearer: client.config.AccessToken, Endpoints: []string{MessageEndpoint}, } - return client.Base.Send(ctx, req, contact) + return client.bc.Send(ctx, req, contact) } -// SendLocationMessage sends a location message to a WhatsApp Business Account. -func (client *Client) SendLocationMessage(ctx context.Context, recipient string, +// SendLocation sends a location message to a WhatsApp Business Account. +func (client *Client) SendLocation(ctx context.Context, recipient string, message *models.Location, ) (*ResponseMessage, error) { location := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: recipient, - RecipientType: individualRecipientType, + RecipientType: RecipientTypeIndividual, Type: locationMessageType, Location: &models.Location{ Name: message.Name, @@ -277,65 +270,54 @@ func (client *Client) SendLocationMessage(ctx context.Context, recipient string, req := &whttp.RequestContext{ Name: "send location", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, - Bearer: client.Config.AccessToken, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, + Bearer: client.config.AccessToken, Endpoints: []string{MessageEndpoint}, } - return client.Base.Send(ctx, req, location) + return client.bc.Send(ctx, req, location) } -// BaseClient wraps the http client only and is used to make requests to the whatsapp api, -// It does not have the context. This is idealy for making requests to the whatsapp api for -// different users. The Client struct is used to make requests to the whatsapp api for a -// single user. -type BaseClient struct { - *whttp.Client -} - -func (base *BaseClient) Send(ctx context.Context, req *whttp.RequestContext, - msg *models.Message, -) (*ResponseMessage, error) { - request := &whttp.Request{ - Context: req, - Method: http.MethodPost, - Headers: map[string]string{"Content-Type": "application/json"}, - Bearer: req.Bearer, - Payload: msg, - } - var resp ResponseMessage - err := base.Do(ctx, request, &resp) - if err != nil { - return nil, fmt.Errorf("%s: %w", req.Name, err) +// SendMessage sends a message. +func (client *Client) SendMessage(ctx context.Context, name string, message *models.Message) ( + *ResponseMessage, error, +) { + req := &whttp.RequestContext{ + Name: name, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, + Bearer: client.config.AccessToken, + Endpoints: []string{MessageEndpoint}, } - return &resp, nil + return client.bc.Send(ctx, req, message) } -func (base *BaseClient) MarkMessageRead(ctx context.Context, req *whttp.RequestContext, - messageID string, -) (*StatusResponse, error) { - reqBody := &MessageStatusUpdateRequest{ - MessagingProduct: messagingProduct, - Status: MessageStatusRead, - MessageID: messageID, - } +// Whatsapp is an interface that represents a whatsapp client. +type Whatsapp interface { + SendText(ctx context.Context, recipient string, message *TextMessage) (*ResponseMessage, error) + React(ctx context.Context, recipient string, msg *ReactMessage) (*ResponseMessage, error) + SendContacts(ctx context.Context, recipient string, contacts []*models.Contact) (*ResponseMessage, error) + SendLocation(ctx context.Context, recipient string, location *models.Location) (*ResponseMessage, error) + SendInteractiveMessage(ctx context.Context, recipient string, req *models.Interactive) (*ResponseMessage, error) + SendTemplate(ctx context.Context, recipient string, template *Template) (*ResponseMessage, error) + SendMedia(ctx context.Context, recipient string, media *MediaMessage, options *CacheOptions) (*ResponseMessage, error) +} - params := &whttp.Request{ - Context: req, - Method: http.MethodPost, - Headers: map[string]string{"Content-Type": "application/json"}, - Bearer: req.Bearer, - Payload: reqBody, - } +var _ Whatsapp = (*Client)(nil) - var success StatusResponse - err := base.Do(ctx, params, &success) - if err != nil { - return nil, fmt.Errorf("mark message read: %w", err) +// MarkMessageRead sends a read receipt for a message. +func (client *Client) MarkMessageRead(ctx context.Context, messageID string) (*StatusResponse, error) { + req := &whttp.RequestContext{ + Name: "mark message read", + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, + Endpoints: []string{MessageEndpoint}, } - return &success, nil + return client.bc.MarkMessageRead(ctx, req, messageID) } diff --git a/client_test.go b/client_test.go index e6b9312..8485890 100644 --- a/client_test.go +++ b/client_test.go @@ -38,18 +38,18 @@ func ExampleNewClient() { }, nil }) - base := &BaseClient{whttp.NewClient( - whttp.WithHTTPClient(http.DefaultClient), + wc := whttp.NewClient(whttp.WithHTTPClient(http.DefaultClient), whttp.WithRequestHooks(), - whttp.WithResponseHooks(), - )} + whttp.WithResponseHooks()) + + base := NewBaseClient(WithBaseHTTPClient(wc)) client, err := NewClient(reader, WithBaseClient(base)) if err != nil { panic(err) } - fmt.Printf("BaseURL: %s\nVersion: %s", client.Config.BaseURL, client.Config.Version) + fmt.Printf("BaseURL: %s\nVersion: %s", client.config.BaseURL, client.config.Version) // Output: // BaseURL: https://graph.facebook.com/ diff --git a/config.go b/config.go new file mode 100644 index 0000000..7bf0c2e --- /dev/null +++ b/config.go @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Pius Alfred + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the “Software”), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package whatsapp + +import ( + "context" + "time" +) + +const ( + MessagingProduct = "whatsapp" + RecipientTypeIndividual = "individual" + BaseURL = "https://graph.facebook.com/" + LowestSupportedVersion = "v16.0" + DateFormatContactBirthday = time.DateOnly // YYYY-MM-DD +) + +type ( + // Config is a struct that holds the configuration for the whatsapp client. + // It is used to create a new whatsapp client + Config struct { + BaseURL string + Version string + AccessToken string + PhoneNumberID string + BusinessAccountID string + } + + // ConfigReader is an interface that can be used to read the configuration + // from a file or any other source. + ConfigReader interface { + Read(ctx context.Context) (*Config, error) + } + + // ConfigReaderFunc is a function that implements the ConfigReader interface. + ConfigReaderFunc func(ctx context.Context) (*Config, error) +) + +// Read implements the ConfigReader interface. +func (fn ConfigReaderFunc) Read(ctx context.Context) (*Config, error) { + return fn(ctx) +} diff --git a/errors/errors.go b/errors/errors.go index 2e818b9..0b8934b 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -133,5 +133,5 @@ func (e *Error) String() string { } func (e *Error) Error() string { - return fmt.Sprintf("whatsapp: %s", strings.ToLower(e.String())) + return fmt.Sprintf("whatsapp error: %s", strings.ToLower(e.String())) } diff --git a/http/http.go b/http/http.go index 6719046..f2ae721 100644 --- a/http/http.go +++ b/http/http.go @@ -61,6 +61,7 @@ func (client *Client) ListenErrors(errorHandler func(error)) { // Close closes the client. func (client *Client) Close() error { close(client.errorChannel) + return nil } @@ -156,6 +157,7 @@ func (client *Client) Do(ctx context.Context, r *Request, v any) error { if err != nil { return fmt.Errorf("prepare request: %w", err) } + response, err := client.http.Do(request) if err != nil { return fmt.Errorf("http send: %w", err) @@ -173,6 +175,7 @@ func (client *Client) Do(ctx context.Context, r *Request, v any) error { if err != nil && !errors.Is(err, io.EOF) { return fmt.Errorf("reading response body: %w", err) } + response.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) if err = runResponseHooks(ctx, response, client.responseHooks...); err != nil { @@ -190,6 +193,7 @@ func (client *Client) DoWithDecoder(ctx context.Context, r *Request, decoder Res if err != nil { return fmt.Errorf("prepare request: %w", err) } + response, err := client.http.Do(request) if err != nil { return fmt.Errorf("http send: %w", err) @@ -279,10 +283,12 @@ func runResponseHooks(ctx context.Context, response *http.Response, hooks ...Res func prepareRequest(ctx context.Context, r *Request, hooks ...RequestHook) (*http.Request, error) { // create a new request, run hooks and return the request after restoring the body ctx = withRequestName(ctx, r.Context.Name) + request, err := NewRequestWithContext(ctx, r) if err != nil { return nil, fmt.Errorf("prepare request: %w", err) } + // run request hooks for _, hook := range hooks { if hook != nil { @@ -291,11 +297,13 @@ func prepareRequest(ctx context.Context, r *Request, hooks ...RequestHook) (*htt } } } + // restore the request body body, err := r.BodyBytes() if err != nil { return nil, fmt.Errorf("prepare request: %w", err) } + request.Body = io.NopCloser(bytes.NewBuffer(body)) return request, nil @@ -330,6 +338,7 @@ type ( Bearer string Form map[string]string Payload any + Metadata map[string]string // This is used to pass metadata for other uses cases like logging, instrumentation etc. } RequestOption func(*Request) @@ -353,10 +362,32 @@ func (request *Request) LogValue() slog.Value { if request.Context != nil { reqURL, _ = url.JoinPath(request.Context.BaseURL, request.Context.Endpoints...) } + + var metadataAttr []any + + for key, value := range request.Metadata { + metadataAttr = append(metadataAttr, slog.String(key, value)) + } + + var headersAttr []any + + for key, value := range request.Headers { + headersAttr = append(headersAttr, slog.String(key, value)) + } + + var queryAttr []any + + for key, value := range request.Query { + queryAttr = append(queryAttr, slog.String(key, value)) + } + value := slog.GroupValue( slog.String("name", request.Context.Name), slog.String("method", request.Method), slog.String("url", reqURL), + slog.Group("metadata", metadataAttr...), + slog.Group("headers", headersAttr...), + slog.Group("query", queryAttr...), ) return value @@ -454,11 +485,14 @@ func (request *Request) BodyBytes() ([]byte, error) { if request.Payload == nil { return nil, nil } + body, err := request.ReaderFunc()() if err != nil { return nil, fmt.Errorf("reader func: %w", err) } + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(body) if err != nil { return nil, fmt.Errorf("read from: %w", err) @@ -505,6 +539,7 @@ func NewRequestWithContext(ctx context.Context, request *Request) (*http.Request if err != nil { return nil, fmt.Errorf("failed to create new request: %w", err) } + if request.BasicAuth != nil { req.SetBasicAuth(request.BasicAuth.Username, request.BasicAuth.Password) } diff --git a/http/response.go b/http/response.go index eb378a9..e3b8150 100644 --- a/http/response.go +++ b/http/response.go @@ -38,11 +38,15 @@ type ( ) // DecodeResponse calls f(response, v). -func (f RawResponseDecoder) DecodeResponse(response *http.Response, v interface{}) error { +func (f RawResponseDecoder) DecodeResponse(response *http.Response, + v interface{}, +) error { return f(response) } // DecodeResponse calls f(ctx, response, v). -func (f ResponseDecoderFunc) DecodeResponse(response *http.Response, v interface{}) error { +func (f ResponseDecoderFunc) DecodeResponse(response *http.Response, + v interface{}, +) error { return f(response, v) } diff --git a/media.go b/media.go index 6154169..6b4a537 100644 --- a/media.go +++ b/media.go @@ -76,21 +76,21 @@ type ( func (client *Client) GetMediaInformation(ctx context.Context, mediaID string) (*MediaInformation, error) { reqCtx := &whttp.RequestContext{ Name: "get media", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, Endpoints: []string{mediaID}, } params := &whttp.Request{ Context: reqCtx, Method: http.MethodGet, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, Payload: nil, } var media MediaInformation - err := client.Base.Do(ctx, params, &media) + err := client.bc.base.Do(ctx, params, &media) if err != nil { return nil, fmt.Errorf("get media: %w", err) } @@ -102,8 +102,8 @@ func (client *Client) GetMediaInformation(ctx context.Context, mediaID string) ( func (client *Client) DeleteMedia(ctx context.Context, mediaID string) (*DeleteMediaResponse, error) { reqCtx := &whttp.RequestContext{ Name: "delete media", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, Endpoints: []string{mediaID}, } @@ -111,12 +111,12 @@ func (client *Client) DeleteMedia(ctx context.Context, mediaID string) (*DeleteM Context: reqCtx, Method: http.MethodDelete, Headers: map[string]string{"Content-Type": "application/json"}, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, Payload: nil, } resp := new(DeleteMediaResponse) - err := client.Base.Do(ctx, params, &resp) + err := client.bc.base.Do(ctx, params, &resp) if err != nil { return nil, fmt.Errorf("delete media: %w", err) } @@ -134,21 +134,21 @@ func (client *Client) UploadMedia(ctx context.Context, mediaType MediaType, file reqCtx := &whttp.RequestContext{ Name: "upload media", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - Endpoints: []string{client.Config.PhoneNumberID, "media"}, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + Endpoints: []string{client.config.PhoneNumberID, "media"}, } params := &whttp.Request{ Context: reqCtx, Method: http.MethodPost, Headers: map[string]string{"Content-Type": contentType}, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, Payload: payload, } resp := new(UploadMediaResponse) - err = client.Base.Do(ctx, params, &resp) + err = client.bc.base.Do(ctx, params, &resp) if err != nil { return nil, fmt.Errorf("upload media: %w", err) } @@ -202,17 +202,17 @@ func (client *Client) DownloadMedia(ctx context.Context, mediaID string, retries whttp.WithRequestContext(&whttp.RequestContext{ Name: "download media", BaseURL: media.URL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, - Bearer: client.Config.AccessToken, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, + Bearer: client.config.AccessToken, BusinessAccountID: "", Endpoints: nil, }), whttp.WithRequestName("download media"), whttp.WithMethod(http.MethodGet), - whttp.WithBearer(client.Config.AccessToken)) + whttp.WithBearer(client.config.AccessToken)) decoder := &DownloadResponseDecoder{} - if err := client.Base.DoWithDecoder( + if err := client.bc.base.DoWithDecoder( ctx, request, whttp.RawResponseDecoder(decoder.Decode), diff --git a/phone_numbers.go b/phone_numbers.go index 2492ad5..0b3f289 100644 --- a/phone_numbers.go +++ b/phone_numbers.go @@ -93,9 +93,9 @@ func (client *Client) RequestVerificationCode(ctx context.Context, ) error { reqCtx := &whttp.RequestContext{ Name: "request code", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, Endpoints: []string{"request_code"}, } @@ -104,11 +104,11 @@ func (client *Client) RequestVerificationCode(ctx context.Context, Method: http.MethodPost, Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, Query: nil, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, Form: map[string]string{"code_method": string(codeMethod), "language": language}, Payload: nil, } - err := client.Base.Do(ctx, params, nil) + err := client.bc.base.Do(ctx, params, nil) if err != nil { return fmt.Errorf("failed to send request: %w", err) } @@ -120,9 +120,9 @@ func (client *Client) RequestVerificationCode(ctx context.Context, func (client *Client) VerifyCode(ctx context.Context, code string) (*StatusResponse, error) { reqCtx := &whttp.RequestContext{ Name: "verify code", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, Endpoints: []string{"verify_code"}, } params := &whttp.Request{ @@ -130,12 +130,12 @@ func (client *Client) VerifyCode(ctx context.Context, code string) (*StatusRespo Method: http.MethodPost, Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, Query: nil, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, Form: map[string]string{"code": code}, } var resp StatusResponse - err := client.Base.Do(ctx, params, &resp) + err := client.bc.base.Do(ctx, params, &resp) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } @@ -211,16 +211,16 @@ func (client *Client) VerifyCode(ctx context.Context, code string) (*StatusRespo func (client *Client) ListPhoneNumbers(ctx context.Context, filters []*FilterParams) (*PhoneNumbersList, error) { reqCtx := &whttp.RequestContext{ Name: "list phone numbers", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.BusinessAccountID, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.BusinessAccountID, Endpoints: []string{"phone_numbers"}, } params := &whttp.Request{ Context: reqCtx, Method: http.MethodGet, - Query: map[string]string{"access_token": client.Config.AccessToken}, + Query: map[string]string{"access_token": client.config.AccessToken}, } if filters != nil { p := filters @@ -231,7 +231,7 @@ func (client *Client) ListPhoneNumbers(ctx context.Context, filters []*FilterPar params.Query["filtering"] = string(jsonParams) } var phoneNumbersList PhoneNumbersList - err := client.Base.Do(ctx, params, &phoneNumbersList) + err := client.bc.base.Do(ctx, params, &phoneNumbersList) if err != nil { return nil, fmt.Errorf("failed to send request: %w", err) } @@ -243,19 +243,19 @@ func (client *Client) ListPhoneNumbers(ctx context.Context, filters []*FilterPar func (client *Client) PhoneNumberByID(ctx context.Context) (*PhoneNumber, error) { reqCtx := &whttp.RequestContext{ Name: "get phone number by id", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, } request := &whttp.Request{ Context: reqCtx, Method: http.MethodGet, Headers: map[string]string{ - "Authorization": "Bearer " + client.Config.AccessToken, + "Authorization": "Bearer " + client.config.AccessToken, }, } var phoneNumber PhoneNumber - if err := client.Base.Do(ctx, request, &phoneNumber); err != nil { + if err := client.bc.base.Do(ctx, request, &phoneNumber); err != nil { return nil, fmt.Errorf("get phone muber by id: %w", err) } diff --git a/qr.go b/qr.go index 2a0fdd7..0b6cb51 100644 --- a/qr.go +++ b/qr.go @@ -62,7 +62,7 @@ type ( } ) -func (base *BaseClient) Create(ctx context.Context, rtx *RequestContext, +func (c *BaseClient) Create(ctx context.Context, rtx *RequestContext, req *CreateRequest, ) (*CreateResponse, error) { queryParams := map[string]string{ @@ -85,7 +85,7 @@ func (base *BaseClient) Create(ctx context.Context, rtx *RequestContext, var response CreateResponse - err := base.Do(ctx, params, &response) + err := c.base.Do(ctx, params, &response) if err != nil { return nil, fmt.Errorf("qr code create: %w", err) } @@ -93,23 +93,23 @@ func (base *BaseClient) Create(ctx context.Context, rtx *RequestContext, return &response, nil } -func (base *BaseClient) List(ctx context.Context, rctx *RequestContext) (*ListResponse, error) { +func (c *BaseClient) List(ctx context.Context, request *RequestContext) (*ListResponse, error) { reqCtx := &whttp.RequestContext{ Name: "list qr codes", - BaseURL: rctx.BaseURL, - ApiVersion: rctx.ApiVersion, - PhoneNumberID: rctx.PhoneID, + BaseURL: request.BaseURL, + ApiVersion: request.ApiVersion, + PhoneNumberID: request.PhoneID, Endpoints: []string{"message_qrdls"}, } req := &whttp.Request{ Context: reqCtx, Method: http.MethodGet, - Query: map[string]string{"access_token": rctx.AccessToken}, + Query: map[string]string{"access_token": request.AccessToken}, } var response ListResponse - err := base.Do(ctx, req, &response) + err := c.base.Do(ctx, req, &response) if err != nil { return nil, fmt.Errorf("qr code list: %w", err) } @@ -126,7 +126,7 @@ type RequestContext struct { var ErrNoDataFound = fmt.Errorf("no data found") -func (base *BaseClient) Get(ctx context.Context, rctx *RequestContext, qrCodeID string, +func (c *BaseClient) Get(ctx context.Context, request *RequestContext, qrCodeID string, ) (*Information, error) { var ( list ListResponse @@ -134,19 +134,19 @@ func (base *BaseClient) Get(ctx context.Context, rctx *RequestContext, qrCodeID ) reqCtx := &whttp.RequestContext{ Name: "get qr code", - BaseURL: rctx.BaseURL, - ApiVersion: rctx.ApiVersion, - PhoneNumberID: rctx.PhoneID, + BaseURL: request.BaseURL, + ApiVersion: request.ApiVersion, + PhoneNumberID: request.PhoneID, Endpoints: []string{"message_qrdls", qrCodeID}, } req := &whttp.Request{ Context: reqCtx, Method: http.MethodGet, - Query: map[string]string{"access_token": rctx.AccessToken}, + Query: map[string]string{"access_token": request.AccessToken}, } - err := base.Do(ctx, req, &list) + err := c.base.Do(ctx, req, &list) if err != nil { return nil, fmt.Errorf("qr code get: %w", err) } @@ -160,7 +160,7 @@ func (base *BaseClient) Get(ctx context.Context, rctx *RequestContext, qrCodeID return &resp, nil } -func (base *BaseClient) UpdateQR(ctx context.Context, rtx *RequestContext, qrCodeID string, +func (c *BaseClient) UpdateQR(ctx context.Context, rtx *RequestContext, qrCodeID string, req *CreateRequest) (*SuccessResponse, error, ) { reqCtx := &whttp.RequestContext{ @@ -182,7 +182,7 @@ func (base *BaseClient) UpdateQR(ctx context.Context, rtx *RequestContext, qrCod } var resp SuccessResponse - err := base.Do(ctx, request, &resp) + err := c.base.Do(ctx, request, &resp) if err != nil { return nil, fmt.Errorf("qr code update (%s): %w", qrCodeID, err) } @@ -190,7 +190,7 @@ func (base *BaseClient) UpdateQR(ctx context.Context, rtx *RequestContext, qrCod return &resp, nil } -func (base *BaseClient) DeleteQR(ctx context.Context, rtx *RequestContext, qrCodeID string, +func (c *BaseClient) DeleteQR(ctx context.Context, rtx *RequestContext, qrCodeID string, ) (*SuccessResponse, error) { reqCtx := &whttp.RequestContext{ Name: "delete qr code", @@ -206,7 +206,7 @@ func (base *BaseClient) DeleteQR(ctx context.Context, rtx *RequestContext, qrCod Query: map[string]string{"access_token": rtx.AccessToken}, } var resp SuccessResponse - err := base.Do(ctx, req, &resp) + err := c.base.Do(ctx, req, &resp) if err != nil { return nil, fmt.Errorf("qr code delete: %w", err) } diff --git a/reply.go b/reply.go index c67e675..fc2408e 100644 --- a/reply.go +++ b/reply.go @@ -71,9 +71,9 @@ func (client *Client) Reply(ctx context.Context, request *ReplyRequest, } reqCtx := &whttp.RequestContext{ Name: "reply to message", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, Endpoints: []string{MessageEndpoint}, } @@ -82,13 +82,13 @@ func (client *Client) Reply(ctx context.Context, request *ReplyRequest, Method: http.MethodPost, Headers: map[string]string{"Content-Type": "application/json"}, Query: nil, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, Form: nil, Payload: payload, } var message ResponseMessage - err = client.Base.Do(ctx, req, &message) + err = client.bc.base.Do(ctx, req, &message) if err != nil { return nil, fmt.Errorf("reply: %w", err) } diff --git a/templates.go b/templates.go index ecf0ce4..37b8a17 100644 --- a/templates.go +++ b/templates.go @@ -89,12 +89,12 @@ type SendTemplateRequest struct { TemplateComponents []*models.TemplateComponent } -func (base *BaseClient) SendTemplate(ctx context.Context, req *SendTemplateRequest, +func (c *BaseClient) SendTemplate(ctx context.Context, req *SendTemplateRequest, ) (*ResponseMessage, error) { template := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: req.Recipient, - RecipientType: individualRecipientType, + RecipientType: RecipientTypeIndividual, Type: templateMessageType, Template: &models.Template{ Language: &models.TemplateLanguage{ @@ -122,7 +122,7 @@ func (base *BaseClient) SendTemplate(ctx context.Context, req *SendTemplateReque Bearer: req.AccessToken, } var message ResponseMessage - err := base.Do(ctx, params, &message) + err := c.base.Do(ctx, params, &message) if err != nil { return nil, fmt.Errorf("send template: %w", err) } @@ -261,7 +261,7 @@ downloaded successfully. }] } */ -func (base *BaseClient) SendMedia(ctx context.Context, req *SendMediaRequest, +func (c *BaseClient) SendMedia(ctx context.Context, req *SendMediaRequest, ) (*ResponseMessage, error) { if req == nil { return nil, fmt.Errorf("request is nil: %w", ErrBadRequestFormat) @@ -304,7 +304,7 @@ func (base *BaseClient) SendMedia(ctx context.Context, req *SendMediaRequest, var message ResponseMessage - err = base.Do(ctx, params, &message) + err = c.base.Do(ctx, params, &message) if err != nil { return nil, fmt.Errorf("send media: %w", err) } diff --git a/webhooks/webhooks.go b/webhooks/webhooks.go index 61c8447..ed462d6 100644 --- a/webhooks/webhooks.go +++ b/webhooks/webhooks.go @@ -113,7 +113,7 @@ type ( } OnOrderMessageHook func( - ctx context.Context, nctx *NotificationContext, mctx *MessageContext, order *Order) error + context.Context, *NotificationContext, *MessageContext, *Order) error OnButtonMessageHook func( ctx context.Context, nctx *NotificationContext, mctx *MessageContext, button *Button) error OnLocationMessageHook func( diff --git a/whatsapp.go b/whatsapp.go index 04077bd..dc93fd5 100644 --- a/whatsapp.go +++ b/whatsapp.go @@ -33,14 +33,6 @@ import ( var ErrBadRequestFormat = errors.New("bad request") -const ( - messagingProduct = "whatsapp" - individualRecipientType = "individual" - BaseURL = "https://graph.facebook.com/" - LowestSupportedVersion = "v16.0" - ContactBirthDayDateFormat = time.DateOnly // YYYY-MM-DD -) - const ( templateMessageType = "template" textMessageType = "text" @@ -88,19 +80,6 @@ func MediaMaxAllowedSize(mediaType MediaType) int { } type ( - ResponseMessage struct { - Product string `json:"messaging_product,omitempty"` - Contacts []*ResponseContact `json:"contacts,omitempty"` - Messages []*MessageID `json:"messages,omitempty"` - } - MessageID struct { - ID string `json:"id,omitempty"` - } - - ResponseContact struct { - Input string `json:"input"` - WhatsappID string `json:"wa_id"` - } // MessageType represents the type of message currently supported. // Which are Text messages,Reaction messages,MediaInformation messages,Location messages,Contact messages, @@ -134,15 +113,6 @@ func (r *ResponseMessage) LogValue() slog.Value { var _ slog.LogValuer = (*ResponseMessage)(nil) -type MediaMessage struct { - Type MediaType - MediaID string - MediaLink string - Caption string - Filename string - Provider string -} - // SendMedia sends a media message to the recipient. Media can be sent using ID or Link. If using id, you must // first upload your media asset to our servers and capture the returned media ID. If using link, your asset must // be on a publicly accessible server or the message will fail to send. @@ -150,10 +120,10 @@ func (client *Client) SendMedia(ctx context.Context, recipient string, req *Medi cacheOptions *CacheOptions, ) (*ResponseMessage, error) { request := &SendMediaRequest{ - BaseURL: client.Config.BaseURL, - AccessToken: client.Config.AccessToken, - PhoneNumberID: client.Config.PhoneNumberID, - ApiVersion: client.Config.Version, + BaseURL: client.config.BaseURL, + AccessToken: client.config.AccessToken, + PhoneNumberID: client.config.PhoneNumberID, + ApiVersion: client.config.Version, Recipient: recipient, Type: req.Type, MediaID: req.MediaID, @@ -201,7 +171,7 @@ func (client *Client) SendMedia(ctx context.Context, recipient string, req *Medi var message ResponseMessage - err = client.Base.Do(ctx, params, &message) + err = client.bc.base.Do(ctx, params, &message) if err != nil { return nil, fmt.Errorf("send media: %w", err) } @@ -209,22 +179,6 @@ func (client *Client) SendMedia(ctx context.Context, recipient string, req *Medi return &message, nil } -type Template struct { - LanguageCode string - LanguagePolicy string - Name string - Components []*models.TemplateComponent -} - -type InteractiveTemplateRequest struct { - Name string - LanguageCode string - LanguagePolicy string - Headers []*models.TemplateParameter - Body []*models.TemplateParameter - Buttons []*models.InteractiveButtonTemplate -} - // SendInteractiveTemplate send an interactive template message which contains some buttons for user intraction. // Interactive message templates expand the content you can send recipients beyond the standard message template // and media messages template types to include interactive buttons using the components object. There are two types @@ -244,17 +198,17 @@ func (client *Client) SendInteractiveTemplate(ctx context.Context, recipient str } template := models.NewInteractiveTemplate(req.Name, tmpLanguage, req.Headers, req.Body, req.Buttons) payload := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: recipient, - RecipientType: individualRecipientType, + RecipientType: RecipientTypeIndividual, Type: templateMessageType, Template: template, } reqCtx := &whttp.RequestContext{ Name: "send template", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, Endpoints: []string{"messages"}, } params := &whttp.Request{ @@ -264,10 +218,10 @@ func (client *Client) SendInteractiveTemplate(ctx context.Context, recipient str Headers: map[string]string{ "Content-Type": "application/json", }, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, } var message ResponseMessage - err := client.Base.Do(ctx, params, &message) + err := client.bc.base.Do(ctx, params, &message) if err != nil { return nil, fmt.Errorf("send template: %w", err) } @@ -294,18 +248,18 @@ func (client *Client) SendMediaTemplate(ctx context.Context, recipient string, r } template := models.NewMediaTemplate(req.Name, tmpLanguage, req.Header, req.Body) payload := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: recipient, - RecipientType: individualRecipientType, + RecipientType: RecipientTypeIndividual, Type: templateMessageType, Template: template, } reqCtx := &whttp.RequestContext{ Name: "send media template", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, Endpoints: []string{"messages"}, } @@ -316,11 +270,11 @@ func (client *Client) SendMediaTemplate(ctx context.Context, recipient string, r Headers: map[string]string{ "Content-Type": "application/json", }, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, } var message ResponseMessage - err := client.Base.Do(ctx, params, &message) + err := client.bc.base.Do(ctx, params, &message) if err != nil { return nil, fmt.Errorf("client: send media template: %w", err) } @@ -328,13 +282,6 @@ func (client *Client) SendMediaTemplate(ctx context.Context, recipient string, r return &message, nil } -type TextTemplateRequest struct { - Name string - LanguageCode string - LanguagePolicy string - Body []*models.TemplateParameter -} - // SendTextTemplate sends a text template message to the recipient. This kind of template message has a text // message as a header. This is its main distinguishing feature from the media based template message. func (client *Client) SendTextTemplate(ctx context.Context, recipient string, req *TextTemplateRequest) ( @@ -348,9 +295,9 @@ func (client *Client) SendTextTemplate(ctx context.Context, recipient string, re payload := models.NewMessage(recipient, models.WithTemplate(template)) reqCtx := &whttp.RequestContext{ Name: "send text template", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, Endpoints: []string{"messages"}, } @@ -361,11 +308,11 @@ func (client *Client) SendTextTemplate(ctx context.Context, recipient string, re Headers: map[string]string{ "Content-Type": "application/json", }, - Bearer: client.Config.AccessToken, + Bearer: client.config.AccessToken, } var message ResponseMessage - err := client.Base.Do(ctx, params, &message) + err := client.bc.base.Do(ctx, params, &message) if err != nil { return nil, fmt.Errorf("client: send text template: %w", err) } @@ -383,9 +330,9 @@ func (client *Client) SendTemplate(ctx context.Context, recipient string, templa *ResponseMessage, error, ) { message := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: recipient, - RecipientType: individualRecipientType, + RecipientType: RecipientTypeIndividual, Type: templateMessageType, Template: &models.Template{ Language: &models.TemplateLanguage{ @@ -399,14 +346,14 @@ func (client *Client) SendTemplate(ctx context.Context, recipient string, templa req := &whttp.RequestContext{ Name: "send message", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, - Bearer: client.Config.AccessToken, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, + Bearer: client.config.AccessToken, Endpoints: []string{"messages"}, } - return client.Base.Send(ctx, req, message) + return client.bc.Send(ctx, req, message) } // SendInteractiveMessage sends an interactive message to the recipient. @@ -414,76 +361,21 @@ func (client *Client) SendInteractiveMessage(ctx context.Context, recipient stri *ResponseMessage, error, ) { template := &models.Message{ - Product: messagingProduct, + Product: MessagingProduct, To: recipient, - RecipientType: individualRecipientType, + RecipientType: RecipientTypeIndividual, Type: "interactive", Interactive: req, } reqc := &whttp.RequestContext{ Name: "send interactive message", - BaseURL: client.Config.BaseURL, - ApiVersion: client.Config.Version, - PhoneNumberID: client.Config.PhoneNumberID, - Bearer: client.Config.AccessToken, + BaseURL: client.config.BaseURL, + ApiVersion: client.config.Version, + PhoneNumberID: client.config.PhoneNumberID, + Bearer: client.config.AccessToken, Endpoints: []string{"messages"}, } - return client.Base.Send(ctx, reqc, template) -} - -var _ Sender = (*BaseClient)(nil) - -// Sender is an interface that represents a sender of a message. -type Sender interface { - Send(ctx context.Context, req *whttp.RequestContext, message *models.Message) (*ResponseMessage, error) -} - -// SenderFunc is a function that implements the Sender interface. -type SenderFunc func(ctx context.Context, req *whttp.RequestContext, message *models.Message) (*ResponseMessage, error) - -// Send calls the function that implements the Sender interface. -func (f SenderFunc) Send(ctx context.Context, req *whttp.RequestContext, message *models.Message) (*ResponseMessage, - error) { - return f(ctx, req, message) -} - -// SendMiddleware that takes a Sender and returns a new Sender that will wrap the original Sender and execute the -// middleware function before sending the message. -type SendMiddleware func(Sender) Sender - -// WrapSender wraps a Sender with a SendMiddleware. -func WrapSender(sender Sender, middleware ...SendMiddleware) Sender { - // iterate backwards so that the middleware is executed in the right order - for i := len(middleware) - 1; i >= 0; i-- { - sender = middleware[i](sender) - } - - return sender -} - -// TransparentClient is a client that can send messages to a recipient without knowing the configuration of the client. -// It uses Sender instead of already configured clients. It is ideal for having a client for different environments. -type TransparentClient struct { - Middlewares []SendMiddleware -} - -// Send sends a message to the recipient. -func (client *TransparentClient) Send(ctx context.Context, sender Sender, - req *whttp.RequestContext, message *models.Message, mw ...SendMiddleware) (*ResponseMessage, error) { - - s := WrapSender(sender, client.Middlewares...) - - return s.Send(ctx, req, message) -} - -// Whatsapp is an interface that represents a whatsapp client. -type Whatsapp interface { - SendText(ctx context.Context, recipient string, message *models.Text) (*ResponseMessage, error) - React(ctx context.Context, recipient string, msg *ReactMessage) (*ResponseMessage, error) - SendContacts(ctx context.Context, recipient string, contacts []*models.Contact) (*ResponseMessage, error) - SendLocation(ctx context.Context, recipient string, location *models.Location) (*ResponseMessage, error) - SendInteractiveMessage(ctx context.Context, recipient string, req *models.Interactive) (*ResponseMessage, error) - SendMedia(ctx context.Context, recipient string, media *models.Media) (*ResponseMessage, error) + return client.bc.Send(ctx, reqc, template) }