Files
libredesk/cmd/whatsapp_template.go
Abhinav Raut 46c3e42266 add WhatsApp template editing and handle Meta failures
Keep local templates unchanged when Meta rejects edits or deletions. Block unsupported template sends and add regression tests for template actions and form controls.
2026-09-17 14:33:33 +05:30

216 lines
6.9 KiB
Go

package main
import (
"context"
"encoding/json"
"strconv"
"time"
"github.com/abhinavxd/libredesk/internal/envelope"
whatsappChannel "github.com/abhinavxd/libredesk/internal/inbox/channel/whatsapp"
"github.com/abhinavxd/libredesk/internal/whatsapp"
wtmodels "github.com/abhinavxd/libredesk/internal/whatsapp/template/models"
"github.com/valyala/fasthttp"
"github.com/zerodha/fastglue"
)
const (
whatsAppTemplateSyncInterval = 6 * time.Hour
whatsAppTemplateSyncTimeout = 2 * time.Minute
)
func whatsappTemplateSyncWorker(ctx context.Context, app *App) {
initial := time.NewTimer(2 * time.Minute)
defer initial.Stop()
select {
case <-ctx.Done():
return
case <-initial.C:
syncAllWhatsAppTemplates(ctx, app)
}
ticker := time.NewTicker(whatsAppTemplateSyncInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
syncAllWhatsAppTemplates(ctx, app)
}
}
}
func syncAllWhatsAppTemplates(ctx context.Context, app *App) {
if app.whatsappTemplate == nil {
return
}
inboxes, err := app.inbox.GetAll()
if err != nil {
return
}
for _, rec := range inboxes {
if rec.Channel != whatsappChannel.ChannelWhatsApp || !rec.Enabled {
continue
}
syncCtx, cancel := context.WithTimeout(ctx, whatsAppTemplateSyncTimeout)
if _, err := app.whatsappTemplate.SyncFromMeta(syncCtx, rec.ID); err != nil {
app.lo.Warn("periodic whatsapp template sync failed", "inbox_id", rec.ID, "error", err)
cancel()
continue
}
cancel()
// Must follow the sync: an edit skipped during review only applies once the status is fresh.
ensureWhatsAppCSATTemplate(app, rec.ID)
}
}
// makeWhatsAppAuthErrorHook flags the matching inbox when Meta rejects its token.
func makeWhatsAppAuthErrorHook(app *App) func(acc whatsapp.Account) {
return func(acc whatsapp.Account) {
inboxes, err := app.inbox.GetAll()
if err != nil {
return
}
for _, rec := range inboxes {
if rec.Channel != whatsappChannel.ChannelWhatsApp {
continue
}
var cfg whatsappChannel.Config
if err := json.Unmarshal(rec.Config, &cfg); err != nil || cfg.PhoneNumberID != acc.PhoneNumberID {
continue
}
if _, flagged := app.inboxAuthErrors.LoadOrStore(rec.ID, time.Now()); !flagged {
app.lo.Error("whatsapp access token rejected by meta, sends and media downloads will fail until the token is replaced", "inbox_id", rec.ID)
}
return
}
}
}
// handleListWhatsAppTemplates lists templates for a given inbox (?inbox_id=123).
func handleListWhatsAppTemplates(r *fastglue.Request) error {
app := r.Context.(*App)
if whatsAppTemplateUnavailable(r, app) {
return nil
}
inboxIDRaw := string(r.RequestCtx.QueryArgs().Peek("inbox_id"))
inboxID, err := strconv.Atoi(inboxIDRaw)
if err != nil || inboxID == 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "inbox_id is required", nil, envelope.InputError)
}
templates, err := app.whatsappTemplate.GetByInbox(inboxID)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(templates)
}
func handleGetWhatsAppTemplate(r *fastglue.Request) error {
app := r.Context.(*App)
if whatsAppTemplateUnavailable(r, app) {
return nil
}
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "invalid id", nil, envelope.InputError)
}
t, err := app.whatsappTemplate.GetByID(id)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(t)
}
// handleCreateWhatsAppTemplate stores a new template and submits it to Meta.
func handleCreateWhatsAppTemplate(r *fastglue.Request) error {
app := r.Context.(*App)
if whatsAppTemplateUnavailable(r, app) {
return nil
}
var t wtmodels.Template
if err := r.Decode(&t, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "invalid request", nil, envelope.InputError)
}
if t.InboxID == 0 || t.Name == "" || t.Language == "" || t.Category == "" || t.BodyContent == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "inbox_id, name, language, category, body_content are required", nil, envelope.InputError)
}
ctx, cancel := context.WithTimeout(r.RequestCtx, whatsappChannel.MetaCallTimeout)
defer cancel()
created, err := app.whatsappTemplate.Create(ctx, t)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(created)
}
func handleUpdateWhatsAppTemplate(r *fastglue.Request) error {
app := r.Context.(*App)
if whatsAppTemplateUnavailable(r, app) {
return nil
}
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
if err != nil || id <= 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "invalid id", nil, envelope.InputError)
}
var t wtmodels.Template
if err := r.Decode(&t, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "invalid request", nil, envelope.InputError)
}
if t.InboxID <= 0 || t.Name == "" || t.Language == "" || t.Category == "" || t.BodyContent == "" {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "inbox_id, name, language, category, body_content are required", nil, envelope.InputError)
}
ctx, cancel := context.WithTimeout(r.RequestCtx, whatsappChannel.MetaCallTimeout)
defer cancel()
updated, err := app.whatsappTemplate.Update(ctx, id, t)
if err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(updated)
}
func handleDeleteWhatsAppTemplate(r *fastglue.Request) error {
app := r.Context.(*App)
if whatsAppTemplateUnavailable(r, app) {
return nil
}
id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string))
if err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "invalid id", nil, envelope.InputError)
}
ctx, cancel := context.WithTimeout(r.RequestCtx, whatsappChannel.MetaCallTimeout)
defer cancel()
if err := app.whatsappTemplate.Delete(ctx, id); err != nil {
return sendErrorEnvelope(r, err)
}
return r.SendEnvelope(map[string]string{"status": "deleted"})
}
func handleSyncWhatsAppTemplates(r *fastglue.Request) error {
app := r.Context.(*App)
if whatsAppTemplateUnavailable(r, app) {
return nil
}
inboxIDRaw := string(r.RequestCtx.QueryArgs().Peek("inbox_id"))
inboxID, err := strconv.Atoi(inboxIDRaw)
if err != nil || inboxID == 0 {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "inbox_id is required", nil, envelope.InputError)
}
ctx, cancel := context.WithTimeout(r.RequestCtx, whatsAppTemplateSyncTimeout)
defer cancel()
count, err := app.whatsappTemplate.SyncFromMeta(ctx, inboxID)
if err != nil {
app.lo.Error("error syncing whatsapp templates", "inbox_id", inboxID, "error", err)
return r.SendErrorEnvelope(fasthttp.StatusBadGateway, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
}
return r.SendEnvelope(map[string]int{"synced": count})
}
func whatsAppTemplateUnavailable(r *fastglue.Request, app *App) bool {
if app.whatsappTemplate != nil {
return false
}
r.SendErrorEnvelope(fasthttp.StatusServiceUnavailable, "whatsapp not configured", nil, envelope.GeneralError)
return true
}