primer commit
This commit is contained in:
7
gateway/.gitignore
vendored
Normal file
7
gateway/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.env.local
|
||||
.env.*.local
|
||||
auth_info/*.json
|
||||
!auth_info/.gitkeep
|
||||
18
gateway/Dockerfile
Normal file
18
gateway/Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
||||
# Build
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 3001
|
||||
EXPOSE 3003
|
||||
|
||||
# Command
|
||||
CMD ["npm", "start"]
|
||||
5118
gateway/package-lock.json
generated
Normal file
5118
gateway/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
gateway/package.json
Normal file
35
gateway/package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "whatsapp-gateway",
|
||||
"version": "1.0.0",
|
||||
"description": "Professional WhatsApp Gateway with Baileys",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"lint": "eslint src --ext .ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"pino": "^8.16.2",
|
||||
"pino-pretty": "^10.2.3",
|
||||
"qrcode": "^1.5.3",
|
||||
"ws": "^8.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.15",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/node": "^20.8.7",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/ws": "^8.5.8",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.0",
|
||||
"@typescript-eslint/parser": "^6.9.0",
|
||||
"eslint": "^8.52.0",
|
||||
"tsx": "^4.1.4",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
108
gateway/src/api/messages.ts
Normal file
108
gateway/src/api/messages.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { logger } from '../config/logger';
|
||||
import { ApiResponse } from './send';
|
||||
|
||||
interface MessageData {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
type: 'text' | 'image' | 'document';
|
||||
}
|
||||
|
||||
interface RecentMessages {
|
||||
messages: MessageData[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export class MessagesController {
|
||||
private recentMessages: MessageData[] = [];
|
||||
private maxMessages = 50;
|
||||
|
||||
constructor() {
|
||||
// Setup message storage
|
||||
}
|
||||
|
||||
addMessage = (messageData: MessageData): void => {
|
||||
this.recentMessages.unshift(messageData);
|
||||
|
||||
if (this.recentMessages.length > this.maxMessages) {
|
||||
this.recentMessages = this.recentMessages.slice(0, this.maxMessages);
|
||||
}
|
||||
|
||||
logger.info(`Message added to recent messages: ${messageData.id}`);
|
||||
};
|
||||
|
||||
getRecentMessages = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
|
||||
const response: ApiResponse<RecentMessages> = {
|
||||
success: true,
|
||||
data: {
|
||||
messages: this.recentMessages.slice(0, limit),
|
||||
total: this.recentMessages.length
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.error(`Get recent messages failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
|
||||
clearMessages = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
this.recentMessages = [];
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: { message: 'Messages cleared successfully' },
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.info('Recent messages cleared');
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.error(`Clear messages failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
|
||||
getMessageCount = (): number => {
|
||||
return this.recentMessages.length;
|
||||
};
|
||||
|
||||
// Método para ser llamado desde el Gateway cuando llega un mensaje
|
||||
handleMessage = (message: any): void => {
|
||||
try {
|
||||
const messageData: MessageData = {
|
||||
id: message.key.id || Date.now().toString(),
|
||||
from: message.key.remoteJid || 'unknown',
|
||||
to: 'me',
|
||||
content: message.message?.conversation || message.message?.extendedTextMessage?.text || '(No text content)',
|
||||
timestamp: new Date().toISOString(),
|
||||
type: message.message?.conversation ? 'text' : 'document'
|
||||
};
|
||||
|
||||
this.addMessage(messageData);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to process message: ${error}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
228
gateway/src/api/n8n.ts
Normal file
228
gateway/src/api/n8n.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { logger } from '../config/logger';
|
||||
import { AuthMiddleware } from '../middleware/auth';
|
||||
|
||||
interface n8nSendMessageRequest {
|
||||
to: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface n8nMessageResponse {
|
||||
success: boolean;
|
||||
messageId?: string;
|
||||
to: string;
|
||||
message: string;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class n8nController {
|
||||
private whatsappClient: any;
|
||||
|
||||
constructor(whatsappClient: any) {
|
||||
this.whatsappClient = whatsappClient;
|
||||
}
|
||||
|
||||
validatePhoneNumber(phone: string): { valid: boolean; formatted: string } {
|
||||
// Allow Group JIDs
|
||||
if (phone.endsWith('@g.us')) {
|
||||
return { valid: true, formatted: phone };
|
||||
}
|
||||
|
||||
// Remove non-numeric characters
|
||||
let cleaned = phone.replace(/[^\d]/g, '');
|
||||
|
||||
// Basic validation for different formats
|
||||
if (cleaned.length < 10) {
|
||||
return { valid: false, formatted: phone };
|
||||
}
|
||||
|
||||
// Ensure country code (if starting with 0, remove it and add country code if needed)
|
||||
if (cleaned.startsWith('0')) {
|
||||
cleaned = cleaned.substring(1);
|
||||
}
|
||||
|
||||
// Add country code if not present (assuming El Salvador by default: 503)
|
||||
if (cleaned.length === 8 && !cleaned.startsWith('503')) {
|
||||
cleaned = '503' + cleaned;
|
||||
}
|
||||
|
||||
// Validate reasonable length
|
||||
if (cleaned.length < 10 || cleaned.length > 15) {
|
||||
return { valid: false, formatted: phone };
|
||||
}
|
||||
|
||||
return { valid: true, formatted: cleaned + '@s.whatsapp.net' };
|
||||
}
|
||||
|
||||
getGroups = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const groups = await this.whatsappClient.getGroups();
|
||||
|
||||
const simplifiedGroups = groups.map((g: any) => ({
|
||||
id: g.id,
|
||||
subject: g.subject,
|
||||
description: g.desc,
|
||||
participants: g.participants?.length || 0,
|
||||
creation: g.creation
|
||||
}));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
count: simplifiedGroups.length,
|
||||
groups: simplifiedGroups,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
sendMessage = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { to, message }: n8nSendMessageRequest = req.body;
|
||||
|
||||
if (!to || !message) {
|
||||
const response: n8nMessageResponse = {
|
||||
success: false,
|
||||
to: to || '(missing)',
|
||||
message: message || '(missing)',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'Both "to" and "message" fields are required'
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate phone number
|
||||
const phoneValidation = this.validatePhoneNumber(to);
|
||||
if (!phoneValidation.valid) {
|
||||
const response: n8nMessageResponse = {
|
||||
success: false,
|
||||
to,
|
||||
message: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
|
||||
timestamp: new Date().toISOString(),
|
||||
error: `Invalid phone number format: ${to}`
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check WhatsApp connection
|
||||
const connectionState = this.whatsappClient.getConnectionState();
|
||||
if (connectionState !== 'connected') {
|
||||
const response: n8nMessageResponse = {
|
||||
success: false,
|
||||
to,
|
||||
message: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
|
||||
timestamp: new Date().toISOString(),
|
||||
error: 'WhatsApp not connected. Please scan QR code first.'
|
||||
};
|
||||
res.status(503).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send message
|
||||
const result = await this.whatsappClient.sendMessage(phoneValidation.formatted, message, 'text');
|
||||
|
||||
const response: n8nMessageResponse = {
|
||||
success: true,
|
||||
messageId: result.key?.id || 'unknown',
|
||||
to: to,
|
||||
message: message,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.info(`n8n message sent to ${to}: ${message.substring(0, 50)}...`);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: n8nMessageResponse = {
|
||||
success: false,
|
||||
to: req.body.to || '(unknown)',
|
||||
message: req.body.message || '(unknown)',
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
|
||||
logger.error(`n8n send message failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
|
||||
getStatus = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const connectionState = this.whatsappClient.getConnectionState();
|
||||
const sessionId = this.whatsappClient.sessionStore?.['sessionId'] || 'default';
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
status: connectionState,
|
||||
sessionId,
|
||||
uptime: process.uptime(),
|
||||
timestamp: new Date().toISOString(),
|
||||
capabilities: {
|
||||
send: connectionState === 'connected',
|
||||
receive: true,
|
||||
webhook: true
|
||||
}
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
generateToken = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const permissions = ['send', 'status', 'messages'];
|
||||
const token = AuthMiddleware.generateToken(permissions);
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
token: token,
|
||||
permissions: permissions,
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.info('n8n token generated');
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
validateToken = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const token = (req as any).token;
|
||||
const tokenInfo = AuthMiddleware.getTokenInfo(token);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
token: token,
|
||||
valid: tokenInfo ? !tokenInfo.expired : false,
|
||||
info: tokenInfo,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
110
gateway/src/api/send.ts
Normal file
110
gateway/src/api/send.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { logger } from '../config/logger';
|
||||
|
||||
export interface SendMessageRequest {
|
||||
jid: string;
|
||||
content: string;
|
||||
type?: 'text' | 'image' | 'document';
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export class SendController {
|
||||
private whatsappClient: any;
|
||||
|
||||
constructor(whatsappClient: any) {
|
||||
this.whatsappClient = whatsappClient;
|
||||
}
|
||||
|
||||
sendMessage = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { jid, content, type = 'text' }: SendMessageRequest = req.body;
|
||||
|
||||
if (!jid || !content) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: 'jid and content are required',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.whatsappClient.sendMessage(jid, content, type);
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: { messageId: result.key.id, status: 'sent' },
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.info(`Message sent to ${jid}`);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.error(`Send message failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
|
||||
sendBulk = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { messages }: { messages: SendMessageRequest[] } = req.body;
|
||||
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: 'messages array is required',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
res.status(400).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
messages.map(msg => this.whatsappClient.sendMessage(msg.jid, msg.content, msg.type))
|
||||
);
|
||||
|
||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: {
|
||||
total: messages.length,
|
||||
successful,
|
||||
failed,
|
||||
results: results.map((r, i) => ({
|
||||
index: i,
|
||||
status: r.status,
|
||||
data: r.status === 'fulfilled' ? r.value.key.id : null,
|
||||
error: r.status === 'rejected' ? r.reason : null
|
||||
}))
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.info(`Bulk send completed: ${successful}/${messages.length} successful`);
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.error(`Bulk send failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
}
|
||||
106
gateway/src/api/session.ts
Normal file
106
gateway/src/api/session.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { logger } from '../config/logger';
|
||||
import { ApiResponse } from './send';
|
||||
|
||||
export class SessionController {
|
||||
private whatsappClient: any;
|
||||
|
||||
constructor(whatsappClient: any) {
|
||||
this.whatsappClient = whatsappClient;
|
||||
}
|
||||
|
||||
restartSession = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
logger.info('Restarting WhatsApp session');
|
||||
|
||||
// Disconnect current session
|
||||
await this.whatsappClient.disconnect();
|
||||
|
||||
// Delete session credentials
|
||||
this.whatsappClient.sessionStore?.delete();
|
||||
|
||||
// Reconnect after a short delay
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.whatsappClient.connect();
|
||||
logger.info('Session restarted successfully');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to restart session: ${error}`);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: { message: 'Session restart initiated' },
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.error(`Session restart failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
|
||||
logoutSession = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
logger.info('Logging out WhatsApp session');
|
||||
|
||||
await this.whatsappClient.disconnect();
|
||||
this.whatsappClient.sessionStore?.delete();
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: { message: 'Session logged out successfully' },
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.error(`Session logout failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
|
||||
generateToken = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const token = Buffer.from(`${Date.now()}-${Math.random().toString(36).substring(2)}`).toString('base64');
|
||||
|
||||
const tokenData = {
|
||||
token,
|
||||
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
|
||||
permissions: ['send', 'bulk', 'status', 'messages']
|
||||
};
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: tokenData,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.info('API token generated');
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.error(`Token generation failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
}
|
||||
60
gateway/src/api/status.ts
Normal file
60
gateway/src/api/status.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { logger } from '../config/logger';
|
||||
import { ApiResponse } from './send';
|
||||
|
||||
export class StatusController {
|
||||
private whatsappClient: any;
|
||||
|
||||
constructor(whatsappClient: any) {
|
||||
this.whatsappClient = whatsappClient;
|
||||
}
|
||||
|
||||
getStatus = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const connectionState = this.whatsappClient.getConnectionState();
|
||||
const sessionId = this.whatsappClient.sessionStore?.['sessionId'] || 'default';
|
||||
|
||||
const response: ApiResponse = {
|
||||
success: true,
|
||||
data: {
|
||||
status: connectionState,
|
||||
sessionId,
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
timestamp: new Date().toISOString()
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse = {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
logger.error(`Get status failed: ${error}`);
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
|
||||
getHealth = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const isHealthy = this.whatsappClient.getConnectionState() === 'connected';
|
||||
|
||||
res.status(isHealthy ? 200 : 503).json({
|
||||
healthy: isHealthy,
|
||||
status: isHealthy ? 'ok' : 'degraded',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(503).json({
|
||||
healthy: false,
|
||||
status: 'unhealthy',
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
151
gateway/src/api/webhook.ts
Normal file
151
gateway/src/api/webhook.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { logger } from '../config/logger';
|
||||
|
||||
interface WebhookMessage {
|
||||
from: string;
|
||||
body: string;
|
||||
timestamp: string;
|
||||
type: 'text' | 'image' | 'document';
|
||||
messageId: string;
|
||||
}
|
||||
|
||||
interface WebhookResponse {
|
||||
success: boolean;
|
||||
received: boolean;
|
||||
timestamp: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class WebhookController {
|
||||
private webhookUrl?: string;
|
||||
private enabled: boolean = false;
|
||||
|
||||
constructor(webhookUrl?: string) {
|
||||
this.webhookUrl = webhookUrl;
|
||||
this.enabled = !!webhookUrl;
|
||||
}
|
||||
|
||||
configureWebhook = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { url, enabled = true } = req.body;
|
||||
|
||||
if (url) {
|
||||
this.webhookUrl = url;
|
||||
this.enabled = enabled;
|
||||
|
||||
// Test webhook
|
||||
const testPayload = {
|
||||
event: 'webhook_configured',
|
||||
timestamp: new Date().toISOString(),
|
||||
message: 'Webhook configured successfully'
|
||||
};
|
||||
|
||||
await this.sendWebhook(testPayload);
|
||||
}
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
webhookUrl: this.webhookUrl,
|
||||
enabled: this.enabled,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
receiveMessage = async (messageData: any): Promise<void> => {
|
||||
if (!this.enabled || !this.webhookUrl) {
|
||||
logger.info('Webhook not configured, skipping message forwarding');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const webhookMessage: WebhookMessage = {
|
||||
from: messageData.from || 'unknown',
|
||||
body: messageData.content || messageData.message || '',
|
||||
timestamp: messageData.timestamp || new Date().toISOString(),
|
||||
type: messageData.type || 'text',
|
||||
messageId: messageData.id || Date.now().toString()
|
||||
};
|
||||
|
||||
await this.sendWebhook(webhookMessage);
|
||||
logger.info(`Message forwarded to webhook: ${webhookMessage.from}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to forward message to webhook: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
private async sendWebhook(payload: any): Promise<void> {
|
||||
const response = await fetch(this.webhookUrl!, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'WhatsApp-Gateway-Webhook/1.0'
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Webhook failed: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
|
||||
testWebhook = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
if (!this.enabled || !this.webhookUrl) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'Webhook not configured',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const testPayload = {
|
||||
event: 'test',
|
||||
from: '50312345678@s.whatsapp.net',
|
||||
body: 'Test message from WhatsApp Gateway',
|
||||
timestamp: new Date().toISOString(),
|
||||
type: 'text',
|
||||
messageId: 'test-' + Date.now()
|
||||
};
|
||||
|
||||
await this.sendWebhook(testPayload);
|
||||
|
||||
const response: WebhookResponse = {
|
||||
success: true,
|
||||
received: true,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: WebhookResponse = {
|
||||
success: false,
|
||||
received: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
|
||||
res.status(500).json(response);
|
||||
}
|
||||
};
|
||||
|
||||
getWebhookStatus = async (req: Request, res: Response): Promise<void> => {
|
||||
const response = {
|
||||
success: true,
|
||||
configured: this.enabled,
|
||||
webhookUrl: this.webhookUrl || null,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
};
|
||||
}
|
||||
13
gateway/src/config/logger.ts
Normal file
13
gateway/src/config/logger.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import pino from 'pino';
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
transport: {
|
||||
target: 'pino-pretty',
|
||||
options: {
|
||||
colorize: true,
|
||||
translateTime: 'HH:MM:ss Z',
|
||||
ignore: 'pid,hostname',
|
||||
},
|
||||
},
|
||||
});
|
||||
293
gateway/src/index.ts
Normal file
293
gateway/src/index.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import express from 'express';
|
||||
//import cors from 'cors';
|
||||
import dotenv from 'dotenv';
|
||||
import { WhatsAppClient } from './whatsapp/client';
|
||||
import { QRSocketServer } from './sockets/qr.socket';
|
||||
import { SendController } from './api/send';
|
||||
import { StatusController } from './api/status';
|
||||
import { SessionController } from './api/session';
|
||||
import { MessagesController } from './api/messages';
|
||||
import { n8nController } from './api/n8n';
|
||||
import { WebhookController } from './api/webhook';
|
||||
import { AuthMiddleware } from './middleware/auth';
|
||||
import { logger } from './config/logger';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
class WhatsAppGateway {
|
||||
private app: express.Application;
|
||||
private whatsappClient!: WhatsAppClient;
|
||||
private socketServer!: QRSocketServer;
|
||||
private sendController!: SendController;
|
||||
private statusController!: StatusController;
|
||||
private sessionController!: SessionController;
|
||||
private messagesController!: MessagesController;
|
||||
private n8nController!: n8nController;
|
||||
private webhookController!: WebhookController;
|
||||
|
||||
constructor() {
|
||||
this.app = express();
|
||||
this.setupMiddleware();
|
||||
this.initializeComponents();
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
private setupMiddleware(): void {
|
||||
// Configure CORS for n8n and Manager
|
||||
const corsOrigins = [
|
||||
...(process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(',') : ['http://localhost:3002']),
|
||||
'http://localhost:3004', // Add 3004 for current setup
|
||||
'http://localhost:5678', // n8n default
|
||||
'http://localhost:5679', // n8n alternative
|
||||
...(process.env.N8N_ORIGINS ? process.env.N8N_ORIGINS.split(',') : [])
|
||||
];
|
||||
|
||||
// ⚠️ SOLUCIÓN BRUTA — SOLO DEV
|
||||
this.app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
// Intercepta preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
/* this.app.use(cors({
|
||||
origin: corsOrigins,
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}));*/
|
||||
this.app.use(express.json({ limit: '10mb' }));
|
||||
this.app.use(express.urlencoded({ extended: true }));
|
||||
}
|
||||
|
||||
private initializeComponents(): void {
|
||||
const sessionId = process.env.SESSION_ID || 'default';
|
||||
|
||||
this.whatsappClient = new WhatsAppClient(sessionId);
|
||||
this.socketServer = new QRSocketServer(3003);
|
||||
this.sendController = new SendController(this.whatsappClient);
|
||||
this.statusController = new StatusController(this.whatsappClient);
|
||||
this.sessionController = new SessionController(this.whatsappClient);
|
||||
this.messagesController = new MessagesController();
|
||||
this.n8nController = new n8nController(this.whatsappClient);
|
||||
this.webhookController = new WebhookController(process.env.WEBHOOK_URL);
|
||||
|
||||
this.setupWhatsAppEvents();
|
||||
this.connectWhatsApp();
|
||||
this.setupWebSocketCommands();
|
||||
|
||||
// Send current state when new clients connect
|
||||
this.socketServer.onClientConnect = () => {
|
||||
const currentState = this.whatsappClient.getConnectionState();
|
||||
this.socketServer.sendStatus(currentState);
|
||||
};
|
||||
}
|
||||
|
||||
private setupWebSocketCommands(): void {
|
||||
// Handle WebSocket commands from Manager
|
||||
this.socketServer.onCommand = async (command: string, data?: any) => {
|
||||
logger.info(`Received WebSocket command: ${command}`);
|
||||
|
||||
switch (command) {
|
||||
case 'restart_session':
|
||||
await this.sessionController.restartSession({} as any, { json: () => {} } as any);
|
||||
break;
|
||||
case 'logout_session':
|
||||
await this.sessionController.logoutSession({} as any, { json: () => {} } as any);
|
||||
break;
|
||||
case 'generate_token':
|
||||
const tokenResponse = await this.sessionController.generateToken({} as any, { json: (data: any) => {
|
||||
this.socketServer.broadcast({
|
||||
type: 'token_generated',
|
||||
data: JSON.stringify(data)
|
||||
});
|
||||
} } as any);
|
||||
break;
|
||||
case 'get_recent_messages':
|
||||
await this.messagesController.getRecentMessages({} as any, { json: (data: any) => {
|
||||
this.socketServer.broadcast({
|
||||
type: 'status',
|
||||
data: JSON.stringify(data)
|
||||
});
|
||||
} } as any);
|
||||
break;
|
||||
default:
|
||||
logger.warn(`Unknown WebSocket command: ${command}`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private setupWhatsAppEvents(): void {
|
||||
this.whatsappClient.onQR((qr: string) => {
|
||||
this.socketServer.sendQR(qr);
|
||||
});
|
||||
|
||||
this.whatsappClient.onStatus((status: string) => {
|
||||
this.socketServer.sendStatus(status);
|
||||
logger.info(`WhatsApp status: ${status}`);
|
||||
});
|
||||
|
||||
this.whatsappClient.onMessage(async (message: any) => {
|
||||
try {
|
||||
logger.info('Message received via WhatsApp');
|
||||
|
||||
if (!message || !message.key) {
|
||||
logger.warn('Invalid message structure received');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store in recent messages
|
||||
this.messagesController.handleMessage(message);
|
||||
|
||||
// Forward to webhook
|
||||
await this.webhookController.receiveMessage({
|
||||
id: message.key.id || Date.now().toString(),
|
||||
from: message.key.remoteJid || 'unknown',
|
||||
content: message.message?.conversation || message.message?.extendedTextMessage?.text || '',
|
||||
type: message.message?.conversation ? 'text' : 'document',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Notify Manager
|
||||
this.socketServer.broadcast({
|
||||
type: 'status',
|
||||
data: 'message_received'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to process message: ${error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async connectWhatsApp(): Promise<void> {
|
||||
try {
|
||||
await this.whatsappClient.connect();
|
||||
logger.info('WhatsApp client initialized');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to initialize WhatsApp client: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private setupRoutes(): void {
|
||||
// Legacy API Routes (Manager)
|
||||
this.app.post('/api/send', this.sendController.sendMessage);
|
||||
this.app.post('/api/send/bulk', this.sendController.sendBulk);
|
||||
this.app.get('/api/status', this.statusController.getStatus);
|
||||
this.app.get('/api/health', this.statusController.getHealth);
|
||||
|
||||
// Session management (Manager)
|
||||
this.app.post('/api/session/restart', this.sessionController.restartSession);
|
||||
this.app.post('/api/session/logout', this.sessionController.logoutSession);
|
||||
this.app.post('/api/token', this.sessionController.generateToken);
|
||||
|
||||
// Messages (Manager)
|
||||
this.app.get('/api/messages', this.messagesController.getRecentMessages);
|
||||
this.app.delete('/api/messages', this.messagesController.clearMessages);
|
||||
|
||||
// n8n API Core (Component 3)
|
||||
this.app.post('/api/messages/send', AuthMiddleware.middleware('send'), this.n8nController.sendMessage);
|
||||
this.app.get('/api/groups', AuthMiddleware.middleware('messages'), this.n8nController.getGroups);
|
||||
this.app.get('/api/status', AuthMiddleware.middleware('status'), this.n8nController.getStatus);
|
||||
this.app.post('/api/n8n/token', AuthMiddleware.middleware('status'), this.n8nController.validateToken);
|
||||
|
||||
// Public n8n token generation (no auth required)
|
||||
this.app.post('/api/n8n/generate-token', this.n8nController.generateToken);
|
||||
|
||||
// Webhooks for n8n (receiving messages)
|
||||
this.app.post('/webhook/whatsapp', (req, res) => {
|
||||
// This endpoint is for n8n to receive messages FROM WhatsApp
|
||||
res.json({ success: true, message: 'Webhook endpoint active' });
|
||||
});
|
||||
|
||||
// Webhook configuration
|
||||
this.app.post('/api/webhook/configure', AuthMiddleware.middleware('messages'), this.webhookController.configureWebhook);
|
||||
this.app.get('/api/webhook/status', AuthMiddleware.middleware('messages'), this.webhookController.getWebhookStatus);
|
||||
this.app.post('/api/webhook/test', AuthMiddleware.middleware('messages'), this.webhookController.testWebhook);
|
||||
|
||||
// Root endpoint
|
||||
this.app.get('/', (req, res) => {
|
||||
res.json({
|
||||
name: 'WhatsApp Gateway',
|
||||
version: '1.0.0',
|
||||
status: 'running',
|
||||
endpoints: {
|
||||
// Manager endpoints
|
||||
send: 'POST /api/send',
|
||||
sendBulk: 'POST /api/send/bulk',
|
||||
status: 'GET /api/status',
|
||||
health: 'GET /api/health',
|
||||
sessionRestart: 'POST /api/session/restart',
|
||||
sessionLogout: 'POST /api/session/logout',
|
||||
token: 'POST /api/token',
|
||||
messages: 'GET /api/messages',
|
||||
websocket: 'ws://localhost:3003',
|
||||
|
||||
// n8n API Core endpoints
|
||||
n8nSend: 'POST /api/messages/send (Bearer auth required)',
|
||||
n8nStatus: 'GET /api/status (Bearer auth required)',
|
||||
n8nToken: 'POST /api/n8n/generate-token',
|
||||
webhook: 'POST /webhook/whatsapp',
|
||||
webhookConfig: 'POST /api/webhook/configure',
|
||||
webhookTest: 'POST /api/webhook/test'
|
||||
},
|
||||
authentication: {
|
||||
type: 'Bearer Token',
|
||||
header: 'Authorization: Bearer <token>',
|
||||
generate: 'POST /api/n8n/generate-token'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling
|
||||
this.app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.error(`Unhandled error: ${err}`);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const port = parseInt(process.env.PORT || '3001');
|
||||
|
||||
this.app.listen(port, () => {
|
||||
logger.info(`WhatsApp Gateway API running on port ${port}`);
|
||||
logger.info(`WebSocket server running on port 3003`);
|
||||
logger.info(`Manager Web should connect to: http://localhost:3002`);
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.whatsappClient.disconnect();
|
||||
this.socketServer.close();
|
||||
logger.info('WhatsApp Gateway stopped');
|
||||
}
|
||||
}
|
||||
|
||||
// Start the gateway
|
||||
const gateway = new WhatsAppGateway();
|
||||
gateway.start().catch(error => {
|
||||
logger.error(`Failed to start gateway: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGINT', async () => {
|
||||
logger.info('Received SIGINT, shutting down gracefully...');
|
||||
await gateway.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async () => {
|
||||
logger.info('Received SIGTERM, shutting down gracefully...');
|
||||
await gateway.stop();
|
||||
process.exit(0);
|
||||
});
|
||||
130
gateway/src/middleware/auth.ts
Normal file
130
gateway/src/middleware/auth.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { logger } from '../config/logger';
|
||||
|
||||
interface TokenStore {
|
||||
[token: string]: {
|
||||
createdAt: string;
|
||||
expiresAt: string;
|
||||
permissions: string[];
|
||||
};
|
||||
}
|
||||
|
||||
// In-memory token store (en producción usar Redis/DB)
|
||||
const tokenStore: TokenStore = {};
|
||||
|
||||
export class AuthMiddleware {
|
||||
static generateToken(permissions: string[] = ['send', 'status', 'messages']): string {
|
||||
const token = Buffer.from(`${Date.now()}-${Math.random().toString(36).substring(2)}`).toString('base64');
|
||||
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); // 24 horas
|
||||
|
||||
tokenStore[token] = {
|
||||
createdAt: new Date().toISOString(),
|
||||
expiresAt,
|
||||
permissions
|
||||
};
|
||||
|
||||
logger.info(`Token generated: ${token.substring(0, 20)}...`);
|
||||
return token;
|
||||
}
|
||||
|
||||
static validateToken(token: string): { valid: boolean; permissions?: string[] } {
|
||||
const tokenData = tokenStore[token];
|
||||
|
||||
if (!tokenData) {
|
||||
logger.warn(`Invalid token: ${token.substring(0, 20)}...`);
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
if (new Date() > new Date(tokenData.expiresAt)) {
|
||||
logger.warn(`Expired token: ${token.substring(0, 20)}...`);
|
||||
delete tokenStore[token];
|
||||
return { valid: false };
|
||||
}
|
||||
|
||||
return { valid: true, permissions: tokenData.permissions };
|
||||
}
|
||||
|
||||
static revokeToken(token: string): boolean {
|
||||
if (tokenStore[token]) {
|
||||
delete tokenStore[token];
|
||||
logger.info(`Token revoked: ${token.substring(0, 20)}...`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static getTokenInfo(token: string): any {
|
||||
const tokenData = tokenStore[token];
|
||||
if (!tokenData) return null;
|
||||
|
||||
return {
|
||||
...tokenData,
|
||||
expired: new Date() > new Date(tokenData.expiresAt)
|
||||
};
|
||||
}
|
||||
|
||||
static listTokens(): Array<{ token: string; info: any }> {
|
||||
return Object.entries(tokenStore).map(([token, info]) => ({
|
||||
token: token.substring(0, 20) + '...',
|
||||
info
|
||||
}));
|
||||
}
|
||||
|
||||
static middleware(requiredPermission?: string) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Authorization header required',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Bearer token format: "Bearer xxxxxx"
|
||||
const parts = authHeader.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid authorization format. Expected: Bearer <token>',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
const token = parts[1];
|
||||
const validation = AuthMiddleware.validateToken(token);
|
||||
|
||||
if (!validation.valid) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid or expired token',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
if (requiredPermission && !validation.permissions?.includes(requiredPermission)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: `Insufficient permissions. Required: ${requiredPermission}`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// Add token info to request
|
||||
(req as any).token = token;
|
||||
(req as any).permissions = validation.permissions;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error(`Auth middleware error: ${error}`);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Authentication error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
115
gateway/src/sockets/qr.socket.ts
Normal file
115
gateway/src/sockets/qr.socket.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { WebSocketServer } from 'ws';
|
||||
import * as WebSocket from 'ws';
|
||||
import { logger } from '../config/logger';
|
||||
|
||||
export interface QRSocketMessage {
|
||||
type: 'qr' | 'status' | 'error' | 'token_generated';
|
||||
data: string;
|
||||
}
|
||||
|
||||
export class QRSocketServer {
|
||||
private wss: WebSocketServer;
|
||||
private clients: Set<WebSocket> = new Set();
|
||||
public onCommand?: (command: string, data?: any) => void;
|
||||
public onClientConnect?: () => void;
|
||||
|
||||
constructor(port: number) {
|
||||
this.wss = new WebSocketServer({ port });
|
||||
this.setupServer();
|
||||
}
|
||||
|
||||
private setupServer(): void {
|
||||
this.wss.on('connection', (ws: WebSocket) => {
|
||||
logger.info('WebSocket client connected');
|
||||
this.clients.add(ws);
|
||||
|
||||
ws.on('close', () => {
|
||||
logger.info('WebSocket client disconnected');
|
||||
this.clients.delete(ws);
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
logger.error(`WebSocket error: ${error}`);
|
||||
this.clients.delete(ws);
|
||||
});
|
||||
|
||||
ws.on('message', (data: WebSocket.Data) => {
|
||||
try {
|
||||
const message = JSON.parse(data.toString());
|
||||
if (message.type === 'command' && this.onCommand) {
|
||||
this.onCommand(message.action, message.data);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to parse WebSocket command: ${error}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Send initial status
|
||||
// Note: We send 'waiting' as default, the actual state will be sent by WhatsApp client events
|
||||
this.sendToClient(ws, {
|
||||
type: 'status',
|
||||
data: 'waiting'
|
||||
});
|
||||
|
||||
// Notify that a new client connected
|
||||
if (this.onClientConnect) {
|
||||
this.onClientConnect();
|
||||
}
|
||||
});
|
||||
|
||||
logger.info(`WebSocket server running on port ${this.wss.options.port}`);
|
||||
}
|
||||
|
||||
broadcast(message: QRSocketMessage): void {
|
||||
const messageStr = JSON.stringify(message);
|
||||
|
||||
this.clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
client.send(messageStr);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to send message to client: ${error}`);
|
||||
this.clients.delete(client);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private sendToClient(client: WebSocket, message: QRSocketMessage): void {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
try {
|
||||
client.send(JSON.stringify(message));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to send message to client: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendQR(qr: string): void {
|
||||
logger.info(`Broadcasting QR code to ${this.clients.size} connected clients`);
|
||||
console.log('QR DATA LENGTH:', qr.length);
|
||||
this.broadcast({
|
||||
type: 'qr',
|
||||
data: qr
|
||||
});
|
||||
}
|
||||
|
||||
sendStatus(status: string): void {
|
||||
this.broadcast({
|
||||
type: 'status',
|
||||
data: status
|
||||
});
|
||||
}
|
||||
|
||||
sendError(error: string): void {
|
||||
this.broadcast({
|
||||
type: 'error',
|
||||
data: error
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.wss.close();
|
||||
logger.info('WebSocket server closed');
|
||||
}
|
||||
}
|
||||
184
gateway/src/whatsapp/client.ts
Normal file
184
gateway/src/whatsapp/client.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { makeWASocket, DisconnectReason, useMultiFileAuthState, AuthenticationState } from '@whiskeysockets/baileys';
|
||||
import { Boom } from '@hapi/boom';
|
||||
import { SessionStore, SessionData } from './session.store';
|
||||
import { logger } from '../config/logger';
|
||||
import { join } from 'path';
|
||||
|
||||
export class WhatsAppClient {
|
||||
private client: any;
|
||||
public sessionStore: SessionStore;
|
||||
private qrCallback?: (qr: string) => void;
|
||||
private statusCallback?: (status: string) => void;
|
||||
private messageCallback?: (message: any) => void;
|
||||
private saveCreds?: () => Promise<void>;
|
||||
private isConnected: boolean = false;
|
||||
|
||||
constructor(sessionId: string) {
|
||||
this.sessionStore = new SessionStore(sessionId);
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
try {
|
||||
this.isConnected = false;
|
||||
const sessionId = this.sessionStore['sessionId'] || 'default';
|
||||
const authPath = join(process.cwd(), 'auth_info', sessionId);
|
||||
|
||||
// Use proper Baileys authentication
|
||||
const { state, saveCreds } = await useMultiFileAuthState(authPath);
|
||||
|
||||
const hasCredentials = state.creds?.registered;
|
||||
if (!hasCredentials) {
|
||||
logger.info('No credentials found, initiating new session - QR will be generated');
|
||||
this.statusCallback?.('waiting');
|
||||
} else {
|
||||
logger.info('Existing credentials found, attempting to connect');
|
||||
this.statusCallback?.('connecting');
|
||||
}
|
||||
|
||||
this.client = makeWASocket({
|
||||
auth: state,
|
||||
printQRInTerminal: false,
|
||||
logger: logger.child({ module: 'baileys' }),
|
||||
browser: ['WhatsApp Gateway', 'Chrome', '120.0.0'],
|
||||
mobile: false,
|
||||
syncFullHistory: false,
|
||||
markOnlineOnConnect: false,
|
||||
generateHighQualityLinkPreview: false,
|
||||
});
|
||||
|
||||
// Store saveCreds for later use
|
||||
this.saveCreds = saveCreds;
|
||||
|
||||
this.setupEventHandlers();
|
||||
} catch (error) {
|
||||
logger.error(`Failed to connect: ${error}`);
|
||||
this.statusCallback?.('error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private setupEventHandlers(): void {
|
||||
this.client.ev.on('connection.update', (update: any) => {
|
||||
const { connection, lastDisconnect, qr } = update;
|
||||
|
||||
if (qr) {
|
||||
logger.info('QR code received, broadcasting to clients');
|
||||
console.log('QR CODE GENERATED - Length:', qr.length);
|
||||
if (this.qrCallback) {
|
||||
this.qrCallback(qr);
|
||||
}
|
||||
if (this.statusCallback) {
|
||||
this.statusCallback('qr');
|
||||
}
|
||||
}
|
||||
|
||||
if (connection === 'close') {
|
||||
this.isConnected = false;
|
||||
const shouldReconnect = (lastDisconnect?.error as Boom)?.output?.statusCode !== DisconnectReason.loggedOut;
|
||||
logger.info(`Connection closed. Reconnect: ${shouldReconnect}`);
|
||||
|
||||
if (shouldReconnect) {
|
||||
this.statusCallback?.('reconnecting');
|
||||
setTimeout(() => this.connect(), 5000);
|
||||
} else {
|
||||
this.statusCallback?.('logout');
|
||||
}
|
||||
}
|
||||
|
||||
if (connection === 'open') {
|
||||
logger.info('WhatsApp connection opened');
|
||||
this.isConnected = true;
|
||||
this.statusCallback?.('connected');
|
||||
}
|
||||
|
||||
if (connection === 'connecting') {
|
||||
logger.info('WhatsApp connecting...');
|
||||
this.statusCallback?.('connecting');
|
||||
}
|
||||
});
|
||||
|
||||
this.client.ev.on('creds.update', () => {
|
||||
logger.info('Credentials updated');
|
||||
if (this.saveCreds) {
|
||||
this.saveCreds();
|
||||
}
|
||||
});
|
||||
|
||||
this.client.ev.on('messages.upsert', (event: any) => {
|
||||
const { messages } = event;
|
||||
if (messages && messages.length > 0) {
|
||||
messages.forEach((msg: any) => {
|
||||
logger.info('Message received');
|
||||
this.messageCallback?.(msg);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onQR(callback: (qr: string) => void): void {
|
||||
this.qrCallback = callback;
|
||||
}
|
||||
|
||||
onStatus(callback: (status: string) => void): void {
|
||||
this.statusCallback = callback;
|
||||
}
|
||||
|
||||
onMessage(callback: (message: any) => void): void {
|
||||
this.messageCallback = callback;
|
||||
}
|
||||
|
||||
async sendMessage(jid: string, content: string, type: 'text' | 'image' | 'document' = 'text'): Promise<any> {
|
||||
try {
|
||||
const message = type === 'text'
|
||||
? { text: content }
|
||||
: { caption: content };
|
||||
|
||||
const result = await this.client.sendMessage(jid, message);
|
||||
logger.info(`Message sent to ${jid}, result: ${JSON.stringify(result)}`);
|
||||
|
||||
// Ensure we always return a valid result structure
|
||||
if (!result) {
|
||||
return {
|
||||
key: { id: `unknown-${Date.now()}` },
|
||||
status: 'sent'
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to send message: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async getGroups(): Promise<any[]> {
|
||||
try {
|
||||
if (!this.client) {
|
||||
throw new Error('WhatsApp client not initialized');
|
||||
}
|
||||
|
||||
const groups = await this.client.groupFetchAllParticipating();
|
||||
return Object.values(groups);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch groups: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getConnectionState(): string {
|
||||
return this.isConnected ? 'connected' : 'disconnected';
|
||||
}
|
||||
|
||||
async disconnect(): Promise<void> {
|
||||
try {
|
||||
this.isConnected = false;
|
||||
if (this.client) {
|
||||
this.client.ws?.close();
|
||||
this.client.end?.();
|
||||
}
|
||||
logger.info('WhatsApp client disconnected');
|
||||
} catch (error) {
|
||||
logger.error(`Failed to disconnect: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
35
gateway/src/whatsapp/session.store.ts
Normal file
35
gateway/src/whatsapp/session.store.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { existsSync, rmSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { logger } from '../config/logger';
|
||||
|
||||
export interface SessionData {
|
||||
id: string;
|
||||
creds?: any;
|
||||
keys?: any;
|
||||
}
|
||||
|
||||
export class SessionStore {
|
||||
private sessionId: string;
|
||||
|
||||
constructor(sessionId: string) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
// Baileys now handles session persistence automatically
|
||||
// We only need this for session metadata
|
||||
getAuthPath(): string {
|
||||
return join(process.cwd(), 'auth_info', this.sessionId);
|
||||
}
|
||||
|
||||
delete(): void {
|
||||
try {
|
||||
const authPath = this.getAuthPath();
|
||||
if (existsSync(authPath)) {
|
||||
rmSync(authPath, { recursive: true, force: true });
|
||||
logger.info(`Session deleted for ${this.sessionId}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to delete session: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
gateway/tsconfig.json
Normal file
19
gateway/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user