Files
clube67_newwhats.local/clube67/newwhats.local/frontend/services/chatApiService.ts
T
VPS 4 Deploy Agent 539b922ce3 feat(media): re-download de mídia não baixada ao clicar
- Armazena payload WAMessage (serializado com _b64 para Uint8Array) em
  messages.mediaPayload no momento da recepção da mensagem
- Nova rota POST /instances/:id/redownload-media/:msgId que usa
  sock.updateMediaMessage para renovar URL expirada + re-upload Wasabi
- Frontend: placeholders clicáveis (spinner + texto) para imagem, vídeo,
  áudio, documento e figurinha sem media_url; atualização automática via
  socket message:media_ready existente

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 18:58:01 +02:00

557 lines
21 KiB
TypeScript

/**
* chatApiService.ts — Client HTTP para o novo backend NewWhats.
* Consumido pelo chatStore e pelos componentes React.
*/
import axios, { type AxiosInstance } from 'axios'
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3003'
function createClient(): AxiosInstance {
const client = axios.create({ baseURL: API })
client.interceptors.request.use((config) => {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// Renova token automaticamente em 401
client.interceptors.response.use(
(r) => r,
async (error) => {
const original = error.config
if (error.response?.status === 401 && !original._retry) {
original._retry = true
try {
const refreshToken = localStorage.getItem('refreshToken')
if (!refreshToken) throw new Error('Sem refresh token')
const { data } = await axios.post(`${API}/api/auth/refresh`, { refreshToken })
localStorage.setItem('token', data.accessToken)
if (typeof window !== 'undefined') {
document.cookie = `accessToken=${data.accessToken}; path=/; SameSite=Lax; Secure`
}
localStorage.setItem('refreshToken', data.refreshToken)
original.headers.Authorization = `Bearer ${data.accessToken}`
return client(original)
} catch {
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
if (typeof window !== 'undefined') {
document.cookie = 'accessToken=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/'
}
window.location.href = '/login'
}
}
return Promise.reject(error)
}
)
return client
}
export const api = createClient()
// ─── Auth ─────────────────────────────────────────────────────────────────────
export const authApi = {
register: (data: { name: string; email: string; password: string }) =>
api.post('/api/auth/register', data).then((r) => r.data),
login: (data: { email: string; password: string }) =>
api.post('/api/auth/login', data).then((r) => r.data),
logout: () =>
api.post('/api/auth/logout').then((r) => r.data),
me: () =>
api.get('/api/auth/me').then((r) => r.data),
changePassword: (data: { currentPassword: string; newPassword: string }) =>
api.patch('/api/auth/password', data).then((r) => r.data),
}
// ─── Instâncias ───────────────────────────────────────────────────────────────
export const instanceApi = {
list: () =>
api.get('/api/instances').then((r) => r.data),
create: (name: string) =>
api.post('/api/instances', { name }).then((r) => r.data),
connect: (id: string) =>
api.post(`/api/instances/${id}/connect`).then((r) => r.data),
disconnect: (id: string) =>
api.post(`/api/instances/${id}/disconnect`).then((r) => r.data),
getQr: (id: string) =>
api.get(`/api/instances/${id}/qr`).then((r) => r.data),
delete: (id: string) =>
api.delete(`/api/instances/${id}`).then((r) => r.data),
}
// ─── Chats ────────────────────────────────────────────────────────────────────
export const chatApi = {
list: (instanceId: string, opts: { search?: string; archived?: boolean; limit?: number } = {}) =>
api.get('/api/chats', { params: { instanceId, ...opts } }).then((r) => r.data),
get: (chatId: string) =>
api.get(`/api/chats/${chatId}`).then((r) => r.data),
markRead: (chatId: string) =>
api.post(`/api/chats/${chatId}/read`).then((r) => r.data),
archive: (chatId: string, archived: boolean) =>
api.patch(`/api/chats/${chatId}/archive`, { archived }).then((r) => r.data),
pin: (chatId: string, pinned: boolean) =>
api.patch(`/api/chats/${chatId}/pin`, { pinned }).then((r) => r.data),
delete: (chatId: string) =>
api.delete(`/api/chats/${chatId}`).then((r) => r.data),
stats: (instanceId: string) =>
api.get('/api/chats/stats/overview', { params: { instanceId } }).then((r) => r.data),
}
// ─── Mensagens ────────────────────────────────────────────────────────────────
export const messageApi = {
list: (chatId: string, opts: { before?: string; after?: string; limit?: number } = {}) =>
api.get(`/api/chats/${chatId}/messages`, { params: opts }).then((r) => r.data) as
Promise<{ messages: any[]; hasMore: boolean }>,
search: (instanceId: string, q: string, limit?: number) =>
api.get('/api/chats/search/messages', { params: { instanceId, q, limit } }).then((r) => r.data),
send: (instanceId: string, jid: string, text: string, replyToMessageId?: string, mentions?: string[]) =>
api.post(`/api/instances/${instanceId}/send`, { jid, text, replyToMessageId, ...(mentions?.length ? { mentions } : {}) }).then((r) => r.data),
}
// ─── Grupos ──────────────────────────────────────────────────────────────────
export interface GroupParticipant {
jid: string
admin: string | null
name: string | null
phone: string
}
export const groupApi = {
getParticipants: (instanceId: string, groupJid: string) =>
api.get(`/api/instances/${instanceId}/groups/${encodeURIComponent(groupJid)}/participants`).then((r) => r.data) as
Promise<{ groupJid: string; subject: string; participants: GroupParticipant[] }>,
}
// ─── Reconciliação ───────────────────────────────────────────────────────────
export const reconcileApi = {
reconcileNames: (instanceId: string) =>
api.post(`/api/instances/${instanceId}/reconcile-names`).then((r) => r.data) as
Promise<{ updated: number } | void>,
}
// ─── Protocolos ───────────────────────────────────────────────────────────────
export const protocolApi = {
list: (chatId: string) =>
api.get(`/api/chats/${chatId}/protocol`).then((r) => r.data),
open: (chatId: string, data: { sectorId?: string; teamId?: string }) =>
api.post(`/api/chats/${chatId}/protocol`, data).then((r) => r.data),
update: (chatId: string, protocolId: string, data: {
status?: string
summary?: string
}) =>
api.patch(`/api/chats/${chatId}/protocol/${protocolId}`, data).then((r) => r.data),
}
// ─── Mídia ────────────────────────────────────────────────────────────────────
export const mediaApi = {
sendMedia: (
instanceId: string,
jid: string,
file: File,
caption?: string,
replyToMessageId?: string
) => {
const fd = new FormData()
fd.append('file', file)
fd.append('jid', jid)
if (caption) fd.append('caption', caption)
if (replyToMessageId) fd.append('replyToMessageId', replyToMessageId)
return api
.post(`/api/instances/${instanceId}/send-media`, fd, {
headers: { 'Content-Type': 'multipart/form-data' },
})
.then((r) => r.data)
},
sendAudio: (instanceId: string, jid: string, blob: Blob) => {
const fd = new FormData()
fd.append('audio', blob, 'audio.ogg')
fd.append('jid', jid)
return api
.post(`/api/instances/${instanceId}/send-audio`, fd, {
headers: { 'Content-Type': 'multipart/form-data' },
})
.then((r) => r.data)
},
sendSticker: (instanceId: string, jid: string, blob: Blob) => {
const fd = new FormData()
fd.append('file', blob, 'sticker.webp')
fd.append('jid', jid)
return api
.post(`/api/instances/${instanceId}/send-media`, fd, {
headers: { 'Content-Type': 'multipart/form-data' },
})
.then((r) => r.data)
},
redownload: (instanceId: string, messageDbId: string): Promise<{ mediaUrl: string }> =>
api.post(`/api/instances/${instanceId}/redownload-media/${messageDbId}`).then((r) => r.data),
}
// ─── Chatbot / IA ─────────────────────────────────────────────────────────────
export const chatbotApi = {
getCredentials: (instanceId: string) =>
api.get(`/api/chatbot/credentials/${instanceId}`).then((r) => r.data),
createCredential: (instanceId: string, data: { name: string; apiKey: string; provider?: 'GEMINI' | 'OPENAI' }) =>
api.post(`/api/chatbot/credentials/${instanceId}`, data).then((r) => r.data),
deleteCredential: (instanceId: string, credId: string) =>
api.delete(`/api/chatbot/credentials/${instanceId}/${credId}`).then((r) => r.data),
getBots: (instanceId: string) =>
api.get(`/api/chatbot/bots/${instanceId}`).then((r) => r.data),
createBot: (instanceId: string, data: Record<string, unknown>) =>
api.post(`/api/chatbot/bots/${instanceId}`, data).then((r) => r.data),
updateBot: (instanceId: string, botId: string, data: Record<string, unknown>) =>
api.put(`/api/chatbot/bots/${instanceId}/${botId}`, data).then((r) => r.data),
deleteBot: (instanceId: string, botId: string) =>
api.delete(`/api/chatbot/bots/${instanceId}/${botId}`).then((r) => r.data),
pauseChat: (_instanceId: string, chatId: string) =>
api.post(`/api/chatbot/chats/${chatId}/pause`).then((r) => r.data),
resumeChat: (_instanceId: string, chatId: string) =>
api.post(`/api/chatbot/chats/${chatId}/resume`).then((r) => r.data),
}
// ─── API Keys ─────────────────────────────────────────────────────────────────
export interface ApiKeyRecord {
id: string
name: string
keyPreview: string // '••••••••XXXXXXXX'
isActive: boolean
createdAt: string
expiresAt: string | null
}
export const apiKeyApi = {
list: () =>
api.get('/api/api-keys').then((r) => r.data) as Promise<ApiKeyRecord[]>,
create: (name: string, expiresAt?: string) =>
api.post('/api/api-keys', { name, expiresAt }).then((r) => r.data) as
Promise<{ id: string; name: string; key: string; isActive: boolean; createdAt: string; expiresAt: string | null }>,
toggle: (id: string, isActive: boolean) =>
api.patch(`/api/api-keys/${id}`, { isActive }).then((r) => r.data),
delete: (id: string) =>
api.delete(`/api/api-keys/${id}`).then((r) => r.data),
}
// ─── Setores e Equipes ────────────────────────────────────────────────────────
export interface Sector {
id: string
tenantId: string
name: string
isActive: boolean
createdAt: string
updatedAt: string
_count?: { teams: number; protocols: number }
}
export interface Team {
id: string
tenantId: string
sectorId: string
name: string
isActive: boolean
createdAt: string
updatedAt: string
_count?: { protocols: number }
}
export const sectorApi = {
list: () =>
api.get('/api/sectors').then((r) => r.data) as Promise<Sector[]>,
create: (name: string) =>
api.post('/api/sectors', { name }).then((r) => r.data) as Promise<Sector>,
update: (id: string, data: { name?: string; isActive?: boolean }) =>
api.patch(`/api/sectors/${id}`, data).then((r) => r.data) as Promise<Sector>,
delete: (id: string) =>
api.delete(`/api/sectors/${id}`).then((r) => r.data),
listTeams: (sectorId: string) =>
api.get(`/api/sectors/${sectorId}/teams`).then((r) => r.data) as Promise<Team[]>,
createTeam: (sectorId: string, name: string) =>
api.post(`/api/sectors/${sectorId}/teams`, { name }).then((r) => r.data) as Promise<Team>,
updateTeam: (sectorId: string, teamId: string, data: { name?: string; isActive?: boolean }) =>
api.patch(`/api/sectors/${sectorId}/teams/${teamId}`, data).then((r) => r.data) as Promise<Team>,
deleteTeam: (sectorId: string, teamId: string) =>
api.delete(`/api/sectors/${sectorId}/teams/${teamId}`).then((r) => r.data),
}
// ─── Contatos / Phonebook ─────────────────────────────────────────────────────
export interface BackendContact {
id: string
instanceId: string
jid: string
name: string | null
verifiedName: string | null
notify: string | null
phone: string | null
avatarUrl: string | null
scoreReputacao: number
flagRestricao: boolean
isBlocked: boolean
createdAt: string
updatedAt: string
}
export const contactApi = {
list: (opts: { instanceId?: string; search?: string; blocked?: boolean; limit?: number; offset?: number } = {}) =>
api.get('/api/contacts', { params: opts }).then((r) => r.data) as
Promise<{ total: number; contacts: BackendContact[] }>,
get: (id: string) =>
api.get(`/api/contacts/${id}`).then((r) => r.data) as Promise<BackendContact>,
update: (id: string, data: { name?: string; isBlocked?: boolean }) =>
api.patch(`/api/contacts/${id}`, data).then((r) => r.data) as Promise<BackendContact>,
delete: (id: string) =>
api.delete(`/api/contacts/${id}`).then((r) => r.data),
stats: (instanceId?: string) =>
api.get('/api/contacts/stats/overview', { params: instanceId ? { instanceId } : undefined }).then((r) => r.data) as
Promise<{ total: number; blocked: number; flagged: number; active: number }>,
}
// ─── Broadcast ───────────────────────────────────────────────────────────────
export const broadcastApi = {
create: (instanceId: string, data: { numbers: string[]; message: string; delayMs?: number }) =>
api.post(`/api/instances/${instanceId}/broadcast`, data).then((r) => r.data) as
Promise<{ jobId: string; total: number }>,
list: (instanceId: string, limit = 20) =>
api.get(`/api/instances/${instanceId}/broadcast`, { params: { limit } }).then((r) => r.data) as
Promise<Array<{
id: string; status: string; message: string
totalCount: number; sentCount: number; failedCount: number
createdAt: string; finishedAt: string | null
_count: { recipients: number }
}>>,
get: (instanceId: string, broadcastId: string) =>
api.get(`/api/instances/${instanceId}/broadcast/${broadcastId}`).then((r) => r.data),
}
// ─── Plano ────────────────────────────────────────────────────────────────────
export const planApi = {
status: () =>
api.get('/api/plan/status').then((r) => r.data) as
Promise<{ daysRemaining: number | null; planName: string }>,
}
// ─── Mensagens Ricas (tipos avançados de WhatsApp) ────────────────────────────
export interface RichPayload {
text?: string
footer?: string
nativeButtons?: Array<{ type: string; id?: string; text: string; url?: string; copyText?: string; phoneNumber?: string }>
nativeList?: { buttonText: string; sections: Array<{ title: string; rows: Array<{ id: string; title: string; description?: string }> }> }
nativeCarousel?: { cards: Array<{ title?: string; body?: string; footer?: string; image?: { url: string }; buttons: Array<{ type?: string; id?: string; text: string }> }> }
poll?: { name: string; values: string[]; selectableCount?: number }
}
export const richMessageApi = {
send: (instanceId: string, jid: string, payload: RichPayload) =>
api.post(`/api/instances/${instanceId}/send`, { jid, ...payload }).then((r) => r.data),
}
// ─── Templates de Mensagem ────────────────────────────────────────────────────
export type TemplateType = 'TEXT' | 'BUTTONS' | 'INTERACTIVE' | 'LIST' | 'POLL' | 'CAROUSEL'
export interface MessageTemplate {
id: string
tenantId: string
name: string
description: string | null
type: TemplateType
payload: Record<string, unknown>
tags: string[]
usageCount: number
createdAt: string
updatedAt: string
}
export const templateApi = {
list: (opts: { type?: TemplateType; search?: string } = {}) =>
api.get('/api/templates', { params: opts }).then((r) => r.data) as Promise<MessageTemplate[]>,
get: (id: string) =>
api.get(`/api/templates/${id}`).then((r) => r.data) as Promise<MessageTemplate>,
create: (data: { name: string; description?: string; type: TemplateType; payload: Record<string, unknown>; tags?: string[] }) =>
api.post('/api/templates', data).then((r) => r.data) as Promise<MessageTemplate>,
update: (id: string, data: Partial<{ name: string; description: string; type: TemplateType; payload: Record<string, unknown>; tags: string[] }>) =>
api.put(`/api/templates/${id}`, data).then((r) => r.data) as Promise<MessageTemplate>,
delete: (id: string) =>
api.delete(`/api/templates/${id}`).then((r) => r.data),
markUsed: (id: string) =>
api.post(`/api/templates/${id}/use`).then((r) => r.data),
}
// ─── Agendamentos de Mensagem ─────────────────────────────────────────────────
export type ScheduleType = 'ONCE' | 'RECURRING' | 'EVENT'
export type ScheduleStatus = 'ACTIVE' | 'PAUSED' | 'DONE' | 'CANCELLED'
export type EventType = 'BIRTHDAY' | 'ANNIVERSARY' | 'SIGNUP_DAYS'
export type RecipientMode = 'MANUAL' | 'ALL_CONTACTS' | 'TAG_FILTER'
export interface ScheduleRecipient {
jid: string
name?: string
birthday?: string
anniversary?: string
signupDate?: string
}
export interface ScheduledMessage {
id: string
tenantId: string
instanceId: string
name: string
templateId: string | null
payload: Record<string, unknown>
recipientMode: RecipientMode
recipients: ScheduleRecipient[]
scheduleType: ScheduleType
cronExpr: string | null
scheduledAt: string | null
eventType: EventType | null
timezone: string
status: ScheduleStatus
lastRunAt: string | null
nextRunAt: string | null
runCount: number
createdAt: string
updatedAt: string
template?: Pick<MessageTemplate, 'id' | 'name' | 'type'> | null
_count?: { logs: number }
}
export interface ScheduleLog {
id: string
scheduleId: string
status: string
sentCount: number
failedCount: number
error: string | null
runAt: string
}
export const scheduleApi = {
list: (opts: { status?: ScheduleStatus; type?: ScheduleType } = {}) =>
api.get('/api/scheduled', { params: opts }).then((r) => r.data) as Promise<ScheduledMessage[]>,
get: (id: string) =>
api.get(`/api/scheduled/${id}`).then((r) => r.data) as Promise<ScheduledMessage>,
create: (data: {
instanceId: string
name: string
templateId?: string
payload: Record<string, unknown>
recipientMode?: RecipientMode
recipients?: ScheduleRecipient[]
scheduleType: ScheduleType
cronExpr?: string
scheduledAt?: string
eventType?: EventType
timezone?: string
}) =>
api.post('/api/scheduled', data).then((r) => r.data) as Promise<ScheduledMessage>,
update: (id: string, data: Partial<Parameters<typeof scheduleApi.create>[0]>) =>
api.put(`/api/scheduled/${id}`, data).then((r) => r.data) as Promise<ScheduledMessage>,
delete: (id: string) =>
api.delete(`/api/scheduled/${id}`).then((r) => r.data),
pause: (id: string) =>
api.post(`/api/scheduled/${id}/pause`).then((r) => r.data),
resume: (id: string) =>
api.post(`/api/scheduled/${id}/resume`).then((r) => r.data),
runNow: (id: string) =>
api.post(`/api/scheduled/${id}/run-now`).then((r) => r.data),
logs: (id: string) =>
api.get(`/api/scheduled/${id}/logs`).then((r) => r.data) as Promise<ScheduleLog[]>,
}
// ─── Stickers ─────────────────────────────────────────────────────────────────
export interface StickerRecord {
id: string
sha256: string
wasabiPath: string
isAnimated: boolean
isFavorite: boolean
useCount?: number
lastSeenAt?: string
}
export const stickerApi = {
list: () =>
api.get('/api/stickers').then((r) => r.data) as Promise<{ listVersion: string; stickers: StickerRecord[] }>,
recent: () =>
api.get('/api/stickers/recent').then((r) => r.data) as Promise<{ stickers: StickerRecord[] }>,
toggleFavorite: (id: string) =>
api.patch(`/api/stickers/${id}/favorite`).then((r) => r.data) as Promise<{ id: string; isFavorite: boolean }>,
}