feat: add Layer 2 - WhatsApp Core logic, API Gateway models/auth, Frontend core
WhatsApp Core: - SessionManager with Baileys integration for multi-account support - Express server with REST API and Socket.IO for real-time events - Session lifecycle management (create, disconnect, delete) - Message sending with support for text, image, document, audio, video API Gateway: - Database models: User, WhatsAppAccount, Contact, Conversation, Message - JWT authentication with access/refresh tokens - Auth endpoints: login, refresh, register, me - Pydantic schemas for request/response validation Frontend: - React 18 app structure with routing - Zustand auth store with persistence - API client with automatic token handling - Base CSS and TypeScript declarations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
99
services/whatsapp-core/src/api/routes.ts
Normal file
99
services/whatsapp-core/src/api/routes.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { SessionManager } from '../sessions/SessionManager';
|
||||
|
||||
export function createRouter(sessionManager: SessionManager): Router {
|
||||
const router = Router();
|
||||
|
||||
// Health check
|
||||
router.get('/health', (req: Request, res: Response) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Create new session
|
||||
router.post('/sessions', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { accountId, name } = req.body;
|
||||
if (!accountId || !name) {
|
||||
return res.status(400).json({ error: 'accountId and name required' });
|
||||
}
|
||||
const session = await sessionManager.createSession(accountId, name);
|
||||
res.json(session);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get session info
|
||||
router.get('/sessions/:accountId', (req: Request, res: Response) => {
|
||||
const session = sessionManager.getSession(req.params.accountId);
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
res.json(session);
|
||||
});
|
||||
|
||||
// Get all sessions
|
||||
router.get('/sessions', (req: Request, res: Response) => {
|
||||
const sessions = sessionManager.getAllSessions();
|
||||
res.json(sessions);
|
||||
});
|
||||
|
||||
// Disconnect session
|
||||
router.post('/sessions/:accountId/disconnect', async (req: Request, res: Response) => {
|
||||
try {
|
||||
await sessionManager.disconnectSession(req.params.accountId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete session
|
||||
router.delete('/sessions/:accountId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
await sessionManager.deleteSession(req.params.accountId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// Send message
|
||||
router.post('/sessions/:accountId/messages', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { to, type, content } = req.body;
|
||||
if (!to || !content) {
|
||||
return res.status(400).json({ error: 'to and content required' });
|
||||
}
|
||||
|
||||
let messageContent: any;
|
||||
switch (type) {
|
||||
case 'image':
|
||||
messageContent = { image: { url: content.url }, caption: content.caption };
|
||||
break;
|
||||
case 'document':
|
||||
messageContent = { document: { url: content.url }, fileName: content.filename };
|
||||
break;
|
||||
case 'audio':
|
||||
messageContent = { audio: { url: content.url } };
|
||||
break;
|
||||
case 'video':
|
||||
messageContent = { video: { url: content.url }, caption: content.caption };
|
||||
break;
|
||||
default:
|
||||
messageContent = { text: content.text || content };
|
||||
}
|
||||
|
||||
const result = await sessionManager.sendMessage(
|
||||
req.params.accountId,
|
||||
to,
|
||||
messageContent
|
||||
);
|
||||
res.json({ success: true, messageId: result?.key.id });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: (error as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
74
services/whatsapp-core/src/index.ts
Normal file
74
services/whatsapp-core/src/index.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import { Server as SocketIOServer } from 'socket.io';
|
||||
import { SessionManager } from './sessions/SessionManager';
|
||||
import { createRouter } from './api/routes';
|
||||
import pino from 'pino';
|
||||
|
||||
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
|
||||
const PORT = parseInt(process.env.WS_PORT || '3001', 10);
|
||||
const API_GATEWAY_URL = process.env.API_GATEWAY_URL || 'http://localhost:8000';
|
||||
|
||||
async function main() {
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
|
||||
const io = new SocketIOServer(httpServer, {
|
||||
cors: {
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST'],
|
||||
},
|
||||
path: '/ws',
|
||||
});
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
const sessionManager = new SessionManager('./sessions');
|
||||
const router = createRouter(sessionManager);
|
||||
app.use('/api', router);
|
||||
|
||||
// Forward events to API Gateway and connected clients
|
||||
sessionManager.on('session_event', async (event) => {
|
||||
logger.info({ event }, 'Session event');
|
||||
|
||||
// Emit to Socket.IO clients
|
||||
io.emit(event.type, event);
|
||||
|
||||
// Forward to API Gateway
|
||||
try {
|
||||
await fetch(`${API_GATEWAY_URL}/api/internal/whatsapp/event`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(event),
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Failed to forward event to API Gateway');
|
||||
}
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
logger.info({ socketId: socket.id }, 'Client connected');
|
||||
|
||||
socket.on('subscribe', (accountId: string) => {
|
||||
socket.join(`account:${accountId}`);
|
||||
logger.info({ socketId: socket.id, accountId }, 'Client subscribed');
|
||||
});
|
||||
|
||||
socket.on('unsubscribe', (accountId: string) => {
|
||||
socket.leave(`account:${accountId}`);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
logger.info({ socketId: socket.id }, 'Client disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
logger.info({ port: PORT }, 'WhatsApp Core server started');
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
logger.error({ error }, 'Failed to start server');
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user