diff --git a/package.json b/package.json index ac0d5d6..04ef42a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "wechaty-puppet-padplus", - "version": "0.7.43", + "name": "@juzi/wechaty-puppet-padplus", + "version": "1.4.0", "description": "Puppet Padplus for Wechaty", "directories": { "test": "tests" @@ -26,6 +26,9 @@ "type": "git", "url": "git+https://github.com/botorange/wechaty-puppet-padplus.git" }, + "files": [ + "dist/" + ], "keywords": [ "chatie", "wechaty", @@ -58,30 +61,33 @@ "memory-card": "^0.7.0", "pkg-jq": "^0.2.2", "shx": "^0.3.1", + "ts-node": "^10.9.1", "ts-protoc-gen": "^0.12.0", "tstest": "^0.4.1", - "wechaty-puppet": "^0.32.0" - }, - "peerDependencies": { - "wechaty-puppet": "^0.32.0" + "typescript": "~4.7.4" }, "dependencies": { + "@ffmpeg-installer/ffmpeg": "^1.1.0", + "@juzi/wechaty-puppet": "^1.0.48", + "@juzi/wechaty-puppet-cache": "^1.0.0", "array-flatten": "^3.0.0", "axios": "^0.19.2", "brolog": "^1.6.5", "ffmpeg-extract-frames": "^2.0.2", "ffmpeg-probe": "^1.0.6", + "file-box": "^1.5.5", "flash-store": "^0.14.5", "fs-extra": "^8.1.0", "google-protobuf": "^3.6.1", "grpc": "^1.22.0", + "install": "^0.13.0", "lru-cache": "^5.1.1", + "npm": "^9.6.2", "promise-retry": "^1.1.1", "rx-queue": "^0.8.5", "state-switch": "^0.9.7", "uuid": "^7.0.0", "watchdog": "^0.8.10", - "wechaty-puppet-cache": "^0.1.10", "xml2js": "^0.4.22" }, "publishConfig": { diff --git a/src/config.ts b/src/config.ts index ceab01e..62f3288 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,6 +18,24 @@ export const padplusToken = () => { return token } +export const NO_USER_INFO_IN_REDIS = ` + + +=========================================================================================== + + WARNING!!! Something wrong with your memory-card file! + Please remove it and restart the bot, then it will be ok. + This default path of memory-card file in the root dir of your project, + Path like: ${process.cwd()}/your-name-memory-card.json + + MemoryCard文件出现问题,请删除后重启bot,即可正常使用。 + MemoryCard文件默认在项目根目录下,路径示例:${process.cwd()}/your-name-memory-card.json + +=========================================================================================== + + +` + export const INVALID_TOKEN_MESSAGE = ` @@ -50,22 +68,37 @@ export const EXPIRED_TOKEN_MESSAGE = ` ` +const SECOND = 1000 +const MINUTE = 60 * SECOND + export const PADPLUS_REPLAY_MESSAGE = process.env.PADPLUS_REPLAY_MESSAGE === 'true' /** * GRPC server */ const WECHATY_PUPPET_PADPLUS_ENDPOINT_ENV_VAR = 'WECHATY_PUPPET_PADPLUS_ENDPOINT' -export const GRPC_ENDPOINT = process.env[WECHATY_PUPPET_PADPLUS_ENDPOINT_ENV_VAR] || 'padplus.juzibot.com:50051' +export const GRPC_ENDPOINT = process.env[WECHATY_PUPPET_PADPLUS_ENDPOINT_ENV_VAR] || 'beijing.padplus.juzibot.com:50052' + +export const MESSAGE_CACHE_AGE = 60 * MINUTE +export const MESSAGE_CACHE_MAX = SECOND + +export const WAIT_FOR_READY_TIME = 10 * MINUTE + +export const COMPACT_CACHE_FIRST_START = 15 * MINUTE -export const MESSAGE_CACHE_AGE = 1000 * 60 * 60 -export const MESSAGE_CACHE_MAX = 1000 +export const COMPACT_CACHE_INTERVAL = 60 * MINUTE -export const WAIT_FOR_READY_TIME = 1000 * 60 * 10 +// sync data timeout in minute +export const SYNC_CONTACT_TIMEOUT = (Number(process.env.SYNC_CONTACT_TIMEOUT) || 2) * MINUTE +export const SYNC_ROOM_TIMEOUT = (Number(process.env.SYNC_ROOM_TIMEOUT) || 2) * MINUTE +export const SYNC_MEMBER_TIMEOUT = (Number(process.env.SYNC_MEMBER_TIMEOUT) || 2) * MINUTE -export const COMPACT_CACHE_FIRST_START = 1000 * 60 * 15 +// delay get data interval in second +export const MEMBER_DELAY_INTERVAL = (Number(process.env.MEMBER_DELAY_INTERVAL) || 0.5) * SECOND -export const COMPACT_CACHE_INTERVAL = 1000 * 60 * 60 +// Expire time for api call data that persist in the pool +// Number of seconds +export const EXPIRE_TIME = (Number(process.env.EXPIRE_TIME) || 1) * SECOND const logLevel = process.env.PADPLUS_LOG || process.env.WECHATY_LOG if (logLevel) { diff --git a/src/convert-manager/contact-convertor.ts b/src/convert-manager/contact-convertor.ts index 7d6f199..041a1ae 100644 --- a/src/convert-manager/contact-convertor.ts +++ b/src/convert-manager/contact-convertor.ts @@ -1,4 +1,4 @@ -import { ContactGender, ContactType } from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { PadplusContactPayload, GrpcContactPayload, TagNewOrListResponse, TagNewOrListGrpcResponse, GetContactSelfInfoGrpcResponse, GrpcSearchContact } from '../schemas' export const convertFromGrpcContact = (contactPayload: GrpcContactPayload, isSync?: boolean): PadplusContactPayload => { @@ -12,7 +12,7 @@ export const convertFromGrpcContact = (contactPayload: GrpcContactPayload, isSyn nickName : contactPayload.NickName, province : contactPayload.Province, remark : contactPayload.RemarkName, - sex : contactPayload.Sex as ContactGender, + sex : contactPayload.Sex as types.ContactGender, signature : contactPayload.Signature, smallHeadUrl : contactPayload.SmallHeadImgUrl, stranger : contactPayload.EncryptUsername, @@ -48,7 +48,7 @@ export const convertFromGrpcContactSelf = (contactPayload: GetContactSelfInfoGrp nickName : contactPayload.nickName, province : contactPayload.province, remark : '', - sex : contactPayload.sex as ContactGender, + sex : contactPayload.sex as types.ContactGender, signature : contactPayload.signature, smallHeadUrl : contactPayload.smallHeadImg, stranger : '', @@ -65,13 +65,13 @@ export const convertSearchContactToContact = (searchContact: GrpcSearchContact, alias: isNumber ? '' : searchContact.searchId, bigHeadUrl: searchContact.avatar, city: '', - contactFlag: ContactType.Unknown, + contactFlag: types.Contact.Unknown, contactType: 0, country: '', nickName: searchContact.nickName, province: '', remark: '', - sex: ContactGender.Unknown, + sex: types.ContactGender.Unknown, signature: '', smallHeadUrl: searchContact.avatar, stranger: searchContact.v1, diff --git a/src/convert-manager/message-convertor.ts b/src/convert-manager/message-convertor.ts index 114187a..175dfbf 100644 --- a/src/convert-manager/message-convertor.ts +++ b/src/convert-manager/message-convertor.ts @@ -5,7 +5,7 @@ import { GrpcMessagePayload, WechatAppMessageType, } from '../schemas' -import { MiniProgramPayload } from 'wechaty-puppet' +import { payloads } from '@juzi/wechaty-puppet' export async function convertMessageFromGrpcToPadplus (rawMessage: GrpcMessagePayload): Promise { const messagePayload: PadplusMessagePayload = { @@ -34,7 +34,7 @@ export async function convertMessageFromGrpcToPadplus (rawMessage: GrpcMessagePa return messagePayload } -export function convertMiniProgramPayloadToParams (miniProgramPayload: MiniProgramPayload): MiniProgramParamsPayload { +export function convertMiniProgramPayloadToParams (miniProgramPayload: payloads.MiniProgram): MiniProgramParamsPayload { const content: MiniProgramParamsPayload = { aeskey: miniProgramPayload.thumbKey || '', appid: miniProgramPayload.appid || '', diff --git a/src/convert-manager/room-convertor.ts b/src/convert-manager/room-convertor.ts index 233cf1d..82832c4 100644 --- a/src/convert-manager/room-convertor.ts +++ b/src/convert-manager/room-convertor.ts @@ -1,27 +1,27 @@ -import { RoomMemberPayload } from 'wechaty-puppet' +import { payloads } from '@juzi/wechaty-puppet' import { PadplusRoomPayload, PadplusRoomMemberPayload, GrpcRoomPayload } from '../schemas' export const convertRoomFromGrpc = (room: GrpcRoomPayload): PadplusRoomPayload => { const roomPayload: PadplusRoomPayload = { - alias : room.Alias, - bigHeadUrl : room.BigHeadImgUrl, - chatRoomOwner : room.ChatRoomOwner, - chatroomId : room.UserName, - chatroomVersion: room.ChatroomVersion, - contactType : room.ContactType, - memberCount : JSON.parse(room.ExtInfo).length, - members : JSON.parse(room.ExtInfo), - nickName : room.NickName, - smallHeadUrl : room.SmallHeadImgUrl, - stranger : room.EncryptUsername, - tagList : room.LabelLists, - ticket : room.Ticket, + alias : room?.Alias || '', + bigHeadUrl : room?.BigHeadImgUrl || '', + chatRoomOwner : room?.ChatRoomOwner || '', + chatroomId : room?.UserName || '', + chatroomVersion: room?.ChatroomVersion || 0, + contactType : room?.ContactType || 0, + memberCount : JSON.parse(room?.ExtInfo || '[]').length, + members : JSON.parse(room?.ExtInfo || '[]'), + nickName : room?.NickName || '', + smallHeadUrl : room?.SmallHeadImgUrl || '', + stranger : room?.EncryptUsername || '', + tagList : room?.LabelLists || '', + ticket : room?.Ticket || '', } return roomPayload } -export const convertToPuppetRoomMember = (input: PadplusRoomMemberPayload): RoomMemberPayload => { - const resut: RoomMemberPayload = { +export const convertToPuppetRoomMember = (input: PadplusRoomMemberPayload): payloads.RoomMember => { + const resut: payloads.RoomMember = { avatar : input.smallHeadUrl, id : input.contactId, inviterId : input.inviterId, // 'wxid_7708837087612', diff --git a/src/padplus-manager/api-request/dedupeApi.ts b/src/padplus-manager/api-request/dedupeApi.ts index 421fda0..20ab8ca 100644 --- a/src/padplus-manager/api-request/dedupeApi.ts +++ b/src/padplus-manager/api-request/dedupeApi.ts @@ -1,15 +1,11 @@ import { DelayQueueExecutor } from 'rx-queue' -import { log } from '../../config' +import { EXPIRE_TIME, log } from '../../config' import { ApiType } from '../../server-manager/proto-ts/PadPlusServer_pb' import { ApiTypeDic } from '../../utils/util' -// Expire time for api call data that persist in the pool -// Number of seconds -const EXPIRE_TIME = 10 - -const DEDUPE_API = [ - ApiType.GET_CONTACT, - ApiType.GET_ROOM_MEMBER, +const DEDUPE_API: ApiType[] = [ + // ApiType.GET_CONTACT, + // ApiType.GET_ROOM_MEMBER, ] interface ApiCall { @@ -45,7 +41,7 @@ export class DedupeApi { constructor () { this.pool = {} - this.cleaner = setInterval(this.cleanData, EXPIRE_TIME * 1000) + this.cleaner = setInterval(this.cleanData, EXPIRE_TIME) this.apiQueue = new DelayQueueExecutor(200) } @@ -60,19 +56,19 @@ export class DedupeApi { log.silly(PRE, `dedupe() no need to dedupe api ${ApiTypeDic[apiName]}.`) return func(apiName, uin, params) } - log.silly(PRE, `dedupeApi(${apiName}, ${uin}, ${params ? JSON.stringify(params) : ''})`) + log.silly(PRE, `dedupeApi(${this.getApiTypeKey(apiName)}, ${uin}, ${params ? JSON.stringify(params) : ''})`) const key = this.getKey(apiName, uin, params) if (forceCall) { delete this.pool[key] } const existCall = this.pool[key] const now = new Date().getTime() - if (existCall && now - existCall.timestamp < EXPIRE_TIME * 1000) { + if (existCall && now - existCall.timestamp < EXPIRE_TIME) { if (existCall.returned) { - log.silly(PRE, `dedupeApi(${apiName}) dedupe api call with existing results.`) + log.silly(PRE, `dedupeApi(${this.getApiTypeKey(apiName)}) dedupe api call with existing results.`) return existCall.result } else { - log.silly(PRE, `dedupeApi(${apiName}) dedupe api call with pending listener.`) + log.silly(PRE, `dedupeApi(${this.getApiTypeKey(apiName)}) dedupe api call with pending listener.`) return new Promise((resolve, reject) => { existCall.listener.push({ reject, @@ -81,7 +77,7 @@ export class DedupeApi { }) } } else { - log.silly(PRE, `dedupeApi(${apiName}) dedupe api call missed, call the external service.`) + log.silly(PRE, `dedupeApi(${this.getApiTypeKey(apiName)}) dedupe api call missed, call the external service.`) this.pool[key] = { listener: [], returned: false, @@ -91,7 +87,7 @@ export class DedupeApi { try { result = await this.apiQueue.execute(() => func(apiName, uin, params)) } catch (e) { - log.silly(PRE, `dedupeApi(${apiName}) failed from external service, reject ${this.pool[key].listener.length} duplicate api calls.`) + log.silly(PRE, `dedupeApi(${this.getApiTypeKey(apiName)}) failed from external service, reject ${this.pool[key].listener.length} duplicate api calls.`) this.pool[key].listener.map(api => { api.reject(e) }) @@ -101,7 +97,7 @@ export class DedupeApi { this.pool[key].result = result this.pool[key].returned = true - log.silly(PRE, `dedupeApi(${apiName}) got results from external service, resolve ${this.pool[key].listener.length} duplicate api calls.`) + log.silly(PRE, `dedupeApi(${this.getApiTypeKey(apiName)}) got results from external service, resolve ${this.pool[key].listener.length} duplicate api calls.`) this.pool[key].listener.map(api => { api.resolve(result) }) @@ -129,7 +125,7 @@ export class DedupeApi { for (const key in this.pool) { if (this.pool.hasOwnProperty(key)) { const apiCache = this.pool[key] - if (apiCache.timestamp - now > EXPIRE_TIME * 1000) { + if (apiCache.timestamp - now > EXPIRE_TIME) { delete this.pool[key] } } @@ -140,4 +136,10 @@ export class DedupeApi { return `${apiName}-${uin}-${params ? JSON.stringify(params) : ''}` } + private getApiTypeKey (apiType: number): string | undefined { + const keys = Object.keys(ApiType).filter(key => isNaN(Number(key))) + const apiTypeKey = keys.find(key => ApiType[key as keyof typeof ApiType] === apiType) + return apiTypeKey || undefined + } + } diff --git a/src/padplus-manager/api-request/message.ts b/src/padplus-manager/api-request/message.ts index 0a1590a..12aef9e 100644 --- a/src/padplus-manager/api-request/message.ts +++ b/src/padplus-manager/api-request/message.ts @@ -1,9 +1,9 @@ import { log } from '../../config' import { RequestClient } from './request' import { ApiType, StreamResponse } from '../../server-manager/proto-ts/PadPlusServer_pb' -import { PadplusMessageType, PadplusRichMediaData, GrpcResponseMessageData, PadplusRecallData, PadplusUploadFileData } from '../../schemas' -import { WechatAppMessageType } from 'wechaty-puppet/dist/src/schemas/message' -import { FileBox } from 'wechaty-puppet' +import { PadplusMessageType, PadplusRichMediaData, GrpcResponseMessageData, PadplusRecallData, PadplusUploadFileData, PadplusGetCDNRequestData } from '../../schemas' +import { types } from '@juzi/wechaty-puppet' +import { FileBox } from 'file-box' const PRE = 'PadplusMessage' @@ -122,7 +122,7 @@ export class PadplusMessage { url, } data = { - appMsgType: WechatAppMessageType.Attach, + appMsgType: types.Message.Attachment, content: JSON.stringify(content), fileName, fromUserName: selfId, @@ -191,8 +191,23 @@ export class PadplusMessage { } } - public async loadRichMeidaData (mediaData: PadplusRichMediaData): Promise { - log.silly(PRE, `loadRichMeidaData()`) + public async getCDNData (data: PadplusGetCDNRequestData): Promise { + log.silly(PRE, `getCDNData()`) + + const response = await this.requestClient.request({ + apiType: ApiType.GET_CDN_DATA, + data, + }) + + if (response) { + return response + } else { + throw new Error(`can not get callback result of GET_CDN_DATA`) + } + } + + public async loadRichMediaData (mediaData: PadplusRichMediaData): Promise { + log.silly(PRE, `loadRichMediaData()`) const response = await this.requestClient.request({ apiType: ApiType.GET_MESSAGE_MEDIA, @@ -235,7 +250,7 @@ export class PadplusMessage { } public async uploadFile (fileBox: FileBox): Promise { - log.verbose(PRE, `recallMessage`) + log.verbose(PRE, `uploadFile`) const data = { data: await fileBox.toBase64(), filename: fileBox.name, diff --git a/src/padplus-manager/api-request/rate-manager.ts b/src/padplus-manager/api-request/rate-manager.ts new file mode 100644 index 0000000..65074a3 --- /dev/null +++ b/src/padplus-manager/api-request/rate-manager.ts @@ -0,0 +1,116 @@ +import { EventEmitter } from 'events' +import { log } from '../../config' +import { sleep } from '../../utils/util' + +interface FunctionObj { + func: () => any, + resolve: (data: any) => void, + reject: (e: any) => void, + delayBefore?: number, + delayAfter?: number, + uniqueKey?: string, +} + +export interface RateOptions { + queueId?: string, + delayBefore?: number, + delayAfter?: number, + uniqueKey?: string, +} + +type RateManagerEvents = 'error' + +const MAX_QUEUE_SIZE = 20000 + +export class RateManager extends EventEmitter { + + private counter = 0 + + public emit (event: 'error', error: string): boolean + public emit (event: never, ...args: never[]): never + public emit (event: RateManagerEvents, ...args: any[]): boolean { + return super.emit(event, ...args) + } + + public on (event: 'error', listener: (error: string) => void): this + public on (event: never, listener: never): never + public on (event: RateManagerEvents, listener : (...args: any[]) => void): this { + super.on(event, listener) + return this + } + + private functionQueueMap: { [id: string]: FunctionObj[] } = {} + private runningMap: { [id: string]: boolean } = {} + + public getQueueLength (queueId: string) { + if (!this.functionQueueMap[queueId]) { + return 0 + } + return this.functionQueueMap[queueId].length + } + + public async exec (func: () => T, options: RateOptions = {}) { + const queueId = options.queueId || 'default' + const { delayAfter, delayBefore, uniqueKey } = options + + if (!this.functionQueueMap[queueId]) { + this.functionQueueMap[queueId] = [] + } + + if (this.functionQueueMap[queueId].length > MAX_QUEUE_SIZE) { + if (this.counter % MAX_QUEUE_SIZE === 0) { + log.error(`EXCEED_QUEUE_SIZE: Max queue size for id: ${queueId} reached: ${this.functionQueueMap[queueId].length} > ${MAX_QUEUE_SIZE}(max queue size). Drop these tasks.`) + this.counter = 0 + } + this.counter++ + } + + return new Promise(async (resolve, reject) => { + this.functionQueueMap[queueId].push({ delayAfter, delayBefore, func, reject, resolve, uniqueKey }) + if (!this.runningMap[queueId]) { + this.runningMap[queueId] = true + await this.execNext(queueId) + } + }) + } + + private async execNext (queueId: string) { + const queue = this.functionQueueMap[queueId] + if (!queue) { + return + } + + const funcObj = queue.shift() + if (!funcObj) { + throw new Error(`can not get funcObj from queue with id: ${queueId}.`) + } + const { delayAfter, delayBefore, func, resolve, reject, uniqueKey } = funcObj + await sleep(delayBefore) + try { + const result = await func() + resolve(result) + /** + * If uniqueKey is given, will resolve functions with same key in the queue + */ + if (uniqueKey) { + const sameFuncIndexes = queue.map((f, index) => ({ func: f, index })) + .filter(o => o.func.uniqueKey === uniqueKey) + .map(o => o.index) + .sort((a, b) => b - a) + for (const index of sameFuncIndexes) { + const [sameFunc] = queue.splice(index, 1) + sameFunc.resolve(result) + } + } + } catch (e) { + reject(e) + } + await sleep(delayAfter) + if (queue.length > 0) { + await this.execNext(queueId) + } else { + delete this.runningMap[queueId] + } + } + +} diff --git a/src/padplus-manager/api-request/room.ts b/src/padplus-manager/api-request/room.ts index 2b80502..ffbc218 100644 --- a/src/padplus-manager/api-request/room.ts +++ b/src/padplus-manager/api-request/room.ts @@ -62,6 +62,11 @@ export class PadplusRoom { }) } + /** + * 服务端会清除member的redis数据 + * @param uin 账号uin + * @param roomId 群聊ID + */ public getRoomMembers = async (uin: string, roomId: string): Promise => { log.verbose(PRE, `getRoomMembers(${uin}, ${roomId})`) diff --git a/src/padplus-manager/padplus-manager.ts b/src/padplus-manager/padplus-manager.ts index d79beb5..c11df94 100644 --- a/src/padplus-manager/padplus-manager.ts +++ b/src/padplus-manager/padplus-manager.ts @@ -3,12 +3,16 @@ import { DelayQueueExecutor, ThrottleQueue, } from 'rx-queue' import { StateSwitch } from 'state-switch' -import { log, GRPC_ENDPOINT, MESSAGE_CACHE_MAX, MESSAGE_CACHE_AGE, WAIT_FOR_READY_TIME, INVALID_TOKEN_MESSAGE, EXPIRED_TOKEN_MESSAGE } from '../config' +import { log, GRPC_ENDPOINT, MESSAGE_CACHE_MAX, MESSAGE_CACHE_AGE, WAIT_FOR_READY_TIME, INVALID_TOKEN_MESSAGE, EXPIRED_TOKEN_MESSAGE, NO_USER_INFO_IN_REDIS, SYNC_CONTACT_TIMEOUT, SYNC_ROOM_TIMEOUT, SYNC_MEMBER_TIMEOUT, MEMBER_DELAY_INTERVAL } from '../config' import LRU from 'lru-cache' import { GrpcGateway } from '../server-manager/grpc-gateway' import { StreamResponse, ResponseType } from '../server-manager/proto-ts/PadPlusServer_pb' -import { ScanStatus, ContactGender, FileBox, FriendshipPayload, EventRoomLeavePayload, MemoryCard } from 'wechaty-puppet' +import { FileBox } from 'file-box' +import { + payloads, + types, +} from '@juzi/wechaty-puppet' import { RequestClient } from './api-request/request' import { PadplusUser } from './api-request/user' import { PadplusContact } from './api-request/contact' @@ -40,6 +44,8 @@ import { TagPayload, PadplusRoomInviteEvent, LoginDeviceInfo, + PadplusGetCDNRequestData, + PadplusCDNData, } from '../schemas' import { convertMessageFromGrpcToPadplus } from '../convert-manager/message-convertor' import { CacheManager } from '../server-manager/cache-manager' @@ -49,10 +55,12 @@ import { convertRoomFromGrpc } from '../convert-manager/room-convertor' import { CallbackPool } from '../utils/callbackHelper' import { PadplusFriendship } from './api-request/friendship' import { briefRoomMemberParser, roomMemberParser } from '../pure-function-helpers/room-member-parser' -import { isRoomId, isContactId } from '../pure-function-helpers' +import { isRoomId, isContactId, isIMContactId, isIMRoomId } from '../pure-function-helpers' import { EventEmitter } from 'events' import { videoPreProcess } from '../pure-function-helpers/video-process' -import { PuppetCacheStoreOptions } from 'wechaty-puppet-cache' +import { PuppetCacheStoreOptions } from '@juzi/wechaty-puppet-cache' +import { MemoryCard } from '@juzi/wechaty-puppet/dist/esm/src/config' +import { RateManager } from './api-request/rate-manager' const MEMORY_SLOT_NAME = 'WECHATY_PUPPET_PADPLUS' @@ -79,14 +87,14 @@ export class PadplusManager extends EventEmitter { private readonly state : StateSwitch private requestClient? : RequestClient private padplusUser? : PadplusUser - private padplusMesasge? : PadplusMessage + private padplusMessage? : PadplusMessage private padplusContact? : PadplusContact private padplusRoom? : PadplusRoom private padplusFriendship? : PadplusFriendship public cacheManager? : CacheManager private memory? : MemoryCard private memorySlot : PadplusMemorySlot - private qrcodeStatus? : ScanStatus + private qrcodeStatus? : types.ScanStatus private loginStatus? : boolean public readonly cachePadplusMessagePayload: LRU public readonly cachePadplusSearchContactPayload: LRU @@ -98,8 +106,8 @@ export class PadplusManager extends EventEmitter { readyEmitted: boolean, } private resetThrottleQueue : ThrottleQueue - private getContactQueue : DelayQueueExecutor private getRoomMemberQueue : DelayQueueExecutor + protected rateManager: RateManager constructor ( public options: ManagerOptions, ) { @@ -133,8 +141,8 @@ export class PadplusManager extends EventEmitter { userName: '', } - this.getContactQueue = new DelayQueueExecutor(200) - this.getRoomMemberQueue = new DelayQueueExecutor(500) + this.rateManager = new RateManager() + this.getRoomMemberQueue = new DelayQueueExecutor(MEMBER_DELAY_INTERVAL) this.resetThrottleQueue = new ThrottleQueue(5000) this.resetThrottleQueue.subscribe(async reason => { log.info(PRE, 'ready to restart due to receive event: %s', reason) @@ -146,7 +154,7 @@ export class PadplusManager extends EventEmitter { delete this.padplusContact delete this.padplusFriendship delete this.padplusRoom - delete this.padplusMesasge + delete this.padplusMessage delete this.requestClient await this.start() @@ -161,7 +169,7 @@ export class PadplusManager extends EventEmitter { public emit (event: 'contact-delete', data: string): boolean public emit (event: 'message', msg: PadplusMessagePayload): boolean public emit (event: 'room-member-list', data: string): boolean - public emit (event: 'room-leave', data: EventRoomLeavePayload): boolean + public emit (event: 'room-leave', data: payloads.EventRoomLeave): boolean public emit (event: 'room-member-modify', data: string): boolean public emit (event: 'status-notify', data: string): boolean public emit (event: 'ready'): boolean @@ -186,7 +194,7 @@ export class PadplusManager extends EventEmitter { public on (event: 'reset', listener: ((this: PadplusManager, reason: string) => void)): this public on (event: 'heartbeat', listener: ((this: PadplusManager, data: string) => void)): this public on (event: 'error', listener: ((this: PadplusManager, error: Error) => void)): this - public on (event: 'room-leave', listener: ((this: PadplusManager, data: EventRoomLeavePayload) => void)): this + public on (event: 'room-leave', listener: ((this: PadplusManager, data: payloads.EventRoomLeave) => void)): this public on (event: never, listener: never): never public on (event: PadplusManagerEvent, listener: ((...args: any[]) => any)): this { @@ -222,7 +230,7 @@ export class PadplusManager extends EventEmitter { } this.requestClient = new RequestClient(GrpcGateway.Instance, emitter) this.padplusUser = new PadplusUser(this.requestClient) - this.padplusMesasge = new PadplusMessage(this.requestClient) + this.padplusMessage = new PadplusMessage(this.requestClient) this.padplusContact = new PadplusContact(this.requestClient) this.padplusRoom = new PadplusRoom(this.requestClient) this.padplusFriendship = new PadplusFriendship(this.requestClient) @@ -395,7 +403,7 @@ export class PadplusManager extends EventEmitter { const fileBox = FileBox.fromBase64(qrcodeData.qrcode, `qrcode${(Math.random() * 10000).toFixed()}.png`) const qrcodeUrl = await fileBox.toQRCode() - this.qrcodeStatus = ScanStatus.Waiting + this.qrcodeStatus = types.ScanStatus.Waiting this.emit('scan', qrcodeUrl, this.qrcodeStatus) } break @@ -405,40 +413,47 @@ export class PadplusManager extends EventEmitter { if (scanRawData) { log.silly(PRE, `QRCODE_SCAN : ${util.inspect(scanRawData)}`) const scanData: ScanData = JSON.parse(scanRawData) - log.info(PRE, ` + log.verbose(PRE, ` ================================================= - QRCODE_SCAN MSG : ${scanData.msg || '已确认'} + QRCODE_SCAN MSG : ${QrcodeStatus[scanData.status] || 'unknown status'} ================================================= `) grpcGatewayEmitter.setQrcodeId(scanData.user_name) switch (scanData.status as QrcodeStatus) { case QrcodeStatus.Scanned: - if (this.qrcodeStatus !== ScanStatus.Scanned) { - this.qrcodeStatus = ScanStatus.Scanned + if (this.qrcodeStatus !== types.ScanStatus.Scanned) { + this.qrcodeStatus = types.ScanStatus.Scanned this.emit('scan', '', this.qrcodeStatus) } break case QrcodeStatus.Confirmed: - if (this.qrcodeStatus !== ScanStatus.Confirmed) { - this.qrcodeStatus = ScanStatus.Confirmed + if (this.qrcodeStatus !== types.ScanStatus.Confirmed) { + this.qrcodeStatus = types.ScanStatus.Confirmed this.emit('scan', '', this.qrcodeStatus) } break case QrcodeStatus.Canceled: case QrcodeStatus.Expired: - const uin = grpcGatewayEmitter.getUIN() - const wxid = grpcGatewayEmitter.getUserName() - const data = { - uin, - wxid, + let data: {uin: string, wxid: string} + if (this.memory) { + const slot = await this.memory!.get(MEMORY_SLOT_NAME) + data = { + uin: slot.uin, + wxid: slot.userName, + } + } else { + data = { + uin: grpcGatewayEmitter.getUIN(), + wxid: grpcGatewayEmitter.getUserName(), + } } if (scanData.status === QrcodeStatus.Expired) { - this.qrcodeStatus = ScanStatus.Timeout + this.qrcodeStatus = types.ScanStatus.Timeout } else { - this.qrcodeStatus = ScanStatus.Cancel + this.qrcodeStatus = types.ScanStatus.Cancel } this.emit('scan', '', this.qrcodeStatus) @@ -491,7 +506,7 @@ export class PadplusManager extends EventEmitter { nickName: loginData.nickName, province: '', remark: '', - sex: ContactGender.Unknown, + sex: types.ContactGender.Unknown, signature: '', smallHeadUrl: '', stranger: '', @@ -519,6 +534,9 @@ export class PadplusManager extends EventEmitter { if (autoLoginData && autoLoginData.online) { if (!this.loginStatus) { const wechatUser = autoLoginData.wechatUser + if (!wechatUser) { + throw new Error(NO_USER_INFO_IN_REDIS) + } log.verbose(PRE, `init cache manager`) await CacheManager.init(wechatUser.userName, this.options.cacheOption) this.cacheManager = CacheManager.Instance @@ -538,7 +556,7 @@ export class PadplusManager extends EventEmitter { nickName: wechatUser.nickName, province: '', remark: '', - sex: ContactGender.Unknown, + sex: types.ContactGender.Unknown, signature: '', smallHeadUrl: '', stranger: '', @@ -633,6 +651,12 @@ export class PadplusManager extends EventEmitter { } else if (isRoomId(_data.UserName)) { const roomData: GrpcRoomPayload = _data const roomPayload: PadplusRoomPayload = convertRoomFromGrpc(roomData) + if (Object.keys(roomData).length === 1) { + log.silly(PRE, `the bot has already left this room: ${_data.UserName}`) + roomPayload.chatRoomOwner = this.memorySlot.userName // FIXME: set fake room owner + CallbackPool.Instance.resolveRoomCallBack(_data.UserName, roomPayload) + return + } if (this.cacheManager) { const roomMembers = briefRoomMemberParser(roomPayload.members) const _roomMembers = await this.cacheManager.getRoomMember(roomPayload.chatroomId) @@ -657,23 +681,33 @@ export class PadplusManager extends EventEmitter { const deleteUserName = contactData.field if (this.cacheManager) { if (isRoomId(deleteUserName)) { + const botId = contactData.userName const roomRawPayload = await this.cacheManager.getRoom(deleteUserName) if (!roomRawPayload) { throw new Error(`can not find room raw payload from cache by id : ${deleteUserName}`) } - roomRawPayload.members = roomRawPayload.members.filter(member => member.UserName !== contactData.userName) + roomRawPayload.members = roomRawPayload.members.filter(member => member.userName !== botId) await this.cacheManager.setRoom(deleteUserName, roomRawPayload) const roomMemberRawPayload = await this.cacheManager.getRoomMember(deleteUserName) if (!roomMemberRawPayload) { throw new Error(`can not find room member raw payload from cache by id : ${deleteUserName}`) } - delete roomMemberRawPayload[contactData.userName] + delete roomMemberRawPayload[botId] await this.cacheManager.setRoomMember(deleteUserName, roomMemberRawPayload) + const eventRoomLeavePayload: payloads.EventRoomLeave = { + removeeIdList : [botId], + removerId: botId, + roomId: deleteUserName, + timestamp: Date.now(), + } + this.emit('room-leave', eventRoomLeavePayload) } else if (isContactId(deleteUserName)) { await this.cacheManager.deleteContact(deleteUserName) - } else { - throw new Error(`the filed is not right.`) + } else if (isIMContactId(deleteUserName)) { + // throw new Error(`the filed is not right.`) + // TODO: im contact deleted event + await this.cacheManager.deleteContact(deleteUserName) } } } @@ -683,6 +717,10 @@ export class PadplusManager extends EventEmitter { if (rawMessageStr) { const rawMessage: GrpcMessagePayload = JSON.parse(rawMessageStr) const message: PadplusMessagePayload = await this.onProcessMessage(rawMessage) + if (isIMContactId(message.fromUserName) || isIMRoomId(message.fromUserName) || isIMRoomId(message.toUserName)) { + log.silly(PRE, `receive im message: ${message.msgId}`) + return + } this.emit('message', message) } break @@ -707,7 +745,7 @@ export class PadplusManager extends EventEmitter { CallbackPool.Instance.removeCallback(roomQrcodeTraceId) } break - case ResponseType.ROOM_MEMBER_LIST : + case ResponseType.ROOM_MEMBER_LIST: const roomMembersStr = data.getData() if (roomMembersStr) { if (!this.cacheManager) { @@ -715,12 +753,35 @@ export class PadplusManager extends EventEmitter { } const roomMemberList: GrpcRoomMemberList = JSON.parse(roomMembersStr) const roomId = roomMemberList.roomId - const membersStr = roomMemberList.membersJson - const membersList: GrpcRoomMemberPayload[] = JSON.parse(membersStr) if (!this.cacheManager) { throw new Error(`no manager`) } const oldMembers = await this.cacheManager.getRoomMember(roomId) + // 已经退出群聊,或者被踢出群聊,将群成员缓存更新为空对象 + if (typeof roomMemberList.membersJson === 'undefined') { + const userName = this.memorySlot.userName + log.silly(PRE, `the bot has already left this room: ${roomId} botId: ${userName}`) + let members = {} + if (oldMembers) { + delete oldMembers[userName] + await this.cacheManager.setRoomMember(roomId, oldMembers) + members = oldMembers + } else { + // 最开始不存在的群聊 membersJson 为 undefined,此时将其 roomMember 设置为空对象。 + // 目前存在一种情况:长时间不说话的群聊 membersJson 也为 undefined + await this.cacheManager.setRoomMember(roomId, members) + } + CallbackPool.Instance.resolveRoomMemberCallback(roomId, members) + return + } + const membersStr = roomMemberList.membersJson + const membersList: GrpcRoomMemberPayload[] = JSON.parse(membersStr) + + // No update to the group member cache when a notification of individual group member information change is received. + if (membersList.length === 1) { + log.warn(`only one member left in this room: ${roomId}`) + return + } if (oldMembers) { const eventRoomLeavePayload = await this.generateLeaveEvent(membersList, oldMembers, roomId) eventRoomLeavePayload && eventRoomLeavePayload.map(event => this.emit('room-leave', event)) @@ -744,7 +805,7 @@ export class PadplusManager extends EventEmitter { nickName: member.NickName, province: '', remark: member.RemarkName, - sex: ContactGender.Unknown, + sex: types.ContactGender.Unknown, signature: '', smallHeadUrl: member.HeadImgUrl, stranger: '', @@ -854,13 +915,30 @@ export class PadplusManager extends EventEmitter { /** * Message Section */ + public async getCDNData (mediaData: PadplusGetCDNRequestData): Promise { + log.silly(PRE, `getCDNData()`) + + if (!this.padplusMessage) { + throw new Error(`no padplus message`) + } + // TODO: get/save cdn result in cache + const data = await this.padplusMessage.getCDNData(mediaData) + const cdnStr = data.getData() + if (cdnStr) { + const cdnData = JSON.parse(cdnStr) + return cdnData + } else { + throw new Error(`can not load media data on manager`) + } + } + public async loadRichMediaData (mediaData: PadplusRichMediaData): Promise { log.silly(PRE, `loadRichMediaData()`) - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - const data = await this.padplusMesasge.loadRichMeidaData(mediaData) + const data = await this.padplusMessage.loadRichMediaData(mediaData) const mediaStr = data.getData() if (mediaStr) { const mediaData = JSON.parse(mediaStr) @@ -873,10 +951,10 @@ export class PadplusManager extends EventEmitter { public async sendMessage (selfId: string, receiver: string, text: string, type: PadplusMessageType, mention?: string) { log.silly(PRE, `selfId : ${selfId}, receiver : ${receiver}, text : ${text}, type : ${type}`) - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - const messageResponse = await this.padplusMesasge.sendMessage(selfId, receiver, text, type, mention) + const messageResponse = await this.padplusMessage.sendMessage(selfId, receiver, text, type, mention) if (!messageResponse.msgId) { throw new Error(`This message send failed, because the response message id is : ${messageResponse.msgId}.`) } @@ -886,15 +964,15 @@ export class PadplusManager extends EventEmitter { public async sendVideo (selfId: string, receiver: string, url: string) { log.silly(PRE, `sendVideo(${selfId}, ${receiver}, ${url})`) - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } if (!this.requestClient) { throw new Error(`no request client`) } - const content = await videoPreProcess(this.padplusMesasge, url) + const content = await videoPreProcess(this.padplusMessage, url) - const messageResponse = await this.padplusMesasge.sendMessage(selfId, receiver, JSON.stringify(content), PadplusMessageType.Video) + const messageResponse = await this.padplusMessage.sendMessage(selfId, receiver, JSON.stringify(content), PadplusMessageType.Video) if (!messageResponse.msgId) { throw new Error(`This message send failed, because the response message id is : ${messageResponse.msgId}.`) } @@ -904,10 +982,10 @@ export class PadplusManager extends EventEmitter { public async sendMiniProgram (selfId: string, receiver: string, content: string) { log.silly(PRE, `sendMiniProgram(${selfId}, ${receiver}, ${content})`) - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - const messageResponse = await this.padplusMesasge.sendMessage(selfId, receiver, content, PadplusMessageType.App) + const messageResponse = await this.padplusMessage.sendMessage(selfId, receiver, content, PadplusMessageType.App) if (!messageResponse.msgId) { throw new Error(`This message send failed, because the response message id is : ${messageResponse.msgId}.`) } @@ -920,10 +998,10 @@ export class PadplusManager extends EventEmitter { if (!this.cacheManager) { throw new PadplusError(PadplusErrorType.NO_CACHE, `sendContact()`) } - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - return this.padplusMesasge.sendVoice(selfId, receiver, url, fileSize) + return this.padplusMessage.sendVoice(selfId, receiver, url, fileSize) } public async sendContact (selfId: string, receiver: string, contentStr: string) { @@ -932,10 +1010,10 @@ export class PadplusManager extends EventEmitter { if (!this.cacheManager) { throw new PadplusError(PadplusErrorType.NO_CACHE, `sendContact()`) } - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - return this.padplusMesasge.sendContact(selfId, receiver, contentStr) + return this.padplusMessage.sendContact(selfId, receiver, contentStr) } public async addFriend ( @@ -956,30 +1034,30 @@ export class PadplusManager extends EventEmitter { public async generatorFileUrl (file: FileBox): Promise { log.verbose(PRE, 'generatorFileUrl(%s)', file) - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - return this.padplusMesasge.uploadFile(file) + return this.padplusMessage.uploadFile(file) } public async sendFile (selfId: string, receiverId: string, url: string, fileName: string, subType: string, fileSize?: number) { log.verbose(PRE, 'sendFile()') - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - return this.padplusMesasge.sendFile(selfId, receiverId, url, fileName, subType, fileSize) + return this.padplusMessage.sendFile(selfId, receiverId, url, fileName, subType, fileSize) } public async sendUrlLink (selfId: string, receiver: string, content: string) { log.verbose(PRE, 'sendUrlLink()') - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - return this.padplusMesasge.sendUrlLink(selfId, receiver, content) + return this.padplusMessage.sendUrlLink(selfId, receiver, content) } private async onProcessMessage (rawMessage: any): Promise { @@ -1127,7 +1205,7 @@ export class PadplusManager extends EventEmitter { } await this.padplusContact.setAlias(selfId, contactId, alias) return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('set alias failed since timeout')), 5000) + const timeout = setTimeout(() => reject(new Error(`set contact ${contactId} alias failed since timeout`)), 50 * 1000) CallbackPool.Instance.pushContactAliasCallback(contactId, alias, () => { clearTimeout(timeout) resolve() @@ -1144,34 +1222,41 @@ export class PadplusManager extends EventEmitter { throw new PadplusError(PadplusErrorType.NO_CACHE, 'contactList()') } - return this.cacheManager.getContactIds() + const contactIdList = await this.cacheManager.getContactIds() + return contactIdList.filter(id => !isIMContactId(id)) } public async getContact ( contactId: string ): Promise { - if (!this.cacheManager) { - throw new Error() - } - const contact = await this.cacheManager.getContact(contactId) - if (contact) { - return contact + if (isIMContactId(contactId)) { + throw new Error(`getContact(${contactId}) is not supported for IM contact`) } + const key = `get-contact-${contactId}` + return this.rateManager.exec>(async () => { + if (!this.cacheManager) { + throw new Error('no cache manager') + } + + const contact = await this.cacheManager.getContact(contactId) + if (contact) { + return contact + } - await this.getContactQueue.execute(async () => { if (!this.padplusContact) { throw new Error(`no padplusContact`) } + const promise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error(`get contact ${contactId} timeout`)), SYNC_CONTACT_TIMEOUT) + CallbackPool.Instance.pushContactCallback(contactId, (data) => { + clearTimeout(timeout) + resolve(data as PadplusContactPayload) + }) + }) await this.padplusContact.getContactInfo(contactId) - }) - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('get contact timeout')), 30 * 1000) - CallbackPool.Instance.pushContactCallback(contactId, (data) => { - clearTimeout(timeout) - resolve(data as PadplusContactPayload) - }) - }) + return promise + }, { delayAfter: 50, queueId: key, uniqueKey: key }) } public async getContactPayload ( @@ -1231,13 +1316,13 @@ export class PadplusManager extends EventEmitter { throw new Error(`no padplus Room.`) } await this.padplusRoom.setTopic(roomId, topic) - if (this.cacheManager) { - await this.cacheManager.deleteRoom(roomId) - } else { - throw new Error(`no cache manager.`) - } - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('set alias failed since timeout')), 5000) + // if (this.cacheManager) { + // await this.cacheManager.deleteRoom(roomId) + // } else { + // throw new Error(`no cache manager.`) + // } + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error(`set room ${roomId} alias failed since timeout`)), 50 * 1000) CallbackPool.Instance.pushRoomTopicCallback(roomId, topic, () => { clearTimeout(timeout) resolve() @@ -1258,7 +1343,8 @@ export class PadplusManager extends EventEmitter { if (!this.cacheManager) { throw new Error(`no cache.`) } - return this.cacheManager.getRoomIds() + const roomIdList = await this.cacheManager.getRoomIds() + return roomIdList.filter(id => !isIMRoomId(id)) } public async getRoomMemberIdList ( @@ -1287,56 +1373,66 @@ export class PadplusManager extends EventEmitter { } public async getRoom (roomId: string): Promise { - if (!this.cacheManager) { - throw new Error() + if (isIMRoomId(roomId)) { + throw new Error(`getRoom(${roomId}) is not supported for IM room`) } - const room = await this.cacheManager.getRoom(roomId) - if (room) { - return room - } - await this.getContactQueue.execute(async () => { + const key = `get-room-${roomId}` + return this.rateManager.exec>(async () => { + if (!this.cacheManager) { + throw new Error('no cache manager') + } + const room = await this.cacheManager.getRoom(roomId) + if (room) { + return room + } if (!this.padplusContact) { - throw new Error(`no padplusContact`) + throw new Error('no padplusContact') } - await this.padplusContact.getContactInfo(roomId) - }) - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('get room timeout')), 30 * 1000) - CallbackPool.Instance.pushContactCallback(roomId, (data) => { - clearTimeout(timeout) - resolve(data as PadplusRoomPayload) + const promise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error(`get room ${roomId} timeout`)), SYNC_ROOM_TIMEOUT) + CallbackPool.Instance.pushContactCallback(roomId, (data) => { + clearTimeout(timeout) + resolve(data as PadplusRoomPayload) + }) }) - }) + await this.padplusContact.getContactInfo(roomId) + return promise + }, { delayAfter: 50, queueId: key, uniqueKey: key }) } public async getRoomMembers ( roomId: string, ): Promise { - if (!this.cacheManager) { - throw new Error(`no cache.`) - } - const memberMap = await this.cacheManager.getRoomMember(roomId) - if (!memberMap) { - if (!this.grpcGatewayEmitter) { - throw new Error(`no grpcGatewayEmitter.`) + const key = `get-room-member-${roomId}` + return this.rateManager.exec>(async () => { + if (!this.cacheManager) { + throw new Error(`no cache.`) } - const uin = this.grpcGatewayEmitter.getUIN() - await this.getRoomMemberQueue.execute(async () => { - if (!this.padplusRoom) { - throw new Error(`no padplus Room.`) + const memberMap = await this.cacheManager.getRoomMember(roomId) + // 当群成员数量为0的情况下,是否该直接返回? + // if (typeof memberMap === 'undefined' || Object.keys(memberMap).length === 0) { + if (typeof memberMap === 'undefined') { + if (!this.grpcGatewayEmitter) { + throw new Error(`no grpcGatewayEmitter.`) } - await this.padplusRoom.getRoomMembers(uin, roomId) - }) - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('get room member failed since timeout')), 5000) - CallbackPool.Instance.pushRoomMemberCallback(roomId, (data: PadplusRoomMemberMap) => { - clearTimeout(timeout) - resolve(data) + const uin = this.grpcGatewayEmitter.getUIN() + await this.getRoomMemberQueue.execute(async () => { + if (!this.padplusRoom) { + throw new Error(`no padplus Room.`) + } + await this.padplusRoom.getRoomMembers(uin, roomId) }) - }) - } else { - return memberMap - } + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error(`get room ${roomId} member failed since timeout`)), SYNC_MEMBER_TIMEOUT) + CallbackPool.Instance.pushRoomMemberCallback(roomId, (data: PadplusRoomMemberMap) => { + clearTimeout(timeout) + resolve(data) + }) + }) + } else { + return memberMap + } + }, { delayAfter: 50, queueId: key, uniqueKey: key }) } public async deleteRoomMember (roomId: string, contactId: string): Promise { @@ -1489,8 +1585,8 @@ export class PadplusManager extends EventEmitter { throw new Error(`no padplusFriendship`) } await this.padplusFriendship.confirmFriendship(encryptUserName, ticket, scene) - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('accept friend request timeout.')), 60 * 1000) + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error(`accept ${contactId} friend request timeout.`)), 60 * 1000) CallbackPool.Instance.pushAcceptFriendCallback(contactId, () => { clearTimeout(timeout) resolve() @@ -1500,7 +1596,7 @@ export class PadplusManager extends EventEmitter { public async saveFriendship ( friendshipId: string, - friendship: FriendshipPayload, + friendship: payloads.Friendship, ): Promise { log.silly(PRE, `saveFriendship(${util.inspect(friendship)})`) @@ -1513,15 +1609,15 @@ export class PadplusManager extends EventEmitter { public async recallMessage (selfId: string, receiverId: string, messageId: string): Promise { log.silly(PRE, `recallMessage(${selfId}, ${receiverId}, ${messageId})`) - if (!this.padplusMesasge) { + if (!this.padplusMessage) { throw new Error(`no padplus message`) } - const isSuccess = await this.padplusMesasge.recallMessage(selfId, receiverId, messageId) + const isSuccess = await this.padplusMessage.recallMessage(selfId, receiverId, messageId) return isSuccess } - private async generateLeaveEvent (newMembers: GrpcRoomMemberPayload[], oldMembers: PadplusRoomMemberMap, roomId: string): Promise { + private async generateLeaveEvent (newMembers: GrpcRoomMemberPayload[], oldMembers: PadplusRoomMemberMap, roomId: string): Promise { log.silly(PRE, `generateLeaveEvent()`) const newLength = newMembers.length @@ -1539,7 +1635,7 @@ export class PadplusManager extends EventEmitter { }) return Object.keys(oldMembers).map(member => { - const eventRoomLeavePayload: EventRoomLeavePayload = { + const eventRoomLeavePayload: payloads.EventRoomLeave = { removeeIdList : [member], removerId: member, roomId, diff --git a/src/puppet-padplus.ts b/src/puppet-padplus.ts index df293d4..c11cb19 100644 --- a/src/puppet-padplus.ts +++ b/src/puppet-padplus.ts @@ -1,40 +1,13 @@ import util from 'util' import path from 'path' +import { FileBox } from 'file-box' import { - ContactPayload, - FileBox, - FriendshipPayload, - FriendshipPayloadReceive, - FriendshipType, - ImageType, - MessagePayload, - MessageType, - MiniProgramPayload, + payloads, + types, Puppet, PuppetOptions, - RoomInvitationPayload, - RoomMemberPayload, - RoomPayload, - ScanStatus, - UrlLinkPayload, - - EventDongPayload, - EventErrorPayload, - EventFriendshipPayload, - EventLogoutPayload, - EventMessagePayload, - EventResetPayload, - EventRoomJoinPayload, - EventRoomLeavePayload, - EventRoomTopicPayload, - EventRoomInvitePayload, - EventScanPayload, - EventReadyPayload, - EventHeartbeatPayload, - YOU, - PayloadType, -} from 'wechaty-puppet' +} from '@juzi/wechaty-puppet' import { log, @@ -59,12 +32,38 @@ import { convertSearchContactToContact } from './convert-manager/contact-convert import checkNumber from './utils/util' import { miniProgramMessageParser } from './pure-function-helpers/message-mini-program-payload-parser' import { convertMiniProgramPayloadToParams, convertMiniProgramPayloadToMessage } from './convert-manager/message-convertor' -import { PuppetCacheStoreOptions } from 'wechaty-puppet-cache' +import { PuppetCacheStoreOptions } from '@juzi/wechaty-puppet-cache' const PRE = 'PuppetPadplus' export class PuppetPadplus extends Puppet { + public async onStart (): Promise { + log.info(PRE, 'onStart()') + + // if (this.isLoggedIn) { + // this.checkDataForReadyEvent() + // } + await this.startManager(this.manager) + } + + public async onStop (): Promise { + log.info(PRE, 'stop()') + + try { + + await this.logout('logout in wechaty', true) + await this.manager.stop() + this.manager.removeAllListeners() + + } catch (e) { + log.warn(PRE, 'stop() rejection: %s', e && (e as Error).message) + // try to fix it whatever... + } + + log.info(PRE, 'stop() stopped') + } + private manager: PadplusManager private leaveEventMap: { [key: string]: NodeJS.Timer } = {} @@ -109,28 +108,13 @@ export class PuppetPadplus extends Puppet { } } - public async start (): Promise { - log.info(PRE, `start()`) - - if (this.state.on()) { - log.silly(PRE, 'start() is called on a ON puppet. await ready(on) and return.') - await this.state.ready('on') - return - } - - this.state.on('pending') - - await this.startManager(this.manager) - - this.state.on(true) - } - private async startManager (manager: PadplusManager) { - manager.on('scan', async (url: string, status: ScanStatus) => { - const eventScanPayload: EventScanPayload = { + manager.on('scan', async (url: string, status: types.ScanStatus) => { + const eventScanPayload: payloads.EventScan = { qrcode: url, status, } + this.__currentUserId = undefined this.emit('scan', eventScanPayload) }) @@ -143,17 +127,17 @@ export class PuppetPadplus extends Puppet { manager.on('message', msg => this.onMessage(msg)) - manager.on('ready', () => this.emit('ready', { data: 'ready' } as EventReadyPayload)) + manager.on('ready', () => this.emit('ready', { data: 'ready' } as payloads.EventReady)) manager.on('reset', (reason: string) => { - const eventResetPayload: EventResetPayload = { + const eventResetPayload: payloads.EventReset = { data: reason, } this.emit('reset', eventResetPayload) }) manager.on('heartbeat', (data: string) => { - const eventWatchdogPayload: EventHeartbeatPayload = { + const eventWatchdogPayload: payloads.EventHeartbeat = { data, } this.emit('heartbeat', eventWatchdogPayload) @@ -161,12 +145,12 @@ export class PuppetPadplus extends Puppet { manager.on('logout', (reason?: string) => this.logout(reason, true)) - manager.on('room-leave', (data: EventRoomLeavePayload) => { + manager.on('room-leave', (data: payloads.EventRoomLeave) => { this.deduplicateRoomLeaveEvent(data) }) manager.on('error', (err: Error) => { - const eventErrorPayload: EventErrorPayload = { + const eventErrorPayload: payloads.EventError = { data: err.toString(), } this.emit('error', eventErrorPayload) @@ -174,28 +158,6 @@ export class PuppetPadplus extends Puppet { await manager.start() } - public async stop (): Promise { - log.info(PRE, 'stop()') - - if (!this.manager) { - throw new Error('no padplus manager') - } - - if (this.state.off()) { - log.silly(PRE, 'stop() is called on a OFF puppet. await ready(off) and return.') - await this.state.ready('off') - return - } - - this.state.off('pending') - await this.logout('logout in wechaty', true) - await this.manager.stop() - this.manager.removeAllListeners() - - this.state.off(true) - log.silly(PRE, `stop() finished`) - } - /** * Three type for logout case: * - case 1: logout by WeChat App, need reset @@ -205,29 +167,17 @@ export class PuppetPadplus extends Puppet { * @param reason */ public async logout (reason?: string, force?: boolean): Promise { - log.info(PRE, `logout(${force}, ${reason})`) - - if (!this.id) { - log.silly(PRE, 'logout() this.id not exist') - return - } + log.info(PRE, `logout(${reason}, ${force})`) if (!force) { - await this.manager.logout(this.selfId()) + await this.manager.logout(this.currentUserId) reason = 'logout by call logout() method' } - const eventLogoutPayload: EventLogoutPayload = { - contactId: this.selfId(), - data: reason ? reason! : 'unknow reason', - } - this.emit('logout', eventLogoutPayload) - this.id = undefined + await super.logout(reason || 'unknown reason') if (reason !== 'logout in wechaty') { - const eventResetPayload: EventResetPayload = { - data: 'padplus reset', - } - this.emit('reset', eventResetPayload) + await this.onStop() + await this.onStart() } } @@ -242,7 +192,7 @@ export class PuppetPadplus extends Puppet { log.error(`not support receive message from WeCom`) return } - const eventMessagePayload: EventMessagePayload = { + const eventMessagePayload: payloads.EventMessage = { messageId: message.msgId, } switch (messageType) { @@ -371,14 +321,14 @@ export class PuppetPadplus extends Puppet { if (!this.manager) { throw new Error(`no padplus manage.`) } - await this.manager.setContactAlias(this.selfId(), contactId, alias || '') + await this.manager.setContactAlias(this.currentUserId, contactId, alias || '') } contactAvatar (contactId: string): Promise contactAvatar (contactId: string, file: FileBox): Promise public async contactAvatar (contactId: string, file?: FileBox): Promise { if (file) { - if (contactId !== this.selfId()) { + if (contactId !== this.currentUserId) { throw new Error(`can not set avatar for others.`) } if (!this.manager) { @@ -412,12 +362,12 @@ export class PuppetPadplus extends Puppet { if (!this.manager) { throw new Error(`no padplus manager.`) } - const selfId = this.selfId() + const selfId = this.currentUserId const contactIds = await this.manager.getContactIdList(selfId) return contactIds } - protected async contactRawPayload (contactId: string): Promise { + public async contactRawPayload (contactId: string): Promise { if (!this.manager) { throw new Error(`no manager.`) @@ -427,7 +377,7 @@ export class PuppetPadplus extends Puppet { return payload } - protected async contactRawPayloadParser (rawPayload: PadplusContactPayload): Promise { + public async contactRawPayloadParser (rawPayload: PadplusContactPayload): Promise { log.silly(PRE, `contactRawPayloadParser()`) const payload = contactRawPayloadParser(rawPayload) @@ -469,7 +419,7 @@ export class PuppetPadplus extends Puppet { } const { msgId } = message await this.manager.saveFriendship(msgId, friendship) - const eventFriendshipPayload: EventFriendshipPayload = { + const eventFriendshipPayload: payloads.EventFriendship = { friendshipId: msgId, } this.emit('friendship', eventFriendshipPayload) @@ -577,18 +527,18 @@ export class PuppetPadplus extends Puppet { if (!this.manager) { throw new Error(`no manager.`) } - const payload = await this.manager.getFriendship(friendshipId) as undefined | FriendshipPayloadReceive - if (!payload || payload.type !== FriendshipType.Receive) { + const payload = await this.manager.getFriendship(friendshipId) as undefined | payloads.FriendshipReceive + if (!payload || payload.type !== types.Friendship.Receive) { throw new Error(`can not find friendship payload ${JSON.stringify(payload)} or friendship type ${payload && payload.type} error.`) } - const { contactId, scene, stranger, ticket } = payload as FriendshipPayloadReceive + const { contactId, scene, stranger, ticket } = payload as payloads.FriendshipReceive if (!stranger || !ticket) { throw new Error(`friendship data error, stranger or ticket is null.`) } await this.manager.confirmFriendship(contactId, stranger, ticket, (scene && scene.toString()) || '3') } - protected async friendshipRawPayload (friendshipId: string): Promise { + public async friendshipRawPayload (friendshipId: string): Promise { log.silly(PRE, `friendshipRawPayload(${friendshipId})`) if (!this.manager) { @@ -601,21 +551,21 @@ export class PuppetPadplus extends Puppet { throw new Error(`can not find friendship.`) } - protected async friendshipRawPayloadParser (rawPayload: FriendshipPayload): Promise { + public async friendshipRawPayloadParser (rawPayload: payloads.Friendship): Promise { log.silly(PRE, `friendshipRawPayloadParser(${util.inspect(rawPayload)})`) - return rawPayload as FriendshipPayload + return rawPayload as payloads.Friendship } // get - public async friendshipPayload (friendshipId: string): Promise + public async friendshipPayload (friendshipId: string): Promise // set - public async friendshipPayload (friendshipId: string, friendshipPayload: FriendshipPayload): Promise + public async friendshipPayload (friendshipId: string, friendshipPayload: payloads.Friendship): Promise public async friendshipPayload ( friendshipId: string, - friendshipPayload?: FriendshipPayload, - ): Promise { + friendshipPayload?: payloads.Friendship, + ): Promise { log.silly('PadPlus', 'friendshipPayload(%s)', friendshipId, friendshipPayload @@ -641,7 +591,7 @@ export class PuppetPadplus extends Puppet { * MESSAGE IMAGE SECTION * ========================= */ - public async messageImage (messageId: string, type: ImageType): Promise { + public async messageImage (messageId: string, type: types.Image): Promise { log.silly(PRE, `messageImage(${messageId})`) const rawPayload = await this.messageRawPayload(messageId) @@ -650,11 +600,11 @@ export class PuppetPadplus extends Puppet { } switch (type) { - case ImageType.Thumbnail: + case types.Image.Thumbnail: return FileBox.fromUrl(rawPayload.url) - case ImageType.HD: + case types.Image.HD: throw new Error(`HD not support!`) - case ImageType.Artwork: + case types.Image.Artwork: let content = rawPayload.content const mediaData: PadplusRichMediaData = { appMsgType: 0, @@ -699,11 +649,11 @@ export class PuppetPadplus extends Puppet { const payload = await this.messagePayload(messageId) let filename = payload.filename || payload.id - const type = payload.type === MessageType.Image ? 'img' : payload.type === MessageType.Video ? 'video' : 'file' + const type = payload.type === types.Message.Image ? 'img' : payload.type === types.Message.Video ? 'video' : 'file' switch (payload.type) { - case MessageType.Image: - case MessageType.Attachment: - case MessageType.Video: + case types.Message.Image: + case types.Message.Attachment: + case types.Message.Video: let content = rawPayload.content const mediaData: PadplusRichMediaData = { appMsgType: type === 'file' ? 6 : 0, @@ -731,14 +681,14 @@ export class PuppetPadplus extends Puppet { } else { throw new Error(`Can not get media data url by this message id: ${messageId}`) } - case MessageType.Emoticon: + case types.Message.Emoticon: if (rawPayload && rawPayload.url) { const name = this.getNameFromUrl(rawPayload.url) return FileBox.fromUrl(rawPayload.url, name) } else { throw new Error(`can not get image/audio url fot message id: ${messageId}`) } - case MessageType.Audio: + case types.Message.Audio: if (rawPayload && rawPayload.url) { const name = this.getNameFromUrl(rawPayload.url) const fileBox = FileBox.fromUrl(rawPayload.url, name) @@ -779,13 +729,13 @@ export class PuppetPadplus extends Puppet { return name } - public async messageUrl (messageId: string): Promise { + public async messageUrl (messageId: string): Promise { log.silly(PRE, `messageUrl(${messageId})`) const rawPayload = await this.messageRawPayload(messageId) const payload = await this.messagePayload(messageId) - if (payload.type !== MessageType.Url) { + if (payload.type !== types.Message.Url) { throw new Error('Can not get url from non url payload') } else { const appPayload = await appMessageParser(rawPayload) @@ -806,7 +756,7 @@ export class PuppetPadplus extends Puppet { throw new Error(`not implement`) } - public async messageMiniProgram (messageId: string): Promise { + public async messageMiniProgram (messageId: string): Promise { log.silly(PRE, `messageMiniProgram(${messageId})`) const messageRawPayload = await this.messageRawPayload(messageId) @@ -823,7 +773,7 @@ export class PuppetPadplus extends Puppet { const payload = await this.messagePayload(messageId) - if (payload.type === MessageType.Text) { + if (payload.type === types.Message.Text) { if (!payload.text) { throw new Error('no text') } @@ -831,7 +781,7 @@ export class PuppetPadplus extends Puppet { conversationId, payload.text, ) - } else if (payload.type === MessageType.Audio) { + } else if (payload.type === types.Message.Audio) { const rawPayload = await this.messageRawPayload(payload.id) let contentXML let url @@ -847,17 +797,17 @@ export class PuppetPadplus extends Puppet { const content = await xmlToJson(contentXML) const voiceLength = content.msg.voicemsg.$.voicelength await this.messageSendVoice(conversationId, url, voiceLength) - } else if (payload.type === MessageType.Url) { + } else if (payload.type === types.Message.Url) { await this.messageSendUrl( conversationId, await this.messageUrl(messageId) ) - } else if (payload.type === MessageType.MiniProgram) { + } else if (payload.type === types.Message.MiniProgram) { await this.messageSendMiniProgram( conversationId, await this.messageMiniProgram(messageId) ) - } else if (payload.type === MessageType.ChatHistory) { + } else if (payload.type === types.Message.ChatHistory) { throw new Error('Message type ChatHistory not supported.') } else { await this.messageSendFile( @@ -871,7 +821,7 @@ export class PuppetPadplus extends Puppet { const msg: PadplusMessagePayload = { content: '', createTime: new Date().getTime(), - fromUserName: this.selfId(), + fromUserName: this.currentUserId, imgStatus: 0, l1MsgType: 0, msgId, @@ -883,7 +833,7 @@ export class PuppetPadplus extends Puppet { status: PadplusMessageStatus.One, toUserName: to, uin: '', - wechatUserName: this.selfId(), + wechatUserName: this.currentUserId, } log.silly(PRE, 'generateBaseMsg(%s) %s', to, JSON.stringify(msg)) this.manager.cachePadplusMessagePayload.set(msgId, msg) @@ -895,12 +845,12 @@ export class PuppetPadplus extends Puppet { let msgData: GrpcResponseMessageData if (mentionIdList && mentionIdList.length > 0) { - msgData = await this.manager.sendMessage(this.selfId(), conversationId, text, PadplusMessageType.Text, mentionIdList.toString()) + msgData = await this.manager.sendMessage(this.currentUserId, conversationId, text, PadplusMessageType.Text, mentionIdList.toString()) if (PADPLUS_REPLAY_MESSAGE) { this.replayTextMsg(msgData.msgId, conversationId, text, mentionIdList) } } else { - msgData = await this.manager.sendMessage(this.selfId(), conversationId, text, PadplusMessageType.Text) + msgData = await this.manager.sendMessage(this.currentUserId, conversationId, text, PadplusMessageType.Text) if (PADPLUS_REPLAY_MESSAGE) { this.replayTextMsg(msgData.msgId, conversationId, text) } @@ -909,7 +859,7 @@ export class PuppetPadplus extends Puppet { const msgPayload: PadplusMessagePayload = { content: text, createTime: msgData.timestamp, - fromUserName: this.selfId(), + fromUserName: this.currentUserId, imgStatus: 0, l1MsgType: 0, msgId: msgData.msgId, @@ -936,7 +886,7 @@ export class PuppetPadplus extends Puppet { payload.msgSource = this.generateMsgSource(atUserList) } log.silly(PRE, 'replayTextMsg replaying message: %s', JSON.stringify(payload)) - const eventMessagePayload: EventMessagePayload = { + const eventMessagePayload: payloads.EventMessage = { messageId: payload.msgId, } this.emit('message', eventMessagePayload) @@ -953,13 +903,13 @@ export class PuppetPadplus extends Puppet { public async messageSendVoice (conversationId: string, url: string, fileSize: string): Promise { log.silly(PRE, `messageSendVoice(${conversationId}, ${url}, ${fileSize})`) - const voiceMessageData: GrpcResponseMessageData = await this.manager.sendVoice(this.selfId(), conversationId, url, fileSize) + const voiceMessageData: GrpcResponseMessageData = await this.manager.sendVoice(this.currentUserId, conversationId, url, fileSize) if (voiceMessageData.success) { const msgPayload: PadplusMessagePayload = { content: url, createTime: voiceMessageData.timestamp, - fromUserName: this.selfId(), + fromUserName: this.currentUserId, imgStatus: 0, l1MsgType: 0, msgId: voiceMessageData.msgId, @@ -988,7 +938,7 @@ export class PuppetPadplus extends Puppet { nickName: contact.nickName, userName: contact.userName, } - const contactData: GrpcResponseMessageData = await this.manager.sendContact(this.selfId(), conversationId, JSON.stringify(content)) + const contactData: GrpcResponseMessageData = await this.manager.sendContact(this.currentUserId, conversationId, JSON.stringify(content)) if (PADPLUS_REPLAY_MESSAGE) { this.replayContactMsg(contactData.msgId, conversationId, JSON.stringify(content)) } @@ -996,7 +946,7 @@ export class PuppetPadplus extends Puppet { const msgPayload: PadplusMessagePayload = { content: JSON.stringify(content), createTime: contactData.timestamp, - fromUserName: this.selfId(), + fromUserName: this.currentUserId, imgStatus: 0, l1MsgType: 0, msgId: contactData.msgId, @@ -1023,7 +973,7 @@ export class PuppetPadplus extends Puppet { payload.msgType = PadplusMessageType.ShareCard payload.content = content log.silly(PRE, 'replayContactMsg replaying message: %s', JSON.stringify(payload)) - const eventMessagePayload: EventMessagePayload = { + const eventMessagePayload: payloads.EventMessage = { messageId: payload.msgId, } this.emit('message', eventMessagePayload) @@ -1031,20 +981,20 @@ export class PuppetPadplus extends Puppet { public async messageSendFile (conversationId: string, file: FileBox): Promise { log.silly(PRE, `messageSendFile(${conversationId})`) + await file.ready() + const type = file.mediaType && file.mediaType !== 'application/octet-stream' && file.mediaType !== 'application/unknown' + ? file.mediaType.replace(/;.*$/, '') + : path.extname(file.name) let fileUrl = '' - if ((file as any).remoteUrl) { - fileUrl = (file as any).remoteUrl + if ((file as any).remoteUrl || (file as any).url) { + fileUrl = (file as any).remoteUrl || (file as any).url } else { fileUrl = await this.manager.generatorFileUrl(file) } const fileSize = (await file.toBuffer()).length log.silly(PRE, `file url : ${fileUrl}`) - const type = (file.mimeType && file.mimeType !== 'application/octet-stream') - ? file.mimeType - : path.extname(file.name) - log.silly(PRE, `fileType ${type}`) switch (type) { case '.slk': @@ -1054,7 +1004,7 @@ export class PuppetPadplus extends Puppet { case '.jpg': case '.jpeg': case '.png': - const picData = await this.manager.sendFile(this.selfId(), conversationId, fileUrl, file.name, 'pic') + const picData = await this.manager.sendFile(this.currentUserId, conversationId, fileUrl, file.name, 'pic') if (PADPLUS_REPLAY_MESSAGE) { this.replayImageMsg(picData.msgId, conversationId, fileUrl) } @@ -1062,7 +1012,7 @@ export class PuppetPadplus extends Puppet { const msgPayload: PadplusMessagePayload = { content: `${fileUrl}`, createTime: picData.timestamp, - fromUserName: this.selfId(), + fromUserName: this.currentUserId, imgStatus: 0, l1MsgType: 0, msgId: picData.msgId, @@ -1081,7 +1031,7 @@ export class PuppetPadplus extends Puppet { return picData.msgId case 'video/mp4': case '.mp4': - const videoData = await this.manager.sendVideo(this.selfId(), conversationId, fileUrl) + const videoData = await this.manager.sendVideo(this.currentUserId, conversationId, fileUrl) if (PADPLUS_REPLAY_MESSAGE) { this.replayAppMsg(videoData.msgId, conversationId, fileUrl) } @@ -1089,7 +1039,7 @@ export class PuppetPadplus extends Puppet { const msgPayload: PadplusMessagePayload = { content: `${fileUrl}`, createTime: videoData.timestamp, - fromUserName: this.selfId(), + fromUserName: this.currentUserId, imgStatus: 0, l1MsgType: 0, msgId: videoData.msgId, @@ -1109,7 +1059,7 @@ export class PuppetPadplus extends Puppet { case 'application/xml': throw new Error(`Can not parse the url data, please input a name for FileBox.fromUrl(url, name).`) default: - const docData = await this.manager.sendFile(this.selfId(), conversationId, fileUrl, file.name, 'doc', fileSize) + const docData = await this.manager.sendFile(this.currentUserId, conversationId, fileUrl, file.name, 'doc', fileSize) if (PADPLUS_REPLAY_MESSAGE) { this.replayAppMsg(docData.msgId, conversationId, fileUrl) } @@ -1117,7 +1067,7 @@ export class PuppetPadplus extends Puppet { const msgPayload: PadplusMessagePayload = { content: `${fileUrl}`, createTime: docData.timestamp, - fromUserName: this.selfId(), + fromUserName: this.currentUserId, imgStatus: 0, l1MsgType: 0, msgId: docData.msgId, @@ -1143,7 +1093,7 @@ export class PuppetPadplus extends Puppet { payload.content = `${url}` payload.url = url log.silly(PRE, 'replayImageMsg replaying message: %s', JSON.stringify(payload)) - const eventMessagePayload: EventMessagePayload = { + const eventMessagePayload: payloads.EventMessage = { messageId: payload.msgId, } this.emit('message', eventMessagePayload) @@ -1154,13 +1104,13 @@ export class PuppetPadplus extends Puppet { payload.msgType = PadplusMessageType.App payload.content = `${content}` log.silly(PRE, 'replayAppMsg replaying message: %s', JSON.stringify(payload)) - const eventMessagePayload: EventMessagePayload = { + const eventMessagePayload: payloads.EventMessage = { messageId: payload.msgId, } this.emit('message', eventMessagePayload) } - public async messageSendUrl (conversationId: string, urlLinkPayload: UrlLinkPayload): Promise { + public async messageSendUrl (conversationId: string, urlLinkPayload: payloads.UrlLink): Promise { log.silly(PRE, `messageSendUrl(${conversationId})`) const { url, title, thumbnailUrl, description } = urlLinkPayload @@ -1172,7 +1122,7 @@ export class PuppetPadplus extends Puppet { type: 5, url, } - const urlLinkData = await this.manager.sendUrlLink(this.selfId(), conversationId, JSON.stringify(payload)) + const urlLinkData = await this.manager.sendUrlLink(this.currentUserId, conversationId, JSON.stringify(payload)) if (PADPLUS_REPLAY_MESSAGE) { this.replayUrlLinkMsg(urlLinkData.msgId, conversationId, JSON.stringify(payload)) } @@ -1180,7 +1130,7 @@ export class PuppetPadplus extends Puppet { const msgPayload: PadplusMessagePayload = { content: JSON.stringify(payload), createTime: urlLinkData.timestamp, - fromUserName: this.selfId(), + fromUserName: this.currentUserId, imgStatus: 0, l1MsgType: 0, msgId: urlLinkData.msgId, @@ -1205,26 +1155,38 @@ export class PuppetPadplus extends Puppet { payload.msgType = PadplusMessageType.App payload.content = content log.silly(PRE, 'replayUrlLinkMsg replaying message: %s', JSON.stringify(payload)) - const eventMessagePayload: EventMessagePayload = { + const eventMessagePayload: payloads.EventMessage = { messageId: payload.msgId, } this.emit('message', eventMessagePayload) } - public async messageSendMiniProgram (conversationId: string, miniProgramPayload: MiniProgramPayload): Promise { + public async messageSendMiniProgram (conversationId: string, miniProgramPayload: payloads.MiniProgram): Promise { log.silly(PRE, `messageSendMiniProgram(${conversationId}, ${miniProgramPayload})`) if (!this.manager) { throw new Error(`no manager`) } const content = convertMiniProgramPayloadToParams(miniProgramPayload) - const miniProgramData = await this.manager.sendMiniProgram(this.selfId(), conversationId, JSON.stringify(content)) + if ( + !miniProgramPayload.thumbKey + || (miniProgramPayload.thumbUrl && miniProgramPayload.thumbUrl.startsWith('http')) + ) { + const cdnData = await this.manager.getCDNData({ + toUserName: conversationId, + url: miniProgramPayload.thumbUrl!, + }) + content.cdnthumburl = cdnData.fileid + content.cdnthumbaeskey = cdnData.aesKey + log.silly(PRE, `messageSendMiniProgram(${conversationId}, ${miniProgramPayload}) content: ${JSON.stringify(content)}`) + } + const miniProgramData = await this.manager.sendMiniProgram(this.currentUserId, conversationId, JSON.stringify(content)) if (PADPLUS_REPLAY_MESSAGE) { this.replayUrlLinkMsg(miniProgramData.msgId, conversationId, JSON.stringify(content)) } if (miniProgramData.success) { const source = this.generateMsgSource() - const msgPayload = convertMiniProgramPayloadToMessage(this.selfId(), conversationId, source, content, miniProgramData) + const msgPayload = convertMiniProgramPayloadToMessage(this.currentUserId, conversationId, source, content, miniProgramData) this.manager.cachePadplusMessagePayload.set(miniProgramData.msgId, msgPayload) } @@ -1258,7 +1220,7 @@ export class PuppetPadplus extends Puppet { return rawPayload } - public async messageRawPayloadParser (rawPayload: PadplusMessagePayload): Promise { + public async messageRawPayloadParser (rawPayload: PadplusMessagePayload): Promise { log.silly(PRE, 'messageRawPayloadParser()') const payload = await messageRawPayloadParser(rawPayload) @@ -1284,7 +1246,7 @@ export class PuppetPadplus extends Puppet { const receiverId = payload.roomId || payload.toId log.silly(PRE, 'messageRecall(%s, %s)', receiverId, messageId) - const isSuccess = await this.manager.recallMessage(this.selfId(), receiverId!, messageId) + const isSuccess = await this.manager.recallMessage(this.currentUserId, receiverId!, messageId) return isSuccess } @@ -1294,26 +1256,30 @@ export class PuppetPadplus extends Puppet { * */ - public async dirtyLocalPayload (type: PayloadType, id: string) { + public async dirtyPayload ( + type : types.Payload, + id : string, + ) { + super.dirtyPayload(type, id) switch (type) { - case PayloadType.Contact: + case types.Payload.Contact: await this.manager.cacheManager?.deleteContact(id) break - case PayloadType.Message: + case types.Payload.Message: this.manager.cachePadplusMessagePayload.del(id) break - case PayloadType.Room: + case types.Payload.Room: await this.manager.cacheManager?.deleteRoom(id) break - case PayloadType.RoomMember: + case types.Payload.RoomMember: await this.manager.cacheManager?.deleteRoomMember(id) break default: - log.info(PRE, `dirtyLocalPayload() Received unknown payload type.`) + log.info(PRE, `dirtyPayload() Received unknown payload type.`) break } } @@ -1346,8 +1312,8 @@ export class PuppetPadplus extends Puppet { } // Set Cache Dirty - await this.dirtyLocalPayload(PayloadType.Room, roomId) - await this.dirtyLocalPayload(PayloadType.RoomMember, roomId) + await this.dirtyPayload(types.Payload.Room, roomId) + await this.dirtyPayload(types.Payload.RoomMember, roomId) // Sync room member const startTime = Date.now() @@ -1358,12 +1324,13 @@ export class PuppetPadplus extends Puppet { memberList = await this.roomMemberList(roomId) } - const eventRoomJoinPayload: EventRoomJoinPayload = { + const eventRoomJoinPayload: payloads.EventRoomJoin = { inviteeIdList, inviterId, roomId, timestamp, } + log.silly(PRE, `emit room-join event : ${util.inspect(eventRoomJoinPayload)}`) this.emit('room-join', eventRoomJoinPayload) } } @@ -1386,7 +1353,22 @@ export class PuppetPadplus extends Puppet { let leaverIdList: string[] = [] if (typeof _leaverIdList[0] === 'symbol') { leaverIdList = [ await this.searchSymbolYou(_leaverIdList[0] as any, roomId) ] - removerId = await this.searchSymbolYou(removerId, roomId) + if (typeof leaveEvent.dismiss === 'undefined') { + // 被群主踢出群聊,群主可能有群昵称和好友备注,都需要进行筛选 + const members = await this.manager.getRoomMembers(roomId) + log.silly(`onRoomLeaveEvent(${message.msgId}) members: ${JSON.stringify(members)} removerId: ${removerId}`) + Object.values(members).map(async member => { + if (removerId === member.nickName || removerId === member.displayName) { + removerId = member.contactId + } else { + const contactInfo = await this.manager.getContact(member.contactId) + const alias = contactInfo?.remark + if (removerId === alias) { + removerId = member.contactId + } + } + }) + } } else { leaverIdList = _leaverIdList as string[] } @@ -1394,13 +1376,19 @@ export class PuppetPadplus extends Puppet { // Sync room member const startTime = Date.now() const expireTime = 1 * 60 * 1000 - let memberList = await this.roomMemberList(roomId) + let memberList: string[] + if (leaveEvent.dismiss) { + await this.manager.cacheManager?.dismissRoomMember(roomId) + memberList = [] + } else { + memberList = await this.roomMemberList(roomId) + } while (leaverIdList.some(c => memberList.includes(c)) && Date.now() - startTime < expireTime) { await new Promise(resolve => setTimeout(resolve, 1000)) memberList = await this.roomMemberList(roomId) } - const eventRoomLeavePayload: EventRoomLeavePayload = { + const eventRoomLeavePayload: payloads.EventRoomLeave = { removeeIdList: leaverIdList, removerId, roomId, @@ -1443,9 +1431,9 @@ export class PuppetPadplus extends Puppet { } // Set Cache Dirty - await this.dirtyLocalPayload(PayloadType.Room, roomId) + await this.dirtyPayload(types.Payload.Room, roomId) - const eventRoomTopicPayload: EventRoomTopicPayload = { + const eventRoomTopicPayload: payloads.EventRoomTopic = { changerId, newTopic, oldTopic, @@ -1466,12 +1454,12 @@ export class PuppetPadplus extends Puppet { throw new Error('no manager') } await this.manager.saveRoomInvitationRawPayload(roomInviteEvent) - const eventRoomInvitePayload: EventRoomInvitePayload = { + const eventRoomInvitePayload: payloads.EventRoomInvite = { roomInvitationId: roomInviteEvent.msgId, } this.emit('room-invite', eventRoomInvitePayload) } else { - const eventMessagePayload: EventMessagePayload = { + const eventMessagePayload: payloads.EventMessage = { messageId: rawPayload.msgId, } this.emit('message', eventMessagePayload) @@ -1497,10 +1485,10 @@ export class PuppetPadplus extends Puppet { return payload } - public async roomInvitationRawPayloadParser (rawPayload: PadplusRoomInvitationPayload): Promise { + public async roomInvitationRawPayloadParser (rawPayload: PadplusRoomInvitationPayload): Promise { log.silly(PRE, `roomInvitationRawPayloadParser()`) - const payload: RoomInvitationPayload = { + const payload: payloads.RoomInvitation = { avatar: rawPayload.thumbUrl, id: rawPayload.id, invitation: rawPayload.url, @@ -1523,7 +1511,11 @@ export class PuppetPadplus extends Puppet { const room = await this.roomRawPayload(roomId) if (room) { const avatarUrl = room.bigHeadUrl || room.smallHeadUrl - return FileBox.fromUrl(avatarUrl, `${roomId}_avatar_${Date.now()}.png`) + if (avatarUrl) { + return FileBox.fromUrl(avatarUrl, `${roomId}_avatar_${Date.now()}.png`) + } else { + throw new Error(`Can not get room avatar due to this room: ${roomId} does not exist.`) + } } else { throw new Error(`Can not load room info by roomId : ${roomId}`) } @@ -1548,14 +1540,24 @@ export class PuppetPadplus extends Puppet { await this.manager.roomAddMember(roomId, contactId) } - public async roomDel (roomId: string, contactId: string): Promise { - log.silly(PRE, `roomDel(${roomId}, ${contactId})`) - + public async roomDel (roomId: string, contactIdList: string | string[]): Promise { + log.silly(PRE, `roomDel(${roomId}, ${contactIdList})`) const memberIdList = await this.roomMemberList(roomId) - if (memberIdList.includes(contactId)) { - await this.manager.deleteRoomMember(roomId, contactId) + if (Array.isArray(contactIdList)) { + for (const contactId of contactIdList) { + if (memberIdList.includes(contactId)) { + await this.manager.deleteRoomMember(roomId, contactId) + } else { + log.silly(PRE, `roomDel() room(${roomId}) has no member contact(${contactId})`) + } + } } else { - log.silly(PRE, `roomDel() room(${roomId}) has no member contact(${contactId})`) + const contactId = contactIdList[0] + if (memberIdList.includes(contactId)) { + await this.manager.deleteRoomMember(roomId, contactId) + } else { + log.silly(PRE, `roomDel() room(${roomId}) has no member contact(${contactId})`) + } } } @@ -1575,14 +1577,17 @@ export class PuppetPadplus extends Puppet { log.silly(PRE, `roomTopic(${roomId}, ${topic})`) if (typeof topic === 'undefined') { - const room = await this.roomPayload(roomId) - return room && (room.topic || '') + const room = await this.manager.getRoom(roomId) + if (room) { + return room.nickName + } + return '' } if (!this.manager) { throw new Error(`no manager.`) } await this.manager.setRoomTopic(roomId, topic as string) - this.emit('dirty', { payloadId: roomId, payloadType: PayloadType.Room }) + this.emit('dirty', { payloadId: roomId, payloadType: types.Payload.Room }) await new Promise(resolve => setTimeout(resolve, 500)) await this.roomTopic(roomId) } @@ -1615,21 +1620,21 @@ export class PuppetPadplus extends Puppet { return roomIds } - protected async roomRawPayload (roomId: string): Promise { + public async roomRawPayload (roomId: string): Promise { log.silly(PRE, `roomRawPayload(${roomId})`) const rawRoom = await this.manager.getRoomInfo(roomId) return rawRoom } - protected async roomRawPayloadParser (rawPayload: PadplusRoomPayload): Promise { + public async roomRawPayloadParser (rawPayload: PadplusRoomPayload): Promise { log.silly(PRE, `roomRawPayloadParser()`) const room = roomRawPayloadParser(rawPayload) return room } - protected async roomMemberRawPayload (roomId: string, contactId: string): Promise { + public async roomMemberRawPayload (roomId: string, contactId: string): Promise { log.silly(PRE, `roomMemberRawPayload(${roomId}, ${contactId})`) if (!this.manager) { @@ -1643,7 +1648,7 @@ export class PuppetPadplus extends Puppet { return member } - protected async roomMemberRawPayloadParser (rawPayload: PadplusRoomMemberPayload): Promise { + public async roomMemberRawPayloadParser (rawPayload: PadplusRoomMemberPayload): Promise { log.silly(PRE, `roomMemberRawPayloadParser()`) const member = convertToPuppetRoomMember(rawPayload) @@ -1690,13 +1695,13 @@ export class PuppetPadplus extends Puppet { public ding (data?: string): void { log.silly(PRE, `ding(${data})`) - const eventDongPayload: EventDongPayload = { + const eventDongPayload: payloads.EventDong = { data: data ? data! : 'ding-dong', } this.emit('dong', eventDongPayload) } - private deduplicateRoomLeaveEvent (data: EventRoomLeavePayload) { + private deduplicateRoomLeaveEvent (data: payloads.EventRoomLeave) { log.silly(`deduplicateRoomLeaveEvent(${JSON.stringify(data)})`) const key = `${data.removeeIdList[0]}_${data.roomId}` @@ -1715,12 +1720,12 @@ export class PuppetPadplus extends Puppet { } } - private async searchSymbolYou (id: string | YOU, roomId: string): Promise { + private async searchSymbolYou (id: string | typeof types.YOU, roomId: string): Promise { let inviteeIdList let inviterIdList = await this.roomMemberSearch(roomId, id) if (inviterIdList.length < 1) { - this.emit('dirty', { payloadId: roomId, payloadType: PayloadType.RoomMember }) + this.emit('dirty', { payloadId: roomId, payloadType: types.Payload.RoomMember }) await this.manager.getRoomMembers(roomId) inviterIdList = await this.roomMemberSearch(roomId, id) if (inviterIdList.length < 1) { diff --git a/src/pure-function-helpers/contact-cache-mapper.spec.ts b/src/pure-function-helpers/contact-cache-mapper.spec.ts index 8e86dc0..0b4723c 100644 --- a/src/pure-function-helpers/contact-cache-mapper.spec.ts +++ b/src/pure-function-helpers/contact-cache-mapper.spec.ts @@ -5,7 +5,7 @@ import test from 'blue-tape' -import { PuppetCacheContactPayload } from 'wechaty-puppet-cache' +import { PuppetCacheContactPayload } from '@juzi/wechaty-puppet-cache' import { PadplusContactPayload } from '../schemas' import { cacheToPadplusContactPayload, padplusToCacheContactPayload } from './contact-cache-mapper' diff --git a/src/pure-function-helpers/contact-cache-mapper.ts b/src/pure-function-helpers/contact-cache-mapper.ts index 7a3be55..2af9626 100644 --- a/src/pure-function-helpers/contact-cache-mapper.ts +++ b/src/pure-function-helpers/contact-cache-mapper.ts @@ -1,4 +1,4 @@ -import { PuppetCacheContactPayload } from 'wechaty-puppet-cache' +import { PuppetCacheContactPayload } from '@juzi/wechaty-puppet-cache' import { PadplusContactPayload } from '../schemas' export function cacheToPadplusContactPayload ( diff --git a/src/pure-function-helpers/contact-raw-payload-parser.spec.ts b/src/pure-function-helpers/contact-raw-payload-parser.spec.ts index 7e9950e..cf2f640 100755 --- a/src/pure-function-helpers/contact-raw-payload-parser.spec.ts +++ b/src/pure-function-helpers/contact-raw-payload-parser.spec.ts @@ -5,11 +5,7 @@ import test from 'blue-tape' -import { - ContactGender, - ContactPayload, - ContactType, -} from 'wechaty-puppet' +import { payloads, types } from '@juzi/wechaty-puppet' import { PadplusContactPayload, @@ -79,41 +75,41 @@ test('contactRawPayloadParser', async t => { verifyFlag : 0, } - const EXPECTED_CONTACT_PAYLOAD_PERSONAL: ContactPayload = { + const EXPECTED_CONTACT_PAYLOAD_PERSONAL: payloads.Contact = { alias : '', avatar : 'http://wx.qlogo.cn/mmhead/KDLS0fhbCTJ0H7wsWRiaeMdibHvaeoZw1jQScfCqfVaPM/132', city : 'Haidian', friend : true, - gender : ContactGender.Male, + gender : types.ContactGender.Male, id : 'lylezhuifeng', name : '高原ོ', phone : [], province : 'Beijing', signature : '', - type : ContactType.Individual, + type : types.Contact.Individual, weixin : 'lylezhuifeng', } - const EXPECTED_CONTACT_PAYLOAD_OFFICIAL: ContactPayload = { + const EXPECTED_CONTACT_PAYLOAD_OFFICIAL: payloads.Contact = { alias : '', avatar : 'http://wx.qlogo.cn/mmhead/ver_1/icxUZE0qz1c1HubRfXHscMA1PialA7q3OEIWiaRtUjYmpj2EDFhTNGwlicUFe1NQR67gVGgjhILV1ZTsZ1qO3XTMehhH1k6icF1adbaibUMJXbMWk/132', city : 'Fengtai', friend : true, - gender : ContactGender.Female, + gender : types.ContactGender.Female, id : 'wxid_v7j3e9kna9l912', name : '李青青', phone : [], province : 'Beijing', signature : '', - type : ContactType.Individual, + type : types.Contact.Individual, weixin : 'wxid_v7j3e9kna9l912', } const resultPersonal = contactRawPayloadParser(PADPLUS_CONTACT_PAYLOAD_PERSONAL) - t.deepEqual(resultPersonal, EXPECTED_CONTACT_PAYLOAD_PERSONAL, 'should parse ContactPayload for male account payload') + t.deepEqual(resultPersonal, EXPECTED_CONTACT_PAYLOAD_PERSONAL, 'should parse payloads.Contact for male account payload') const resultOfficial = contactRawPayloadParser(PADPLUS_CONTACT_PAYLOAD_OFFICIAL) - t.deepEqual(resultOfficial, EXPECTED_CONTACT_PAYLOAD_OFFICIAL, 'should parse ContactPayload for female account payload') + t.deepEqual(resultOfficial, EXPECTED_CONTACT_PAYLOAD_OFFICIAL, 'should parse payloads.Contact for female account payload') t.throws(() => contactRawPayloadParser({} as any), 'should throw exception for invalid object') t.throws(() => contactRawPayloadParser(undefined as any), 'should throw exception for undifined') diff --git a/src/pure-function-helpers/contact-raw-payload-parser.ts b/src/pure-function-helpers/contact-raw-payload-parser.ts index 5d49fb2..d0c554b 100644 --- a/src/pure-function-helpers/contact-raw-payload-parser.ts +++ b/src/pure-function-helpers/contact-raw-payload-parser.ts @@ -1,7 +1,4 @@ -import { - ContactPayload, - ContactType, -} from 'wechaty-puppet' +import { payloads, types } from '@juzi/wechaty-puppet' import { PadplusContactPayload, @@ -14,7 +11,7 @@ import { export function contactRawPayloadParser ( rawPayload: PadplusContactPayload, -): ContactPayload { +): payloads.Contact { if (!rawPayload.userName) { /** * { big_head: '', @@ -43,20 +40,20 @@ export function contactRawPayloadParser ( } if (!isContactId(rawPayload.userName)) { - throw Error('Room Object instead of Contact!') + throw Error(`userName: ${rawPayload.userName} is not contact, detail: ${JSON.stringify(rawPayload)}`) } - let contactType = ContactType.Unknown + let contactType = types.Contact.Unknown if (isContactOfficialId(rawPayload.userName) || rawPayload.verifyFlag !== 0) { - contactType = ContactType.Official + contactType = types.Contact.Official } else { - contactType = ContactType.Individual + contactType = types.Contact.Individual } let friend = false if (rawPayload.contactFlag && rawPayload.contactFlag !== 0 && rawPayload.verifyFlag === 0) { friend = true } - const payload: ContactPayload = { + const payload: payloads.Contact = { alias : rawPayload.remark, avatar : rawPayload.bigHeadUrl, city : rawPayload.city, diff --git a/src/pure-function-helpers/friendship-cache-mapper.spec.ts b/src/pure-function-helpers/friendship-cache-mapper.spec.ts index ed01ecb..4b1cd08 100644 --- a/src/pure-function-helpers/friendship-cache-mapper.spec.ts +++ b/src/pure-function-helpers/friendship-cache-mapper.spec.ts @@ -5,32 +5,32 @@ import test from 'blue-tape' -import { PuppetCacheFriendshipPayload } from 'wechaty-puppet-cache' +import { PuppetCacheFriendshipPayload } from '@juzi/wechaty-puppet-cache' import { cacheToPadplusFriendshipPayload, padplusToCacheFriendshipPayload } from './friendship-cache-mapper' -import { FriendshipType, FriendshipPayload, FriendshipSceneType } from 'wechaty-puppet' +import { payloads, types } from '@juzi/wechaty-puppet' test('friendship-cache-mapper', async t => { - const PADPLUS_FRIENDSHIP_PAYLOAD: FriendshipPayload = { + const PADPLUS_FRIENDSHIP_PAYLOAD: payloads.Friendship = { contactId : 'contactId', hello : 'hello', id : 'id', - scene : FriendshipSceneType.Phone, + scene : types.FriendshipScene.Phone, stranger : 'stranger', ticket : 'ticket', timestamp : 1591069294176, - type : FriendshipType.Receive, + type : types.Friendship.Receive, } const EXPECTED_FRIENDSHIP_PAYLOAD: PuppetCacheFriendshipPayload = { contactId : 'contactId', hello : 'hello', id : 'id', - scene : FriendshipSceneType.Phone, + scene : types.FriendshipScene.Phone, stranger : 'stranger', ticket : 'ticket', timestamp : 1591069294176, - type : FriendshipType.Receive, + type : 2, // types.Friendship.Receive } const resultCache = padplusToCacheFriendshipPayload(PADPLUS_FRIENDSHIP_PAYLOAD) diff --git a/src/pure-function-helpers/friendship-cache-mapper.ts b/src/pure-function-helpers/friendship-cache-mapper.ts index 20dab72..377f14b 100644 --- a/src/pure-function-helpers/friendship-cache-mapper.ts +++ b/src/pure-function-helpers/friendship-cache-mapper.ts @@ -1,9 +1,9 @@ -import { PuppetCacheFriendshipPayload } from 'wechaty-puppet-cache' -import { FriendshipType, FriendshipPayload, FriendshipPayloadReceive } from 'wechaty-puppet' +import { PuppetCacheFriendshipPayload } from '@juzi/wechaty-puppet-cache' +import { payloads, types } from '@juzi/wechaty-puppet' export function cacheToPadplusFriendshipPayload ( cachePayload: PuppetCacheFriendshipPayload, -): FriendshipPayload { +): payloads.Friendship { if (!cachePayload.id) { throw Error('cannot get id from cache payload: ' + JSON.stringify(cachePayload)) } @@ -16,42 +16,42 @@ export function cacheToPadplusFriendshipPayload ( ticket : cachePayload.ticket, timestamp : cachePayload.timestamp, type : cachePayload.type, - } as FriendshipPayload + } as payloads.Friendship } export function padplusToCacheFriendshipPayload ( - padplusPayload: FriendshipPayload, + padplusPayload: payloads.Friendship, ): PuppetCacheFriendshipPayload { if (!padplusPayload.id) { throw Error('cannot get id from padplus payload: ' + JSON.stringify(padplusPayload)) } switch (padplusPayload.type) { - case FriendshipType.Confirm: + case types.Friendship.Confirm: return { contactId : padplusPayload.contactId, hello : padplusPayload.hello, id : padplusPayload.id, timestamp : padplusPayload.timestamp, - type : FriendshipType.Confirm, + type : types.Friendship.Confirm, } as PuppetCacheFriendshipPayload - case FriendshipType.Receive: + case types.Friendship.Receive: return { contactId : padplusPayload.contactId, hello : padplusPayload.hello, id : padplusPayload.id, - scene : (padplusPayload as FriendshipPayloadReceive).scene, + scene : padplusPayload.scene, stranger : padplusPayload.stranger, ticket : padplusPayload.ticket, timestamp : padplusPayload.timestamp, - type : FriendshipType.Receive, + type : types.Friendship.Receive, } as PuppetCacheFriendshipPayload - case FriendshipType.Verify: + case types.Friendship.Verify: return { contactId : padplusPayload.contactId, hello : padplusPayload.hello, id : padplusPayload.id, timestamp : padplusPayload.timestamp, - type : FriendshipType.Verify, + type : types.Friendship.Verify, } as PuppetCacheFriendshipPayload default: throw new Error(`unknown friendship type.`) diff --git a/src/pure-function-helpers/friendship-raw-payload-parser.ts b/src/pure-function-helpers/friendship-raw-payload-parser.ts index 85197e7..009a303 100644 --- a/src/pure-function-helpers/friendship-raw-payload-parser.ts +++ b/src/pure-function-helpers/friendship-raw-payload-parser.ts @@ -1,14 +1,7 @@ /* eslint-disable sort-keys */ import { xmlToJson } from './xml-to-json' -import { - FriendshipType, - FriendshipPayload, - FriendshipPayloadConfirm, - FriendshipPayloadReceive, - FriendshipPayloadVerify, - FriendshipSceneType, -} from 'wechaty-puppet' +import { payloads, types } from '@juzi/wechaty-puppet' import { PadplusFriendshipPayload, @@ -21,23 +14,23 @@ import { friendshipVerifyEventMessageParser, } from './friendship-event-message-parser' -const friendshipTypeMap: { [scene: string]: FriendshipSceneType } = { - '1': FriendshipSceneType.QQ, - '2': FriendshipSceneType.Email, - '3': FriendshipSceneType.Weixin, - '12': FriendshipSceneType.QQtbd, - '14': FriendshipSceneType.Room, - '15': FriendshipSceneType.Phone, - '17': FriendshipSceneType.Card, - '18': FriendshipSceneType.Location, - '25': FriendshipSceneType.Bottle, - '29': FriendshipSceneType.Shaking, - '30': FriendshipSceneType.QRCode, +const friendshipTypeMap: { [scene: string]: types.FriendshipScene } = { + '1': types.FriendshipScene.QQ, + '2': types.FriendshipScene.Email, + '3': types.FriendshipScene.Weixin, + '12': types.FriendshipScene.QQtbd, + '14': types.FriendshipScene.Room, + '15': types.FriendshipScene.Phone, + '17': types.FriendshipScene.Card, + '18': types.FriendshipScene.Location, + '25': types.FriendshipScene.Bottle, + '29': types.FriendshipScene.Shaking, + '30': types.FriendshipScene.QRCode, } export async function friendshipRawPayloadParser ( rawPayload: PadplusMessagePayload, -) : Promise { +) : Promise { if (friendshipConfirmEventMessageParser(rawPayload)) { /** @@ -64,24 +57,24 @@ export async function friendshipRawPayloadParser ( async function friendshipRawPayloadParserConfirm ( rawPayload: PadplusMessagePayload, -): Promise { - const payload: FriendshipPayloadConfirm = { +): Promise { + const payload: payloads.FriendshipConfirm = { contactId : rawPayload.fromUserName, id : rawPayload.msgId, timestamp : rawPayload.createTime, - type : FriendshipType.Confirm, + type : types.Friendship.Confirm, } return payload } function friendshipRawPayloadParserVerify ( rawPayload: PadplusMessagePayload, -): FriendshipPayload { - const payload: FriendshipPayloadVerify = { +): payloads.Friendship { + const payload: payloads.FriendshipVerify = { contactId : rawPayload.fromUserName, id : rawPayload.msgId, timestamp : rawPayload.createTime, - type : FriendshipType.Verify, + type : types.Friendship.Verify, } return payload } @@ -104,7 +97,7 @@ async function friendshipRawPayloadParserReceive ( } const padplusFriendshipPayload: PadplusFriendshipPayload = jsonPayload.msg.$ - const friendshipPayload: FriendshipPayloadReceive = { + const friendshipPayload: payloads.FriendshipReceive = { contactId : padplusFriendshipPayload.fromusername, hello : padplusFriendshipPayload.content, id : rawPayload.msgId, @@ -112,7 +105,7 @@ async function friendshipRawPayloadParserReceive ( stranger : padplusFriendshipPayload.encryptusername, ticket : padplusFriendshipPayload.ticket, timestamp : rawPayload.createTime, - type : FriendshipType.Receive, + type : types.Friendship.Receive, } return friendshipPayload diff --git a/src/pure-function-helpers/is-type.ts b/src/pure-function-helpers/is-type.ts index a6c5ecf..3d68902 100644 --- a/src/pure-function-helpers/is-type.ts +++ b/src/pure-function-helpers/is-type.ts @@ -16,7 +16,7 @@ export function isContactId (id?: string): boolean { if (!id) { return false } - return !isRoomId(id) && !isIMRoomId(id) && !isIMContactId(id) + return !isRoomId(id) && !isIMRoomId(id) } export function isIMContactId (id?: string): boolean { diff --git a/src/pure-function-helpers/message-app-payload-parser.ts b/src/pure-function-helpers/message-app-payload-parser.ts index 7fa8c7c..410c90a 100644 --- a/src/pure-function-helpers/message-app-payload-parser.ts +++ b/src/pure-function-helpers/message-app-payload-parser.ts @@ -6,8 +6,6 @@ import { PadplusMessagePayload, } from '../schemas' -import { log } from '../config' - import { isPayload } from './is-type' export async function appMessageParser (rawPayload: PadplusMessagePayload): Promise { @@ -80,7 +78,6 @@ export async function appMessageParser (rawPayload: PadplusMessagePayload): Prom url, } } catch (e) { - log.verbose(e.stack) return null } } diff --git a/src/pure-function-helpers/message-cache-mapper.spec.ts b/src/pure-function-helpers/message-cache-mapper.spec.ts index 5c7626f..305fe7b 100644 --- a/src/pure-function-helpers/message-cache-mapper.spec.ts +++ b/src/pure-function-helpers/message-cache-mapper.spec.ts @@ -5,7 +5,7 @@ import test from 'blue-tape' -import { PuppetCacheMessagePayload, PuppetCacheMessageType } from 'wechaty-puppet-cache' +import { PuppetCacheMessagePayload, PuppetCacheMessageType } from '@juzi/wechaty-puppet-cache' import { PadplusMessagePayload, PadplusMessageType } from '../schemas' import { cacheToPadplusMessagePayload, padplusToCacheMessagePayload } from './message-cache-mapper' diff --git a/src/pure-function-helpers/message-cache-mapper.ts b/src/pure-function-helpers/message-cache-mapper.ts index fdcb65c..12dc5b1 100644 --- a/src/pure-function-helpers/message-cache-mapper.ts +++ b/src/pure-function-helpers/message-cache-mapper.ts @@ -1,4 +1,4 @@ -import { PuppetCacheMessagePayload, PuppetCacheMessageType } from 'wechaty-puppet-cache' +import { PuppetCacheMessagePayload, PuppetCacheMessageType } from '@juzi/wechaty-puppet-cache' import { PadplusMessagePayload, PadplusMessageType } from '../schemas' export function cacheToPadplusMessagePayload ( diff --git a/src/pure-function-helpers/message-mini-program-payload-parser.ts b/src/pure-function-helpers/message-mini-program-payload-parser.ts index 31652a1..6131d6e 100644 --- a/src/pure-function-helpers/message-mini-program-payload-parser.ts +++ b/src/pure-function-helpers/message-mini-program-payload-parser.ts @@ -1,15 +1,13 @@ -import { MiniProgramPayload } from 'wechaty-puppet' +import { payloads } from '@juzi/wechaty-puppet' import { xmlToJson } from './xml-to-json' import { PadplusMessagePayload, } from '../schemas' -import { log } from '../config' - import { isPayload } from './is-type' -export async function miniProgramMessageParser (rawPayload: PadplusMessagePayload): Promise { +export async function miniProgramMessageParser (rawPayload: PadplusMessagePayload): Promise { if (!isPayload(rawPayload)) { return null } @@ -56,8 +54,7 @@ export async function miniProgramMessageParser (rawPayload: PadplusMessagePayloa title, username, } - } catch (e) { - log.verbose(e.stack) + } catch (err) { return null } } diff --git a/src/pure-function-helpers/message-raw-payload-parser.spec.ts b/src/pure-function-helpers/message-raw-payload-parser.spec.ts index 2a606cf..9ec4bac 100755 --- a/src/pure-function-helpers/message-raw-payload-parser.spec.ts +++ b/src/pure-function-helpers/message-raw-payload-parser.spec.ts @@ -5,9 +5,7 @@ import test from 'blue-tape' -import { - MessagePayload, MessageType, -} from 'wechaty-puppet' +import { payloads, types } from '@juzi/wechaty-puppet' import { PadplusMessagePayload, @@ -33,7 +31,7 @@ test('messageRawPayloadParser', async t => { // to_user : 'wxid_zj2cahpwzgie12', // uin : 324216852, // } - // const EXPECTED_MESSAGE_PAYLOAD_TEXT: MessagePayload = { + // const EXPECTED_MESSAGE_PAYLOAD_TEXT: payloads.Message = { // // // } // }) @@ -56,7 +54,7 @@ test('messageRawPayloadParser', async t => { // to_user : 'wxid_zj2cahpwzgie12', // uin : 324216852, // } - // const EXPECTED_MESSAGE_PAYLOAD_VOICE: MessagePayload = { + // const EXPECTED_MESSAGE_PAYLOAD_VOICE: payloads.Message = { // // // } // }) @@ -83,15 +81,16 @@ test('sys', async t => { uin: '2963338780', wechatUserName: 'wxid_zovb9ol86m7l22', } - const EXPECTED_MESSAGE_PAYLOAD_SYS: MessagePayload = { + const EXPECTED_MESSAGE_PAYLOAD_SYS: payloads.Message = { fromId: undefined, id: '1745697108690097034', mentionIdList: [], roomId: '23238546298@chatroom', + talkerId: '', text: '\n\t\n\t\t\n\t\t\n\t\t\n\t\t\tinvite\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\n', timestamp: 1568205434181, toId: 'wxid_zovb9ol86m7l22', - type: MessageType.Unknown, + type: types.Message.Unknown, } const payload = await messageRawPayloadParser(PADPLUS_MESSAGE_PAYLOAD_SYS) @@ -121,16 +120,17 @@ test('room invitation created by others', async t => { wechatUserName: 'wxid_zovb9ol86m7l22', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { filename: '7451739954714199526-to-be-implement.txt', fromId: 'lylezhuifeng', id: '7451739954714199526', mentionIdList: [], - roomId: undefined, + roomId: '', + talkerId: '', text: '<![CDATA[邀请你加入群聊]]>view500', timestamp: 1568205752199, toId: 'wxid_zovb9ol86m7l22', - type: MessageType.Url, + type: types.Message.Url, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) @@ -158,15 +158,16 @@ test('room ownership transfer message', async t => { wechatUserName: 'wxid_zovb9ol86m7l22', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { fromId: undefined, id: '6345486005160255967', mentionIdList: [], roomId: '18295482296@chatroom', + talkerId: '', text: '你已成为新群主', timestamp: 1568206018206, toId: 'wxid_zovb9ol86m7l22', - type: MessageType.Unknown, + type: types.Message.Unknown, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) @@ -192,15 +193,16 @@ test('share card peer to peer', async t => { uin: '2963338780', wechatUserName: 'wxid_zovb9ol86m7l22', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { fromId: 'lylezhuifeng', id: '4856541606299990582', mentionIdList: [], - roomId: undefined, + roomId: '', + talkerId: '', text: '', timestamp: 1568206141208, toId: 'wxid_zovb9ol86m7l22', - type: MessageType.Contact, + type: types.Message.Contact, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) @@ -230,15 +232,16 @@ test('share card in room', async t => { wechatUserName: 'wxid_zovb9ol86m7l22', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { fromId: 'lylezhuifeng', id: '2899852992039617138', mentionIdList: [], roomId: '18295482296@chatroom', + talkerId: '', text: '\n\n', timestamp: 1568206684229, toId: 'wxid_zovb9ol86m7l22', - type: MessageType.Contact, + type: types.Message.Contact, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) @@ -267,16 +270,17 @@ test('attachment file with ext .xlsx', async t => { wechatUserName: 'wxid_zovb9ol86m7l22', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { filename: '报价.xlsx', fromId: 'lylezhuifeng', id: '4361591746319570095', mentionIdList: [], - roomId: undefined, + roomId: '', + talkerId: '', text: '报价.xlsx6011934@cdn_304e020100044730450201000204d8e50c6e02033d0af80204ba30feb602045d78f1e10420777869645f7a6f7662396f6c38366d376c323235305f313536383230373332390204010400050201000400_0780b7157995b47c0e88275d0c40da6a_1xlsx304e020100044730450201000204d8e50c6e02033d0af80204ba30feb602045d78f1e10420777869645f7a6f7662396f6c38366d376c323235305f3135363832303733323902040104000502010004000780b7157995b47c0e88275d0c40da6a0420e8bc8986eb1539bbab10d396e814dlylezhuifeng01', timestamp: 1568207329248, toId: 'wxid_zovb9ol86m7l22', - type: MessageType.Attachment, + type: types.Message.Attachment, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) @@ -305,15 +309,16 @@ test('others recalled message in room', async t => { wechatUserName: 'wxid_zovb9ol86m7l22', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { fromId: 'lylezhuifeng', id: '7451323945505661106', mentionIdList: [], roomId: '18295482296@chatroom', + talkerId: '', text: '6800642263058603981', timestamp: 1568207817258, toId: 'wxid_zovb9ol86m7l22', - type: MessageType.Recalled, + type: types.Message.Recalled, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) t.deepEqual(payload, @@ -346,15 +351,16 @@ test('bot recalled message in room', async t => { wechatUserName: 'wxid_v7j3e9kna9l912', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { fromId: 'wxid_v7j3e9kna9l912', id: '3195375600040238004', mentionIdList: [], roomId: '23446751259@chatroom', + talkerId: '', text: '245587684513446090', timestamp: 1568776956065, toId: 'wxid_v7j3e9kna9l912', - type: MessageType.Recalled, + type: types.Message.Recalled, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) t.deepEqual(payload, @@ -387,15 +393,16 @@ test('others recalled message in private chat', async t => { wechatUserName: 'wxid_v7j3e9kna9l912', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { fromId: 'Soul001001', id: '8551294433062845570', mentionIdList: [], - roomId: undefined, + roomId: '', + talkerId: '', text: '3169605043756821364', timestamp: 1568777325070, toId: 'wxid_v7j3e9kna9l912', - type: MessageType.Recalled, + type: types.Message.Recalled, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) t.deepEqual(payload, @@ -428,15 +435,16 @@ test('bot recalled message in private chat', async t => { wechatUserName: 'wxid_v7j3e9kna9l912', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { fromId: 'wxid_v7j3e9kna9l912', id: '5403623995065243191', mentionIdList: [], - roomId: undefined, + roomId: '', + talkerId: '', text: '5435185973422451659', timestamp: 1568777162068, toId: 'Soul001001', - type: MessageType.Recalled, + type: types.Message.Recalled, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) t.deepEqual(payload, @@ -457,7 +465,7 @@ test('bot recalled message in private chat', async t => { // toUser: 'wxid_x01jgln69ath22', // } -// const EXPECTED_PAYLOAD: MessagePayload = { +// const EXPECTED_PAYLOAD: payloads.Message = { // filename: '1006688399-to-be-implement.txt', // fromId: 'gh_87e03c422b73', // id: '1006688399', @@ -466,7 +474,7 @@ test('bot recalled message in private chat', async t => { // text: '<![CDATA[这是一个测试的图文消息]]>51000000<![CDATA[这是一个测试的图文消息]]>15597078651000000020000000000200001', // timestamp: 1559707890, // toId: 'wxid_x01jgln69ath22', -// type: MessageType.Recalled, +// type: types.Message.Recalled, // } // const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) // t.deepEqual(payload, EXPECTED_PAYLOAD, 'should parse official account sent url.') @@ -485,7 +493,7 @@ test('bot recalled message in private chat', async t => { // toUser: 'wxid_x01jgln69ath22', // } -// const EXPECTED_PAYLOAD: MessagePayload = { +// const EXPECTED_PAYLOAD: payloads.Message = { // filename: '1601417885-to-be-implement.txt', // fromId: 'wxid_2965349653612', // id: '1601417885', @@ -494,7 +502,7 @@ test('bot recalled message in private chat', async t => { // text: '\n \n <![CDATA[“演员”孙宇晨]]>\n \n \n 5\n 1\n \n 0\n \n \n \n 0\n \n \n \n \n \n \n \n \n \n 0\n 0\n \n \n \n \n 0\n <![CDATA[“演员”孙宇晨]]>\n \n \n \n 1559707142\n \n \n \n 504497991\n \n \n \n \n \n \n \n 0\n 0\n 0\n \n \n 0\n 0\n \n \n \n \n 840787998215241730\n \n \n 2\n 0\n 0\n \n \n \n 0\n <![CDATA[深创投孙东升:专业化是本土创投转型升级的必由之路]]>\n \n \n \n 1559707142\n \n \n \n 504497993\n \n \n \n \n \n \n \n 0\n 0\n 0\n \n \n 0\n 0\n \n \n \n \n 840787998986993664\n \n \n 2\n 0\n 0\n \n \n \n 0\n <![CDATA[松禾资本厉伟:老老实实做生意]]>\n \n \n \n 1559707142\n \n \n \n 504497994\n \n \n \n \n \n \n \n 0\n 0\n 0\n \n \n 0\n 0\n \n \n \n \n 840787999741968385\n \n \n 2\n 0\n 0\n \n \n \n \n \n \n \n \n \n 0\n \n \n \n \n \n \n \n 1\n \n \n \n \n \n \n \n', // timestamp: 1559707752, // toId: 'wxid_x01jgln69ath22', -// type: MessageType.Recalled, +// type: types.Message.Recalled, // } // const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) // t.deepEqual(payload, EXPECTED_PAYLOAD, 'should parse official account sent url.') @@ -521,16 +529,17 @@ test('Transfer money message', async t => { uin: '2963338780', wechatUserName: 'wxid_zovb9ol86m7l22', } - const EXPECTED_PAYLOAD: MessagePayload = { + const EXPECTED_PAYLOAD: payloads.Message = { filename: '2632022077853375799-to-be-implement.txt', fromId: 'lylezhuifeng', id: '2632022077853375799', mentionIdList: [], - roomId: undefined, + roomId: '', + talkerId: '', text: '<![CDATA[微信转账]]>20001', timestamp: 1568207943259, toId: 'wxid_zovb9ol86m7l22', - type: MessageType.Transfer, + type: types.Message.Transfer, } const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) t.deepEqual(payload, EXPECTED_PAYLOAD, 'should parse transfer money message.') @@ -548,7 +557,7 @@ test('Transfer money message', async t => { // timestamp: 1559715714, // toUser: 'lylezhuifeng', // } -// const EXPECTED_PAYLOAD: MessagePayload = { +// const EXPECTED_PAYLOAD: payloads.Message = { // filename: '1601417905-to-be-implement.txt', // fromId: 'wxid_x01jgln69ath22', // id: '1601417905', @@ -557,7 +566,7 @@ test('Transfer money message', async t => { // text: '\n\n<![CDATA[微信转账]]>\n\n\n2000\n\n\n\n\n\n\n\n3\n\n\n\n\n\n\n\n\n\n\n\n', // timestamp: 1559715714, // toId: 'lylezhuifeng', -// type: MessageType.Transfer, +// type: types.Message.Transfer, // } // const payload = await messageRawPayloadParser(MESSAGE_PAYLOAD) // t.deepEqual(payload, EXPECTED_PAYLOAD, 'should parse transfer money confirm message.') diff --git a/src/pure-function-helpers/message-raw-payload-parser.ts b/src/pure-function-helpers/message-raw-payload-parser.ts index 14a6b98..82719f6 100644 --- a/src/pure-function-helpers/message-raw-payload-parser.ts +++ b/src/pure-function-helpers/message-raw-payload-parser.ts @@ -1,7 +1,4 @@ -import { - MessagePayload, - MessageType, -} from 'wechaty-puppet' +import { payloads, types } from '@juzi/wechaty-puppet' import { PadplusMessagePayload, @@ -26,7 +23,7 @@ const PRE = 'messageRawPayloadParser' export async function messageRawPayloadParser ( rawPayload: PadplusMessagePayload, -): Promise { +): Promise { /** * 0. Set Message Type @@ -41,26 +38,26 @@ export async function messageRawPayloadParser ( } as { id : string, timestamp : number, - type : MessageType, + type : types.Message, filename? : string, url? : string, } - if (type === MessageType.Image - || type === MessageType.Audio - || type === MessageType.Video - || type === MessageType.Attachment + if (type === types.Message.Image + || type === types.Message.Audio + || type === types.Message.Video + || type === types.Message.Attachment ) { payloadBase.filename = messageFileName(rawPayload) || undefined } - if (type === MessageType.Emoticon) { + if (type === types.Message.Emoticon) { payloadBase.url = rawPayload.url } - let fromId: undefined | string + let talkerId: undefined | string let roomId: undefined | string - let toId: undefined | string + let listenerId: undefined | string let text: undefined | string @@ -86,12 +83,12 @@ export async function messageRawPayloadParser ( */ if (isContactId(rawPayload.toUserName)) { - toId = rawPayload.toUserName + listenerId = rawPayload.toUserName } else { // TODO: if the message @someone, the toId should set to the mentioned contact id(?) - toId = undefined + listenerId = undefined } @@ -100,19 +97,19 @@ export async function messageRawPayloadParser ( */ if (isContactId(rawPayload.fromUserName)) { - fromId = rawPayload.fromUserName + talkerId = rawPayload.fromUserName } else { const parts = rawPayload.content.split(':\n') if (parts && parts.length > 1) { if (isContactId(parts[0])) { - fromId = parts[0] + talkerId = parts[0] } } else { - fromId = undefined + talkerId = undefined } } @@ -133,7 +130,7 @@ export async function messageRawPayloadParser ( } - if (type === MessageType.Recalled) { + if (type === types.Message.Recalled) { const recalledPayload = await recalledPayloadParser(rawPayload) const pattern = [ @@ -150,18 +147,18 @@ export async function messageRawPayloadParser ( if (isRecalled || isRecalledSelf) { text = recalledPayload.newMsgId if (isRecalledSelf) { - fromId = rawPayload.toUserName + talkerId = rawPayload.toUserName if (isRoomId(rawPayload.fromUserName)) { roomId = rawPayload.fromUserName } else if (isContactId(rawPayload.fromUserName)) { - toId = rawPayload.fromUserName + listenerId = rawPayload.fromUserName } } } else { - payloadBase.type = MessageType.Unknown + payloadBase.type = types.Message.Unknown } } else { - payloadBase.type = MessageType.Unknown + payloadBase.type = types.Message.Unknown } } @@ -169,13 +166,13 @@ export async function messageRawPayloadParser ( /** * 5.1 Validate Room & From ID */ - if (!roomId && !fromId) { - throw Error('empty roomId and empty fromId!') + if (!roomId && !talkerId) { + throw Error('empty roomId and empty talkerId!') } /** * 5.1 Validate Room & To ID */ - if (!roomId && !toId) { + if (!roomId && !listenerId) { throw Error('empty roomId and empty toId!') } @@ -196,27 +193,27 @@ export async function messageRawPayloadParser ( text = await quotePayloadParser(rawPayload) } - let payload: MessagePayload + let payload: payloads.Message // Two branch is the same code. // Only for making TypeScript happy - if (fromId && toId) { + if (talkerId && listenerId) { payload = { ...payloadBase, - fromId, + listenerId: listenerId!, mentionIdList, - roomId, + roomId: roomId!, + talkerId, text, - toId, } } else if (roomId) { payload = { ...payloadBase, - fromId, + listenerId: listenerId!, mentionIdList, roomId, + talkerId: talkerId!, text, - toId, } } else { throw new Error('neither toId nor roomId') @@ -225,47 +222,47 @@ export async function messageRawPayloadParser ( /** * 6. Set app payload type */ - if (type === MessageType.Attachment) { + if (type === types.Message.Attachment) { const appPayload = await appMessageParser(rawPayload) if (appPayload) { switch (appPayload.type) { case WechatAppMessageType.Text: - payload.type = MessageType.Text + payload.type = types.Message.Text payload.text = appPayload.title payload.filename = undefined break case WechatAppMessageType.Url: - payload.type = MessageType.Url + payload.type = types.Message.Url break case WechatAppMessageType.Attach: - payload.type = MessageType.Attachment + payload.type = types.Message.Attachment payload.filename = appPayload.title break case WechatAppMessageType.ChatHistory: - payload.type = MessageType.ChatHistory + payload.type = types.Message.ChatHistory break case WechatAppMessageType.MiniProgram: case WechatAppMessageType.MiniProgramApp: - payload.type = MessageType.MiniProgram + payload.type = types.Message.MiniProgram break case WechatAppMessageType.RedEnvelopes: - payload.type = MessageType.RedEnvelope + payload.type = types.Message.RedEnvelope break case WechatAppMessageType.Transfers: - payload.type = MessageType.Transfer + payload.type = types.Message.Transfer break case WechatAppMessageType.RealtimeShareLocation: - payload.type = MessageType.Location + payload.type = types.Message.Location break case WechatAppMessageType.GroupNote: - payload.type = MessageType.GroupNote + payload.type = types.Message.GroupNote payload.text = appPayload.title break case WechatAppMessageType.QuoteMessage: - payload.type = MessageType.Text + payload.type = types.Message.Text break default: - payload.type = MessageType.Unknown + payload.type = types.Message.Unknown break } } diff --git a/src/pure-function-helpers/message-type.ts b/src/pure-function-helpers/message-type.ts index 8bcedfa..e4a3526 100644 --- a/src/pure-function-helpers/message-type.ts +++ b/src/pure-function-helpers/message-type.ts @@ -1,6 +1,4 @@ -import { - MessageType, -} from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { PadplusMessageType, @@ -8,61 +6,61 @@ import { export function messageType ( rawType: PadplusMessageType, -): MessageType { - let type: MessageType +): types.Message { + let type: types.Message switch (rawType) { case PadplusMessageType.Text: - type = MessageType.Text + type = types.Message.Text break case PadplusMessageType.Image: - type = MessageType.Image + type = types.Message.Image // console.log(rawPayload) break case PadplusMessageType.Voice: - type = MessageType.Audio + type = types.Message.Audio // console.log(rawPayload) break case PadplusMessageType.Emoticon: - type = MessageType.Emoticon + type = types.Message.Emoticon // console.log(rawPayload) break case PadplusMessageType.App: - type = MessageType.Attachment + type = types.Message.Attachment // console.log(rawPayload) break case PadplusMessageType.Location: - type = MessageType.Location + type = types.Message.Location // console.log(rawPayload) break case PadplusMessageType.Video: - type = MessageType.Video + type = types.Message.Video // console.log(rawPayload) break case PadplusMessageType.Sys: - type = MessageType.Unknown + type = types.Message.Unknown break case PadplusMessageType.ShareCard: - type = MessageType.Contact + type = types.Message.Contact break case PadplusMessageType.VoipMsg: case PadplusMessageType.Recalled: - type = MessageType.Recalled + type = types.Message.Recalled break case PadplusMessageType.StatusNotify: case PadplusMessageType.SysNotice: - type = MessageType.Unknown + type = types.Message.Unknown break default: diff --git a/src/pure-function-helpers/room-cache-mapper.spec.ts b/src/pure-function-helpers/room-cache-mapper.spec.ts index aacb240..4b3a8b5 100644 --- a/src/pure-function-helpers/room-cache-mapper.spec.ts +++ b/src/pure-function-helpers/room-cache-mapper.spec.ts @@ -5,7 +5,7 @@ import test from 'blue-tape' -import { PuppetCacheRoomPayload } from 'wechaty-puppet-cache' +import { PuppetCacheRoomPayload } from '@juzi/wechaty-puppet-cache' import { PadplusRoomPayload } from '../schemas' import { cacheToPadplusRoomPayload, padplusToCacheRoomPayload } from './room-cache-mapper' @@ -20,14 +20,14 @@ test('room-cache-mapper', async t => { contactType : 2, memberCount : 3, members : [{ - NickName : 'NickName', - UserName : 'UserName', + nickName : 'nickName', + userName : 'userName', }, { - NickName : 'NickName', - UserName : 'UserName', + nickName : 'nickName', + userName : 'userName', }, { - NickName : 'NickName', - UserName : 'UserName', + nickName : 'nickName', + userName : 'userName', }], nickName : 'nickName', smallHeadUrl : 'smallHeadUrl', @@ -48,18 +48,18 @@ test('room-cache-mapper', async t => { members : [{ avatar : undefined, inviteBy : undefined, - nickName : 'NickName', - userName : 'UserName', + nickName : 'nickName', + userName : 'userName', }, { avatar : undefined, inviteBy : undefined, - nickName : 'NickName', - userName : 'UserName', + nickName : 'nickName', + userName : 'userName', }, { avatar : undefined, inviteBy : undefined, - nickName : 'NickName', - userName : 'UserName', + nickName : 'nickName', + userName : 'userName', }], nickName : 'nickName', smallHeadUrl : 'smallHeadUrl', diff --git a/src/pure-function-helpers/room-cache-mapper.ts b/src/pure-function-helpers/room-cache-mapper.ts index 68a9259..2621d83 100644 --- a/src/pure-function-helpers/room-cache-mapper.ts +++ b/src/pure-function-helpers/room-cache-mapper.ts @@ -1,4 +1,4 @@ -import { PuppetCacheRoomPayload, PuppetMemberBrief } from 'wechaty-puppet-cache' +import { PuppetCacheRoomPayload, PuppetMemberBrief } from '@juzi/wechaty-puppet-cache' import { PadplusRoomPayload, PadplusMemberBrief } from '../schemas' export function cacheToPadplusRoomPayload ( @@ -54,28 +54,28 @@ export function cacheToPadplusMemberBriefPayload ( if (!cachePayload.userName) { if ((cachePayload as any).UserName) { return { - NickName : (cachePayload as any).NickName as string, - UserName : (cachePayload as any).UserName as string, + nickName : (cachePayload as any).nickName as string, + userName : (cachePayload as any).userName as string, } } throw Error('cannot get userName from cache payload: ' + JSON.stringify(cachePayload)) } return { - NickName : cachePayload.nickName, - UserName : cachePayload.userName, + nickName : cachePayload.nickName, + userName : cachePayload.userName, } as PadplusMemberBrief } export function padplusToCacheMemberBriefPayload ( padplusPayload: PadplusMemberBrief, ): PuppetMemberBrief { - if (!padplusPayload.UserName) { + if (!padplusPayload.userName) { throw Error('cannot get UserName from padplus payload: ' + JSON.stringify(padplusPayload)) } return { avatar : undefined, inviteBy : undefined, - nickName : padplusPayload.NickName, - userName : padplusPayload.UserName, + nickName : padplusPayload.nickName, + userName : padplusPayload.userName, } as PuppetMemberBrief } diff --git a/src/pure-function-helpers/room-event-join-message-parser.spec.ts b/src/pure-function-helpers/room-event-join-message-parser.spec.ts index b592ee2..b1bfa27 100755 --- a/src/pure-function-helpers/room-event-join-message-parser.spec.ts +++ b/src/pure-function-helpers/room-event-join-message-parser.spec.ts @@ -4,7 +4,7 @@ // tslint:disable:no-shadowed-variable import test from 'blue-tape' -import { YOU } from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { PadplusMessagePayload, @@ -69,7 +69,7 @@ test('roomJoinEventMessageParser() bot invite other', async t => { } const actual = { inviteeIdList: [ 'wxid_3s4v7osfgpbc22' ], - inviterId: YOU, + inviterId: types.YOU, roomId: '20434481305@chatroom', timestamp: 1595916797061 } diff --git a/src/pure-function-helpers/room-event-join-message-parser.ts b/src/pure-function-helpers/room-event-join-message-parser.ts index c3ae087..267a636 100644 --- a/src/pure-function-helpers/room-event-join-message-parser.ts +++ b/src/pure-function-helpers/room-event-join-message-parser.ts @@ -1,7 +1,7 @@ /* eslint-disable */ import { xmlToJson } from './xml-to-json' -import { YOU } from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { PadplusMessagePayload, @@ -49,6 +49,8 @@ const ROOM_JOIN_BOT_INVITE_OTHER_REGEX_LIST_EN = [ const ROOM_JOIN_OTHER_INVITE_BOT_REGEX_LIST_ZH = [ /^"([^"]+?)"邀请你加入了群聊,群聊参与人还有:(.+)/, + /^"([^"]+?)"邀请你加入了群聊/, + /^"([^"]+?)"邀请你和"(.+?)"加入了群聊/, ] const ROOM_JOIN_OTHER_INVITE_BOT_REGEX_LIST_EN = [ @@ -171,7 +173,7 @@ export async function roomJoinEventMessageParser ( */ const other = matches[1] const inviteeIdList = getUserName(linkList, other) - const inviterId: string | YOU = YOU + const inviterId: string | typeof types.YOU = types.YOU const joinEvent: RoomJoinEvent = { inviteeIdList: checkString(inviteeIdList), inviterId, @@ -188,7 +190,7 @@ export async function roomJoinEventMessageParser ( // /^"([^"]+?)"邀请你和"(.+?)"加入了群聊/, const _inviterName = matches[1] const inviterId = getUserName(linkList, _inviterName) - let inviteeIdList: Array = [ YOU ] + const inviteeIdList: Array = [ types.YOU ] const joinEvent: RoomJoinEvent = { inviteeIdList, diff --git a/src/pure-function-helpers/room-event-leave-message-parser.spec.ts b/src/pure-function-helpers/room-event-leave-message-parser.spec.ts index 9e33054..8966362 100755 --- a/src/pure-function-helpers/room-event-leave-message-parser.spec.ts +++ b/src/pure-function-helpers/room-event-leave-message-parser.spec.ts @@ -5,7 +5,7 @@ // tslint:disable:no-shadowed-variable import test from 'blue-tape' -import { YOU } from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { PadplusMessagePayload, @@ -71,7 +71,7 @@ test('roomLeaveEventMessageParser() bot kick out other', async t => { const actual = { leaverIdList: [ 'Soul001001' ], - removerId: YOU, + removerId: types.YOU, roomId: '25044049015@chatroom', timestamp: 1597032764085, } diff --git a/src/pure-function-helpers/room-event-leave-message-parser.ts b/src/pure-function-helpers/room-event-leave-message-parser.ts index bc82299..9c18a70 100644 --- a/src/pure-function-helpers/room-event-leave-message-parser.ts +++ b/src/pure-function-helpers/room-event-leave-message-parser.ts @@ -8,7 +8,7 @@ import { isPayload, isRoomId, } from './is-type' -import { YOU } from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { xmlToJson } from './xml-to-json' import { getUserName } from './get-xml-label' @@ -37,6 +37,10 @@ const ROOM_LEAVE_BOT_REGEX_LIST = [ /^(你)被"([^"]+?)"移出群聊/, ] +const ROOM_DISMISS_BY_OWNER_REGEX_LIST = [ + /^群主"([^"]+?)"已解散该群聊/, +] + export async function roomLeaveEventMessageParser ( rawPayload: PadplusMessagePayload, ): Promise { @@ -86,26 +90,40 @@ export async function roomLeaveEventMessageParser ( ), ) - const matches = matchesForOther || matchesForBot + let matchesForDismiss: null | string[] = [] + ROOM_DISMISS_BY_OWNER_REGEX_LIST.some( + re => !!( + matchesForDismiss = content.match(re) + ), + ) + + const matches = matchesForOther || matchesForBot || matchesForDismiss if (!matches) { return null } - let leaverId : string | YOU - let removerId : string | YOU + let leaverId : string | typeof types.YOU + let removerId : string | typeof types.YOU + let dismiss: boolean | undefined if (matchesForOther) { - removerId = YOU + removerId = types.YOU const leaverName = matchesForOther[2] leaverId = getUserName([linkList], leaverName) } else if (matchesForBot) { removerId = matchesForBot[2] - leaverId = YOU + leaverId = types.YOU + } else if (matchesForDismiss) { + const removerName = matchesForDismiss[1] + removerId = getUserName([linkList], removerName) // maybe removerId is YOU + leaverId = types.YOU + dismiss = true } else { throw new Error('for typescript type checking, will never go here') } const roomLeaveEvent: RoomLeaveEvent = { + dismiss, leaverIdList : [leaverId], removerId, roomId, diff --git a/src/pure-function-helpers/room-event-topic-message-parser.en.spec.ts b/src/pure-function-helpers/room-event-topic-message-parser.en.spec.ts index 19f10b2..0deccd2 100755 --- a/src/pure-function-helpers/room-event-topic-message-parser.en.spec.ts +++ b/src/pure-function-helpers/room-event-topic-message-parser.en.spec.ts @@ -6,9 +6,7 @@ import test from 'blue-tape' -import { - YOU, -} from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { PadplusMessagePayload, RoomTopicEvent, @@ -67,7 +65,7 @@ test('roomTopicEventMessageParser() EN-bot-modify-topic', async t => { wechatUserName: 'wxid_orp7dihe2pm112' } const EXPECTED_EVENT: RoomTopicEvent = { - changerId: YOU, + changerId: types.YOU, roomId: '24674062762@chatroom', timestamp: 1595938073258, topic: 'new topic !', diff --git a/src/pure-function-helpers/room-event-topic-message-parser.ts b/src/pure-function-helpers/room-event-topic-message-parser.ts index 41b5aea..ed04044 100644 --- a/src/pure-function-helpers/room-event-topic-message-parser.ts +++ b/src/pure-function-helpers/room-event-topic-message-parser.ts @@ -9,7 +9,7 @@ import { isPayload, isRoomId, } from './is-type' -import { YOU } from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { xmlToJson } from './xml-to-json' import { getUserName, getNickName } from './get-xml-label' @@ -64,7 +64,7 @@ export async function roomTopicEventMessageParser ( ROOM_TOPIC_OTHER_REGEX_LIST.some(regex => !!(matchesForOther = content.match(regex))) ROOM_TOPIC_YOU_REGEX_LIST.some(regex => !!(matchesForYou = content.match(regex))) - const matches: Array = matchesForOther || matchesForYou + const matches: Array = matchesForOther || matchesForYou if (!matches) { return null } @@ -73,7 +73,7 @@ export async function roomTopicEventMessageParser ( let topic = matches[2] as string if ((matchesForYou && changerId === '你') || changerId === 'You') { - changerId = YOU + changerId = types.YOU } else { changerId = getUserName(linkList, changerId as string) topic = getNickName(linkList, topic) diff --git a/src/pure-function-helpers/room-event-topic-message-parser.zh.spec.ts b/src/pure-function-helpers/room-event-topic-message-parser.zh.spec.ts index 467b2e6..3ce42cf 100755 --- a/src/pure-function-helpers/room-event-topic-message-parser.zh.spec.ts +++ b/src/pure-function-helpers/room-event-topic-message-parser.zh.spec.ts @@ -5,9 +5,7 @@ import test from 'blue-tape' -import { - YOU, -} from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { PadplusMessagePayload, RoomTopicEvent, @@ -65,7 +63,7 @@ test('roomTopicEventMessageParser() ZH-other-modify-topic', async t => { wechatUserName: 'wxid_zovb9ol86m7l22', } const EXPECTED_EVENT: RoomTopicEvent = { - changerId: YOU, + changerId: types.YOU, roomId: '18295482296@chatroom', timestamp: 1568208437265, topic: '命名了', diff --git a/src/pure-function-helpers/room-invitation-cache-mapper.spec.ts b/src/pure-function-helpers/room-invitation-cache-mapper.spec.ts index 3015918..94cb599 100644 --- a/src/pure-function-helpers/room-invitation-cache-mapper.spec.ts +++ b/src/pure-function-helpers/room-invitation-cache-mapper.spec.ts @@ -5,7 +5,7 @@ import test from 'blue-tape' -import { PuppetCacheRoomInvitationPayload } from 'wechaty-puppet-cache' +import { PuppetCacheRoomInvitationPayload } from '@juzi/wechaty-puppet-cache' import { PadplusRoomInvitationPayload } from '../schemas' import { cacheToPadplusRoomInvitationPayload, padplusToCacheRoomInvitationPayload } from './room-invitation-cache-mapper' diff --git a/src/pure-function-helpers/room-invitation-cache-mapper.ts b/src/pure-function-helpers/room-invitation-cache-mapper.ts index b6a72f8..3b6165a 100644 --- a/src/pure-function-helpers/room-invitation-cache-mapper.ts +++ b/src/pure-function-helpers/room-invitation-cache-mapper.ts @@ -1,4 +1,4 @@ -import { PuppetCacheRoomInvitationPayload } from 'wechaty-puppet-cache' +import { PuppetCacheRoomInvitationPayload } from '@juzi/wechaty-puppet-cache' import { PadplusRoomInvitationPayload } from '../schemas' export function cacheToPadplusRoomInvitationPayload ( diff --git a/src/pure-function-helpers/room-member-cache-mapper.spec.ts b/src/pure-function-helpers/room-member-cache-mapper.spec.ts index e2d53c8..11fd6e8 100644 --- a/src/pure-function-helpers/room-member-cache-mapper.spec.ts +++ b/src/pure-function-helpers/room-member-cache-mapper.spec.ts @@ -5,7 +5,7 @@ import test from 'blue-tape' -import { PuppetCacheRoomMemberPayload } from 'wechaty-puppet-cache' +import { PuppetCacheRoomMemberPayload } from '@juzi/wechaty-puppet-cache' import { PadplusRoomMemberPayload } from '../schemas' import { cacheToPadplusRoomMemberPayload, padplusToCacheRoomMemberPayload } from './room-member-cache-mapper' diff --git a/src/pure-function-helpers/room-member-cache-mapper.ts b/src/pure-function-helpers/room-member-cache-mapper.ts index 89ba883..2d41ced 100644 --- a/src/pure-function-helpers/room-member-cache-mapper.ts +++ b/src/pure-function-helpers/room-member-cache-mapper.ts @@ -1,4 +1,4 @@ -import { PuppetCacheRoomMemberPayload } from 'wechaty-puppet-cache' +import { PuppetCacheRoomMemberPayload } from '@juzi/wechaty-puppet-cache' import { PadplusRoomMemberPayload } from '../schemas' export function cacheToPadplusRoomMemberPayload ( diff --git a/src/pure-function-helpers/room-member-parser.ts b/src/pure-function-helpers/room-member-parser.ts index 6fa01ee..053a9e0 100644 --- a/src/pure-function-helpers/room-member-parser.ts +++ b/src/pure-function-helpers/room-member-parser.ts @@ -7,13 +7,13 @@ export function briefRoomMemberParser ( input.forEach(brief => { const memberPayload: PadplusRoomMemberPayload = { bigHeadUrl: '', - contactId: brief.UserName, + contactId: brief.userName, displayName: '', inviterId: '', - nickName: brief.NickName || '', + nickName: brief.nickName || '', smallHeadUrl: '', } - result[brief.UserName] = memberPayload + result[brief.userName] = memberPayload }) return result } @@ -27,7 +27,7 @@ export function roomMemberParser ( bigHeadUrl: member.HeadImgUrl, contactId: member.UserName, displayName: member.DisplayName, - inviterId: '', + inviterId: member.InvitedBy, nickName: member.NickName, smallHeadUrl: member.HeadImgUrl, } diff --git a/src/pure-function-helpers/room-raw-payload-parser.ts b/src/pure-function-helpers/room-raw-payload-parser.ts index 6623938..dda6130 100644 --- a/src/pure-function-helpers/room-raw-payload-parser.ts +++ b/src/pure-function-helpers/room-raw-payload-parser.ts @@ -1,6 +1,4 @@ -import { - RoomPayload, -} from 'wechaty-puppet' +import { payloads } from '@juzi/wechaty-puppet' import { PadplusRoomPayload, @@ -8,12 +6,12 @@ import { export function roomRawPayloadParser ( rawPayload: PadplusRoomPayload, -): RoomPayload { - const payload: RoomPayload = { +): payloads.Room { + const payload: payloads.Room = { adminIdList : [], avatar : rawPayload.bigHeadUrl, id : rawPayload.chatroomId, - memberIdList : rawPayload.members.map(m => m.UserName) || [], + memberIdList : rawPayload.members.map(m => m.userName) || [], ownerId : rawPayload.chatRoomOwner, topic : rawPayload.nickName, } diff --git a/src/pure-function-helpers/video-process.ts b/src/pure-function-helpers/video-process.ts index c4c17f3..f4060d6 100644 --- a/src/pure-function-helpers/video-process.ts +++ b/src/pure-function-helpers/video-process.ts @@ -1,13 +1,17 @@ -import { FileBox } from 'wechaty-puppet' +import { FileBox } from 'file-box' import { VideoContent } from '../schemas' import { PadplusMessage } from '../padplus-manager/api-request/message' +import { log } from '../config' +const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path const extractFrames = require('ffmpeg-extract-frames') const probe = require('ffmpeg-probe') export async function videoPreProcess (request: PadplusMessage, url: string): Promise { const name = `screenshot-${Date.now()}.jpg` const path = './screenshot/' + name + log.info(`videoPreProcess ffmpegPath: ${ffmpegPath}`) await extractFrames({ + ffmpegPath, input: url, offsets: [1], output: path, diff --git a/src/schemas/grpc-schemas.ts b/src/schemas/grpc-schemas.ts index f4a6b6c..f245d59 100644 --- a/src/schemas/grpc-schemas.ts +++ b/src/schemas/grpc-schemas.ts @@ -1,5 +1,5 @@ /* eslint camelcase: 0 */ -import { ContactGender } from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' import { ContactOperationBitVal, @@ -66,7 +66,7 @@ export interface GrpcSelfInfoPayload { signature: string, // "", userName: string, // "lylezhuifeng", -> unique id nickName: string, // "高原ོ", - sex: ContactGender, // 1, + sex: types.ContactGender, // 1, province: string, // "Beijing", city: string, // "", bindEmail: string, // "lylezhuifeng@qq.com", diff --git a/src/schemas/model-contact.ts b/src/schemas/model-contact.ts index d6638de..559e6a3 100644 --- a/src/schemas/model-contact.ts +++ b/src/schemas/model-contact.ts @@ -1,6 +1,4 @@ -import { - ContactGender, -} from 'wechaty-puppet' +import { types } from '@juzi/wechaty-puppet' export interface PadplusContactPayload { alias : string, @@ -12,7 +10,7 @@ export interface PadplusContactPayload { nickName : string, // "梦君君", Contact: 用户昵称, Room: 群昵称 province : string, // "Beijing", remark : string, // "女儿", - sex : ContactGender, + sex : types.ContactGender, signature : string, // "且行且珍惜", smallHeadUrl : string, stranger : string, // 用户v1码,从未加过好友则为空 "v1_0468f2cd3f0efe7ca2589d57c3f9ba952a3789e41b6e78ee00ed53d1e6096b88@stranger" diff --git a/src/schemas/model-event.ts b/src/schemas/model-event.ts index d1d30fa..54581ea 100644 --- a/src/schemas/model-event.ts +++ b/src/schemas/model-event.ts @@ -1,5 +1,5 @@ -import { YOU } from 'wechaty-puppet' - +import { types } from '@juzi/wechaty-puppet' +type YOU = typeof types.YOU export interface RoomJoinEvent { inviteeIdList : Array, inviterId : string | YOU, @@ -8,17 +8,18 @@ export interface RoomJoinEvent { } export interface RoomLeaveEvent { - leaverIdList : Array<(string | YOU)>, + leaverIdList : Array, removerId : string | YOU, roomId : string, timestamp : number, // Unix Timestamp, in seconds + dismiss? : boolean, } export interface RoomTopicEvent { changerId : string | YOU, - roomId : string, - topic : string, - timestamp : number, // Unix Timestamp, in seconds + roomId : string, + topic : string, + timestamp : number, // Unix Timestamp, in seconds } export interface RoomInviteEvent { diff --git a/src/schemas/model-message.ts b/src/schemas/model-message.ts index 21c4666..6f18b21 100644 --- a/src/schemas/model-message.ts +++ b/src/schemas/model-message.ts @@ -5,6 +5,11 @@ import { WechatAppMessageType, } from './padplus-enums' +export interface PadplusGetCDNRequestData { + url: string, + toUserName: string, +} + export interface PadplusRichMediaData { content: string, msgType: number, @@ -160,6 +165,13 @@ export interface PadplusUrlLink { url : string, } +export interface PadplusCDNData { + url: string, + toUserName: string, + fileid: string, + aesKey: string, +} + export interface PadplusMediaData { content: string, msgId: string, diff --git a/src/schemas/model-room.ts b/src/schemas/model-room.ts index d3c11f0..12870e1 100644 --- a/src/schemas/model-room.ts +++ b/src/schemas/model-room.ts @@ -9,8 +9,8 @@ export interface PadplusRoomMemberPayload { } export interface PadplusMemberBrief { - UserName : string, - NickName?: string, + userName : string, + nickName?: string, } export interface GrpcRoomMemberPayload { diff --git a/src/server-manager/cache-manager.ts b/src/server-manager/cache-manager.ts index 9bb9db6..a9a8444 100644 --- a/src/server-manager/cache-manager.ts +++ b/src/server-manager/cache-manager.ts @@ -11,7 +11,7 @@ import { PuppetCache, PuppetCacheFriendshipPayload, PuppetCacheRoomMemberPayloadMap, PuppetCacheStoreOptions, -} from 'wechaty-puppet-cache' +} from '@juzi/wechaty-puppet-cache' import { log } from '../config' import { @@ -21,7 +21,7 @@ import { PadplusRoomMemberMap, PadplusMessagePayload, } from '../schemas' -import { FriendshipPayload } from 'wechaty-puppet' +import { payloads } from '@juzi/wechaty-puppet' import { cacheToPadplusMessagePayload, padplusToCacheMessagePayload, padplusToCacheContactPayload, @@ -278,6 +278,10 @@ export class CacheManager { throw new Error(`${PRE} setRoomMember() has no cache.`) } const map: PuppetCacheRoomMemberPayloadMap = {} + if (Object.keys(payload).length === 0) { + await this.cacheRoomMemberRawPayload.set(roomId, map) + return + } for (const memberId of Object.keys(payload)) { map[memberId] = padplusToCacheRoomMemberPayload(payload[memberId]) } @@ -293,6 +297,15 @@ export class CacheManager { await this.cacheRoomMemberRawPayload.delete(roomId) } + public async dismissRoomMember ( + roomId: string, + ): Promise { + if (!this.cacheRoomMemberRawPayload) { + throw new Error(`${PRE} dismissRoomMember() has no cache.`) + } + await this.cacheRoomMemberRawPayload.set(roomId, {}) + } + /** * ------------------------------- * Room Invitation Section @@ -343,7 +356,7 @@ export class CacheManager { public async setFriendshipRawPayload ( id: string, - payload: FriendshipPayload, + payload: payloads.Friendship, ) { if (!this.cacheFriendshipRawPayload) { throw new Error(`${PRE} setFriendshipRawPayload() has no cache.`) diff --git a/src/server-manager/grpc-gateway.ts b/src/server-manager/grpc-gateway.ts index 60fa8ea..56e5a3b 100644 --- a/src/server-manager/grpc-gateway.ts +++ b/src/server-manager/grpc-gateway.ts @@ -38,6 +38,7 @@ const NEED_CALLBACK_API_LIST: ApiType[] = [ ApiType.INIT, ApiType.SEND_MESSAGE, ApiType.SEND_FILE, + ApiType.GET_CDN_DATA, ApiType.GET_MESSAGE_MEDIA, ApiType.SEARCH_CONTACT, ApiType.ADD_CONTACT, @@ -63,6 +64,13 @@ const NEED_CALLBACK_API_LIST: ApiType[] = [ export type GrpcGatewayEvent = 'data' | 'reconnect' | 'grpc-end' | 'grpc-close' | 'heartbeat' +export enum RECONNECT_STATUS { + NEED_RECONNECT = 1, + INVALID = 2, + EXPIRED = 3, + DUPLICATED = 4, +} + export class GrpcGateway extends EventEmitter { private static _instance?: GrpcGateway = undefined @@ -128,7 +136,7 @@ export class GrpcGateway extends EventEmitter { private timeoutNumber: number private startTime: number private stream?: grpc.ClientReadableStream - private reconnectStatus: boolean + private reconnectStatus: RECONNECT_STATUS private constructor ( private token: string, @@ -138,7 +146,7 @@ export class GrpcGateway extends EventEmitter { this.stopping = false this.client = new PadPlusServerClient(this.endpoint, grpc.credentials.createInsecure(), GRPC_LIMITATION) this.isAlive = false - this.reconnectStatus = true + this.reconnectStatus = RECONNECT_STATUS.NEED_RECONNECT this.timeoutNumber = 0 this.startTime = Date.now() } @@ -292,7 +300,7 @@ export class GrpcGateway extends EventEmitter { Object.values(this.eventEmitterMap).map(emitter => { emitter.emit('reconnect') }) - if (err.details === 'INVALID_TOKEN') { + if ((err as unknown as any).details === 'INVALID_TOKEN') { padplusToken() } } @@ -367,7 +375,7 @@ export class GrpcGateway extends EventEmitter { const channel = this.client.getChannel() if (channel) { - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { channel.getConnectivityState(true) const beforeState = channel.getConnectivityState(false) channel.watchConnectivityState(beforeState, Date.now() + 5000, (err) => { @@ -407,27 +415,33 @@ export class GrpcGateway extends EventEmitter { } }) stream.on('end', async () => { - if (this.reconnectStatus) { - log.error(PRE, `GRPC SERVER END. - ===================================================================== - try to reconnect grpc server, waiting... - ===================================================================== - `) - await new Promise(resolve => setTimeout(resolve, 5000)) - this.isAlive = false - if (!this.stopping) { - Object.values(this.eventEmitterMap).map(emitter => { - emitter.emit('reconnect') - }) - } - } else { - log.info(PRE, ` - ===================================================================== - DUPLICATE CONNECTED, THIS THREAD WILL EXIT NOW - ===================================================================== - See: https://github.com/wechaty/wechaty-puppet-padplus/issues/169 - `) - process.exit() + switch (this.reconnectStatus) { + case RECONNECT_STATUS.NEED_RECONNECT: + log.error(PRE, `GRPC SERVER END. + ===================================================================== + try to reconnect grpc server, waiting... + ===================================================================== + `) + await new Promise(resolve => setTimeout(resolve, 5000)) + this.isAlive = false + if (!this.stopping) { + Object.values(this.eventEmitterMap).map(emitter => { + emitter.emit('reconnect') + }) + } + break + case RECONNECT_STATUS.INVALID: + case RECONNECT_STATUS.EXPIRED: + process.exit() + case RECONNECT_STATUS.DUPLICATED: + default: + log.info(PRE, ` + ===================================================================== + DUPLICATE CONNECTED, THIS THREAD WILL EXIT NOW + ===================================================================== + See: https://github.com/wechaty/wechaty-puppet-padplus/issues/169 + `) + process.exit() } }) stream.on('close', async () => { @@ -459,15 +473,15 @@ export class GrpcGateway extends EventEmitter { } } if (message && message === 'Another instance connected, disconnected the current one.') { - this.reconnectStatus = false + this.reconnectStatus = RECONNECT_STATUS.DUPLICATED } else if (message && message === 'EXPIRED_TOKEN') { Object.values(this.eventEmitterMap).map(emitter => { - this.reconnectStatus = false + this.reconnectStatus = RECONNECT_STATUS.EXPIRED emitter.emit('EXPIRED_TOKEN') }) } else if (message && message === 'INVALID_TOKEN') { Object.values(this.eventEmitterMap).map(emitter => { - this.reconnectStatus = false + this.reconnectStatus = RECONNECT_STATUS.INVALID emitter.emit('INVALID_TOKEN') }) } diff --git a/src/server-manager/proto-ts/PadPlusServer_pb.d.ts b/src/server-manager/proto-ts/PadPlusServer_pb.d.ts index 810f1c6..3be60fd 100644 --- a/src/server-manager/proto-ts/PadPlusServer_pb.d.ts +++ b/src/server-manager/proto-ts/PadPlusServer_pb.d.ts @@ -195,6 +195,7 @@ export enum ApiType { REVOKE_MESSAGE = 52, GET_MESSAGE_MEDIA = 53, UPLOAD_FILE = 55, + GET_CDN_DATA = 56, GET_ALL_TAG = 70, CREATE_TAG = 71, ADD_TAG = 72, @@ -229,6 +230,7 @@ export enum ResponseType { MESSAGE_MEDIA_SRC = 32, MESSAGE_REVOKE = 33, FILE_UPLOADED = 35, + CDN_DATA_RSP = 36, TAG_LIST = 40, TAG_CREATE = 41, TAG_ADD = 42, diff --git a/src/server-manager/proto-ts/PadPlusServer_pb.js b/src/server-manager/proto-ts/PadPlusServer_pb.js index 9824b33..8364a01 100644 --- a/src/server-manager/proto-ts/PadPlusServer_pb.js +++ b/src/server-manager/proto-ts/PadPlusServer_pb.js @@ -1044,6 +1044,7 @@ proto.PadPlusServer.ApiType = { REVOKE_MESSAGE: 52, GET_MESSAGE_MEDIA: 53, UPLOAD_FILE: 55, + GET_CDN_DATA: 56, GET_ALL_TAG: 70, CREATE_TAG: 71, ADD_TAG: 72, @@ -1081,6 +1082,7 @@ proto.PadPlusServer.ResponseType = { MESSAGE_MEDIA_SRC: 32, MESSAGE_REVOKE: 33, FILE_UPLOADED: 35, + CDN_DATA_RSP: 36, TAG_LIST: 40, TAG_CREATE: 41, TAG_ADD: 42, diff --git a/src/server-manager/proto/PadPlusServer.proto b/src/server-manager/proto/PadPlusServer.proto index 1d76a5a..c432b4c 100644 --- a/src/server-manager/proto/PadPlusServer.proto +++ b/src/server-manager/proto/PadPlusServer.proto @@ -36,6 +36,7 @@ enum ApiType { REVOKE_MESSAGE = 52; GET_MESSAGE_MEDIA = 53; UPLOAD_FILE = 55; + GET_CDN_DATA = 56; GET_ALL_TAG = 70; CREATE_TAG = 71; @@ -91,6 +92,7 @@ enum ResponseType { MESSAGE_MEDIA_SRC = 32; MESSAGE_REVOKE = 33; FILE_UPLOADED = 35; + CDN_DATA_RSP = 36; TAG_LIST = 40; TAG_CREATE = 41; diff --git a/src/utils/util.ts b/src/utils/util.ts index b771afd..1286d9f 100644 --- a/src/utils/util.ts +++ b/src/utils/util.ts @@ -120,6 +120,12 @@ export default function checkNumber (phone: string): boolean { } +export const sleep = async (milliseconds?: number) => { + if (milliseconds) { + await new Promise(resolve => setTimeout(resolve, milliseconds)) + } +} + export const ApiTypeDic = { [ApiType.ACCEPT_CONTACT]: 'ACCEPT_CONTACT', [ApiType.ACCEPT_ROOM_INVITATION]: 'ACCEPT_ROOM_INVITATION', @@ -133,6 +139,7 @@ export const ApiTypeDic = { [ApiType.DELETE_TAG]: 'DELETE_TAG', [ApiType.GET_ALL_TAG]: 'GET_ALL_TAG', [ApiType.GET_CONTACT]: 'GET_CONTACT', + [ApiType.GET_CDN_DATA]: 'GET_CDN_DATA', [ApiType.GET_CONTACT_SELF_INFO]: 'GET_CONTACT_SELF_INFO', [ApiType.GET_CONTACT_SELF_QRCODE]: 'GET_CONTACT_SELF_QRCODE', [ApiType.GET_MESSAGE_MEDIA]: 'GET_MESSAGE_MEDIA', @@ -183,6 +190,7 @@ export const ResponseTypeDic = { [ResponseType.MESSAGE_RECEIVE]: 'MESSAGE_RECEIVE', [ResponseType.STATUS_NOTIFY]: 'STATUS_NOTIFY', [ResponseType.MESSAGE_MEDIA_SRC]: 'MESSAGE_MEDIA_SRC', + [ResponseType.CDN_DATA_RSP]: 'CDN_DATA_RSP', [ResponseType.MESSAGE_REVOKE]: 'MESSAGE_REVOKE', [ResponseType.FILE_UPLOADED]: 'FILE_UPLOADED', [ResponseType.TAG_LIST]: 'TAG_LIST', diff --git a/tsconfig.json b/tsconfig.json index a5c55d2..37235f9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "dist", "noUnusedParameters": false, + "lib": ["ESNext", "DOM"], }, "exclude": [ "node_modules/",