mirror of
https://github.com/abhinavxd/libredesk.git
synced 2026-10-25 22:09:36 +00:00
46c3e42266
Keep local templates unchanged when Meta rejects edits or deletions. Block unsupported template sends and add regression tests for template actions and form controls.
216 lines
6.9 KiB
Go
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
|
|
}
|