puppet-padchat.ts 29.6 KB
Newer Older
ruiruibupt's avatar
init  
ruiruibupt 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
/**
 *   Wechaty - https://github.com/chatie/wechaty
 *
 *   @copyright 2016-2018 Huan LI <zixia@zixia.net>
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 */

20
import path  from 'path'
21 22
// import fs    from 'fs'
// import cuid from 'cuid'
ruiruibupt's avatar
init  
ruiruibupt 已提交
23

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
24 25
import LRU      from 'lru-cache'
import flatten  from 'array-flatten'
26

ruiruibupt's avatar
init  
ruiruibupt 已提交
27 28
import {
  FileBox,
29
}               from 'file-box'
ruiruibupt's avatar
init  
ruiruibupt 已提交
30 31

import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
32 33
  ContactPayload,

34 35
  MessagePayload,
  MessageType,
ruiruibupt's avatar
init  
ruiruibupt 已提交
36 37

  RoomPayload,
38
  RoomMemberPayload,
ruiruibupt's avatar
init  
ruiruibupt 已提交
39 40 41

  Puppet,
  PuppetOptions,
42

ruiruibupt's avatar
init  
ruiruibupt 已提交
43
  Receiver,
44

45 46
  FriendshipPayload,
  FriendshipPayloadReceive,
47 48

  WATCHDOG_TIMEOUT,
49
}                                 from '../puppet/'
ruiruibupt's avatar
init  
ruiruibupt 已提交
50

51
import {
52 53
  contactRawPayloadParser,
  fileBoxToQrcode,
54
  // friendRequestEventMessageParser,
55
  friendshipRawPayloadParser,
56
  messageRawPayloadParser,
57 58 59

  roomRawPayloadParser,

60 61 62
  roomJoinEventMessageParser,
  roomLeaveEventMessageParser,
  roomTopicEventMessageParser,
63 64 65 66

  friendshipConfirmEventMessageParser,
  friendshipReceiveEventMessageParser,
  friendshipVerifyEventMessageParser,
67
}                                         from './pure-function-helpers'
68

ruiruibupt's avatar
init  
ruiruibupt 已提交
69 70
import {
  log,
71 72
  qrCodeForChatie,
}                   from '../config'
ruiruibupt's avatar
init  
ruiruibupt 已提交
73

74
import {
75
  padchatToken,
76 77 78
  WECHATY_PUPPET_PADCHAT_ENDPOINT,
}                                   from './config'

ruiruibupt's avatar
init  
ruiruibupt 已提交
79
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
80
  PadchatManager,
81
  // resolverDict,
ruiruibupt's avatar
ruiruibupt 已提交
82
  // AutoDataType,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
83
}                       from './padchat-manager'
ruiruibupt's avatar
init  
ruiruibupt 已提交
84 85

import {
86
  // PadchatPayload,
87
  PadchatContactPayload,
88
  PadchatMessagePayload,
89
  PadchatRoomPayload,
90 91
  // PadchatRoomMemberListPayload,
  PadchatRoomMemberPayload,
92
  PadchatMessageType,
93

94
  // PadchatMessageType,
95 96 97 98
  // PadchatContinue,
  // PadchatMsgType,
  // PadchatStatus,
  // PadchatPayloadType,
99
  // PadchatRoomRawMember,
100
}                           from './padchat-schemas'
E
Egg 已提交
101 102 103 104
import {
  WXSearchContactType,
  WXSearchContactTypeStatus,
}                           from './padchat-rpc.type'
ruiruibupt's avatar
init  
ruiruibupt 已提交
105 106 107 108 109

export type PuppetFoodType = 'scan' | 'ding'
export type ScanFoodType   = 'scan' | 'login' | 'logout'

export class PuppetPadchat extends Puppet {
110

111 112 113
  // in seconds, 4 minute for padchat
  protected [WATCHDOG_TIMEOUT] = 4 * 60

114
  // private readonly cachePadchatContactPayload       : LRU.Cache<string, PadchatContactRawPayload>
115 116
  private readonly cachePadchatFriendshipPayload : LRU.Cache<string, PadchatMessagePayload>
  private readonly cachePadchatMessagePayload    : LRU.Cache<string, PadchatMessagePayload>
117
  // private readonly cachePadchatRoomPayload          : LRU.Cache<string, PadchatRoomRawPayload>
118

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
119
  public bridge?:  PadchatManager
ruiruibupt's avatar
init  
ruiruibupt 已提交
120 121 122 123 124 125

  constructor(
    public options: PuppetOptions,
  ) {
    super(options)

126 127 128
    const lruOptions: LRU.Options = {
      max: 1000,
      // length: function (n) { return n * 2},
129
      dispose: function (key: string, val: any) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
130
        log.silly('PuppetPadchat', 'constructor() lruOptions.dispose(%s, %s)', key, JSON.stringify(val))
131 132 133 134
      },
      maxAge: 1000 * 60 * 60,
    }

135
    // this.cachePadchatContactPayload       = new LRU<string, PadchatContactRawPayload>(lruOptions)
136
    this.cachePadchatFriendshipPayload = new LRU<string, PadchatMessagePayload>(lruOptions)
137
    this.cachePadchatMessagePayload       = new LRU<string, PadchatMessagePayload>(lruOptions)
138
    // this.cachePadchatRoomPayload          = new LRU<string, PadchatRoomRawPayload>(lruOptions)
ruiruibupt's avatar
init  
ruiruibupt 已提交
139 140 141
  }

  public toString() {
142
    return `PuppetPadchat<${this.options.memory.name}>`
ruiruibupt's avatar
init  
ruiruibupt 已提交
143 144 145 146 147 148
  }

  public ding(data?: any): Promise<string> {
    return data
  }

ruiruibupt's avatar
ruiruibupt 已提交
149
  public startWatchdog(): void {
ruiruibupt's avatar
ruiruibupt 已提交
150
    log.verbose('PuppetPadchat', 'initWatchdogForPuppet()')
ruiruibupt's avatar
init  
ruiruibupt 已提交
151

152 153 154
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
155

ruiruibupt's avatar
ruiruibupt 已提交
156 157 158
    // clean the dog because this could be re-inited
    this.watchdog.removeAllListeners()

159
    /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
     * Use bridge's heartbeat to feed dog
161 162 163 164 165 166 167
     */
    this.bridge.on('heartbeat', (data: string) => {
      log.silly('PuppetPadchat', 'startWatchdog() bridge.on(heartbeat)')
      this.watchdog.feed({
        data,
      })
    })
ruiruibupt's avatar
ruiruibupt 已提交
168
    this.watchdog.on('feed', async food => {
169
      log.silly('PuppetPadchat', 'startWatchdog() watchdog.on(feed, food={type=%s, data=%s})', food.type, food.data)
ruiruibupt's avatar
ruiruibupt 已提交
170 171
    })

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
172
    this.watchdog.on('reset', async (food, timeout) => {
173 174 175 176 177
      log.warn('PuppetPadchat', 'startWatchdog() dog.on(reset) last food:%s, timeout:%s',
                                food.data,
                                timeout,
              )
      await this.restart('watchdog.on(reset)')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
178
    })
ruiruibupt's avatar
ruiruibupt 已提交
179 180 181 182 183

    this.emit('watchdog', {
      data: 'inited',
    })

ruiruibupt's avatar
ruiruibupt 已提交
184
  }
ruiruibupt's avatar
init  
ruiruibupt 已提交
185 186

  public async start(): Promise<void> {
ruiruibupt's avatar
ruiruibupt 已提交
187
    log.verbose('PuppetPadchat', `start() with ${this.options.memory.name}`)
ruiruibupt's avatar
init  
ruiruibupt 已提交
188

189 190 191 192 193 194
    if (this.state.on()) {
      log.warn('PuppetPadchat', 'start() already on(pending)?')
      await this.state.ready('on')
      return
    }

ruiruibupt's avatar
ruiruibupt 已提交
195 196 197 198 199 200 201
    /**
     * state has two main state: ON / OFF
     * ON (pending)
     * OFF (pending)
     */
    this.state.on('pending')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
202
    const bridge = this.bridge = new PadchatManager({
203 204 205 206 207 208
      memory   : this.options.memory,
      token    : padchatToken(),
      endpoint : WECHATY_PUPPET_PADCHAT_ENDPOINT,
    })

    await this.startBridge(bridge)
ruiruibupt's avatar
ruiruibupt 已提交
209
    await this.startWatchdog()
ruiruibupt's avatar
ruiruibupt 已提交
210

ruiruibupt's avatar
ruiruibupt 已提交
211
    this.state.on(true)
212
    this.emit('start')
213
  }
ruiruibupt's avatar
init  
ruiruibupt 已提交
214

215
  protected async login(selfId: string): Promise<void> {
216 217 218
    if (!this.bridge) {
      throw new Error('no bridge')
    }
219 220
    await super.login(selfId)
    this.bridge.syncContactsAndRooms()
ruiruibupt's avatar
init  
ruiruibupt 已提交
221 222
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
223
  public async startBridge(bridge: PadchatManager): Promise<void> {
ruiruibupt's avatar
ruiruibupt 已提交
224
    log.verbose('PuppetPadchat', 'startBridge()')
ruiruibupt's avatar
ruiruibupt 已提交
225 226

    if (this.state.off()) {
227
      throw new Error('startBridge() state is off')
ruiruibupt's avatar
ruiruibupt 已提交
228 229
    }

230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
    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()
ruiruibupt's avatar
init  
ruiruibupt 已提交
252 253 254

  }

Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
255
  protected async onPadchatMessage(rawPayload: PadchatMessagePayload): Promise<void> {
256 257 258 259 260
    log.verbose('PuppetPadchat', 'onPadchatMessage({id=%s, type=%s(%s)})',
                                rawPayload.msg_id,
                                PadchatMessageType[rawPayload.sub_type],
                                rawPayload.msg_type,
              )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
261 262
    console.log('rawPayload:', rawPayload)

263 264
    switch (rawPayload.sub_type) {
      case PadchatMessageType.VerifyMsg:
265
        this.cachePadchatFriendshipPayload.set(
266 267 268
          rawPayload.msg_id,
          rawPayload,
        )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
269
        this.emit('friendship', rawPayload.msg_id)
270 271
        break

272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
      case PadchatMessageType.Recalled:
        /**
         * When someone joined the room invited by Bot,
         * the bot will receive a `recall-able` message for room event
         *
         * { content: '12740017638@chatroom:\n<sysmsg type="delchatroommember">\n\t<delchatroommember>\n\t\t<plain>
         *            <![CDATA[You invited 卓桓、Zhuohuan, 太阁_传话助手, 桔小秘 to the group chat.   ]]></plain>...,
         *  continue: 1,
         *  description: '',
         *  from_user: '12740017638@chatroom',
         *  msg_id: '232220931339852872',
         *  msg_source: '',
         *  msg_type: 5,
         *  status: 1,
         *  sub_type: 10002,
         *  timestamp: 1528831349,
         *  to_user: 'wxid_zj2cahpwzgie12',
         *  uin: 324216852 }
         */
        await Promise.all([
          this.onPadchatMessageRoomEvent(rawPayload),
        ])
        break
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
295
      case PadchatMessageType.Sys:
296 297 298 299
        await Promise.all([
          this.onPadchatMessageFriendshipEvent(rawPayload),
          this.onPadchatMessageRoomEvent(rawPayload),
        ])
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300 301
        break

302 303 304 305 306 307 308 309
      case PadchatMessageType.App:
      case PadchatMessageType.Emoticon:
      case PadchatMessageType.Image:
      case PadchatMessageType.MicroVideo:
      case PadchatMessageType.Video:
      case PadchatMessageType.Voice:
         // TODO: the above types are filel type

310 311 312 313 314 315 316 317 318 319
      default:
        this.cachePadchatMessagePayload.set(
          rawPayload.msg_id,
          rawPayload,
        )
        this.emit('message', rawPayload.msg_id)
        break
    }
  }

320 321
  protected async onPadchatMessageRoomEvent(rawPayload: PadchatMessagePayload): Promise<void> {
    log.verbose('PuppetPadchat', 'onPadchatMessageRoomEvent({id=%s})')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
    /**
     * 1. Look for room join event
     */
    const roomJoin = roomJoinEventMessageParser(rawPayload)
    if (roomJoin) {
      const inviteeNameList = roomJoin.inviteeNameList
      const inviterName     = roomJoin.inviterName
      const roomId          = roomJoin.roomId

      const inviteeIdList = flatten<string>(
        await Promise.all(
          inviteeNameList.map(
            inviteeName => this.roomMemberSearch(roomId, inviteeName),
          ),
        ),
      )
      const inviterIdList = await this.roomMemberSearch(roomId, inviterName)
      if (inviterIdList.length < 1) {
        throw new Error('no inviterId found')
      } else if (inviterIdList.length > 1) {
342
        log.warn('PuppetPadchat', 'onPadchatMessageRoomEvent() case PadchatMesssageSys: inviterId found more than 1, use the first one.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395
      }
      const inviterId = inviterIdList[0]

      this.emit('room-join',   roomId, inviteeIdList,  inviterId)
    }
    /**
     * 2. Look for room leave event
     */
    const roomLeave = roomLeaveEventMessageParser(rawPayload)
    if (roomLeave) {
      const leaverNameList = roomLeave.leaverNameList
      const removerName    = roomLeave.removerName
      const roomId         = roomLeave.roomId

      const leaverIdList = flatten<string>(
        await Promise.all(
          leaverNameList.map(
            leaverName => this.roomMemberSearch(roomId, leaverName),
          ),
        ),
      )
      const removerIdList = await this.roomMemberSearch(roomId, removerName)
      if (removerIdList.length < 1) {
        throw new Error('no removerId found')
      } else if (removerIdList.length > 1) {
        log.warn('PuppetPadchat', 'onPadchatMessage() case PadchatMesssageSys: removerId found more than 1, use the first one.')
      }
      const removerId = removerIdList[0]

      this.emit('room-leave',  roomId, leaverIdList, removerId)
    }
    /**
     * 3. Look for room topic event
     */
    const roomTopic = roomTopicEventMessageParser(rawPayload)
    if (roomTopic) {
      const changerName = roomTopic.changerName
      const newTopic    = roomTopic.topic
      const roomId      = roomTopic.roomId

      const roomPayload = await this.roomPayload(roomId)
      const oldTopic = roomPayload.topic

      const changerIdList = await this.roomMemberSearch(roomId, changerName)
      if (changerIdList.length < 1) {
        throw new Error('no changerId found')
      } else if (changerIdList.length > 1) {
        log.warn('PuppetPadchat', 'onPadchatMessage() case PadchatMesssageSys: changerId found more than 1, use the first one.')
      }
      const changerId = changerIdList[0]

      this.emit('room-topic',  roomId, newTopic, oldTopic, changerId)
    }
396 397 398 399
  }

  protected async onPadchatMessageFriendshipEvent(rawPayload: PadchatMessagePayload): Promise<void> {
    log.verbose('PuppetPadchat', 'onPadchatMessageFriendshipEvent({id=%s})')
400
    /**
401
     * 1. Look for friendship confirm event
402
     */
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
    const friendshipConfirmContactId = friendshipConfirmEventMessageParser(rawPayload)
    /**
     * 2. Look for friendship receive event
     */
    const friendshipReceiveContactId = friendshipReceiveEventMessageParser(rawPayload)
    /**
     * 3. Look for friendship verify event
     */
    const friendshipVerifyContactId = friendshipVerifyEventMessageParser(rawPayload)

    if (   friendshipConfirmContactId
        || friendshipReceiveContactId
        || friendshipVerifyContactId
    ) {
      this.emit('friendship', rawPayload.msg_id)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
419 420
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
421
  public async stop(): Promise<void> {
422 423 424 425 426
    log.verbose('PuppetPadchat', 'stop()')

    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
427 428

    if (this.state.off()) {
429
      log.warn('PuppetPadchat', 'stop() is called on a OFF puppet. await ready(off) and return.')
ruiruibupt's avatar
init  
ruiruibupt 已提交
430 431 432 433 434
      await this.state.ready('off')
      return
    }

    this.state.off('pending')
ruiruibupt's avatar
ruiruibupt 已提交
435 436 437

    this.watchdog.sleep()
    await this.logout()
ruiruibupt's avatar
ruiruibupt 已提交
438

439
    setImmediate(() => this.bridge && this.bridge.removeAllListeners())
ruiruibupt's avatar
ruiruibupt 已提交
440
    await this.bridge.stop()
ruiruibupt's avatar
ruiruibupt 已提交
441

ruiruibupt's avatar
init  
ruiruibupt 已提交
442 443
    // await some tasks...
    this.state.off(true)
444
    this.emit('stop')
ruiruibupt's avatar
init  
ruiruibupt 已提交
445 446
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
447
  public async logout(): Promise<void> {
ruiruibupt's avatar
init  
ruiruibupt 已提交
448 449
    log.verbose('PuppetPadchat', 'logout()')

450
    if (!this.id) {
451 452 453 454 455 456 457
      log.warn('PuppetPadchat', 'logout() this.id not exist')
      // throw new Error('logout before login?')
      return
    }

    if (!this.bridge) {
      throw new Error('no bridge')
ruiruibupt's avatar
init  
ruiruibupt 已提交
458 459
    }

460 461
    this.emit('logout', this.id) // becore we will throw above by logonoff() when this.user===undefined
    this.id = undefined
ruiruibupt's avatar
init  
ruiruibupt 已提交
462

463 464 465 466
    // if (!passive) {
    //   await this.bridge.WXLogout()
    // }

467
    await this.bridge.logout()
ruiruibupt's avatar
init  
ruiruibupt 已提交
468 469 470 471 472 473 474 475 476 477 478 479 480 481
  }

  /**
   *
   * Contact
   *
   */
  public contactAlias(contactId: string)                      : Promise<string>
  public contactAlias(contactId: string, alias: string | null): Promise<void>

  public async contactAlias(contactId: string, alias?: string|null): Promise<void | string> {
    log.verbose('PuppetPadchat', 'contactAlias(%s, %s)', contactId, alias)

    if (typeof alias === 'undefined') {
ruiruibupt's avatar
ruiruibupt 已提交
482
      const payload = await this.contactPayload(contactId)
ruiruibupt's avatar
ruiruibupt 已提交
483
      return payload.alias || ''
ruiruibupt's avatar
init  
ruiruibupt 已提交
484
    }
ruiruibupt's avatar
ruiruibupt 已提交
485

486 487 488 489
    if (!this.bridge) {
      throw new Error('no bridge')
    }

ruiruibupt's avatar
ruiruibupt 已提交
490 491
    await this.bridge.WXSetUserRemark(contactId, alias || '')

ruiruibupt's avatar
init  
ruiruibupt 已提交
492 493 494
    return
  }

495 496
  public async contactList(): Promise<string[]> {
    log.verbose('PuppetPadchat', 'contactList()')
ruiruibupt's avatar
init  
ruiruibupt 已提交
497

498 499 500 501
    if (!this.bridge) {
      throw new Error('no bridge')
    }

ruiruibupt's avatar
ruiruibupt 已提交
502
    const contactIdList = this.bridge.getContactIdList()
ruiruibupt's avatar
ruiruibupt 已提交
503 504 505 506

    return contactIdList
  }

507 508
  public async contactAvatar(contactId: string)                : Promise<FileBox>
  public async contactAvatar(contactId: string, file: FileBox) : Promise<void>
ruiruibupt's avatar
init  
ruiruibupt 已提交
509

510 511 512 513 514 515 516 517 518 519
  public async contactAvatar(contactId: string, file?: FileBox): Promise<void | FileBox> {
    log.verbose('PuppetPadchat', 'contactAvatar(%s, %s)', contactId, file ? file.name : '')

    /**
     * 1. set avatar for user self
     */
    if (file) {
      if (contactId !== this.selfId()) {
        throw new Error('can not set avatar for others')
      }
520 521 522
      if (!this.bridge) {
        throw new Error('no bridge')
      }
523 524 525 526 527 528 529
      await this.bridge.WXSetHeadImage(await file.toBase64())
      return
    }

    /**
     * 2. get avatar
     */
ruiruibupt's avatar
ruiruibupt 已提交
530 531 532 533 534 535
    const payload = await this.contactPayload(contactId)

    if (!payload.avatar) {
      throw new Error('no avatar')
    }

536 537 538 539
    const fileBox = FileBox.fromUrl(payload.avatar)
    return fileBox
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
540
  public async contactQrcode(contactId: string): Promise<string> {
541 542 543
    if (contactId !== this.selfId()) {
      throw new Error('can not set avatar for others')
    }
544 545 546
    if (!this.bridge) {
      throw new Error('no bridge')
    }
547
    const base64 = await this.bridge.WXGetUserQRCode(contactId, 0)
548
    const qrcode = await fileBoxToQrcode(base64)
549
    return qrcode
ruiruibupt's avatar
init  
ruiruibupt 已提交
550 551
  }

552
  public async contactRawPayload(contactId: string): Promise<PadchatContactPayload> {
553
    log.silly('PuppetPadchat', 'contactRawPayload(%s)', contactId)
ruiruibupt's avatar
ruiruibupt 已提交
554

555 556 557
    if (!this.bridge) {
      throw new Error('no bridge')
    }
558
    const rawPayload = await this.bridge.contactRawPayload(contactId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
559 560 561
    return rawPayload
  }

562
  public async contactRawPayloadParser(rawPayload: PadchatContactPayload): Promise<ContactPayload> {
563
    log.silly('PuppetPadchat', 'contactRawPayloadParser({user_name="%s"})', rawPayload.user_name)
ruiruibupt's avatar
init  
ruiruibupt 已提交
564

565
    const payload: ContactPayload = contactRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
566 567 568 569 570 571 572 573
    return payload
  }

  /**
   *
   * Message
   *
   */
ruiruibupt's avatar
ruiruibupt 已提交
574

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
575 576
  public async messageFile(messageId: string): Promise<FileBox> {
    log.warn('PuppetPadchat', 'messageFile(%s) not implemented yet', messageId)
577

ruiruibupt's avatar
ruiruibupt 已提交
578 579
    // const rawPayload = await this.messageRawPayload(id)

ruiruibupt's avatar
ruiruibupt 已提交
580 581
    // TODO

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620
    if (!this.bridge) {
      throw new Error('no bridge')
    }

    const rawPayload = await this.messageRawPayload(messageId)
    const payload    = await this.messagePayload(messageId)

    const rawText = JSON.stringify(rawPayload)

    let result

    switch (payload.type) {
      case MessageType.Attachment:
        break

      case MessageType.Audio:
        result = await this.bridge.WXGetMsgVoice(rawText)
        console.log(result)
        return FileBox.fromBase64(result.data.image, 'test.slk')

      case MessageType.Emoticon:
        result = await this.bridge.WXGetMsgImage(rawText)
        console.log(result)
        return FileBox.fromBase64(result.data.image, 'test.gif')

      case MessageType.Image:
        result = await this.bridge.WXGetMsgImage(rawText)
        console.log(result)
        return FileBox.fromBase64(result.data.image, 'test.jpg')

      case MessageType.Video:
        result = await this.bridge.WXGetMsgVideo(rawText)
        console.log(result)
        return FileBox.fromBase64(result.data.image, 'test.mp4')

      default:
        throw new Error('unsupport type: ' + PadchatMessageType[rawPayload.sub_type] + ':' + rawPayload.sub_type)
    }

ruiruibupt's avatar
ruiruibupt 已提交
621
    const base64 = 'cRH9qeL3XyVnaXJkppBuH20tf5JlcG9uFX1lL2IvdHRRRS9kMMQxOPLKNYIzQQ=='
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
622
    const filename = 'test-' + messageId + '.txt'
ruiruibupt's avatar
ruiruibupt 已提交
623

624
    const file = FileBox.fromBase64(
ruiruibupt's avatar
ruiruibupt 已提交
625 626 627 628 629 630 631
      base64,
      filename,
    )

    return file
  }

632
  public async messageRawPayload(id: string): Promise<PadchatMessagePayload> {
633 634 635 636 637
    // throw Error('should not call messageRawPayload: ' + id)

    /**
     * Issue #1249
     */
ruiruibupt's avatar
ruiruibupt 已提交
638 639 640 641 642

    // this.cachePadchatMessageRawPayload.set(id, {
    //   id: 'xxx',
    //   data: 'xxx',
    // } as any)
643

644
    const rawPayload = this.cachePadchatMessagePayload.get(id)
645 646 647 648 649 650

    if (!rawPayload) {
      throw new Error('no rawPayload')
    }

    return rawPayload
ruiruibupt's avatar
init  
ruiruibupt 已提交
651 652
  }

653
  public async messageRawPayloadParser(rawPayload: PadchatMessagePayload): Promise<MessagePayload> {
654
    log.verbose('PuppetPadChat', 'messageRawPayloadParser({msg_id="%s"})', rawPayload.msg_id)
655

656
    const payload: MessagePayload = messageRawPayloadParser(rawPayload)
ruiruibupt's avatar
ruiruibupt 已提交
657

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
658
    log.silly('PuppetPadchat', 'messagePayload(%s)', JSON.stringify(payload))
ruiruibupt's avatar
init  
ruiruibupt 已提交
659 660 661 662 663 664 665
    return payload
  }

  public async messageSendText(
    receiver : Receiver,
    text     : string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
666
    log.verbose('PuppetPadchat', 'messageSend(%s, %s)', JSON.stringify(receiver), text)
ruiruibupt's avatar
ruiruibupt 已提交
667 668
    const id = receiver.contactId || receiver.roomId
    if (!id) {
669
      throw Error('no id')
ruiruibupt's avatar
ruiruibupt 已提交
670
    }
671 672 673
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
ruiruibupt 已提交
674
    await this.bridge.WXSendMsg(id, text)
ruiruibupt's avatar
init  
ruiruibupt 已提交
675 676 677 678 679 680
  }

  public async messageSendFile(
    receiver : Receiver,
    file     : FileBox,
  ): Promise<void> {
681
    log.verbose('PuppetPadchat', 'messageSend("%s", %s)', JSON.stringify(receiver), file)
ruiruibupt's avatar
ruiruibupt 已提交
682 683 684 685 686 687

    const id = receiver.contactId || receiver.roomId
    if (!id) {
      throw new Error('no id!')
    }

688 689 690 691
    if (!this.bridge) {
      throw new Error('no bridge')
    }

692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
    const type = file.mimeType || path.extname(file.name)
    switch (type) {
      case '.slk':
        // 发送语音消息(微信silk格式语音)
        await this.bridge.WXSendVoice(
          id,
          await file.toBase64(),
          60,
        )
        break

      default:
        await this.bridge.WXSendImage(
          id,
          await file.toBase64(),
        )
        break
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
710 711
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731
  public async messageSendContact(
    receiver  : Receiver,
    contactId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'messageSend("%s", %s)', JSON.stringify(receiver), contactId)

    if (!this.bridge) {
      throw new Error('no bridge')
    }

    const id = receiver.contactId || receiver.roomId
    if (!id) {
      throw Error('no id')
    }

    const payload = await this.contactPayload(contactId)
    const title = payload.name + '名片'
    await this.bridge.WXShareCard(id, contactId, title)
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
732 733 734 735 736
  public async messageForward(
    receiver  : Receiver,
    messageId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'messageForward(%s, %s)',
737
                              JSON.stringify(receiver),
ruiruibupt's avatar
init  
ruiruibupt 已提交
738 739
                              messageId,
              )
740
    const payload = await this.messagePayload(messageId)
ruiruibupt's avatar
ruiruibupt 已提交
741

742 743 744 745
    if (payload.type === MessageType.Text) {
      if (!payload.text) {
        throw new Error('no text')
      }
ruiruibupt's avatar
ruiruibupt 已提交
746 747
      await this.messageSendText(
        receiver,
748
        payload.text,
ruiruibupt's avatar
ruiruibupt 已提交
749 750 751 752
      )
    } else {
      await this.messageSendFile(
        receiver,
753
        await this.messageFile(messageId),
ruiruibupt's avatar
ruiruibupt 已提交
754 755
      )
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
756 757 758 759 760 761 762
  }

  /**
   *
   * Room
   *
   */
763 764 765 766 767
  public async roomMemberRawPayload(
    roomId    : string,
    contactId : string,
  ): Promise<PadchatRoomMemberPayload> {
    log.silly('PuppetPadchat', 'roomMemberRawPayload(%s)', roomId)
ruiruibupt's avatar
ruiruibupt 已提交
768

769 770 771 772
    if (!this.bridge) {
      throw new Error('no bridge')
    }

773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
    const rawPayload = await this.bridge.roomMemberRawPayload(roomId, contactId)
    return rawPayload
  }

  public async roomMemberRawPayloadParser(
    rawPayload: PadchatRoomMemberPayload,
  ): Promise<RoomMemberPayload> {
    log.silly('PuppetPadchat', 'roomMemberRawPayloadParser(%s)', rawPayload)

    const payload: RoomMemberPayload = {
      id        : rawPayload.user_name,
      inviterId : rawPayload.invited_by,
      roomAlias : rawPayload.chatroom_nick_name,
    }

    return payload
  }

  public async roomRawPayload(roomId: string): Promise<PadchatRoomPayload> {
    log.verbose('PuppetPadchat', 'roomRawPayload(%s)', roomId)

794 795 796 797
    if (!this.bridge) {
      throw new Error('no bridge')
    }

798
    const rawPayload = await this.bridge.roomRawPayload(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
799 800 801
    return rawPayload
  }

802
  public async roomRawPayloadParser(rawPayload: PadchatRoomPayload): Promise<RoomPayload> {
803
    log.verbose('PuppetPadchat', 'roomRawPayloadParser(rawPayload.user_name="%s")', rawPayload.user_name)
ruiruibupt's avatar
init  
ruiruibupt 已提交
804

805 806
    // const memberIdList = await this.bridge.getRoomMemberIdList()
    //  WXGetChatRoomMember(rawPayload.user_name)
807

808
    const payload: RoomPayload = roomRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
809 810 811 812

    return payload
  }

813 814 815
  public async roomMemberList(roomId: string): Promise<string[]> {
    log.verbose('PuppetPadchat', 'roomMemberList(%s)', roomId)

816 817 818 819
    if (!this.bridge) {
      throw new Error('no bridge')
    }

820 821 822 823 824 825
    const memberIdList = await this.bridge.getRoomMemberIdList(roomId)
    log.silly('PuppetPadchat', 'roomMemberList()=%d', memberIdList.length)

    return memberIdList
  }

826
  public async roomList(): Promise<string[]> {
ruiruibupt's avatar
ruiruibupt 已提交
827
    log.verbose('PuppetPadchat', 'roomList()')
828

829 830 831 832
    if (!this.bridge) {
      throw new Error('no bridge')
    }

833 834
    const roomIdList = await this.bridge.getRoomIdList()
    log.silly('PuppetPadchat', 'roomList()=%d', roomIdList.length)
ruiruibupt's avatar
ruiruibupt 已提交
835

ruiruibupt's avatar
ruiruibupt 已提交
836
    return roomIdList
ruiruibupt's avatar
init  
ruiruibupt 已提交
837 838 839 840 841 842 843
  }

  public async roomDel(
    roomId    : string,
    contactId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'roomDel(%s, %s)', roomId, contactId)
ruiruibupt's avatar
ruiruibupt 已提交
844

845 846 847 848
    if (!this.bridge) {
      throw new Error('no bridge')
    }

849
    // Should check whether user is in the room. WXDeleteChatRoomMember won't check if user in the room automatically
ruiruibupt's avatar
ruiruibupt 已提交
850
    await this.bridge.WXDeleteChatRoomMember(roomId, contactId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
851 852
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
853
  public async roomQrcode(roomId: string): Promise<string> {
854 855 856 857 858 859 860 861
    log.verbose('PuppetPadchat', 'roomQrCode(%s)', roomId)

    // TODO

    throw new Error('not support')

  }

862 863 864 865 866 867 868 869 870 871 872 873 874
  public async roomAvatar(roomId: string): Promise<FileBox> {
    log.verbose('PuppetPadchat', 'roomAvatar(%s)', roomId)

    const payload = await this.roomPayload(roomId)

    if (payload.avatar) {
      return FileBox.fromUrl(payload.avatar)
    }
    log.warn('PuppetPadchat', 'roomAvatar() avatar not found, use the chatie default.')

    return qrCodeForChatie()
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
875 876 877 878 879
  public async roomAdd(
    roomId    : string,
    contactId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'roomAdd(%s, %s)', roomId, contactId)
880

881 882 883 884
    if (!this.bridge) {
      throw new Error('no bridge')
    }

885 886 887 888 889 890 891 892 893 894 895 896
    // 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)
    } catch (e) {
      // FIXME
      console.error(e)
      log.warn('PuppetPadchat', 'roomAdd(%s, %s) Add exception: %s', e)
      log.verbose('PuppetPadchat', 'roomAdd(%s, %s) try to Invite', roomId, contactId)
      await this.bridge.WXInviteChatRoomMember(roomId, contactId)
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
897 898
  }

899 900 901
  public async roomTopic(roomId: string)                : Promise<string>
  public async roomTopic(roomId: string, topic: string) : Promise<void>

ruiruibupt's avatar
init  
ruiruibupt 已提交
902 903 904 905 906 907 908
  public async roomTopic(
    roomId: string,
    topic?: string,
  ): Promise<void | string> {
    log.verbose('PuppetPadchat', 'roomTopic(%s, %s)', roomId, topic)

    if (typeof topic === 'undefined') {
ruiruibupt's avatar
ruiruibupt 已提交
909
      const payload = await this.roomPayload(roomId)
ruiruibupt's avatar
ruiruibupt 已提交
910
      return payload.topic
ruiruibupt's avatar
init  
ruiruibupt 已提交
911
    }
ruiruibupt's avatar
ruiruibupt 已提交
912

913 914 915 916
    if (!this.bridge) {
      throw new Error('no bridge')
    }

ruiruibupt's avatar
ruiruibupt 已提交
917 918
    await this.bridge.WXSetChatroomName(roomId, topic)

ruiruibupt's avatar
init  
ruiruibupt 已提交
919 920 921 922 923 924 925 926 927
    return
  }

  public async roomCreate(
    contactIdList : string[],
    topic         : string,
  ): Promise<string> {
    log.verbose('PuppetPadchat', 'roomCreate(%s, %s)', contactIdList, topic)

928 929 930 931
    if (!this.bridge) {
      throw new Error('no bridge')
    }

932 933 934 935 936
    // FIXME:
    const roomId = this.bridge.WXCreateChatRoom(contactIdList)
    console.log('roomCreate returl:', roomId)

    return roomId
ruiruibupt's avatar
init  
ruiruibupt 已提交
937 938 939 940
  }

  public async roomQuit(roomId: string): Promise<void> {
    log.verbose('PuppetPadchat', 'roomQuit(%s)', roomId)
941 942 943 944 945

    if (!this.bridge) {
      throw new Error('no bridge')
    }

ruiruibupt's avatar
ruiruibupt 已提交
946
    await this.bridge.WXQuitChatRoom(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
947 948
  }

949 950 951 952 953
  public async roomAnnounce(roomId: string)             : Promise<string>
  public async roomAnnounce(roomId: string, text: string) : Promise<void>

  public async roomAnnounce(roomId: string, text?: string): Promise<void | string> {
    log.verbose('PuppetPadchat', 'roomAnnounce(%s, %s)', roomId, text ? text : '')
954 955 956 957 958

    if (!this.bridge) {
      throw new Error('no bridge')
    }

959 960 961 962 963 964 965
    if (text) {
      await this.bridge.WXSetChatroomAnnouncement(roomId, text)
    } else {
      return await this.bridge.WXGetChatroomAnnouncement(roomId)
    }
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
966 967
  /**
   *
968
   * Friendship
ruiruibupt's avatar
init  
ruiruibupt 已提交
969 970
   *
   */
971
  public async friendshipVerify(
ruiruibupt's avatar
init  
ruiruibupt 已提交
972 973 974
    contactId : string,
    hello     : string,
  ): Promise<void> {
975
    log.verbose('PuppetPadchat', 'friendshipVerify(%s, %s)', contactId, hello)
ruiruibupt's avatar
ruiruibupt 已提交
976

E
Egg 已提交
977 978 979
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
ruiruibupt 已提交
980

E
Egg 已提交
981
    const rawSearchPayload: WXSearchContactType = await this.bridge.WXSearchContact(contactId)
982

E
Egg 已提交
983 984 985 986 987 988 989
    /**
     * If the contact is not stranger, than ussing WXSearchContact can get user_name
     */
    if (rawSearchPayload.user_name !== '' && !pfHelper.isStrangerV1(rawSearchPayload.user_name) && !pfHelper.isStrangerV2(rawSearchPayload.user_name)) {
      log.warn('PuppetPadchat', 'friendRequestSend %s has been friend with bot, no need to send friend request!', contactId)
      return
    }
ruiruibupt's avatar
ruiruibupt 已提交
990

E
Egg 已提交
991 992 993 994 995 996 997 998 999 1000
    let strangerV1
    let strangerV2
    if (pfHelper.isStrangerV1(rawSearchPayload.stranger)) {
      strangerV1 = rawSearchPayload.stranger
      strangerV2 = rawSearchPayload.user_name
    } else if (pfHelper.isStrangerV2(rawSearchPayload.stranger)) {
      strangerV2 = rawSearchPayload.stranger
      strangerV1 = rawSearchPayload.user_name
    } else {
      throw new Error('stranger neither v1 nor v2!')
1001 1002
    }

E
Egg 已提交
1003
    // Issue #1252 : what's wrong here?, Trying to fix now...
1004

ruiruibupt's avatar
ruiruibupt 已提交
1005
    await this.bridge.WXAddUser(
E
Egg 已提交
1006 1007 1008
      strangerV1 || '',
      strangerV2 || '',
      WXSearchContactTypeStatus.WXID, // default
ruiruibupt's avatar
ruiruibupt 已提交
1009 1010
      hello,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
1011 1012
  }

1013 1014
  public async friendshipAccept(
    friendshipId : string,
ruiruibupt's avatar
init  
ruiruibupt 已提交
1015
  ): Promise<void> {
1016
    log.verbose('PuppetPadchat', 'friendshipAccept(%s)', friendshipId)
ruiruibupt's avatar
ruiruibupt 已提交
1017

1018
    const payload = await this.friendshipPayload(friendshipId) as FriendshipPayloadReceive
ruiruibupt's avatar
ruiruibupt 已提交
1019

1020
    console.log('friendshipAccept: ', payload)
1021

1022 1023 1024
    if (!payload.ticket) {
      throw new Error('no ticket')
    }
1025 1026 1027
    if (!payload.stranger) {
      throw new Error('no stranger')
    }
ruiruibupt's avatar
ruiruibupt 已提交
1028

1029 1030 1031 1032
    if (!this.bridge) {
      throw new Error('no bridge')
    }

1033
    await this.bridge.WXAcceptUser(
1034
      payload.stranger,
1035 1036
      payload.ticket,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
1037 1038
  }

1039 1040
  public async friendshipRawPayloadParser(rawPayload: PadchatMessagePayload) : Promise<FriendshipPayload> {
    log.verbose('PuppetPadchat', 'friendshipRawPayloadParser({id=%s})', rawPayload.msg_id)
ruiruibupt's avatar
ruiruibupt 已提交
1041

1042
    const payload: FriendshipPayload = await friendshipRawPayloadParser(rawPayload)
1043
    return payload
ruiruibupt's avatar
ruiruibupt 已提交
1044 1045
  }

1046 1047
  public async friendshipRawPayload(friendshipId: string): Promise<PadchatMessagePayload> {
    log.verbose('PuppetPadchat', 'friendshipRawPayload(%s)', friendshipId)
ruiruibupt's avatar
ruiruibupt 已提交
1048

1049
    const rawPayload = this.cachePadchatFriendshipPayload.get(friendshipId)
1050
    if (!rawPayload) {
1051
      throw new Error('no rawPayload for id ' + friendshipId)
1052
    }
ruiruibupt's avatar
ruiruibupt 已提交
1053

1054
    return rawPayload
ruiruibupt's avatar
ruiruibupt 已提交
1055 1056
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
1057 1058 1059
}

export default PuppetPadchat