Files
whatshooked/pkg/whatsapp/businessapi/sending.go
Hein a7a5831911
Some checks failed
CI / Test (1.23) (push) Failing after -24m15s
CI / Test (1.22) (push) Failing after -24m12s
CI / Build (push) Successful in -26m47s
CI / Lint (push) Successful in -26m36s
feat(whatsapp): 🎉 Add extended sending and template management
* Implemented new endpoints for sending various message types:
  - Audio
  - Sticker
  - Location
  - Contacts
  - Interactive messages
  - Template messages
  - Flow messages
  - Reactions
  - Marking messages as read

* Added template management endpoints:
  - List templates
  - Upload templates
  - Delete templates

* Introduced flow management endpoints:
  - List flows
  - Create flows
  - Get flow details
  - Upload flow assets
  - Publish flows
  - Delete flows

* Added phone number management endpoints:
  - List phone numbers
  - Request verification code
  - Verify code

* Enhanced media management with delete media endpoint.

* Updated dependencies.
2026-02-03 18:07:42 +02:00

268 lines
8.2 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 business account.
func (c *Client) ListPhoneNumbers(ctx context.Context) (*PhoneNumberListResponse, error) {
if c.config.BusinessAccountID == "" {
return nil, errNoBusinessAccount
}
params := url.Values{
"fields": {"id,display_phone_number,phone_number,verified_name,code_verification_status,quality_rating,throughput"},
}
var resp PhoneNumberListResponse
if err := c.graphAPIGet(ctx, c.config.BusinessAccountID+"/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) error {
body := map[string]string{
"verification_method": method,
}
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)
}
// DeleteMedia deletes a previously uploaded media file.
func (c *Client) DeleteMedia(ctx context.Context, mediaID string) error {
return c.graphAPIDelete(ctx, mediaID, nil)
}