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

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

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

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

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

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

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

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

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

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

export class PuppetPadchat extends Puppet {
99

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  }

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
261
      case PadchatMessageType.Sys:
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
262
        await this.onPadchatMessageSys(rawPayload)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263 264
        break

265 266 267 268 269 270 271 272
      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

273 274 275 276 277 278 279 280 281 282
      default:
        this.cachePadchatMessagePayload.set(
          rawPayload.msg_id,
          rawPayload,
        )
        this.emit('message', rawPayload.msg_id)
        break
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
  protected async onPadchatMessageSys(rawPayload: PadchatMessagePayload) {
    log.verbose('PuppetPadchat', 'onPadchatMessageSys({id=%s})')
    /**
     * 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) {
        log.warn('PuppetPadchat', 'onPadchatMessage() case PadchatMesssageSys: inviterId found more than 1, use the first one.')
      }
      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)
    }
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
361
  public async stop(): Promise<void> {
362 363 364 365 366
    log.verbose('PuppetPadchat', 'stop()')

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

    if (this.state.off()) {
369
      log.warn('PuppetPadchat', 'stop() is called on a OFF puppet. await ready(off) and return.')
ruiruibupt's avatar
init  
ruiruibupt 已提交
370 371 372 373 374
      await this.state.ready('off')
      return
    }

    this.state.off('pending')
ruiruibupt's avatar
ruiruibupt 已提交
375 376 377

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

379
    setImmediate(() => this.bridge && this.bridge.removeAllListeners())
ruiruibupt's avatar
ruiruibupt 已提交
380
    await this.bridge.stop()
ruiruibupt's avatar
ruiruibupt 已提交
381

ruiruibupt's avatar
init  
ruiruibupt 已提交
382 383
    // await some tasks...
    this.state.off(true)
384
    this.emit('stop')
ruiruibupt's avatar
init  
ruiruibupt 已提交
385 386
  }

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

390
    if (!this.id) {
391 392 393 394 395 396 397
      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 已提交
398 399
    }

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

403 404 405 406
    // if (!passive) {
    //   await this.bridge.WXLogout()
    // }

407
    await this.bridge.logout()
ruiruibupt's avatar
init  
ruiruibupt 已提交
408 409 410 411 412 413 414 415 416 417 418 419 420 421
  }

  /**
   *
   * 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 已提交
422
      const payload = await this.contactPayload(contactId)
ruiruibupt's avatar
ruiruibupt 已提交
423
      return payload.alias || ''
ruiruibupt's avatar
init  
ruiruibupt 已提交
424
    }
ruiruibupt's avatar
ruiruibupt 已提交
425

426 427 428 429
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
432 433 434
    return
  }

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

438 439 440 441
    if (!this.bridge) {
      throw new Error('no bridge')
    }

ruiruibupt's avatar
ruiruibupt 已提交
442
    const contactIdList = this.bridge.getContactIdList()
ruiruibupt's avatar
ruiruibupt 已提交
443 444 445 446

    return contactIdList
  }

447 448
  public async contactAvatar(contactId: string)                : Promise<FileBox>
  public async contactAvatar(contactId: string, file: FileBox) : Promise<void>
ruiruibupt's avatar
init  
ruiruibupt 已提交
449

450 451 452 453 454 455 456 457 458 459
  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')
      }
460 461 462
      if (!this.bridge) {
        throw new Error('no bridge')
      }
463 464 465 466 467 468 469
      await this.bridge.WXSetHeadImage(await file.toBase64())
      return
    }

    /**
     * 2. get avatar
     */
ruiruibupt's avatar
ruiruibupt 已提交
470 471 472 473 474 475
    const payload = await this.contactPayload(contactId)

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

476 477 478 479
    const fileBox = FileBox.fromUrl(payload.avatar)
    return fileBox
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
480
  public async contactQrcode(contactId: string): Promise<string> {
481 482 483
    if (contactId !== this.selfId()) {
      throw new Error('can not set avatar for others')
    }
484 485 486
    if (!this.bridge) {
      throw new Error('no bridge')
    }
487
    const base64 = await this.bridge.WXGetUserQRCode(contactId, 0)
488
    const qrcode = await fileBoxToQrcode(base64)
489
    return qrcode
ruiruibupt's avatar
init  
ruiruibupt 已提交
490 491
  }

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

495 496 497
    if (!this.bridge) {
      throw new Error('no bridge')
    }
498
    const rawPayload = await this.bridge.contactRawPayload(contactId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
499 500 501
    return rawPayload
  }

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

505
    const payload: ContactPayload = contactRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
506 507 508 509 510 511 512 513
    return payload
  }

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

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

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

ruiruibupt's avatar
ruiruibupt 已提交
520 521
    // TODO

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560
    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 已提交
561
    const base64 = 'cRH9qeL3XyVnaXJkppBuH20tf5JlcG9uFX1lL2IvdHRRRS9kMMQxOPLKNYIzQQ=='
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
562
    const filename = 'test-' + messageId + '.txt'
ruiruibupt's avatar
ruiruibupt 已提交
563

564
    const file = FileBox.fromBase64(
ruiruibupt's avatar
ruiruibupt 已提交
565 566 567 568 569 570 571
      base64,
      filename,
    )

    return file
  }

572
  public async messageRawPayload(id: string): Promise<PadchatMessagePayload> {
573 574 575 576 577
    // throw Error('should not call messageRawPayload: ' + id)

    /**
     * Issue #1249
     */
ruiruibupt's avatar
ruiruibupt 已提交
578 579 580 581 582

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

584
    const rawPayload = this.cachePadchatMessagePayload.get(id)
585 586 587 588 589 590

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

    return rawPayload
ruiruibupt's avatar
init  
ruiruibupt 已提交
591 592
  }

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
598
    log.silly('PuppetPadchat', 'messagePayload(%s)', JSON.stringify(payload))
ruiruibupt's avatar
init  
ruiruibupt 已提交
599 600 601 602 603 604 605
    return payload
  }

  public async messageSendText(
    receiver : Receiver,
    text     : string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
606
    log.verbose('PuppetPadchat', 'messageSend(%s, %s)', JSON.stringify(receiver), text)
ruiruibupt's avatar
ruiruibupt 已提交
607 608
    const id = receiver.contactId || receiver.roomId
    if (!id) {
609
      throw Error('no id')
ruiruibupt's avatar
ruiruibupt 已提交
610
    }
611 612 613
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
ruiruibupt 已提交
614
    await this.bridge.WXSendMsg(id, text)
ruiruibupt's avatar
init  
ruiruibupt 已提交
615 616 617 618 619 620
  }

  public async messageSendFile(
    receiver : Receiver,
    file     : FileBox,
  ): Promise<void> {
621
    log.verbose('PuppetPadchat', 'messageSend("%s", %s)', JSON.stringify(receiver), file)
ruiruibupt's avatar
ruiruibupt 已提交
622 623 624 625 626 627

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

628 629 630 631
    if (!this.bridge) {
      throw new Error('no bridge')
    }

632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
    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 已提交
650 651
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
  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 已提交
672 673 674 675 676
  public async messageForward(
    receiver  : Receiver,
    messageId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'messageForward(%s, %s)',
677
                              JSON.stringify(receiver),
ruiruibupt's avatar
init  
ruiruibupt 已提交
678 679
                              messageId,
              )
680
    const payload = await this.messagePayload(messageId)
ruiruibupt's avatar
ruiruibupt 已提交
681

682 683 684 685
    if (payload.type === MessageType.Text) {
      if (!payload.text) {
        throw new Error('no text')
      }
ruiruibupt's avatar
ruiruibupt 已提交
686 687
      await this.messageSendText(
        receiver,
688
        payload.text,
ruiruibupt's avatar
ruiruibupt 已提交
689 690 691 692
      )
    } else {
      await this.messageSendFile(
        receiver,
693
        await this.messageFile(messageId),
ruiruibupt's avatar
ruiruibupt 已提交
694 695
      )
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
696 697 698 699 700 701 702
  }

  /**
   *
   * Room
   *
   */
703 704 705 706 707
  public async roomMemberRawPayload(
    roomId    : string,
    contactId : string,
  ): Promise<PadchatRoomMemberPayload> {
    log.silly('PuppetPadchat', 'roomMemberRawPayload(%s)', roomId)
ruiruibupt's avatar
ruiruibupt 已提交
708

709 710 711 712
    if (!this.bridge) {
      throw new Error('no bridge')
    }

713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733
    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)

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

738
    const rawPayload = await this.bridge.roomRawPayload(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
739 740 741
    return rawPayload
  }

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

745 746
    // const memberIdList = await this.bridge.getRoomMemberIdList()
    //  WXGetChatRoomMember(rawPayload.user_name)
747

748
    const payload: RoomPayload = roomRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
749 750 751 752

    return payload
  }

753 754 755
  public async roomMemberList(roomId: string): Promise<string[]> {
    log.verbose('PuppetPadchat', 'roomMemberList(%s)', roomId)

756 757 758 759
    if (!this.bridge) {
      throw new Error('no bridge')
    }

760 761 762 763 764 765
    const memberIdList = await this.bridge.getRoomMemberIdList(roomId)
    log.silly('PuppetPadchat', 'roomMemberList()=%d', memberIdList.length)

    return memberIdList
  }

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

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

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

ruiruibupt's avatar
ruiruibupt 已提交
776
    return roomIdList
ruiruibupt's avatar
init  
ruiruibupt 已提交
777 778 779 780 781 782 783
  }

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
793
  public async roomQrcode(roomId: string): Promise<string> {
794 795 796 797 798 799 800 801
    log.verbose('PuppetPadchat', 'roomQrCode(%s)', roomId)

    // TODO

    throw new Error('not support')

  }

802 803 804 805 806 807 808 809 810 811 812 813 814
  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 已提交
815 816 817 818 819
  public async roomAdd(
    roomId    : string,
    contactId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'roomAdd(%s, %s)', roomId, contactId)
820

821 822 823 824
    if (!this.bridge) {
      throw new Error('no bridge')
    }

825 826 827 828 829 830 831 832 833 834 835 836
    // 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 已提交
837 838
  }

839 840 841
  public async roomTopic(roomId: string)                : Promise<string>
  public async roomTopic(roomId: string, topic: string) : Promise<void>

ruiruibupt's avatar
init  
ruiruibupt 已提交
842 843 844 845 846 847 848
  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 已提交
849
      const payload = await this.roomPayload(roomId)
ruiruibupt's avatar
ruiruibupt 已提交
850
      return payload.topic
ruiruibupt's avatar
init  
ruiruibupt 已提交
851
    }
ruiruibupt's avatar
ruiruibupt 已提交
852

853 854 855 856
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
859 860 861 862 863 864 865 866 867
    return
  }

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

868 869 870 871
    if (!this.bridge) {
      throw new Error('no bridge')
    }

872 873 874 875 876
    // FIXME:
    const roomId = this.bridge.WXCreateChatRoom(contactIdList)
    console.log('roomCreate returl:', roomId)

    return roomId
ruiruibupt's avatar
init  
ruiruibupt 已提交
877 878 879 880
  }

  public async roomQuit(roomId: string): Promise<void> {
    log.verbose('PuppetPadchat', 'roomQuit(%s)', roomId)
881 882 883 884 885

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

ruiruibupt's avatar
ruiruibupt 已提交
886
    await this.bridge.WXQuitChatRoom(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
887 888
  }

889 890 891 892 893
  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 : '')
894 895 896 897 898

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

899 900 901 902 903 904 905
    if (text) {
      await this.bridge.WXSetChatroomAnnouncement(roomId, text)
    } else {
      return await this.bridge.WXGetChatroomAnnouncement(roomId)
    }
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
906 907 908 909 910 911 912 913 914 915
  /**
   *
   * FriendRequest
   *
   */
  public async friendRequestSend(
    contactId : string,
    hello     : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'friendRequestSend(%s, %s)', contactId, hello)
ruiruibupt's avatar
ruiruibupt 已提交
916 917 918

    const rawPayload = await this.contactRawPayload(contactId)

919 920 921 922 923
    // XXX
    console.log('rawPayload.stranger: ', rawPayload)

    // let strangerV1
    // let strangerV2
924
    // if (isStrangerV1(rawPayload.stranger)) {
925
    //   strangerV1 = rawPayload.stranger
926
    // } else if (isStrangerV2(rawPayload.stranger)) {
927 928 929 930
    //   strangerV2 = rawPayload.stranger
    // } else {
    //   throw new Error('stranger neither v1 nor v2!')
    // }
ruiruibupt's avatar
ruiruibupt 已提交
931

932 933 934 935
    if (!this.bridge) {
      throw new Error('no bridge')
    }

936 937
    // Issue #1252 : what's wrong here?

ruiruibupt's avatar
ruiruibupt 已提交
938
    await this.bridge.WXAddUser(
939 940
      rawPayload.stranger || '',
      rawPayload.ticket   || '',
ruiruibupt's avatar
ruiruibupt 已提交
941 942 943
      '14',
      hello,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
944 945 946
  }

  public async friendRequestAccept(
947
    friendRequestId : string,
ruiruibupt's avatar
init  
ruiruibupt 已提交
948
  ): Promise<void> {
949
    log.verbose('PuppetPadchat', 'friendRequestAccept(%s)', friendRequestId)
ruiruibupt's avatar
ruiruibupt 已提交
950

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

953 954
    console.log('friendRequestAccept: ', payload)

955 956 957
    if (!payload.ticket) {
      throw new Error('no ticket')
    }
958 959 960
    if (!payload.stranger) {
      throw new Error('no stranger')
    }
ruiruibupt's avatar
ruiruibupt 已提交
961

962 963 964 965
    if (!this.bridge) {
      throw new Error('no bridge')
    }

966
    await this.bridge.WXAcceptUser(
967
      payload.stranger,
968 969
      payload.ticket,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
970 971
  }

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

975
    const payload: FriendRequestPayload = await friendRequestRawPayloadParser(rawPayload)
976
    return payload
ruiruibupt's avatar
ruiruibupt 已提交
977 978
  }

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

982 983
    const rawPayload = this.cachePadchatFriendRequestPayload.get(friendRequestId)
    if (!rawPayload) {
984
      throw new Error('no rawPayload for id ' + friendRequestId)
985
    }
ruiruibupt's avatar
ruiruibupt 已提交
986

987
    return rawPayload
ruiruibupt's avatar
ruiruibupt 已提交
988 989
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
990 991 992
}

export default PuppetPadchat