UNPKG

@warriorteam/zalo-personal

Version:

Unofficial Zalo Personal API for JavaScript - A powerful library for interacting with Zalo personal accounts with URL attachment support

1,019 lines (809 loc) 28.6 kB
# Lắng Nghe Sự Kiện (Event Listener) ## Tổng Quan SDK cung cấp hệ thống listener mạnh mẽ để lắng nghe các sự kiện real-time: - Tin nhắn mới - Sự kiện nhóm (thêm/xóa thành viên, đổi tên, v.v.) - Sự kiện bạn bè (kết bạn, unfriend) - Reaction, typing, seen, delivered - Undo actions ## Khởi Tạo Listener ### 1. Cấu Hình Cơ Bản ```typescript import { Zalo, CloseReason } from 'zalo-personal-sdk'; // Khởi tạo với selfListen để nghe tin nhắn của chính mình const zalo = new Zalo({ selfListen: true }); const api = await zalo.login(credentials); // Bắt đầu lắng nghe await api.listener.start(); console.log('🎧 Listener đã khởi động!'); ``` ### 2. Xử Lý Kết Nối ```typescript // Sự kiện kết nối api.listener.on('connect', () => { console.log('✅ Đã kết nối listener'); }); // Sự kiện mất kết nối api.listener.on('disconnect', (reason: CloseReason) => { console.log('❌ Mất kết nối:', reason); switch (reason) { case CloseReason.Normal: console.log('Ngắt kết nối bình thường'); break; case CloseReason.Error: console.log('Lỗi kết nối'); break; case CloseReason.Reconnect: console.log('Đang reconnect...'); break; } }); // Sự kiện lỗi api.listener.on('error', (error) => { console.error('🚨 Listener error:', error); }); ``` ### 3. Dừng Listener ```typescript // Dừng lắng nghe await api.listener.stop(); console.log('⏹️ Listener đã dừng'); // Hoặc sử dụng trong signal handler process.on('SIGINT', async () => { console.log('🛑 Đang tắt listener...'); await api.listener.stop(); process.exit(0); }); ``` ## Lắng Nghe Tin Nhắn ### 1. Tin Nhắn Cơ Bản ```typescript api.listener.on('message', (message) => { const { threadId, threadType, data, senderId } = message; console.log(`💬 Tin nhắn mới từ ${senderId}:`); console.log(` Thread: ${threadId} (${threadType === 0 ? 'User' : 'Group'})`); console.log(` Nội dung: ${data.content || 'File đính kèm'}`); console.log(` Loại: ${data.msgType}`); console.log(` Thời gian: ${new Date(data.ts).toLocaleString()}`); }); ``` ### 2. Lọc Tin Nhắn Theo Loại ```typescript api.listener.on('message', (message) => { const { data, senderId, threadId, threadType } = message; switch (data.msgType) { case 'webchat': console.log(`📝 Tin nhắn văn bản: ${data.content}`); break; case 'chat.photo': console.log(`📷 Ảnh: ${data.href}`); console.log(` Kích thước: ${data.width}x${data.height}`); break; case 'chat.video.msg': console.log(`🎥 Video: ${data.href}`); console.log(` Thời lượng: ${data.duration}s`); break; case 'chat.audio': console.log(`🎵 Audio: ${data.href}`); break; case 'chat.file': console.log(`📎 File: ${data.fileName}`); console.log(` Kích thước: ${data.fileSize} bytes`); break; case 'chat.sticker': console.log(`😀 Sticker: ${data.stickerId}`); break; case 'chat.link': console.log(`🔗 Link: ${data.href}`); break; case 'chat.location': console.log(`📍 Vị trí: ${data.latitude}, ${data.longitude}`); break; default: console.log(`❓ Loại tin nhắn không xác định: ${data.msgType}`); } }); ``` ### 3. Auto Reply Bot ```typescript api.listener.on('message', async (message) => { const { threadId, threadType, data, senderId } = message; // Bỏ qua tin nhắn của chính mình const myId = await api.getOwnId(); if (senderId === myId) return; // Bỏ qua nếu không phải tin nhắn văn bản if (data.msgType !== 'webchat' || !data.content) return; const text = data.content.toLowerCase().trim(); try { if (text === '/help') { const helpText = ` 🤖 Bot Commands: /help - Hiển thị hướng dẫn /time - Xem thời gian hiện tại /weather - Dự báo thời tiết /ping - Test bot /info - Thông tin bot `.trim(); await api.sendMessage(helpText, threadId, threadType); } else if (text === '/time') { const now = new Date().toLocaleString('vi-VN'); await api.sendMessage(`🕐 Bây giờ là: ${now}`, threadId, threadType); } else if (text === '/ping') { await api.sendMessage('🏓 Pong!', threadId, threadType); } else if (text === '/weather') { await api.sendMessage('⛅ Hôm nay trời đẹp, nắng nhẹ 25°C', threadId, threadType); } else if (text === '/info') { const info = ` 🤖 Zalo Bot SDK v1.0 📅 Khởi động: ${new Date().toLocaleDateString()} 💻 Node.js ${process.version} `.trim(); await api.sendMessage(info, threadId, threadType); } else if (text.startsWith('/echo ')) { const echoText = data.content.substring(6); await api.sendMessage(`🔊 ${echoText}`, threadId, threadType); } else if (text.includes('hello') || text.includes('hi') || text.includes('chào')) { await api.sendMessage('👋 Xin chào! Gõ /help để xem hướng dẫn.', threadId, threadType); } } catch (error) { console.error('❌ Lỗi auto reply:', error.message); } }); ``` ### 4. Message Analytics ```typescript class MessageAnalytics { private stats = { totalMessages: 0, messagesByType: new Map<string, number>(), messagesByUser: new Map<string, number>(), messagesByThread: new Map<string, number>(), dailyStats: new Map<string, number>() }; constructor(api: any) { api.listener.on('message', (message: any) => { this.recordMessage(message); }); } recordMessage(message: any) { const { data, senderId, threadId } = message; const today = new Date().toDateString(); // Tổng tin nhắn this.stats.totalMessages++; // Theo loại const currentTypeCount = this.stats.messagesByType.get(data.msgType) || 0; this.stats.messagesByType.set(data.msgType, currentTypeCount + 1); // Theo user const currentUserCount = this.stats.messagesByUser.get(senderId) || 0; this.stats.messagesByUser.set(senderId, currentUserCount + 1); // Theo thread const currentThreadCount = this.stats.messagesByThread.get(threadId) || 0; this.stats.messagesByThread.set(threadId, currentThreadCount + 1); // Theo ngày const currentDayCount = this.stats.dailyStats.get(today) || 0; this.stats.dailyStats.set(today, currentDayCount + 1); } getStats() { return { total: this.stats.totalMessages, byType: Object.fromEntries(this.stats.messagesByType), byUser: Object.fromEntries(this.stats.messagesByUser), byThread: Object.fromEntries(this.stats.messagesByThread), byDay: Object.fromEntries(this.stats.dailyStats) }; } printStats() { const stats = this.getStats(); console.log('📊 Message Statistics:'); console.log(` Tổng tin nhắn: ${stats.total}`); console.log(' Top message types:'); Object.entries(stats.byType) .sort(([,a], [,b]) => (b as number) - (a as number)) .slice(0, 5) .forEach(([type, count]) => { console.log(` ${type}: ${count}`); }); console.log(' Hôm nay:', stats.byDay[new Date().toDateString()] || 0); } } // Sử dụng const analytics = new MessageAnalytics(api); // In thống kê mỗi 10 phút setInterval(() => { analytics.printStats(); }, 10 * 60 * 1000); ``` ## Sự Kiện Nhóm ### 1. Quản Lý Thành Viên ```typescript api.listener.on('group_event', (event) => { const { groupId, actorId, targetId, type, data } = event; switch (type) { case 'group.member.add': console.log(`👥 ${actorId} đã thêm ${targetId} vào nhóm ${groupId}`); // Chào mừng thành viên mới api.sendMessage( `🎉 Chào mừng bạn ${targetId} tham gia nhóm!`, groupId, ThreadType.Group ); break; case 'group.member.remove': console.log(`👋 ${actorId} đã xóa ${targetId} khỏi nhóm ${groupId}`); break; case 'group.member.left': console.log(`🚪 ${targetId} đã rời nhóm ${groupId}`); break; case 'group.name.change': console.log(`📝 ${actorId} đã đổi tên nhóm ${groupId} thành: ${data.newName}`); break; case 'group.avatar.change': console.log(`🖼️ ${actorId} đã thay avatar nhóm ${groupId}`); break; case 'group.admin.add': console.log(`👑 ${targetId} đã được thêm làm admin nhóm ${groupId}`); break; case 'group.admin.remove': console.log(`👤 ${targetId} đã bị xóa khỏi admin nhóm ${groupId}`); break; } }); ``` ### 2. Auto Moderation ```typescript class GroupModerator { private bannedWords = ['spam', 'hack', 'cheat']; private warningCount = new Map<string, number>(); private api: any; constructor(api: any) { this.api = api; api.listener.on('message', (message: any) => { this.moderateMessage(message); }); } async moderateMessage(message: any) { const { threadId, threadType, data, senderId } = message; // Chỉ moderate trong nhóm if (threadType !== 1) return; // Bỏ qua nếu không phải tin nhắn văn bản if (data.msgType !== 'webchat' || !data.content) return; const content = data.content.toLowerCase(); // Kiểm tra từ cấm const hasBannedWord = this.bannedWords.some(word => content.includes(word)); if (hasBannedWord) { try { // Xóa tin nhắn await this.api.deleteMessage({ messageId: data.msgId, threadId: threadId, threadType: threadType }); // Cảnh báo user const warnings = (this.warningCount.get(senderId) || 0) + 1; this.warningCount.set(senderId, warnings); if (warnings >= 3) { // Kick khỏi nhóm sau 3 lần cảnh báo await this.api.removeUserFromGroup(threadId, senderId); await this.api.sendMessage( `🚫 ${senderId} đã bị kick khỏi nhóm do vi phạm nhiều lần.`, threadId, threadType ); this.warningCount.delete(senderId); } else { await this.api.sendMessage( `⚠️ @${senderId} Cảnh báo ${warnings}/3: Không sử dụng từ ngữ không phù hợp!`, threadId, threadType, { mentions: [{ pos: 2, len: senderId.length + 1, uid: senderId }] } ); } console.log(`🛡️ Đã xóa tin nhắn vi phạm từ ${senderId}`); } catch (error) { console.error('❌ Lỗi moderation:', error.message); } } } addBannedWord(word: string) { this.bannedWords.push(word.toLowerCase()); } removeBannedWord(word: string) { const index = this.bannedWords.indexOf(word.toLowerCase()); if (index > -1) { this.bannedWords.splice(index, 1); } } } // Sử dụng const moderator = new GroupModerator(api); moderator.addBannedWord('badword'); ``` ## Sự Kiện Bạn Bè ### 1. Quản Lý Lời Mời Kết Bạn ```typescript api.listener.on('friend_event', async (event) => { const { type, fromId, toId, data } = event; switch (type) { case 'friend.request.received': console.log(`👋 Nhận lời mời kết bạn từ ${fromId}`); // Auto accept từ whitelist const whitelist = ['trusted-user-1', 'trusted-user-2']; if (whitelist.includes(fromId)) { try { await api.acceptFriendRequest(fromId); console.log(`✅ Đã tự động chấp nhận kết bạn với ${fromId}`); // Gửi tin nhắn chào mừng await api.sendMessage( '🎉 Chào mừng bạn! Cảm ơn bạn đã kết bạn với mình.', fromId, ThreadType.User ); } catch (error) { console.error('❌ Lỗi auto accept:', error.message); } } break; case 'friend.request.accepted': console.log(`✅ ${toId} đã chấp nhận lời mời kết bạn`); break; case 'friend.removed': console.log(`💔 ${fromId} đã unfriend`); break; case 'friend.blocked': console.log(`🚫 ${fromId} đã chặn bạn`); break; } }); ``` ### 2. Friend Request Manager ```typescript class FriendRequestManager { private api: any; private autoAcceptRules: Array<(fromId: string, data: any) => boolean> = []; constructor(api: any) { this.api = api; api.listener.on('friend_event', (event: any) => { if (event.type === 'friend.request.received') { this.handleFriendRequest(event); } }); } addRule(rule: (fromId: string, data: any) => boolean) { this.autoAcceptRules.push(rule); } async handleFriendRequest(event: any) { const { fromId, data } = event; try { // Lấy thông tin người gửi const userInfo = await this.api.getUserInfo([fromId]); const user = userInfo.data[0]; console.log(`👋 Lời mời kết bạn từ: ${user.displayName}`); console.log(` Lời nhắn: ${data.message || 'Không có'}`); // Kiểm tra rules const shouldAccept = this.autoAcceptRules.some(rule => rule(fromId, data)); if (shouldAccept) { await this.api.acceptFriendRequest(fromId); console.log(`✅ Đã tự động chấp nhận kết bạn với ${user.displayName}`); // Gửi tin nhắn welcome await this.api.sendMessage( `🎉 Xin chào ${user.displayName}! Rất vui được kết bạn với bạn.`, fromId, ThreadType.User ); } else { console.log(`⏳ Lời mời từ ${user.displayName} cần xem xét thủ công`); } } catch (error) { console.error('❌ Lỗi xử lý lời mời kết bạn:', error.message); } } } // Sử dụng const friendManager = new FriendRequestManager(api); // Rule: Accept nếu có từ khóa trong lời nhắn friendManager.addRule((fromId, data) => { const message = data.message?.toLowerCase() || ''; return message.includes('developer') || message.includes('programmer'); }); // Rule: Accept nếu tên có từ khóa friendManager.addRule(async (fromId, data) => { try { const userInfo = await api.getUserInfo([fromId]); const displayName = userInfo.data[0].displayName.toLowerCase(); return displayName.includes('dev') || displayName.includes('tech'); } catch { return false; } }); ``` ## Sự Kiện Reaction và Typing ### 1. Reaction Events ```typescript api.listener.on('reaction', (event) => { const { messageId, threadId, reactorId, emoji, action } = event; if (action === 'add') { console.log(`❤️ ${reactorId} đã react ${emoji} vào tin nhắn ${messageId}`); } else { console.log(`💔 ${reactorId} đã bỏ react ${emoji} khỏi tin nhắn ${messageId}`); } }); ``` ### 2. Typing Events ```typescript api.listener.on('typing', (event) => { const { threadId, userId, isTyping } = event; if (isTyping) { console.log(`⌨️ ${userId} đang typing trong ${threadId}`); } else { console.log(`⏸️ ${userId} đã dừng typing trong ${threadId}`); } }); ``` ### 3. Seen/Delivered Events ```typescript api.listener.on('seen', (event) => { const { messageId, threadId, userId, timestamp } = event; console.log(`👀 ${userId} đã seen tin nhắn ${messageId} lúc ${new Date(timestamp).toLocaleString()}`); }); api.listener.on('delivered', (event) => { const { messageId, threadId, userId, timestamp } = event; console.log(`📩 Tin nhắn ${messageId} đã delivered đến ${userId}`); }); ``` ## Sự Kiện Undo ```typescript api.listener.on('undo', (event) => { const { messageId, threadId, userId, undoType } = event; switch (undoType) { case 'message': console.log(`🔄 ${userId} đã thu hồi tin nhắn ${messageId}`); break; case 'reaction': console.log(`🔄 ${userId} đã thu hồi reaction`); break; default: console.log(`🔄 ${userId} đã thu hồi: ${undoType}`); } }); ``` ## Utility Classes ### 1. Event Logger ```typescript import fs from 'fs/promises'; class EventLogger { private logFile: string; private buffer: any[] = []; private flushInterval: NodeJS.Timeout; constructor(api: any, logFile: string = './events.log') { this.logFile = logFile; // Log tất cả events const events = ['message', 'group_event', 'friend_event', 'reaction', 'typing', 'seen', 'delivered', 'undo']; events.forEach(eventName => { api.listener.on(eventName, (event: any) => { this.logEvent(eventName, event); }); }); // Flush buffer mỗi 30 giây this.flushInterval = setInterval(() => { this.flushBuffer(); }, 30000); } private logEvent(type: string, event: any) { const logEntry = { timestamp: new Date().toISOString(), type, event: JSON.stringify(event) }; this.buffer.push(logEntry); console.log(`📝 Logged ${type} event`); // Flush nếu buffer đầy if (this.buffer.length >= 100) { this.flushBuffer(); } } private async flushBuffer() { if (this.buffer.length === 0) return; try { const logs = this.buffer.map(entry => `[${entry.timestamp}] ${entry.type}: ${entry.event}` ).join('\n') + '\n'; await fs.appendFile(this.logFile, logs); console.log(`💾 Flushed ${this.buffer.length} events to log`); this.buffer = []; } catch (error) { console.error('❌ Lỗi ghi log:', error.message); } } async close() { if (this.flushInterval) { clearInterval(this.flushInterval); } await this.flushBuffer(); } } // Sử dụng const logger = new EventLogger(api); // Cleanup khi tắt app process.on('SIGINT', async () => { await logger.close(); await api.listener.stop(); process.exit(0); }); ``` ### 2. Event Router ```typescript class EventRouter { private routes = new Map<string, Array<(event: any) => void>>(); constructor(api: any) { // Route tất cả events qua router const events = ['message', 'group_event', 'friend_event', 'reaction', 'typing', 'seen', 'delivered', 'undo']; events.forEach(eventName => { api.listener.on(eventName, (event: any) => { this.routeEvent(eventName, event); }); }); } on(pattern: string, handler: (event: any) => void) { if (!this.routes.has(pattern)) { this.routes.set(pattern, []); } this.routes.get(pattern)!.push(handler); } private routeEvent(eventName: string, event: any) { // Tìm các route phù hợp for (const [pattern, handlers] of this.routes.entries()) { if (this.matchPattern(pattern, eventName, event)) { handlers.forEach(handler => { try { handler(event); } catch (error) { console.error(`❌ Lỗi handler cho ${pattern}:`, error.message); } }); } } } private matchPattern(pattern: string, eventName: string, event: any): boolean { // Simple pattern matching if (pattern === eventName) return true; if (pattern === '*') return true; // Pattern như "message:webchat" if (pattern.includes(':')) { const [eventPattern, typePattern] = pattern.split(':'); return eventPattern === eventName && event.data?.msgType === typePattern; } return false; } } // Sử dụng const router = new EventRouter(api); // Route tin nhắn văn bản router.on('message:webchat', (event) => { console.log(`📝 Text message: ${event.data.content}`); }); // Route tin nhắn ảnh router.on('message:chat.photo', (event) => { console.log(`📷 Image: ${event.data.href}`); }); // Route tất cả sự kiện nhóm router.on('group_event', (event) => { console.log(`👥 Group event: ${event.type}`); }); // Route tất cả sự kiện router.on('*', (event) => { console.log(`🎯 Any event received`); }); ``` ## Reconnection và Error Handling ### 1. Auto Reconnect ```typescript class ReliableListener { private api: any; private reconnectAttempts = 0; private maxReconnectAttempts = 10; private reconnectDelay = 5000; private isReconnecting = false; constructor(api: any) { this.api = api; this.setupListener(); } private setupListener() { this.api.listener.on('disconnect', (reason: CloseReason) => { console.log(`❌ Listener disconnected: ${reason}`); if (reason === CloseReason.Error && !this.isReconnecting) { this.reconnect(); } }); this.api.listener.on('connect', () => { console.log('✅ Listener connected'); this.reconnectAttempts = 0; this.isReconnecting = false; }); this.api.listener.on('error', (error: any) => { console.error('🚨 Listener error:', error.message); if (!this.isReconnecting) { this.reconnect(); } }); } private async reconnect() { if (this.isReconnecting) return; this.isReconnecting = true; while (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; console.log(`🔄 Reconnect attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`); try { await this.api.listener.stop(); await new Promise(resolve => setTimeout(resolve, this.reconnectDelay)); await this.api.listener.start(); console.log('✅ Reconnected successfully'); return; } catch (error) { console.error(`❌ Reconnect attempt ${this.reconnectAttempts} failed:`, error.message); // Exponential backoff this.reconnectDelay *= 1.5; } } console.error('💥 Max reconnect attempts reached. Listener stopped.'); this.isReconnecting = false; } async start() { try { await this.api.listener.start(); } catch (error) { console.error('❌ Failed to start listener:', error.message); this.reconnect(); } } } // Sử dụng const reliableListener = new ReliableListener(api); await reliableListener.start(); ``` ### 2. Health Monitor ```typescript class ListenerHealthMonitor { private api: any; private lastEventTime = Date.now(); private healthCheckInterval: NodeJS.Timeout; private isHealthy = true; constructor(api: any, checkIntervalMs: number = 60000) { this.api = api; // Track last event time const events = ['message', 'group_event', 'friend_event', 'reaction', 'typing', 'seen', 'delivered']; events.forEach(eventName => { api.listener.on(eventName, () => { this.lastEventTime = Date.now(); if (!this.isHealthy) { console.log('💚 Listener health recovered'); this.isHealthy = true; } }); }); // Health check this.healthCheckInterval = setInterval(() => { this.checkHealth(); }, checkIntervalMs); } private checkHealth() { const now = Date.now(); const timeSinceLastEvent = now - this.lastEventTime; // Coi như unhealthy nếu không có event nào trong 5 phút if (timeSinceLastEvent > 5 * 60 * 1000) { if (this.isHealthy) { console.log('💔 Listener appears unhealthy - no events for 5 minutes'); this.isHealthy = false; // Send keep alive or restart listener this.tryRecover(); } } } private async tryRecover() { try { // Try keep alive first await this.api.keepAlive(); console.log('📡 Keep alive sent'); // If still no events after 2 more minutes, restart listener setTimeout(() => { if (!this.isHealthy) { console.log('🔄 Restarting listener due to health issues'); this.restartListener(); } }, 2 * 60 * 1000); } catch (error) { console.error('❌ Keep alive failed:', error.message); this.restartListener(); } } private async restartListener() { try { await this.api.listener.stop(); await new Promise(resolve => setTimeout(resolve, 5000)); await this.api.listener.start(); console.log('✅ Listener restarted'); } catch (error) { console.error('❌ Failed to restart listener:', error.message); } } stop() { if (this.healthCheckInterval) { clearInterval(this.healthCheckInterval); } } } // Sử dụng const healthMonitor = new ListenerHealthMonitor(api); ``` ## Best Practices ### 1. Memory Management ```typescript // Cleanup listeners khi không cần const messageHandler = (message: any) => { console.log('New message:', message.data.content); }; api.listener.on('message', messageHandler); // Cleanup api.listener.off('message', messageHandler); ``` ### 2. Error Boundaries ```typescript function safeEventHandler(handler: Function) { return (event: any) => { try { handler(event); } catch (error) { console.error('❌ Event handler error:', error.message); console.error('Event data:', JSON.stringify(event, null, 2)); } }; } // Sử dụng api.listener.on('message', safeEventHandler((message) => { // Handler code có thể throw error processMessage(message); })); ``` ### 3. Performance Monitoring ```typescript class PerformanceMonitor { private eventCounts = new Map<string, number>(); private lastReset = Date.now(); constructor(api: any) { const events = ['message', 'group_event', 'friend_event', 'reaction']; events.forEach(eventName => { api.listener.on(eventName, () => { const count = this.eventCounts.get(eventName) || 0; this.eventCounts.set(eventName, count + 1); }); }); // Report mỗi phút setInterval(() => { this.reportStats(); }, 60000); } private reportStats() { const now = Date.now(); const elapsed = (now - this.lastReset) / 1000; console.log(`📊 Events per second (last ${elapsed.toFixed(0)}s):`); for (const [eventName, count] of this.eventCounts.entries()) { const eps = (count / elapsed).toFixed(2); console.log(` ${eventName}: ${eps} eps (${count} total)`); } // Reset counters this.eventCounts.clear(); this.lastReset = now; } } ``` ## Lưu Ý Quan Trọng 1. **Memory Leaks**: Cleanup listeners khi không cần thiết 2. **Rate Limiting**: Không xử lý quá nhiều events cùng lúc 3. **Error Handling**: Luôn wrap handlers trong try-catch 4. **Reconnection**: Implement auto-reconnect cho production 5. **Health Monitoring**: Monitor health của listener connection 6. **Performance**: Monitor performance và memory usage 7. **Logging**: Log events quan trọng để debug 8. **Cleanup**: Đảm bảo cleanup khi shutdown application