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

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 315 316
      default:
        this.cachePadchatMessagePayload.set(
          rawPayload.msg_id,
          rawPayload,
        )
        this.emit('message', rawPayload.msg_id)
        break
    }
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
317
  public async stop(): Promise<void> {
318 319 320 321 322
    log.verbose('PuppetPadchat', 'stop()')

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

    if (this.state.off()) {
325
      log.warn('PuppetPadchat', 'stop() is called on a OFF puppet. await ready(off) and return.')
ruiruibupt's avatar
init  
ruiruibupt 已提交
326 327 328 329 330
      await this.state.ready('off')
      return
    }

    this.state.off('pending')
ruiruibupt's avatar
ruiruibupt 已提交
331 332 333

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

335
    setImmediate(() => this.bridge && this.bridge.removeAllListeners())
ruiruibupt's avatar
ruiruibupt 已提交
336
    await this.bridge.stop()
ruiruibupt's avatar
ruiruibupt 已提交
337

ruiruibupt's avatar
init  
ruiruibupt 已提交
338 339
    // await some tasks...
    this.state.off(true)
340
    this.emit('stop')
ruiruibupt's avatar
init  
ruiruibupt 已提交
341 342
  }

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

346
    if (!this.id) {
347 348 349 350 351 352 353
      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 已提交
354 355
    }

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

359 360 361 362
    // if (!passive) {
    //   await this.bridge.WXLogout()
    // }

363
    await this.bridge.logout()
ruiruibupt's avatar
init  
ruiruibupt 已提交
364 365 366 367 368 369 370 371 372 373 374 375 376 377
  }

  /**
   *
   * 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 已提交
378
      const payload = await this.contactPayload(contactId)
ruiruibupt's avatar
ruiruibupt 已提交
379
      return payload.alias || ''
ruiruibupt's avatar
init  
ruiruibupt 已提交
380
    }
ruiruibupt's avatar
ruiruibupt 已提交
381

382 383 384 385
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
388 389 390
    return
  }

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

394 395 396 397
    if (!this.bridge) {
      throw new Error('no bridge')
    }

ruiruibupt's avatar
ruiruibupt 已提交
398
    const contactIdList = this.bridge.getContactIdList()
ruiruibupt's avatar
ruiruibupt 已提交
399 400 401 402

    return contactIdList
  }

403 404
  public async contactAvatar(contactId: string)                : Promise<FileBox>
  public async contactAvatar(contactId: string, file: FileBox) : Promise<void>
ruiruibupt's avatar
init  
ruiruibupt 已提交
405

406 407 408 409 410 411 412 413 414 415
  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')
      }
416 417 418
      if (!this.bridge) {
        throw new Error('no bridge')
      }
419 420 421 422 423 424 425
      await this.bridge.WXSetHeadImage(await file.toBase64())
      return
    }

    /**
     * 2. get avatar
     */
ruiruibupt's avatar
ruiruibupt 已提交
426 427 428 429 430 431
    const payload = await this.contactPayload(contactId)

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

432 433 434 435
    const fileBox = FileBox.fromUrl(payload.avatar)
    return fileBox
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
436
  public async contactQrcode(contactId: string): Promise<string> {
437 438 439
    if (contactId !== this.selfId()) {
      throw new Error('can not set avatar for others')
    }
440 441 442
    if (!this.bridge) {
      throw new Error('no bridge')
    }
443
    const base64 = await this.bridge.WXGetUserQRCode(contactId, 0)
444
    const qrcode = await fileBoxToQrcode(base64)
445
    return qrcode
ruiruibupt's avatar
init  
ruiruibupt 已提交
446 447
  }

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

451 452 453
    if (!this.bridge) {
      throw new Error('no bridge')
    }
454
    const rawPayload = await this.bridge.contactRawPayload(contactId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
455 456 457
    return rawPayload
  }

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

461
    const payload: ContactPayload = contactRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
462 463 464 465 466 467 468 469
    return payload
  }

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

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

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

ruiruibupt's avatar
ruiruibupt 已提交
476 477
    // TODO

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
478 479 480 481 482 483 484 485 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
    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 已提交
517
    const base64 = 'cRH9qeL3XyVnaXJkppBuH20tf5JlcG9uFX1lL2IvdHRRRS9kMMQxOPLKNYIzQQ=='
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
518
    const filename = 'test-' + messageId + '.txt'
ruiruibupt's avatar
ruiruibupt 已提交
519

520
    const file = FileBox.fromBase64(
ruiruibupt's avatar
ruiruibupt 已提交
521 522 523 524 525 526 527
      base64,
      filename,
    )

    return file
  }

528
  public async messageRawPayload(id: string): Promise<PadchatMessagePayload> {
529 530 531 532 533
    // throw Error('should not call messageRawPayload: ' + id)

    /**
     * Issue #1249
     */
ruiruibupt's avatar
ruiruibupt 已提交
534 535 536 537 538

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

540
    const rawPayload = this.cachePadchatMessagePayload.get(id)
541 542 543 544 545 546

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

    return rawPayload
ruiruibupt's avatar
init  
ruiruibupt 已提交
547 548
  }

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
554
    log.silly('PuppetPadchat', 'messagePayload(%s)', JSON.stringify(payload))
ruiruibupt's avatar
init  
ruiruibupt 已提交
555 556 557 558 559 560 561
    return payload
  }

  public async messageSendText(
    receiver : Receiver,
    text     : string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
562
    log.verbose('PuppetPadchat', 'messageSend(%s, %s)', JSON.stringify(receiver), text)
ruiruibupt's avatar
ruiruibupt 已提交
563 564
    const id = receiver.contactId || receiver.roomId
    if (!id) {
565
      throw Error('no id')
ruiruibupt's avatar
ruiruibupt 已提交
566
    }
567 568 569
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
ruiruibupt 已提交
570
    await this.bridge.WXSendMsg(id, text)
ruiruibupt's avatar
init  
ruiruibupt 已提交
571 572 573 574 575 576
  }

  public async messageSendFile(
    receiver : Receiver,
    file     : FileBox,
  ): Promise<void> {
577
    log.verbose('PuppetPadchat', 'messageSend("%s", %s)', JSON.stringify(receiver), file)
ruiruibupt's avatar
ruiruibupt 已提交
578 579 580 581 582 583

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

584 585 586 587
    if (!this.bridge) {
      throw new Error('no bridge')
    }

588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605
    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 已提交
606 607
  }

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

638 639 640 641
    if (payload.type === MessageType.Text) {
      if (!payload.text) {
        throw new Error('no text')
      }
ruiruibupt's avatar
ruiruibupt 已提交
642 643
      await this.messageSendText(
        receiver,
644
        payload.text,
ruiruibupt's avatar
ruiruibupt 已提交
645 646 647 648
      )
    } else {
      await this.messageSendFile(
        receiver,
649
        await this.messageFile(messageId),
ruiruibupt's avatar
ruiruibupt 已提交
650 651
      )
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
652 653 654 655 656 657 658
  }

  /**
   *
   * Room
   *
   */
659 660 661 662 663
  public async roomMemberRawPayload(
    roomId    : string,
    contactId : string,
  ): Promise<PadchatRoomMemberPayload> {
    log.silly('PuppetPadchat', 'roomMemberRawPayload(%s)', roomId)
ruiruibupt's avatar
ruiruibupt 已提交
664

665 666 667 668
    if (!this.bridge) {
      throw new Error('no bridge')
    }

669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689
    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)

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

694
    const rawPayload = await this.bridge.roomRawPayload(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
695 696 697
    return rawPayload
  }

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

701 702
    // const memberIdList = await this.bridge.getRoomMemberIdList()
    //  WXGetChatRoomMember(rawPayload.user_name)
703

704
    const payload: RoomPayload = roomRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
705 706 707 708

    return payload
  }

709 710 711
  public async roomMemberList(roomId: string): Promise<string[]> {
    log.verbose('PuppetPadchat', 'roomMemberList(%s)', roomId)

712 713 714 715
    if (!this.bridge) {
      throw new Error('no bridge')
    }

716 717 718 719 720 721
    const memberIdList = await this.bridge.getRoomMemberIdList(roomId)
    log.silly('PuppetPadchat', 'roomMemberList()=%d', memberIdList.length)

    return memberIdList
  }

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

725 726 727 728
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
ruiruibupt 已提交
732
    return roomIdList
ruiruibupt's avatar
init  
ruiruibupt 已提交
733 734 735 736 737 738 739
  }

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

741 742 743 744
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
749
  public async roomQrcode(roomId: string): Promise<string> {
750 751 752 753 754 755 756 757
    log.verbose('PuppetPadchat', 'roomQrCode(%s)', roomId)

    // TODO

    throw new Error('not support')

  }

758 759 760 761 762 763 764 765 766 767 768 769 770
  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 已提交
771 772 773 774 775
  public async roomAdd(
    roomId    : string,
    contactId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'roomAdd(%s, %s)', roomId, contactId)
776

777 778 779 780
    if (!this.bridge) {
      throw new Error('no bridge')
    }

781 782 783 784 785 786 787 788 789 790 791 792
    // 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 已提交
793 794
  }

795 796 797
  public async roomTopic(roomId: string)                : Promise<string>
  public async roomTopic(roomId: string, topic: string) : Promise<void>

ruiruibupt's avatar
init  
ruiruibupt 已提交
798 799 800 801 802 803 804
  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 已提交
805
      const payload = await this.roomPayload(roomId)
ruiruibupt's avatar
ruiruibupt 已提交
806
      return payload.topic
ruiruibupt's avatar
init  
ruiruibupt 已提交
807
    }
ruiruibupt's avatar
ruiruibupt 已提交
808

809 810 811 812
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
815 816 817 818 819 820 821 822 823
    return
  }

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

824 825 826 827
    if (!this.bridge) {
      throw new Error('no bridge')
    }

828 829 830 831 832
    // FIXME:
    const roomId = this.bridge.WXCreateChatRoom(contactIdList)
    console.log('roomCreate returl:', roomId)

    return roomId
ruiruibupt's avatar
init  
ruiruibupt 已提交
833 834 835 836
  }

  public async roomQuit(roomId: string): Promise<void> {
    log.verbose('PuppetPadchat', 'roomQuit(%s)', roomId)
837 838 839 840 841

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

ruiruibupt's avatar
ruiruibupt 已提交
842
    await this.bridge.WXQuitChatRoom(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
843 844
  }

845 846 847 848 849
  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 : '')
850 851 852 853 854

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

855 856 857 858 859 860 861
    if (text) {
      await this.bridge.WXSetChatroomAnnouncement(roomId, text)
    } else {
      return await this.bridge.WXGetChatroomAnnouncement(roomId)
    }
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
862 863 864 865 866 867 868 869 870 871
  /**
   *
   * FriendRequest
   *
   */
  public async friendRequestSend(
    contactId : string,
    hello     : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'friendRequestSend(%s, %s)', contactId, hello)
ruiruibupt's avatar
ruiruibupt 已提交
872 873 874

    const rawPayload = await this.contactRawPayload(contactId)

875 876 877 878 879
    // XXX
    console.log('rawPayload.stranger: ', rawPayload)

    // let strangerV1
    // let strangerV2
880
    // if (isStrangerV1(rawPayload.stranger)) {
881
    //   strangerV1 = rawPayload.stranger
882
    // } else if (isStrangerV2(rawPayload.stranger)) {
883 884 885 886
    //   strangerV2 = rawPayload.stranger
    // } else {
    //   throw new Error('stranger neither v1 nor v2!')
    // }
ruiruibupt's avatar
ruiruibupt 已提交
887

888 889 890 891
    if (!this.bridge) {
      throw new Error('no bridge')
    }

892 893
    // Issue #1252 : what's wrong here?

ruiruibupt's avatar
ruiruibupt 已提交
894
    await this.bridge.WXAddUser(
895 896
      rawPayload.stranger || '',
      rawPayload.ticket   || '',
ruiruibupt's avatar
ruiruibupt 已提交
897 898 899
      '14',
      hello,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
900 901 902
  }

  public async friendRequestAccept(
903
    friendRequestId : string,
ruiruibupt's avatar
init  
ruiruibupt 已提交
904
  ): Promise<void> {
905
    log.verbose('PuppetPadchat', 'friendRequestAccept(%s)', friendRequestId)
ruiruibupt's avatar
ruiruibupt 已提交
906

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

909 910
    console.log('friendRequestAccept: ', payload)

911 912 913
    if (!payload.ticket) {
      throw new Error('no ticket')
    }
914 915 916
    if (!payload.stranger) {
      throw new Error('no stranger')
    }
ruiruibupt's avatar
ruiruibupt 已提交
917

918 919 920 921
    if (!this.bridge) {
      throw new Error('no bridge')
    }

922
    await this.bridge.WXAcceptUser(
923
      payload.stranger,
924 925
      payload.ticket,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
926 927
  }

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

931
    const payload: FriendRequestPayload = await friendRequestRawPayloadParser(rawPayload)
932
    return payload
ruiruibupt's avatar
ruiruibupt 已提交
933 934
  }

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

938 939
    const rawPayload = this.cachePadchatFriendRequestPayload.get(friendRequestId)
    if (!rawPayload) {
940
      throw new Error('no rawPayload for id ' + friendRequestId)
941
    }
ruiruibupt's avatar
ruiruibupt 已提交
942

943
    return rawPayload
ruiruibupt's avatar
ruiruibupt 已提交
944 945
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
946 947 948
}

export default PuppetPadchat