puppet-puppeteer.ts 48.6 KB
Newer Older
1
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
2
 *   Wechaty - https://github.com/chatie/wechaty
3
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4
 *   @copyright 2016-2018 Huan LI <zixia@zixia.net>
5
 *
6 7 8
 *   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
9
 *
10
 *       http://www.apache.org/licenses/LICENSE-2.0
11
 *
12 13 14 15 16
 *   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.
17
 *
18
 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
19 20 21 22 23 24 25
// import * as http    from 'http'
import * as path    from 'path'
import * as nodeUrl from 'url'

import * as bl      from 'bl'
import * as mime  from 'mime'
import * as request from 'request'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
26

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
27
// import cloneClass   from 'clone-class'
28 29 30
import {
  FileBox,
}                   from 'file-box'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
31 32 33
import {
  ThrottleQueue,
}                   from 'rx-queue'
34 35 36 37 38
import {
  Watchdog,
  WatchdogFood,
}                   from 'watchdog'

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
39 40 41
import {
  Puppet,
  PuppetOptions,
42
  Receiver,
43
  ScanPayload,
44
}                     from '../puppet/'
45
import {
46
  config,
47 48
  log,
  Raven,
49
}                     from '../config'
50
// import Profile        from '../profile'
51 52
import Misc           from '../misc'

53 54
import {
  Bridge,
55
  Cookie,
56 57
}                       from './bridge'
import Event            from './event'
58 59

import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
60
  WebAppMsgType,
61
  WebContactRawPayload,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
62
  WebMessageMediaPayload,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
63
  WebMessageRawPayload,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
64
  WebMediaType,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
65 66 67
  WebMessageType,
  WebRoomRawMember,
  WebRoomRawPayload,
68
}                           from './web-schemas'
69

70
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
71 72 73
  Contact,
  ContactPayload,
  ContactQueryFilter,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
74
  // Gender,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
75
}                             from '../contact'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
76
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
77
  // Messageirection,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
78
  MessagePayload,
79
  MessageType,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
80
}                             from '../message'
81
import {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
82 83 84 85
  Room,
  RoomMemberQueryFilter,
  RoomPayload,
  RoomQueryFilter,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
86
}                             from '../room'
M
Mukaiu 已提交
87

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
88 89
export type PuppetFoodType = 'scan' | 'ding'
export type ScanFoodType   = 'scan' | 'login' | 'logout'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
90

91
export class PuppetPuppeteer extends Puppet {
92 93
  public bridge       : Bridge
  public scanPayload? : ScanPayload
94

95
  public scanWatchdog: Watchdog<ScanFoodType>
96

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
97
  private fileId: number
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
98

99 100 101
  constructor(
    public options: PuppetOptions,
  ) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
102
    super(options)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
103

104 105 106 107 108
    this.fileId = 0
    this.bridge = new Bridge({
      head    : config.head,
      profile : this.options.profile,
    })
109

110
    const SCAN_TIMEOUT  = 2 * 60 * 1000 // 2 minutes
111
    this.scanWatchdog   = new Watchdog<ScanFoodType>(SCAN_TIMEOUT, 'Scan')
112 113
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114
  public async start(): Promise<void> {
115
    log.verbose('PuppetPuppeteer', `start() with ${this.options.profile}`)
116

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
117
    this.state.on('pending')
118

119
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
120
      this.initWatchdog()
121
      this.initWatchdogForScan()
122

123
      this.bridge = await this.initBridge()
124
      log.verbose('PuppetPuppeteer', 'initBridge() done')
125

126 127 128 129
      /**
       *  state must set to `live`
       *  before feed Watchdog
       */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
130
      this.state.on(true)
131

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
132 133 134
      /**
       * Feed the dog and start watch
       */
135
      const food: WatchdogFood = {
L
lijiarui 已提交
136 137
        data: 'inited',
        timeout: 2 * 60 * 1000, // 2 mins for first login
138 139
      }
      this.emit('watchdog', food)
140

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
141 142 143
      /**
       * Save cookie for every 5 minutes
       */
144 145
      const throttleQueue = new ThrottleQueue(5 * 60 * 1000)
      this.on('heartbeat', data => throttleQueue.next(data))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
146
      throttleQueue.subscribe(async data => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
147
        log.verbose('Wechaty', 'start() throttleQueue.subscribe() new item: %s', data)
148 149 150
        await this.saveCookie()
      })

151
      log.verbose('PuppetPuppeteer', 'start() done')
152 153

      this.emit('start')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
155

156
    } catch (e) {
157
      log.error('PuppetPuppeteer', 'start() exception: %s', e)
158

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159
      this.state.off(true)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
      this.emit('error', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
161
      await this.stop()
162

163
      Raven.captureException(e)
164
      throw e
165
    }
166 167
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
168
  private initWatchdog(): void {
169
    log.verbose('PuppetPuppeteer', 'initWatchdogForPuppet()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170

171 172
    const puppet = this

173
    // clean the dog because this could be re-inited
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
174
    this.watchdog.removeAllListeners()
175

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
176 177 178
    // fix issue #981
    puppet.removeAllListeners('watchdog')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
179 180
    puppet.on('watchdog', food => this.watchdog.feed(food))
    this.watchdog.on('feed', food => {
181
      log.silly('PuppetPuppeteer', 'initWatchdogForPuppet() dog.on(feed, food={type=%s, data=%s})', food.type, food.data)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
182
      // feed the dog, heartbeat the puppet.
183 184 185
      puppet.emit('heartbeat', food.data)
    })

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
186
    this.watchdog.on('reset', async (food, timeout) => {
187
      log.warn('PuppetPuppeteer', 'initWatchdogForPuppet() dog.on(reset) last food:%s, timeout:%s',
188
                            food.data, timeout)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
189
      try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
190 191
        await this.stop()
        await this.start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
192 193 194
      } catch (e) {
        puppet.emit('error', e)
      }
195 196 197 198 199 200 201 202 203 204
    })
  }

  /**
   * Deal with SCAN events
   *
   * if web browser stay at login qrcode page long time,
   * sometimes the qrcode will not refresh, leave there expired.
   * so we need to refresh the page after a while
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205
  private initWatchdogForScan(): void {
206
    log.verbose('PuppetPuppeteer', 'initWatchdogForScan()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
207

208
    const puppet = this
209
    const dog    = this.scanWatchdog
210

211 212 213
    // clean the dog because this could be re-inited
    dog.removeAllListeners()

214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
    puppet.on('scan', info => dog.feed({
      data: info,
      type: 'scan',
    }))
    puppet.on('login',  user => {
      dog.feed({
        data: user,
        type: 'login',
      })
      // do not monitor `scan` event anymore
      // after user login
      dog.sleep()
    })

    // active monitor again for `scan` event
    puppet.on('logout', user => dog.feed({
      data: user,
      type: 'logout',
    }))

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
234
    dog.on('reset', async (food, timePast) => {
235
      log.warn('PuppetPuppeteer', 'initScanWatchdog() on(reset) lastFood: %s, timePast: %s',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
236
                            food.data, timePast)
237 238 239
      try {
        await this.bridge.reload()
      } catch (e) {
240
        log.error('PuppetPuppeteer', 'initScanWatchdog() on(reset) exception: %s', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
241
        try {
242
          log.error('PuppetPuppeteer', 'initScanWatchdog() on(reset) try to recover by bridge.{quit,init}()', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
243 244
          await this.bridge.quit()
          await this.bridge.init()
245
          log.error('PuppetPuppeteer', 'initScanWatchdog() on(reset) recover successful')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
246
        } catch (e) {
247
          log.error('PuppetPuppeteer', 'initScanWatchdog() on(reset) recover FAIL: %s', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
248 249
          this.emit('error', e)
        }
250 251 252 253
      }
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
254
  public async stop(): Promise<void> {
255
    log.verbose('PuppetPuppeteer', 'quit()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
256

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
257
    if (this.state.off()) {
258
      log.warn('PuppetPuppeteer', 'quit() is called on a OFF puppet. await ready(off) and return.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
259 260
      await this.state.ready('off')
      return
261
    }
262
    this.state.off('pending')
263

264
    log.verbose('PuppetPuppeteer', 'quit() make watchdog sleep before do quit')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
265
    this.watchdog.sleep()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
266
    this.scanWatchdog.sleep()
267

268
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
269 270 271
      await this.bridge.quit()
      // register the removeListeners micro task at then end of the task queue
      setImmediate(() => this.bridge.removeAllListeners())
272
    } catch (e) {
273
      log.error('PuppetPuppeteer', 'quit() exception: %s', e.message)
274
      Raven.captureException(e)
275
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
276
    } finally {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
277
      this.state.off(true)
278
    }
279 280

    this.emit('stop')
281 282
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
283
  private async initBridge(): Promise<Bridge> {
284
    log.verbose('PuppetPuppeteer', 'initBridge()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
286
    if (this.state.off()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287
      const e = new Error('initBridge() found targetState != live, no init anymore')
288
      log.warn('PuppetPuppeteer', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
289
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
290
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
291

292
    this.bridge.on('ding'     , Event.onDing.bind(this))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
293
    this.bridge.on('error'    , e => this.emit('error', e))
294 295 296 297 298
    this.bridge.on('log'      , Event.onLog.bind(this))
    this.bridge.on('login'    , Event.onLogin.bind(this))
    this.bridge.on('logout'   , Event.onLogout.bind(this))
    this.bridge.on('message'  , Event.onMessage.bind(this))
    this.bridge.on('scan'     , Event.onScan.bind(this))
299
    this.bridge.on('unload'   , Event.onUnload.bind(this))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300

301
    try {
302
      await this.bridge.init()
303
    } catch (e) {
304
      log.error('PuppetPuppeteer', 'initBridge() exception: %s', e.message)
305
      await this.bridge.quit().catch(console.error)
306 307 308 309
      this.emit('error', e)

      Raven.captureException(e)
      throw e
310
    }
311 312

    return this.bridge
313 314
  }

315 316 317
  public async messageRawPayload(id: string): Promise <WebMessageRawPayload> {
    const rawPayload = await this.bridge.getMessage(id)
    return rawPayload
318
  }
319

320
  public async messageRawPayloadParser(
321
    rawPayload: WebMessageRawPayload,
322
  ): Promise<MessagePayload> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
323
    log.verbose('PuppetPuppeteer', 'messageRawPayloadParser(%s) @ %s', rawPayload, this)
324

325 326 327 328
    const fromId                       = rawPayload.MMActualSender               // MMPeerUserName
    const text: string                 = rawPayload.MMActualContent              // Content has @id prefix added by wx
    const timestamp: number            = rawPayload.MMDisplayTime                // Javascript timestamp of milliseconds
    const filename: undefined | string = this.filename(rawPayload) || undefined
329

330 331
    let roomId : undefined | string
    let toId   : undefined | string
332 333 334 335

    // FIXME: has there any better method to know the room ID?
    if (rawPayload.MMIsChatRoom) {
      if (/^@@/.test(rawPayload.FromUserName)) {
336
        roomId = rawPayload.FromUserName // MMPeerUserName always eq FromUserName ?
337
      } else if (/^@@/.test(rawPayload.ToUserName)) {
338
        roomId = rawPayload.ToUserName
339 340 341
      } else {
        throw new Error('parse found a room message, but neither FromUserName nor ToUserName is a room(/^@@/)')
      }
M
Mukaiu 已提交
342 343
    }

344 345
    if (rawPayload.ToUserName) {
      if (!/^@@/.test(rawPayload.ToUserName)) { // if a message in room without any specific receiver, then it will set to be `undefined`
346
        toId = rawPayload.ToUserName
347
      }
348 349
    }

350
    const type: MessageType = this.messageTypeFromWeb(rawPayload.MsgType)
351

352 353
    const payload: MessagePayload = {
      type,
354
      fromId,
355
      filename,
356 357
      toId,
      roomId,
358
      text,
359
      timestamp,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
360
    }
M
Mukaiu 已提交
361

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
362 363 364
    return payload
  }

365 366 367 368 369 370
  public async messageFile(messageId: string): Promise<FileBox> {
    const rawPayload = await this.messageRawPayload(messageId)
    const fileBox = await this.messageRawPayloadToFile(rawPayload)
    return fileBox
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
371
  private async messageRawPayloadToFile(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419
    rawPayload: WebMessageRawPayload,
  ): Promise<FileBox> {
    let url = await this.messageRawPayloadToUrl(rawPayload)

    if (!url) {
      throw new Error('no url for type ' + MessageType[rawPayload.MsgType])
    }

    url = url.replace(/^https/i, 'http') // use http instead of https, because https will only success on the very first request!
    const parsedUrl = nodeUrl.parse(url)

    const filename = this.filename(rawPayload)

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

    const cookies = await this.cookies()

    const headers = {
      'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36',

      // Accept: 'image/webp,image/*,*/*;q=0.8',
      // Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', //  MsgType.IMAGE | VIDEO
      Accept: '*/*',

      Host: parsedUrl.hostname!, // 'wx.qq.com',  // MsgType.VIDEO | IMAGE

      // Referer: protocol + '//wx.qq.com/',
      Referer: url,

      // 'Upgrade-Insecure-Requests': 1, // MsgType.VIDEO | IMAGE

      Range: 'bytes=0-',

      // 'Accept-Encoding': 'gzip, deflate, sdch',
      // 'Accept-Encoding': 'gzip, deflate, sdch, br', // MsgType.IMAGE | VIDEO
      'Accept-Encoding': 'identity;q=1, *;q=0',

      'Accept-Language': 'zh-CN,zh;q=0.8', // MsgType.IMAGE | VIDEO
      // 'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.6,en-US;q=0.4,en;q=0.2',

      /**
       * pgv_pvi=6639183872; pgv_si=s8359147520; webwx_data_ticket=gSeBbuhX+0kFdkXbgeQwr6Ck
       */
      Cookie: cookies.map(c => `${c['name']}=${c['value']}`).join('; '),
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
420
    const fileBox = FileBox.packRemote(url, filename, headers)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
421 422 423 424

    return fileBox
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
425
  private messageTypeFromWeb(webMsgType: WebMessageType): MessageType {
426
    switch (webMsgType) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
427
      case WebMessageType.TEXT:
428 429
        return MessageType.Text

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
430 431
      case WebMessageType.EMOTICON:
      case WebMessageType.IMAGE:
432 433
        return MessageType.Image

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
434
      case WebMessageType.VOICE:
435
        return MessageType.Audio
436

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
437 438
      case WebMessageType.MICROVIDEO:
      case WebMessageType.VIDEO:
439
        return MessageType.Video
440

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
441
      case WebMessageType.TEXT:
442 443
        return MessageType.Text

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
444 445 446 447 448 449
      /**
       * Treat those Types as TEXT
       *
       * FriendRequest is a SYS message
       * FIXME: should we use better message type at here???
       */
450
      case WebMessageType.SYS:
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
451
      case WebMessageType.APP:
452
        return MessageType.Text
453 454 455 456 457 458 459 460 461 462 463 464

      // VERIFYMSG           = 37,
      // POSSIBLEFRIEND_MSG  = 40,
      // SHARECARD           = 42,
      // LOCATION            = 48,
      // VOIPMSG             = 50,
      // STATUSNOTIFY        = 51,
      // VOIPNOTIFY          = 52,
      // VOIPINVITE          = 53,
      // SYSNOTICE           = 9999,
      // RECALLED            = 10002,
      default:
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
465 466
        log.warn('PuppetPuppeteer', 'messageTypeFromWeb(%d) un-supported WebMsgType, treat as TEXT', webMsgType)
        return MessageType.Text
467
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
468 469
  }

470
  /**
471
   * TODO: Test this function if it could work...
472 473
   */
  // public async forward(baseData: MsgRawObj, patchData: MsgRawObj): Promise<boolean> {
474
  public async messageForward(
475 476
    receiver  : Receiver,
    messageId : string,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
477
  ): Promise<void> {
478

479 480 481 482
    log.silly('PuppetPuppeteer', 'forward(receiver=%s, messageId=%s)',
                                  receiver,
                                  messageId,
              )
483

484
    let rawPayload = await this.messageRawPayload(messageId)
485 486

    // rawPayload = Object.assign({}, rawPayload)
487

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
488
    const newMsg = <WebMessageRawPayload>{}
489 490 491 492 493 494 495 496 497 498 499 500 501
    const largeFileSize = 25 * 1024 * 1024
    // let ret = false
    // if you know roomId or userId, you can use `Room.load(roomId)` or `Contact.load(userId)`
    // let sendToList: Contact[] = [].concat(sendTo as any || [])
    // sendToList = sendToList.filter(s => {
    //   if ((s instanceof Room || s instanceof Contact) && s.id) {
    //     return true
    //   }
    //   return false
    // }) as Contact[]
    // if (sendToList.length < 1) {
    //   throw new Error('param must be Room or Contact and array')
    // }
502
    if (rawPayload.FileSize >= largeFileSize && !rawPayload.Signature) {
503 504
      // if has RawObj.Signature, can forward the 25Mb+ file
      log.warn('MediaMessage', 'forward() Due to webWx restrictions, more than 25MB of files can not be downloaded and can not be forwarded.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
505
      throw new Error('forward() Due to webWx restrictions, more than 25MB of files can not be downloaded and can not be forwarded.')
506 507
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
508
    newMsg.FromUserName         = this.userId || ''
509
    newMsg.isTranspond          = true
510 511
    newMsg.MsgIdBeforeTranspond = rawPayload.MsgIdBeforeTranspond || rawPayload.MsgId
    newMsg.MMSourceMsgId        = rawPayload.MsgId
512
    // In room msg, the content prefix sender:, need to be removed, otherwise the forwarded sender will display the source message sender, causing self () to determine the error
513
    newMsg.Content      = Misc.unescapeHtml(rawPayload.Content.replace(/^@\w+:<br\/>/, '')).replace(/^[\w\-]+:<br\/>/, '')
514
    newMsg.MMIsChatRoom = receiver instanceof Room ? true : false
515 516 517

    // The following parameters need to be overridden after calling createMessage()

518
    rawPayload = Object.assign(rawPayload, newMsg)
519 520 521 522 523
    // for (let i = 0; i < sendToList.length; i++) {
      // newMsg.ToUserName = sendToList[i].id
      // // all call success return true
      // ret = (i === 0 ? true : ret) && await config.puppetInstance().forward(m, newMsg)
    // }
524
    newMsg.ToUserName = receiver.contactId || receiver.roomId as string
525 526
    // ret = await config.puppetInstance().forward(m, newMsg)
    // return ret
527
    const baseData  = rawPayload
528 529
    const patchData = newMsg

530
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
531 532 533 534
      const ret = await this.bridge.forward(baseData, patchData)
      if (!ret) {
        throw new Error('forward failed')
      }
535
    } catch (e) {
536
      log.error('PuppetPuppeteer', 'forward() exception: %s', e.message)
537 538 539 540 541
      Raven.captureException(e)
      throw e
    }
  }

542 543 544 545 546
  public async messageSendText(
    receiver : Receiver,
    text     : string,
  ): Promise<void> {
    log.verbose('PuppetPuppeteer', 'messageSendText(receiver=%s, text=%s)', receiver, text)
M
Mukaiu 已提交
547 548 549

    let destinationId

550 551 552 553
    if (receiver.roomId) {
      destinationId = receiver.roomId
    } else if (receiver.contactId) {
      destinationId = receiver.contactId
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
554
    } else {
555
      throw new Error('PuppetPuppeteer.messageSendText(): message with neither room nor to?')
M
Mukaiu 已提交
556 557
    }

558 559 560 561
    log.silly('PuppetPuppeteer', 'messageSendText() destination: %s, text: %s)',
                                  destinationId,
                                  text,
              )
M
Mukaiu 已提交
562

563 564 565 566 567 568 569 570
    try {
      await this.bridge.send(destinationId, text)
    } catch (e) {
      log.error('PuppetPuppeteer', 'messageSendText() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }
  }
M
Mukaiu 已提交
571

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
572
  public async login(userId: string): Promise<void> {
573
    return super.login(userId)
574 575
  }

576 577 578
  /**
   * logout from browser, then server will emit `logout` event
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
579
  public async logout(): Promise<void> {
580
    log.verbose('PuppetPuppeteer', 'logout()')
581

582
    const user = this.selfId()
583 584 585 586
    if (!user) {
      log.warn('PuppetPuppeteer', 'logout() without self()')
      return
    }
587

588 589 590
    try {
      await this.bridge.logout()
    } catch (e) {
591
      log.error('PuppetPuppeteer', 'logout() exception: %s', e.message)
592
      Raven.captureException(e)
593
      throw e
594
    } finally {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
595
      this.userId = undefined
596
      this.emit('logout', user)
597
    }
598 599
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
600 601 602 603 604
  /**
   *
   * Contact
   *
   */
605
  public async contactRawPayload(id: string): Promise<WebContactRawPayload> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
606
    log.silly('PuppetPuppeteer', 'contactRawPayload(%s) @ %s', id, this)
607 608 609 610
    try {
      const rawPayload = await this.bridge.getContact(id) as WebContactRawPayload
      return rawPayload
    } catch (e) {
611
      log.error('PuppetPuppeteer', 'contactRawPayload(%s) exception: %s', id, e.message)
612 613 614 615 616 617 618
      Raven.captureException(e)
      throw e
    }

  }

  public async contactRawPayloadParser(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
619
    rawPayload: WebContactRawPayload,
620
  ): Promise<ContactPayload> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
621
    log.silly('PuppetPuppeteer', 'contactParseRawPayload(Object.keys(payload).length=%d)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
622 623 624
                                    Object.keys(rawPayload).length,
                )
    if (!Object.keys(rawPayload).length) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
625 626 627
      log.error('PuppetPuppeteer', 'contactParseRawPayload(Object.keys(payload).length=%d)',
                                    Object.keys(rawPayload).length,
                )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
628
      log.error('PuppetPuppeteer', 'contactParseRawPayload() got empty rawPayload!')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
629 630 631 632 633
      throw new Error('empty raw payload')
      // return {
      //   gender: Gender.Unknown,
      //   type:   Contact.Type.Unknown,
      // }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
634 635 636 637 638 639 640
    }

    // this.id = rawPayload.UserName   // MMActualSender??? MMPeerUserName??? `getUserContact(message.MMActualSender,message.MMPeerUserName).HeadImgUrl`
    // uin:        rawPayload.Uin,    // stable id: 4763975 || getCookie("wxuin")

    return {
      weixin:     rawPayload.Alias,  // Wechat ID
641
      name:       Misc.plainText(rawPayload.NickName || ''),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
642
      alias:      rawPayload.RemarkName,
643
      gender:     rawPayload.Sex,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
      province:   rawPayload.Province,
      city:       rawPayload.City,
      signature:  rawPayload.Signature,

      address:    rawPayload.Alias, // XXX: need a stable address for user

      star:       !!rawPayload.StarFriend,
      friend:     rawPayload.stranger === undefined
                    ? undefined
                    : !rawPayload.stranger, // assign by injectio.js
      avatar:     rawPayload.HeadImgUrl,
      /**
       * @see 1. https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3243
       * @see 2. https://github.com/Urinx/WeixinBot/blob/master/README.md
       * @ignore
       */
      // tslint:disable-next-line
      type:      (!!rawPayload.UserName && !rawPayload.UserName.startsWith('@@') && !!(rawPayload.VerifyFlag & 8))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
662 663
                    ? Contact.Type.Official
                    : Contact.Type.Personal,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
664 665 666 667 668 669 670 671
      /**
       * @see 1. https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3246
       * @ignore
       */
      // special:       specialContactList.indexOf(rawPayload.UserName) > -1 || /@qqim$/.test(rawPayload.UserName),
    }
  }

672 673 674 675
  public async ding(data?: any): Promise<string> {
    try {
      return await this.bridge.ding(data)
    } catch (e) {
676
      log.warn('PuppetPuppeteer', 'ding(%s) rejected: %s', data, e.message)
677
      Raven.captureException(e)
678
      throw e
679 680
    }
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
681

682
  public async contactAvatar(contactId: string): Promise<FileBox> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
683
    log.verbose('PuppetPuppeteer', 'contactAvatar(%s)', contactId)
684
    const payload = await this.contactPayload(contactId)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
685 686 687 688 689 690 691
    if (!payload.avatar) {
      throw new Error('Can not get avatar: no payload.avatar!')
    }

    try {
      const hostname = await this.hostname()
      const avatarUrl = `http://${hostname}${payload.avatar}&type=big` // add '&type=big' to get big image
692
      const cookieList = await this.cookies()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
693 694
      log.silly('PuppeteerContact', 'avatar() url: %s', avatarUrl)

695
      /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
696
       * FileBox headers (will be used in NodeJS.http.get param options)
697 698 699 700 701 702
       */
      const headers = {
        cookie: cookieList.map(c => `${c['name']}=${c['value']}`).join('; '),
      }
      // return Misc.urlStream(avatarUrl, cookies)

703 704 705
      const contact = this.Contact.load(contactId)
      await contact.ready()

706
      const fileName = (contact.name() || 'unknown') + '-avatar.jpg'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
707
      return FileBox.packRemote(
708
        avatarUrl,
709
        fileName,
710 711 712
        headers,
      )

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
713 714 715 716 717 718 719
    } catch (err) {
      log.warn('PuppeteerContact', 'avatar() exception: %s', err.stack)
      Raven.captureException(err)
      throw err
    }
  }

720 721
  public contactAlias(contactId: string)                      : Promise<string>
  public contactAlias(contactId: string, alias: string | null): Promise<void>
722 723

  public async contactAlias(
724 725
    contactId : string,
    alias?    : string | null,
726 727 728 729 730
  ): Promise<string | void> {
    if (typeof alias === 'undefined') {
      throw new Error('to be implement')
    }

731
    try {
732
      const ret = await this.bridge.contactAlias(contactId, alias)
733
      if (!ret) {
734
        log.warn('PuppetPuppeteer', 'contactRemark(%s, %s) bridge.contactAlias() return false',
735
                              contactId, alias,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
736 737
                            )
        throw new Error('bridge.contactAlias fail')
738 739
      }
    } catch (e) {
740
      log.warn('PuppetPuppeteer', 'contactRemark(%s, %s) rejected: %s', contactId, alias, e.message)
741
      Raven.captureException(e)
742 743 744 745
      throw e
    }
  }

Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
746 747 748
  private contactQueryFilterToFunctionString(
    query: ContactQueryFilter,
  ): string {
749
    log.verbose('PuppetPuppeteer', 'contactQueryFilterToFunctionString({ %s })',
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
750
                            Object.keys(query)
751
                                  .map(k => `${k}: ${query[k as keyof ContactQueryFilter]}`)
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
752 753 754 755 756 757 758
                                  .join(', '),
              )

    if (Object.keys(query).length !== 1) {
      throw new Error('query only support one key. multi key support is not availble now.')
    }

759
    const filterKey = Object.keys(query)[0] as keyof ContactQueryFilter
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
760

761 762 763
    let filterValue: string | RegExp | undefined  = query[filterKey]
    if (!filterValue) {
      throw new Error('filterValue not found')
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
764 765
    }

766 767 768
    const protocolKeyMap = {
      name:   'NickName',
      alias:  'RemarkName',
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
769 770
    }

771 772 773
    const protocolFilterKey = protocolKeyMap[filterKey]
    if (!protocolFilterKey) {
      throw new Error('unsupport protocol filter key')
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
774 775 776 777 778 779 780 781 782
    }

    /**
     * must be string because we need inject variable value
     * into code as variable namespecialContactList
     */
    let filterFunction: string

    if (filterValue instanceof RegExp) {
783
      filterFunction = `(function (c) { return ${filterValue.toString()}.test(c.${protocolFilterKey}) })`
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
784 785
    } else if (typeof filterValue === 'string') {
      filterValue = filterValue.replace(/'/g, '\\\'')
786
      filterFunction = `(function (c) { return c.${protocolFilterKey} === '${filterValue}' })`
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
787 788 789 790 791 792 793
    } else {
      throw new Error('unsupport name type')
    }

    return filterFunction
  }

794
  public async contactFindAll(query: ContactQueryFilter): Promise<string[]> {
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
795 796 797

    const filterFunc = this.contactQueryFilterToFunctionString(query)

798 799
    try {
      const idList = await this.bridge.contactFind(filterFunc)
800
      return idList
801
    } catch (e) {
802
      log.warn('PuppetPuppeteer', 'contactFind(%s) rejected: %s', filterFunc, e.message)
803 804 805
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
806 807
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
808 809 810 811 812 813
  /**
   *
   * Room
   *
   */
  public async roomRawPayload(id: string): Promise<WebRoomRawPayload> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
814
    log.verbose('PuppetPuppeteer', 'roomRawPayload(%s)', id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
815 816

    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
817
      let rawPayload: WebRoomRawPayload | undefined  // = await this.bridge.getContact(room.id) as PuppeteerRoomRawPayload
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
818 819 820 821

      // let currNum = rawPayload.MemberList && rawPayload.MemberList.length || 0
      // let prevNum = room.memberList().length  // rawPayload && rawPayload.MemberList && this.rawObj.MemberList.length || 0

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
822 823
      let prevNum = 0

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
824 825
      let ttl = 7
      while (ttl--/* && currNum !== prevNum */) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
826
        rawPayload = await this.bridge.getContact(id) as WebRoomRawPayload
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
827 828 829 830

        const currNum = rawPayload.MemberList && rawPayload.MemberList.length || 0

        log.silly('PuppetPuppeteer', `roomPayload() this.bridge.getContact(%s) MemberList.length:%d at ttl:%d`,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
831
          id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
832 833 834 835
          currNum,
          ttl,
        )

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
836 837 838
        if (currNum > 0 && prevNum === currNum) {
          log.silly('PuppetPuppeteer', `roomPayload() puppet.getContact(${id}) done at ttl:%d`, ttl)
          break
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
839
        }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
840
        prevNum = currNum
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
841

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
842
        log.silly('PuppetPuppeteer', `roomPayload() puppet.getContact(${id}) retry at ttl:%d`, ttl)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
843 844 845 846 847 848 849 850
        await new Promise(r => setTimeout(r, 1000)) // wait for 1 second
      }

      // await this.readyAllMembers(rawPayload && rawPayload.MemberList || [])
      if (!rawPayload) {
        throw new Error('no payload')
      }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
851
      return rawPayload
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
852
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
853
      log.error('PuppetPuppeteer', 'roomRawPayload(%s) exception: %s', id, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
854 855 856 857 858
      Raven.captureException(e)
      throw e
    }
  }

859
  public async roomRawPayloadParser(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
860
    rawPayload: WebRoomRawPayload,
861 862
  ): Promise<RoomPayload> {
    log.verbose('PuppetPuppeteer', 'roomRawPayloadParser(%s)', rawPayload)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
863

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
864
    // console.log(rawPayload)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
865
    const memberList = (rawPayload.MemberList || [])
866
                        .map(m => this.Contact.load(m.UserName))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
867
    await Promise.all(memberList.map(c => c.ready()))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
868 869 870 871 872

    const nameMap         = this.roomParseMap('name'        , rawPayload.MemberList)
    const roomAliasMap    = this.roomParseMap('roomAlias'   , rawPayload.MemberList)
    const contactAliasMap = this.roomParseMap('contactAlias', rawPayload.MemberList)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
873
    const roomPayload: RoomPayload = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
874 875
      // id:         rawPayload.UserName,
      // encryId:    rawPayload.EncryChatRoomId, // ???
876
      topic:      Misc.plainText(rawPayload.NickName || ''),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
877
      // ownerUin:   rawPayload.OwnerUin,
878
      memberIdList: memberList.map(c => c.id),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
879 880 881 882 883

      nameMap,
      roomAliasMap,
      contactAliasMap,
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
884 885
    // console.log(roomPayload)
    return roomPayload
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
886 887 888 889
  }

  private roomParseMap(
    parseSection: keyof RoomMemberQueryFilter,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
890
    memberList?:  WebRoomRawMember[],
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
891
  ): Map<string, string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
892 893 894 895 896 897
    log.verbose('PuppetPuppeteer', 'roomParseMap(%s, memberList.length=%d)',
                                    parseSection,
                                    memberList && memberList.length,
                )

    const dict: Map<string, string> = new Map<string, string>()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
898 899 900
    if (memberList && memberList.map) {
      memberList.forEach(member => {
        let tmpName: string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
901
        // console.log(member)
902
        const contact = this.Contact.load(member.UserName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
903 904 905
        // contact.ready().then(() => console.log('###############', contact.name()))
        // console.log(contact)
        // log.silly('PuppetPuppeteer', 'roomParseMap() memberList.forEach(contact=%s)', contact)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926

        switch (parseSection) {
          case 'name':
            tmpName = contact.name()
            break
          case 'roomAlias':
            tmpName = member.DisplayName
            break
          case 'contactAlias':
            tmpName = contact.alias() || ''
            break
          default:
            throw new Error('parseMap failed, member not found')
        }
        /**
         * ISSUE #64 emoji need to be striped
         * ISSUE #104 never use remark name because sys group message will never use that
         * @rui: Wrong for 'never use remark name because sys group message will never use that', see more in the latest comment in #104
         * @rui: webwx's NickName here return contactAlias, if not set contactAlias, return name
         * @rui: 2017-7-2 webwx's NickName just ruturn name, no contactAlias
         */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
927
        dict.set(member.UserName, Misc.stripEmoji(tmpName))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
928 929
      })
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
930
    return dict
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
931 932
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
933 934
  public async roomFindAll(
    query: RoomQueryFilter = { topic: /.*/ },
935
  ): Promise<string[]> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953

    let topicFilter = query.topic

    if (!topicFilter) {
      throw new Error('topicFilter not found')
    }

    let filterFunction: string

    if (topicFilter instanceof RegExp) {
      filterFunction = `(function (c) { return ${topicFilter.toString()}.test(c) })`
    } else if (typeof topicFilter === 'string') {
      topicFilter = topicFilter.replace(/'/g, '\\\'')
      filterFunction = `(function (c) { return c === '${topicFilter}' })`
    } else {
      throw new Error('unsupport topic type')
    }

954
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
955
      const idList = await this.bridge.roomFind(filterFunction)
956
      return idList
957
    } catch (e) {
958
      log.warn('PuppetPuppeteer', 'roomFind(%s) rejected: %s', filterFunction, e.message)
959 960 961
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
962 963
  }

964 965 966 967
  public async roomDel(
    roomId    : string,
    contactId : string,
  ): Promise<void> {
968
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
969
      await this.bridge.roomDelMember(roomId, contactId)
970
    } catch (e) {
971
      log.warn('PuppetPuppeteer', 'roomDelMember(%s, %d) rejected: %s', roomId, contactId, e.message)
972 973 974
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
975 976
  }

977 978 979 980
  public async roomAdd(
    roomId    : string,
    contactId : string,
  ): Promise<void> {
981
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
982
      await this.bridge.roomAddMember(roomId, contactId)
983
    } catch (e) {
984
      log.warn('PuppetPuppeteer', 'roomAddMember(%s) rejected: %s', contactId, e.message)
985 986 987
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
988
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
989

990 991 992 993
  public async roomTopic(
    roomId : string,
    topic  : string,
  ): Promise<string> {
994 995 996
    try {
      return await this.bridge.roomModTopic(roomId, topic)
    } catch (e) {
997
      log.warn('PuppetPuppeteer', 'roomTopic(%s) rejected: %s', topic, e.message)
998 999 1000
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1001 1002
  }

1003 1004 1005 1006
  public async roomCreate(
    contactIdList : string[],
    topic         : string,
  ): Promise<string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1007 1008
    try {
      const roomId = await this.bridge.roomCreate(contactIdList, topic)
1009
      if (!roomId) {
1010
        throw new Error('PuppetPuppeteer.roomCreate() roomId "' + roomId + '" not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1011
      }
1012
      return roomId
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1013 1014

    } catch (e) {
1015
      log.warn('PuppetPuppeteer', 'roomCreate(%s, %s) rejected: %s', contactIdList.join(','), topic, e.message)
1016
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1017 1018
      throw e
    }
1019 1020
  }

1021 1022
  public async roomQuit(roomId: string): Promise<void> {
    log.warn('PuppetPuppeteer', 'roomQuit(%s) not supported by Web API', roomId)
1023 1024
  }

1025
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1026
   *
1027
   * FriendRequest
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1028
   *
1029
   */
1030 1031 1032 1033
  public async friendRequestSend(
    contactId : string,
    hello     : string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1034
    try {
1035
      await this.bridge.verifyUserRequest(contactId, hello)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1036
    } catch (e) {
1037
      log.warn('PuppetPuppeteer', 'bridge.verifyUserRequest(%s, %s) rejected: %s', contactId, hello, e.message)
1038
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1039 1040
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1041 1042
  }

1043 1044 1045 1046
  public async friendRequestAccept(
    contactId : string,
    ticket    : string,
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1047
    try {
1048
      await this.bridge.verifyUserOk(contactId, ticket)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1049
    } catch (e) {
1050
      log.warn('PuppetPuppeteer', 'bridge.verifyUserOk(%s, %s) rejected: %s', contactId, ticket, e.message)
1051
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1052 1053
      throw e
    }
1054
  }
1055 1056 1057 1058 1059 1060

  /**
   * @private
   * For issue #668
   */
  public async readyStable(): Promise<void> {
1061
    log.verbose('PuppetPuppeteer', 'readyStable()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1062
    let counter = -1
1063

1064
    const stable = async (done: Function): Promise<void> => {
1065
      log.silly('PuppetPuppeteer', 'readyStable() stable() counter=%d', counter)
1066

1067
      const contactList = await this.Contact.findAll()
1068
      if (counter === contactList.length) {
1069
        log.verbose('PuppetPuppeteer', 'readyStable() stable() READY counter=%d', counter)
1070
        return done()
1071 1072
      }
      counter = contactList.length
1073
      setTimeout(() => stable(done), 1000)
1074 1075 1076 1077
        .unref()
    }

    return new Promise<void>((resolve, reject) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1078
      const timer = setTimeout(() => {
1079
        log.warn('PuppetPuppeteer', 'readyStable() stable() reject at counter=%d', counter)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1080 1081 1082 1083
        return reject(new Error('timeout after 60 seconds'))
      }, 60 * 1000)
      timer.unref()

1084
      const done = () => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1085
        clearTimeout(timer)
1086
        return resolve()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1087
      }
1088

1089
      return stable(done)
1090 1091 1092
    })

  }
1093

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1094 1095 1096 1097 1098 1099
  /**
   * https://www.chatie.io:8080/api
   * location.hostname = www.chatie.io
   * location.host = www.chatie.io:8080
   * See: https://stackoverflow.com/a/11379802/1123955
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1100
  private async hostname(): Promise<string> {
1101 1102 1103 1104 1105 1106 1107
    try {
      const name = await this.bridge.hostname()
      if (!name) {
        throw new Error('no hostname found')
      }
      return name
    } catch (e) {
1108
      log.error('PuppetPuppeteer', 'hostname() exception:%s', e)
1109 1110 1111 1112 1113
      this.emit('error', e)
      throw e
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1114
  private async cookies(): Promise<Cookie[]> {
1115 1116 1117 1118 1119 1120 1121 1122
    return await this.bridge.cookies()
  }

  public async saveCookie(): Promise<void> {
    const cookieList = await this.bridge.cookies()
    this.options.profile.set('cookies', cookieList)
    this.options.profile.save()
  }
1123

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1124
  private extToType(ext: string): WebMessageType {
1125
    switch (ext) {
1126 1127 1128 1129
      case '.bmp':
      case '.jpeg':
      case '.jpg':
      case '.png':
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1130
        return WebMessageType.IMAGE
1131
      case '.gif':
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1132
        return WebMessageType.EMOTICON
1133
      case '.mp4':
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1134
        return WebMessageType.VIDEO
1135
      default:
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1136
        return WebMessageType.APP
1137 1138 1139
    }
  }

1140
  // public async readyMedia(): Promise<this> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1141 1142 1143 1144
  private async messageRawPayloadToUrl(
    rawPayload: WebMessageRawPayload,
  ): Promise<null | string> {
    log.silly('PuppetPuppeteer', 'readyMedia()')
1145

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1146 1147
    // let type = MessageType.Unknown
    let url: undefined | string
1148

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1149
    try {
1150

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168
      switch (rawPayload.MsgType) {
        case WebMessageType.EMOTICON:
          // type = MessageType.Emoticon
          url = await this.bridge.getMsgEmoticon(rawPayload.MsgId)
          break
        case WebMessageType.IMAGE:
          // type = MessageType.Image
          url = await this.bridge.getMsgImg(rawPayload.MsgId)
          break
        case WebMessageType.VIDEO:
        case WebMessageType.MICROVIDEO:
          // type = MessageType.Video
          url = await this.bridge.getMsgVideo(rawPayload.MsgId)
          break
        case WebMessageType.VOICE:
          // type = MessageType.Audio
          url = await this.bridge.getMsgVoice(rawPayload.MsgId)
          break
1169

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
        case WebMessageType.APP:
          switch (rawPayload.AppMsgType) {
            case WebAppMsgType.ATTACH:
              if (!rawPayload.MMAppMsgDownloadUrl) {
                throw new Error('no MMAppMsgDownloadUrl')
              }
              // had set in Message
              // type = MessageType.Attachment
              url = rawPayload.MMAppMsgDownloadUrl
              break

            case WebAppMsgType.URL:
            case WebAppMsgType.READER_TYPE:
              if (!rawPayload.Url) {
                throw new Error('no Url')
              }
              // had set in Message
              // type = MessageType.Attachment
              url = rawPayload.Url
              break

            default:
              const e = new Error('ready() unsupported typeApp(): ' + rawPayload.AppMsgType)
              log.warn('PuppeteerMessage', e.message)
              throw e
          }
          break
1197

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1198 1199 1200 1201 1202 1203
        case WebMessageType.TEXT:
          if (rawPayload.SubMsgType === WebMessageType.LOCATION) {
            // type = MessageType.Image
            url = await this.bridge.getMsgPublicLinkImg(rawPayload.MsgId)
          }
          break
1204

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235
        default:
          /**
           * not a support media message, do nothing.
           */
          return null
          // return this
      }

      if (!url) {
        // if (!this.payload.url) {
        //   /**
        //    * not a support media message, do nothing.
        //    */
        //   return this
        // }
        // url = this.payload.url
        // return {
        //   type: MessageType.Unknown,
        // }
        return null
      }

    } catch (e) {
      log.warn('PuppetPuppeteer', 'ready() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }

    return url

  }
1236

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1237 1238 1239 1240 1241 1242
  private filename(
    rawPayload: WebMessageRawPayload,
  ): null | string {
    log.verbose('PuppetPuppeteer', 'filename()')

    let filename = rawPayload.FileName || rawPayload.MediaId || rawPayload.MsgId
1243

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1244 1245
    const re = /\.[a-z0-9]{1,7}$/i
    if (!re.test(filename)) {
1246 1247 1248 1249 1250
      if (rawPayload.MMAppMsgFileExt) {
        filename += '.' + rawPayload.MMAppMsgFileExt
      } else {
        filename += this.extname(rawPayload)
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1251
    }
1252

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1253 1254 1255
    log.silly('PuppetPuppeteer', 'filename()=%s, build from rawPayload', filename)
    return filename
  }
1256

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1257 1258 1259 1260
  private extname(
    rawPayload: WebMessageRawPayload,
  ): string {
    let ext: string
1261

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1262
    // const type = this.type()
1263

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308
    switch (rawPayload.MsgType) {
      case WebMessageType.EMOTICON:
        ext = '.gif'
        break

      case WebMessageType.IMAGE:
        ext = '.jpg'
        break

      case WebMessageType.VIDEO:
      case WebMessageType.MICROVIDEO:
        ext = '.mp4'
        break

      case WebMessageType.VOICE:
        ext = '.mp3'
        break

      case WebMessageType.APP:
        switch (rawPayload.AppMsgType) {
          case WebAppMsgType.URL:
            ext = '.url' // XXX
            break
          default:
            ext = '.' + rawPayload.MsgType
            break
        }
        break

      case WebMessageType.TEXT:
        if (rawPayload.SubMsgType === WebMessageType.LOCATION) {
          ext = '.jpg'
        }
        ext = '.' + rawPayload.MsgType

        break

      default:
        log.silly('PuppeteerMessage', `ext() got unknown type: ${rawPayload.MsgType}`)
        ext = '.' + rawPayload.MsgType
    }

    return ext

  }
1309

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323
  private async uploadMedia(
    file       : FileBox,
    toUserName : string,
  ): Promise<WebMessageMediaPayload> {
    const filename = file.name
    const ext      = path.extname(filename) //  message.ext()

    // const contentType = Misc.mime(ext)
    const contentType = mime.getType(ext)
    // const contentType = message.mimeType()
    if (!contentType) {
      throw new Error('no MIME Type found on mediaMessage: ' + file.name)
    }
    let mediatype: WebMediaType
1324

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338
    switch (ext) {
      case '.bmp':
      case '.jpeg':
      case '.jpg':
      case '.png':
      case '.gif':
        mediatype = WebMediaType.Image
        break
      case '.mp4':
        mediatype = WebMediaType.Video
        break
      default:
        mediatype = WebMediaType.Attachment
    }
1339

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1340
    const buffer = await new Promise<Buffer>((resolve, reject) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1341 1342 1343 1344 1345
      file.pipe(bl((err: Error, data: Buffer) => {
        if (err) reject(err)
        else resolve(data)
      }))
    })
1346

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1347 1348 1349 1350 1351
    // Sending video files is not allowed to exceed 20MB
    // https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L1115
    const MAX_FILE_SIZE   = 100 * 1024 * 1024
    const LARGE_FILE_SIZE = 25 * 1024 * 1024
    const MAX_VIDEO_SIZE  = 20 * 1024 * 1024
1352

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1353 1354 1355 1356 1357
    if (mediatype === WebMediaType.Video && buffer.length > MAX_VIDEO_SIZE)
      throw new Error(`Sending video files is not allowed to exceed ${MAX_VIDEO_SIZE / 1024 / 1024}MB`)
    if (buffer.length > MAX_FILE_SIZE) {
      throw new Error(`Sending files is not allowed to exceed ${MAX_FILE_SIZE / 1024 / 1024}MB`)
    }
1358

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1359 1360 1361 1362 1363 1364 1365 1366 1367 1368
    const md5 = Misc.md5(buffer)

    const baseRequest     = await this.getBaseRequest()
    const passTicket      = await this.bridge.getPassticket()
    const uploadMediaUrl  = await this.bridge.getUploadMediaUrl()
    const checkUploadUrl  = await this.bridge.getCheckUploadUrl()
    const cookie          = await this.bridge.cookies()
    const first           = cookie.find(c => c.name === 'webwx_data_ticket')
    const webwxDataTicket = first && first.value
    const size            = buffer.length
1369
    const fromUserName    = this.selfId()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1370 1371 1372 1373 1374 1375 1376 1377 1378
    const id              = 'WU_FILE_' + this.fileId
    this.fileId++

    const hostname = await this.bridge.hostname()
    const headers = {
      Referer: `https://${hostname}`,
      'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36',
      Cookie: cookie.map(c => c.name + '=' + c.value).join('; '),
    }
1379

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395
    log.silly('PuppetPuppeteer', 'uploadMedia() headers:%s', JSON.stringify(headers))

    const uploadMediaRequest = {
      BaseRequest:   baseRequest,
      FileMd5:       md5,
      FromUserName:  fromUserName,
      ToUserName:    toUserName,
      UploadType:    2,
      ClientMediaId: +new Date,
      MediaType:     WebMediaType.Attachment,
      StartPos:      0,
      DataLen:       size,
      TotalLen:      size,
      Signature:     '',
      AESKey:        '',
    }
1396

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1397 1398 1399 1400 1401 1402 1403 1404 1405
    const checkData = {
      BaseRequest:  baseRequest,
      FromUserName: fromUserName,
      ToUserName:   toUserName,
      FileName:     filename,
      FileSize:     size,
      FileMd5:      md5,
      FileType:     7,              // If do not have this parameter, the api will fail
    }
1406

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527
    const mediaData = {
      ToUserName: toUserName,
      MediaId:    '',
      FileName:   filename,
      FileSize:   size,
      FileMd5:    md5,
      MMFileExt:  ext,
    } as WebMessageMediaPayload

    // If file size > 25M, must first call checkUpload to get Signature and AESKey, otherwise it will fail to upload
    // https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L1132 #1182
    if (size > LARGE_FILE_SIZE) {
      let ret
      try {
        ret = <any> await new Promise((resolve, reject) => {
          const r = {
            url: `https://${hostname}${checkUploadUrl}`,
            headers,
            json: checkData,
          }
          request.post(r, (err, _ /* res */, body) => {
            try {
              if (err) {
                reject(err)
              } else {
                let obj = body
                if (typeof body !== 'object') {
                  log.silly('PuppetPuppeteer', 'updateMedia() typeof body = %s', typeof body)
                  try {
                    obj = JSON.parse(body)
                  } catch (e) {
                    log.error('PuppetPuppeteer', 'updateMedia() body = %s', body)
                    log.error('PuppetPuppeteer', 'updateMedia() exception: %s', e)
                    this.emit('error', e)
                  }
                }
                if (typeof obj !== 'object' || obj.BaseResponse.Ret !== 0) {
                  const errMsg = obj.BaseResponse || 'api return err'
                  log.silly('PuppetPuppeteer', 'uploadMedia() checkUpload err:%s \nreq:%s\nret:%s', JSON.stringify(errMsg), JSON.stringify(r), body)
                  reject(new Error('chackUpload err:' + JSON.stringify(errMsg)))
                }
                resolve({
                  Signature : obj.Signature,
                  AESKey    : obj.AESKey,
                })
              }
            } catch (e) {
              reject(e)
            }
          })
        })
      } catch (e) {
        log.error('PuppetPuppeteer', 'uploadMedia() checkUpload exception: %s', e.message)
        throw e
      }
      if (!ret.Signature) {
        log.error('PuppetPuppeteer', 'uploadMedia(): chackUpload failed to get Signature')
        throw new Error('chackUpload failed to get Signature')
      }
      uploadMediaRequest.Signature = ret.Signature
      uploadMediaRequest.AESKey    = ret.AESKey
      mediaData.Signature          = ret.Signature
    } else {
      delete uploadMediaRequest.Signature
      delete uploadMediaRequest.AESKey
    }

    log.verbose('PuppetPuppeteer', 'uploadMedia() webwx_data_ticket: %s', webwxDataTicket)
    log.verbose('PuppetPuppeteer', 'uploadMedia() pass_ticket: %s', passTicket)

    const formData = {
      id,
      name: filename,
      type: contentType,
      lastModifiedDate: Date().toString(),
      size,
      mediatype,
      uploadmediarequest: JSON.stringify(uploadMediaRequest),
      webwx_data_ticket: webwxDataTicket,
      pass_ticket: passTicket || '',
      filename: {
        value: buffer,
        options: {
          filename,
          contentType,
          size,
        },
      },
    }
    let mediaId: string
    try {
      mediaId = <string>await new Promise((resolve, reject) => {
        try {
          request.post({
            url: uploadMediaUrl + '?f=json',
            headers,
            formData,
          }, function (err, _, body) {
            if (err) { reject(err) }
            else {
              let obj = body
              if (typeof body !== 'object') {
                obj = JSON.parse(body)
              }
              resolve(obj.MediaId || '')
            }
          })
        } catch (e) {
          reject(e)
        }
      })
    } catch (e) {
      log.error('PuppetPuppeteer', 'uploadMedia() uploadMedia exception: %s', e.message)
      throw new Error('uploadMedia err: ' + e.message)
    }
    if (!mediaId) {
      log.error('PuppetPuppeteer', 'uploadMedia(): upload fail')
      throw new Error('PuppetPuppeteer.uploadMedia(): upload fail')
    }
    return Object.assign(mediaData, { MediaId: mediaId })
  }
1528

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1529 1530 1531 1532 1533 1534 1535 1536
  public async messageSendFile(
    receiver : Receiver,
    file     : FileBox,
  ): Promise<void> {
    log.verbose('PuppetPuppeteer', 'messageSendFile(receiver=%s, file=%s)',
                                    receiver,
                                    file.toString(),
                )
1537

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1538
    let destinationId
1539

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1540 1541 1542 1543 1544 1545 1546
    if (receiver.roomId) {
      destinationId = receiver.roomId
    } else if (receiver.contactId) {
      destinationId = receiver.contactId
    } else {
      throw new Error('PuppetPuppeteer.messageSendFile(): message with neither room nor to?')
    }
1547

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1548 1549
    let mediaData: WebMessageMediaPayload
    let rawPayload = {} as WebMessageRawPayload
1550

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607
    if (!rawPayload || !rawPayload.MediaId) {
      try {
        mediaData = await this.uploadMedia(file, destinationId)
        rawPayload = Object.assign(rawPayload, mediaData)
        log.silly('PuppetPuppeteer', 'Upload completed, new rawObj:%s', JSON.stringify(rawPayload))
      } catch (e) {
        log.error('PuppetPuppeteer', 'sendMedia() exception: %s', e.message)
        throw e
      }
    } else {
      // To support forward file
      log.silly('PuppetPuppeteer', 'skip upload file, rawObj:%s', JSON.stringify(rawPayload))
      mediaData = {
        ToUserName : destinationId,
        MediaId    : rawPayload.MediaId,
        MsgType    : rawPayload.MsgType,
        FileName   : rawPayload.FileName,
        FileSize   : rawPayload.FileSize,
        MMFileExt  : rawPayload.MMFileExt,
      }
      if (rawPayload.Signature) {
        mediaData.Signature = rawPayload.Signature
      }
    }
    // console.log('mediaData.MsgType', mediaData.MsgType)
    // console.log('rawObj.MsgType', message.rawObj && message.rawObj.MsgType)

    mediaData.MsgType = this.extToType(path.extname(file.name))
    log.silly('PuppetPuppeteer', 'sendMedia() destination: %s, mediaId: %s, MsgType; %s)',
      destinationId,
      mediaData.MediaId,
      mediaData.MsgType,
    )
    let ret = false
    try {
      ret = await this.bridge.sendMedia(mediaData)
    } catch (e) {
      log.error('PuppetPuppeteer', 'sendMedia() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }
    if (!ret) {
      throw new Error('sendMedia fail')
    }
  }

  private async getBaseRequest(): Promise<any> {
    try {
      const json = await this.bridge.getBaseRequest()
      const obj = JSON.parse(json)
      return obj.BaseRequest
    } catch (e) {
      log.error('PuppetPuppeteer', 'send() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }
  }
1608

1609
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
1610

1611
export default PuppetPuppeteer