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" ) // ListFlows returns all flows for the account. // POST /api/flows {"account_id"} func (h *Handlers) ListFlows(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.ListFlows(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, resp) } // CreateFlow creates a new flow. // POST /api/flows/create {"account_id","name","categories":[...],"endpoint_url"} func (h *Handlers) CreateFlow(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"` businessapi.FlowCreateRequest } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.Name == "" || len(req.Categories) == 0 { http.Error(w, "name and categories are required", http.StatusBadRequest) return } baClient, err := h.getBusinessAPIClient(req.AccountID) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } resp, err := baClient.CreateFlow(r.Context(), req.FlowCreateRequest) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, resp) } // GetFlow returns details for a single flow. // POST /api/flows/get {"account_id","flow_id"} func (h *Handlers) GetFlow(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"` FlowID string `json:"flow_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.FlowID == "" { http.Error(w, "flow_id is required", http.StatusBadRequest) return } // validate account exists and is business-api (even though GetFlow doesn't strictly need it, consistency) if _, err := h.getBusinessAPIClient(req.AccountID); 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.GetFlow(r.Context(), req.FlowID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, resp) } // UploadFlowAsset uploads a screens JSON to a DRAFT flow. // POST /api/flows/upload {"account_id","flow_id","screens_json":"{...}"} func (h *Handlers) UploadFlowAsset(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"` FlowID string `json:"flow_id"` ScreensJSON string `json:"screens_json"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.FlowID == "" || req.ScreensJSON == "" { http.Error(w, "flow_id and screens_json are required", http.StatusBadRequest) return } baClient, err := h.getBusinessAPIClient(req.AccountID) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := baClient.UpdateFlowAssets(r.Context(), req.FlowID, req.ScreensJSON); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } // PublishFlow publishes a DRAFT flow. // POST /api/flows/publish {"account_id","flow_id"} func (h *Handlers) PublishFlow(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"` FlowID string `json:"flow_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.FlowID == "" { http.Error(w, "flow_id is required", http.StatusBadRequest) return } baClient, err := h.getBusinessAPIClient(req.AccountID) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := baClient.PublishFlow(r.Context(), req.FlowID); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } // DeprecateFlow transitions a PUBLISHED flow to DEPRECATED. // Deprecated flows block new sessions but remain usable by sessions already in progress. // POST /api/flows/deprecate {"account_id","flow_id"} func (h *Handlers) DeprecateFlow(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"` FlowID string `json:"flow_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.FlowID == "" { http.Error(w, "flow_id is required", http.StatusBadRequest) return } baClient, err := h.getBusinessAPIClient(req.AccountID) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := baClient.DeprecateFlow(r.Context(), req.FlowID); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } // DeleteFlow permanently removes a flow. // POST /api/flows/delete {"account_id","flow_id"} func (h *Handlers) DeleteFlow(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"` FlowID string `json:"flow_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.FlowID == "" { http.Error(w, "flow_id is required", http.StatusBadRequest) return } baClient, err := h.getBusinessAPIClient(req.AccountID) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if err := baClient.DeleteFlow(r.Context(), req.FlowID); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) } // SendFlow sends an interactive flow message. // POST /api/send/flow {"account_id","to","flow_id","flow_token","screen_name","data":{...}} func (h *Handlers) SendFlow(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"` FlowID string `json:"flow_id"` FlowToken string `json:"flow_token"` ScreenName string `json:"screen_name"` Data map[string]any `json:"data"` Header string `json:"header"` Body string `json:"body"` Footer string `json:"footer"` Dynamic bool `json:"dynamic"` // if true, use type "unpublished" } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if req.FlowID == "" || req.Body == "" { http.Error(w, "flow_id and body 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 } paramType := "payload" if req.Dynamic { paramType = "unpublished" } interactive := &businessapi.InteractiveObject{ Type: "flow", Body: &businessapi.InteractiveBody{Text: req.Body}, Action: &businessapi.InteractiveAction{ Name: "flow", Parameters: &businessapi.FlowActionParams{ Type: paramType, FlowToken: req.FlowToken, Name: req.ScreenName, Data: req.Data, }, }, } if req.Header != "" { interactive.Header = &businessapi.InteractiveHeader{Type: "text", Text: req.Header} } if req.Footer != "" { interactive.Footer = &businessapi.InteractiveFooter{Text: req.Footer} } if _, err := baClient.SendInteractive(r.Context(), jid, interactive); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]string{"status": "ok"}) }