mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Add full support for WhatsApp Cloud API interactive messages across the inbox, automations, and a new quick-replies feature. Extends the interactive plumbing that previously existed only for the Flows engine. Sending - send-message core handles `interactive` (reply buttons ≤3, list ≤10 rows) via the existing Meta senders; validated server-side before send. - Inbox composer gains a "+" menu → builder dialog with live WhatsApp-style preview + Meta char limits (button 20, list row 24, body 1024). - Automation "Send Buttons" / "Send List" steps in the builder. - Public v1 /messages accepts type=interactive. Storage / round-trip - migration 035: messages.interactive_payload JSONB + quick_replies table. - Outbound interactive persists the structured payload (composer, automations, and Flows) so the thread re-renders the buttons/rows. Receiving - Bubble renders outbound interactive via the shared preview; inbound taps (button_reply/list_reply) keep the "chosen option" affordance. Automations - New `interactive_reply` trigger matches on tapped button/list IDs (exact), enabling chained multi-step menus. Quick replies - New account-scoped quick_replies (text or interactive snippets), managed in Settings and insertable from the composer; "Save as quick reply" in the builder. Shared core in src/lib/whatsapp/interactive.ts + src/components/interactive/. Includes unit tests for the validator, send-param validation, the interactive_reply trigger match, and the new automation step/trigger validation. typecheck + lint clean; full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { getCurrentAccount, requireRole, toErrorResponse } from '@/lib/auth/account'
|
|
import { supabaseAdmin } from '@/lib/automations/admin-client'
|
|
import { validateInteractivePayload } from '@/lib/whatsapp/interactive'
|
|
|
|
// Quick replies — reusable snippets (plain text or a saved interactive
|
|
// message) shared across the account. GET lists; POST creates. Mirrors
|
|
// the automations route: RLS-scoped read via the user client, service-
|
|
// role write after an explicit role check.
|
|
|
|
export async function GET() {
|
|
try {
|
|
const { supabase } = await getCurrentAccount()
|
|
// RLS (quick_replies_select) scopes to the caller's account.
|
|
const { data, error } = await supabase
|
|
.from('quick_replies')
|
|
.select('*')
|
|
.order('created_at', { ascending: false })
|
|
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
|
return NextResponse.json({ quick_replies: data ?? [] })
|
|
} catch (err) {
|
|
return toErrorResponse(err)
|
|
}
|
|
}
|
|
|
|
export async function POST(request: Request) {
|
|
let ctx
|
|
try {
|
|
ctx = await requireRole('agent')
|
|
} catch (err) {
|
|
return toErrorResponse(err)
|
|
}
|
|
|
|
const body = await request.json().catch(() => null)
|
|
if (!body) return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
|
|
|
|
const title = typeof body.title === 'string' ? body.title.trim() : ''
|
|
const kind = body.kind === 'interactive' ? 'interactive' : 'text'
|
|
if (!title) {
|
|
return NextResponse.json({ error: 'title is required' }, { status: 400 })
|
|
}
|
|
|
|
let content_text: string | null = null
|
|
let interactive_payload: unknown = null
|
|
|
|
if (kind === 'interactive') {
|
|
const result = validateInteractivePayload(body.interactive_payload)
|
|
if (!result.ok) {
|
|
return NextResponse.json({ error: result.error }, { status: 400 })
|
|
}
|
|
interactive_payload = body.interactive_payload
|
|
} else {
|
|
const text = typeof body.content_text === 'string' ? body.content_text : ''
|
|
if (!text.trim()) {
|
|
return NextResponse.json(
|
|
{ error: 'content_text is required for text quick replies' },
|
|
{ status: 400 },
|
|
)
|
|
}
|
|
content_text = text
|
|
}
|
|
|
|
const { data, error } = await supabaseAdmin()
|
|
.from('quick_replies')
|
|
.insert({
|
|
account_id: ctx.accountId,
|
|
user_id: ctx.userId,
|
|
title,
|
|
kind,
|
|
content_text,
|
|
interactive_payload,
|
|
})
|
|
.select()
|
|
.single()
|
|
|
|
if (error) {
|
|
return NextResponse.json({ error: error.message }, { status: 500 })
|
|
}
|
|
return NextResponse.json({ quick_reply: data }, { status: 201 })
|
|
}
|