feat(api): 🎉 Add business profile and catalog management
* Implement endpoints for managing business profiles: - Get business profile - Update business profile * Add catalog management features: - List catalogs - List products in a catalog - Send catalog messages - Send single product messages - Send product list messages * Introduce media upload functionality for sending media files. * Add flow management capabilities: - Deprecate flows * Update API documentation to reflect new endpoints and features.
This commit is contained in:
172
pkg/whatsapp/businessapi/catalog.go
Normal file
172
pkg/whatsapp/businessapi/catalog.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package businessapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/events"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
||||
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
)
|
||||
|
||||
// ListCatalogs returns all product catalogs linked to the business account.
|
||||
func (c *Client) ListCatalogs(ctx context.Context) (*CatalogListResponse, error) {
|
||||
if c.config.BusinessAccountID == "" {
|
||||
return nil, errNoBusinessAccount
|
||||
}
|
||||
|
||||
params := url.Values{
|
||||
"fields": {"id,name,product_count"},
|
||||
}
|
||||
|
||||
var resp CatalogListResponse
|
||||
if err := c.graphAPIGet(ctx, c.config.BusinessAccountID+"/catalogs", params, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// ListProducts returns products in a specific catalog.
|
||||
func (c *Client) ListProducts(ctx context.Context, catalogID string) (*ProductListResponse, error) {
|
||||
params := url.Values{
|
||||
"fields": {"product_retailer_id,name,description,image_url,base_price,currency,availability,category"},
|
||||
}
|
||||
|
||||
var resp ProductListResponse
|
||||
if err := c.graphAPIGet(ctx, catalogID+"/products", params, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
// SendCatalogMessage sends a catalog message that shares the full product catalog.
|
||||
// thumbnailProductRetailerID is optional — when non-empty it sets which product image
|
||||
// appears as the catalog preview thumbnail.
|
||||
func (c *Client) SendCatalogMessage(ctx context.Context, jid types.JID, bodyText string, thumbnailProductRetailerID string) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
phoneNumber := jidToPhoneNumber(jid)
|
||||
|
||||
action := map[string]any{
|
||||
"name": "catalog_message",
|
||||
}
|
||||
if thumbnailProductRetailerID != "" {
|
||||
action["parameters"] = map[string]any{
|
||||
"thumbnail_product_retailer_id": thumbnailProductRetailerID,
|
||||
}
|
||||
}
|
||||
|
||||
msg := map[string]any{
|
||||
"messaging_product": "whatsapp",
|
||||
"to": phoneNumber,
|
||||
"type": "interactive",
|
||||
"interactive": map[string]any{
|
||||
"type": "catalog_message",
|
||||
"body": map[string]any{"text": bodyText},
|
||||
"action": action,
|
||||
},
|
||||
}
|
||||
|
||||
messageID, err := c.postToMessagesEndpoint(ctx, msg)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, bodyText, err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
logging.Debug("Catalog message sent via Business API", "account_id", c.id, "to", phoneNumber)
|
||||
c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, bodyText))
|
||||
return messageID, nil
|
||||
}
|
||||
|
||||
// SendSingleProduct sends a single-product interactive message.
|
||||
func (c *Client) SendSingleProduct(ctx context.Context, jid types.JID, catalogID, productRetailerID, bodyText, footerText string) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
phoneNumber := jidToPhoneNumber(jid)
|
||||
|
||||
interactive := map[string]any{
|
||||
"type": "product",
|
||||
"header": map[string]any{
|
||||
"type": "product",
|
||||
"product_retailer_id": productRetailerID,
|
||||
},
|
||||
"body": map[string]any{"text": bodyText},
|
||||
"action": map[string]any{
|
||||
"catalog_id": catalogID,
|
||||
"product_retailer_id": productRetailerID,
|
||||
},
|
||||
}
|
||||
if footerText != "" {
|
||||
interactive["footer"] = map[string]any{"text": footerText}
|
||||
}
|
||||
|
||||
msg := map[string]any{
|
||||
"messaging_product": "whatsapp",
|
||||
"to": phoneNumber,
|
||||
"type": "interactive",
|
||||
"interactive": interactive,
|
||||
}
|
||||
|
||||
messageID, err := c.postToMessagesEndpoint(ctx, msg)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, bodyText, err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
logging.Debug("Single product sent via Business API", "account_id", c.id, "to", phoneNumber, "product", productRetailerID)
|
||||
c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, bodyText))
|
||||
return messageID, nil
|
||||
}
|
||||
|
||||
// SendProductList sends a multi-product list message. Up to 30 products across up to 10 sections.
|
||||
func (c *Client) SendProductList(ctx context.Context, jid types.JID, headerText, bodyText, footerText, catalogID string, sections []ProductListSection) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
phoneNumber := jidToPhoneNumber(jid)
|
||||
|
||||
actionSections := make([]map[string]any, len(sections))
|
||||
for i, s := range sections {
|
||||
items := make([]map[string]any, len(s.ProductItems))
|
||||
for j, item := range s.ProductItems {
|
||||
items[j] = map[string]any{"product_retailer_id": item.ProductRetailerID}
|
||||
}
|
||||
actionSections[i] = map[string]any{
|
||||
"title": s.Title,
|
||||
"product_items": items,
|
||||
}
|
||||
}
|
||||
|
||||
interactive := map[string]any{
|
||||
"type": "product_list",
|
||||
"header": map[string]any{"type": "text", "text": headerText},
|
||||
"body": map[string]any{"text": bodyText},
|
||||
"action": map[string]any{
|
||||
"catalog_id": catalogID,
|
||||
"sections": actionSections,
|
||||
},
|
||||
}
|
||||
if footerText != "" {
|
||||
interactive["footer"] = map[string]any{"text": footerText}
|
||||
}
|
||||
|
||||
msg := map[string]any{
|
||||
"messaging_product": "whatsapp",
|
||||
"to": phoneNumber,
|
||||
"type": "interactive",
|
||||
"interactive": interactive,
|
||||
}
|
||||
|
||||
messageID, err := c.postToMessagesEndpoint(ctx, msg)
|
||||
if err != nil {
|
||||
c.eventBus.Publish(events.MessageFailedEvent(ctx, c.id, phoneNumber, bodyText, err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
logging.Debug("Product list sent via Business API", "account_id", c.id, "to", phoneNumber, "sections", len(sections))
|
||||
c.eventBus.Publish(events.MessageSentEvent(ctx, c.id, messageID, phoneNumber, bodyText))
|
||||
return messageID, nil
|
||||
}
|
||||
Reference in New Issue
Block a user