@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,28 @@
|
|
|
1
|
+
const { spawnSync } = require("child_process");
|
|
2
|
+
const { prisma } = require("./client");
|
|
3
|
+
const { ensureRuntimeDirs, prismaSchemaPath, rootDir } = require("../utils/paths");
|
|
4
|
+
|
|
5
|
+
function runPrismaCommand(args) {
|
|
6
|
+
const prismaCli = require.resolve("prisma/build/index.js");
|
|
7
|
+
const result = spawnSync(process.execPath, [prismaCli, ...args, "--schema", prismaSchemaPath], {
|
|
8
|
+
cwd: rootDir,
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
env: process.env
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (result.status !== 0) {
|
|
14
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
15
|
+
throw new Error(`Prisma command failed: prisma ${args.join(" ")}\n${output}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function initializeDatabase() {
|
|
20
|
+
ensureRuntimeDirs();
|
|
21
|
+
runPrismaCommand(["generate"]);
|
|
22
|
+
runPrismaCommand(["db", "push"]);
|
|
23
|
+
await prisma.$connect();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
initializeDatabase
|
|
28
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const multer = require("multer");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const crypto = require("crypto");
|
|
5
|
+
const { authMiddleware, dashboardAuthMiddleware, isSqliteTimeoutError, loginUser, registerUser } = require("../services/auth-service");
|
|
6
|
+
const apiKeyService = require("../services/api-key-service");
|
|
7
|
+
const chatService = require("../services/chat-service");
|
|
8
|
+
const sessionService = require("../services/session-service");
|
|
9
|
+
const { createAgentReadme, createOpenApiDocument, createSwaggerHtml, packageName, packageVersion } = require("./openapi");
|
|
10
|
+
const { mediaDir } = require("../utils/paths");
|
|
11
|
+
|
|
12
|
+
function inferMessageType(file) {
|
|
13
|
+
if (!file?.mimetype) {
|
|
14
|
+
return "document";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (file.mimetype === "image/webp") {
|
|
18
|
+
return "sticker";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (file.mimetype.startsWith("image/")) {
|
|
22
|
+
return "image";
|
|
23
|
+
}
|
|
24
|
+
if (file.mimetype.startsWith("video/")) {
|
|
25
|
+
return "video";
|
|
26
|
+
}
|
|
27
|
+
if (file.mimetype.startsWith("audio/")) {
|
|
28
|
+
return "audio";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return "document";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createUploader() {
|
|
35
|
+
const storage = multer.diskStorage({
|
|
36
|
+
destination: mediaDir,
|
|
37
|
+
filename: (req, file, cb) => {
|
|
38
|
+
const ext = path.extname(file.originalname);
|
|
39
|
+
cb(null, `${crypto.randomUUID()}${ext}`);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return multer({ storage });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function withAsync(handler, statusCode = 500) {
|
|
47
|
+
return async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
await handler(req, res);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
res.status(error?.code === "P1008" ? 503 : statusCode).json({
|
|
52
|
+
error: error?.code === "P1008" ? "Database is busy. Please try again." : error.message
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createApp({ config, sessionManager }) {
|
|
59
|
+
const app = express();
|
|
60
|
+
const upload = createUploader();
|
|
61
|
+
const requireAuth = authMiddleware(config);
|
|
62
|
+
const requireDashboardAuth = dashboardAuthMiddleware(config);
|
|
63
|
+
const openApiDocument = createOpenApiDocument(config);
|
|
64
|
+
|
|
65
|
+
app.use((req, res, next) => {
|
|
66
|
+
res.header("Access-Control-Allow-Origin", config.frontendUrl);
|
|
67
|
+
res.header("Access-Control-Allow-Credentials", "true");
|
|
68
|
+
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-OpenWA-API-Key");
|
|
69
|
+
res.header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
|
|
70
|
+
|
|
71
|
+
if (req.method === "OPTIONS") {
|
|
72
|
+
return res.sendStatus(204);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return next();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
app.use(express.json({ limit: "10mb" }));
|
|
79
|
+
app.use(express.urlencoded({ extended: true }));
|
|
80
|
+
app.use("/media", express.static(mediaDir));
|
|
81
|
+
|
|
82
|
+
app.get("/docs", (req, res) => {
|
|
83
|
+
res.type("html").send(createSwaggerHtml());
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
app.get("/docs/json", (req, res) => {
|
|
87
|
+
res.json(openApiDocument);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
app.get("/docs/readme", (req, res) => {
|
|
91
|
+
res.type("text/markdown").send(createAgentReadme(config));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
app.get("/health", (req, res) => {
|
|
95
|
+
res.json({
|
|
96
|
+
ok: true,
|
|
97
|
+
service: packageName,
|
|
98
|
+
version: packageVersion
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.get("/version", (req, res) => {
|
|
103
|
+
res.json({
|
|
104
|
+
name: packageName,
|
|
105
|
+
version: packageVersion
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
app.get("/api/health", async (req, res) => {
|
|
110
|
+
res.json({
|
|
111
|
+
ok: true,
|
|
112
|
+
service: packageName,
|
|
113
|
+
version: packageVersion
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
app.post("/api/auth/register", async (req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const result = await registerUser({
|
|
120
|
+
...req.body,
|
|
121
|
+
config
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await chatService.ensureWelcomeWorkspace(result.user.id);
|
|
125
|
+
|
|
126
|
+
res.status(201).json(result);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
res.status(isSqliteTimeoutError(error) ? 503 : 400).json({
|
|
129
|
+
error: isSqliteTimeoutError(error) ? "Database is busy. Please try again." : error.message
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
app.post("/api/auth/login", async (req, res) => {
|
|
135
|
+
try {
|
|
136
|
+
const result = await loginUser({
|
|
137
|
+
...req.body,
|
|
138
|
+
config
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
await chatService.ensureWelcomeWorkspace(result.user.id);
|
|
142
|
+
|
|
143
|
+
res.json(result);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
res.status(isSqliteTimeoutError(error) ? 503 : 400).json({
|
|
146
|
+
error: isSqliteTimeoutError(error) ? "Database is busy. Please try again." : error.message
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
app.get("/api/auth/me", requireAuth, withAsync(async (req, res) => {
|
|
152
|
+
res.json({ user: req.user });
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
app.get("/api/api-keys", requireDashboardAuth, withAsync(async (req, res) => {
|
|
156
|
+
const apiKeys = await apiKeyService.listApiKeys(req.user.id);
|
|
157
|
+
res.json({ apiKeys });
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
app.post("/api/api-keys", requireDashboardAuth, async (req, res) => {
|
|
161
|
+
try {
|
|
162
|
+
const result = await apiKeyService.createApiKey(req.user.id, req.body);
|
|
163
|
+
res.status(201).json(result);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
res.status(400).json({ error: error.message });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
app.delete("/api/api-keys/:apiKeyId", requireDashboardAuth, async (req, res) => {
|
|
170
|
+
try {
|
|
171
|
+
const result = await apiKeyService.revokeApiKey(req.user.id, req.params.apiKeyId);
|
|
172
|
+
res.json(result);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
res.status(404).json({ error: error.message });
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
app.get("/api/bootstrap", requireAuth, async (req, res) => {
|
|
179
|
+
try {
|
|
180
|
+
await chatService.ensureWelcomeWorkspace(req.user.id);
|
|
181
|
+
const sessions = await sessionService.listUserSessions(req.user.id);
|
|
182
|
+
const chats = await chatService.listChats(req.user.id);
|
|
183
|
+
const activeChatId = chats[0]?.id || null;
|
|
184
|
+
const messageResult = activeChatId ? await chatService.listMessages(req.user.id, activeChatId) : { messages: [], hasMore: false, nextBefore: null };
|
|
185
|
+
|
|
186
|
+
res.json({
|
|
187
|
+
user: req.user,
|
|
188
|
+
sessions,
|
|
189
|
+
chats,
|
|
190
|
+
activeChatId,
|
|
191
|
+
messages: messageResult.messages,
|
|
192
|
+
hasMoreMessages: messageResult.hasMore,
|
|
193
|
+
nextBefore: messageResult.nextBefore
|
|
194
|
+
});
|
|
195
|
+
} catch (error) {
|
|
196
|
+
res.status(error?.code === "P1008" ? 503 : 500).json({
|
|
197
|
+
error: error?.code === "P1008" ? "Database is busy. Please try again." : error.message
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
app.get("/api/sessions", requireAuth, withAsync(async (req, res) => {
|
|
203
|
+
const sessions = await sessionService.listUserSessions(req.user.id);
|
|
204
|
+
res.json({ sessions });
|
|
205
|
+
}));
|
|
206
|
+
|
|
207
|
+
app.post("/api/sessions", requireAuth, async (req, res) => {
|
|
208
|
+
try {
|
|
209
|
+
const session = await sessionService.createUserSession(req.user.id, req.body);
|
|
210
|
+
await chatService.createSessionCompanionChat(req.user.id, session);
|
|
211
|
+
res.status(201).json({ session });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
res.status(400).json({ error: error.message });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
app.post("/api/sessions/:sessionId/connect", requireAuth, async (req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
await sessionManager.connectSession(req.user.id, req.params.sessionId, { force: true });
|
|
220
|
+
const session = await sessionService.getSessionById(req.user.id, req.params.sessionId);
|
|
221
|
+
res.json({ session });
|
|
222
|
+
} catch (error) {
|
|
223
|
+
res.status(400).json({ error: error.message });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
app.post("/api/sessions/:sessionId/disconnect", requireAuth, async (req, res) => {
|
|
228
|
+
try {
|
|
229
|
+
await sessionManager.disconnectSession(req.user.id, req.params.sessionId);
|
|
230
|
+
const session = await sessionService.getSessionById(req.user.id, req.params.sessionId);
|
|
231
|
+
res.json({ session });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
res.status(400).json({ error: error.message });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
app.get("/api/chats", requireAuth, withAsync(async (req, res) => {
|
|
238
|
+
const chats = await chatService.listChats(req.user.id, req.query.sessionId || undefined, req.query.q || "");
|
|
239
|
+
res.json({ chats });
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
app.get("/api/contacts", requireAuth, withAsync(async (req, res) => {
|
|
243
|
+
const contacts = await chatService.listContacts(req.user.id, req.query.sessionId || undefined, req.query.q || "");
|
|
244
|
+
res.json({ contacts });
|
|
245
|
+
}));
|
|
246
|
+
|
|
247
|
+
app.post("/api/contacts/:contactId/open", requireAuth, async (req, res) => {
|
|
248
|
+
try {
|
|
249
|
+
const chat = await chatService.openChatForContact(req.user.id, req.params.contactId);
|
|
250
|
+
res.json({ chat });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
res.status(error?.code === "P1008" ? 503 : 400).json({
|
|
253
|
+
error: error?.code === "P1008" ? "Database is busy. Please try again." : error.message
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
app.get("/api/chats/:chatId/messages", requireAuth, async (req, res) => {
|
|
259
|
+
try {
|
|
260
|
+
const result = await chatService.listMessages(req.user.id, req.params.chatId, {
|
|
261
|
+
take: req.query.take,
|
|
262
|
+
before: req.query.before,
|
|
263
|
+
search: req.query.search
|
|
264
|
+
});
|
|
265
|
+
res.json(result);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
res.status(error?.code === "P1008" ? 503 : 404).json({
|
|
268
|
+
error: error?.code === "P1008" ? "Database is busy. Please try again." : error.message
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
app.post("/api/chats/:chatId/messages/send", requireAuth, async (req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
const result = await chatService.createOutgoingMessage({
|
|
276
|
+
userId: req.user.id,
|
|
277
|
+
chatId: req.params.chatId,
|
|
278
|
+
body: req.body.body,
|
|
279
|
+
type: req.body.type || "text",
|
|
280
|
+
mediaFileId: req.body.mediaFileId || null,
|
|
281
|
+
replyToId: req.body.replyToId || null
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (result.message.sessionId) {
|
|
285
|
+
await sessionManager.sendMessage(result.message.sessionId, {
|
|
286
|
+
recipient: result.message.receiver,
|
|
287
|
+
body: result.message.body,
|
|
288
|
+
mediaFileId: result.message.mediaFileId,
|
|
289
|
+
mediaPath: result.message.mediaFile?.relativePath || null
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
await chatService.addMessageStatus(result.message.id, "delivered");
|
|
293
|
+
result.message.statuses = [...(result.message.statuses || []), { status: "delivered", createdAt: new Date().toISOString() }];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
res.json(result);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
res.status(error?.code === "P1008" ? 503 : 400).json({
|
|
299
|
+
error: error?.code === "P1008" ? "Database is busy. Please try again." : error.message
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
app.delete("/api/messages/:messageId", requireAuth, async (req, res) => {
|
|
305
|
+
try {
|
|
306
|
+
const result = await chatService.deleteMessage(req.user.id, req.params.messageId);
|
|
307
|
+
res.json(result);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
res.status(error?.code === "P1008" ? 503 : 400).json({
|
|
310
|
+
error: error?.code === "P1008" ? "Database is busy. Please try again." : error.message
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
app.post("/api/messages/:messageId/forward", requireAuth, async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const result = await chatService.forwardMessage(req.user.id, req.params.messageId, req.body.targetChatId);
|
|
318
|
+
res.json(result);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
res.status(error?.code === "P1008" ? 503 : 400).json({
|
|
321
|
+
error: error?.code === "P1008" ? "Database is busy. Please try again." : error.message
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
app.post("/api/media", requireAuth, upload.single("file"), async (req, res) => {
|
|
327
|
+
try {
|
|
328
|
+
if (!req.file) {
|
|
329
|
+
return res.status(400).json({ error: "File is required." });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const mediaFile = await chatService.createMediaFile(req.user.id, req.file);
|
|
333
|
+
return res.status(201).json({
|
|
334
|
+
mediaFile,
|
|
335
|
+
type: inferMessageType(req.file)
|
|
336
|
+
});
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return res.status(400).json({ error: error.message });
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
app.use((req, res) => {
|
|
343
|
+
res.status(404).json({ error: "Route not found." });
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return app;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = { createApp };
|