@juzi/wechaty-puppet-whatsapp
Version:
Wechaty Puppet for WhatsApp
301 lines • 11.8 kB
JavaScript
import { EventEmitter as EE } from 'ee-ts';
import { log, MAX_HEARTBEAT_MISSED, MEMORY_SLOT } from './config.js';
import { CacheManager } from './data/cache-manager.js';
import { WA_ERROR_TYPE } from './exception/error-type.js';
import WAError from './exception/whatsapp-error.js';
import { batchProcess, getMaxTimestampForLoadHistoryMessages, isRoomId, sleep } from './helper/miscellaneous.js';
import ScheduleManager from './helper/schedule/schedule-manager.js';
import { RequestManager, requestManagerKeys } from './request/request-manager.js';
import { MessageAck } from './schema/whatsapp-interface.js';
import WhatsAppManager from './whatsapp/whatsapp-manager.js';
import * as PUPPET from '@juzi/wechaty-puppet';
const PRE = 'Manager';
export default class Manager extends EE {
options;
whatsAppManager;
cacheManager;
_requestManager;
scheduleManager;
memory;
fetchingMessages = false;
heartbeatTimer;
selfId = '';
constructor(options) {
super();
this.options = options;
this.whatsAppManager = new WhatsAppManager(this);
this.scheduleManager = ScheduleManager.Instance;
this.whatsAppManager.on({
friendship: data => this.emit('friendship', data),
login: data => {
this.emit('login', data);
this.selfId = data;
},
logout: (botId, data) => this.emit('logout', botId, data),
message: data => this.emit('message', data),
ready: () => this.emit('ready'),
'room-invite': data => this.emit('room-invite', data),
'room-join': data => this.emit('room-join', data),
'room-leave': data => this.emit('room-leave', data),
'room-topic': data => this.emit('room-topic', data),
scan: data => this.emit('scan', data),
dirty: data => this.onDirty(data),
error: error => this.emit('error', error),
'room-announce': data => this.emit('room-announce', data),
});
return new Proxy(this, {
get: (target, prop) => {
return requestManagerKeys.indexOf(prop) > -1 ? target.requestManager[prop].bind(target.requestManager) : target[prop];
},
});
}
getMemory() {
if (this.memory) {
return this.memory;
}
else {
throw WAError(WA_ERROR_TYPE.ERR_INIT, 'No Memory');
}
}
/**
* Lifecycle
*/
async start(memory) {
if (memory) {
this.memory = memory;
}
const session = await this.getMemory().get(MEMORY_SLOT);
log.verbose(PRE, 'start()');
const whatsAppClient = await this.whatsAppManager.genWhatsAppClient(this.options['puppeteerOptions'], session);
try {
await this.whatsAppManager.initWhatsAppEvents();
await this.whatsAppManager.initWhatsAppClient();
}
catch (error) {
log.error(PRE, `start() error message: ${error.stack}`);
await sleep(2 * 1000);
await this.start(session);
}
this._requestManager = new RequestManager(whatsAppClient);
this.startHeartbeat();
return whatsAppClient;
}
async stop() {
log.verbose(PRE, 'stop()');
this.stopSchedule();
await this.whatsAppManager.stop();
await this.releaseCache();
this._requestManager = undefined;
this.stopHeartbeat();
}
get requestManager() {
if (!this._requestManager) {
throw WAError(WA_ERROR_TYPE.ERR_INIT, 'No request manager');
}
return this._requestManager;
}
getWhatsAppClient() {
return this.whatsAppManager.getWhatsAppClient();
}
/**
* LOGIC METHODS
*/
/**
* Fetch history messages of contact or room, and then call onMessage method to emit them or not.
* @param {WhatsAppContact} contactOrRoom contact or room instance
*/
async processHistoryMessages(contactOrRoom) {
if (this.fetchingMessages) {
return;
}
this.fetchingMessages = true;
const fetchedMessageList = await this.fetchMessages(contactOrRoom);
const filteredMessageList = await this.filterFetchedMessages(contactOrRoom.id._serialized, fetchedMessageList);
await this.processFetchedMessages(filteredMessageList);
this.fetchingMessages = false;
}
async fetchMessages(contactOrRoom) {
if (contactOrRoom.isMe) {
// can not get chat for bot self
return [];
}
const chat = await contactOrRoom.getChat();
const messageList = await chat.fetchMessages({});
return messageList;
}
async filterFetchedMessages(contactOrRoomId, messageList) {
const cacheManager = await this.getCacheManager();
const maxTimestampForLoadHistoryMessages = getMaxTimestampForLoadHistoryMessages();
const latestTimestampInCache = await cacheManager.getLatestMessageTimestampForChat(contactOrRoomId);
const minTimestamp = Math.min(latestTimestampInCache, maxTimestampForLoadHistoryMessages);
try {
const _messageList = messageList.filter(m => m.timestamp >= minTimestamp);
const latestMessageTimestamp = _messageList[_messageList.length - 1]?.timestamp;
if (latestMessageTimestamp) {
await cacheManager.setLatestMessageTimestampForChat(contactOrRoomId, latestMessageTimestamp);
}
return _messageList;
}
catch (error) {
log.error(PRE, `filterFetchedMessages error: ${error.message}`);
return [];
}
}
async processFetchedMessages(messageList) {
const batchSize = 50;
await batchProcess(batchSize, messageList, async (message) => {
if (message.ack === MessageAck.ACK_DEVICE || message.ack === MessageAck.ACK_READ) {
await this.processMessage(message);
}
});
}
async getRoomChatById(roomId) {
if (isRoomId(roomId)) {
const roomChat = (await this.requestManager.getChatById(roomId));
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!roomChat.participants) {
roomChat.participants = [{
id: (await this.requestManager.getContactById(this.selfId)).id,
isAdmin: true,
isSuperAdmin: true,
}];
}
return roomChat;
}
else {
throw WAError(WA_ERROR_TYPE.ERR_GROUP_OR_CONTACT_ID, `The roomId: ${roomId} is not right.`);
}
}
/**
* Get member id list from web api
* @param { PuppetWhatsApp } this whatsapp client
* @param { string } roomId roomId
* @returns { string[] } member id list
*/
async syncRoomMemberList(roomId) {
const roomChat = await this.getRoomChatById(roomId);
// FIXME: How to deal with pendingParticipants? Maybe we should find which case could has this attribute.
const memberIdList = roomChat.participants.map(m => m.id._serialized);
const cacheManager = await this.getCacheManager();
await cacheManager.setRoomMemberIdList(roomId, memberIdList);
return memberIdList;
}
async syncContactOrRoomList() {
const whatsapp = this.getWhatsAppClient();
const now = Date.now();
log.info(PRE, `syncContactOrRoomList() whatsapp.getContacts() start at ${new Date().toISOString()}`);
const contactList = await whatsapp.getContacts();
log.info(PRE, `syncContactOrRoomList() whatsapp.getContacts() end at ${new Date().toISOString()}, cost ${Date.now() - now}ms`);
const contactOrRoomList = contactList.filter(c => c.id.server !== 'broadcast' && c.id._serialized !== '0@c.us');
log.info(PRE, `syncContactOrRoomList() contactOrRoomList.length: ${contactOrRoomList.length}`);
return contactOrRoomList;
}
async processMessage(message) {
log.silly(PRE, `processMessage(${message})`);
await this.whatsAppManager.getMessageEventHandler().onMessage(message);
}
/**
* Cache Section
*/
async initCache(userId) {
log.verbose(PRE, `initCache(${userId})`);
if (this.cacheManager) {
log.warn(PRE, 'initCache() already initialized, skip the init...');
return;
}
await CacheManager.init(userId);
this.cacheManager = CacheManager.Instance;
}
async releaseCache() {
log.verbose(PRE, 'releaseCache()');
if (this.cacheManager) {
log.warn(PRE, 'releaseCache() already initialized, skip the init...');
return;
}
await CacheManager.release();
}
async getCacheManager() {
if (!this.cacheManager) {
throw WAError(WA_ERROR_TYPE.ERR_INIT, 'no cache manager');
}
return this.cacheManager;
}
/**
* Schedule
*/
startSchedule() {
this.scheduleManager.addScheduledTask('0 */2 * * * *', async () => {
log.silly(PRE, 'startSyncMissedMessages');
const contactOrRoomList = await this.syncContactOrRoomList();
const batchSize = 100;
await batchProcess(batchSize, contactOrRoomList, async (contactOrRoom) => {
await this.processHistoryMessages(contactOrRoom);
});
log.silly(PRE, 'startSyncMissedMessages finished');
});
}
stopSchedule() {
this.scheduleManager.clearAllTasks();
}
/**
* Heatbeat
*/
startHeartbeat() {
if (!this.heartbeatTimer) {
this.asystoleCount = 0;
this.heartbeatTimer = setInterval(this.heartbeat.bind(this), 15 * 1000);
}
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined;
}
}
asystoleCount = 0;
async heartbeat() {
/**
* puppteer.isConnected behaviour: (in MacOs)
* it will still return true if the Chromium window is closed with command + w
* it will not return true if the Chromium process is terminated with command + q
*/
let alive = false;
try {
alive = !!this.getWhatsAppClient().pupBrowser?.isConnected();
}
catch (e) {
alive = false;
}
if (alive) {
this.asystoleCount = 0;
this.emit('heartbeat', 'puppeteer still connected');
}
else {
this.asystoleCount += 1;
log.warn(PRE, `asystole count: ${this.asystoleCount}`);
if (this.asystoleCount > MAX_HEARTBEAT_MISSED) {
log.error(PRE, 'max asystole reached, restarting...');
await this.stop();
await this.start();
this.asystoleCount = 0;
}
}
}
async onDirty(data) {
log.info(PRE, `onDirty(${JSON.stringify(data)})`);
switch (data.payloadType) {
case PUPPET.types.Dirty.Contact: {
const contactId = data.payloadId;
const rawContact = await this.requestManager.getContactById(contactId);
const avatar = await rawContact.getProfilePicUrl() || '';
const contact = Object.assign(rawContact, { avatar });
await this.cacheManager?.setContactOrRoomRawPayload(contactId, contact);
break;
}
default:
break;
}
this.emit('dirty', data);
}
}
//# sourceMappingURL=manager.js.map