feat: Major WhatsApp integration update with Odoo and pause/resume

## Frontend
- Add media display (images, audio, video, docs) in Inbox
- Add pause/resume functionality for WhatsApp accounts
- Fix media URLs to use nginx proxy (relative URLs)

## API Gateway
- Add /accounts/:id/pause and /accounts/:id/resume endpoints
- Fix media URL handling for browser access

## WhatsApp Core
- Add pauseSession() - disconnect without logout
- Add resumeSession() - reconnect using saved credentials
- Add media download and storage for incoming messages
- Serve media files via /media/ static route

## Odoo Module (odoo_whatsapp_hub)
- Add Chat Hub interface with DOLLARS theme (dark, 3-column layout)
- Add WhatsApp/DRRR theme switcher for chat view
- Add "ABRIR CHAT" button in conversation form
- Add send_message_from_chat() method
- Add security/ir.model.access.csv
- Fix CSS scoping to avoid breaking Odoo UI
- Update webhook to handle message events properly

## Documentation
- Add docs/CONTEXTO_DESARROLLO.md with complete project context

## Infrastructure
- Add whatsapp_media Docker volume
- Configure nginx proxy for /media/ route
- Update .gitignore to track src/sessions/ source files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude AI
2026-01-30 20:48:56 +00:00
parent 1040debe2e
commit 5dd3499097
33 changed files with 3636 additions and 138 deletions

View File

@@ -17,6 +17,7 @@ class Settings(BaseSettings):
# WhatsApp Core
WHATSAPP_CORE_URL: str = "http://localhost:3001"
WHATSAPP_CORE_PUBLIC_URL: str = "http://localhost:3001" # URL accessible from browser
# Flow Engine
FLOW_ENGINE_URL: str = "http://localhost:8001"
@@ -24,6 +25,9 @@ class Settings(BaseSettings):
# Integrations
INTEGRATIONS_URL: str = "http://localhost:8002"
# Odoo Webhook
ODOO_WEBHOOK_URL: str = "" # e.g., "http://192.168.10.188:8069/whatsapp/webhook"
# CORS
CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000"

View File

@@ -62,11 +62,29 @@ async def create_account(
@router.get("/accounts", response_model=List[WhatsAppAccountResponse])
def list_accounts(
async def list_accounts(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
accounts = db.query(WhatsAppAccount).all()
# Sync status with WhatsApp Core for each account
async with httpx.AsyncClient() as client:
for account in accounts:
try:
response = await client.get(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{account.id}",
timeout=5,
)
if response.status_code == 200:
data = response.json()
account.qr_code = data.get("qrCode")
account.status = AccountStatus(data.get("status", "disconnected"))
account.phone_number = data.get("phoneNumber")
except Exception:
pass
db.commit()
return accounts
@@ -122,6 +140,63 @@ async def delete_account(
return {"success": True}
@router.post("/accounts/{account_id}/pause")
async def pause_account(
account_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Pause WhatsApp connection without logging out (preserves session)"""
account = db.query(WhatsAppAccount).filter(WhatsAppAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Account not found")
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{account_id}/pause",
timeout=10,
)
if response.status_code == 200:
account.status = AccountStatus.DISCONNECTED
db.commit()
return {"success": True, "status": "paused"}
else:
raise HTTPException(status_code=500, detail="Failed to pause session")
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=f"Connection error: {str(e)}")
@router.post("/accounts/{account_id}/resume")
async def resume_account(
account_id: UUID,
db: Session = Depends(get_db),
current_user: User = Depends(require_admin),
):
"""Resume paused WhatsApp connection"""
account = db.query(WhatsAppAccount).filter(WhatsAppAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Account not found")
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{account_id}/resume",
timeout=30,
)
if response.status_code == 200:
data = response.json()
session = data.get("session", {})
account.status = AccountStatus(session.get("status", "connecting"))
account.qr_code = session.get("qrCode")
db.commit()
return {"success": True, "status": account.status.value}
else:
raise HTTPException(status_code=500, detail="Failed to resume session")
except httpx.RequestError as e:
raise HTTPException(status_code=500, detail=f"Connection error: {str(e)}")
@router.get("/conversations", response_model=List[ConversationResponse])
def list_conversations(
status: ConversationStatus = None,
@@ -228,6 +303,7 @@ async def handle_whatsapp_event(
elif event.type == "message":
msg_data = event.data
phone = msg_data.get("from", "").split("@")[0]
is_from_me = msg_data.get("fromMe", False)
contact = db.query(Contact).filter(Contact.phone_number == phone).first()
if not contact:
@@ -256,19 +332,47 @@ async def handle_whatsapp_event(
db.refresh(conversation)
wa_message = msg_data.get("message", {})
media_url = msg_data.get("mediaUrl")
media_type = msg_data.get("mediaType", "text")
# Extract text content
content = (
wa_message.get("conversation") or
wa_message.get("extendedTextMessage", {}).get("text") or
"[Media]"
wa_message.get("imageMessage", {}).get("caption") or
wa_message.get("videoMessage", {}).get("caption") or
wa_message.get("documentMessage", {}).get("fileName") or
""
)
# Map media type to MessageType
type_mapping = {
"text": MessageType.TEXT,
"image": MessageType.IMAGE,
"audio": MessageType.AUDIO,
"video": MessageType.VIDEO,
"document": MessageType.DOCUMENT,
"sticker": MessageType.IMAGE,
}
msg_type = type_mapping.get(media_type, MessageType.TEXT)
# Build full media URL if present (use relative URL for browser access via nginx proxy)
full_media_url = None
if media_url:
# Use relative URL that nginx will proxy to whatsapp-core
full_media_url = media_url # e.g., "/media/uuid.jpg"
# Set direction based on fromMe flag
direction = MessageDirection.OUTBOUND if is_from_me else MessageDirection.INBOUND
message = Message(
conversation_id=conversation.id,
whatsapp_message_id=msg_data.get("id"),
direction=MessageDirection.INBOUND,
type=MessageType.TEXT,
content=content,
status=MessageStatus.DELIVERED,
direction=direction,
type=msg_type,
content=content if content else f"[{media_type.capitalize()}]",
media_url=full_media_url,
status=MessageStatus.DELIVERED if not is_from_me else MessageStatus.SENT,
)
db.add(message)
@@ -277,8 +381,8 @@ async def handle_whatsapp_event(
db.commit()
db.refresh(message)
# Process message through Flow Engine (if in BOT status)
if conversation.status == ConversationStatus.BOT:
# Process message through Flow Engine (only for inbound messages in BOT status)
if not is_from_me and conversation.status == ConversationStatus.BOT:
async with httpx.AsyncClient() as client:
try:
await client.post(
@@ -305,8 +409,53 @@ async def handle_whatsapp_event(
except Exception as e:
print(f"Flow engine error: {e}")
# Send webhook to Odoo if configured
if settings.ODOO_WEBHOOK_URL:
async with httpx.AsyncClient() as client:
try:
await client.post(
settings.ODOO_WEBHOOK_URL,
json={
"type": "message",
"account_id": str(account.id),
"data": {
"id": str(message.id),
"conversation_id": str(conversation.id),
"from": phone,
"contact_name": contact.name,
"content": content,
"type": media_type,
"direction": "outbound" if is_from_me else "inbound",
"media_url": full_media_url,
},
},
timeout=10,
)
except Exception as e:
print(f"Odoo webhook error: {e}")
return {"status": "ok"}
# Send account status to Odoo webhook
if settings.ODOO_WEBHOOK_URL and event.type in ["connected", "disconnected", "qr"]:
async with httpx.AsyncClient() as client:
try:
await client.post(
settings.ODOO_WEBHOOK_URL,
json={
"type": "account_status",
"account_id": str(account.id),
"data": {
"status": account.status.value if account.status else "disconnected",
"phone_number": account.phone_number,
"qr_code": account.qr_code,
},
},
timeout=10,
)
except Exception as e:
print(f"Odoo webhook error: {e}")
db.commit()
return {"status": "ok"}
@@ -448,3 +597,183 @@ def add_internal_note(
db.commit()
db.refresh(message)
return {"success": True, "message_id": str(message.id)}
# ============================================
# Odoo Internal Endpoints (no authentication)
# ============================================
class OdooSendMessageRequest(BaseModel):
phone_number: str
message: str
account_id: str
@router.get("/internal/odoo/accounts/{account_id}")
async def odoo_get_account(
account_id: UUID,
db: Session = Depends(get_db),
):
"""Get account status for Odoo (no auth required)"""
account = db.query(WhatsAppAccount).filter(WhatsAppAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="Account not found")
# Sync with WhatsApp Core
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{account_id}",
timeout=10,
)
if response.status_code == 200:
data = response.json()
account.qr_code = data.get("qrCode")
account.status = AccountStatus(data.get("status", "disconnected"))
account.phone_number = data.get("phoneNumber")
db.commit()
except Exception:
pass
return {
"id": str(account.id),
"phone_number": account.phone_number,
"name": account.name,
"status": account.status.value if account.status else "disconnected",
"qr_code": account.qr_code,
}
@router.get("/internal/odoo/accounts")
async def odoo_list_accounts(
db: Session = Depends(get_db),
):
"""List all accounts for Odoo (no auth required)"""
accounts = db.query(WhatsAppAccount).all()
async with httpx.AsyncClient() as client:
for account in accounts:
try:
response = await client.get(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{account.id}",
timeout=5,
)
if response.status_code == 200:
data = response.json()
account.qr_code = data.get("qrCode")
account.status = AccountStatus(data.get("status", "disconnected"))
account.phone_number = data.get("phoneNumber")
except Exception:
pass
db.commit()
return [
{
"id": str(a.id),
"phone_number": a.phone_number,
"name": a.name,
"status": a.status.value if a.status else "disconnected",
}
for a in accounts
]
@router.post("/internal/odoo/send")
async def odoo_send_message(
request: OdooSendMessageRequest,
db: Session = Depends(get_db),
):
"""Send WhatsApp message from Odoo (no auth required)"""
account = db.query(WhatsAppAccount).filter(
WhatsAppAccount.id == request.account_id
).first()
if not account:
raise HTTPException(status_code=404, detail="Account not found")
# Find or create contact
phone = request.phone_number.replace("+", "").replace(" ", "").replace("-", "")
contact = db.query(Contact).filter(Contact.phone_number == phone).first()
if not contact:
contact = Contact(phone_number=phone)
db.add(contact)
db.commit()
db.refresh(contact)
# Find or create conversation
conversation = db.query(Conversation).filter(
Conversation.whatsapp_account_id == account.id,
Conversation.contact_id == contact.id,
Conversation.status != ConversationStatus.RESOLVED,
).first()
if not conversation:
conversation = Conversation(
whatsapp_account_id=account.id,
contact_id=contact.id,
status=ConversationStatus.BOT,
)
db.add(conversation)
db.commit()
db.refresh(conversation)
# Create message
message = Message(
conversation_id=conversation.id,
direction=MessageDirection.OUTBOUND,
type=MessageType.TEXT,
content=request.message,
status=MessageStatus.PENDING,
)
db.add(message)
db.commit()
db.refresh(message)
# Send via WhatsApp Core
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{settings.WHATSAPP_CORE_URL}/api/sessions/{account.id}/messages",
json={
"to": phone,
"type": "text",
"content": {"text": request.message},
},
timeout=30,
)
if response.status_code == 200:
data = response.json()
message.whatsapp_message_id = data.get("messageId")
message.status = MessageStatus.SENT
else:
message.status = MessageStatus.FAILED
except Exception as e:
message.status = MessageStatus.FAILED
raise HTTPException(status_code=500, detail=f"Failed to send: {e}")
db.commit()
return {"success": True, "message_id": str(message.id)}
@router.get("/internal/odoo/conversations")
def odoo_list_conversations(
account_id: str = None,
db: Session = Depends(get_db),
):
"""List conversations for Odoo (no auth required)"""
query = db.query(Conversation)
if account_id:
query = query.filter(Conversation.whatsapp_account_id == account_id)
conversations = query.order_by(Conversation.last_message_at.desc()).limit(100).all()
return [
{
"id": str(c.id),
"contact_phone": c.contact.phone_number if c.contact else None,
"contact_name": c.contact.name if c.contact else None,
"status": c.status.value if c.status else None,
"last_message_at": c.last_message_at.isoformat() if c.last_message_at else None,
}
for c in conversations
]

View File

@@ -4,7 +4,8 @@ sqlalchemy==2.0.36
alembic==1.14.0
psycopg2-binary==2.9.10
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
passlib==1.7.4
bcrypt==4.0.1
python-multipart==0.0.20
pydantic[email]==2.10.4
pydantic-settings==2.7.1

View File

@@ -10,7 +10,8 @@
"lint": "eslint src/**/*.ts"
},
"dependencies": {
"@whiskeysockets/baileys": "^6.7.16",
"@whiskeysockets/baileys": "^6.7.17",
"cors": "^2.8.5",
"express": "^4.21.2",
"socket.io": "^4.8.1",
"ioredis": "^5.4.1",
@@ -19,6 +20,7 @@
"uuid": "^11.0.5"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/node": "^22.10.7",
"@types/uuid": "^10.0.0",

View File

@@ -50,6 +50,28 @@ export function createRouter(sessionManager: SessionManager): Router {
}
});
// Pause session (disconnect without logout)
router.post('/sessions/:accountId/pause', async (req: Request, res: Response) => {
try {
const accountId = req.params.accountId as string;
await sessionManager.pauseSession(accountId);
res.json({ success: true, status: 'paused' });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
// Resume session
router.post('/sessions/:accountId/resume', async (req: Request, res: Response) => {
try {
const accountId = req.params.accountId as string;
const session = await sessionManager.resumeSession(accountId);
res.json({ success: true, session });
} catch (error) {
res.status(500).json({ error: (error as Error).message });
}
});
// Delete session
router.delete('/sessions/:accountId', async (req: Request, res: Response) => {
try {

View File

@@ -1,4 +1,6 @@
import express from 'express';
import cors from 'cors';
import path from 'path';
import { createServer } from 'http';
import { Server as SocketIOServer } from 'socket.io';
import { SessionManager } from './sessions/SessionManager';
@@ -21,8 +23,12 @@ async function main() {
path: '/ws',
});
app.use(cors());
app.use(express.json());
// Serve media files statically
app.use('/media', express.static(path.join(process.cwd(), 'media')));
const sessionManager = new SessionManager('./sessions');
const router = createRouter(sessionManager);
app.use('/api', router);
@@ -36,11 +42,14 @@ async function main() {
// Forward to API Gateway
try {
await fetch(`${API_GATEWAY_URL}/api/internal/whatsapp/event`, {
const response = await fetch(`${API_GATEWAY_URL}/api/whatsapp/internal/whatsapp/event`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
if (!response.ok) {
logger.error({ status: response.status }, 'API Gateway rejected event');
}
} catch (error) {
logger.error({ error }, 'Failed to forward event to API Gateway');
}

View File

@@ -0,0 +1,319 @@
import makeWASocket, {
DisconnectReason,
useMultiFileAuthState,
WASocket,
proto,
fetchLatestBaileysVersion,
makeCacheableSignalKeyStore,
downloadMediaMessage,
getContentType,
} from '@whiskeysockets/baileys';
import { Boom } from '@hapi/boom';
import * as QRCode from 'qrcode';
import { EventEmitter } from 'events';
import * as path from 'path';
import * as fs from 'fs';
import pino from 'pino';
import { randomUUID } from 'crypto';
import { SessionStore, SessionInfo, SessionEvent } from './types';
const logger = pino({ level: 'info' });
export class SessionManager extends EventEmitter {
private sessions: Map<string, SessionStore> = new Map();
private sessionsPath: string;
private mediaPath: string;
constructor(sessionsPath: string = './sessions') {
super();
this.sessionsPath = sessionsPath;
this.mediaPath = './media';
if (!fs.existsSync(sessionsPath)) {
fs.mkdirSync(sessionsPath, { recursive: true });
}
if (!fs.existsSync(this.mediaPath)) {
fs.mkdirSync(this.mediaPath, { recursive: true });
}
}
async createSession(accountId: string, name: string, phoneNumber?: string): Promise<SessionInfo> {
if (this.sessions.has(accountId)) {
const existing = this.sessions.get(accountId)!;
return existing.info;
}
const sessionPath = path.join(this.sessionsPath, accountId);
const { state, saveCreds } = await useMultiFileAuthState(sessionPath);
const info: SessionInfo = {
accountId,
phoneNumber: null,
name,
status: 'connecting',
qrCode: null,
pairingCode: null,
createdAt: new Date(),
};
const store: SessionStore = { socket: null, info };
this.sessions.set(accountId, store);
// Get latest version
const { version } = await fetchLatestBaileysVersion();
logger.info({ version }, 'Using WA version');
const socket = makeWASocket({
version,
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, logger),
},
logger: pino({ level: 'warn' }),
syncFullHistory: false,
markOnlineOnConnect: false,
browser: ['WhatsApp Central', 'Desktop', '1.0.0'],
connectTimeoutMs: 60000,
retryRequestDelayMs: 2000,
});
store.socket = socket;
socket.ev.on('creds.update', saveCreds);
socket.ev.on('connection.update', async (update) => {
const { connection, lastDisconnect, qr } = update;
logger.info({ connection, hasQR: !!qr }, 'Connection update');
if (qr) {
try {
const qrDataUrl = await QRCode.toDataURL(qr);
info.qrCode = qrDataUrl;
info.status = 'connecting';
logger.info({ accountId }, 'QR code generated');
this.emitEvent({ type: 'qr', accountId, data: { qrCode: qrDataUrl } });
} catch (err) {
logger.error({ err }, 'Failed to generate QR');
}
}
if (connection === 'close') {
const reason = (lastDisconnect?.error as Boom)?.output?.statusCode;
const shouldReconnect = reason !== DisconnectReason.loggedOut;
logger.info({ reason, shouldReconnect }, `Connection closed for ${accountId}`);
info.status = 'disconnected';
info.qrCode = null;
if (shouldReconnect) {
// Delete session data and try fresh
this.sessions.delete(accountId);
setTimeout(() => {
logger.info(`Retrying session ${accountId}...`);
this.createSession(accountId, name, phoneNumber);
}, 5000);
} else {
logger.info(`Session ${accountId} logged out`);
this.emitEvent({ type: 'disconnected', accountId, data: { reason: 'logged_out' } });
}
}
if (connection === 'open') {
info.status = 'connected';
info.qrCode = null;
info.phoneNumber = socket.user?.id?.split(':')[0] || null;
logger.info(`Session ${accountId} connected: ${info.phoneNumber}`);
this.emitEvent({ type: 'connected', accountId, data: { phoneNumber: info.phoneNumber } });
}
});
// Request pairing code if phone number provided
if (phoneNumber && !state.creds.registered) {
try {
const code = await socket.requestPairingCode(phoneNumber);
info.pairingCode = code;
logger.info({ accountId, code }, 'Pairing code generated');
this.emitEvent({ type: 'pairing_code', accountId, data: { code } });
} catch (err) {
logger.error({ err }, 'Failed to request pairing code');
}
}
socket.ev.on('messages.upsert', async ({ messages, type }) => {
if (type !== 'notify') return;
for (const msg of messages) {
const remoteJid = msg.key.remoteJid || '';
// Skip group messages (groups end with @g.us)
if (remoteJid.endsWith('@g.us')) continue;
// Skip broadcast lists
if (remoteJid === 'status@broadcast') continue;
// Skip non-user messages (like system messages)
if (!remoteJid.endsWith('@s.whatsapp.net')) continue;
// Detect message type and handle media
let mediaUrl: string | null = null;
let mediaType: string = 'text';
const msgContent = msg.message;
if (msgContent) {
const contentType = getContentType(msgContent);
if (contentType && ['imageMessage', 'audioMessage', 'videoMessage', 'documentMessage', 'stickerMessage'].includes(contentType)) {
try {
// Download media
const buffer = await downloadMediaMessage(
msg,
'buffer',
{},
{
logger,
reuploadRequest: socket.updateMediaMessage,
}
);
if (buffer) {
// Determine file extension
const extensions: Record<string, string> = {
imageMessage: 'jpg',
audioMessage: 'ogg',
videoMessage: 'mp4',
documentMessage: 'pdf',
stickerMessage: 'webp',
};
const ext = extensions[contentType] || 'bin';
const filename = `${randomUUID()}.${ext}`;
const filepath = path.join(this.mediaPath, filename);
// Save file
fs.writeFileSync(filepath, buffer as Buffer);
mediaUrl = `/media/${filename}`;
mediaType = contentType.replace('Message', '');
logger.info({ accountId, filename, mediaType }, 'Media downloaded');
}
} catch (err) {
logger.error({ err }, 'Failed to download media');
}
}
}
const messageData = {
id: msg.key.id,
from: remoteJid,
fromMe: msg.key.fromMe || false,
pushName: msg.pushName,
message: msg.message,
timestamp: msg.messageTimestamp,
mediaUrl,
mediaType,
};
this.emitEvent({ type: 'message', accountId, data: messageData });
}
});
socket.ev.on('messages.update', (updates) => {
for (const update of updates) {
if (update.update.status) {
this.emitEvent({
type: 'message_status',
accountId,
data: {
id: update.key.id,
remoteJid: update.key.remoteJid,
status: update.update.status,
},
});
}
}
});
return info;
}
async disconnectSession(accountId: string): Promise<void> {
const store = this.sessions.get(accountId);
if (!store || !store.socket) return;
await store.socket.logout();
store.socket = null;
store.info.status = 'disconnected';
store.info.qrCode = null;
}
async pauseSession(accountId: string): Promise<void> {
const store = this.sessions.get(accountId);
if (!store || !store.socket) return;
// Close connection without logout (keeps credentials)
store.socket.end(undefined);
store.socket = null;
store.info.status = 'paused';
store.info.qrCode = null;
logger.info({ accountId }, 'Session paused');
this.emitEvent({ type: 'paused', accountId, data: {} });
}
async resumeSession(accountId: string): Promise<SessionInfo> {
const store = this.sessions.get(accountId);
if (!store) {
throw new Error(`Session ${accountId} not found`);
}
if (store.info.status === 'connected') {
return store.info;
}
logger.info({ accountId }, 'Resuming session');
// Recreate the session using existing credentials
const name = store.info.name;
this.sessions.delete(accountId);
return this.createSession(accountId, name);
}
async deleteSession(accountId: string): Promise<void> {
await this.disconnectSession(accountId);
this.sessions.delete(accountId);
const sessionPath = path.join(this.sessionsPath, accountId);
if (fs.existsSync(sessionPath)) {
fs.rmSync(sessionPath, { recursive: true });
}
}
getSession(accountId: string): SessionInfo | null {
const store = this.sessions.get(accountId);
return store?.info || null;
}
getAllSessions(): SessionInfo[] {
return Array.from(this.sessions.values()).map((s) => s.info);
}
async sendMessage(
accountId: string,
to: string,
content: proto.IMessage
): Promise<proto.WebMessageInfo | null> {
const store = this.sessions.get(accountId);
if (!store?.socket || store.info.status !== 'connected') {
throw new Error(`Session ${accountId} not connected`);
}
const jid = to.includes('@') ? to : `${to}@s.whatsapp.net`;
const result = await store.socket.sendMessage(jid, content as any);
return result || null;
}
private emitEvent(event: SessionEvent): void {
this.emit('session_event', event);
}
}

View File

@@ -0,0 +1,31 @@
import { WASocket } from '@whiskeysockets/baileys';
export interface SessionInfo {
accountId: string;
phoneNumber: string | null;
name: string;
status: 'connecting' | 'connected' | 'disconnected' | 'paused';
qrCode: string | null;
pairingCode: string | null;
createdAt: Date;
}
export interface SessionStore {
socket: WASocket | null;
info: SessionInfo;
}
export type SessionEventType =
| 'qr'
| 'pairing_code'
| 'connected'
| 'disconnected'
| 'paused'
| 'message'
| 'message_status';
export interface SessionEvent {
type: SessionEventType;
accountId: string;
data: unknown;
}