289 lines
8.9 KiB
Go
289 lines
8.9 KiB
Go
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)
|
|
}
|