Skip to content

Commit 489d9e5

Browse files
committed
feat: implement delete functionality for WhatsApp instances with confirmation dialog
1 parent c215471 commit 489d9e5

5 files changed

Lines changed: 234 additions & 32 deletions

File tree

src/app/whatsapp/[slug]/edit/actions.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"use server";
22

33
import { db } from "@/db";
4-
import { whatsappTable } from "@/db/schema";
4+
import { whatsappTable, whatsappMemberTable } from "@/db/schema";
55
import { eq, and, ne } from "drizzle-orm";
66
import { revalidatePath } from "next/cache";
77
import { redirect } from "next/navigation";
8-
import { getWhatsappWithRole } from "@/lib/auth-utils";
8+
import { getWhatsappWithRole, requireInstancePermission } from "@/lib/auth-utils";
9+
import { disconnectWhatsApp } from "@/lib/whatsapp";
10+
import fs from "fs";
11+
import path from "path";
912

1013
export async function updateWhatsappAction(id: string, formData: FormData) {
1114
// Require manager role to edit WhatsApp settings
@@ -40,3 +43,37 @@ export async function updateWhatsappAction(id: string, formData: FormData) {
4043
revalidatePath(`/whatsapp/${slug}`);
4144
redirect(`/whatsapp/${slug}`);
4245
}
46+
47+
const SESSIONS_DIR = "whatsapp_sessions";
48+
49+
export async function deleteWhatsappAction(slug: string) {
50+
// Get the instance and verify owner permission
51+
const wa = await db.query.whatsappTable.findFirst({
52+
where: eq(whatsappTable.slug, slug),
53+
});
54+
55+
if (!wa) {
56+
throw new Error("WhatsApp instance not found");
57+
}
58+
59+
// Require delete_instance permission (only owners have this)
60+
await requireInstancePermission(wa.id, "delete_instance");
61+
62+
// Disconnect the WhatsApp session if active
63+
await disconnectWhatsApp(wa.id);
64+
65+
// Delete session files from disk
66+
const sessionPath = path.join(SESSIONS_DIR, wa.id);
67+
if (fs.existsSync(sessionPath)) {
68+
fs.rmSync(sessionPath, { recursive: true, force: true });
69+
}
70+
71+
// Delete all members first (to avoid FK constraint issues)
72+
await db.delete(whatsappMemberTable).where(eq(whatsappMemberTable.whatsappId, wa.id));
73+
74+
// Delete the WhatsApp instance (cascade will delete contacts, groups, messages, etc.)
75+
await db.delete(whatsappTable).where(eq(whatsappTable.id, wa.id));
76+
77+
revalidatePath("/");
78+
redirect("/");
79+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useRouter } from "next/navigation";
5+
import {
6+
AlertDialog,
7+
AlertDialogAction,
8+
AlertDialogCancel,
9+
AlertDialogContent,
10+
AlertDialogDescription,
11+
AlertDialogFooter,
12+
AlertDialogHeader,
13+
AlertDialogTitle,
14+
AlertDialogTrigger,
15+
} from "@/components/ui/alert-dialog";
16+
import { Button } from "@/components/ui/button";
17+
import { Input } from "@/components/ui/input";
18+
import { Trash2 } from "lucide-react";
19+
import { deleteWhatsappAction } from "./actions";
20+
21+
interface DeleteWhatsappButtonProps {
22+
slug: string;
23+
name: string;
24+
}
25+
26+
export function DeleteWhatsappButton({ slug, name }: DeleteWhatsappButtonProps) {
27+
const [confirmText, setConfirmText] = useState("");
28+
const [isDeleting, setIsDeleting] = useState(false);
29+
const [open, setOpen] = useState(false);
30+
const router = useRouter();
31+
32+
const isConfirmed = confirmText === name;
33+
34+
async function handleDelete() {
35+
if (!isConfirmed) return;
36+
37+
setIsDeleting(true);
38+
try {
39+
await deleteWhatsappAction(slug);
40+
router.push("/whatsapp");
41+
} catch (error) {
42+
console.error("Error deleting WhatsApp instance:", error);
43+
setIsDeleting(false);
44+
}
45+
}
46+
47+
return (
48+
<AlertDialog open={open} onOpenChange={setOpen}>
49+
<AlertDialogTrigger asChild>
50+
<Button variant="destructive" className="w-full">
51+
<Trash2 className="w-4 h-4 mr-2" />
52+
Eliminar instancia
53+
</Button>
54+
</AlertDialogTrigger>
55+
<AlertDialogContent>
56+
<AlertDialogHeader>
57+
<AlertDialogTitle>Eliminar instancia de WhatsApp</AlertDialogTitle>
58+
<AlertDialogDescription className="space-y-3">
59+
<p>
60+
Esta acción eliminará permanentemente la instancia <strong>{name}</strong> y todos sus datos asociados:
61+
</p>
62+
<ul className="list-disc list-inside text-sm space-y-1">
63+
<li>Todos los contactos</li>
64+
<li>Todos los grupos</li>
65+
<li>Todos los mensajes</li>
66+
<li>Todas las conexiones API</li>
67+
<li>Archivos de sesión</li>
68+
</ul>
69+
<p className="font-medium text-destructive">
70+
Esta acción no se puede deshacer.
71+
</p>
72+
<div className="pt-2">
73+
<p className="text-sm mb-2">
74+
Escribe <strong>{name}</strong> para confirmar:
75+
</p>
76+
<Input
77+
value={confirmText}
78+
onChange={(e) => setConfirmText(e.target.value)}
79+
placeholder={name}
80+
className="font-mono"
81+
/>
82+
</div>
83+
</AlertDialogDescription>
84+
</AlertDialogHeader>
85+
<AlertDialogFooter>
86+
<AlertDialogCancel onClick={() => setConfirmText("")}>
87+
Cancelar
88+
</AlertDialogCancel>
89+
<AlertDialogAction
90+
onClick={handleDelete}
91+
disabled={!isConfirmed || isDeleting}
92+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
93+
>
94+
{isDeleting ? "Eliminando..." : "Eliminar permanentemente"}
95+
</AlertDialogAction>
96+
</AlertDialogFooter>
97+
</AlertDialogContent>
98+
</AlertDialog>
99+
);
100+
}

src/app/whatsapp/[slug]/edit/page.tsx

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,23 @@
1-
import { auth } from "@/lib/auth";
21
import { db } from "@/db";
32
import { whatsappTable } from "@/db/schema";
4-
import { headers } from "next/headers";
53
import { notFound, redirect } from "next/navigation";
6-
import { eq, and } from "drizzle-orm";
4+
import { eq } from "drizzle-orm";
75
import Link from "next/link";
86
import { updateWhatsappAction } from "./actions";
7+
import { DeleteWhatsappButton } from "./delete-whatsapp-button";
8+
import { getWhatsappBySlugWithRole } from "@/lib/auth-utils";
99

1010
export default async function EditWhatsappPage({
1111
params,
1212
}: {
1313
params: Promise<{ slug: string }>;
1414
}) {
1515
const { slug } = await params;
16-
const session = await auth.api.getSession({
17-
headers: await headers(),
18-
});
1916

20-
if (!session) {
21-
redirect("/login");
22-
}
23-
24-
const wa = await db.query.whatsappTable.findFirst({
25-
where: and(
26-
eq(whatsappTable.slug, slug),
27-
eq(whatsappTable.userId, session.user.id)
28-
),
29-
});
17+
// Require at least manager role to edit, get actual role for delete button visibility
18+
const { wa, role } = await getWhatsappBySlugWithRole(slug, "manager");
3019

31-
if (!wa) {
32-
notFound();
33-
}
20+
const isOwner = role === "owner";
3421

3522
async function updateAction(formData: FormData) {
3623
"use server";
@@ -100,6 +87,16 @@ export default async function EditWhatsappPage({
10087
</button>
10188
</div>
10289
</form>
90+
91+
{isOwner && (
92+
<div className="mt-8 pt-6 border-t border-gray-200">
93+
<h3 className="text-sm font-medium text-gray-900 mb-2">Zona de peligro</h3>
94+
<p className="text-sm text-gray-500 mb-4">
95+
Una vez eliminada, la instancia no podrá ser recuperada.
96+
</p>
97+
<DeleteWhatsappButton slug={wa.slug} name={wa.name} />
98+
</div>
99+
)}
103100
</div>
104101
</div>
105102
</div>

src/lib/whatsapp-utils.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,20 @@ export const get_Receiver_and_Sender_and_Context_FromMessage = (param: {
107107
pushName: string;
108108
broadcast: boolean;
109109
messageTimestamp: number;
110-
}) => {
110+
}): {
111+
messageId: string;
112+
messageTimestamp: number;
113+
context:
114+
| {
115+
type: "personal";
116+
}
117+
| {
118+
type: "group";
119+
gid: `${string}@g.us`;
120+
};
121+
sender: (TPersonId | "me");
122+
receiver: (TPersonId | "me" | "group");
123+
} | null => {
111124
const { key, pushName, broadcast, messageTimestamp } = param;
112125

113126
if (broadcast) return null;
@@ -125,7 +138,7 @@ export const get_Receiver_and_Sender_and_Context_FromMessage = (param: {
125138
}
126139
| {
127140
type: "group";
128-
gid: string;
141+
gid: `${string}@g.us`;
129142
}
130143
| null = null;
131144

@@ -150,7 +163,7 @@ export const get_Receiver_and_Sender_and_Context_FromMessage = (param: {
150163
if (yoEscriboAGrupo.success) {
151164
context = {
152165
type: "group",
153-
gid: yoEscriboAGrupo.data.remoteJid,
166+
gid: yoEscriboAGrupo.data.remoteJid as `${string}@g.us`,
154167
};
155168
sender = "me";
156169
receiver = "group";
@@ -177,7 +190,7 @@ export const get_Receiver_and_Sender_and_Context_FromMessage = (param: {
177190
if (alguienDelGrupoMeEscribe.success) {
178191
context = {
179192
type: "group",
180-
gid: alguienDelGrupoMeEscribe.data.remoteJid,
193+
gid: alguienDelGrupoMeEscribe.data.remoteJid as `${string}@g.us`,
181194
};
182195
receiver = "me";
183196
if (alguienDelGrupoMeEscribe.data.addressingMode === "lid") {
@@ -195,6 +208,8 @@ export const get_Receiver_and_Sender_and_Context_FromMessage = (param: {
195208
}
196209
}
197210

211+
if (!context || !sender || !receiver) return null;
212+
198213
return {
199214
messageId: key.id,
200215
messageTimestamp,

src/test-socket/index.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ import makeWASocket, {
77
import pino from "pino";
88
import fs from "fs";
99
import path from "path";
10+
import { get_Receiver_and_Sender_and_Context_FromMessage } from "../lib/whatsapp-utils";
11+
import type {
12+
YoEscriboAContacto,
13+
ContactoMeEscribe,
14+
YoEscriboAGrupo,
15+
GrupoMeEscribe,
16+
} from "../lib/whatsapp-types";
1017

1118
const SESSIONS_DIR = "whatsapp_sessions";
1219

@@ -43,6 +50,39 @@ async function main() {
4350

4451
sock.ev.on("creds.update", saveCreds);
4552

53+
// Listen for incoming messages and parse them with get_Receiver_and_Sender_and_Context_FromMessage
54+
sock.ev.on("messages.upsert", async (m) => {
55+
for (const msg of m.messages) {
56+
// Skip broadcast messages
57+
if (msg.broadcast) continue;
58+
59+
const result = get_Receiver_and_Sender_and_Context_FromMessage(
60+
msg as unknown as {
61+
key: YoEscriboAContacto | ContactoMeEscribe | YoEscriboAGrupo | GrupoMeEscribe;
62+
pushName: string;
63+
broadcast: boolean;
64+
messageTimestamp: number;
65+
}
66+
);
67+
68+
if (!result) {
69+
console.log("[SKIP] Could not parse message:", msg.key);
70+
continue;
71+
}
72+
73+
const { messageId, messageTimestamp, receiver, sender, context } = result;
74+
75+
console.log("\n========== NEW MESSAGE ==========");
76+
console.log("Message ID (for DB):", messageId);
77+
console.log("Timestamp:", new Date(messageTimestamp * 1000).toISOString());
78+
console.log("Context:", JSON.stringify(context, null, 2));
79+
console.log("Sender:", JSON.stringify(sender, null, 2));
80+
console.log("Receiver:", JSON.stringify(receiver, null, 2));
81+
console.log("Body:", msg.message?.conversation || msg.message?.extendedTextMessage?.text || "[media/other]");
82+
console.log("=================================\n");
83+
}
84+
});
85+
4686
sock.ev.on("connection.update", async (update) => {
4787
const { connection, lastDisconnect } = update;
4888

@@ -59,21 +99,34 @@ async function main() {
5999
console.log("\n--- Socket ready for sending messages ---");
60100
console.log("To send a message, modify testJid variable with a valid JID");
61101
console.log("Example: 5491112345678@s.whatsapp.net");
62-
console.log("\nPress Ctrl+C to exit");
102+
console.log("\nListening for incoming messages...");
103+
console.log("Press Ctrl+C to exit\n");
63104
return;
64105
}
65106

66107
try {
67108
console.log(`\nSending message to ${testJid}...`);
68-
const result = await sock.sendMessage(testJid, { text: testMessage });
69-
console.log("Message sent successfully!");
70-
console.log("Result:", JSON.stringify(result, null, 2));
109+
const sentResult = await sock.sendMessage(testJid, { text: testMessage });
110+
111+
if (sentResult) {
112+
// Parse the sent message with our function
113+
const parsedSent = get_Receiver_and_Sender_and_Context_FromMessage({
114+
key: sentResult.key as YoEscriboAContacto | ContactoMeEscribe | YoEscriboAGrupo | GrupoMeEscribe,
115+
pushName: sock.user?.name || "",
116+
broadcast: false,
117+
messageTimestamp: Math.floor(Date.now() / 1000),
118+
});
119+
120+
console.log("\n========== SENT MESSAGE ==========");
121+
console.log("Message ID (for DB):", parsedSent?.messageId);
122+
console.log("Context:", JSON.stringify(parsedSent?.context, null, 2));
123+
console.log("Sender:", JSON.stringify(parsedSent?.sender, null, 2));
124+
console.log("Receiver:", JSON.stringify(parsedSent?.receiver, null, 2));
125+
console.log("==================================\n");
126+
}
71127
} catch (error) {
72128
console.error("Error sending message:", error);
73129
}
74-
75-
// Keep the connection open or close it
76-
// sock.end(undefined);
77130
}
78131

79132
if (connection === "close") {

0 commit comments

Comments
 (0)