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

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

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

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

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

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

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

  Puppet,
  PuppetOptions,
42

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

45 46
  FriendshipPayload,
  FriendshipPayloadReceive,
47 48

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

51
import {
52
  contactRawPayloadParser,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
53

54
  fileBoxToQrcode,
55
  // friendRequestEventMessageParser,
56
  friendshipRawPayloadParser,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57 58 59 60

  isStrangerV1,
  isStrangerV2,

61
  messageRawPayloadParser,
62 63 64

  roomRawPayloadParser,

65 66 67
  roomJoinEventMessageParser,
  roomLeaveEventMessageParser,
  roomTopicEventMessageParser,
68 69 70 71

  friendshipConfirmEventMessageParser,
  friendshipReceiveEventMessageParser,
  friendshipVerifyEventMessageParser,
72
}                                         from './pure-function-helpers'
73

ruiruibupt's avatar
init  
ruiruibupt 已提交
74 75
import {
  log,
76 77
  qrCodeForChatie,
}                   from '../config'
ruiruibupt's avatar
init  
ruiruibupt 已提交
78

79
import {
80
  padchatToken,
81 82 83
  WECHATY_PUPPET_PADCHAT_ENDPOINT,
}                                   from './config'

ruiruibupt's avatar
init  
ruiruibupt 已提交
84
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
85
  PadchatManager,
86
  // resolverDict,
ruiruibupt's avatar
ruiruibupt 已提交
87
  // AutoDataType,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
88
}                       from './padchat-manager'
ruiruibupt's avatar
init  
ruiruibupt 已提交
89 90

import {
91
  // PadchatPayload,
92
  PadchatContactPayload,
93
  PadchatMessagePayload,
94
  PadchatRoomPayload,
95 96
  // PadchatRoomMemberListPayload,
  PadchatRoomMemberPayload,
97
  PadchatMessageType,
98

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

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

export class PuppetPadchat extends Puppet {
115

116 117 118
  // in seconds, 4 minute for padchat
  protected [WATCHDOG_TIMEOUT] = 4 * 60

119
  // private readonly cachePadchatContactPayload       : LRU.Cache<string, PadchatContactRawPayload>
120 121
  private readonly cachePadchatFriendshipPayload : LRU.Cache<string, PadchatMessagePayload>
  private readonly cachePadchatMessagePayload    : LRU.Cache<string, PadchatMessagePayload>
122
  // private readonly cachePadchatRoomPayload          : LRU.Cache<string, PadchatRoomRawPayload>
123

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
124
  public bridge?:  PadchatManager
ruiruibupt's avatar
init  
ruiruibupt 已提交
125 126 127 128 129 130

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

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

140
    // this.cachePadchatContactPayload       = new LRU<string, PadchatContactRawPayload>(lruOptions)
141
    this.cachePadchatFriendshipPayload = new LRU<string, PadchatMessagePayload>(lruOptions)
142
    this.cachePadchatMessagePayload       = new LRU<string, PadchatMessagePayload>(lruOptions)
143
    // this.cachePadchatRoomPayload          = new LRU<string, PadchatRoomRawPayload>(lruOptions)
ruiruibupt's avatar
init  
ruiruibupt 已提交
144 145 146
  }

  public toString() {
147
    return `PuppetPadchat<${this.options.memory.name}>`
ruiruibupt's avatar
init  
ruiruibupt 已提交
148 149 150 151 152 153
  }

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

ruiruibupt's avatar
ruiruibupt 已提交
154
  public startWatchdog(): void {
ruiruibupt's avatar
ruiruibupt 已提交
155
    log.verbose('PuppetPadchat', 'initWatchdogForPuppet()')
ruiruibupt's avatar
init  
ruiruibupt 已提交
156

157 158 159
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
160

ruiruibupt's avatar
ruiruibupt 已提交
161 162 163
    // clean the dog because this could be re-inited
    this.watchdog.removeAllListeners()

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
177
    this.watchdog.on('reset', async (food, timeout) => {
178 179 180 181 182
      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 (李卓桓) 已提交
183
    })
ruiruibupt's avatar
ruiruibupt 已提交
184 185 186 187 188

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

ruiruibupt's avatar
ruiruibupt 已提交
189
  }
ruiruibupt's avatar
init  
ruiruibupt 已提交
190 191

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

194 195 196 197 198 199
    if (this.state.on()) {
      log.warn('PuppetPadchat', 'start() already on(pending)?')
      await this.state.ready('on')
      return
    }

ruiruibupt's avatar
ruiruibupt 已提交
200 201 202 203 204 205 206
    /**
     * state has two main state: ON / OFF
     * ON (pending)
     * OFF (pending)
     */
    this.state.on('pending')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
207
    const bridge = this.bridge = new PadchatManager({
208 209 210 211 212 213
      memory   : this.options.memory,
      token    : padchatToken(),
      endpoint : WECHATY_PUPPET_PADCHAT_ENDPOINT,
    })

    await this.startBridge(bridge)
ruiruibupt's avatar
ruiruibupt 已提交
214
    await this.startWatchdog()
ruiruibupt's avatar
ruiruibupt 已提交
215

ruiruibupt's avatar
ruiruibupt 已提交
216
    this.state.on(true)
217
    this.emit('start')
218
  }
ruiruibupt's avatar
init  
ruiruibupt 已提交
219

220
  protected async login(selfId: string): Promise<void> {
221 222 223
    if (!this.bridge) {
      throw new Error('no bridge')
    }
224 225
    await super.login(selfId)
    this.bridge.syncContactsAndRooms()
ruiruibupt's avatar
init  
ruiruibupt 已提交
226 227
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
228
  public async startBridge(bridge: PadchatManager): Promise<void> {
ruiruibupt's avatar
ruiruibupt 已提交
229
    log.verbose('PuppetPadchat', 'startBridge()')
ruiruibupt's avatar
ruiruibupt 已提交
230 231

    if (this.state.off()) {
232
      throw new Error('startBridge() state is off')
ruiruibupt's avatar
ruiruibupt 已提交
233 234
    }

235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
    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 已提交
257 258 259

  }

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

268 269
    switch (rawPayload.sub_type) {
      case PadchatMessageType.VerifyMsg:
270
        this.cachePadchatFriendshipPayload.set(
271 272 273
          rawPayload.msg_id,
          rawPayload,
        )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
274
        this.emit('friendship', rawPayload.msg_id)
275 276
        break

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

325 326
  protected async onPadchatMessageRoomEvent(rawPayload: PadchatMessagePayload): Promise<void> {
    log.verbose('PuppetPadchat', 'onPadchatMessageRoomEvent({id=%s})')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
    /**
     * 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) {
347
        log.warn('PuppetPadchat', 'onPadchatMessageRoomEvent() case PadchatMesssageSys: inviterId found more than 1, use the first one.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
      }
      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)
    }
401 402 403 404
  }

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

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
426
  public async stop(): Promise<void> {
427 428 429 430 431
    log.verbose('PuppetPadchat', 'stop()')

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

    if (this.state.off()) {
434
      log.warn('PuppetPadchat', 'stop() is called on a OFF puppet. await ready(off) and return.')
ruiruibupt's avatar
init  
ruiruibupt 已提交
435 436 437 438 439
      await this.state.ready('off')
      return
    }

    this.state.off('pending')
ruiruibupt's avatar
ruiruibupt 已提交
440 441 442

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

444
    setImmediate(() => this.bridge && this.bridge.removeAllListeners())
ruiruibupt's avatar
ruiruibupt 已提交
445
    await this.bridge.stop()
ruiruibupt's avatar
ruiruibupt 已提交
446

ruiruibupt's avatar
init  
ruiruibupt 已提交
447 448
    // await some tasks...
    this.state.off(true)
449
    this.emit('stop')
ruiruibupt's avatar
init  
ruiruibupt 已提交
450 451
  }

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

455
    if (!this.id) {
456 457 458 459 460 461 462
      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 已提交
463 464
    }

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

468 469 470 471
    // if (!passive) {
    //   await this.bridge.WXLogout()
    // }

472
    await this.bridge.logout()
ruiruibupt's avatar
init  
ruiruibupt 已提交
473 474 475 476 477 478 479 480 481 482 483 484 485 486
  }

  /**
   *
   * 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 已提交
487
      const payload = await this.contactPayload(contactId)
ruiruibupt's avatar
ruiruibupt 已提交
488
      return payload.alias || ''
ruiruibupt's avatar
init  
ruiruibupt 已提交
489
    }
ruiruibupt's avatar
ruiruibupt 已提交
490

491 492 493 494
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
497 498 499
    return
  }

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

503 504 505 506
    if (!this.bridge) {
      throw new Error('no bridge')
    }

ruiruibupt's avatar
ruiruibupt 已提交
507
    const contactIdList = this.bridge.getContactIdList()
ruiruibupt's avatar
ruiruibupt 已提交
508 509 510 511

    return contactIdList
  }

512 513
  public async contactAvatar(contactId: string)                : Promise<FileBox>
  public async contactAvatar(contactId: string, file: FileBox) : Promise<void>
ruiruibupt's avatar
init  
ruiruibupt 已提交
514

515
  public async contactAvatar(contactId: string, file?: FileBox): Promise<void | FileBox> {
516 517 518 519
    log.verbose('PuppetPadchat', 'contactAvatar(%s%s)',
                                  contactId,
                                  file ? (', ' + file.name) : '',
                )
520 521 522 523 524 525 526 527

    /**
     * 1. set avatar for user self
     */
    if (file) {
      if (contactId !== this.selfId()) {
        throw new Error('can not set avatar for others')
      }
528 529 530
      if (!this.bridge) {
        throw new Error('no bridge')
      }
531 532 533 534 535 536 537
      await this.bridge.WXSetHeadImage(await file.toBase64())
      return
    }

    /**
     * 2. get avatar
     */
ruiruibupt's avatar
ruiruibupt 已提交
538 539 540 541 542 543
    const payload = await this.contactPayload(contactId)

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

544 545 546 547
    const fileBox = FileBox.fromUrl(payload.avatar)
    return fileBox
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
548
  public async contactQrcode(contactId: string): Promise<string> {
549 550 551
    if (contactId !== this.selfId()) {
      throw new Error('can not set avatar for others')
    }
552 553 554
    if (!this.bridge) {
      throw new Error('no bridge')
    }
555
    const base64 = await this.bridge.WXGetUserQRCode(contactId, 0)
556
    const qrcode = await fileBoxToQrcode(base64)
557
    return qrcode
ruiruibupt's avatar
init  
ruiruibupt 已提交
558 559
  }

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

563 564 565
    if (!this.bridge) {
      throw new Error('no bridge')
    }
566
    const rawPayload = await this.bridge.contactRawPayload(contactId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
567 568 569
    return rawPayload
  }

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

573
    const payload: ContactPayload = contactRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
574 575 576 577 578 579 580 581
    return payload
  }

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

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

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

ruiruibupt's avatar
ruiruibupt 已提交
588 589
    // TODO

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
    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 已提交
629
    const base64 = 'cRH9qeL3XyVnaXJkppBuH20tf5JlcG9uFX1lL2IvdHRRRS9kMMQxOPLKNYIzQQ=='
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
630
    const filename = 'test-' + messageId + '.txt'
ruiruibupt's avatar
ruiruibupt 已提交
631

632
    const file = FileBox.fromBase64(
ruiruibupt's avatar
ruiruibupt 已提交
633 634 635 636 637 638 639
      base64,
      filename,
    )

    return file
  }

640
  public async messageRawPayload(id: string): Promise<PadchatMessagePayload> {
641 642 643 644 645
    // throw Error('should not call messageRawPayload: ' + id)

    /**
     * Issue #1249
     */
ruiruibupt's avatar
ruiruibupt 已提交
646 647 648 649 650

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

652
    const rawPayload = this.cachePadchatMessagePayload.get(id)
653 654 655 656 657 658

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

    return rawPayload
ruiruibupt's avatar
init  
ruiruibupt 已提交
659 660
  }

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
666
    log.silly('PuppetPadchat', 'messagePayload(%s)', JSON.stringify(payload))
ruiruibupt's avatar
init  
ruiruibupt 已提交
667 668 669 670 671 672 673
    return payload
  }

  public async messageSendText(
    receiver : Receiver,
    text     : string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
674
    log.verbose('PuppetPadchat', 'messageSend(%s, %s)', JSON.stringify(receiver), text)
ruiruibupt's avatar
ruiruibupt 已提交
675 676
    const id = receiver.contactId || receiver.roomId
    if (!id) {
677
      throw Error('no id')
ruiruibupt's avatar
ruiruibupt 已提交
678
    }
679 680 681
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
ruiruibupt 已提交
682
    await this.bridge.WXSendMsg(id, text)
ruiruibupt's avatar
init  
ruiruibupt 已提交
683 684 685 686 687 688
  }

  public async messageSendFile(
    receiver : Receiver,
    file     : FileBox,
  ): Promise<void> {
689
    log.verbose('PuppetPadchat', 'messageSend("%s", %s)', JSON.stringify(receiver), file)
ruiruibupt's avatar
ruiruibupt 已提交
690 691 692 693 694 695

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

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

700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717
    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 已提交
718 719
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
  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 已提交
740 741 742 743 744
  public async messageForward(
    receiver  : Receiver,
    messageId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'messageForward(%s, %s)',
745
                              JSON.stringify(receiver),
ruiruibupt's avatar
init  
ruiruibupt 已提交
746 747
                              messageId,
              )
748
    const payload = await this.messagePayload(messageId)
ruiruibupt's avatar
ruiruibupt 已提交
749

750 751 752 753
    if (payload.type === MessageType.Text) {
      if (!payload.text) {
        throw new Error('no text')
      }
ruiruibupt's avatar
ruiruibupt 已提交
754 755
      await this.messageSendText(
        receiver,
756
        payload.text,
ruiruibupt's avatar
ruiruibupt 已提交
757 758 759 760
      )
    } else {
      await this.messageSendFile(
        receiver,
761
        await this.messageFile(messageId),
ruiruibupt's avatar
ruiruibupt 已提交
762 763
      )
    }
ruiruibupt's avatar
init  
ruiruibupt 已提交
764 765 766 767 768 769 770
  }

  /**
   *
   * Room
   *
   */
771 772 773 774 775
  public async roomMemberRawPayload(
    roomId    : string,
    contactId : string,
  ): Promise<PadchatRoomMemberPayload> {
    log.silly('PuppetPadchat', 'roomMemberRawPayload(%s)', roomId)
ruiruibupt's avatar
ruiruibupt 已提交
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 793 794 795 796 797 798 799 800 801
    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)

802 803 804 805
    if (!this.bridge) {
      throw new Error('no bridge')
    }

806
    const rawPayload = await this.bridge.roomRawPayload(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
807 808 809
    return rawPayload
  }

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

813 814
    // const memberIdList = await this.bridge.getRoomMemberIdList()
    //  WXGetChatRoomMember(rawPayload.user_name)
815

816
    const payload: RoomPayload = roomRawPayloadParser(rawPayload)
ruiruibupt's avatar
init  
ruiruibupt 已提交
817 818 819 820

    return payload
  }

821 822 823
  public async roomMemberList(roomId: string): Promise<string[]> {
    log.verbose('PuppetPadchat', 'roomMemberList(%s)', roomId)

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

828 829 830 831 832 833
    const memberIdList = await this.bridge.getRoomMemberIdList(roomId)
    log.silly('PuppetPadchat', 'roomMemberList()=%d', memberIdList.length)

    return memberIdList
  }

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

837 838 839 840
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
ruiruibupt 已提交
844
    return roomIdList
ruiruibupt's avatar
init  
ruiruibupt 已提交
845 846 847 848 849 850 851
  }

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
861
  public async roomQrcode(roomId: string): Promise<string> {
862 863 864 865 866 867 868 869
    log.verbose('PuppetPadchat', 'roomQrCode(%s)', roomId)

    // TODO

    throw new Error('not support')

  }

870 871 872 873 874 875 876 877 878 879 880 881 882
  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 已提交
883 884 885 886 887
  public async roomAdd(
    roomId    : string,
    contactId : string,
  ): Promise<void> {
    log.verbose('PuppetPadchat', 'roomAdd(%s, %s)', roomId, contactId)
888

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

893 894 895 896 897 898 899 900 901 902 903 904
    // 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 已提交
905 906
  }

907 908 909
  public async roomTopic(roomId: string)                : Promise<string>
  public async roomTopic(roomId: string, topic: string) : Promise<void>

ruiruibupt's avatar
init  
ruiruibupt 已提交
910 911 912 913 914 915 916
  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 已提交
917
      const payload = await this.roomPayload(roomId)
ruiruibupt's avatar
ruiruibupt 已提交
918
      return payload.topic
ruiruibupt's avatar
init  
ruiruibupt 已提交
919
    }
ruiruibupt's avatar
ruiruibupt 已提交
920

921 922 923 924
    if (!this.bridge) {
      throw new Error('no bridge')
    }

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

ruiruibupt's avatar
init  
ruiruibupt 已提交
927 928 929 930 931 932 933 934 935
    return
  }

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

936 937 938 939
    if (!this.bridge) {
      throw new Error('no bridge')
    }

940 941 942 943 944
    // FIXME:
    const roomId = this.bridge.WXCreateChatRoom(contactIdList)
    console.log('roomCreate returl:', roomId)

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

  public async roomQuit(roomId: string): Promise<void> {
    log.verbose('PuppetPadchat', 'roomQuit(%s)', roomId)
949 950 951 952 953

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

ruiruibupt's avatar
ruiruibupt 已提交
954
    await this.bridge.WXQuitChatRoom(roomId)
ruiruibupt's avatar
init  
ruiruibupt 已提交
955 956
  }

957 958 959 960 961
  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 : '')
962 963 964 965 966

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

967 968 969 970 971 972 973
    if (text) {
      await this.bridge.WXSetChatroomAnnouncement(roomId, text)
    } else {
      return await this.bridge.WXGetChatroomAnnouncement(roomId)
    }
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
974 975
  /**
   *
976
   * Friendship
ruiruibupt's avatar
init  
ruiruibupt 已提交
977 978
   *
   */
979
  public async friendshipVerify(
ruiruibupt's avatar
init  
ruiruibupt 已提交
980 981 982
    contactId : string,
    hello     : string,
  ): Promise<void> {
983
    log.verbose('PuppetPadchat', 'friendshipVerify(%s, %s)', contactId, hello)
ruiruibupt's avatar
ruiruibupt 已提交
984

E
Egg 已提交
985 986 987
    if (!this.bridge) {
      throw new Error('no bridge')
    }
ruiruibupt's avatar
ruiruibupt 已提交
988

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

E
Egg 已提交
991 992 993
    /**
     * If the contact is not stranger, than ussing WXSearchContact can get user_name
     */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
994
    if (rawSearchPayload.user_name !== '' && !isStrangerV1(rawSearchPayload.user_name) && !isStrangerV2(rawSearchPayload.user_name)) {
E
Egg 已提交
995 996 997
      log.warn('PuppetPadchat', 'friendRequestSend %s has been friend with bot, no need to send friend request!', contactId)
      return
    }
ruiruibupt's avatar
ruiruibupt 已提交
998

E
Egg 已提交
999 1000
    let strangerV1
    let strangerV2
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1001
    if (isStrangerV1(rawSearchPayload.stranger)) {
E
Egg 已提交
1002 1003
      strangerV1 = rawSearchPayload.stranger
      strangerV2 = rawSearchPayload.user_name
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1004
    } else if (isStrangerV2(rawSearchPayload.stranger)) {
E
Egg 已提交
1005 1006 1007 1008
      strangerV2 = rawSearchPayload.stranger
      strangerV1 = rawSearchPayload.user_name
    } else {
      throw new Error('stranger neither v1 nor v2!')
1009 1010
    }

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

ruiruibupt's avatar
ruiruibupt 已提交
1013
    await this.bridge.WXAddUser(
E
Egg 已提交
1014 1015 1016
      strangerV1 || '',
      strangerV2 || '',
      WXSearchContactTypeStatus.WXID, // default
ruiruibupt's avatar
ruiruibupt 已提交
1017 1018
      hello,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
1019 1020
  }

1021 1022
  public async friendshipAccept(
    friendshipId : string,
ruiruibupt's avatar
init  
ruiruibupt 已提交
1023
  ): Promise<void> {
1024
    log.verbose('PuppetPadchat', 'friendshipAccept(%s)', friendshipId)
ruiruibupt's avatar
ruiruibupt 已提交
1025

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

1028
    console.log('friendshipAccept: ', payload)
1029

1030 1031 1032
    if (!payload.ticket) {
      throw new Error('no ticket')
    }
1033 1034 1035
    if (!payload.stranger) {
      throw new Error('no stranger')
    }
ruiruibupt's avatar
ruiruibupt 已提交
1036

1037 1038 1039 1040
    if (!this.bridge) {
      throw new Error('no bridge')
    }

1041
    await this.bridge.WXAcceptUser(
1042
      payload.stranger,
1043 1044
      payload.ticket,
    )
ruiruibupt's avatar
init  
ruiruibupt 已提交
1045 1046
  }

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

1050
    const payload: FriendshipPayload = await friendshipRawPayloadParser(rawPayload)
1051
    return payload
ruiruibupt's avatar
ruiruibupt 已提交
1052 1053
  }

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

1057
    const rawPayload = this.cachePadchatFriendshipPayload.get(friendshipId)
1058
    if (!rawPayload) {
1059
      throw new Error('no rawPayload for id ' + friendshipId)
1060
    }
ruiruibupt's avatar
ruiruibupt 已提交
1061

1062
    return rawPayload
ruiruibupt's avatar
ruiruibupt 已提交
1063 1064
  }

ruiruibupt's avatar
init  
ruiruibupt 已提交
1065 1066 1067
}

export default PuppetPadchat