puppet-padchat.ts 25.8 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

24
import LRU from 'lru-cache'
25

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

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

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

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

  Puppet,
  PuppetOptions,
41

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

ruiruibupt's avatar
ruiruibupt 已提交
44
  FriendRequestPayload,
45
  FriendRequestPayloadReceive,
46 47

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

50
import {
51 52 53 54 55 56 57 58 59
  contactRawPayloadParser,
  fileBoxToQrcode,
  friendRequestRawPayloadParser,
  messageRawPayloadParser,
  roomJoinEventMessageParser,
  roomLeaveEventMessageParser,
  roomRawPayloadParser,
  roomTopicEventMessageParser,
}                                         from './pure-function-helpers'
60

ruiruibupt's avatar
init  
ruiruibupt 已提交
61 62
import {
  log,
63 64
  qrCodeForChatie,
}                   from '../config'
ruiruibupt's avatar
init  
ruiruibupt 已提交
65

66
import {
67
  padchatToken,
68 69 70
  WECHATY_PUPPET_PADCHAT_ENDPOINT,
}                                   from './config'

ruiruibupt's avatar
init  
ruiruibupt 已提交
71
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
72
  PadchatManager,
73
  // resolverDict,
ruiruibupt's avatar
ruiruibupt 已提交
74
  // AutoDataType,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
75
}                       from './padchat-manager'
ruiruibupt's avatar
init  
ruiruibupt 已提交
76 77

import {
78
  // PadchatPayload,
79
  PadchatContactPayload,
80
  PadchatMessagePayload,
81
  PadchatRoomPayload,
82 83
  // PadchatRoomMemberListPayload,
  PadchatRoomMemberPayload,
84
  PadchatMessageType,
85

86
  // PadchatMessageType,
87 88 89 90
  // PadchatContinue,
  // PadchatMsgType,
  // PadchatStatus,
  // PadchatPayloadType,
91
  // PadchatRoomRawMember,
92
}                           from './padchat-schemas'
ruiruibupt's avatar
init  
ruiruibupt 已提交
93 94 95 96 97

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

export class PuppetPadchat extends Puppet {
98

99 100 101
  // in seconds, 4 minute for padchat
  protected [WATCHDOG_TIMEOUT] = 4 * 60

102
  // private readonly cachePadchatContactPayload       : LRU.Cache<string, PadchatContactRawPayload>
103
  private readonly cachePadchatFriendRequestPayload : LRU.Cache<string, PadchatMessagePayload>
104 105
  private readonly cachePadchatMessagePayload       : LRU.Cache<string, PadchatMessagePayload>
  // private readonly cachePadchatRoomPayload          : LRU.Cache<string, PadchatRoomRawPayload>
106

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
107
  public bridge?:  PadchatManager
ruiruibupt's avatar
init  
ruiruibupt 已提交
108 109 110 111 112 113

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

114 115 116
    const lruOptions: LRU.Options = {
      max: 1000,
      // length: function (n) { return n * 2},
117
      dispose: function (key: string, val: any) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118
        log.silly('PuppetPadchat', 'constructor() lruOptions.dispose(%s, %s)', key, JSON.stringify(val))
119 120 121 122
      },
      maxAge: 1000 * 60 * 60,
    }

123
    // this.cachePadchatContactPayload       = new LRU<string, PadchatContactRawPayload>(lruOptions)
124
    this.cachePadchatFriendRequestPayload = new LRU<string, PadchatMessagePayload>(lruOptions)
125
    this.cachePadchatMessagePayload       = new LRU<string, PadchatMessagePayload>(lruOptions)
126
    // this.cachePadchatRoomPayload          = new LRU<string, PadchatRoomRawPayload>(lruOptions)
ruiruibupt's avatar
init  
ruiruibupt 已提交
127 128 129
  }

  public toString() {
130
    return `PuppetPadchat<${this.options.memory.name}>`
ruiruibupt's avatar
init  
ruiruibupt 已提交
131 132 133 134 135 136
  }

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

ruiruibupt's avatar
ruiruibupt 已提交
137
  public startWatchdog(): void {
ruiruibupt's avatar
ruiruibupt 已提交
138
    log.verbose('PuppetPadchat', 'initWatchdogForPuppet()')
ruiruibupt's avatar
init  
ruiruibupt 已提交
139

140 141 142
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
143

ruiruibupt's avatar
ruiruibupt 已提交
144 145 146
    // clean the dog because this could be re-inited
    this.watchdog.removeAllListeners()

147
    /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
148
     * Use bridge's heartbeat to feed dog
149 150 151 152 153 154 155
     */
    this.bridge.on('heartbeat', (data: string) => {
      log.silly('PuppetPadchat', 'startWatchdog() bridge.on(heartbeat)')
      this.watchdog.feed({
        data,
      })
    })
ruiruibupt's avatar
ruiruibupt 已提交
156
    this.watchdog.on('feed', async food => {
157
      log.silly('PuppetPadchat', 'startWatchdog() watchdog.on(feed, food={type=%s, data=%s})', food.type, food.data)
ruiruibupt's avatar
ruiruibupt 已提交
158 159
    })

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
    this.watchdog.on('reset', async (food, timeout) => {
161 162 163 164 165
      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 (李卓桓) 已提交
166
    })
ruiruibupt's avatar
ruiruibupt 已提交
167 168 169 170 171

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

ruiruibupt's avatar
ruiruibupt 已提交
172
  }
ruiruibupt's avatar
init  
ruiruibupt 已提交
173 174

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

177 178 179 180 181 182
    if (this.state.on()) {
      log.warn('PuppetPadchat', 'start() already on(pending)?')
      await this.state.ready('on')
      return
    }

ruiruibupt's avatar
ruiruibupt 已提交
183 184 185 186 187 188 189
    /**
     * state has two main state: ON / OFF
     * ON (pending)
     * OFF (pending)
     */
    this.state.on('pending')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
190
    const bridge = this.bridge = new PadchatManager({
191 192 193 194 195 196
      memory   : this.options.memory,
      token    : padchatToken(),
      endpoint : WECHATY_PUPPET_PADCHAT_ENDPOINT,
    })

    await this.startBridge(bridge)
ruiruibupt's avatar
ruiruibupt 已提交
197
    await this.startWatchdog()
ruiruibupt's avatar
ruiruibupt 已提交
198

ruiruibupt's avatar
ruiruibupt 已提交
199
    this.state.on(true)
200
    this.emit('start')
201
  }
ruiruibupt's avatar
init  
ruiruibupt 已提交
202

203
  protected async login(selfId: string): Promise<void> {
204 205 206
    if (!this.bridge) {
      throw new Error('no bridge')
    }
207 208
    await super.login(selfId)
    this.bridge.syncContactsAndRooms()
ruiruibupt's avatar
init  
ruiruibupt 已提交
209 210
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
211
  public async startBridge(bridge: PadchatManager): Promise<void> {
ruiruibupt's avatar
ruiruibupt 已提交
212
    log.verbose('PuppetPadchat', 'startBridge()')
ruiruibupt's avatar
ruiruibupt 已提交
213 214

    if (this.state.off()) {
215
      throw new Error('startBridge() state is off')
ruiruibupt's avatar
ruiruibupt 已提交
216 217
    }

218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
    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 已提交
240 241 242

  }

Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
243
  protected async onPadchatMessage(rawPayload: PadchatMessagePayload): Promise<void> {
244 245 246 247 248
    log.verbose('PuppetPadchat', 'onPadchatMessage({id=%s, type=%s(%s)})',
                                rawPayload.msg_id,
                                PadchatMessageType[rawPayload.sub_type],
                                rawPayload.msg_type,
              )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
249 250
    console.log('rawPayload:', rawPayload)

251 252
    switch (rawPayload.sub_type) {
      case PadchatMessageType.VerifyMsg:
253 254 255 256
        this.cachePadchatFriendRequestPayload.set(
          rawPayload.msg_id,
          rawPayload,
        )
257 258 259
        this.emit('friend', rawPayload.msg_id)
        break

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
260 261
      case PadchatMessageType.Sys:
        console.log('sys message:', rawPayload)
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
262

263
        const roomJoin = roomJoinEventMessageParser(rawPayload)
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
264 265 266 267 268 269 270 271 272 273 274 275 276
        if (roomJoin) {
          const inviteeNameList = roomJoin.inviteeNameList
          const inviterName     = roomJoin.inviterName
          const roomId          = roomJoin.roomId

          const inviteeIdList = await Promise.all(
            inviteeNameList.map(inviteeName => this.roomMemberSearch(roomId, inviteeName))
          )
          const inviterId = await this.roomMemberSearch(roomId, inviterName)

          this.emit('room-join',   roomId, inviteeIdList,  inviterId)
        }

277
        const roomLeave = roomLeaveEventMessageParser(rawPayload)
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
278 279 280 281 282 283 284 285 286 287 288 289 290
        if (roomLeave) {
          const leaverNameList = roomLeave.leaverNameList
          const removerName    = roomLeave.removerName
          const roomId         = roomLeave.roomId

          const leaverIdList = await Promise.all(
            leaverNameList.map(leaverName => this.roomMemberSearch(roomId, leaverName))
          )
          const removerId = await this.roomMemberSearch(roomId, removerName)

          this.emit('room-leave',  roomId, leaverIdList, removerId)
        }

291
        const roomTopic = roomTopicEventMessageParser(rawPayload)
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
292 293 294 295 296 297 298 299 300 301 302 303
        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 changerId = await this.roomMemberSearch(roomId, changerName)

          this.emit('room-topic',  roomId, newTopic, oldTopic, changerId)
        }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
304 305 306

        break

307 308 309 310 311 312 313 314
      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

315 316 317 318 319 320 321 322 323 324
      default:
        this.cachePadchatMessagePayload.set(
          rawPayload.msg_id,
          rawPayload,
        )
        this.emit('message', rawPayload.msg_id)
        break
    }
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
325
  public async stop(): Promise<void> {
326 327 328 329 330
    log.verbose('PuppetPadchat', 'stop()')

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

    if (this.state.off()) {
333
      log.warn('PuppetPadchat', 'stop() is called on a OFF puppet. await ready(off) and return.')
ruiruibupt's avatar
init  
ruiruibupt 已提交
334 335 336 337 338
      await this.state.ready('off')
      return
    }

    this.state.off('pending')
ruiruibupt's avatar
ruiruibupt 已提交
339 340 341

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

343
    setImmediate(() => this.bridge && this.bridge.removeAllListeners())
ruiruibupt's avatar
ruiruibupt 已提交
344
    await this.bridge.stop()
ruiruibupt's avatar
ruiruibupt 已提交
345

ruiruibupt's avatar
init  
ruiruibupt 已提交
346 347
    // await some tasks...
    this.state.off(true)
348
    this.emit('stop')
ruiruibupt's avatar
init  
ruiruibupt 已提交
349 350
  }

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

354
    if (!this.id) {
355 356 357 358 359 360 361
      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 已提交
362 363
    }

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

367 368 369 370
    // if (!passive) {
    //   await this.bridge.WXLogout()
    // }

371
    await this.bridge.logout()
ruiruibupt's avatar
init  
ruiruibupt 已提交
372 373 374 375 376 377 378 379 380 381 382 383 384 385
  }

  /**
   *
   * 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 已提交
386
      const payload = await this.contactPayload(contactId)
ruiruibupt's avatar
ruiruibupt 已提交
387
      return payload.alias || ''
ruiruibupt's avatar
init  
ruiruibupt 已提交
388
    }
ruiruibupt's avatar
ruiruibupt 已提交
389

390 391 392 393
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
396 397 398
    return
  }

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

402 403 404 405
    if (!this.bridge) {
      throw new Error('no bridge')
    }

ruiruibupt's avatar
ruiruibupt 已提交
406
    const contactIdList = this.bridge.getContactIdList()
ruiruibupt's avatar
ruiruibupt 已提交
407 408 409 410

    return contactIdList
  }

411 412
  public async contactAvatar(contactId: string)                : Promise<FileBox>
  public async contactAvatar(contactId: string, file: FileBox) : Promise<void>
ruiruibupt's avatar
init  
ruiruibupt 已提交
413

414 415 416 417 418 419 420 421 422 423
  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')
      }
424 425 426
      if (!this.bridge) {
        throw new Error('no bridge')
      }
427 428 429 430 431 432 433
      await this.bridge.WXSetHeadImage(await file.toBase64())
      return
    }

    /**
     * 2. get avatar
     */
ruiruibupt's avatar
ruiruibupt 已提交
434 435 436 437 438 439
    const payload = await this.contactPayload(contactId)

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

440 441 442 443
    const fileBox = FileBox.fromUrl(payload.avatar)
    return fileBox
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
444
  public async contactQrcode(contactId: string): Promise<string> {
445 446 447
    if (contactId !== this.selfId()) {
      throw new Error('can not set avatar for others')
    }
448 449 450
    if (!this.bridge) {
      throw new Error('no bridge')
    }
451
    const base64 = await this.bridge.WXGetUserQRCode(contactId, 0)
452
    const qrcode = await fileBoxToQrcode(base64)
453
    return qrcode
ruiruibupt's avatar
init  
ruiruibupt 已提交
454 455
  }

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

459 460 461
    if (!this.bridge) {
      throw new Error('no bridge')
    }
462
    const rawPayload = await this.bridge.contactRawPayload(contactId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
463 464 465
    return rawPayload
  }

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

469
    const payload: ContactPayload = contactRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
470 471 472 473 474 475 476 477
    return payload
  }

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

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

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

ruiruibupt's avatar
ruiruibupt 已提交
484 485
    // TODO

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
    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 已提交
525
    const base64 = 'cRH9qeL3XyVnaXJkppBuH20tf5JlcG9uFX1lL2IvdHRRRS9kMMQxOPLKNYIzQQ=='
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
526
    const filename = 'test-' + messageId + '.txt'
ruiruibupt's avatar
ruiruibupt 已提交
527

528
    const file = FileBox.fromBase64(
ruiruibupt's avatar
ruiruibupt 已提交
529 530 531 532 533 534 535
      base64,
      filename,
    )

    return file
  }

536
  public async messageRawPayload(id: string): Promise<PadchatMessagePayload> {
537 538 539 540 541
    // throw Error('should not call messageRawPayload: ' + id)

    /**
     * Issue #1249
     */
ruiruibupt's avatar
ruiruibupt 已提交
542 543 544 545 546

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

548
    const rawPayload = this.cachePadchatMessagePayload.get(id)
549 550 551 552 553 554

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

    return rawPayload
ruiruibupt's avatar
init  
ruiruibupt 已提交
555 556
  }

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
562
    log.silly('PuppetPadchat', 'messagePayload(%s)', JSON.stringify(payload))
ruiruibupt's avatar
init  
ruiruibupt 已提交
563 564 565 566 567 568 569
    return payload
  }

  public async messageSendText(
    receiver : Receiver,
    text     : string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
570
    log.verbose('PuppetPadchat', 'messageSend(%s, %s)', JSON.stringify(receiver), text)
ruiruibupt's avatar
ruiruibupt 已提交
571 572
    const id = receiver.contactId || receiver.roomId
    if (!id) {
573
      throw Error('no id')
ruiruibupt's avatar
ruiruibupt 已提交
574
    }
575 576 577
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
ruiruibupt 已提交
578
    await this.bridge.WXSendMsg(id, text)
ruiruibupt's avatar
init  
ruiruibupt 已提交
579 580 581 582 583 584
  }

  public async messageSendFile(
    receiver : Receiver,
    file     : FileBox,
  ): Promise<void> {
585
    log.verbose('PuppetPadchat', 'messageSend("%s", %s)', JSON.stringify(receiver), file)
ruiruibupt's avatar
ruiruibupt 已提交
586 587 588 589 590 591

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

592 593 594 595
    if (!this.bridge) {
      throw new Error('no bridge')
    }

596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613
    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 已提交
614 615
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
  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 已提交
636 637 638 639 640
  public async messageForward(
    receiver  : Receiver,
    messageId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'messageForward(%s, %s)',
641
                              JSON.stringify(receiver),
ruiruibupt's avatar
init  
ruiruibupt 已提交
642 643
                              messageId,
              )
644
    const payload = await this.messagePayload(messageId)
ruiruibupt's avatar
ruiruibupt 已提交
645

646 647 648 649
    if (payload.type === MessageType.Text) {
      if (!payload.text) {
        throw new Error('no text')
      }
ruiruibupt's avatar
ruiruibupt 已提交
650 651
      await this.messageSendText(
        receiver,
652
        payload.text,
ruiruibupt's avatar
ruiruibupt 已提交
653 654 655 656
      )
    } else {
      await this.messageSendFile(
        receiver,
657
        await this.messageFile(messageId),
ruiruibupt's avatar
ruiruibupt 已提交
658 659
      )
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
660 661 662 663 664 665 666
  }

  /**
   *
   * Room
   *
   */
667 668 669 670 671
  public async roomMemberRawPayload(
    roomId    : string,
    contactId : string,
  ): Promise<PadchatRoomMemberPayload> {
    log.silly('PuppetPadchat', 'roomMemberRawPayload(%s)', roomId)
ruiruibupt's avatar
ruiruibupt 已提交
672

673 674 675 676
    if (!this.bridge) {
      throw new Error('no bridge')
    }

677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697
    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)

698 699 700 701
    if (!this.bridge) {
      throw new Error('no bridge')
    }

702
    const rawPayload = await this.bridge.roomRawPayload(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
703 704 705
    return rawPayload
  }

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

709 710
    // const memberIdList = await this.bridge.getRoomMemberIdList()
    //  WXGetChatRoomMember(rawPayload.user_name)
711

712
    const payload: RoomPayload = roomRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
713 714 715 716

    return payload
  }

717 718 719
  public async roomMemberList(roomId: string): Promise<string[]> {
    log.verbose('PuppetPadchat', 'roomMemberList(%s)', roomId)

720 721 722 723
    if (!this.bridge) {
      throw new Error('no bridge')
    }

724 725 726 727 728 729
    const memberIdList = await this.bridge.getRoomMemberIdList(roomId)
    log.silly('PuppetPadchat', 'roomMemberList()=%d', memberIdList.length)

    return memberIdList
  }

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

733 734 735 736
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
ruiruibupt 已提交
740
    return roomIdList
ruiruibupt's avatar
init  
ruiruibupt 已提交
741 742 743 744 745 746 747
  }

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

749 750 751 752
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
757
  public async roomQrcode(roomId: string): Promise<string> {
758 759 760 761 762 763 764 765
    log.verbose('PuppetPadchat', 'roomQrCode(%s)', roomId)

    // TODO

    throw new Error('not support')

  }

766 767 768 769 770 771 772 773 774 775 776 777 778
  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 已提交
779 780 781 782 783
  public async roomAdd(
    roomId    : string,
    contactId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'roomAdd(%s, %s)', roomId, contactId)
784

785 786 787 788
    if (!this.bridge) {
      throw new Error('no bridge')
    }

789 790 791 792 793 794 795 796 797 798 799 800
    // 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 已提交
801 802
  }

803 804 805
  public async roomTopic(roomId: string)                : Promise<string>
  public async roomTopic(roomId: string, topic: string) : Promise<void>

ruiruibupt's avatar
init  
ruiruibupt 已提交
806 807 808 809 810 811 812
  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 已提交
813
      const payload = await this.roomPayload(roomId)
ruiruibupt's avatar
ruiruibupt 已提交
814
      return payload.topic
ruiruibupt's avatar
init  
ruiruibupt 已提交
815
    }
ruiruibupt's avatar
ruiruibupt 已提交
816

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

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
823 824 825 826 827 828 829 830 831
    return
  }

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

832 833 834 835
    if (!this.bridge) {
      throw new Error('no bridge')
    }

836 837 838 839 840
    // FIXME:
    const roomId = this.bridge.WXCreateChatRoom(contactIdList)
    console.log('roomCreate returl:', roomId)

    return roomId
ruiruibupt's avatar
init  
ruiruibupt 已提交
841 842 843 844
  }

  public async roomQuit(roomId: string): Promise<void> {
    log.verbose('PuppetPadchat', 'roomQuit(%s)', roomId)
845 846 847 848 849

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

ruiruibupt's avatar
ruiruibupt 已提交
850
    await this.bridge.WXQuitChatRoom(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
851 852
  }

853 854 855 856 857
  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 : '')
858 859 860 861 862

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

863 864 865 866 867 868 869
    if (text) {
      await this.bridge.WXSetChatroomAnnouncement(roomId, text)
    } else {
      return await this.bridge.WXGetChatroomAnnouncement(roomId)
    }
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
870 871 872 873 874 875 876 877 878 879
  /**
   *
   * FriendRequest
   *
   */
  public async friendRequestSend(
    contactId : string,
    hello     : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'friendRequestSend(%s, %s)', contactId, hello)
ruiruibupt's avatar
ruiruibupt 已提交
880 881 882

    const rawPayload = await this.contactRawPayload(contactId)

883 884 885 886 887
    // XXX
    console.log('rawPayload.stranger: ', rawPayload)

    // let strangerV1
    // let strangerV2
888
    // if (isStrangerV1(rawPayload.stranger)) {
889
    //   strangerV1 = rawPayload.stranger
890
    // } else if (isStrangerV2(rawPayload.stranger)) {
891 892 893 894
    //   strangerV2 = rawPayload.stranger
    // } else {
    //   throw new Error('stranger neither v1 nor v2!')
    // }
ruiruibupt's avatar
ruiruibupt 已提交
895

896 897 898 899
    if (!this.bridge) {
      throw new Error('no bridge')
    }

900 901
    // Issue #1252 : what's wrong here?

ruiruibupt's avatar
ruiruibupt 已提交
902
    await this.bridge.WXAddUser(
903 904
      rawPayload.stranger || '',
      rawPayload.ticket   || '',
ruiruibupt's avatar
ruiruibupt 已提交
905 906 907
      '14',
      hello,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
908 909 910
  }

  public async friendRequestAccept(
911
    friendRequestId : string,
ruiruibupt's avatar
init  
ruiruibupt 已提交
912
  ): Promise<void> {
913
    log.verbose('PuppetPadchat', 'friendRequestAccept(%s)', friendRequestId)
ruiruibupt's avatar
ruiruibupt 已提交
914

915
    const payload = await this.friendRequestPayload(friendRequestId) as FriendRequestPayloadReceive
ruiruibupt's avatar
ruiruibupt 已提交
916

917 918
    console.log('friendRequestAccept: ', payload)

919 920 921
    if (!payload.ticket) {
      throw new Error('no ticket')
    }
922 923 924
    if (!payload.stranger) {
      throw new Error('no stranger')
    }
ruiruibupt's avatar
ruiruibupt 已提交
925

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

930
    await this.bridge.WXAcceptUser(
931
      payload.stranger,
932 933
      payload.ticket,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
934 935
  }

936
  public async friendRequestRawPayloadParser(rawPayload: PadchatMessagePayload) : Promise<FriendRequestPayload> {
937
    log.verbose('PuppetPadchat', 'friendRequestRawPayloadParser({id=%s})', rawPayload.msg_id)
ruiruibupt's avatar
ruiruibupt 已提交
938

939
    const payload: FriendRequestPayload = await friendRequestRawPayloadParser(rawPayload)
940
    return payload
ruiruibupt's avatar
ruiruibupt 已提交
941 942
  }

943 944
  public async friendRequestRawPayload(friendRequestId: string): Promise<PadchatMessagePayload> {
    log.verbose('PuppetPadchat', 'friendRequestRawPayload(%s)', friendRequestId)
ruiruibupt's avatar
ruiruibupt 已提交
945

946 947
    const rawPayload = this.cachePadchatFriendRequestPayload.get(friendRequestId)
    if (!rawPayload) {
948
      throw new Error('no rawPayload for id ' + friendRequestId)
949
    }
ruiruibupt's avatar
ruiruibupt 已提交
950

951
    return rawPayload
ruiruibupt's avatar
ruiruibupt 已提交
952 953
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
954 955 956
}

export default PuppetPadchat