import * as path from 'path' import * as fs from 'fs-extra' import * as os from 'os' import { FlashStore } from 'flash-store' import { log } from '../config.js' import { WA_ERROR_TYPE } from '../exception/error-type.js' import WAError from '../exception/whatsapp-error.js' import type { WhatsAppContactPayload, InviteV4Data, WhatsAppMessagePayload, } from '../schema/whatsapp-type.js' import { Friendship } from '@juzi/wechaty-puppet/payloads' const PRE = 'CacheManager' export class CacheManager { /** * ************************************************************************ * Static Methods * ************************************************************************ */ private static _instance?: CacheManager public static get Instance () { if (!this._instance) { throw WAError(WA_ERROR_TYPE.ERR_NO_CACHE, 'no instance') } return this._instance } public static async init (userId: string) { if (this._instance) { return } this._instance = new CacheManager() await this._instance.initCache(userId) } public static async release () { if (!this._instance) { return } await this._instance.releaseCache() this._instance = undefined } /** * ************************************************************************ * Instance Methods * ************************************************************************ */ // Static cache, won't change over time private cacheMessageRawPayload?: FlashStore private cacheContactOrRoomRawPayload?: FlashStore private cacheRoomMemberIdList?: FlashStore private cacheRoomInvitationRawPayload?: FlashStore> private cacheLatestMessageTimestampForChat?: FlashStore private cacheFriendshipRawPayload?: FlashStore /** * ------------------------------- * Message Cache Section * -------------------------------- */ public async getMessageRawPayload (id: string) { const cache = this.getMessageCache() return cache.get(id) } public async setMessageRawPayload (id: string, payload: WhatsAppMessagePayload): Promise { const cache = this.getMessageCache() // @ts-ignore client is in implementation but not in interface const { client, ...rest } = payload await cache.set(id, rest) } public deleteMessage (id: string) { const cache = this.getMessageCache() return cache.delete(id) } private getMessageCache () { if (!this.cacheMessageRawPayload) { throw WAError(WA_ERROR_TYPE.ERR_NO_CACHE, 'getMessageCache() has no cache') } return this.cacheMessageRawPayload } /** * ------------------------------- * Contact And Room Cache Section * -------------------------------- */ public async getContactOrRoomRawPayload (id: string) { const cache = this.getContactOrRoomCache() return cache.get(id) } public async setContactOrRoomRawPayload (id: string, payload: WhatsAppContactPayload): Promise { const cache = this.getContactOrRoomCache() // @ts-ignore client is in implementation but not in interface const { client, ...rest } = payload await cache.set(id, rest) } public deleteContactOrRoom (id: string) { const cache = this.getContactOrRoomCache() return cache.delete(id) } private getContactOrRoomCache () { if (!this.cacheContactOrRoomRawPayload) { throw WAError(WA_ERROR_TYPE.ERR_NO_CACHE, 'getContactOrRoomCache() has no cache') } return this.cacheContactOrRoomRawPayload } public async getContactIdList () { const cache = this.getContactOrRoomCache() const list = [] for await (const key of cache.keys()) { const value = await cache.get(key) if (!value) { continue } if (!value.isGroup && value.id._serialized) { list.push(value.id._serialized) } } return list } public async getRoomIdList () { const cache = this.getContactOrRoomCache() const list = [] for await (const key of cache.keys()) { const value = await cache.get(key) if (!value) { continue } if (value.isGroup && value.id._serialized) { list.push(value.id._serialized) } } return list } /** * ------------------------------- * Room Member Cache Section * -------------------------------- */ public async getRoomMemberIdList (roomId: string) { const cache = this.getRoomMemberCache() const memberIdList = await cache.get(roomId) return memberIdList || [] } public async setRoomMemberIdList (roomId: string, list: string[]): Promise { const cache = this.getRoomMemberCache() await cache.set(roomId, list) } public async addRoomMemberToList (roomId: string, memberIds: string | string[]): Promise { const memberIdListInCache = await this.getRoomMemberIdList(roomId) if (Array.isArray(memberIds)) { memberIds.forEach(memberId => !memberIdListInCache.includes(memberId) && memberIdListInCache.push(memberId)) await this.setRoomMemberIdList(roomId, memberIdListInCache) } else { !memberIdListInCache.includes(memberIds) && memberIdListInCache.push(memberIds) await this.setRoomMemberIdList(roomId, memberIdListInCache) } } public async removeRoomMemberFromList (roomId: string, memberIds: string | string[]): Promise { const memberIdListInCache = await this.getRoomMemberIdList(roomId) if (Array.isArray(memberIds)) { const memberIdList = memberIdListInCache.filter(id => !memberIds.includes(id)) await this.setRoomMemberIdList(roomId, memberIdList) } else { if (memberIdListInCache.includes(memberIds)) { const memberIdList = memberIdListInCache.filter(id => id !== memberIds) await this.setRoomMemberIdList(roomId, memberIdList) } } } public async deleteRoomMemberIdList (roomId: string) { const cache = this.getRoomMemberCache() await cache.delete(roomId) } private getRoomMemberCache () { if (!this.cacheRoomMemberIdList) { throw WAError(WA_ERROR_TYPE.ERR_NO_CACHE, 'getRoomMemberCache() has no cache') } return this.cacheRoomMemberIdList } /** * ------------------------------- * Room Invitation Cache Section * -------------------------------- */ public async getRoomInvitationRawPayload (id: string) { const cache = this.getRoomInvitationCache() return cache.get(id) } public async setRoomInvitationRawPayload (id: string, payload: Partial): Promise { const cache = this.getRoomInvitationCache() await cache.set(id, payload) } public deleteRoomInvitation (id: string) { const cache = this.getRoomInvitationCache() return cache.delete(id) } private getRoomInvitationCache () { if (!this.cacheRoomInvitationRawPayload) { throw WAError(WA_ERROR_TYPE.ERR_NO_CACHE, 'getRoomInvitationCache() has no cache') } return this.cacheRoomInvitationRawPayload } /** * ------------------------------- * Message Cache Section * -------------------------------- */ /** * get timestamp of the latest message for contact or room, if timestamp is not in cache, return Number.MAX_SAFE_INTEGER * @param {string} id message id * @returns {number} timestamp or Number.MAX_SAFE_INTEGER */ public async getLatestMessageTimestampForChat (id: string): Promise { const cache = this.getLatestMessageTimestampForChatCache() const timestamp = await cache.get(id) if (!timestamp) { return Number.MAX_SAFE_INTEGER } return timestamp } public async setLatestMessageTimestampForChat (id: string, num: number): Promise { const cache = this.getLatestMessageTimestampForChatCache() await cache.set(id, num) } private getLatestMessageTimestampForChatCache () { if (!this.cacheLatestMessageTimestampForChat) { throw WAError(WA_ERROR_TYPE.ERR_NO_CACHE, 'getLatestMessageTimestampForChatCache() has no cache') } return this.cacheLatestMessageTimestampForChat } /** * ------------------------------- * Friendship Cache Section * -------------------------------- */ public async getFriendshipRawPayload (id: string) { const cache = this.getFriendshipCache() return cache.get(id) } public async setFriendshipRawPayload (id: string, payload: Friendship): Promise { const cache = this.getFriendshipCache() // @ts-ignore client is in implementation but not in interface const { client, ...rest } = payload await cache.set(id, rest) } public deleteFriendship (id: string) { const cache = this.getFriendshipCache() return cache.delete(id) } private getFriendshipCache () { if (!this.cacheFriendshipRawPayload) { throw WAError(WA_ERROR_TYPE.ERR_NO_CACHE, 'getFriendshipCache() has no cache') } return this.cacheFriendshipRawPayload } /** * ------------------------------- * Private Method Section * -------------------------------- */ private async initCache ( userId: string, ): Promise { if (this.cacheMessageRawPayload) { throw WAError(WA_ERROR_TYPE.ERR_INIT, 'cacheMessageRawPayload does not exist.') } const baseDir = path.join( os.homedir(), '.wechaty', 'puppet-whatsapp', 'flash-store-v0.12', userId, ) const baseDirExist = await fs.pathExists(baseDir) if (!baseDirExist) { await fs.mkdirp(baseDir) } this.cacheMessageRawPayload = new FlashStore(path.join(baseDir, 'message')) this.cacheContactOrRoomRawPayload = new FlashStore(path.join(baseDir, 'contact-or-room')) this.cacheRoomInvitationRawPayload = new FlashStore(path.join(baseDir, 'room-invitation')) this.cacheRoomMemberIdList = new FlashStore(path.join(baseDir, 'room-member')) this.cacheLatestMessageTimestampForChat = new FlashStore(path.join(baseDir, 'latest-message-timestamp-for-chat')) this.cacheFriendshipRawPayload = new FlashStore(path.join(baseDir, 'friendship')) const messageTotal = await this.cacheMessageRawPayload.size log.verbose(PRE, `initCache() inited Messages: ${messageTotal} cacheDir="${baseDir}"`) } private async releaseCache () { log.verbose(PRE, 'releaseCache()') if (this.cacheMessageRawPayload && this.cacheContactOrRoomRawPayload && this.cacheRoomInvitationRawPayload && this.cacheRoomMemberIdList && this.cacheLatestMessageTimestampForChat && this.cacheFriendshipRawPayload ) { log.silly(PRE, 'releaseCache() closing caches ...') await Promise.all([ this.cacheMessageRawPayload.close(), this.cacheContactOrRoomRawPayload.close(), this.cacheRoomInvitationRawPayload.close(), this.cacheRoomMemberIdList.close(), this.cacheLatestMessageTimestampForChat.close(), this.cacheFriendshipRawPayload.close(), ]) this.cacheMessageRawPayload = undefined this.cacheContactOrRoomRawPayload = undefined this.cacheRoomInvitationRawPayload = undefined this.cacheRoomMemberIdList = undefined this.cacheLatestMessageTimestampForChat = undefined this.cacheFriendshipRawPayload = undefined log.silly(PRE, 'releaseCache() cache closed.') } else { log.verbose(PRE, 'releaseCache() cache not exist.') } } }