|
| 1 | +package whatsmeow |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "errors" |
| 6 | + "fmt" |
| 7 | + "net/url" |
| 8 | + "strings" |
| 9 | + |
| 10 | + "google.golang.org/protobuf/proto" |
| 11 | + |
| 12 | + "github.com/polymorfa/hypermeow/proto/waE2E" |
| 13 | +) |
| 14 | + |
| 15 | +// BusinessHydratedButtonsMessageParams is an uploaded-media hydrated template. |
| 16 | +// Encoding this format does not imply Meta template approval or relay admission. |
| 17 | +type BusinessHydratedButtonsMessageParams struct { |
| 18 | + Title, Body, Footer string |
| 19 | + Buttons []BusinessNativeFlowButton |
| 20 | + Image *waE2E.ImageMessage |
| 21 | + Video *waE2E.VideoMessage |
| 22 | + Document *waE2E.DocumentMessage |
| 23 | + ContextInfo *waE2E.ContextInfo |
| 24 | +} |
| 25 | + |
| 26 | +// BuildBusinessHydratedButtonsMessage permits replies mixed with URL/call CTAs. |
| 27 | +// Received Web relays additionally require a server HSM envelope. |
| 28 | +func BuildBusinessHydratedButtonsMessage(p BusinessHydratedButtonsMessageParams) (*waE2E.Message, error) { |
| 29 | + if strings.TrimSpace(p.Body) == "" || !bounded(p.Body, 4096) || !bounded(p.Title, 256) || !bounded(p.Footer, 256) || len(p.Buttons) < 1 || len(p.Buttons) > 10 { |
| 30 | + return nil, errors.New("invalid hydrated template text or button count") |
| 31 | + } |
| 32 | + mediaCount := 0 |
| 33 | + if p.Image != nil { |
| 34 | + mediaCount++ |
| 35 | + } |
| 36 | + if p.Video != nil { |
| 37 | + mediaCount++ |
| 38 | + } |
| 39 | + if p.Document != nil { |
| 40 | + mediaCount++ |
| 41 | + } |
| 42 | + if mediaCount > 1 || (mediaCount > 0 && p.Title != "") { |
| 43 | + return nil, errors.New("hydrated template accepts one title or media header") |
| 44 | + } |
| 45 | + t := &waE2E.TemplateMessage_HydratedFourRowTemplate{HydratedContentText: proto.String(p.Body), HydratedFooterText: optionalString(p.Footer)} |
| 46 | + ids := map[string]bool{} |
| 47 | + ctas := 0 |
| 48 | + for i, b := range p.Buttons { |
| 49 | + var params struct { |
| 50 | + Text string `json:"display_text"` |
| 51 | + ID string `json:"id"` |
| 52 | + URL string `json:"url"` |
| 53 | + Phone string `json:"phone_number"` |
| 54 | + } |
| 55 | + if !bounded(b.ParamsJSON, 8192) || json.Unmarshal([]byte(b.ParamsJSON), ¶ms) != nil || strings.TrimSpace(params.Text) == "" || !bounded(params.Text, 256) { |
| 56 | + return nil, fmt.Errorf("invalid hydrated button %d", i) |
| 57 | + } |
| 58 | + button := &waE2E.HydratedTemplateButton{Index: proto.Uint32(uint32(i + 1))} |
| 59 | + switch b.Name { |
| 60 | + case "quick_reply": |
| 61 | + if strings.TrimSpace(params.ID) == "" || !bounded(params.ID, 256) || ids[params.ID] { |
| 62 | + return nil, errors.New("hydrated reply IDs must be nonempty and unique") |
| 63 | + } |
| 64 | + ids[params.ID] = true |
| 65 | + button.HydratedButton = &waE2E.HydratedTemplateButton_QuickReplyButton{QuickReplyButton: &waE2E.HydratedTemplateButton_HydratedQuickReplyButton{DisplayText: proto.String(params.Text), ID: proto.String(params.ID)}} |
| 66 | + case "cta_url": |
| 67 | + parsed, err := url.ParseRequestURI(params.URL) |
| 68 | + if err != nil || parsed.Scheme != "https" || parsed.Hostname() == "" || !bounded(params.URL, 2048) { |
| 69 | + return nil, errors.New("hydrated URL must be absolute HTTPS") |
| 70 | + } |
| 71 | + ctas++ |
| 72 | + button.HydratedButton = &waE2E.HydratedTemplateButton_UrlButton{UrlButton: &waE2E.HydratedTemplateButton_HydratedURLButton{DisplayText: proto.String(params.Text), URL: proto.String(params.URL)}} |
| 73 | + case "cta_call": |
| 74 | + phone := strings.TrimPrefix(params.Phone, "+") |
| 75 | + if len(phone) < 6 || len(phone) > 20 || strings.IndexFunc(phone, func(r rune) bool { return r < '0' || r > '9' }) >= 0 { |
| 76 | + return nil, errors.New("invalid hydrated call phone") |
| 77 | + } |
| 78 | + ctas++ |
| 79 | + button.HydratedButton = &waE2E.HydratedTemplateButton_CallButton{CallButton: &waE2E.HydratedTemplateButton_HydratedCallButton{DisplayText: proto.String(params.Text), PhoneNumber: proto.String(params.Phone)}} |
| 80 | + default: |
| 81 | + return nil, errors.New("hydrated templates support replies and URL/call buttons") |
| 82 | + } |
| 83 | + t.HydratedButtons = append(t.HydratedButtons, button) |
| 84 | + } |
| 85 | + if ctas > 3 { |
| 86 | + return nil, errors.New("hydrated template exceeds three CTA buttons") |
| 87 | + } |
| 88 | + switch { |
| 89 | + case p.Image != nil: |
| 90 | + t.Title = &waE2E.TemplateMessage_HydratedFourRowTemplate_ImageMessage{ImageMessage: p.Image} |
| 91 | + case p.Video != nil: |
| 92 | + t.Title = &waE2E.TemplateMessage_HydratedFourRowTemplate_VideoMessage{VideoMessage: p.Video} |
| 93 | + case p.Document != nil: |
| 94 | + t.Title = &waE2E.TemplateMessage_HydratedFourRowTemplate_DocumentMessage{DocumentMessage: p.Document} |
| 95 | + case p.Title != "": |
| 96 | + t.Title = &waE2E.TemplateMessage_HydratedFourRowTemplate_HydratedTitleText{HydratedTitleText: p.Title} |
| 97 | + } |
| 98 | + return &waE2E.Message{TemplateMessage: &waE2E.TemplateMessage{HydratedTemplate: t, ContextInfo: p.ContextInfo}}, nil |
| 99 | +} |
0 commit comments