提交 d08c7d2c 编写于 作者: Huan (李卓桓)'s avatar Huan (李卓桓)

refactoring & code clean & add new feat

上级 112edee9
......@@ -86,7 +86,13 @@ bot
})
.on('message', async msg => {
if (msg.type() !== bot.Message.Type.Text) {
log.info('Bot', 'on(message) skip non-text message ' + msg)
log.info('Bot', 'on(message) skip non-text message: %s', msg)
return
}
const msgAge = Date.now() - msg.date().getTime()
if (msgAge > 1000 * 60) {
log.info('Bot', 'on(message) skip message older than 60 seconds: %s', msg)
return
}
......
......@@ -43,9 +43,9 @@ export abstract class Accessory extends EventEmitter {
}
public static get puppet(): Puppet {
log.silly('Accessory', '<%s> static get puppet()',
this.name,
)
// log.silly('Accessory', '<%s> static get puppet()',
// this.name,
// )
if (this._puppet) {
return this._puppet
......@@ -68,9 +68,9 @@ export abstract class Accessory extends EventEmitter {
}
public static get wechaty(): Wechaty {
log.silly('Accessory', '<%s> static get wechaty()',
this.name,
)
// log.silly('Accessory', '<%s> static get wechaty()',
// this.name,
// )
if (this._wechaty) {
return this._wechaty
......@@ -112,10 +112,10 @@ export abstract class Accessory extends EventEmitter {
*
*/
public get puppet(): Puppet {
log.silly('Accessory', '#%d<%s> get puppet()',
this[SYMBOL_COUNTER],
this[SYMBOL_NAME] || this,
)
// log.silly('Accessory', '#%d<%s> get puppet()',
// this[SYMBOL_COUNTER],
// this[SYMBOL_NAME] || this,
// )
if (this._puppet) {
return this._puppet
......@@ -129,17 +129,6 @@ export abstract class Accessory extends EventEmitter {
return instanceToClass(this, Accessory).puppet
}
// public set wechaty(wechaty: Wechaty) {
// log.silly('Accessory', '<%s> set wechaty = %s',
// this[SYMBOL_NAME] || this,
// wechaty,
// )
// if (this._wechaty) {
// throw new Error('set twice')
// }
// this._wechaty = wechaty
// }
/**
* instance.wechaty is for:
* Contact.wechaty
......@@ -150,14 +139,11 @@ export abstract class Accessory extends EventEmitter {
* So it only need one `wechaty` for all the instances
*/
public get wechaty(): Wechaty {
log.silly('Accessory', '#%d<%s> get wechaty()',
this[SYMBOL_COUNTER],
this[SYMBOL_NAME] || this,
)
// log.silly('Accessory', '#%d<%s> get wechaty()',
// this[SYMBOL_COUNTER],
// this[SYMBOL_NAME] || this,
// )
// if (this._wechaty) {
// return this._wechaty
// }
/**
* Get `wechaty` from Class Static puppet property
* note: use `instanceToClass` at here is because
......
......@@ -554,8 +554,7 @@ export class Contact extends Accessory implements Sayable {
try {
await this.puppet.contactPayload(this.id, noCache)
log.silly('Contact', `ready() this.puppet.contactPayload(%s) resolved`, this)
// console.log(this.payload)
// log.silly('Contact', `ready() this.puppet.contactPayload(%s) resolved`, this)
} catch (e) {
log.error('Contact', `ready() this.puppet.contactPayload(%s) exception: %s`,
......
......@@ -726,6 +726,16 @@ export class Message extends Accessory implements Sayable {
throw e
}
}
public date(): Date {
if (!this.payload) {
throw new Error('no payload')
}
// convert the unit timestamp to milliseconds
// (from seconds to milliseconds)
return new Date(1000 * this.payload.timestamp)
}
}
export default Message
......@@ -35,13 +35,13 @@ import {
log,
} from '../config'
const MEMORY_SLOT_WECHATY_PUPPET_PADCHAT = 'WECHATY_PUPPET_PADCHAT'
const MEMORY_SLOT_NAME = 'WECHATY_PUPPET_PADCHAT'
export interface PadchatMemorySlot {
device: {
[userId: string]: undefined | {
token : string,
name? : string,
data : string,
token : string,
},
},
currentUserId?: string,
......@@ -102,7 +102,7 @@ export class Bridge extends PadchatRpc {
}
// this.padchatRpc = new PadchatRpc(options.endpoint, options.token)
this.state = new StateSwitch('PuppetPadchatBridge')
this.state = new StateSwitch('PuppetPadchatBridge')
}
private async initCache(
......@@ -139,28 +139,42 @@ export class Bridge extends PadchatRpc {
this.cacheRoomRawPayload = new FlashStoreSync(path.join(baseDir, 'room-raw-payload'))
this.cacheRoomMemberRawPayload = new FlashStoreSync(path.join(baseDir, 'room-member-raw-payload'))
await this.cacheContactRawPayload.ready()
await this.cacheRoomRawPayload.ready()
await this.cacheRoomMemberRawPayload.ready()
await Promise.all([
this.cacheContactRawPayload.ready(),
this.cacheRoomMemberRawPayload.ready(),
this.cacheRoomRawPayload.ready(),
])
const roomMemberTotalNum = [...this.cacheRoomMemberRawPayload.values()].reduce(
(accuVal, currVal) => {
return accuVal + Object.keys(currVal).length
},
0,
)
log.verbose('PuppetPadchatBridge', 'initCache() inited %d Contacts, %d Rooms, %d RoomMembers, cachedir="%s"',
this.cacheContactRawPayload.size,
this.cacheRoomRawPayload.size,
this.cacheRoomMemberRawPayload.size,
roomMemberTotalNum,
baseDir,
)
}
private releaseCache(): void {
log.verbose('PuppetPadchatBridge', 'releaseCache(%s, %s)')
private async releaseCache(): Promise<void> {
log.verbose('PuppetPadchatBridge', 'releaseCache()')
if ( this.cacheRoomRawPayload
&& this.cacheRoomMemberRawPayload
&& this.cacheContactRawPayload
) {
this.cacheContactRawPayload.clear()
this.cacheRoomRawPayload.clear()
this.cacheRoomMemberRawPayload.clear()
await Promise.all([
this.cacheContactRawPayload.ready(),
this.cacheRoomMemberRawPayload.ready(),
this.cacheRoomRawPayload.ready(),
])
this.cacheContactRawPayload = undefined
this.cacheRoomMemberRawPayload = undefined
this.cacheRoomRawPayload = undefined
} else {
throw new Error('cache not exist')
}
......@@ -177,23 +191,12 @@ export class Bridge extends PadchatRpc {
this.memorySlot = {
...this.memorySlot,
...await this.options.memory.get<PadchatMemorySlot>(MEMORY_SLOT_WECHATY_PUPPET_PADCHAT),
...await this.options.memory.get<PadchatMemorySlot>(MEMORY_SLOT_NAME),
}
// await this.padchatRpc.start()
await super.start()
this.on('padchat-logout', () => {
if (this.selfId) {
this.emit('logout')
} else {
/**
* PadchatRpc will receive 'logout' message multiple times when the server is logouted.
*/
log.warn('PuppetPadchatBridge', 'start() on(padchat-logout) received but selfId is undefined')
}
})
// this.padchatRpc.on('message', messageRawPayload => {
// log.silly('PuppetPadchatBridge', 'start() padchatRpc.on(message)')
// this.emit('message', messageRawPayload)
......@@ -231,7 +234,7 @@ export class Bridge extends PadchatRpc {
this.state.off(true)
}
protected async login(userId: string, userName?: string): Promise<void> {
protected async onLogin(userId: string): Promise<void> {
log.verbose('PuppetPadchatBridge', `login(%s)`, userId)
if (this.selfId) {
......@@ -250,9 +253,8 @@ export class Bridge extends PadchatRpc {
this.memorySlot = await this.refresh62DataForMemory(
this.memorySlot,
userId,
userName,
)
await this.options.memory.set(MEMORY_SLOT_WECHATY_PUPPET_PADCHAT, this.memorySlot)
await this.options.memory.set(MEMORY_SLOT_NAME, this.memorySlot)
await this.options.memory.save()
/**
......@@ -294,7 +296,7 @@ export class Bridge extends PadchatRpc {
if (this.selfId) {
log.warn('PuppetPadchatBridge', 'startCheckScan() this.username exist.')
this.login(this.selfId)
this.onLogin(this.selfId)
return
}
......@@ -360,10 +362,7 @@ export class Bridge extends PadchatRpc {
// this.autoData.nick_name = loginResult.nick_name
// this.autoData.user_name = loginResult.user_name
this.login(
loginResult.user_name,
loginResult.nick_name,
)
this.onLogin(loginResult.user_name)
return
case WXCheckQRCodeStatus.Timeout:
......@@ -480,7 +479,7 @@ export class Bridge extends PadchatRpc {
* 2 Auto Login Success
*/
if (autoLoginResult.status === 0) {
this.login(autoLoginResult.user_name)
this.onLogin(autoLoginResult.user_name)
return true
}
......@@ -536,9 +535,8 @@ export class Bridge extends PadchatRpc {
protected async refresh62DataForMemory(
memorySlot: PadchatMemorySlot,
userId : string,
userName?: string,
): Promise<PadchatMemorySlot> {
log.verbose('PuppetPadchatBridge', `refresh62Data(%s, %s)`, userId, userName)
log.verbose('PuppetPadchatBridge', `refresh62Data(%s, %s)`, userId)
/**
* must do a HeatBeat before WXGenerateWxData()
......@@ -554,14 +552,12 @@ export class Bridge extends PadchatRpc {
if (!memorySlot.currentUserId) {
log.silly('PuppetPadchatBridge', 'refresh62Data() memorySlot is empty, init & return it')
const name = userName
const data = await this.WXGenerateWxDat()
const token = await this.WXGetLoginToken()
memorySlot.currentUserId = userId
memorySlot.device[userId] = {
data,
name,
token,
}
......@@ -580,15 +576,13 @@ export class Bridge extends PadchatRpc {
* 3. Current user is a user that had used this memorySlot, use the old data for it.
*/
if (userId in memorySlot.device) {
log.silly('PuppetPadchatBridge', 'refresh62Data() current userId has existing device info, set %s(%s) as currentUserId and use old data for it',
log.silly('PuppetPadchatBridge', 'refresh62Data() current userId has existing device info, set %s as currentUserId and use old data for it',
userId,
userName,
)
memorySlot.currentUserId = userId
memorySlot.device[userId] = {
...memorySlot.device[userId]!,
name : userName,
token : await this.WXGetLoginToken(),
}
......@@ -597,21 +591,17 @@ export class Bridge extends PadchatRpc {
/**
* 4. New user login, generate 62data for it
*/
log.verbose('PuppetPadchatBridge', `refresh62Data() user switch detected: from "%s(%s)" to "%s(%s)"`,
memorySlot.currentUserId && memorySlot.device[memorySlot.currentUserId]!.name,
log.verbose('PuppetPadchatBridge', 'refresh62Data() user switch detected: from "%s" to "%s"',
memorySlot.currentUserId,
userName,
userId,
)
const name = userName
const data = await this.WXGenerateWxDat()
const token = await this.WXGetLoginToken()
memorySlot.currentUserId = userId
memorySlot.device[userId] = {
data,
name,
token,
}
......@@ -710,7 +700,8 @@ export class Bridge extends PadchatRpc {
// const memberListPayload = await this.padchatRpc.WXGetChatRoomMember(roomId)
const memberListPayload = await this.WXGetChatRoomMember(roomId)
if (!memberListPayload) {
if (!memberListPayload || !('user_name' in memberListPayload)) { // check user_name too becasue the server might return {}
console.log('memberListPayload', memberListPayload)
throw new Error('no memberListPayload')
}
......@@ -741,7 +732,7 @@ export class Bridge extends PadchatRpc {
}
public async syncContactsAndRooms(): Promise<void> {
log.verbose('PuppetPadchatBridge', `syncContactsAndRooms()`)
log.verbose('PuppetPadchatBridge', `synctactsAndRooms()`)
let cont = true
while (cont && this.state.on() && this.selfId) {
......@@ -831,24 +822,24 @@ export class Bridge extends PadchatRpc {
}
}
public async contactRawPayload(contactid: string): Promise<PadchatContactPayload> {
log.silly('PuppetPadchatBridge', 'contactRawPayload(%s)', contactid)
public async contactRawPayload(contactId: string): Promise<PadchatContactPayload> {
log.silly('PuppetPadchatBridge', 'contactRawPayload(%s)', contactId)
const rawPayload = await Misc.retry(async (retry, attempt) => {
log.silly('PuppetPadchatBridge', 'contactRawPayload(%s) retry() attempt=%d', contactid, attempt)
log.silly('PuppetPadchatBridge', 'contactRawPayload(%s) retry() attempt=%d', contactId, attempt)
if (!this.cacheContactRawPayload) {
throw new Error('no cache')
}
if (this.cacheContactRawPayload.has(contactid)) {
return this.cacheContactRawPayload.get(contactid)
if (this.cacheContactRawPayload.has(contactId)) {
return this.cacheContactRawPayload.get(contactId)
}
// const tryRawPayload = await this.padchatRpc.WXGetContactPayload(contactid)
const tryRawPayload = await this.WXGetContactPayload(contactid)
if (tryRawPayload) {
this.cacheContactRawPayload.set(contactid, tryRawPayload)
const tryRawPayload = await this.WXGetContactPayload(contactId)
if (tryRawPayload && tryRawPayload.user_name) { // check user_name too becasue the server might return {}
this.cacheContactRawPayload.set(contactId, tryRawPayload)
return tryRawPayload
}
return retry(new Error('tryRawPayload empty'))
......@@ -876,7 +867,7 @@ export class Bridge extends PadchatRpc {
// const tryRawPayload = await this.padchatRpc.WXGetRoomPayload(id)
const tryRawPayload = await this.WXGetRoomPayload(id)
if (tryRawPayload) {
if (tryRawPayload && tryRawPayload.user_name) { // check user_name too becasue the server might return {}
this.cacheRoomRawPayload.set(id, tryRawPayload)
return tryRawPayload
}
......
......@@ -18,7 +18,7 @@ function padchatToken() {
Learn more about it at: https://github.com/Chatie/wechaty/issues/1296
`)
throw new Error('set WECHATY_PUPPET_PADCHAT_TOKEN and try again')
throw new Error('You need a valid WECHATY_PUPPET_PADCHAT_TOKEN to use PuppetPadchat')
}
return token
}
......
......@@ -7,6 +7,11 @@ import Peer, {
parse,
} from 'json-rpc-peer'
import {
ThrottleQueue,
DebounceQueue,
} from 'rx-queue'
// , {
// JsonRpcPayload,
// JsonRpcPayloadRequest,
......@@ -63,10 +68,16 @@ import {
import { log } from '../config'
let HEART_BEAT_COUNTER = 0
export class PadchatRpc extends EventEmitter {
private socket? : WebSocket
private readonly jsonRpc : any // Peer
private readonly throttleQueue: ThrottleQueue<string>
private readonly debounceQueue: DebounceQueue<string>
private readonly logoutThrottleQueue: ThrottleQueue<string>
constructor(
protected endpoint : string,
protected token : string,
......@@ -75,6 +86,22 @@ export class PadchatRpc extends EventEmitter {
log.verbose('PadchatRpc', 'constructor(%s, %s)', endpoint, token)
this.jsonRpc = new Peer()
/**
* Throttle for 10 seconds
*/
this.throttleQueue = new ThrottleQueue<string>(1000 * 10)
/**
* Debounce for 20 seconds
*/
this.debounceQueue = new DebounceQueue<string>(1000 * 10 * 2)
/**
* Throttle for 5 seconds for the `logout` event:
* we should only fire once for logout,
* but the server will send many events of 'logout'
*/
this.logoutThrottleQueue = new ThrottleQueue<string>(1000 * 5)
}
public async start(): Promise<void> {
......@@ -85,6 +112,12 @@ export class PadchatRpc extends EventEmitter {
await this.init()
await this.WXInitialize()
await this.initHearteat()
this.logoutThrottleQueue.subscribe(msg => {
this.destroy(msg)
})
}
protected async initJsonRpc(): Promise<void> {
......@@ -144,6 +177,7 @@ export class PadchatRpc extends EventEmitter {
this.socket = ws
ws.on('message', (data: string) => {
// log.silly('PadchatRpc', 'initWebSocket() ws.on(message)')
try {
const payload: PadchatPayload = JSON.parse(data)
......@@ -156,14 +190,75 @@ export class PadchatRpc extends EventEmitter {
ws.on('error', err => this.emit('error', err))
// TODO: add reconnect logic here when disconnected, or throw Error
await new Promise((resolve, reject) => {
ws.once('open', resolve)
ws.once('error', reject)
ws.once('close', reject)
})
/**
* emit reset to broadcast that it need to be reseted
* when the websocket is closed
*/
ws.on('close', e => {
log.warn('PadchatRpc', 'initWebSocket() ws.on(close) %s', e)
this.logoutThrottleQueue.next('ws.on(close, ' + e)
})
/**
* use websocket message as heartbeat source
*/
ws.on('message', () => {
this.throttleQueue.next('ws.on(message)')
this.debounceQueue.next('ws.on(message)')
})
ws.on('pong', data => {
this.throttleQueue.next(data.toString())
this.debounceQueue.next(data.toString())
})
}
private initHearteat(): void {
log.verbose('PadchatRpc', 'initHeartbeat()')
this.throttleQueue.subscribe(e => {
/**
* This block will only be run once in a period,
* no matter than how many message the queue received.
*/
log.silly('PadchatRpc', 'initHeartbeat() throttleQueue.subscribe(%s)', e)
this.emit('heartbeat', e)
})
this.debounceQueue.subscribe(e => {
/**
* This block will be run when:
* the queue did not receive any message after a period.
*/
log.silly('PadchatRpc', 'initHeartbeat() debounceQueue.subscribe(%s)', e)
if (!this.socket) {
throw new Error('no socket')
}
// expect the server will response a 'pong' message
this.socket.ping(`#${HEART_BEAT_COUNTER++} from debounceQueue`)
})
}
private async destroy(reason = 'unknown reason'): Promise<void> {
log.verbose('PadchatRpc', 'destroy(%s)', reason)
this.emit('destroy', reason)
try {
this.stop()
} catch (e) {
// fall safe
}
this.removeAllListeners()
}
public stop(): void {
......@@ -174,7 +269,9 @@ export class PadchatRpc extends EventEmitter {
}
this.jsonRpc.removeAllListeners()
this.jsonRpc.end()
// TODO: use huan's version of JsonRpcPeer, to support end at here.
// this.jsonRpc.end()
this.socket.removeAllListeners()
this.socket.close()
......@@ -196,7 +293,7 @@ export class PadchatRpc extends EventEmitter {
// )
// XXX
console.log('server payload:', payload)
// console.log('server payload:', payload)
if (payload.type === PadchatPayloadType.Logout) {
// {"type":-1,"msg":"掉线了"}
......@@ -205,7 +302,7 @@ export class PadchatRpc extends EventEmitter {
payload.type,
JSON.stringify(payload),
)
this.emit('padchat-logout', payload.msg)
this.logoutThrottleQueue.next(payload.msg || 'onSocket(logout)')
return
}
......@@ -213,6 +310,11 @@ export class PadchatRpc extends EventEmitter {
/**
* Discard message that have neither msgId(Padchat API Call) nor data(Tencent Message)
*/
if (Object.keys(payload).length === 4) {
// {"apiName":"","data":"","msgId":"","userId":"padchat-token-zixia"}
// just return for this message
return
}
log.silly('PadchatRpc', 'onSocket() discard payload without `msgId` and `data` for: %s', JSON.stringify(payload))
return
}
......@@ -239,10 +341,7 @@ export class PadchatRpc extends EventEmitter {
// "userId": "test"
// }
// https://stackoverflow.com/a/24417399/1123955
const data = payload.data.replace(/\+/g, '%20')
const tencentPayloadList: PadchatMessagePayload[] = JSON.parse(decodeURIComponent(data))
const tencentPayloadList: PadchatMessagePayload[] = pfHelper.padchatDecode(payload.data)
if (!Array.isArray(tencentPayloadList)) {
throw new Error('not array')
......@@ -268,10 +367,7 @@ export class PadchatRpc extends EventEmitter {
let result: any
if (padchatPayload.data) {
// https://stackoverflow.com/a/24417399/1123955
const data = padchatPayload.data.replace(/\+/g, '%20')
result = JSON.parse(decodeURIComponent(data))
result = pfHelper.padchatDecode(padchatPayload.data)
} else {
log.silly('PadchatRpc', 'onServerMessagePadchat() discard empty payload.data for apiName: %s', padchatPayload.apiName)
result = {}
......@@ -491,7 +587,7 @@ export class PadchatRpc extends EventEmitter {
log.silly('PadchatRpc', 'WXGetContact(%s) result: %s', id, JSON.stringify(result))
if (!result.user_name) {
log.warn('PadchatRpc', 'WXGetContact cannot get user_name, id: %s', id)
log.warn('PadchatRpc', 'WXGetContact cannot get user_name, id: %s, "%s"', id, JSON.stringify(result))
}
return result
}
......@@ -519,10 +615,7 @@ export class PadchatRpc extends EventEmitter {
const result = await this.WXGetContact(id)
if (result.member) {
// https://stackoverflow.com/a/24417399/1123955
const data = result.member.replace(/\+/g, '%20')
result.member = JSON.parse(decodeURIComponent(data))
result.member = pfHelper.padchatDecode(result.member)
}
return result
......@@ -539,45 +632,20 @@ export class PadchatRpc extends EventEmitter {
}
log.silly('PadchatRpc', 'WXGetChatRoomMember() result: %s', JSON.stringify(result).substr(0, 500))
// console.log(result)
// 00:40:44 SILL PadchatRpc WXGetChatRoomMember() result: {"chatroom_id":0,"count":0,"member":"null\n","message":"","status":0,"user_name":""}
if (!result.user_name || !result.member) {
/**
* { chatroom_id: 0,
* count: 0,
* member: 'null\n',
* message: '',
* status: 0,
* user_name: '' }
*/
// console.error(result)
// throw new Error('WXGetChatRoomMember cannot get user_name or member!')
log.warn('PadchatRpc', 'WXGetChatRoomMember(%s) cannot get user_name or member!', roomId)
const emptyResult: PadchatRoomMemberListPayload = {
chatroom_id : 0,
count : 0,
member : [],
message : '',
status : 0,
user_name : '',
}
return emptyResult
}
try {
// tslint:disable-next-line:max-line-length
// change '[{"big_head":"http://wx.qlogo.cn/mmhead/ver_1/DpS0ZssJ5s8tEpSr9JuPTRxEUrCK0USrZcR3PjOMfUKDwpnZLxWXlD4Q38bJpcXBtwXWwevsul1lJqwsQzwItQ/0","chatroom_nick_name":"","invited_by":"wxid_7708837087612","nick_name":"李佳芮","small_head":"http://wx.qlogo.cn/mmhead/ver_1/DpS0ZssJ5s8tEpSr9JuPTRxEUrCK0USrZcR3PjOMfUKDwpnZLxWXlD4Q38bJpcXBtwXWwevsul1lJqwsQzwItQ/132","user_name":"qq512436430"},{"big_head":"http://wx.qlogo.cn/mmhead/ver_1/kcBj3gSibfFd2I9vQ8PBFyQ77cpPIfqkFlpTdkFZzBicMT6P567yj9IO6xG68WsibhqdPuG82tjXsveFATSDiaXRjw/0","chatroom_nick_name":"","invited_by":"wxid_7708837087612","nick_name":"梦君君","small_head":"http://wx.qlogo.cn/mmhead/ver_1/kcBj3gSibfFd2I9vQ8PBFyQ77cpPIfqkFlpTdkFZzBicMT6P567yj9IO6xG68WsibhqdPuG82tjXsveFATSDiaXRjw/132","user_name":"mengjunjun001"},{"big_head":"http://wx.qlogo.cn/mmhead/ver_1/3CsKibSktDV05eReoAicV0P8yfmuHSowfXAMvRuU7HEy8wMcQ2eibcaO1ccS95PskZchEWqZibeiap6Gpb9zqJB1WmNc6EdD6nzQiblSx7dC1eGtA/0","chatroom_nick_name":"","invited_by":"wxid_7708837087612","nick_name":"苏轼","small_head":"http://wx.qlogo.cn/mmhead/ver_1/3CsKibSktDV05eReoAicV0P8yfmuHSowfXAMvRuU7HEy8wMcQ2eibcaO1ccS95PskZchEWqZibeiap6Gpb9zqJB1WmNc6EdD6nzQiblSx7dC1eGtA/132","user_name":"wxid_zj2cahpwzgie12"},{"big_head":"http://wx.qlogo.cn/mmhead/ver_1/piaHuicak41b6ibmcEVxoWKnnhgGDG5EbaD0hibwkrRvKeDs3gs7XQrkym3Q5MlUeSKY8vw2FRVVstialggUxf2zic2O8CvaEsicSJcghf41nibA940/0","chatroom_nick_name":"","invited_by":"wxid_zj2cahpwzgie12","nick_name":"王宁","small_head":"http://wx.qlogo.cn/mmhead/ver_1/piaHuicak41b6ibmcEVxoWKnnhgGDG5EbaD0hibwkrRvKeDs3gs7XQrkym3Q5MlUeSKY8vw2FRVVstialggUxf2zic2O8CvaEsicSJcghf41nibA940/132","user_name":"wxid_7708837087612"}]'
// to Array (PadchatRoomRawMember[])
// https://stackoverflow.com/a/24417399/1123955
const data = result.member && result.member.data && result.member.data.replace(/\+/g, '%20') || null
const tryMemberList = JSON.parse(decodeURIComponent(data)) as PadchatRoomMemberPayload[]
const tryMemberList: null | PadchatRoomMemberPayload[] = pfHelper.padchatDecode(result.member)
if (Array.isArray(tryMemberList)) {
result.member = tryMemberList
} else {
log.warn('PadchatRpc', 'WXGetChatRoomMember(%s) member is not array: %s', roomId, JSON.stringify(result.member))
} else if (tryMemberList !== null) {
log.warn('PadchatRpc', 'WXGetChatRoomMember(%s) member is neither array nor null: %s', roomId, JSON.stringify(result.member))
// throw Error('faild to parse chatroom member!')
result.member = []
}
......
......@@ -90,7 +90,7 @@ export class PuppetPadchat extends Puppet {
private readonly cachePadchatMessagePayload : LRU.Cache<string, PadchatMessagePayload>
// private readonly cachePadchatRoomPayload : LRU.Cache<string, PadchatRoomRawPayload>
public readonly bridge: Bridge
public bridge?: Bridge
constructor(
public options: PuppetOptions,
......@@ -110,12 +110,6 @@ export class PuppetPadchat extends Puppet {
this.cachePadchatFriendRequestPayload = new LRU<string, PadchatMessagePayload>(lruOptions)
this.cachePadchatMessagePayload = new LRU<string, PadchatMessagePayload>(lruOptions)
// this.cachePadchatRoomPayload = new LRU<string, PadchatRoomRawPayload>(lruOptions)
this.bridge = new Bridge({
memory : this.options.memory,
token : padchatToken(),
endpoint : WECHATY_PUPPET_PADCHAT_ENDPOINT,
})
}
public toString() {
......@@ -129,42 +123,32 @@ export class PuppetPadchat extends Puppet {
public startWatchdog(): void {
log.verbose('PuppetPadchat', 'initWatchdogForPuppet()')
const puppet = this
if (!this.bridge) {
throw new Error('no bridge')
}
// clean the dog because this could be re-inited
this.watchdog.removeAllListeners()
puppet.on('watchdog', food => this.watchdog.feed(food))
/**
* Use puppet's heartbeat to feed dog
*/
this.bridge.on('heartbeat', (data: string) => {
log.silly('PuppetPadchat', 'startWatchdog() bridge.on(heartbeat)')
this.watchdog.feed({
data,
})
})
this.watchdog.on('feed', async food => {
log.silly('PuppetPadchat', 'initWatchdogForPuppet() dog.on(feed, food={type=%s, data=%s})', food.type, food.data)
// feed the dog, heartbeat the puppet.
// puppet.emit('heartbeat', food.data)
// const feedAfterTenSeconds = async () => {
// this.bridge.WXHeartBeat()
// .then(() => {
// this.emit('watchdog', {
// data: 'WXHeartBeat()',
// })
// })
// .catch(e => {
// log.warn('PuppetPadchat', 'initWatchdogForPuppet() feedAfterTenSeconds rejected: %s', e && e.message || '')
// })
// }
// setTimeout(feedAfterTenSeconds, 15 * 1000)
log.silly('PuppetPadchat', 'startWatchdog() watchdog.on(feed, food={type=%s, data=%s})', food.type, food.data)
})
this.watchdog.on('reset', async (food, timeout) => {
log.warn('PuppetPadchat', 'initWatchdogForPuppet() dog.on(reset) last food:%s, timeout:%s',
food.data, timeout)
// try {
// await this.stop()
// await this.start()
// } catch (e) {
// puppet.emit('error', e)
// }
log.warn('PuppetPadchat', 'startWatchdog() dog.on(reset) last food:%s, timeout:%s',
food.data,
timeout,
)
await this.restart('watchdog.on(reset)')
})
this.emit('watchdog', {
......@@ -189,7 +173,13 @@ export class PuppetPadchat extends Puppet {
*/
this.state.on('pending')
await this.startBridge()
const bridge = this.bridge = new Bridge({
memory : this.options.memory,
token : padchatToken(),
endpoint : WECHATY_PUPPET_PADCHAT_ENDPOINT,
})
await this.startBridge(bridge)
await this.startWatchdog()
this.state.on(true)
......@@ -197,27 +187,43 @@ export class PuppetPadchat extends Puppet {
}
protected async login(selfId: string): Promise<void> {
if (!this.bridge) {
throw new Error('no bridge')
}
await super.login(selfId)
this.bridge.syncContactsAndRooms()
}
public async startBridge(): Promise<void> {
public async startBridge(bridge: Bridge): Promise<void> {
log.verbose('PuppetPadchat', 'startBridge()')
if (this.state.off()) {
throw new Error('startBridge() state is off')
}
this.bridge.removeAllListeners()
// this.bridge.on('ding' , Event.onDing.bind(this))
// this.bridge.on('error' , e => this.emit('error', e))
// this.bridge.on('log' , Event.onLog.bind(this))
this.bridge.on('scan', (qrcode: string, status: number, data?: string) => this.emit('scan', qrcode, status, data))
this.bridge.on('login', (userId: string) => this.login(userId))
this.bridge.on('message', (rawPayload: PadchatMessagePayload) => this.onPadchatMessage(rawPayload))
this.bridge.on('logout', () => this.logout())
bridge.removeAllListeners()
// bridge.on('ding' , Event.onDing.bind(this))
// bridge.on('error' , e => this.emit('error', e))
// bridge.on('log' , Event.onLog.bind(this))
bridge.on('scan', (qrcode: string, status: number, data?: string) => this.emit('scan', qrcode, status, data))
bridge.on('login', (userId: string) => this.login(userId))
bridge.on('message', (rawPayload: PadchatMessagePayload) => this.onPadchatMessage(rawPayload))
bridge.on('logout', () => this.logout())
bridge.on('destroy', reason => {
log.warn('PuppetPadchat', 'startBridge() bridge.on(destroy) for %s. Restarting PuppetPadchat ... ', reason)
this.restart(reason)
})
await bridge.start()
}
protected async restart(reason: string): Promise<void> {
log.verbose('PuppetPadchat', 'restart(%s)', reason)
await this.stop()
await this.start()
await this.bridge.start()
}
protected onPadchatMessage(rawPayload: PadchatMessagePayload) {
......@@ -246,7 +252,11 @@ export class PuppetPadchat extends Puppet {
}
public async stop(): Promise<void> {
log.verbose('PuppetPadchat', 'quit()')
log.verbose('PuppetPadchat', 'stop()')
if (!this.bridge) {
throw new Error('no bridge')
}
if (this.state.off()) {
log.warn('PuppetPadchat', 'stop() is called on a OFF puppet. await ready(off) and return.')
......@@ -259,7 +269,7 @@ export class PuppetPadchat extends Puppet {
this.watchdog.sleep()
await this.logout()
setImmediate(() => this.bridge.removeAllListeners())
setImmediate(() => this.bridge && this.bridge.removeAllListeners())
await this.bridge.stop()
// await some tasks...
......@@ -271,7 +281,13 @@ export class PuppetPadchat extends Puppet {
log.verbose('PuppetPadchat', 'logout()')
if (!this.id) {
throw new Error('logout before login?')
log.warn('PuppetPadchat', 'logout() this.id not exist')
// throw new Error('logout before login?')
return
}
if (!this.bridge) {
throw new Error('no bridge')
}
this.emit('logout', this.id) // becore we will throw above by logonoff() when this.user===undefined
......@@ -300,6 +316,10 @@ export class PuppetPadchat extends Puppet {
return payload.alias || ''
}
if (!this.bridge) {
throw new Error('no bridge')
}
await this.bridge.WXSetUserRemark(contactId, alias || '')
return
......@@ -308,6 +328,10 @@ export class PuppetPadchat extends Puppet {
public async contactList(): Promise<string[]> {
log.verbose('PuppetPadchat', 'contactList()')
if (!this.bridge) {
throw new Error('no bridge')
}
const contactIdList = this.bridge.getContactIdList()
return contactIdList
......@@ -326,6 +350,9 @@ export class PuppetPadchat extends Puppet {
if (contactId !== this.selfId()) {
throw new Error('can not set avatar for others')
}
if (!this.bridge) {
throw new Error('no bridge')
}
await this.bridge.WXSetHeadImage(await file.toBase64())
return
}
......@@ -347,7 +374,9 @@ export class PuppetPadchat extends Puppet {
if (contactId !== this.selfId()) {
throw new Error('can not set avatar for others')
}
if (!this.bridge) {
throw new Error('no bridge')
}
const base64 = await this.bridge.WXGetUserQRCode(contactId, 0)
const qrcode = await pfHelper.imageBase64ToQrcode(base64)
return qrcode
......@@ -356,6 +385,9 @@ export class PuppetPadchat extends Puppet {
public async contactRawPayload(contactId: string): Promise<PadchatContactPayload> {
log.silly('PuppetPadchat', 'contactRawPayload(%s)', contactId)
if (!this.bridge) {
throw new Error('no bridge')
}
const rawPayload = await this.bridge.contactRawPayload(contactId)
return rawPayload
}
......@@ -430,6 +462,9 @@ export class PuppetPadchat extends Puppet {
if (!id) {
throw Error('no id')
}
if (!this.bridge) {
throw new Error('no bridge')
}
await this.bridge.WXSendMsg(id, text)
}
......@@ -444,6 +479,10 @@ export class PuppetPadchat extends Puppet {
throw new Error('no id!')
}
if (!this.bridge) {
throw new Error('no bridge')
}
const type = file.mimeType || path.extname(file.name)
switch (type) {
case '.slk':
......@@ -501,6 +540,10 @@ export class PuppetPadchat extends Puppet {
): Promise<PadchatRoomMemberPayload> {
log.silly('PuppetPadchat', 'roomMemberRawPayload(%s)', roomId)
if (!this.bridge) {
throw new Error('no bridge')
}
const rawPayload = await this.bridge.roomMemberRawPayload(roomId, contactId)
return rawPayload
}
......@@ -522,6 +565,10 @@ export class PuppetPadchat extends Puppet {
public async roomRawPayload(roomId: string): Promise<PadchatRoomPayload> {
log.verbose('PuppetPadchat', 'roomRawPayload(%s)', roomId)
if (!this.bridge) {
throw new Error('no bridge')
}
const rawPayload = await this.bridge.roomRawPayload(roomId)
return rawPayload
}
......@@ -540,6 +587,10 @@ export class PuppetPadchat extends Puppet {
public async roomMemberList(roomId: string): Promise<string[]> {
log.verbose('PuppetPadchat', 'roomMemberList(%s)', roomId)
if (!this.bridge) {
throw new Error('no bridge')
}
const memberIdList = await this.bridge.getRoomMemberIdList(roomId)
log.silly('PuppetPadchat', 'roomMemberList()=%d', memberIdList.length)
......@@ -549,6 +600,10 @@ export class PuppetPadchat extends Puppet {
public async roomList(): Promise<string[]> {
log.verbose('PuppetPadchat', 'roomList()')
if (!this.bridge) {
throw new Error('no bridge')
}
const roomIdList = await this.bridge.getRoomIdList()
log.silly('PuppetPadchat', 'roomList()=%d', roomIdList.length)
......@@ -561,6 +616,10 @@ export class PuppetPadchat extends Puppet {
): Promise<void> {
log.verbose('PuppetPadchat', 'roomDel(%s, %s)', roomId, contactId)
if (!this.bridge) {
throw new Error('no bridge')
}
// Should check whether user is in the room. WXDeleteChatRoomMember won't check if user in the room automatically
await this.bridge.WXDeleteChatRoomMember(roomId, contactId)
}
......@@ -593,9 +652,12 @@ export class PuppetPadchat extends Puppet {
): Promise<void> {
log.verbose('PuppetPadchat', 'roomAdd(%s, %s)', roomId, contactId)
if (!this.bridge) {
throw new Error('no bridge')
}
// XXX: did there need to calc the total number of the members in this room?
// if n <= 40 then add() else invite() ?
try {
log.verbose('PuppetPadchat', 'roomAdd(%s, %s) try to Add', roomId, contactId)
await this.bridge.WXAddChatRoomMember(roomId, contactId)
......@@ -622,6 +684,10 @@ export class PuppetPadchat extends Puppet {
return payload.topic
}
if (!this.bridge) {
throw new Error('no bridge')
}
await this.bridge.WXSetChatroomName(roomId, topic)
return
......@@ -633,6 +699,10 @@ export class PuppetPadchat extends Puppet {
): Promise<string> {
log.verbose('PuppetPadchat', 'roomCreate(%s, %s)', contactIdList, topic)
if (!this.bridge) {
throw new Error('no bridge')
}
// FIXME:
const roomId = this.bridge.WXCreateChatRoom(contactIdList)
console.log('roomCreate returl:', roomId)
......@@ -642,6 +712,11 @@ export class PuppetPadchat extends Puppet {
public async roomQuit(roomId: string): Promise<void> {
log.verbose('PuppetPadchat', 'roomQuit(%s)', roomId)
if (!this.bridge) {
throw new Error('no bridge')
}
await this.bridge.WXQuitChatRoom(roomId)
}
......@@ -650,6 +725,11 @@ export class PuppetPadchat extends Puppet {
public async roomAnnounce(roomId: string, text?: string): Promise<void | string> {
log.verbose('PuppetPadchat', 'roomAnnounce(%s, %s)', roomId, text ? text : '')
if (!this.bridge) {
throw new Error('no bridge')
}
if (text) {
await this.bridge.WXSetChatroomAnnouncement(roomId, text)
} else {
......@@ -683,6 +763,10 @@ export class PuppetPadchat extends Puppet {
// throw new Error('stranger neither v1 nor v2!')
// }
if (!this.bridge) {
throw new Error('no bridge')
}
// Issue #1252 : what's wrong here?
await this.bridge.WXAddUser(
......@@ -709,6 +793,10 @@ export class PuppetPadchat extends Puppet {
throw new Error('no stranger')
}
if (!this.bridge) {
throw new Error('no bridge')
}
await this.bridge.WXAcceptUser(
payload.stranger,
payload.ticket,
......
......@@ -179,6 +179,16 @@ test('friendRequestRawPayloadParser()', async t => {
t.deepEqual(friendRequestPayload, EXPECTED_FRIEND_REQUEST_PAYLOAD, 'should parse friendRequestPayload right')
})
test('padchatDecode()', async t => {
const JSON_TEXT = '%7B%22big_head%22%3A%22http%3A%2F%2Fwx.qlogo.cn%2Fmmhead%2FP3UGRtJrgyEMkmOExtdq1xpGcic2z1b5wZuicFibfHNPnYttF9n9ZzE2Q%2F0%22%2C%22bit_mask%22%3A4294967295%2C%22bit_value%22%3A2051%2C%22chatroom_id%22%3A0%2C%22chatroom_owner%22%3A%22%22%2C%22city%22%3A%22San+Francisco%22%2C%22continue%22%3A1%2C%22country%22%3A%22US%22%2C%22id%22%3A0%2C%22img_flag%22%3A1%2C%22intro%22%3A%22%22%2C%22label%22%3A%22%22%2C%22level%22%3A7%2C%22max_member_count%22%3A0%2C%22member_count%22%3A0%2C%22msg_type%22%3A2%2C%22nick_name%22%3A%22Huan+LI%2B%2B%22%2C%22provincia%22%3A%22California%22%2C%22py_initial%22%3A%22HUANLI%22%2C%22quan_pin%22%3A%22HuanLI%22%2C%22remark%22%3A%22%22%2C%22remark_py_initial%22%3A%22%22%2C%22remark_quan_pin%22%3A%22%22%2C%22sex%22%3A1%2C%22signature%22%3A%22angel+invester%2C+serial+entrepreneur+with+tech+background.%22%2C%22small_head%22%3A%22http%3A%2F%2Fwx.qlogo.cn%2Fmmhead%2FP3UGRtJrgyEMkmOExtdq1xpGcic2z1b5wZuicFibfHNPnYttF9n9ZzE2Q%2F132%22%2C%22source%22%3A14%2C%22status%22%3A1%2C%22stranger%22%3A%22v1_7f8c54ac5a1b1bcec9a7ccfae9b0a9564373a1559d2e545d2d2b5a3708e61928b2fc43c009b7512a75d53b312422d6e6%40stranger%22%2C%22uin%22%3A1211516682%2C%22user_name%22%3A%22wxid_5zj4i5htp9ih22%22%7D'
const EXPECTED_OBJ = { big_head: 'http://wx.qlogo.cn/mmhead/P3UGRtJrgyEMkmOExtdq1xpGcic2z1b5wZuicFibfHNPnYttF9n9ZzE2Q/0', bit_mask: 4294967295, bit_value: 2051, chatroom_id: 0, chatroom_owner: '', city: 'San Francisco', continue: 1, country: 'US', id: 0, img_flag: 1, intro: '', label: '', level: 7, max_member_count: 0, member_count: 0, msg_type: 2, nick_name: 'Huan LI++', provincia: 'California', py_initial: 'HUANLI', quan_pin: 'HuanLI', remark: '', remark_py_initial: '', remark_quan_pin: '', sex: 1, signature: 'angel invester, serial entrepreneur with tech background.', small_head: 'http://wx.qlogo.cn/mmhead/P3UGRtJrgyEMkmOExtdq1xpGcic2z1b5wZuicFibfHNPnYttF9n9ZzE2Q/132', source: 14, status: 1, stranger: 'v1_7f8c54ac5a1b1bcec9a7ccfae9b0a9564373a1559d2e545d2d2b5a3708e61928b2fc43c009b7512a75d53b312422d6e6@stranger', uin: 1211516682, user_name: 'wxid_5zj4i5htp9ih22' }
const result = pfHelper.padchatDecode(JSON_TEXT)
t.deepEqual(result, EXPECTED_OBJ, 'should parse json text with "+" right')
})
// TODO
// test('roomRawPayloadParser', async t => {
......
......@@ -2,6 +2,9 @@
*
* Pure Function Helpers
*
* Huan LI <zixia@zixia.net> https://github.com/zixia
* License: Apache 2.0
*
* See: What's Pure Function Programming
* [Functional Programming Concepts: Pure Functions](https://hackernoon.com/functional-programming-concepts-pure-functions-cafa2983f757)
* [What Are Pure Functions And Why Use Them?](https://medium.com/@jamesjefferyuk/javascript-what-are-pure-functions-4d4d5392d49c)
......@@ -181,7 +184,7 @@ export class PadchatPureFunctionHelper {
const payloadBase = {
id : rawPayload.msg_id,
timestamp : Date.now(),
timestamp : rawPayload.timestamp, // Padchat message timestamp is seconds
fromId : rawPayload.from_user,
text : rawPayload.content,
// toId : rawPayload.to_user,
......@@ -360,6 +363,21 @@ export class PadchatPureFunctionHelper {
throw new Error('no qrcode in image: ' + e.message)
}
}
// https://stackoverflow.com/a/24417399/1123955
public static padchatDecode<T = Object>(encodedText: string): T {
if (!encodedText) {
throw new Error('no encodedText')
}
let decodedText: string
decodedText = encodedText.replace(/\+/g, '%20')
decodedText = decodeURIComponent(decodedText)
const decodedObject: T = JSON.parse(decodedText)
return decodedObject
}
}
export default PadchatPureFunctionHelper
......@@ -443,14 +443,14 @@ export abstract class Puppet extends EventEmitter implements Sayable {
}
public contactPayloadCache(contactId: string): undefined | ContactPayload {
log.silly('Puppet', 'contactPayloadCache(id=%s) @ %s', contactId, this)
// log.silly('Puppet', 'contactPayloadCache(id=%s) @ %s', contactId, this)
if (!contactId) {
throw new Error('no id')
}
const cachedPayload = this.cacheContactPayload.get(contactId)
if (cachedPayload) {
log.silly('Puppet', 'contactPayload(%s) cache HIT', contactId)
// log.silly('Puppet', 'contactPayload(%s) cache HIT', contactId)
} else {
log.silly('Puppet', 'contactPayload(%s) cache MISS', contactId)
}
......@@ -462,7 +462,7 @@ export abstract class Puppet extends EventEmitter implements Sayable {
contactId: string,
noCache = false,
): Promise<ContactPayload> {
log.silly('Puppet', 'contactPayload(id=%s, noCache=%s) @ %s', contactId, noCache, this)
// log.silly('Puppet', 'contactPayload(id=%s, noCache=%s) @ %s', contactId, noCache, this)
if (!contactId) {
throw new Error('no id')
......
......@@ -16,7 +16,7 @@ export interface MessagePayloadBase {
filename? : string,
fromId : string,
text? : string,
timestamp : number, // milliseconds
timestamp : number, // unit timestamp, in seconds
}
export interface MessagePayloadRoom {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册