feat: webhook groups
This commit is contained in:
parent
f4632e77d5
commit
92145d36b1
20 changed files with 3460 additions and 15 deletions
30
docs/api.md
30
docs/api.md
|
|
@ -71,6 +71,36 @@ Default: 60 req/s burst, 10 req/s sustained per API key. Configurable. Returns `
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## WhatsApp Groups
|
||||||
|
|
||||||
|
Groups are tracked automatically when the session joins a group or receives messages from one. Records sync on connect and update in real-time via Baileys events.
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
| ------ | -------------------------------------- | ---------------------------------------- |
|
||||||
|
| GET | `/v1/whatsapp-groups` | List tracked groups. `session_id` required. |
|
||||||
|
| GET | `/v1/whatsapp-groups/:jid` | Single group by JID. `session_id` required. |
|
||||||
|
|
||||||
|
`:jid` must be URL-encoded (e.g. `120363429229698693%40g.us`).
|
||||||
|
|
||||||
|
**Group object:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"appId": "uuid",
|
||||||
|
"sessionId": "uuid",
|
||||||
|
"jid": "120363429229698693@g.us",
|
||||||
|
"subject": "Dev Team",
|
||||||
|
"description": "Internal dev chat",
|
||||||
|
"participantCount": 12,
|
||||||
|
"createdAt": "2026-05-16T10:00:00Z",
|
||||||
|
"updatedAt": "2026-05-16T10:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`subject` and `description` may be `null` if metadata has not been received yet. `participantCount` is updated incrementally on `group.participants_added` / `group.participants_removed` and replaced wholesale on `group.joined` / history sync.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Webhooks
|
## Webhooks
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ If `secret` is omitted, one is generated. **Store it — shown once.**
|
||||||
## Event types
|
## Event types
|
||||||
|
|
||||||
| Event | When fired |
|
| Event | When fired |
|
||||||
| ------------------- | ------------------------------------- |
|
| --------------------------- | ----------------------------------------------- |
|
||||||
| `session.status` | Any session state transition |
|
| `session.status` | Any session state transition |
|
||||||
| `session.deleted` | Session deleted via API |
|
| `session.deleted` | Session deleted via API |
|
||||||
| `message.received` | Inbound message |
|
| `message.received` | Inbound message |
|
||||||
|
|
@ -31,6 +31,59 @@ If `secret` is omitted, one is generated. **Store it — shown once.**
|
||||||
| `presence.update` | Contact presence change |
|
| `presence.update` | Contact presence change |
|
||||||
| `contact.created` | New contact added |
|
| `contact.created` | New contact added |
|
||||||
| `contact.updated` | Contact modified |
|
| `contact.updated` | Contact modified |
|
||||||
|
| `group.joined` | Session was added to a WhatsApp group |
|
||||||
|
| `group.left` | Session was removed or kicked from a group |
|
||||||
|
| `group.updated` | Group subject or description changed |
|
||||||
|
| `group.participants_added` | Other participants added to a group |
|
||||||
|
| `group.participants_removed`| Other participants removed from a group |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Group event payloads
|
||||||
|
|
||||||
|
All group payloads appear in the `data` field of the standard envelope (see [Payload structure](#payload-structure) below).
|
||||||
|
|
||||||
|
**`group.joined`**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sessionId": "uuid",
|
||||||
|
"jid": "120363429229698693@g.us",
|
||||||
|
"subject": "Dev Team",
|
||||||
|
"description": "Internal dev chat",
|
||||||
|
"participantCount": 12
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`group.left`**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sessionId": "uuid",
|
||||||
|
"jid": "120363429229698693@g.us",
|
||||||
|
"participants": ["6281234567890@s.whatsapp.net"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`participants` contains the JID(s) that triggered the removal (may include the session's own JID when kicked).
|
||||||
|
|
||||||
|
**`group.updated`**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sessionId": "uuid",
|
||||||
|
"jid": "120363429229698693@g.us",
|
||||||
|
"subject": "New Name",
|
||||||
|
"description": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Only fields reported by WhatsApp are present. `null` means the field was explicitly cleared.
|
||||||
|
|
||||||
|
**`group.participants_added`** / **`group.participants_removed`**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sessionId": "uuid",
|
||||||
|
"jid": "120363429229698693@g.us",
|
||||||
|
"participants": ["491234567890@s.whatsapp.net", "441234567890@s.whatsapp.net"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
These fire for other participants — not for the session account itself (that triggers `group.joined` / `group.left`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
17
drizzle/0008_tricky_miek.sql
Normal file
17
drizzle/0008_tricky_miek.sql
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
CREATE TABLE "whatsapp_groups" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"app_id" uuid NOT NULL,
|
||||||
|
"session_id" uuid NOT NULL,
|
||||||
|
"jid" text NOT NULL,
|
||||||
|
"subject" text,
|
||||||
|
"description" text,
|
||||||
|
"participant_count" integer,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "whatsapp_groups_session_jid_unique" UNIQUE("session_id","jid")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "whatsapp_groups" ADD CONSTRAINT "whatsapp_groups_app_id_apps_id_fk" FOREIGN KEY ("app_id") REFERENCES "public"."apps"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "whatsapp_groups" ADD CONSTRAINT "whatsapp_groups_session_id_sessions_id_fk" FOREIGN KEY ("session_id") REFERENCES "public"."sessions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "whatsapp_groups_app_id_idx" ON "whatsapp_groups" USING btree ("app_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "whatsapp_groups_session_id_idx" ON "whatsapp_groups" USING btree ("session_id");
|
||||||
1777
drizzle/meta/0008_snapshot.json
Normal file
1777
drizzle/meta/0008_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -57,6 +57,13 @@
|
||||||
"when": 1778920492721,
|
"when": 1778920492721,
|
||||||
"tag": "0007_silky_sentinel",
|
"tag": "0007_silky_sentinel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778923691302,
|
||||||
|
"tag": "0008_tricky_miek",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Key,
|
Key,
|
||||||
Users,
|
Users,
|
||||||
|
Users2,
|
||||||
Webhook,
|
Webhook,
|
||||||
Activity,
|
Activity,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
@ -132,14 +133,17 @@ function AppContextNav({
|
||||||
const sessionsPath = `/apps/${appId}/sessions`;
|
const sessionsPath = `/apps/${appId}/sessions`;
|
||||||
const contactsPath = `/apps/${appId}/contacts`;
|
const contactsPath = `/apps/${appId}/contacts`;
|
||||||
const webhooksPath = `/apps/${appId}/webhooks`;
|
const webhooksPath = `/apps/${appId}/webhooks`;
|
||||||
|
const groupsPath = `/apps/${appId}/groups`;
|
||||||
|
|
||||||
const isContacts = pathname.startsWith(contactsPath);
|
const isContacts = pathname.startsWith(contactsPath);
|
||||||
const isSessions = pathname.startsWith(sessionsPath);
|
const isSessions = pathname.startsWith(sessionsPath);
|
||||||
const isWebhooks = pathname.startsWith(webhooksPath);
|
const isWebhooks = pathname.startsWith(webhooksPath);
|
||||||
|
const isGroups = pathname.startsWith(groupsPath);
|
||||||
const isKeys =
|
const isKeys =
|
||||||
!isContacts &&
|
!isContacts &&
|
||||||
!isSessions &&
|
!isSessions &&
|
||||||
!isWebhooks &&
|
!isWebhooks &&
|
||||||
|
!isGroups &&
|
||||||
(pathname === `/apps/${appId}` ||
|
(pathname === `/apps/${appId}` ||
|
||||||
pathname === `/apps/${appId}/` ||
|
pathname === `/apps/${appId}/` ||
|
||||||
pathname.startsWith(`/apps/${appId}/`));
|
pathname.startsWith(`/apps/${appId}/`));
|
||||||
|
|
@ -245,6 +249,24 @@ function AppContextNav({
|
||||||
Webhooks
|
Webhooks
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/apps/$appId/groups/"
|
||||||
|
params={{ appId }}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2.5 px-3 py-2 rounded-md text-[13px] font-medium transition-colors duration-150 ml-1",
|
||||||
|
isGroups
|
||||||
|
? "bg-zinc-800 text-white"
|
||||||
|
: "text-zinc-400 hover:text-white hover:bg-zinc-900",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Users2
|
||||||
|
size={13}
|
||||||
|
strokeWidth={isGroups ? 2 : 1.75}
|
||||||
|
className={isGroups ? "text-emerald-400" : ""}
|
||||||
|
/>
|
||||||
|
Groups
|
||||||
|
</Link>
|
||||||
|
|
||||||
{/* Separator */}
|
{/* Separator */}
|
||||||
<div className="border-t border-zinc-900 my-2 mx-1" />
|
<div className="border-t border-zinc-900 my-2 mx-1" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,40 @@ export const customFieldSchemasRelations = relations(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ─── WhatsApp Groups ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const whatsappGroups = pgTable(
|
||||||
|
"whatsapp_groups",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
appId: uuid("app_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => apps.id),
|
||||||
|
sessionId: uuid("session_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => sessions.id, { onDelete: "cascade" }),
|
||||||
|
jid: text("jid").notNull(),
|
||||||
|
subject: text("subject"),
|
||||||
|
description: text("description"),
|
||||||
|
participantCount: integer("participant_count"),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
unique("whatsapp_groups_session_jid_unique").on(t.sessionId, t.jid),
|
||||||
|
index("whatsapp_groups_app_id_idx").on(t.appId),
|
||||||
|
index("whatsapp_groups_session_id_idx").on(t.sessionId),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const whatsappGroupsRelations = relations(whatsappGroups, ({ one }) => ({
|
||||||
|
app: one(apps, { fields: [whatsappGroups.appId], references: [apps.id] }),
|
||||||
|
session: one(sessions, {
|
||||||
|
fields: [whatsappGroups.sessionId],
|
||||||
|
references: [sessions.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
// ─── Webhooks ────────────────────────────────────────────────────────────────
|
// ─── Webhooks ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export const webhookDeliveryStatusEnum = pgEnum("webhook_delivery_status", [
|
export const webhookDeliveryStatusEnum = pgEnum("webhook_delivery_status", [
|
||||||
|
|
|
||||||
90
src/lib/actions/whatsapp-groups.ts
Normal file
90
src/lib/actions/whatsapp-groups.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { and, eq, sql } from "drizzle-orm";
|
||||||
|
import { db } from "#/db/index";
|
||||||
|
import { whatsappGroups } from "#/db/schema";
|
||||||
|
|
||||||
|
type WhatsappGroup = typeof whatsappGroups.$inferSelect;
|
||||||
|
|
||||||
|
export async function upsertWhatsappGroup(data: {
|
||||||
|
appId: string;
|
||||||
|
sessionId: string;
|
||||||
|
jid: string;
|
||||||
|
subject?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
participantCount?: number | null;
|
||||||
|
}): Promise<WhatsappGroup> {
|
||||||
|
const set: Partial<typeof whatsappGroups.$inferInsert> = {
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
if (data.subject !== undefined) set.subject = data.subject;
|
||||||
|
if (data.description !== undefined) set.description = data.description;
|
||||||
|
if (data.participantCount !== undefined) set.participantCount = data.participantCount;
|
||||||
|
|
||||||
|
const [row] = await db
|
||||||
|
.insert(whatsappGroups)
|
||||||
|
.values({
|
||||||
|
appId: data.appId,
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
jid: data.jid,
|
||||||
|
subject: data.subject ?? null,
|
||||||
|
description: data.description ?? null,
|
||||||
|
participantCount: data.participantCount ?? null,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [whatsappGroups.sessionId, whatsappGroups.jid],
|
||||||
|
set,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function adjustParticipantCount(
|
||||||
|
sessionId: string,
|
||||||
|
jid: string,
|
||||||
|
delta: number,
|
||||||
|
): Promise<void> {
|
||||||
|
await db
|
||||||
|
.update(whatsappGroups)
|
||||||
|
.set({
|
||||||
|
participantCount: sql`GREATEST(0, COALESCE(${whatsappGroups.participantCount}, 0) + ${delta})`,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(whatsappGroups.sessionId, sessionId),
|
||||||
|
eq(whatsappGroups.jid, jid),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listWhatsappGroups(
|
||||||
|
appId: string,
|
||||||
|
sessionId?: string,
|
||||||
|
): Promise<WhatsappGroup[]> {
|
||||||
|
const conditions = [eq(whatsappGroups.appId, appId)];
|
||||||
|
if (sessionId) conditions.push(eq(whatsappGroups.sessionId, sessionId));
|
||||||
|
return db
|
||||||
|
.select()
|
||||||
|
.from(whatsappGroups)
|
||||||
|
.where(and(...conditions))
|
||||||
|
.orderBy(whatsappGroups.updatedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getWhatsappGroup(
|
||||||
|
jid: string,
|
||||||
|
sessionId: string,
|
||||||
|
appId: string,
|
||||||
|
): Promise<WhatsappGroup | null> {
|
||||||
|
const [row] = await db
|
||||||
|
.select()
|
||||||
|
.from(whatsappGroups)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(whatsappGroups.jid, jid),
|
||||||
|
eq(whatsappGroups.sessionId, sessionId),
|
||||||
|
eq(whatsappGroups.appId, appId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
return row ?? null;
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,10 @@ import { db } from "#/db/index";
|
||||||
import { apps, sessions } from "#/db/schema";
|
import { apps, sessions } from "#/db/schema";
|
||||||
import { decryptColumn, encryptColumn, unwrapDek } from "#/lib/crypto";
|
import { decryptColumn, encryptColumn, unwrapDek } from "#/lib/crypto";
|
||||||
import { createMessage, updateMessageStatus } from "#/lib/actions/messages";
|
import { createMessage, updateMessageStatus } from "#/lib/actions/messages";
|
||||||
|
import {
|
||||||
|
upsertWhatsappGroup,
|
||||||
|
adjustParticipantCount,
|
||||||
|
} from "#/lib/actions/whatsapp-groups";
|
||||||
import { getStorageDriver } from "#/lib/storage";
|
import { getStorageDriver } from "#/lib/storage";
|
||||||
import { usePostgresAuthState } from "./auth-state";
|
import { usePostgresAuthState } from "./auth-state";
|
||||||
import { emitWebhook } from "./webhook";
|
import { emitWebhook } from "./webhook";
|
||||||
|
|
@ -333,6 +337,107 @@ export async function startSession(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
sock.ev.on("groups.upsert", async (groups) => {
|
||||||
|
for (const group of groups) {
|
||||||
|
await upsertWhatsappGroup({
|
||||||
|
appId,
|
||||||
|
sessionId,
|
||||||
|
jid: group.id,
|
||||||
|
subject: group.subject,
|
||||||
|
description: group.desc,
|
||||||
|
participantCount: group.participants.length,
|
||||||
|
}).catch(console.error);
|
||||||
|
emitWebhook(appId, "group.joined", {
|
||||||
|
sessionId,
|
||||||
|
jid: group.id,
|
||||||
|
subject: group.subject,
|
||||||
|
description: group.desc ?? null,
|
||||||
|
participantCount: group.participants.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sock.ev.on("groups.update", async (updates) => {
|
||||||
|
for (const update of updates) {
|
||||||
|
if (!update.id) continue;
|
||||||
|
await upsertWhatsappGroup({
|
||||||
|
appId,
|
||||||
|
sessionId,
|
||||||
|
jid: update.id,
|
||||||
|
subject: update.subject,
|
||||||
|
description: update.desc,
|
||||||
|
}).catch(console.error);
|
||||||
|
emitWebhook(appId, "group.updated", {
|
||||||
|
sessionId,
|
||||||
|
jid: update.id,
|
||||||
|
subject: update.subject ?? null,
|
||||||
|
description: update.desc ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sock.ev.on("group-participants.update", async ({ id, participants, action }) => {
|
||||||
|
const ownJid = state.creds.me?.id;
|
||||||
|
const jidUser = (j: string) => j.split(":")[0].split("@")[0];
|
||||||
|
const ownRemoved = action === "remove" && ownJid &&
|
||||||
|
participants.some((p) => p === ownJid || jidUser(p) === jidUser(ownJid));
|
||||||
|
const ownAdded = action === "add" && ownJid &&
|
||||||
|
participants.some((p) => p === ownJid || jidUser(p) === jidUser(ownJid));
|
||||||
|
|
||||||
|
if (ownAdded) {
|
||||||
|
try {
|
||||||
|
const meta = await sock.groupMetadata(id);
|
||||||
|
await upsertWhatsappGroup({
|
||||||
|
appId,
|
||||||
|
sessionId,
|
||||||
|
jid: id,
|
||||||
|
subject: meta.subject,
|
||||||
|
description: meta.desc,
|
||||||
|
participantCount: meta.participants.length,
|
||||||
|
});
|
||||||
|
emitWebhook(appId, "group.joined", {
|
||||||
|
sessionId,
|
||||||
|
jid: id,
|
||||||
|
subject: meta.subject,
|
||||||
|
description: meta.desc,
|
||||||
|
participantCount: meta.participants.length,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
await adjustParticipantCount(sessionId, id, participants.length).catch(console.error);
|
||||||
|
emitWebhook(appId, "group.joined", { sessionId, jid: id });
|
||||||
|
}
|
||||||
|
} else if (ownRemoved) {
|
||||||
|
await adjustParticipantCount(sessionId, id, -participants.length).catch(console.error);
|
||||||
|
emitWebhook(appId, "group.left", { sessionId, jid: id, participants });
|
||||||
|
} else if (action === "add") {
|
||||||
|
await adjustParticipantCount(sessionId, id, participants.length).catch(console.error);
|
||||||
|
emitWebhook(appId, "group.participants_added", { sessionId, jid: id, participants });
|
||||||
|
} else if (action === "remove") {
|
||||||
|
await adjustParticipantCount(sessionId, id, -participants.length).catch(console.error);
|
||||||
|
emitWebhook(appId, "group.participants_removed", { sessionId, jid: id, participants });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// History sync on reconnect — captures all current groups
|
||||||
|
sock.ev.on("messaging-history.set", async ({ chats }) => {
|
||||||
|
const groupChats = chats.filter((c) => c.id?.endsWith("@g.us"));
|
||||||
|
for (const chat of groupChats) {
|
||||||
|
try {
|
||||||
|
const meta = await sock.groupMetadata(chat.id);
|
||||||
|
await upsertWhatsappGroup({
|
||||||
|
appId,
|
||||||
|
sessionId,
|
||||||
|
jid: chat.id,
|
||||||
|
subject: meta.subject,
|
||||||
|
description: meta.desc,
|
||||||
|
participantCount: meta.participants.length,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
await upsertWhatsappGroup({ appId, sessionId, jid: chat.id }).catch(console.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
sock.ev.on("connection.update", async (update) => {
|
sock.ev.on("connection.update", async (update) => {
|
||||||
const { connection, lastDisconnect, qr } = update;
|
const { connection, lastDisconnect, qr } = update;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,17 @@ export const webhookDeliveriesQuery = (appId: string, webhookId: string) =>
|
||||||
staleTime: 10_000,
|
staleTime: 10_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const whatsappGroupsQuery = (appId: string, sessionId?: string) => {
|
||||||
|
const params = new URLSearchParams({ app_id: appId });
|
||||||
|
if (sessionId) params.set("session_id", sessionId);
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: ["whatsapp-groups", appId, sessionId],
|
||||||
|
queryFn: () =>
|
||||||
|
apiFetch(`/api/v1/admin/whatsapp-groups?${params.toString()}`),
|
||||||
|
staleTime: 15_000,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const monitoringStatsQuery = queryOptions({
|
export const monitoringStatsQuery = queryOptions({
|
||||||
queryKey: ["monitoring", "stats"],
|
queryKey: ["monitoring", "stats"],
|
||||||
queryFn: () => apiFetch("/api/v1/admin/monitoring/stats"),
|
queryFn: () => apiFetch("/api/v1/admin/monitoring/stats"),
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@ import { Route as ApiAuthLogoutRouteImport } from './routes/api/auth/logout'
|
||||||
import { Route as ApiAuthLoginRouteImport } from './routes/api/auth/login'
|
import { Route as ApiAuthLoginRouteImport } from './routes/api/auth/login'
|
||||||
import { Route as AppsAppIdWebhooksIndexRouteImport } from './routes/apps/$appId/webhooks/index'
|
import { Route as AppsAppIdWebhooksIndexRouteImport } from './routes/apps/$appId/webhooks/index'
|
||||||
import { Route as AppsAppIdSessionsIndexRouteImport } from './routes/apps/$appId/sessions/index'
|
import { Route as AppsAppIdSessionsIndexRouteImport } from './routes/apps/$appId/sessions/index'
|
||||||
|
import { Route as AppsAppIdGroupsIndexRouteImport } from './routes/apps/$appId/groups/index'
|
||||||
import { Route as AppsAppIdContactsIndexRouteImport } from './routes/apps/$appId/contacts/index'
|
import { Route as AppsAppIdContactsIndexRouteImport } from './routes/apps/$appId/contacts/index'
|
||||||
|
import { Route as ApiV1WhatsappGroupsIndexRouteImport } from './routes/api/v1/whatsapp-groups/index'
|
||||||
import { Route as ApiV1WebhooksIndexRouteImport } from './routes/api/v1/webhooks/index'
|
import { Route as ApiV1WebhooksIndexRouteImport } from './routes/api/v1/webhooks/index'
|
||||||
import { Route as ApiV1SessionsIndexRouteImport } from './routes/api/v1/sessions/index'
|
import { Route as ApiV1SessionsIndexRouteImport } from './routes/api/v1/sessions/index'
|
||||||
import { Route as ApiV1MediaIndexRouteImport } from './routes/api/v1/media/index'
|
import { Route as ApiV1MediaIndexRouteImport } from './routes/api/v1/media/index'
|
||||||
|
|
@ -44,12 +46,14 @@ import { Route as ApiV1AdminAuditLogRouteImport } from './routes/api/v1/admin/au
|
||||||
import { Route as ApiAuthTotpEnrollRouteImport } from './routes/api/auth/totp/enroll'
|
import { Route as ApiAuthTotpEnrollRouteImport } from './routes/api/auth/totp/enroll'
|
||||||
import { Route as AppsAppIdContactsSettingsIndexRouteImport } from './routes/apps/$appId/contacts/settings/index'
|
import { Route as AppsAppIdContactsSettingsIndexRouteImport } from './routes/apps/$appId/contacts/settings/index'
|
||||||
import { Route as AppsAppIdContactsGroupsIndexRouteImport } from './routes/apps/$appId/contacts/groups/index'
|
import { Route as AppsAppIdContactsGroupsIndexRouteImport } from './routes/apps/$appId/contacts/groups/index'
|
||||||
|
import { Route as ApiV1WhatsappGroupsJidIndexRouteImport } from './routes/api/v1/whatsapp-groups/$jid/index'
|
||||||
import { Route as ApiV1WebhooksIdIndexRouteImport } from './routes/api/v1/webhooks/$id/index'
|
import { Route as ApiV1WebhooksIdIndexRouteImport } from './routes/api/v1/webhooks/$id/index'
|
||||||
import { Route as ApiV1SessionsIdIndexRouteImport } from './routes/api/v1/sessions/$id/index'
|
import { Route as ApiV1SessionsIdIndexRouteImport } from './routes/api/v1/sessions/$id/index'
|
||||||
import { Route as ApiV1ContactsSyncFromWhatsappIndexRouteImport } from './routes/api/v1/contacts/sync-from-whatsapp/index'
|
import { Route as ApiV1ContactsSyncFromWhatsappIndexRouteImport } from './routes/api/v1/contacts/sync-from-whatsapp/index'
|
||||||
import { Route as ApiV1ContactsImportIndexRouteImport } from './routes/api/v1/contacts/import/index'
|
import { Route as ApiV1ContactsImportIndexRouteImport } from './routes/api/v1/contacts/import/index'
|
||||||
import { Route as ApiV1ContactsIdIndexRouteImport } from './routes/api/v1/contacts/$id/index'
|
import { Route as ApiV1ContactsIdIndexRouteImport } from './routes/api/v1/contacts/$id/index'
|
||||||
import { Route as ApiV1ContactGroupsIdIndexRouteImport } from './routes/api/v1/contact-groups/$id/index'
|
import { Route as ApiV1ContactGroupsIdIndexRouteImport } from './routes/api/v1/contact-groups/$id/index'
|
||||||
|
import { Route as ApiV1AdminWhatsappGroupsIndexRouteImport } from './routes/api/v1/admin/whatsapp-groups/index'
|
||||||
import { Route as ApiV1AdminWebhooksIndexRouteImport } from './routes/api/v1/admin/webhooks/index'
|
import { Route as ApiV1AdminWebhooksIndexRouteImport } from './routes/api/v1/admin/webhooks/index'
|
||||||
import { Route as ApiV1AdminSessionsIndexRouteImport } from './routes/api/v1/admin/sessions/index'
|
import { Route as ApiV1AdminSessionsIndexRouteImport } from './routes/api/v1/admin/sessions/index'
|
||||||
import { Route as ApiV1AdminCustomFieldSchemasIndexRouteImport } from './routes/api/v1/admin/custom-field-schemas/index'
|
import { Route as ApiV1AdminCustomFieldSchemasIndexRouteImport } from './routes/api/v1/admin/custom-field-schemas/index'
|
||||||
|
|
@ -69,6 +73,7 @@ import { Route as ApiV1AdminMonitoringStatsRouteImport } from './routes/api/v1/a
|
||||||
import { Route as ApiV1AdminCustomFieldSchemasIdRouteImport } from './routes/api/v1/admin/custom-field-schemas/$id'
|
import { Route as ApiV1AdminCustomFieldSchemasIdRouteImport } from './routes/api/v1/admin/custom-field-schemas/$id'
|
||||||
import { Route as ApiV1SessionsIdMessagesIndexRouteImport } from './routes/api/v1/sessions/$id/messages/index'
|
import { Route as ApiV1SessionsIdMessagesIndexRouteImport } from './routes/api/v1/sessions/$id/messages/index'
|
||||||
import { Route as ApiV1ContactGroupsIdMembersIndexRouteImport } from './routes/api/v1/contact-groups/$id/members/index'
|
import { Route as ApiV1ContactGroupsIdMembersIndexRouteImport } from './routes/api/v1/contact-groups/$id/members/index'
|
||||||
|
import { Route as ApiV1AdminWhatsappGroupsJidIndexRouteImport } from './routes/api/v1/admin/whatsapp-groups/$jid/index'
|
||||||
import { Route as ApiV1AdminWebhooksIdIndexRouteImport } from './routes/api/v1/admin/webhooks/$id/index'
|
import { Route as ApiV1AdminWebhooksIdIndexRouteImport } from './routes/api/v1/admin/webhooks/$id/index'
|
||||||
import { Route as ApiV1AdminSessionsIdIndexRouteImport } from './routes/api/v1/admin/sessions/$id/index'
|
import { Route as ApiV1AdminSessionsIdIndexRouteImport } from './routes/api/v1/admin/sessions/$id/index'
|
||||||
import { Route as ApiV1AdminContactsIdIndexRouteImport } from './routes/api/v1/admin/contacts/$id/index'
|
import { Route as ApiV1AdminContactsIdIndexRouteImport } from './routes/api/v1/admin/contacts/$id/index'
|
||||||
|
|
@ -190,11 +195,22 @@ const AppsAppIdSessionsIndexRoute = AppsAppIdSessionsIndexRouteImport.update({
|
||||||
path: '/sessions/',
|
path: '/sessions/',
|
||||||
getParentRoute: () => AppsAppIdRoute,
|
getParentRoute: () => AppsAppIdRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AppsAppIdGroupsIndexRoute = AppsAppIdGroupsIndexRouteImport.update({
|
||||||
|
id: '/groups/',
|
||||||
|
path: '/groups/',
|
||||||
|
getParentRoute: () => AppsAppIdRoute,
|
||||||
|
} as any)
|
||||||
const AppsAppIdContactsIndexRoute = AppsAppIdContactsIndexRouteImport.update({
|
const AppsAppIdContactsIndexRoute = AppsAppIdContactsIndexRouteImport.update({
|
||||||
id: '/contacts/',
|
id: '/contacts/',
|
||||||
path: '/contacts/',
|
path: '/contacts/',
|
||||||
getParentRoute: () => AppsAppIdRoute,
|
getParentRoute: () => AppsAppIdRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiV1WhatsappGroupsIndexRoute =
|
||||||
|
ApiV1WhatsappGroupsIndexRouteImport.update({
|
||||||
|
id: '/api/v1/whatsapp-groups/',
|
||||||
|
path: '/api/v1/whatsapp-groups/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ApiV1WebhooksIndexRoute = ApiV1WebhooksIndexRouteImport.update({
|
const ApiV1WebhooksIndexRoute = ApiV1WebhooksIndexRouteImport.update({
|
||||||
id: '/api/v1/webhooks/',
|
id: '/api/v1/webhooks/',
|
||||||
path: '/api/v1/webhooks/',
|
path: '/api/v1/webhooks/',
|
||||||
|
|
@ -276,6 +292,12 @@ const AppsAppIdContactsGroupsIndexRoute =
|
||||||
path: '/contacts/groups/',
|
path: '/contacts/groups/',
|
||||||
getParentRoute: () => AppsAppIdRoute,
|
getParentRoute: () => AppsAppIdRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiV1WhatsappGroupsJidIndexRoute =
|
||||||
|
ApiV1WhatsappGroupsJidIndexRouteImport.update({
|
||||||
|
id: '/api/v1/whatsapp-groups/$jid/',
|
||||||
|
path: '/api/v1/whatsapp-groups/$jid/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ApiV1WebhooksIdIndexRoute = ApiV1WebhooksIdIndexRouteImport.update({
|
const ApiV1WebhooksIdIndexRoute = ApiV1WebhooksIdIndexRouteImport.update({
|
||||||
id: '/api/v1/webhooks/$id/',
|
id: '/api/v1/webhooks/$id/',
|
||||||
path: '/api/v1/webhooks/$id/',
|
path: '/api/v1/webhooks/$id/',
|
||||||
|
|
@ -309,6 +331,12 @@ const ApiV1ContactGroupsIdIndexRoute =
|
||||||
path: '/api/v1/contact-groups/$id/',
|
path: '/api/v1/contact-groups/$id/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiV1AdminWhatsappGroupsIndexRoute =
|
||||||
|
ApiV1AdminWhatsappGroupsIndexRouteImport.update({
|
||||||
|
id: '/api/v1/admin/whatsapp-groups/',
|
||||||
|
path: '/api/v1/admin/whatsapp-groups/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ApiV1AdminWebhooksIndexRoute = ApiV1AdminWebhooksIndexRouteImport.update({
|
const ApiV1AdminWebhooksIndexRoute = ApiV1AdminWebhooksIndexRouteImport.update({
|
||||||
id: '/api/v1/admin/webhooks/',
|
id: '/api/v1/admin/webhooks/',
|
||||||
path: '/api/v1/admin/webhooks/',
|
path: '/api/v1/admin/webhooks/',
|
||||||
|
|
@ -414,6 +442,12 @@ const ApiV1ContactGroupsIdMembersIndexRoute =
|
||||||
path: '/api/v1/contact-groups/$id/members/',
|
path: '/api/v1/contact-groups/$id/members/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const ApiV1AdminWhatsappGroupsJidIndexRoute =
|
||||||
|
ApiV1AdminWhatsappGroupsJidIndexRouteImport.update({
|
||||||
|
id: '/api/v1/admin/whatsapp-groups/$jid/',
|
||||||
|
path: '/api/v1/admin/whatsapp-groups/$jid/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
const ApiV1AdminWebhooksIdIndexRoute =
|
const ApiV1AdminWebhooksIdIndexRoute =
|
||||||
ApiV1AdminWebhooksIdIndexRouteImport.update({
|
ApiV1AdminWebhooksIdIndexRouteImport.update({
|
||||||
id: '/api/v1/admin/webhooks/$id/',
|
id: '/api/v1/admin/webhooks/$id/',
|
||||||
|
|
@ -594,7 +628,9 @@ export interface FileRoutesByFullPath {
|
||||||
'/api/v1/media/': typeof ApiV1MediaIndexRoute
|
'/api/v1/media/': typeof ApiV1MediaIndexRoute
|
||||||
'/api/v1/sessions/': typeof ApiV1SessionsIndexRoute
|
'/api/v1/sessions/': typeof ApiV1SessionsIndexRoute
|
||||||
'/api/v1/webhooks/': typeof ApiV1WebhooksIndexRoute
|
'/api/v1/webhooks/': typeof ApiV1WebhooksIndexRoute
|
||||||
|
'/api/v1/whatsapp-groups/': typeof ApiV1WhatsappGroupsIndexRoute
|
||||||
'/apps/$appId/contacts/': typeof AppsAppIdContactsIndexRoute
|
'/apps/$appId/contacts/': typeof AppsAppIdContactsIndexRoute
|
||||||
|
'/apps/$appId/groups/': typeof AppsAppIdGroupsIndexRoute
|
||||||
'/apps/$appId/sessions/': typeof AppsAppIdSessionsIndexRoute
|
'/apps/$appId/sessions/': typeof AppsAppIdSessionsIndexRoute
|
||||||
'/apps/$appId/webhooks/': typeof AppsAppIdWebhooksIndexRoute
|
'/apps/$appId/webhooks/': typeof AppsAppIdWebhooksIndexRoute
|
||||||
'/api/v1/admin/custom-field-schemas/$id': typeof ApiV1AdminCustomFieldSchemasIdRoute
|
'/api/v1/admin/custom-field-schemas/$id': typeof ApiV1AdminCustomFieldSchemasIdRoute
|
||||||
|
|
@ -614,12 +650,14 @@ export interface FileRoutesByFullPath {
|
||||||
'/api/v1/admin/custom-field-schemas/': typeof ApiV1AdminCustomFieldSchemasIndexRoute
|
'/api/v1/admin/custom-field-schemas/': typeof ApiV1AdminCustomFieldSchemasIndexRoute
|
||||||
'/api/v1/admin/sessions/': typeof ApiV1AdminSessionsIndexRoute
|
'/api/v1/admin/sessions/': typeof ApiV1AdminSessionsIndexRoute
|
||||||
'/api/v1/admin/webhooks/': typeof ApiV1AdminWebhooksIndexRoute
|
'/api/v1/admin/webhooks/': typeof ApiV1AdminWebhooksIndexRoute
|
||||||
|
'/api/v1/admin/whatsapp-groups/': typeof ApiV1AdminWhatsappGroupsIndexRoute
|
||||||
'/api/v1/contact-groups/$id/': typeof ApiV1ContactGroupsIdIndexRoute
|
'/api/v1/contact-groups/$id/': typeof ApiV1ContactGroupsIdIndexRoute
|
||||||
'/api/v1/contacts/$id/': typeof ApiV1ContactsIdIndexRoute
|
'/api/v1/contacts/$id/': typeof ApiV1ContactsIdIndexRoute
|
||||||
'/api/v1/contacts/import/': typeof ApiV1ContactsImportIndexRoute
|
'/api/v1/contacts/import/': typeof ApiV1ContactsImportIndexRoute
|
||||||
'/api/v1/contacts/sync-from-whatsapp/': typeof ApiV1ContactsSyncFromWhatsappIndexRoute
|
'/api/v1/contacts/sync-from-whatsapp/': typeof ApiV1ContactsSyncFromWhatsappIndexRoute
|
||||||
'/api/v1/sessions/$id/': typeof ApiV1SessionsIdIndexRoute
|
'/api/v1/sessions/$id/': typeof ApiV1SessionsIdIndexRoute
|
||||||
'/api/v1/webhooks/$id/': typeof ApiV1WebhooksIdIndexRoute
|
'/api/v1/webhooks/$id/': typeof ApiV1WebhooksIdIndexRoute
|
||||||
|
'/api/v1/whatsapp-groups/$jid/': typeof ApiV1WhatsappGroupsJidIndexRoute
|
||||||
'/apps/$appId/contacts/groups/': typeof AppsAppIdContactsGroupsIndexRoute
|
'/apps/$appId/contacts/groups/': typeof AppsAppIdContactsGroupsIndexRoute
|
||||||
'/apps/$appId/contacts/settings/': typeof AppsAppIdContactsSettingsIndexRoute
|
'/apps/$appId/contacts/settings/': typeof AppsAppIdContactsSettingsIndexRoute
|
||||||
'/api/v1/admin/apps/$id/sync-contacts': typeof ApiV1AdminAppsIdSyncContactsRoute
|
'/api/v1/admin/apps/$id/sync-contacts': typeof ApiV1AdminAppsIdSyncContactsRoute
|
||||||
|
|
@ -641,6 +679,7 @@ export interface FileRoutesByFullPath {
|
||||||
'/api/v1/admin/contacts/$id/': typeof ApiV1AdminContactsIdIndexRoute
|
'/api/v1/admin/contacts/$id/': typeof ApiV1AdminContactsIdIndexRoute
|
||||||
'/api/v1/admin/sessions/$id/': typeof ApiV1AdminSessionsIdIndexRoute
|
'/api/v1/admin/sessions/$id/': typeof ApiV1AdminSessionsIdIndexRoute
|
||||||
'/api/v1/admin/webhooks/$id/': typeof ApiV1AdminWebhooksIdIndexRoute
|
'/api/v1/admin/webhooks/$id/': typeof ApiV1AdminWebhooksIdIndexRoute
|
||||||
|
'/api/v1/admin/whatsapp-groups/$jid/': typeof ApiV1AdminWhatsappGroupsJidIndexRoute
|
||||||
'/api/v1/contact-groups/$id/members/': typeof ApiV1ContactGroupsIdMembersIndexRoute
|
'/api/v1/contact-groups/$id/members/': typeof ApiV1ContactGroupsIdMembersIndexRoute
|
||||||
'/api/v1/sessions/$id/messages/': typeof ApiV1SessionsIdMessagesIndexRoute
|
'/api/v1/sessions/$id/messages/': typeof ApiV1SessionsIdMessagesIndexRoute
|
||||||
'/api/v1/admin/apps/$id/api-keys/$keyId': typeof ApiV1AdminAppsIdApiKeysKeyIdRoute
|
'/api/v1/admin/apps/$id/api-keys/$keyId': typeof ApiV1AdminAppsIdApiKeysKeyIdRoute
|
||||||
|
|
@ -680,7 +719,9 @@ export interface FileRoutesByTo {
|
||||||
'/api/v1/media': typeof ApiV1MediaIndexRoute
|
'/api/v1/media': typeof ApiV1MediaIndexRoute
|
||||||
'/api/v1/sessions': typeof ApiV1SessionsIndexRoute
|
'/api/v1/sessions': typeof ApiV1SessionsIndexRoute
|
||||||
'/api/v1/webhooks': typeof ApiV1WebhooksIndexRoute
|
'/api/v1/webhooks': typeof ApiV1WebhooksIndexRoute
|
||||||
|
'/api/v1/whatsapp-groups': typeof ApiV1WhatsappGroupsIndexRoute
|
||||||
'/apps/$appId/contacts': typeof AppsAppIdContactsIndexRoute
|
'/apps/$appId/contacts': typeof AppsAppIdContactsIndexRoute
|
||||||
|
'/apps/$appId/groups': typeof AppsAppIdGroupsIndexRoute
|
||||||
'/apps/$appId/sessions': typeof AppsAppIdSessionsIndexRoute
|
'/apps/$appId/sessions': typeof AppsAppIdSessionsIndexRoute
|
||||||
'/apps/$appId/webhooks': typeof AppsAppIdWebhooksIndexRoute
|
'/apps/$appId/webhooks': typeof AppsAppIdWebhooksIndexRoute
|
||||||
'/api/v1/admin/custom-field-schemas/$id': typeof ApiV1AdminCustomFieldSchemasIdRoute
|
'/api/v1/admin/custom-field-schemas/$id': typeof ApiV1AdminCustomFieldSchemasIdRoute
|
||||||
|
|
@ -700,12 +741,14 @@ export interface FileRoutesByTo {
|
||||||
'/api/v1/admin/custom-field-schemas': typeof ApiV1AdminCustomFieldSchemasIndexRoute
|
'/api/v1/admin/custom-field-schemas': typeof ApiV1AdminCustomFieldSchemasIndexRoute
|
||||||
'/api/v1/admin/sessions': typeof ApiV1AdminSessionsIndexRoute
|
'/api/v1/admin/sessions': typeof ApiV1AdminSessionsIndexRoute
|
||||||
'/api/v1/admin/webhooks': typeof ApiV1AdminWebhooksIndexRoute
|
'/api/v1/admin/webhooks': typeof ApiV1AdminWebhooksIndexRoute
|
||||||
|
'/api/v1/admin/whatsapp-groups': typeof ApiV1AdminWhatsappGroupsIndexRoute
|
||||||
'/api/v1/contact-groups/$id': typeof ApiV1ContactGroupsIdIndexRoute
|
'/api/v1/contact-groups/$id': typeof ApiV1ContactGroupsIdIndexRoute
|
||||||
'/api/v1/contacts/$id': typeof ApiV1ContactsIdIndexRoute
|
'/api/v1/contacts/$id': typeof ApiV1ContactsIdIndexRoute
|
||||||
'/api/v1/contacts/import': typeof ApiV1ContactsImportIndexRoute
|
'/api/v1/contacts/import': typeof ApiV1ContactsImportIndexRoute
|
||||||
'/api/v1/contacts/sync-from-whatsapp': typeof ApiV1ContactsSyncFromWhatsappIndexRoute
|
'/api/v1/contacts/sync-from-whatsapp': typeof ApiV1ContactsSyncFromWhatsappIndexRoute
|
||||||
'/api/v1/sessions/$id': typeof ApiV1SessionsIdIndexRoute
|
'/api/v1/sessions/$id': typeof ApiV1SessionsIdIndexRoute
|
||||||
'/api/v1/webhooks/$id': typeof ApiV1WebhooksIdIndexRoute
|
'/api/v1/webhooks/$id': typeof ApiV1WebhooksIdIndexRoute
|
||||||
|
'/api/v1/whatsapp-groups/$jid': typeof ApiV1WhatsappGroupsJidIndexRoute
|
||||||
'/apps/$appId/contacts/groups': typeof AppsAppIdContactsGroupsIndexRoute
|
'/apps/$appId/contacts/groups': typeof AppsAppIdContactsGroupsIndexRoute
|
||||||
'/apps/$appId/contacts/settings': typeof AppsAppIdContactsSettingsIndexRoute
|
'/apps/$appId/contacts/settings': typeof AppsAppIdContactsSettingsIndexRoute
|
||||||
'/api/v1/admin/apps/$id/sync-contacts': typeof ApiV1AdminAppsIdSyncContactsRoute
|
'/api/v1/admin/apps/$id/sync-contacts': typeof ApiV1AdminAppsIdSyncContactsRoute
|
||||||
|
|
@ -727,6 +770,7 @@ export interface FileRoutesByTo {
|
||||||
'/api/v1/admin/contacts/$id': typeof ApiV1AdminContactsIdIndexRoute
|
'/api/v1/admin/contacts/$id': typeof ApiV1AdminContactsIdIndexRoute
|
||||||
'/api/v1/admin/sessions/$id': typeof ApiV1AdminSessionsIdIndexRoute
|
'/api/v1/admin/sessions/$id': typeof ApiV1AdminSessionsIdIndexRoute
|
||||||
'/api/v1/admin/webhooks/$id': typeof ApiV1AdminWebhooksIdIndexRoute
|
'/api/v1/admin/webhooks/$id': typeof ApiV1AdminWebhooksIdIndexRoute
|
||||||
|
'/api/v1/admin/whatsapp-groups/$jid': typeof ApiV1AdminWhatsappGroupsJidIndexRoute
|
||||||
'/api/v1/contact-groups/$id/members': typeof ApiV1ContactGroupsIdMembersIndexRoute
|
'/api/v1/contact-groups/$id/members': typeof ApiV1ContactGroupsIdMembersIndexRoute
|
||||||
'/api/v1/sessions/$id/messages': typeof ApiV1SessionsIdMessagesIndexRoute
|
'/api/v1/sessions/$id/messages': typeof ApiV1SessionsIdMessagesIndexRoute
|
||||||
'/api/v1/admin/apps/$id/api-keys/$keyId': typeof ApiV1AdminAppsIdApiKeysKeyIdRoute
|
'/api/v1/admin/apps/$id/api-keys/$keyId': typeof ApiV1AdminAppsIdApiKeysKeyIdRoute
|
||||||
|
|
@ -768,7 +812,9 @@ export interface FileRoutesById {
|
||||||
'/api/v1/media/': typeof ApiV1MediaIndexRoute
|
'/api/v1/media/': typeof ApiV1MediaIndexRoute
|
||||||
'/api/v1/sessions/': typeof ApiV1SessionsIndexRoute
|
'/api/v1/sessions/': typeof ApiV1SessionsIndexRoute
|
||||||
'/api/v1/webhooks/': typeof ApiV1WebhooksIndexRoute
|
'/api/v1/webhooks/': typeof ApiV1WebhooksIndexRoute
|
||||||
|
'/api/v1/whatsapp-groups/': typeof ApiV1WhatsappGroupsIndexRoute
|
||||||
'/apps/$appId/contacts/': typeof AppsAppIdContactsIndexRoute
|
'/apps/$appId/contacts/': typeof AppsAppIdContactsIndexRoute
|
||||||
|
'/apps/$appId/groups/': typeof AppsAppIdGroupsIndexRoute
|
||||||
'/apps/$appId/sessions/': typeof AppsAppIdSessionsIndexRoute
|
'/apps/$appId/sessions/': typeof AppsAppIdSessionsIndexRoute
|
||||||
'/apps/$appId/webhooks/': typeof AppsAppIdWebhooksIndexRoute
|
'/apps/$appId/webhooks/': typeof AppsAppIdWebhooksIndexRoute
|
||||||
'/api/v1/admin/custom-field-schemas/$id': typeof ApiV1AdminCustomFieldSchemasIdRoute
|
'/api/v1/admin/custom-field-schemas/$id': typeof ApiV1AdminCustomFieldSchemasIdRoute
|
||||||
|
|
@ -788,12 +834,14 @@ export interface FileRoutesById {
|
||||||
'/api/v1/admin/custom-field-schemas/': typeof ApiV1AdminCustomFieldSchemasIndexRoute
|
'/api/v1/admin/custom-field-schemas/': typeof ApiV1AdminCustomFieldSchemasIndexRoute
|
||||||
'/api/v1/admin/sessions/': typeof ApiV1AdminSessionsIndexRoute
|
'/api/v1/admin/sessions/': typeof ApiV1AdminSessionsIndexRoute
|
||||||
'/api/v1/admin/webhooks/': typeof ApiV1AdminWebhooksIndexRoute
|
'/api/v1/admin/webhooks/': typeof ApiV1AdminWebhooksIndexRoute
|
||||||
|
'/api/v1/admin/whatsapp-groups/': typeof ApiV1AdminWhatsappGroupsIndexRoute
|
||||||
'/api/v1/contact-groups/$id/': typeof ApiV1ContactGroupsIdIndexRoute
|
'/api/v1/contact-groups/$id/': typeof ApiV1ContactGroupsIdIndexRoute
|
||||||
'/api/v1/contacts/$id/': typeof ApiV1ContactsIdIndexRoute
|
'/api/v1/contacts/$id/': typeof ApiV1ContactsIdIndexRoute
|
||||||
'/api/v1/contacts/import/': typeof ApiV1ContactsImportIndexRoute
|
'/api/v1/contacts/import/': typeof ApiV1ContactsImportIndexRoute
|
||||||
'/api/v1/contacts/sync-from-whatsapp/': typeof ApiV1ContactsSyncFromWhatsappIndexRoute
|
'/api/v1/contacts/sync-from-whatsapp/': typeof ApiV1ContactsSyncFromWhatsappIndexRoute
|
||||||
'/api/v1/sessions/$id/': typeof ApiV1SessionsIdIndexRoute
|
'/api/v1/sessions/$id/': typeof ApiV1SessionsIdIndexRoute
|
||||||
'/api/v1/webhooks/$id/': typeof ApiV1WebhooksIdIndexRoute
|
'/api/v1/webhooks/$id/': typeof ApiV1WebhooksIdIndexRoute
|
||||||
|
'/api/v1/whatsapp-groups/$jid/': typeof ApiV1WhatsappGroupsJidIndexRoute
|
||||||
'/apps/$appId/contacts/groups/': typeof AppsAppIdContactsGroupsIndexRoute
|
'/apps/$appId/contacts/groups/': typeof AppsAppIdContactsGroupsIndexRoute
|
||||||
'/apps/$appId/contacts/settings/': typeof AppsAppIdContactsSettingsIndexRoute
|
'/apps/$appId/contacts/settings/': typeof AppsAppIdContactsSettingsIndexRoute
|
||||||
'/api/v1/admin/apps/$id/sync-contacts': typeof ApiV1AdminAppsIdSyncContactsRoute
|
'/api/v1/admin/apps/$id/sync-contacts': typeof ApiV1AdminAppsIdSyncContactsRoute
|
||||||
|
|
@ -815,6 +863,7 @@ export interface FileRoutesById {
|
||||||
'/api/v1/admin/contacts/$id/': typeof ApiV1AdminContactsIdIndexRoute
|
'/api/v1/admin/contacts/$id/': typeof ApiV1AdminContactsIdIndexRoute
|
||||||
'/api/v1/admin/sessions/$id/': typeof ApiV1AdminSessionsIdIndexRoute
|
'/api/v1/admin/sessions/$id/': typeof ApiV1AdminSessionsIdIndexRoute
|
||||||
'/api/v1/admin/webhooks/$id/': typeof ApiV1AdminWebhooksIdIndexRoute
|
'/api/v1/admin/webhooks/$id/': typeof ApiV1AdminWebhooksIdIndexRoute
|
||||||
|
'/api/v1/admin/whatsapp-groups/$jid/': typeof ApiV1AdminWhatsappGroupsJidIndexRoute
|
||||||
'/api/v1/contact-groups/$id/members/': typeof ApiV1ContactGroupsIdMembersIndexRoute
|
'/api/v1/contact-groups/$id/members/': typeof ApiV1ContactGroupsIdMembersIndexRoute
|
||||||
'/api/v1/sessions/$id/messages/': typeof ApiV1SessionsIdMessagesIndexRoute
|
'/api/v1/sessions/$id/messages/': typeof ApiV1SessionsIdMessagesIndexRoute
|
||||||
'/api/v1/admin/apps/$id/api-keys/$keyId': typeof ApiV1AdminAppsIdApiKeysKeyIdRoute
|
'/api/v1/admin/apps/$id/api-keys/$keyId': typeof ApiV1AdminAppsIdApiKeysKeyIdRoute
|
||||||
|
|
@ -857,7 +906,9 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/media/'
|
| '/api/v1/media/'
|
||||||
| '/api/v1/sessions/'
|
| '/api/v1/sessions/'
|
||||||
| '/api/v1/webhooks/'
|
| '/api/v1/webhooks/'
|
||||||
|
| '/api/v1/whatsapp-groups/'
|
||||||
| '/apps/$appId/contacts/'
|
| '/apps/$appId/contacts/'
|
||||||
|
| '/apps/$appId/groups/'
|
||||||
| '/apps/$appId/sessions/'
|
| '/apps/$appId/sessions/'
|
||||||
| '/apps/$appId/webhooks/'
|
| '/apps/$appId/webhooks/'
|
||||||
| '/api/v1/admin/custom-field-schemas/$id'
|
| '/api/v1/admin/custom-field-schemas/$id'
|
||||||
|
|
@ -877,12 +928,14 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/admin/custom-field-schemas/'
|
| '/api/v1/admin/custom-field-schemas/'
|
||||||
| '/api/v1/admin/sessions/'
|
| '/api/v1/admin/sessions/'
|
||||||
| '/api/v1/admin/webhooks/'
|
| '/api/v1/admin/webhooks/'
|
||||||
|
| '/api/v1/admin/whatsapp-groups/'
|
||||||
| '/api/v1/contact-groups/$id/'
|
| '/api/v1/contact-groups/$id/'
|
||||||
| '/api/v1/contacts/$id/'
|
| '/api/v1/contacts/$id/'
|
||||||
| '/api/v1/contacts/import/'
|
| '/api/v1/contacts/import/'
|
||||||
| '/api/v1/contacts/sync-from-whatsapp/'
|
| '/api/v1/contacts/sync-from-whatsapp/'
|
||||||
| '/api/v1/sessions/$id/'
|
| '/api/v1/sessions/$id/'
|
||||||
| '/api/v1/webhooks/$id/'
|
| '/api/v1/webhooks/$id/'
|
||||||
|
| '/api/v1/whatsapp-groups/$jid/'
|
||||||
| '/apps/$appId/contacts/groups/'
|
| '/apps/$appId/contacts/groups/'
|
||||||
| '/apps/$appId/contacts/settings/'
|
| '/apps/$appId/contacts/settings/'
|
||||||
| '/api/v1/admin/apps/$id/sync-contacts'
|
| '/api/v1/admin/apps/$id/sync-contacts'
|
||||||
|
|
@ -904,6 +957,7 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/admin/contacts/$id/'
|
| '/api/v1/admin/contacts/$id/'
|
||||||
| '/api/v1/admin/sessions/$id/'
|
| '/api/v1/admin/sessions/$id/'
|
||||||
| '/api/v1/admin/webhooks/$id/'
|
| '/api/v1/admin/webhooks/$id/'
|
||||||
|
| '/api/v1/admin/whatsapp-groups/$jid/'
|
||||||
| '/api/v1/contact-groups/$id/members/'
|
| '/api/v1/contact-groups/$id/members/'
|
||||||
| '/api/v1/sessions/$id/messages/'
|
| '/api/v1/sessions/$id/messages/'
|
||||||
| '/api/v1/admin/apps/$id/api-keys/$keyId'
|
| '/api/v1/admin/apps/$id/api-keys/$keyId'
|
||||||
|
|
@ -943,7 +997,9 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/media'
|
| '/api/v1/media'
|
||||||
| '/api/v1/sessions'
|
| '/api/v1/sessions'
|
||||||
| '/api/v1/webhooks'
|
| '/api/v1/webhooks'
|
||||||
|
| '/api/v1/whatsapp-groups'
|
||||||
| '/apps/$appId/contacts'
|
| '/apps/$appId/contacts'
|
||||||
|
| '/apps/$appId/groups'
|
||||||
| '/apps/$appId/sessions'
|
| '/apps/$appId/sessions'
|
||||||
| '/apps/$appId/webhooks'
|
| '/apps/$appId/webhooks'
|
||||||
| '/api/v1/admin/custom-field-schemas/$id'
|
| '/api/v1/admin/custom-field-schemas/$id'
|
||||||
|
|
@ -963,12 +1019,14 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/admin/custom-field-schemas'
|
| '/api/v1/admin/custom-field-schemas'
|
||||||
| '/api/v1/admin/sessions'
|
| '/api/v1/admin/sessions'
|
||||||
| '/api/v1/admin/webhooks'
|
| '/api/v1/admin/webhooks'
|
||||||
|
| '/api/v1/admin/whatsapp-groups'
|
||||||
| '/api/v1/contact-groups/$id'
|
| '/api/v1/contact-groups/$id'
|
||||||
| '/api/v1/contacts/$id'
|
| '/api/v1/contacts/$id'
|
||||||
| '/api/v1/contacts/import'
|
| '/api/v1/contacts/import'
|
||||||
| '/api/v1/contacts/sync-from-whatsapp'
|
| '/api/v1/contacts/sync-from-whatsapp'
|
||||||
| '/api/v1/sessions/$id'
|
| '/api/v1/sessions/$id'
|
||||||
| '/api/v1/webhooks/$id'
|
| '/api/v1/webhooks/$id'
|
||||||
|
| '/api/v1/whatsapp-groups/$jid'
|
||||||
| '/apps/$appId/contacts/groups'
|
| '/apps/$appId/contacts/groups'
|
||||||
| '/apps/$appId/contacts/settings'
|
| '/apps/$appId/contacts/settings'
|
||||||
| '/api/v1/admin/apps/$id/sync-contacts'
|
| '/api/v1/admin/apps/$id/sync-contacts'
|
||||||
|
|
@ -990,6 +1048,7 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/admin/contacts/$id'
|
| '/api/v1/admin/contacts/$id'
|
||||||
| '/api/v1/admin/sessions/$id'
|
| '/api/v1/admin/sessions/$id'
|
||||||
| '/api/v1/admin/webhooks/$id'
|
| '/api/v1/admin/webhooks/$id'
|
||||||
|
| '/api/v1/admin/whatsapp-groups/$jid'
|
||||||
| '/api/v1/contact-groups/$id/members'
|
| '/api/v1/contact-groups/$id/members'
|
||||||
| '/api/v1/sessions/$id/messages'
|
| '/api/v1/sessions/$id/messages'
|
||||||
| '/api/v1/admin/apps/$id/api-keys/$keyId'
|
| '/api/v1/admin/apps/$id/api-keys/$keyId'
|
||||||
|
|
@ -1030,7 +1089,9 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/media/'
|
| '/api/v1/media/'
|
||||||
| '/api/v1/sessions/'
|
| '/api/v1/sessions/'
|
||||||
| '/api/v1/webhooks/'
|
| '/api/v1/webhooks/'
|
||||||
|
| '/api/v1/whatsapp-groups/'
|
||||||
| '/apps/$appId/contacts/'
|
| '/apps/$appId/contacts/'
|
||||||
|
| '/apps/$appId/groups/'
|
||||||
| '/apps/$appId/sessions/'
|
| '/apps/$appId/sessions/'
|
||||||
| '/apps/$appId/webhooks/'
|
| '/apps/$appId/webhooks/'
|
||||||
| '/api/v1/admin/custom-field-schemas/$id'
|
| '/api/v1/admin/custom-field-schemas/$id'
|
||||||
|
|
@ -1050,12 +1111,14 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/admin/custom-field-schemas/'
|
| '/api/v1/admin/custom-field-schemas/'
|
||||||
| '/api/v1/admin/sessions/'
|
| '/api/v1/admin/sessions/'
|
||||||
| '/api/v1/admin/webhooks/'
|
| '/api/v1/admin/webhooks/'
|
||||||
|
| '/api/v1/admin/whatsapp-groups/'
|
||||||
| '/api/v1/contact-groups/$id/'
|
| '/api/v1/contact-groups/$id/'
|
||||||
| '/api/v1/contacts/$id/'
|
| '/api/v1/contacts/$id/'
|
||||||
| '/api/v1/contacts/import/'
|
| '/api/v1/contacts/import/'
|
||||||
| '/api/v1/contacts/sync-from-whatsapp/'
|
| '/api/v1/contacts/sync-from-whatsapp/'
|
||||||
| '/api/v1/sessions/$id/'
|
| '/api/v1/sessions/$id/'
|
||||||
| '/api/v1/webhooks/$id/'
|
| '/api/v1/webhooks/$id/'
|
||||||
|
| '/api/v1/whatsapp-groups/$jid/'
|
||||||
| '/apps/$appId/contacts/groups/'
|
| '/apps/$appId/contacts/groups/'
|
||||||
| '/apps/$appId/contacts/settings/'
|
| '/apps/$appId/contacts/settings/'
|
||||||
| '/api/v1/admin/apps/$id/sync-contacts'
|
| '/api/v1/admin/apps/$id/sync-contacts'
|
||||||
|
|
@ -1077,6 +1140,7 @@ export interface FileRouteTypes {
|
||||||
| '/api/v1/admin/contacts/$id/'
|
| '/api/v1/admin/contacts/$id/'
|
||||||
| '/api/v1/admin/sessions/$id/'
|
| '/api/v1/admin/sessions/$id/'
|
||||||
| '/api/v1/admin/webhooks/$id/'
|
| '/api/v1/admin/webhooks/$id/'
|
||||||
|
| '/api/v1/admin/whatsapp-groups/$jid/'
|
||||||
| '/api/v1/contact-groups/$id/members/'
|
| '/api/v1/contact-groups/$id/members/'
|
||||||
| '/api/v1/sessions/$id/messages/'
|
| '/api/v1/sessions/$id/messages/'
|
||||||
| '/api/v1/admin/apps/$id/api-keys/$keyId'
|
| '/api/v1/admin/apps/$id/api-keys/$keyId'
|
||||||
|
|
@ -1113,6 +1177,7 @@ export interface RootRouteChildren {
|
||||||
ApiV1MediaIndexRoute: typeof ApiV1MediaIndexRoute
|
ApiV1MediaIndexRoute: typeof ApiV1MediaIndexRoute
|
||||||
ApiV1SessionsIndexRoute: typeof ApiV1SessionsIndexRoute
|
ApiV1SessionsIndexRoute: typeof ApiV1SessionsIndexRoute
|
||||||
ApiV1WebhooksIndexRoute: typeof ApiV1WebhooksIndexRoute
|
ApiV1WebhooksIndexRoute: typeof ApiV1WebhooksIndexRoute
|
||||||
|
ApiV1WhatsappGroupsIndexRoute: typeof ApiV1WhatsappGroupsIndexRoute
|
||||||
ApiV1AdminCustomFieldSchemasIdRoute: typeof ApiV1AdminCustomFieldSchemasIdRoute
|
ApiV1AdminCustomFieldSchemasIdRoute: typeof ApiV1AdminCustomFieldSchemasIdRoute
|
||||||
ApiV1AdminMonitoringStatsRoute: typeof ApiV1AdminMonitoringStatsRoute
|
ApiV1AdminMonitoringStatsRoute: typeof ApiV1AdminMonitoringStatsRoute
|
||||||
ApiV1ContactsImportJobIdRoute: typeof ApiV1ContactsImportJobIdRoute
|
ApiV1ContactsImportJobIdRoute: typeof ApiV1ContactsImportJobIdRoute
|
||||||
|
|
@ -1129,12 +1194,14 @@ export interface RootRouteChildren {
|
||||||
ApiV1AdminCustomFieldSchemasIndexRoute: typeof ApiV1AdminCustomFieldSchemasIndexRoute
|
ApiV1AdminCustomFieldSchemasIndexRoute: typeof ApiV1AdminCustomFieldSchemasIndexRoute
|
||||||
ApiV1AdminSessionsIndexRoute: typeof ApiV1AdminSessionsIndexRoute
|
ApiV1AdminSessionsIndexRoute: typeof ApiV1AdminSessionsIndexRoute
|
||||||
ApiV1AdminWebhooksIndexRoute: typeof ApiV1AdminWebhooksIndexRoute
|
ApiV1AdminWebhooksIndexRoute: typeof ApiV1AdminWebhooksIndexRoute
|
||||||
|
ApiV1AdminWhatsappGroupsIndexRoute: typeof ApiV1AdminWhatsappGroupsIndexRoute
|
||||||
ApiV1ContactGroupsIdIndexRoute: typeof ApiV1ContactGroupsIdIndexRoute
|
ApiV1ContactGroupsIdIndexRoute: typeof ApiV1ContactGroupsIdIndexRoute
|
||||||
ApiV1ContactsIdIndexRoute: typeof ApiV1ContactsIdIndexRoute
|
ApiV1ContactsIdIndexRoute: typeof ApiV1ContactsIdIndexRoute
|
||||||
ApiV1ContactsImportIndexRoute: typeof ApiV1ContactsImportIndexRoute
|
ApiV1ContactsImportIndexRoute: typeof ApiV1ContactsImportIndexRoute
|
||||||
ApiV1ContactsSyncFromWhatsappIndexRoute: typeof ApiV1ContactsSyncFromWhatsappIndexRoute
|
ApiV1ContactsSyncFromWhatsappIndexRoute: typeof ApiV1ContactsSyncFromWhatsappIndexRoute
|
||||||
ApiV1SessionsIdIndexRoute: typeof ApiV1SessionsIdIndexRoute
|
ApiV1SessionsIdIndexRoute: typeof ApiV1SessionsIdIndexRoute
|
||||||
ApiV1WebhooksIdIndexRoute: typeof ApiV1WebhooksIdIndexRoute
|
ApiV1WebhooksIdIndexRoute: typeof ApiV1WebhooksIdIndexRoute
|
||||||
|
ApiV1WhatsappGroupsJidIndexRoute: typeof ApiV1WhatsappGroupsJidIndexRoute
|
||||||
ApiV1AdminAppsIdSyncContactsRoute: typeof ApiV1AdminAppsIdSyncContactsRoute
|
ApiV1AdminAppsIdSyncContactsRoute: typeof ApiV1AdminAppsIdSyncContactsRoute
|
||||||
ApiV1AdminContactsIdTagsRoute: typeof ApiV1AdminContactsIdTagsRoute
|
ApiV1AdminContactsIdTagsRoute: typeof ApiV1AdminContactsIdTagsRoute
|
||||||
ApiV1AdminSessionsIdDisconnectRoute: typeof ApiV1AdminSessionsIdDisconnectRoute
|
ApiV1AdminSessionsIdDisconnectRoute: typeof ApiV1AdminSessionsIdDisconnectRoute
|
||||||
|
|
@ -1154,6 +1221,7 @@ export interface RootRouteChildren {
|
||||||
ApiV1AdminContactsIdIndexRoute: typeof ApiV1AdminContactsIdIndexRoute
|
ApiV1AdminContactsIdIndexRoute: typeof ApiV1AdminContactsIdIndexRoute
|
||||||
ApiV1AdminSessionsIdIndexRoute: typeof ApiV1AdminSessionsIdIndexRoute
|
ApiV1AdminSessionsIdIndexRoute: typeof ApiV1AdminSessionsIdIndexRoute
|
||||||
ApiV1AdminWebhooksIdIndexRoute: typeof ApiV1AdminWebhooksIdIndexRoute
|
ApiV1AdminWebhooksIdIndexRoute: typeof ApiV1AdminWebhooksIdIndexRoute
|
||||||
|
ApiV1AdminWhatsappGroupsJidIndexRoute: typeof ApiV1AdminWhatsappGroupsJidIndexRoute
|
||||||
ApiV1ContactGroupsIdMembersIndexRoute: typeof ApiV1ContactGroupsIdMembersIndexRoute
|
ApiV1ContactGroupsIdMembersIndexRoute: typeof ApiV1ContactGroupsIdMembersIndexRoute
|
||||||
ApiV1SessionsIdMessagesIndexRoute: typeof ApiV1SessionsIdMessagesIndexRoute
|
ApiV1SessionsIdMessagesIndexRoute: typeof ApiV1SessionsIdMessagesIndexRoute
|
||||||
ApiV1AdminAppsIdApiKeysKeyIdRoute: typeof ApiV1AdminAppsIdApiKeysKeyIdRoute
|
ApiV1AdminAppsIdApiKeysKeyIdRoute: typeof ApiV1AdminAppsIdApiKeysKeyIdRoute
|
||||||
|
|
@ -1299,6 +1367,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof AppsAppIdSessionsIndexRouteImport
|
preLoaderRoute: typeof AppsAppIdSessionsIndexRouteImport
|
||||||
parentRoute: typeof AppsAppIdRoute
|
parentRoute: typeof AppsAppIdRoute
|
||||||
}
|
}
|
||||||
|
'/apps/$appId/groups/': {
|
||||||
|
id: '/apps/$appId/groups/'
|
||||||
|
path: '/groups'
|
||||||
|
fullPath: '/apps/$appId/groups/'
|
||||||
|
preLoaderRoute: typeof AppsAppIdGroupsIndexRouteImport
|
||||||
|
parentRoute: typeof AppsAppIdRoute
|
||||||
|
}
|
||||||
'/apps/$appId/contacts/': {
|
'/apps/$appId/contacts/': {
|
||||||
id: '/apps/$appId/contacts/'
|
id: '/apps/$appId/contacts/'
|
||||||
path: '/contacts'
|
path: '/contacts'
|
||||||
|
|
@ -1306,6 +1381,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof AppsAppIdContactsIndexRouteImport
|
preLoaderRoute: typeof AppsAppIdContactsIndexRouteImport
|
||||||
parentRoute: typeof AppsAppIdRoute
|
parentRoute: typeof AppsAppIdRoute
|
||||||
}
|
}
|
||||||
|
'/api/v1/whatsapp-groups/': {
|
||||||
|
id: '/api/v1/whatsapp-groups/'
|
||||||
|
path: '/api/v1/whatsapp-groups'
|
||||||
|
fullPath: '/api/v1/whatsapp-groups/'
|
||||||
|
preLoaderRoute: typeof ApiV1WhatsappGroupsIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/api/v1/webhooks/': {
|
'/api/v1/webhooks/': {
|
||||||
id: '/api/v1/webhooks/'
|
id: '/api/v1/webhooks/'
|
||||||
path: '/api/v1/webhooks'
|
path: '/api/v1/webhooks'
|
||||||
|
|
@ -1411,6 +1493,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof AppsAppIdContactsGroupsIndexRouteImport
|
preLoaderRoute: typeof AppsAppIdContactsGroupsIndexRouteImport
|
||||||
parentRoute: typeof AppsAppIdRoute
|
parentRoute: typeof AppsAppIdRoute
|
||||||
}
|
}
|
||||||
|
'/api/v1/whatsapp-groups/$jid/': {
|
||||||
|
id: '/api/v1/whatsapp-groups/$jid/'
|
||||||
|
path: '/api/v1/whatsapp-groups/$jid'
|
||||||
|
fullPath: '/api/v1/whatsapp-groups/$jid/'
|
||||||
|
preLoaderRoute: typeof ApiV1WhatsappGroupsJidIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/api/v1/webhooks/$id/': {
|
'/api/v1/webhooks/$id/': {
|
||||||
id: '/api/v1/webhooks/$id/'
|
id: '/api/v1/webhooks/$id/'
|
||||||
path: '/api/v1/webhooks/$id'
|
path: '/api/v1/webhooks/$id'
|
||||||
|
|
@ -1453,6 +1542,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof ApiV1ContactGroupsIdIndexRouteImport
|
preLoaderRoute: typeof ApiV1ContactGroupsIdIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/api/v1/admin/whatsapp-groups/': {
|
||||||
|
id: '/api/v1/admin/whatsapp-groups/'
|
||||||
|
path: '/api/v1/admin/whatsapp-groups'
|
||||||
|
fullPath: '/api/v1/admin/whatsapp-groups/'
|
||||||
|
preLoaderRoute: typeof ApiV1AdminWhatsappGroupsIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/api/v1/admin/webhooks/': {
|
'/api/v1/admin/webhooks/': {
|
||||||
id: '/api/v1/admin/webhooks/'
|
id: '/api/v1/admin/webhooks/'
|
||||||
path: '/api/v1/admin/webhooks'
|
path: '/api/v1/admin/webhooks'
|
||||||
|
|
@ -1586,6 +1682,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof ApiV1ContactGroupsIdMembersIndexRouteImport
|
preLoaderRoute: typeof ApiV1ContactGroupsIdMembersIndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/api/v1/admin/whatsapp-groups/$jid/': {
|
||||||
|
id: '/api/v1/admin/whatsapp-groups/$jid/'
|
||||||
|
path: '/api/v1/admin/whatsapp-groups/$jid'
|
||||||
|
fullPath: '/api/v1/admin/whatsapp-groups/$jid/'
|
||||||
|
preLoaderRoute: typeof ApiV1AdminWhatsappGroupsJidIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
'/api/v1/admin/webhooks/$id/': {
|
'/api/v1/admin/webhooks/$id/': {
|
||||||
id: '/api/v1/admin/webhooks/$id/'
|
id: '/api/v1/admin/webhooks/$id/'
|
||||||
path: '/api/v1/admin/webhooks/$id'
|
path: '/api/v1/admin/webhooks/$id'
|
||||||
|
|
@ -1771,6 +1874,7 @@ interface AppsAppIdRouteChildren {
|
||||||
AppsAppIdSessionsSessionIdRoute: typeof AppsAppIdSessionsSessionIdRoute
|
AppsAppIdSessionsSessionIdRoute: typeof AppsAppIdSessionsSessionIdRoute
|
||||||
AppsAppIdSessionsNewRoute: typeof AppsAppIdSessionsNewRoute
|
AppsAppIdSessionsNewRoute: typeof AppsAppIdSessionsNewRoute
|
||||||
AppsAppIdContactsIndexRoute: typeof AppsAppIdContactsIndexRoute
|
AppsAppIdContactsIndexRoute: typeof AppsAppIdContactsIndexRoute
|
||||||
|
AppsAppIdGroupsIndexRoute: typeof AppsAppIdGroupsIndexRoute
|
||||||
AppsAppIdSessionsIndexRoute: typeof AppsAppIdSessionsIndexRoute
|
AppsAppIdSessionsIndexRoute: typeof AppsAppIdSessionsIndexRoute
|
||||||
AppsAppIdWebhooksIndexRoute: typeof AppsAppIdWebhooksIndexRoute
|
AppsAppIdWebhooksIndexRoute: typeof AppsAppIdWebhooksIndexRoute
|
||||||
AppsAppIdWebhooksWebhookIdDeliveriesRoute: typeof AppsAppIdWebhooksWebhookIdDeliveriesRoute
|
AppsAppIdWebhooksWebhookIdDeliveriesRoute: typeof AppsAppIdWebhooksWebhookIdDeliveriesRoute
|
||||||
|
|
@ -1785,6 +1889,7 @@ const AppsAppIdRouteChildren: AppsAppIdRouteChildren = {
|
||||||
AppsAppIdSessionsSessionIdRoute: AppsAppIdSessionsSessionIdRoute,
|
AppsAppIdSessionsSessionIdRoute: AppsAppIdSessionsSessionIdRoute,
|
||||||
AppsAppIdSessionsNewRoute: AppsAppIdSessionsNewRoute,
|
AppsAppIdSessionsNewRoute: AppsAppIdSessionsNewRoute,
|
||||||
AppsAppIdContactsIndexRoute: AppsAppIdContactsIndexRoute,
|
AppsAppIdContactsIndexRoute: AppsAppIdContactsIndexRoute,
|
||||||
|
AppsAppIdGroupsIndexRoute: AppsAppIdGroupsIndexRoute,
|
||||||
AppsAppIdSessionsIndexRoute: AppsAppIdSessionsIndexRoute,
|
AppsAppIdSessionsIndexRoute: AppsAppIdSessionsIndexRoute,
|
||||||
AppsAppIdWebhooksIndexRoute: AppsAppIdWebhooksIndexRoute,
|
AppsAppIdWebhooksIndexRoute: AppsAppIdWebhooksIndexRoute,
|
||||||
AppsAppIdWebhooksWebhookIdDeliveriesRoute:
|
AppsAppIdWebhooksWebhookIdDeliveriesRoute:
|
||||||
|
|
@ -1823,6 +1928,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
ApiV1MediaIndexRoute: ApiV1MediaIndexRoute,
|
ApiV1MediaIndexRoute: ApiV1MediaIndexRoute,
|
||||||
ApiV1SessionsIndexRoute: ApiV1SessionsIndexRoute,
|
ApiV1SessionsIndexRoute: ApiV1SessionsIndexRoute,
|
||||||
ApiV1WebhooksIndexRoute: ApiV1WebhooksIndexRoute,
|
ApiV1WebhooksIndexRoute: ApiV1WebhooksIndexRoute,
|
||||||
|
ApiV1WhatsappGroupsIndexRoute: ApiV1WhatsappGroupsIndexRoute,
|
||||||
ApiV1AdminCustomFieldSchemasIdRoute: ApiV1AdminCustomFieldSchemasIdRoute,
|
ApiV1AdminCustomFieldSchemasIdRoute: ApiV1AdminCustomFieldSchemasIdRoute,
|
||||||
ApiV1AdminMonitoringStatsRoute: ApiV1AdminMonitoringStatsRoute,
|
ApiV1AdminMonitoringStatsRoute: ApiV1AdminMonitoringStatsRoute,
|
||||||
ApiV1ContactsImportJobIdRoute: ApiV1ContactsImportJobIdRoute,
|
ApiV1ContactsImportJobIdRoute: ApiV1ContactsImportJobIdRoute,
|
||||||
|
|
@ -1840,6 +1946,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
ApiV1AdminCustomFieldSchemasIndexRoute,
|
ApiV1AdminCustomFieldSchemasIndexRoute,
|
||||||
ApiV1AdminSessionsIndexRoute: ApiV1AdminSessionsIndexRoute,
|
ApiV1AdminSessionsIndexRoute: ApiV1AdminSessionsIndexRoute,
|
||||||
ApiV1AdminWebhooksIndexRoute: ApiV1AdminWebhooksIndexRoute,
|
ApiV1AdminWebhooksIndexRoute: ApiV1AdminWebhooksIndexRoute,
|
||||||
|
ApiV1AdminWhatsappGroupsIndexRoute: ApiV1AdminWhatsappGroupsIndexRoute,
|
||||||
ApiV1ContactGroupsIdIndexRoute: ApiV1ContactGroupsIdIndexRoute,
|
ApiV1ContactGroupsIdIndexRoute: ApiV1ContactGroupsIdIndexRoute,
|
||||||
ApiV1ContactsIdIndexRoute: ApiV1ContactsIdIndexRoute,
|
ApiV1ContactsIdIndexRoute: ApiV1ContactsIdIndexRoute,
|
||||||
ApiV1ContactsImportIndexRoute: ApiV1ContactsImportIndexRoute,
|
ApiV1ContactsImportIndexRoute: ApiV1ContactsImportIndexRoute,
|
||||||
|
|
@ -1847,6 +1954,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
ApiV1ContactsSyncFromWhatsappIndexRoute,
|
ApiV1ContactsSyncFromWhatsappIndexRoute,
|
||||||
ApiV1SessionsIdIndexRoute: ApiV1SessionsIdIndexRoute,
|
ApiV1SessionsIdIndexRoute: ApiV1SessionsIdIndexRoute,
|
||||||
ApiV1WebhooksIdIndexRoute: ApiV1WebhooksIdIndexRoute,
|
ApiV1WebhooksIdIndexRoute: ApiV1WebhooksIdIndexRoute,
|
||||||
|
ApiV1WhatsappGroupsJidIndexRoute: ApiV1WhatsappGroupsJidIndexRoute,
|
||||||
ApiV1AdminAppsIdSyncContactsRoute: ApiV1AdminAppsIdSyncContactsRoute,
|
ApiV1AdminAppsIdSyncContactsRoute: ApiV1AdminAppsIdSyncContactsRoute,
|
||||||
ApiV1AdminContactsIdTagsRoute: ApiV1AdminContactsIdTagsRoute,
|
ApiV1AdminContactsIdTagsRoute: ApiV1AdminContactsIdTagsRoute,
|
||||||
ApiV1AdminSessionsIdDisconnectRoute: ApiV1AdminSessionsIdDisconnectRoute,
|
ApiV1AdminSessionsIdDisconnectRoute: ApiV1AdminSessionsIdDisconnectRoute,
|
||||||
|
|
@ -1867,6 +1975,7 @@ const rootRouteChildren: RootRouteChildren = {
|
||||||
ApiV1AdminContactsIdIndexRoute: ApiV1AdminContactsIdIndexRoute,
|
ApiV1AdminContactsIdIndexRoute: ApiV1AdminContactsIdIndexRoute,
|
||||||
ApiV1AdminSessionsIdIndexRoute: ApiV1AdminSessionsIdIndexRoute,
|
ApiV1AdminSessionsIdIndexRoute: ApiV1AdminSessionsIdIndexRoute,
|
||||||
ApiV1AdminWebhooksIdIndexRoute: ApiV1AdminWebhooksIdIndexRoute,
|
ApiV1AdminWebhooksIdIndexRoute: ApiV1AdminWebhooksIdIndexRoute,
|
||||||
|
ApiV1AdminWhatsappGroupsJidIndexRoute: ApiV1AdminWhatsappGroupsJidIndexRoute,
|
||||||
ApiV1ContactGroupsIdMembersIndexRoute: ApiV1ContactGroupsIdMembersIndexRoute,
|
ApiV1ContactGroupsIdMembersIndexRoute: ApiV1ContactGroupsIdMembersIndexRoute,
|
||||||
ApiV1SessionsIdMessagesIndexRoute: ApiV1SessionsIdMessagesIndexRoute,
|
ApiV1SessionsIdMessagesIndexRoute: ApiV1SessionsIdMessagesIndexRoute,
|
||||||
ApiV1AdminAppsIdApiKeysKeyIdRoute: ApiV1AdminAppsIdApiKeysKeyIdRoute,
|
ApiV1AdminAppsIdApiKeysKeyIdRoute: ApiV1AdminAppsIdApiKeysKeyIdRoute,
|
||||||
|
|
|
||||||
28
src/routes/api/v1/admin/whatsapp-groups/$jid/index.ts
Normal file
28
src/routes/api/v1/admin/whatsapp-groups/$jid/index.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { json, jsonError, readSession } from "#/lib/session-api";
|
||||||
|
import { getWhatsappGroup } from "#/lib/actions/whatsapp-groups";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/api/v1/admin/whatsapp-groups/$jid/")({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async ({ request, params }) => {
|
||||||
|
const session = await readSession(request);
|
||||||
|
if (!session?.userId) return jsonError("Unauthorized", 401);
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const appId = url.searchParams.get("app_id");
|
||||||
|
const sessionId = url.searchParams.get("session_id");
|
||||||
|
if (!appId) return jsonError("app_id required", 400);
|
||||||
|
if (!sessionId) return jsonError("session_id required", 400);
|
||||||
|
|
||||||
|
const group = await getWhatsappGroup(
|
||||||
|
decodeURIComponent(params.jid),
|
||||||
|
sessionId,
|
||||||
|
appId,
|
||||||
|
);
|
||||||
|
if (!group) return jsonError("Group not found", 404);
|
||||||
|
return json(group);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
33
src/routes/api/v1/admin/whatsapp-groups/index.ts
Normal file
33
src/routes/api/v1/admin/whatsapp-groups/index.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { json, jsonError, readSession } from "#/lib/session-api";
|
||||||
|
import { listWhatsappGroups } from "#/lib/actions/whatsapp-groups";
|
||||||
|
import { db } from "#/db/index";
|
||||||
|
import { apps } from "#/db/schema";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/api/v1/admin/whatsapp-groups/")({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async ({ request }) => {
|
||||||
|
const session = await readSession(request);
|
||||||
|
if (!session?.userId) return jsonError("Unauthorized", 401);
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const appId = url.searchParams.get("app_id");
|
||||||
|
const sessionId = url.searchParams.get("session_id") ?? undefined;
|
||||||
|
|
||||||
|
if (!appId) return jsonError("app_id required", 400);
|
||||||
|
|
||||||
|
const appRows = await db
|
||||||
|
.select({ id: apps.id })
|
||||||
|
.from(apps)
|
||||||
|
.where(eq(apps.id, appId))
|
||||||
|
.limit(1);
|
||||||
|
if (!appRows.length) return jsonError("App not found", 404);
|
||||||
|
|
||||||
|
const groups = await listWhatsappGroups(appId, sessionId);
|
||||||
|
return json(groups);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
27
src/routes/api/v1/whatsapp-groups/$jid/index.ts
Normal file
27
src/routes/api/v1/whatsapp-groups/$jid/index.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { authenticateApiKey } from "#/lib/api-key-auth";
|
||||||
|
import { json, jsonError } from "#/lib/session-api";
|
||||||
|
import { getWhatsappGroup } from "#/lib/actions/whatsapp-groups";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/api/v1/whatsapp-groups/$jid/")({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async ({ request, params }) => {
|
||||||
|
const auth = await authenticateApiKey(request);
|
||||||
|
if (auth instanceof Response) return auth;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const sessionId = url.searchParams.get("session_id");
|
||||||
|
if (!sessionId) return jsonError("session_id required", 400);
|
||||||
|
|
||||||
|
const group = await getWhatsappGroup(
|
||||||
|
decodeURIComponent(params.jid),
|
||||||
|
sessionId,
|
||||||
|
auth.appId,
|
||||||
|
);
|
||||||
|
if (!group) return jsonError("Group not found", 404);
|
||||||
|
return json(group);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
23
src/routes/api/v1/whatsapp-groups/index.ts
Normal file
23
src/routes/api/v1/whatsapp-groups/index.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { authenticateApiKey } from "#/lib/api-key-auth";
|
||||||
|
import { json, jsonError } from "#/lib/session-api";
|
||||||
|
import { listWhatsappGroups } from "#/lib/actions/whatsapp-groups";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/api/v1/whatsapp-groups/")({
|
||||||
|
server: {
|
||||||
|
handlers: {
|
||||||
|
GET: async ({ request }) => {
|
||||||
|
const auth = await authenticateApiKey(request);
|
||||||
|
if (auth instanceof Response) return auth;
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const sessionId = url.searchParams.get("session_id") ?? undefined;
|
||||||
|
|
||||||
|
if (!sessionId) return jsonError("session_id required", 400);
|
||||||
|
|
||||||
|
const groups = await listWhatsappGroups(auth.appId, sessionId);
|
||||||
|
return json(groups);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
206
src/routes/apps/$appId/groups/index.tsx
Normal file
206
src/routes/apps/$appId/groups/index.tsx
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Users2, ChevronDown } from "lucide-react";
|
||||||
|
import { whatsappGroupsQuery, appSessionsQuery } from "#/lib/query";
|
||||||
|
import { cn } from "#/lib/utils";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/apps/$appId/groups/")({
|
||||||
|
component: GroupsPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface WhatsappGroup {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
sessionId: string;
|
||||||
|
jid: string;
|
||||||
|
subject: string | null;
|
||||||
|
description: string | null;
|
||||||
|
participantCount: number | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionRow {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
appName: string;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeAgo(iso: string): string {
|
||||||
|
const diff = Date.now() - new Date(iso).getTime();
|
||||||
|
const m = Math.floor(diff / 60000);
|
||||||
|
const h = Math.floor(m / 60);
|
||||||
|
const d = Math.floor(h / 24);
|
||||||
|
if (m < 1) return "just now";
|
||||||
|
if (m < 60) return `${m}m ago`;
|
||||||
|
if (h < 24) return `${h}h ago`;
|
||||||
|
if (d < 30) return `${d}d ago`;
|
||||||
|
return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitJid(jid: string) {
|
||||||
|
const at = jid.indexOf("@");
|
||||||
|
if (at === -1) return { num: jid, domain: "" };
|
||||||
|
return { num: jid.slice(0, at), domain: "@" + jid.slice(at + 1) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function SkeletonRow() {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border border-gray-100 bg-white p-4 animate-pulse">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="h-4 w-48 rounded bg-gray-100" />
|
||||||
|
<div className="h-3 w-64 rounded bg-gray-100" />
|
||||||
|
</div>
|
||||||
|
<div className="h-5 w-12 rounded-full bg-gray-100" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GroupsPage() {
|
||||||
|
const { appId } = Route.useParams();
|
||||||
|
const [sessionFilter, setSessionFilter] = useState<string>("");
|
||||||
|
|
||||||
|
const { data: sessions = [] } = useQuery<SessionRow[]>(appSessionsQuery(appId));
|
||||||
|
const { data: groups = [], isPending } = useQuery<WhatsappGroup[]>(
|
||||||
|
whatsappGroupsQuery(appId, sessionFilter || undefined),
|
||||||
|
);
|
||||||
|
|
||||||
|
const sessionMap = Object.fromEntries(sessions.map((s) => [s.id, s]));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-full bg-gray-50/50">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white border-b border-gray-200 px-8 h-14 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Users2 size={15} className="text-gray-400" strokeWidth={1.75} />
|
||||||
|
<h1 className="text-[14px] font-semibold text-gray-900">Groups</h1>
|
||||||
|
{!(isPending as boolean) && groups.length > 0 && (
|
||||||
|
<span className="text-[11px] font-medium text-gray-400 bg-gray-100 rounded-full px-2 py-0.5">
|
||||||
|
{groups.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Session filter */}
|
||||||
|
{sessions.length > 1 && (
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
value={sessionFilter}
|
||||||
|
onChange={(e) => setSessionFilter(e.target.value)}
|
||||||
|
className="appearance-none text-[12px] text-gray-600 bg-white border border-gray-200 rounded-lg pl-3 pr-7 py-1.5 focus:outline-none focus:ring-1 focus:ring-zinc-300 cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="">All sessions</option>
|
||||||
|
{sessions.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.id.slice(0, 8)}… ({s.status})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<ChevronDown size={11} className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-8 py-8">
|
||||||
|
{/* Loading */}
|
||||||
|
{(isPending as boolean) && (
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<SkeletonRow />
|
||||||
|
<SkeletonRow />
|
||||||
|
<SkeletonRow />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty state */}
|
||||||
|
{!(isPending as boolean) && groups.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-24 text-center">
|
||||||
|
<div className="mb-4 flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 text-gray-300">
|
||||||
|
<Users2 size={28} strokeWidth={1.25} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-[15px] font-semibold text-gray-800 mb-1">No groups tracked yet</h2>
|
||||||
|
<p className="text-[13px] text-gray-400 max-w-xs leading-relaxed">
|
||||||
|
Groups appear here automatically when messages are received from WhatsApp group chats.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Group cards */}
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{groups.map((g) => {
|
||||||
|
const { num, domain } = splitJid(g.jid);
|
||||||
|
const session = sessionMap[g.sessionId];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={g.id}
|
||||||
|
className="rounded-xl border border-gray-100 bg-white overflow-hidden hover:shadow-sm transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="w-0.5 shrink-0 bg-violet-300" />
|
||||||
|
<div className="flex-1 px-4 py-3.5">
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* Subject + JID */}
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
{g.subject ? (
|
||||||
|
<span className="text-[13px] font-semibold text-gray-900 truncate">
|
||||||
|
{g.subject}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-[13px] text-gray-400 italic">Unnamed group</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="font-mono text-[11px] text-gray-400">
|
||||||
|
{num}
|
||||||
|
<span className="text-gray-300">{domain}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{g.description && (
|
||||||
|
<p className="text-[12px] text-gray-500 mb-2 line-clamp-2 leading-relaxed">
|
||||||
|
{g.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Meta row */}
|
||||||
|
<div className="flex items-center gap-3 text-[11px] text-gray-400">
|
||||||
|
{g.participantCount !== null && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Users2 size={10} />
|
||||||
|
{g.participantCount} participant{g.participantCount !== 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{session && (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-mono border",
|
||||||
|
session.status === "CONNECTED"
|
||||||
|
? "bg-emerald-50 border-emerald-100 text-emerald-600"
|
||||||
|
: "bg-gray-50 border-gray-100 text-gray-500",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{g.sessionId.slice(0, 8)}…
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span title={new Date(g.updatedAt).toLocaleString()}>
|
||||||
|
updated {timeAgo(g.updatedAt)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -117,7 +117,7 @@ function DeliveriesPage() {
|
||||||
<ScrollText size={14} className="text-gray-400" strokeWidth={1.75} />
|
<ScrollText size={14} className="text-gray-400" strokeWidth={1.75} />
|
||||||
<h1 className="text-[14px] font-semibold text-gray-900">Delivery Log</h1>
|
<h1 className="text-[14px] font-semibold text-gray-900">Delivery Log</h1>
|
||||||
</div>
|
</div>
|
||||||
{!isPending && deliveries.length > 0 && (
|
{!(isPending as boolean) && deliveries.length > 0 && (
|
||||||
<span className="text-[11px] font-medium text-gray-400 bg-gray-100 rounded-full px-2 py-0.5">
|
<span className="text-[11px] font-medium text-gray-400 bg-gray-100 rounded-full px-2 py-0.5">
|
||||||
{deliveries.length}
|
{deliveries.length}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,11 @@ const ALL_EVENTS = [
|
||||||
"presence.update",
|
"presence.update",
|
||||||
"contact.created",
|
"contact.created",
|
||||||
"contact.updated",
|
"contact.updated",
|
||||||
|
"group.joined",
|
||||||
|
"group.left",
|
||||||
|
"group.updated",
|
||||||
|
"group.participants_added",
|
||||||
|
"group.participants_removed",
|
||||||
];
|
];
|
||||||
|
|
||||||
function formatDate(iso: string) {
|
function formatDate(iso: string) {
|
||||||
|
|
@ -128,7 +133,7 @@ function WebhooksPage() {
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
const [testingId, setTestingId] = useState<string | null>(null);
|
const [testingId, setTestingId] = useState<string | null>(null);
|
||||||
const [testResults, setTestResults] = useState<Record<string, TestResult>>({});
|
const [testResults, setTestResults] = useState<Partial<Record<string, TestResult>>>({});
|
||||||
|
|
||||||
function toggleEvent(ev: string) {
|
function toggleEvent(ev: string) {
|
||||||
setNewEvents((prev) =>
|
setNewEvents((prev) =>
|
||||||
|
|
|
||||||
344
tests/critical-systems-unit.test.ts
Normal file
344
tests/critical-systems-unit.test.ts
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
/**
|
||||||
|
* Critical system unit tests — pure logic, no DB/network/Redis required.
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - API key generation: format, prefix, env prefix, uniqueness
|
||||||
|
* - Webhook URL validation: invalid URL, bad protocol, SSRF private IP block,
|
||||||
|
* production HTTPS enforcement, private allowed in non-prod
|
||||||
|
* - Key rotation guard: concurrent rotation prevention conditions
|
||||||
|
* - API key revocation idempotency: 409 on double-revoke
|
||||||
|
* - Broadcast recipient cap (1000)
|
||||||
|
* - File upload size limit (10 MB)
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
|
||||||
|
// ─── API key generation ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function generateApiKey(env: "live" | "test" = "live"): { key: string; prefix: string } {
|
||||||
|
const rand = randomBytes(24).toString("base64url");
|
||||||
|
const key = `wak_${env}_${rand}`;
|
||||||
|
const prefix = key.slice(-4);
|
||||||
|
return { key, prefix };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("API key generation", () => {
|
||||||
|
it("live key starts with wak_live_", () => {
|
||||||
|
const { key } = generateApiKey("live");
|
||||||
|
expect(key).toMatch(/^wak_live_/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("test key starts with wak_test_", () => {
|
||||||
|
const { key } = generateApiKey("test");
|
||||||
|
expect(key).toMatch(/^wak_test_/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefix is last 4 characters of key", () => {
|
||||||
|
const { key, prefix } = generateApiKey("live");
|
||||||
|
expect(prefix).toBe(key.slice(-4));
|
||||||
|
expect(prefix).toHaveLength(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("random portion is base64url safe (no +/=)", () => {
|
||||||
|
for (let i = 0; i < 50; i++) {
|
||||||
|
const { key } = generateApiKey("live");
|
||||||
|
const rand = key.replace(/^wak_live_/, "");
|
||||||
|
expect(rand).toMatch(/^[A-Za-z0-9_-]+$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("each call produces unique key", () => {
|
||||||
|
const keys = new Set(Array.from({ length: 200 }, () => generateApiKey("live").key));
|
||||||
|
expect(keys.size).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("default env is live", () => {
|
||||||
|
const { key } = generateApiKey();
|
||||||
|
expect(key).toMatch(/^wak_live_/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("key total length is consistent", () => {
|
||||||
|
// wak_live_ (9) + base64url(24 bytes) = 9 + 32 = 41
|
||||||
|
const { key } = generateApiKey("live");
|
||||||
|
expect(key.length).toBeGreaterThan(20);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Webhook URL validation ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const BLOCKED_HOSTNAMES =
|
||||||
|
/^(localhost|127\.\d+\.\d+\.\d+|0\.0\.0\.0|::1|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+)$/i;
|
||||||
|
|
||||||
|
function validateWebhookUrl(
|
||||||
|
raw: string,
|
||||||
|
env: "production" | "development" | "test" = "test",
|
||||||
|
): { ok: true } | { ok: false; error: string } {
|
||||||
|
let parsed: URL;
|
||||||
|
try {
|
||||||
|
parsed = new URL(raw);
|
||||||
|
} catch {
|
||||||
|
return { ok: false, error: "Invalid URL" };
|
||||||
|
}
|
||||||
|
if (!["http:", "https:"].includes(parsed.protocol)) {
|
||||||
|
return { ok: false, error: "URL must use http or https" };
|
||||||
|
}
|
||||||
|
if (env === "production" && parsed.protocol !== "https:") {
|
||||||
|
return { ok: false, error: "URL must use https in production" };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateHostname(hostname: string): boolean {
|
||||||
|
return BLOCKED_HOSTNAMES.test(hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("webhook URL validation — basic", () => {
|
||||||
|
it("valid https URL passes", () => {
|
||||||
|
expect(validateWebhookUrl("https://example.com/webhook")).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("valid http URL passes in non-prod", () => {
|
||||||
|
expect(validateWebhookUrl("http://example.com/webhook", "development")).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invalid URL rejected", () => {
|
||||||
|
const r = validateWebhookUrl("not-a-url");
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect((r as { ok: false; error: string }).error).toMatch(/invalid url/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ftp:// protocol rejected", () => {
|
||||||
|
const r = validateWebhookUrl("ftp://example.com/hook");
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ws:// protocol rejected", () => {
|
||||||
|
const r = validateWebhookUrl("ws://example.com/hook");
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty string rejected", () => {
|
||||||
|
expect(validateWebhookUrl("").ok).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("webhook URL validation — production HTTPS enforcement", () => {
|
||||||
|
it("http rejected in production", () => {
|
||||||
|
const r = validateWebhookUrl("http://example.com/webhook", "production");
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect((r as { ok: false; error: string }).error).toMatch(/https/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("https accepted in production", () => {
|
||||||
|
expect(validateWebhookUrl("https://example.com/webhook", "production")).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("http accepted in development", () => {
|
||||||
|
expect(validateWebhookUrl("http://example.com/webhook", "development")).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SSRF — private hostname detection", () => {
|
||||||
|
const blocked = [
|
||||||
|
"localhost",
|
||||||
|
"LOCALHOST",
|
||||||
|
"127.0.0.1",
|
||||||
|
"127.1.2.3",
|
||||||
|
"0.0.0.0",
|
||||||
|
"::1",
|
||||||
|
"10.0.0.1",
|
||||||
|
"10.255.255.255",
|
||||||
|
"172.16.0.1",
|
||||||
|
"172.20.5.5",
|
||||||
|
"172.31.255.255",
|
||||||
|
"192.168.0.1",
|
||||||
|
"192.168.1.100",
|
||||||
|
"169.254.0.1",
|
||||||
|
"169.254.169.254", // AWS metadata
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowed = [
|
||||||
|
"example.com",
|
||||||
|
"api.example.com",
|
||||||
|
"172.15.0.1", // just outside 172.16-31 range
|
||||||
|
"172.32.0.1", // just outside 172.16-31 range
|
||||||
|
"192.169.0.1", // not 192.168
|
||||||
|
"11.0.0.1", // not 10.x
|
||||||
|
"8.8.8.8",
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const host of blocked) {
|
||||||
|
it(`blocks ${host}`, () => {
|
||||||
|
expect(isPrivateHostname(host)).toBe(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const host of allowed) {
|
||||||
|
it(`allows ${host}`, () => {
|
||||||
|
expect(isPrivateHostname(host)).toBe(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Key rotation guard ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type RotationStatus = "pending" | "running" | "done" | "failed" | null;
|
||||||
|
|
||||||
|
function canStartRotation(currentStatus: RotationStatus): boolean {
|
||||||
|
return currentStatus !== "pending" && currentStatus !== "running";
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("key rotation concurrency guard", () => {
|
||||||
|
it("null status → can start", () => {
|
||||||
|
expect(canStartRotation(null)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("done status → can start", () => {
|
||||||
|
expect(canStartRotation("done")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("failed status → can start (retry allowed)", () => {
|
||||||
|
expect(canStartRotation("failed")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pending status → cannot start (already queued)", () => {
|
||||||
|
expect(canStartRotation("pending")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("running status → cannot start (in progress)", () => {
|
||||||
|
expect(canStartRotation("running")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── API key revocation idempotency ──────────────────────────────────────────
|
||||||
|
|
||||||
|
interface FakeApiKey {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
revokedAt: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeApiKey(
|
||||||
|
key: FakeApiKey | null,
|
||||||
|
requestedAppId: string,
|
||||||
|
): { ok: true } | { ok: false; error: string; status: number } {
|
||||||
|
if (!key || key.appId !== requestedAppId) {
|
||||||
|
return { ok: false, error: "Key not found", status: 404 };
|
||||||
|
}
|
||||||
|
if (key.revokedAt) {
|
||||||
|
return { ok: false, error: "Key already revoked", status: 409 };
|
||||||
|
}
|
||||||
|
key.revokedAt = new Date();
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("API key revocation idempotency", () => {
|
||||||
|
function makeKey(appId = "app1"): FakeApiKey {
|
||||||
|
return { id: "key1", appId, revokedAt: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("first revocation succeeds", () => {
|
||||||
|
const key = makeKey();
|
||||||
|
expect(revokeApiKey(key, "app1")).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("second revocation → 409", () => {
|
||||||
|
const key = makeKey();
|
||||||
|
revokeApiKey(key, "app1");
|
||||||
|
const r = revokeApiKey(key, "app1");
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect((r as { ok: false; error: string; status: number }).status).toBe(409);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("key not found → 404", () => {
|
||||||
|
const r = revokeApiKey(null, "app1");
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect((r as { ok: false; error: string; status: number }).status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wrong appId → 404 (cross-app isolation)", () => {
|
||||||
|
const key = makeKey("app1");
|
||||||
|
const r = revokeApiKey(key, "app2");
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect((r as { ok: false; error: string; status: number }).status).toBe(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("revocation sets revokedAt timestamp", () => {
|
||||||
|
const key = makeKey();
|
||||||
|
const before = Date.now();
|
||||||
|
revokeApiKey(key, "app1");
|
||||||
|
expect(key.revokedAt).toBeInstanceOf(Date);
|
||||||
|
expect(key.revokedAt!.getTime()).toBeGreaterThanOrEqual(before);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Broadcast recipient cap ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const BROADCAST_MAX = 1000;
|
||||||
|
|
||||||
|
function validateBroadcastRecipients(
|
||||||
|
recipients: string[],
|
||||||
|
): { ok: true } | { ok: false; error: string } {
|
||||||
|
if (recipients.length === 0) return { ok: false, error: "No recipients" };
|
||||||
|
if (recipients.length > BROADCAST_MAX) {
|
||||||
|
return { ok: false, error: `Recipient count ${recipients.length} exceeds maximum of ${BROADCAST_MAX}` };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("broadcast recipient cap", () => {
|
||||||
|
it("1 recipient → ok", () => {
|
||||||
|
expect(validateBroadcastRecipients(["a"])).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1000 recipients → ok (at limit)", () => {
|
||||||
|
const r = Array.from({ length: 1000 }, (_, i) => `${i}@s.whatsapp.net`);
|
||||||
|
expect(validateBroadcastRecipients(r)).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("1001 recipients → rejected", () => {
|
||||||
|
const r = Array.from({ length: 1001 }, (_, i) => `${i}@s.whatsapp.net`);
|
||||||
|
const result = validateBroadcastRecipients(r);
|
||||||
|
expect(result.ok).toBe(false);
|
||||||
|
expect((result as { ok: false; error: string }).error).toMatch(/1001/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty list → rejected", () => {
|
||||||
|
expect(validateBroadcastRecipients([])).toEqual({ ok: false, error: "No recipients" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── File upload size limit ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||||
|
|
||||||
|
function validateUploadSize(bytes: number): { ok: true } | { ok: false; error: string } {
|
||||||
|
if (bytes > MAX_UPLOAD_BYTES) {
|
||||||
|
return { ok: false, error: `File too large: ${bytes} bytes (max ${MAX_UPLOAD_BYTES})` };
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("contact import file size limit (10 MB)", () => {
|
||||||
|
it("exactly 10 MB → ok", () => {
|
||||||
|
expect(validateUploadSize(MAX_UPLOAD_BYTES)).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("10 MB + 1 byte → rejected", () => {
|
||||||
|
expect(validateUploadSize(MAX_UPLOAD_BYTES + 1).ok).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("0 bytes → ok (let content validation handle empty)", () => {
|
||||||
|
expect(validateUploadSize(0)).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("5 MB → ok", () => {
|
||||||
|
expect(validateUploadSize(5 * 1024 * 1024)).toEqual({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("error message includes actual and max size", () => {
|
||||||
|
const r = validateUploadSize(MAX_UPLOAD_BYTES + 1) as { ok: false; error: string };
|
||||||
|
expect(r.error).toMatch(/10485761/);
|
||||||
|
expect(r.error).toMatch(/10485760/);
|
||||||
|
});
|
||||||
|
});
|
||||||
524
tests/whatsapp-groups-unit.test.ts
Normal file
524
tests/whatsapp-groups-unit.test.ts
Normal file
|
|
@ -0,0 +1,524 @@
|
||||||
|
/**
|
||||||
|
* WhatsApp groups unit tests — pure logic, no DB/network required.
|
||||||
|
*
|
||||||
|
* Coverage:
|
||||||
|
* - JID group detection (@g.us suffix)
|
||||||
|
* - upsert merge logic (partial updates, undefined fields not clobbered)
|
||||||
|
* - adjustParticipantCount: add/remove deltas, floor at 0
|
||||||
|
* - listWhatsappGroups filter: by appId, by sessionId, unfiltered
|
||||||
|
* - Group subject/description normalization
|
||||||
|
* - Webhook event routing: ownAdded/ownRemoved/other add/remove
|
||||||
|
* - Webhook payload shapes for all 5 group event types
|
||||||
|
* - Own JID matching: exact, LID prefix (:0 suffix stripped)
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
// ─── JID group detection ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function isGroupJid(jid: string): boolean {
|
||||||
|
return jid.endsWith("@g.us");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("JID group detection", () => {
|
||||||
|
it("@g.us JID is a group", () => {
|
||||||
|
expect(isGroupJid("120363429229698693@g.us")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("@s.whatsapp.net JID is not a group", () => {
|
||||||
|
expect(isGroupJid("6281234567890@s.whatsapp.net")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty string is not a group", () => {
|
||||||
|
expect(isGroupJid("")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("partial @g.us without domain prefix is not a group", () => {
|
||||||
|
expect(isGroupJid("@g.us")).toBe(true); // ends with @g.us — still truthy by rule
|
||||||
|
});
|
||||||
|
|
||||||
|
it("random string without suffix is not a group", () => {
|
||||||
|
expect(isGroupJid("somegroup")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Upsert merge logic ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface FakeGroup {
|
||||||
|
jid: string;
|
||||||
|
sessionId: string;
|
||||||
|
appId: string;
|
||||||
|
subject: string | null;
|
||||||
|
description: string | null;
|
||||||
|
participantCount: number | null;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertGroup(
|
||||||
|
store: Map<string, FakeGroup>,
|
||||||
|
data: {
|
||||||
|
appId: string;
|
||||||
|
sessionId: string;
|
||||||
|
jid: string;
|
||||||
|
subject?: string | null;
|
||||||
|
description?: string | null;
|
||||||
|
participantCount?: number | null;
|
||||||
|
},
|
||||||
|
): FakeGroup {
|
||||||
|
const key = `${data.sessionId}:${data.jid}`;
|
||||||
|
const existing = store.get(key);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
const row: FakeGroup = {
|
||||||
|
jid: data.jid,
|
||||||
|
sessionId: data.sessionId,
|
||||||
|
appId: data.appId,
|
||||||
|
subject: data.subject ?? null,
|
||||||
|
description: data.description ?? null,
|
||||||
|
participantCount: data.participantCount ?? null,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
store.set(key, row);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.subject !== undefined) existing.subject = data.subject;
|
||||||
|
if (data.description !== undefined) existing.description = data.description;
|
||||||
|
if (data.participantCount !== undefined) existing.participantCount = data.participantCount;
|
||||||
|
existing.updatedAt = now;
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("upsert merge logic", () => {
|
||||||
|
it("inserts new group", () => {
|
||||||
|
const store = new Map<string, FakeGroup>();
|
||||||
|
const g = upsertGroup(store, {
|
||||||
|
appId: "app1",
|
||||||
|
sessionId: "sess1",
|
||||||
|
jid: "123@g.us",
|
||||||
|
subject: "Dev Team",
|
||||||
|
participantCount: 5,
|
||||||
|
});
|
||||||
|
expect(g.subject).toBe("Dev Team");
|
||||||
|
expect(g.participantCount).toBe(5);
|
||||||
|
expect(store.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates subject on re-upsert", () => {
|
||||||
|
const store = new Map<string, FakeGroup>();
|
||||||
|
upsertGroup(store, { appId: "a", sessionId: "s", jid: "123@g.us", subject: "Old" });
|
||||||
|
const g = upsertGroup(store, { appId: "a", sessionId: "s", jid: "123@g.us", subject: "New" });
|
||||||
|
expect(g.subject).toBe("New");
|
||||||
|
expect(store.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("undefined subject does not clobber existing subject", () => {
|
||||||
|
const store = new Map<string, FakeGroup>();
|
||||||
|
upsertGroup(store, { appId: "a", sessionId: "s", jid: "123@g.us", subject: "Keep me" });
|
||||||
|
const g = upsertGroup(store, { appId: "a", sessionId: "s", jid: "123@g.us", description: "new desc" });
|
||||||
|
expect(g.subject).toBe("Keep me");
|
||||||
|
expect(g.description).toBe("new desc");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("null subject explicitly clears subject", () => {
|
||||||
|
const store = new Map<string, FakeGroup>();
|
||||||
|
upsertGroup(store, { appId: "a", sessionId: "s", jid: "123@g.us", subject: "Has Name" });
|
||||||
|
const g = upsertGroup(store, { appId: "a", sessionId: "s", jid: "123@g.us", subject: null });
|
||||||
|
expect(g.subject).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("different sessions create separate entries for same JID", () => {
|
||||||
|
const store = new Map<string, FakeGroup>();
|
||||||
|
upsertGroup(store, { appId: "a", sessionId: "s1", jid: "123@g.us", subject: "A" });
|
||||||
|
upsertGroup(store, { appId: "a", sessionId: "s2", jid: "123@g.us", subject: "B" });
|
||||||
|
expect(store.size).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── adjustParticipantCount ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function adjustCount(current: number | null, delta: number): number {
|
||||||
|
return Math.max(0, (current ?? 0) + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("adjustParticipantCount delta", () => {
|
||||||
|
it("add participants increases count", () => {
|
||||||
|
expect(adjustCount(5, 2)).toBe(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remove participants decreases count", () => {
|
||||||
|
expect(adjustCount(5, -2)).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remove more than current clamps to 0", () => {
|
||||||
|
expect(adjustCount(2, -10)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("null current treated as 0", () => {
|
||||||
|
expect(adjustCount(null, 3)).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remove from null/0 clamps to 0", () => {
|
||||||
|
expect(adjustCount(null, -5)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("zero delta leaves count unchanged", () => {
|
||||||
|
expect(adjustCount(8, 0)).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("large add works correctly", () => {
|
||||||
|
expect(adjustCount(100, 50)).toBe(150);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── listWhatsappGroups filter ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface GroupRow {
|
||||||
|
id: string;
|
||||||
|
appId: string;
|
||||||
|
sessionId: string;
|
||||||
|
jid: string;
|
||||||
|
subject: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listGroups(
|
||||||
|
rows: GroupRow[],
|
||||||
|
appId: string,
|
||||||
|
sessionId?: string,
|
||||||
|
): GroupRow[] {
|
||||||
|
return rows.filter(
|
||||||
|
(r) => r.appId === appId && (sessionId ? r.sessionId === sessionId : true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("listWhatsappGroups filter", () => {
|
||||||
|
const rows: GroupRow[] = [
|
||||||
|
{ id: "1", appId: "app1", sessionId: "sessA", jid: "111@g.us", subject: "Alpha" },
|
||||||
|
{ id: "2", appId: "app1", sessionId: "sessB", jid: "222@g.us", subject: "Beta" },
|
||||||
|
{ id: "3", appId: "app2", sessionId: "sessC", jid: "333@g.us", subject: "Gamma" },
|
||||||
|
];
|
||||||
|
|
||||||
|
it("filters by appId only", () => {
|
||||||
|
expect(listGroups(rows, "app1")).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters by appId + sessionId", () => {
|
||||||
|
const result = listGroups(rows, "app1", "sessA");
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].subject).toBe("Alpha");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wrong appId returns empty", () => {
|
||||||
|
expect(listGroups(rows, "app999")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("wrong sessionId returns empty", () => {
|
||||||
|
expect(listGroups(rows, "app1", "sessZ")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("different app's groups not included", () => {
|
||||||
|
const result = listGroups(rows, "app1");
|
||||||
|
expect(result.every((r) => r.appId === "app1")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Subject/description normalization ───────────────────────────────────────
|
||||||
|
|
||||||
|
function normalizeSubject(raw: string | undefined | null): string | null {
|
||||||
|
if (!raw?.trim()) return null;
|
||||||
|
return raw.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("group subject normalization", () => {
|
||||||
|
it("undefined → null", () => {
|
||||||
|
expect(normalizeSubject(undefined)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("null → null", () => {
|
||||||
|
expect(normalizeSubject(null)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty string → null", () => {
|
||||||
|
expect(normalizeSubject("")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("whitespace-only → null", () => {
|
||||||
|
expect(normalizeSubject(" ")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("valid string trimmed", () => {
|
||||||
|
expect(normalizeSubject(" Dev Team ")).toBe("Dev Team");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normal string unchanged", () => {
|
||||||
|
expect(normalizeSubject("Marketing")).toBe("Marketing");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Own JID matching ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function jidUser(jid: string): string {
|
||||||
|
return jid.split(":")[0].split("@")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOwnJid(participant: string, ownJid: string): boolean {
|
||||||
|
return participant === ownJid || jidUser(participant) === jidUser(ownJid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function participantsIncludeOwn(participants: string[], ownJid: string): boolean {
|
||||||
|
return participants.some((p) => isOwnJid(p, ownJid));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("own JID matching", () => {
|
||||||
|
const OWN = "6281234567890@s.whatsapp.net";
|
||||||
|
|
||||||
|
it("exact match", () => {
|
||||||
|
expect(isOwnJid(OWN, OWN)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("LID suffix variant matches (e.g. 628...@lid vs 628...@s.whatsapp.net)", () => {
|
||||||
|
// Baileys may use :0 device suffix
|
||||||
|
expect(isOwnJid("6281234567890:0@s.whatsapp.net", "6281234567890@s.whatsapp.net")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("different number does not match", () => {
|
||||||
|
expect(isOwnJid("6289999999999@s.whatsapp.net", OWN)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty participants → not own", () => {
|
||||||
|
expect(participantsIncludeOwn([], OWN)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("list with own JID → true", () => {
|
||||||
|
expect(participantsIncludeOwn([OWN, "6289@s.whatsapp.net"], OWN)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("list without own JID → false", () => {
|
||||||
|
expect(participantsIncludeOwn(["6289@s.whatsapp.net"], OWN)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Webhook event routing ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type GroupWebhookEvent =
|
||||||
|
| "group.joined"
|
||||||
|
| "group.left"
|
||||||
|
| "group.updated"
|
||||||
|
| "group.participants_added"
|
||||||
|
| "group.participants_removed";
|
||||||
|
|
||||||
|
interface GroupParticipantsUpdate {
|
||||||
|
id: string;
|
||||||
|
participants: string[];
|
||||||
|
action: "add" | "remove" | "promote" | "demote";
|
||||||
|
}
|
||||||
|
|
||||||
|
function routeParticipantsEvent(
|
||||||
|
update: GroupParticipantsUpdate,
|
||||||
|
ownJid: string,
|
||||||
|
): GroupWebhookEvent | null {
|
||||||
|
const ownInvolved = participantsIncludeOwn(update.participants, ownJid);
|
||||||
|
if (update.action === "add") {
|
||||||
|
return ownInvolved ? "group.joined" : "group.participants_added";
|
||||||
|
}
|
||||||
|
if (update.action === "remove") {
|
||||||
|
return ownInvolved ? "group.left" : "group.participants_removed";
|
||||||
|
}
|
||||||
|
return null; // promote/demote: no group webhook
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("webhook event routing (group-participants.update)", () => {
|
||||||
|
const OWN = "6281234567890@s.whatsapp.net";
|
||||||
|
const GID = "120363429229698693@g.us";
|
||||||
|
const OTHER = "6289999999999@s.whatsapp.net";
|
||||||
|
|
||||||
|
it("add own JID → group.joined", () => {
|
||||||
|
expect(routeParticipantsEvent({ id: GID, participants: [OWN], action: "add" }, OWN))
|
||||||
|
.toBe("group.joined");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remove own JID → group.left", () => {
|
||||||
|
expect(routeParticipantsEvent({ id: GID, participants: [OWN], action: "remove" }, OWN))
|
||||||
|
.toBe("group.left");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("add other JID → group.participants_added", () => {
|
||||||
|
expect(routeParticipantsEvent({ id: GID, participants: [OTHER], action: "add" }, OWN))
|
||||||
|
.toBe("group.participants_added");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("remove other JID → group.participants_removed", () => {
|
||||||
|
expect(routeParticipantsEvent({ id: GID, participants: [OTHER], action: "remove" }, OWN))
|
||||||
|
.toBe("group.participants_removed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("promote → null (no group webhook)", () => {
|
||||||
|
expect(routeParticipantsEvent({ id: GID, participants: [OTHER], action: "promote" }, OWN))
|
||||||
|
.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("demote → null (no group webhook)", () => {
|
||||||
|
expect(routeParticipantsEvent({ id: GID, participants: [OTHER], action: "demote" }, OWN))
|
||||||
|
.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("multiple participants, own included → group.joined", () => {
|
||||||
|
expect(routeParticipantsEvent({ id: GID, participants: [OTHER, OWN], action: "add" }, OWN))
|
||||||
|
.toBe("group.joined");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Webhook payload shapes ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface GroupMeta {
|
||||||
|
jid: string;
|
||||||
|
subject: string | null;
|
||||||
|
description: string | null;
|
||||||
|
participantCount: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGroupJoinedPayload(sessionId: string, meta: GroupMeta) {
|
||||||
|
return {
|
||||||
|
sessionId,
|
||||||
|
jid: meta.jid,
|
||||||
|
subject: meta.subject,
|
||||||
|
description: meta.description,
|
||||||
|
participantCount: meta.participantCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGroupLeftPayload(sessionId: string, jid: string, participants: string[]) {
|
||||||
|
return { sessionId, jid, participants };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGroupUpdatedPayload(sessionId: string, jid: string, subject?: string | null, description?: string | null) {
|
||||||
|
return { sessionId, jid, subject: subject ?? null, description: description ?? null };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParticipantsPayload(sessionId: string, jid: string, participants: string[]) {
|
||||||
|
return { sessionId, jid, participants };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("group.joined payload", () => {
|
||||||
|
it("includes all metadata fields", () => {
|
||||||
|
const p = buildGroupJoinedPayload("sess1", {
|
||||||
|
jid: "123@g.us",
|
||||||
|
subject: "Dev",
|
||||||
|
description: "Chat",
|
||||||
|
participantCount: 5,
|
||||||
|
});
|
||||||
|
expect(p).toEqual({
|
||||||
|
sessionId: "sess1",
|
||||||
|
jid: "123@g.us",
|
||||||
|
subject: "Dev",
|
||||||
|
description: "Chat",
|
||||||
|
participantCount: 5,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("null metadata fields preserved", () => {
|
||||||
|
const p = buildGroupJoinedPayload("sess1", {
|
||||||
|
jid: "123@g.us",
|
||||||
|
subject: null,
|
||||||
|
description: null,
|
||||||
|
participantCount: null,
|
||||||
|
});
|
||||||
|
expect(p.subject).toBeNull();
|
||||||
|
expect(p.description).toBeNull();
|
||||||
|
expect(p.participantCount).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("group.left payload", () => {
|
||||||
|
it("includes sessionId, jid, participants", () => {
|
||||||
|
const p = buildGroupLeftPayload("sess1", "123@g.us", ["628@s.whatsapp.net"]);
|
||||||
|
expect(p).toEqual({
|
||||||
|
sessionId: "sess1",
|
||||||
|
jid: "123@g.us",
|
||||||
|
participants: ["628@s.whatsapp.net"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT include subject or description", () => {
|
||||||
|
const p = buildGroupLeftPayload("s", "j", ["x"]);
|
||||||
|
expect(p).not.toHaveProperty("subject");
|
||||||
|
expect(p).not.toHaveProperty("description");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("group.updated payload", () => {
|
||||||
|
it("includes changed subject", () => {
|
||||||
|
const p = buildGroupUpdatedPayload("sess1", "123@g.us", "New Name", null);
|
||||||
|
expect(p.subject).toBe("New Name");
|
||||||
|
expect(p.description).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("both null when no metadata in update", () => {
|
||||||
|
const p = buildGroupUpdatedPayload("sess1", "123@g.us");
|
||||||
|
expect(p.subject).toBeNull();
|
||||||
|
expect(p.description).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("group.participants_added / group.participants_removed payload", () => {
|
||||||
|
it("shape: sessionId, jid, participants array", () => {
|
||||||
|
const p = buildParticipantsPayload("sess1", "123@g.us", ["a@s.whatsapp.net", "b@s.whatsapp.net"]);
|
||||||
|
expect(p.sessionId).toBe("sess1");
|
||||||
|
expect(p.jid).toBe("123@g.us");
|
||||||
|
expect(p.participants).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT include subject, description, participantCount", () => {
|
||||||
|
const p = buildParticipantsPayload("s", "j", ["x"]);
|
||||||
|
expect(p).not.toHaveProperty("subject");
|
||||||
|
expect(p).not.toHaveProperty("description");
|
||||||
|
expect(p).not.toHaveProperty("participantCount");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("empty participants array is valid", () => {
|
||||||
|
const p = buildParticipantsPayload("s", "j", []);
|
||||||
|
expect(p.participants).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── All 15 event types recognized ───────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("ALL_EVENTS completeness", () => {
|
||||||
|
const ALL_EVENTS = [
|
||||||
|
"session.status",
|
||||||
|
"session.deleted",
|
||||||
|
"message.received",
|
||||||
|
"message.sent",
|
||||||
|
"message.delivered",
|
||||||
|
"message.read",
|
||||||
|
"message.failed",
|
||||||
|
"presence.update",
|
||||||
|
"contact.created",
|
||||||
|
"contact.updated",
|
||||||
|
"group.joined",
|
||||||
|
"group.left",
|
||||||
|
"group.updated",
|
||||||
|
"group.participants_added",
|
||||||
|
"group.participants_removed",
|
||||||
|
];
|
||||||
|
|
||||||
|
it("has 15 event types", () => {
|
||||||
|
expect(ALL_EVENTS).toHaveLength(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no duplicates", () => {
|
||||||
|
expect(new Set(ALL_EVENTS).size).toBe(ALL_EVENTS.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("all group events present", () => {
|
||||||
|
const groupEvents = ALL_EVENTS.filter((e) => e.startsWith("group."));
|
||||||
|
expect(groupEvents).toHaveLength(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("group events follow noun.verb pattern", () => {
|
||||||
|
const groupEvents = ALL_EVENTS.filter((e) => e.startsWith("group."));
|
||||||
|
for (const e of groupEvents) {
|
||||||
|
expect(e).toMatch(/^group\.[a-z_]+$/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue