@adens/openwa 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/LICENSE +21 -0
- package/README.md +319 -0
- package/bin/openwa.js +11 -0
- package/favicon.ico +0 -0
- package/logo-long.png +0 -0
- package/logo-square.png +0 -0
- package/package.json +69 -0
- package/prisma/schema.prisma +182 -0
- package/server/config.js +29 -0
- package/server/database/client.js +11 -0
- package/server/database/init.js +28 -0
- package/server/express/create-app.js +349 -0
- package/server/express/openapi.js +853 -0
- package/server/index.js +163 -0
- package/server/services/api-key-service.js +131 -0
- package/server/services/auth-service.js +162 -0
- package/server/services/chat-service.js +1014 -0
- package/server/services/session-service.js +81 -0
- package/server/socket/register.js +127 -0
- package/server/utils/avatar.js +34 -0
- package/server/utils/paths.js +29 -0
- package/server/whatsapp/adapters/mock-adapter.js +47 -0
- package/server/whatsapp/adapters/wwebjs-adapter.js +263 -0
- package/server/whatsapp/session-manager.js +356 -0
- package/web/components/AppHead.js +14 -0
- package/web/components/AuthCard.js +170 -0
- package/web/components/BrandLogo.js +11 -0
- package/web/components/ChatWindow.js +875 -0
- package/web/components/ChatWindow.js.tmp +0 -0
- package/web/components/ContactList.js +97 -0
- package/web/components/ContactsPanel.js +90 -0
- package/web/components/EmojiPicker.js +108 -0
- package/web/components/MediaPreviewModal.js +146 -0
- package/web/components/MessageActionMenu.js +155 -0
- package/web/components/SessionSidebar.js +167 -0
- package/web/components/SettingsModal.js +266 -0
- package/web/components/Skeletons.js +73 -0
- package/web/jsconfig.json +10 -0
- package/web/lib/api.js +33 -0
- package/web/lib/socket.js +9 -0
- package/web/pages/_app.js +5 -0
- package/web/pages/dashboard.js +541 -0
- package/web/pages/index.js +62 -0
- package/web/postcss.config.js +10 -0
- package/web/public/favicon.ico +0 -0
- package/web/public/logo-long.png +0 -0
- package/web/public/logo-square.png +0 -0
- package/web/store/useAppStore.js +209 -0
- package/web/styles/globals.css +52 -0
- package/web/tailwind.config.js +36 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { prisma } = require("../database/client");
|
|
2
|
+
|
|
3
|
+
async function retryOnSqliteTimeout(operation) {
|
|
4
|
+
let lastError = null;
|
|
5
|
+
for (const delayMs of [0, 100, 250, 500]) {
|
|
6
|
+
if (delayMs > 0) {
|
|
7
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
return await operation();
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error?.code !== "P1008") {
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
lastError = error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
throw lastError;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function listUserSessions(userId) {
|
|
25
|
+
return prisma.whatsappSession.findMany({
|
|
26
|
+
where: { userId },
|
|
27
|
+
orderBy: { createdAt: "desc" }
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function createUserSession(userId, { name, phoneNumber }) {
|
|
32
|
+
if (!name) {
|
|
33
|
+
throw new Error("Session name is required.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return prisma.whatsappSession.create({
|
|
37
|
+
data: {
|
|
38
|
+
userId,
|
|
39
|
+
name: String(name).trim(),
|
|
40
|
+
phoneNumber: phoneNumber ? String(phoneNumber).trim() : null,
|
|
41
|
+
status: "disconnected",
|
|
42
|
+
transportType: "wwebjs"
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function getSessionById(userId, sessionId) {
|
|
48
|
+
return prisma.whatsappSession.findFirst({
|
|
49
|
+
where: {
|
|
50
|
+
id: sessionId,
|
|
51
|
+
userId
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function listReconnectableSessions() {
|
|
57
|
+
return prisma.whatsappSession.findMany({
|
|
58
|
+
where: {
|
|
59
|
+
status: {
|
|
60
|
+
in: ["ready", "connecting"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function touchSessionState(sessionId, data) {
|
|
67
|
+
return retryOnSqliteTimeout(() =>
|
|
68
|
+
prisma.whatsappSession.update({
|
|
69
|
+
where: { id: sessionId },
|
|
70
|
+
data
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
createUserSession,
|
|
77
|
+
getSessionById,
|
|
78
|
+
listReconnectableSessions,
|
|
79
|
+
listUserSessions,
|
|
80
|
+
touchSessionState
|
|
81
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const { getUserFromToken } = require("../services/auth-service");
|
|
2
|
+
const chatService = require("../services/chat-service");
|
|
3
|
+
|
|
4
|
+
function userRoom(userId) {
|
|
5
|
+
return `user:${userId}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function registerSocketHandlers({ io, config, sessionManager }) {
|
|
9
|
+
io.use(async (socket, next) => {
|
|
10
|
+
try {
|
|
11
|
+
const token = socket.handshake.auth?.token;
|
|
12
|
+
const user = await getUserFromToken(token, config);
|
|
13
|
+
|
|
14
|
+
if (!user) {
|
|
15
|
+
return next(new Error("Unauthorized"));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
socket.user = user;
|
|
19
|
+
return next();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return next(error);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
io.on("connection", (socket) => {
|
|
26
|
+
socket.join(userRoom(socket.user.id));
|
|
27
|
+
|
|
28
|
+
socket.on("send_message", async (payload = {}, ack) => {
|
|
29
|
+
try {
|
|
30
|
+
const result = await chatService.createOutgoingMessage({
|
|
31
|
+
userId: socket.user.id,
|
|
32
|
+
chatId: payload.chatId,
|
|
33
|
+
body: payload.body,
|
|
34
|
+
type: payload.type || "text",
|
|
35
|
+
mediaFileId: payload.mediaFileId || null,
|
|
36
|
+
replyToId: payload.replyToId || null
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
io.to(userRoom(socket.user.id)).emit("new_message", result.message);
|
|
40
|
+
io.to(userRoom(socket.user.id)).emit("contact_list_update", result.chat);
|
|
41
|
+
|
|
42
|
+
if (result.message.sessionId) {
|
|
43
|
+
await sessionManager.sendMessage(result.message.sessionId, {
|
|
44
|
+
recipient: result.message.receiver,
|
|
45
|
+
body: result.message.body,
|
|
46
|
+
mediaFileId: result.message.mediaFileId,
|
|
47
|
+
mediaPath: result.message.mediaFile?.relativePath || null
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
await chatService.addMessageStatus(result.message.id, "delivered");
|
|
51
|
+
io.to(userRoom(socket.user.id)).emit("message_status_update", {
|
|
52
|
+
messageId: result.message.id,
|
|
53
|
+
status: "delivered"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (ack) {
|
|
58
|
+
ack({ ok: true, message: result.message });
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (ack) {
|
|
62
|
+
ack({ ok: false, error: error.message });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
socket.on("send_media", async (payload = {}, ack) => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await chatService.createOutgoingMessage({
|
|
70
|
+
userId: socket.user.id,
|
|
71
|
+
chatId: payload.chatId,
|
|
72
|
+
body: payload.body,
|
|
73
|
+
type: payload.type || "document",
|
|
74
|
+
mediaFileId: payload.mediaFileId,
|
|
75
|
+
replyToId: payload.replyToId || null
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
io.to(userRoom(socket.user.id)).emit("new_message", result.message);
|
|
79
|
+
io.to(userRoom(socket.user.id)).emit("contact_list_update", result.chat);
|
|
80
|
+
|
|
81
|
+
if (result.message.sessionId) {
|
|
82
|
+
await sessionManager.sendMessage(result.message.sessionId, {
|
|
83
|
+
recipient: result.message.receiver,
|
|
84
|
+
body: result.message.body,
|
|
85
|
+
mediaFileId: result.message.mediaFileId,
|
|
86
|
+
mediaPath: result.message.mediaFile?.relativePath || null
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (ack) {
|
|
91
|
+
ack({ ok: true, message: result.message });
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (ack) {
|
|
95
|
+
ack({ ok: false, error: error.message });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
socket.on("typing", (payload = {}) => {
|
|
101
|
+
io.to(userRoom(socket.user.id)).emit("typing_event", {
|
|
102
|
+
chatId: payload.chatId,
|
|
103
|
+
isTyping: Boolean(payload.isTyping),
|
|
104
|
+
userId: socket.user.id,
|
|
105
|
+
name: socket.user.name
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
socket.on("open_chat", async (payload = {}, ack) => {
|
|
110
|
+
try {
|
|
111
|
+
await chatService.markChatOpened(socket.user.id, payload.chatId);
|
|
112
|
+
if (ack) {
|
|
113
|
+
ack({ ok: true });
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (ack) {
|
|
117
|
+
ack({ ok: false, error: error.message });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
registerSocketHandlers,
|
|
126
|
+
userRoom
|
|
127
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function initials(name) {
|
|
2
|
+
return String(name || "?")
|
|
3
|
+
.split(" ")
|
|
4
|
+
.map((part) => part[0])
|
|
5
|
+
.join("")
|
|
6
|
+
.slice(0, 2)
|
|
7
|
+
.toUpperCase();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function colorForSeed(seed) {
|
|
11
|
+
const palette = [
|
|
12
|
+
["#25d366", "#d2f8e9"],
|
|
13
|
+
["#00a884", "#dffaf1"],
|
|
14
|
+
["#7c4dff", "#efe7ff"],
|
|
15
|
+
["#ff8a65", "#ffe8e0"],
|
|
16
|
+
["#5c6bc0", "#e7eaff"],
|
|
17
|
+
["#26a69a", "#e0f7f4"]
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const source = String(seed || "");
|
|
21
|
+
const index = source.split("").reduce((sum, char) => sum + char.charCodeAt(0), 0) % palette.length;
|
|
22
|
+
return palette[index];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createAvatarDataUrl(name, seed = name) {
|
|
26
|
+
const label = initials(name);
|
|
27
|
+
const [background, foreground] = colorForSeed(seed);
|
|
28
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96"><rect width="96" height="96" rx="48" fill="${background}"/><text x="48" y="55" text-anchor="middle" font-family="Segoe UI, Arial, sans-serif" font-size="30" font-weight="700" fill="${foreground}">${label}</text></svg>`;
|
|
29
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
createAvatarDataUrl
|
|
34
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const rootDir = path.resolve(__dirname, "..", "..");
|
|
5
|
+
const storageDir = path.join(rootDir, "storage");
|
|
6
|
+
const sessionsDir = path.join(storageDir, "sessions");
|
|
7
|
+
const mediaDir = path.join(storageDir, "media");
|
|
8
|
+
const databaseDir = path.join(storageDir, "database");
|
|
9
|
+
const prismaSchemaPath = path.join(rootDir, "prisma", "schema.prisma");
|
|
10
|
+
const webDir = path.join(rootDir, "web");
|
|
11
|
+
|
|
12
|
+
function ensureDir(dirPath) {
|
|
13
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ensureRuntimeDirs() {
|
|
17
|
+
[storageDir, sessionsDir, mediaDir, databaseDir].forEach(ensureDir);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
rootDir,
|
|
22
|
+
storageDir,
|
|
23
|
+
sessionsDir,
|
|
24
|
+
mediaDir,
|
|
25
|
+
databaseDir,
|
|
26
|
+
prismaSchemaPath,
|
|
27
|
+
webDir,
|
|
28
|
+
ensureRuntimeDirs
|
|
29
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const EventEmitter = require("events");
|
|
2
|
+
const QRCode = require("qrcode");
|
|
3
|
+
|
|
4
|
+
class MockAdapter extends EventEmitter {
|
|
5
|
+
constructor({ session }) {
|
|
6
|
+
super();
|
|
7
|
+
this.session = session;
|
|
8
|
+
this.connected = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async connect() {
|
|
12
|
+
this.emit("status", { status: "connecting", transportType: "mock" });
|
|
13
|
+
const qrCode = await QRCode.toDataURL(`openwa:${this.session.id}:${Date.now()}`);
|
|
14
|
+
this.emit("qr", { qrCode, transportType: "mock" });
|
|
15
|
+
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
this.connected = true;
|
|
18
|
+
this.emit("status", { status: "ready", transportType: "mock" });
|
|
19
|
+
}, 1200);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async disconnect() {
|
|
23
|
+
this.connected = false;
|
|
24
|
+
this.emit("status", { status: "disconnected", transportType: "mock" });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async sendMessage(payload) {
|
|
28
|
+
if (!this.connected) {
|
|
29
|
+
throw new Error("Mock session is not connected yet.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setTimeout(() => {
|
|
33
|
+
this.emit("message", {
|
|
34
|
+
sender: payload.recipient,
|
|
35
|
+
displayName: this.session.name,
|
|
36
|
+
body: `Auto reply: ${payload.body || "Media received"}`,
|
|
37
|
+
type: payload.mediaFileId ? "document" : "text"
|
|
38
|
+
});
|
|
39
|
+
}, 1800);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
externalMessageId: `mock-${Date.now()}`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { MockAdapter };
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const EventEmitter = require("events");
|
|
2
|
+
const QRCode = require("qrcode");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { sessionsDir, rootDir } = require("../../utils/paths");
|
|
5
|
+
|
|
6
|
+
class WwebjsAdapter extends EventEmitter {
|
|
7
|
+
constructor({ session }) {
|
|
8
|
+
super();
|
|
9
|
+
this.session = session;
|
|
10
|
+
this.client = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async resolveProfilePic(externalId) {
|
|
14
|
+
if (!this.client || !externalId) {
|
|
15
|
+
return {
|
|
16
|
+
url: null,
|
|
17
|
+
status: "missing",
|
|
18
|
+
reason: "client-not-ready-or-empty-id"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = await this.client.pupPage.evaluate(async (contactId) => {
|
|
23
|
+
try {
|
|
24
|
+
const chatWid = window.Store.WidFactory.createWid(contactId);
|
|
25
|
+
let profilePic = null;
|
|
26
|
+
|
|
27
|
+
if (typeof window.Store.ProfilePic.profilePicFind === "function") {
|
|
28
|
+
profilePic = await window.Store.ProfilePic.profilePicFind(chatWid);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!profilePic && typeof window.Store.ProfilePic.requestProfilePicFromServer === "function") {
|
|
32
|
+
profilePic = await window.Store.ProfilePic.requestProfilePicFromServer(chatWid);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return profilePic?.eurl || profilePic?.imgFull || profilePic?.img || null;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const message = String(error?.message || error || "");
|
|
38
|
+
if (message.includes("isNewsletter")) {
|
|
39
|
+
return {
|
|
40
|
+
__status: "newsletter-guard"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (error?.name === "ServerStatusCodeError") {
|
|
45
|
+
return {
|
|
46
|
+
__status: "server-status"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
__error: {
|
|
52
|
+
name: String(error?.name || "Error"),
|
|
53
|
+
message
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}, externalId);
|
|
58
|
+
|
|
59
|
+
if (result?.__error) {
|
|
60
|
+
return {
|
|
61
|
+
url: null,
|
|
62
|
+
status: "error",
|
|
63
|
+
reason: result.__error.message,
|
|
64
|
+
errorName: result.__error.name
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (result?.__status === "newsletter-guard") {
|
|
69
|
+
return {
|
|
70
|
+
url: null,
|
|
71
|
+
status: "newsletterGuard",
|
|
72
|
+
reason: "wwebjs-newsletter-guard"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (result?.__status === "server-status") {
|
|
77
|
+
return {
|
|
78
|
+
url: null,
|
|
79
|
+
status: "serverStatus",
|
|
80
|
+
reason: "server-status-code-error"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!result) {
|
|
85
|
+
return {
|
|
86
|
+
url: null,
|
|
87
|
+
status: "missing",
|
|
88
|
+
reason: "no-profile-photo-returned"
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
url: result,
|
|
94
|
+
status: "found",
|
|
95
|
+
reason: "profile-photo-found"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async resolveProfilePicUrl(externalId) {
|
|
100
|
+
const result = await this.resolveProfilePic(externalId);
|
|
101
|
+
if (result.status === "error") {
|
|
102
|
+
console.warn(`Failed to fetch WhatsApp profile photo for ${externalId}: ${result.reason}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result.url;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async connect() {
|
|
109
|
+
const { Client, LocalAuth } = require("whatsapp-web.js");
|
|
110
|
+
|
|
111
|
+
this.client = new Client({
|
|
112
|
+
authStrategy: new LocalAuth({
|
|
113
|
+
clientId: this.session.id,
|
|
114
|
+
dataPath: sessionsDir
|
|
115
|
+
}),
|
|
116
|
+
puppeteer: {
|
|
117
|
+
headless: true,
|
|
118
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.client.on("qr", async (qr) => {
|
|
123
|
+
const qrCode = await QRCode.toDataURL(qr);
|
|
124
|
+
this.emit("qr", { qrCode, transportType: "wwebjs" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.client.on("loading_screen", () => {
|
|
128
|
+
this.emit("status", { status: "connecting", transportType: "wwebjs" });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
this.client.on("ready", () => {
|
|
132
|
+
this.emit("status", { status: "ready", transportType: "wwebjs" });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
this.client.on("disconnected", (reason) => {
|
|
136
|
+
this.emit("status", {
|
|
137
|
+
status: "disconnected",
|
|
138
|
+
transportType: "wwebjs",
|
|
139
|
+
lastError: typeof reason === "string" ? reason : "Disconnected"
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.client.on("auth_failure", (message) => {
|
|
144
|
+
this.emit("status", {
|
|
145
|
+
status: "error",
|
|
146
|
+
transportType: "wwebjs",
|
|
147
|
+
lastError: message
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.client.on("message", (message) => {
|
|
152
|
+
void (async () => {
|
|
153
|
+
const chatId = String(message.from || "");
|
|
154
|
+
const isPrivateChat = chatId.endsWith("@c.us");
|
|
155
|
+
const isGroupChat = chatId.endsWith("@g.us");
|
|
156
|
+
|
|
157
|
+
if (!isPrivateChat && !isGroupChat) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.emit("message", {
|
|
162
|
+
sender: chatId,
|
|
163
|
+
displayName: message._data?.notifyName || message._data?.pushname || chatId,
|
|
164
|
+
avatarUrl: await this.resolveProfilePicUrl(chatId),
|
|
165
|
+
body: message.body,
|
|
166
|
+
type: "text"
|
|
167
|
+
});
|
|
168
|
+
})().catch((error) => {
|
|
169
|
+
console.error("Failed to process incoming WhatsApp message.", error);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await this.client.initialize();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async disconnect() {
|
|
177
|
+
if (this.client) {
|
|
178
|
+
await this.client.destroy();
|
|
179
|
+
this.client = null;
|
|
180
|
+
}
|
|
181
|
+
this.emit("status", { status: "disconnected", transportType: "wwebjs" });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async getSyncSnapshot() {
|
|
185
|
+
if (!this.client) {
|
|
186
|
+
throw new Error("WhatsApp client is not ready.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const [contacts, chats] = await Promise.all([this.client.getContacts(), this.client.getChats()]);
|
|
190
|
+
const contactSnapshots = await Promise.all(
|
|
191
|
+
contacts.map(async (contact) => {
|
|
192
|
+
const externalId = contact.id?._serialized || "";
|
|
193
|
+
const avatarResult = await this.resolveProfilePic(externalId);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
externalId,
|
|
197
|
+
name: contact.name || contact.shortName || "",
|
|
198
|
+
pushname: contact.pushname || contact.name || "",
|
|
199
|
+
avatarUrl: avatarResult.url
|
|
200
|
+
};
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
const contactAvatarMap = new Map(contactSnapshots.map((contact) => [contact.externalId, contact.avatarUrl]));
|
|
204
|
+
const chatSnapshots = [];
|
|
205
|
+
|
|
206
|
+
for (const chat of chats) {
|
|
207
|
+
const externalId = chat.id?._serialized || "";
|
|
208
|
+
if (!externalId.endsWith("@c.us") && !externalId.endsWith("@g.us")) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const messages = await chat.fetchMessages({ limit: 50 });
|
|
213
|
+
const chatAvatarResult = contactAvatarMap.get(externalId)
|
|
214
|
+
? { url: contactAvatarMap.get(externalId), status: "found", reason: "reused-contact-avatar" }
|
|
215
|
+
: await this.resolveProfilePic(externalId);
|
|
216
|
+
|
|
217
|
+
chatSnapshots.push({
|
|
218
|
+
externalId,
|
|
219
|
+
name: chat.name || chat.formattedTitle || externalId,
|
|
220
|
+
pushname: chat.name || chat.formattedTitle || externalId,
|
|
221
|
+
avatarUrl: chatAvatarResult.url,
|
|
222
|
+
messages: messages.map((message) => ({
|
|
223
|
+
externalMessageId: message.id?._serialized || null,
|
|
224
|
+
sender: message.fromMe ? `user:${this.session.userId}` : message.author || message.from || externalId,
|
|
225
|
+
body: message.body || null,
|
|
226
|
+
type: message.type,
|
|
227
|
+
direction: message.fromMe ? "outbound" : "inbound",
|
|
228
|
+
ack: message.ack ?? 0,
|
|
229
|
+
createdAt: new Date((message.timestamp || Math.floor(Date.now() / 1000)) * 1000).toISOString()
|
|
230
|
+
}))
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
contacts: contactSnapshots,
|
|
236
|
+
chats: chatSnapshots
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async sendMessage(payload) {
|
|
241
|
+
if (!this.client) {
|
|
242
|
+
throw new Error("WhatsApp client is not ready.");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (payload.mediaFileId) {
|
|
246
|
+
const { MessageMedia } = require("whatsapp-web.js");
|
|
247
|
+
const mediaPath = path.join(rootDir, "storage", payload.mediaPath || "");
|
|
248
|
+
|
|
249
|
+
const media = MessageMedia.fromFilePath(mediaPath);
|
|
250
|
+
const response = await this.client.sendMessage(payload.recipient, media, payload.body ? { caption: payload.body } : undefined);
|
|
251
|
+
return {
|
|
252
|
+
externalMessageId: response.id?._serialized || null
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const response = await this.client.sendMessage(payload.recipient, payload.body || "");
|
|
257
|
+
return {
|
|
258
|
+
externalMessageId: response.id?._serialized || null
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { WwebjsAdapter };
|