package businessapi import ( "context" "errors" "fmt" "net/url" "git.warky.dev/wdevs/whatshooked/pkg/events" "git.warky.dev/wdevs/whatshooked/pkg/logging" "go.mau.fi/whatsmeow/types" ) var errNoBusinessAccount = errors.New("business_account_id is required for this operation") // SendAudio sends an audio message via Business API. func (c *Client) SendAudio(ctx context.Context, jid types.JID, audioData []byte, mimeType string) (string, error) { if ctx == nil { ctx = context.Background() } phoneNumber := jidToPhoneNumber(jid) mediaID, err := c.uploadMedia(ctx, audioData, mimeType) if err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, "", err)) return "", fmt.Errorf("failed to upload audio: %w", err) } reqBody := SendMessageRequest{ MessagingProduct: "whatsapp", To: phoneNumber, Type: "audio", Audio: &MediaObject{ID: mediaID}, } messageID, err := c.sendMessage(ctx, reqBody) if err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, "", err)) return "", err } logging.Debug("Audio sent via Business API", "account_id", c.id, "to", phoneNumber) c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, "")) return messageID, nil } // SendSticker sends a sticker message via Business API. func (c *Client) SendSticker(ctx context.Context, jid types.JID, stickerData []byte, mimeType string) (string, error) { if ctx == nil { ctx = context.Background() } phoneNumber := jidToPhoneNumber(jid) mediaID, err := c.uploadMedia(ctx, stickerData, mimeType) if err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, "", err)) return "", fmt.Errorf("failed to upload sticker: %w", err) } reqBody := SendMessageRequest{ MessagingProduct: "whatsapp", To: phoneNumber, Type: "sticker", Sticker: &MediaObject{ID: mediaID}, } messageID, err := c.sendMessage(ctx, reqBody) if err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, "", err)) return "", err } logging.Debug("Sticker sent via Business API", "account_id", c.id, "to", phoneNumber) c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, "")) return messageID, nil } // SendLocation sends a location message via Business API. func (c *Client) SendLocation(ctx context.Context, jid types.JID, latitude, longitude float64, name, address string) (string, error) { if ctx == nil { ctx = context.Background() } phoneNumber := jidToPhoneNumber(jid) reqBody := SendMessageRequest{ MessagingProduct: "whatsapp", To: phoneNumber, Type: "location", Location: &LocationObject{ Latitude: latitude, Longitude: longitude, Name: name, Address: address, }, } messageID, err := c.sendMessage(ctx, reqBody) if err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, "", err)) return "", err } logging.Debug("Location sent via Business API", "account_id", c.id, "to", phoneNumber) c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, fmt.Sprintf("%.6f,%.6f", latitude, longitude))) return messageID, nil } // SendContacts sends one or more contact cards. func (c *Client) SendContacts(ctx context.Context, jid types.JID, contacts []SendContactObject) (string, error) { if ctx == nil { ctx = context.Background() } phoneNumber := jidToPhoneNumber(jid) reqBody := SendMessageRequest{ MessagingProduct: "whatsapp", To: phoneNumber, Type: "contacts", Contacts: contacts, } messageID, err := c.sendMessage(ctx, reqBody) if err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, "", err)) return "", err } logging.Debug("Contacts sent via Business API", "account_id", c.id, "to", phoneNumber) c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, "")) return messageID, nil } // SendInteractive sends an interactive message (buttons, list, or flow). func (c *Client) SendInteractive(ctx context.Context, jid types.JID, interactive *InteractiveObject) (string, error) { if ctx == nil { ctx = context.Background() } phoneNumber := jidToPhoneNumber(jid) reqBody := SendMessageRequest{ MessagingProduct: "whatsapp", To: phoneNumber, Type: "interactive", Interactive: interactive, } messageID, err := c.sendMessage(ctx, reqBody) if err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, "", err)) return "", err } logging.Debug("Interactive message sent via Business API", "account_id", c.id, "to", phoneNumber, "type", interactive.Type) c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, "")) return messageID, nil } // SendTemplateMessage sends a template message. func (c *Client) SendTemplateMessage(ctx context.Context, jid types.JID, tmpl *TemplateMessageObject) (string, error) { if ctx == nil { ctx = context.Background() } phoneNumber := jidToPhoneNumber(jid) reqBody := SendMessageRequest{ MessagingProduct: "whatsapp", To: phoneNumber, Type: "template", Template: tmpl, } messageID, err := c.sendMessage(ctx, reqBody) if err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, "", err)) return "", err } logging.Debug("Template message sent via Business API", "account_id", c.id, "to", phoneNumber, "template", tmpl.Name) c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, tmpl.Name)) return messageID, nil } // SendReaction sends a reaction (emoji) to an existing message. func (c *Client) SendReaction(ctx context.Context, jid types.JID, messageID string, emoji string) error { if ctx == nil { ctx = context.Background() } phoneNumber := jidToPhoneNumber(jid) msg := ReactionMessage{ MessagingProduct: "whatsapp", MessageType: "reaction", Reaction: ReactionObject{ MessageID: messageID, Emoji: emoji, }, } if _, err := c.postToMessagesEndpoint(ctx, msg); err != nil { c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, emoji, err)) return err } logging.Debug("Reaction sent via Business API", "account_id", c.id, "to", phoneNumber, "emoji", emoji) return nil } // MarkAsRead marks a received message as read. func (c *Client) MarkAsRead(ctx context.Context, messageID string) error { if ctx == nil { ctx = context.Background() } msg := ReadReceiptMessage{ MessagingProduct: "whatsapp", Status: "read", MessageID: messageID, } if _, err := c.postToMessagesEndpoint(ctx, msg); err != nil { return err } logging.Debug("Message marked as read via Business API", "account_id", c.id, "message_id", messageID) return nil } // ListPhoneNumbers returns all phone numbers for the WhatsApp Business Account. // The WABA ID is resolved from the phone number at connect time; it is distinct from // the Facebook Business Manager ID stored in business_account_id. func (c *Client) ListPhoneNumbers(ctx context.Context) (*PhoneNumberListResponse, error) { wabaID := c.wabaID if wabaID == "" { // Fallback: resolve on demand if Connect() was not called id, err := c.fetchWABAID(ctx) if err != nil { return nil, fmt.Errorf("could not resolve WABA ID: %w", err) } c.wabaID = id wabaID = id } params := url.Values{ "fields": {"id,display_phone_number,verified_name,code_verification_status,quality_rating,throughput"}, } var resp PhoneNumberListResponse if err := c.graphAPIGet(ctx, wabaID+"/phone_numbers", params, &resp); err != nil { return nil, err } return &resp, nil } // RequestVerificationCode sends a verification code (SMS or VOICE) to the given phone number. func (c *Client) RequestVerificationCode(ctx context.Context, phoneNumberID string, method string, language string) error { body := map[string]string{ "code_method": method, "language": language, } var resp RequestCodeResponse return c.graphAPIPost(ctx, phoneNumberID+"/request_code", body, &resp) } // VerifyCode verifies a phone number with the code received via SMS/VOICE. func (c *Client) VerifyCode(ctx context.Context, phoneNumberID string, code string) error { body := VerifyCodeRequest{Code: code} var resp VerifyCodeResponse return c.graphAPIPost(ctx, phoneNumberID+"/verify_code", body, &resp) } // RegisterPhoneNumber registers a phone number for use with the WhatsApp Cloud API. // POST /{phone-number-id}/register func (c *Client) RegisterPhoneNumber(ctx context.Context, phoneNumberID string, pin string) error { body := RegisterPhoneNumberRequest{ MessagingProduct: "whatsapp", Pin: pin, } var resp RegisterPhoneNumberResponse return c.graphAPIPost(ctx, phoneNumberID+"/register", body, &resp) } // DeleteMedia deletes a previously uploaded media file. func (c *Client) DeleteMedia(ctx context.Context, mediaID string) error { return c.graphAPIDelete(ctx, mediaID, nil) }