@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +319 -0
  4. package/bin/openwa.js +11 -0
  5. package/favicon.ico +0 -0
  6. package/logo-long.png +0 -0
  7. package/logo-square.png +0 -0
  8. package/package.json +69 -0
  9. package/prisma/schema.prisma +182 -0
  10. package/server/config.js +29 -0
  11. package/server/database/client.js +11 -0
  12. package/server/database/init.js +28 -0
  13. package/server/express/create-app.js +349 -0
  14. package/server/express/openapi.js +853 -0
  15. package/server/index.js +163 -0
  16. package/server/services/api-key-service.js +131 -0
  17. package/server/services/auth-service.js +162 -0
  18. package/server/services/chat-service.js +1014 -0
  19. package/server/services/session-service.js +81 -0
  20. package/server/socket/register.js +127 -0
  21. package/server/utils/avatar.js +34 -0
  22. package/server/utils/paths.js +29 -0
  23. package/server/whatsapp/adapters/mock-adapter.js +47 -0
  24. package/server/whatsapp/adapters/wwebjs-adapter.js +263 -0
  25. package/server/whatsapp/session-manager.js +356 -0
  26. package/web/components/AppHead.js +14 -0
  27. package/web/components/AuthCard.js +170 -0
  28. package/web/components/BrandLogo.js +11 -0
  29. package/web/components/ChatWindow.js +875 -0
  30. package/web/components/ChatWindow.js.tmp +0 -0
  31. package/web/components/ContactList.js +97 -0
  32. package/web/components/ContactsPanel.js +90 -0
  33. package/web/components/EmojiPicker.js +108 -0
  34. package/web/components/MediaPreviewModal.js +146 -0
  35. package/web/components/MessageActionMenu.js +155 -0
  36. package/web/components/SessionSidebar.js +167 -0
  37. package/web/components/SettingsModal.js +266 -0
  38. package/web/components/Skeletons.js +73 -0
  39. package/web/jsconfig.json +10 -0
  40. package/web/lib/api.js +33 -0
  41. package/web/lib/socket.js +9 -0
  42. package/web/pages/_app.js +5 -0
  43. package/web/pages/dashboard.js +541 -0
  44. package/web/pages/index.js +62 -0
  45. package/web/postcss.config.js +10 -0
  46. package/web/public/favicon.ico +0 -0
  47. package/web/public/logo-long.png +0 -0
  48. package/web/public/logo-square.png +0 -0
  49. package/web/store/useAppStore.js +209 -0
  50. package/web/styles/globals.css +52 -0
  51. 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 };