Files
whatshooked/pkg/handlers/catalog.go
Hein ecd5525430
Some checks failed
CI / Lint (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Test (1.23) (push) Has been cancelled
CI / Test (1.22) (push) Has been cancelled
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.
2026-02-04 11:17:40 +02:00

216 lines
6.6 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"git.warky.dev/wdevs/whatshooked/pkg/utils"
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp/businessapi"
"go.mau.fi/whatsmeow/types"
)
// ListCatalogs returns product catalogs for a Business API account.
// POST /api/catalogs {"account_id"}
func (h *Handlers) ListCatalogs(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
AccountID string `json:"account_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
baClient, err := h.getBusinessAPIClient(req.AccountID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := baClient.ListCatalogs(r.Context())
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, resp)
}
// ListProducts returns products in a specific catalog.
// POST /api/catalogs/products {"account_id","catalog_id"}
func (h *Handlers) ListProducts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
AccountID string `json:"account_id"`
CatalogID string `json:"catalog_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.CatalogID == "" {
http.Error(w, "catalog_id is required", http.StatusBadRequest)
return
}
baClient, err := h.getBusinessAPIClient(req.AccountID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
resp, err := baClient.ListProducts(r.Context(), req.CatalogID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, resp)
}
// SendCatalogMessage sends a catalog message that shares the full product catalog.
// POST /api/send/catalog {"account_id","to","body_text","thumbnail_product_retailer_id"}
func (h *Handlers) SendCatalogMessage(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
AccountID string `json:"account_id"`
To string `json:"to"`
BodyText string `json:"body_text"`
ThumbnailProductRetailerID string `json:"thumbnail_product_retailer_id"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.BodyText == "" {
http.Error(w, "body_text is required", http.StatusBadRequest)
return
}
baClient, err := h.getBusinessAPIClient(req.AccountID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
jid, err := types.ParseJID(utils.FormatPhoneToJID(req.To, h.config.Server.DefaultCountryCode))
if err != nil {
http.Error(w, "Invalid phone number", http.StatusBadRequest)
return
}
if _, err := baClient.SendCatalogMessage(r.Context(), jid, req.BodyText, req.ThumbnailProductRetailerID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]string{"status": "ok"})
}
// SendSingleProduct sends a single-product interactive message.
// POST /api/send/product {"account_id","to","catalog_id","product_retailer_id","body_text","footer_text"}
func (h *Handlers) SendSingleProduct(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
AccountID string `json:"account_id"`
To string `json:"to"`
CatalogID string `json:"catalog_id"`
ProductRetailerID string `json:"product_retailer_id"`
BodyText string `json:"body_text"`
FooterText string `json:"footer_text"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.CatalogID == "" || req.ProductRetailerID == "" || req.BodyText == "" {
http.Error(w, "catalog_id, product_retailer_id, and body_text are required", http.StatusBadRequest)
return
}
baClient, err := h.getBusinessAPIClient(req.AccountID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
jid, err := types.ParseJID(utils.FormatPhoneToJID(req.To, h.config.Server.DefaultCountryCode))
if err != nil {
http.Error(w, "Invalid phone number", http.StatusBadRequest)
return
}
if _, err := baClient.SendSingleProduct(r.Context(), jid, req.CatalogID, req.ProductRetailerID, req.BodyText, req.FooterText); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]string{"status": "ok"})
}
// SendProductList sends a multi-product list message (up to 30 products across up to 10 sections).
// POST /api/send/product-list {"account_id","to","header_text","body_text","footer_text","catalog_id","sections":[...]}
func (h *Handlers) SendProductList(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
AccountID string `json:"account_id"`
To string `json:"to"`
HeaderText string `json:"header_text"`
BodyText string `json:"body_text"`
FooterText string `json:"footer_text"`
CatalogID string `json:"catalog_id"`
Sections []businessapi.ProductListSection `json:"sections"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.CatalogID == "" || req.HeaderText == "" || req.BodyText == "" || len(req.Sections) == 0 {
http.Error(w, "catalog_id, header_text, body_text, and sections are required", http.StatusBadRequest)
return
}
baClient, err := h.getBusinessAPIClient(req.AccountID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
jid, err := types.ParseJID(utils.FormatPhoneToJID(req.To, h.config.Server.DefaultCountryCode))
if err != nil {
http.Error(w, "Invalid phone number", http.StatusBadRequest)
return
}
if _, err := baClient.SendProductList(r.Context(), jid, req.HeaderText, req.BodyText, req.FooterText, req.CatalogID, req.Sections); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]string{"status": "ok"})
}