puppet-puppeteer.ts 48.7 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,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
43
  ScanData,
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
  public bridge   : Bridge
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93
  public scanInfo?: ScanData
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')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
152
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153

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

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

161
      Raven.captureException(e)
162
      throw e
163
    }
164 165
  }

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

169 170
    const puppet = this

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

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

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

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

  /**
   * 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 (李卓桓) 已提交
203
  private initWatchdogForScan(): void {
204
    log.verbose('PuppetPuppeteer', 'initWatchdogForScan()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205

206
    const puppet = this
207
    const dog    = this.scanWatchdog
208

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

212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
    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 (李卓桓) 已提交
232
    dog.on('reset', async (food, timePast) => {
233
      log.warn('PuppetPuppeteer', 'initScanWatchdog() on(reset) lastFood: %s, timePast: %s',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
234
                            food.data, timePast)
235 236 237
      try {
        await this.bridge.reload()
      } catch (e) {
238
        log.error('PuppetPuppeteer', 'initScanWatchdog() on(reset) exception: %s', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
239
        try {
240
          log.error('PuppetPuppeteer', 'initScanWatchdog() on(reset) try to recover by bridge.{quit,init}()', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
241 242
          await this.bridge.quit()
          await this.bridge.init()
243
          log.error('PuppetPuppeteer', 'initScanWatchdog() on(reset) recover successful')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
244
        } catch (e) {
245
          log.error('PuppetPuppeteer', 'initScanWatchdog() on(reset) recover FAIL: %s', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
246 247
          this.emit('error', e)
        }
248 249 250 251
      }
    })
  }

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

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

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

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

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

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

288
    this.bridge.on('ding'     , Event.onDing.bind(this))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
289
    this.bridge.on('error'    , e => this.emit('error', e))
290 291 292 293 294
    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))
295
    this.bridge.on('unload'   , Event.onUnload.bind(this))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
296

297
    try {
298
      await this.bridge.init()
299
    } catch (e) {
300
      log.error('PuppetPuppeteer', 'initBridge() exception: %s', e.message)
301
      await this.bridge.quit().catch(console.error)
302 303 304 305
      this.emit('error', e)

      Raven.captureException(e)
      throw e
306
    }
307 308

    return this.bridge
309 310
  }

311 312 313
  public async messageRawPayload(id: string): Promise <WebMessageRawPayload> {
    const rawPayload = await this.bridge.getMessage(id)
    return rawPayload
314
  }
315

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

321
    const from: Contact     = this.Contact.load(rawPayload.MMActualSender)  // MMPeerUserName
322 323
    const text: string      = rawPayload.MMActualContent                    // Content has @id prefix added by wx
    const date: Date        = new Date(rawPayload.MMDisplayTime)            // Javascript timestamp of milliseconds
324 325 326 327 328 329 330

    let room : undefined | Room
    let to   : undefined | Contact

    // FIXME: has there any better method to know the room ID?
    if (rawPayload.MMIsChatRoom) {
      if (/^@@/.test(rawPayload.FromUserName)) {
331
        room = this.Room.load(rawPayload.FromUserName) // MMPeerUserName always eq FromUserName ?
332
      } else if (/^@@/.test(rawPayload.ToUserName)) {
333
        room = this.Room.load(rawPayload.ToUserName)
334 335 336
      } else {
        throw new Error('parse found a room message, but neither FromUserName nor ToUserName is a room(/^@@/)')
      }
M
Mukaiu 已提交
337 338
    }

339 340
    if (rawPayload.ToUserName) {
      if (!/^@@/.test(rawPayload.ToUserName)) { // if a message in room without any specific receiver, then it will set to be `undefined`
341
        to = this.Contact.load(rawPayload.ToUserName)
342
      }
343 344
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
345
    const file: undefined | FileBox = undefined
M
Mukaiu 已提交
346

347
    const type: MessageType = this.messageTypeFromWeb(rawPayload.MsgType)
348

349 350 351 352
    const fromId = from && from.id
    const roomId = room && room.id
    const toId   = to   && to.id

353
    const payload: MessagePayload = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
354
      // direction: MessageDirection.MT,
355
      type,
356 357 358
      fromId,
      toId,
      roomId,
359 360 361
      text,
      date,
      file,
362 363
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
364
    if (type !== MessageType.Text && type !== MessageType.Unknown) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
365
      payload.file = await this.messageRawPayloadToFile(rawPayload)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
366
    }
M
Mukaiu 已提交
367

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
368 369 370
    return payload
  }

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 573 574 575
  public async login(userId: string): Promise<void> {
    log.verbose('PuppetPuppeteer', 'login(%s)', userId)
    this.userId = userId
    this.emit('login', this.Contact.load(userId))
576 577
  }

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

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

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

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

  }

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

    // 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
643
      name:       Misc.plainText(rawPayload.NickName || ''),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
644
      alias:      rawPayload.RemarkName,
645
      gender:     rawPayload.Sex,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
      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 (李卓桓) 已提交
664 665
                    ? Contact.Type.Official
                    : Contact.Type.Personal,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
666 667 668 669 670 671 672 673
      /**
       * @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),
    }
  }

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

684
  public async contactAvatar(contactId: string): Promise<FileBox> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
685
    log.verbose('PuppetPuppeteer', 'contactAvatar(%s)', contactId)
686
    const payload = await this.contactPayload(contactId)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
687 688 689 690 691 692 693
    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
694
      const cookieList = await this.cookies()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
695 696
      log.silly('PuppeteerContact', 'avatar() url: %s', avatarUrl)

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

705 706 707
      const contact = this.Contact.load(contactId)
      await contact.ready()

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

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

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

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

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

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

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

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

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

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

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

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

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

    return filterFunction
  }

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

    const filterFunc = this.contactQueryFilterToFunctionString(query)

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

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

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

      // 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 (李卓桓) 已提交
824 825
      let prevNum = 0

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

        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 (李卓桓) 已提交
833
          id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
834 835 836 837
          currNum,
          ttl,
        )

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
844
        log.silly('PuppetPuppeteer', `roomPayload() puppet.getContact(${id}) retry at ttl:%d`, ttl)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
845 846 847 848 849 850 851 852
        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 (李卓桓) 已提交
853
      return rawPayload
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
854
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
855
      log.error('PuppetPuppeteer', 'roomRawPayload(%s) exception: %s', id, e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
856 857 858 859 860
      Raven.captureException(e)
      throw e
    }
  }

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

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

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

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

  private roomParseMap(
    parseSection: keyof RoomMemberQueryFilter,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
892
    memberList?:  WebRoomRawMember[],
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
893
  ): Map<string, string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
894 895 896 897 898 899
    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 (李卓桓) 已提交
900 901 902
    if (memberList && memberList.map) {
      memberList.forEach(member => {
        let tmpName: string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
903
        // console.log(member)
904
        const contact = this.Contact.load(member.UserName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
905 906 907
        // contact.ready().then(() => console.log('###############', contact.name()))
        // console.log(contact)
        // log.silly('PuppetPuppeteer', 'roomParseMap() memberList.forEach(contact=%s)', contact)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928

        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 (李卓桓) 已提交
929
        dict.set(member.UserName, Misc.stripEmoji(tmpName))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
930 931
      })
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
932
    return dict
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
933 934
  }

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

    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')
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1091
      return stable(done)
1092 1093 1094
    })

  }
1095

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1096 1097 1098 1099 1100 1101
  /**
   * 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 (李卓桓) 已提交
1102
  private async hostname(): Promise<string> {
1103 1104 1105 1106 1107 1108 1109
    try {
      const name = await this.bridge.hostname()
      if (!name) {
        throw new Error('no hostname found')
      }
      return name
    } catch (e) {
1110
      log.error('PuppetPuppeteer', 'hostname() exception:%s', e)
1111 1112 1113 1114 1115
      this.emit('error', e)
      throw e
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1116
  private async cookies(): Promise<Cookie[]> {
1117 1118 1119 1120 1121 1122 1123 1124
    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()
  }
1125

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1151
    try {
1152

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170
      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
1171

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 1197 1198
        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
1199

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 1236 1237
        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

  }
1238

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

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

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

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 1309 1310
    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

  }
1311

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325
  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
1326

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340
    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
    }
1341

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1349 1350 1351 1352 1353
    // 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
1354

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1355 1356 1357 1358 1359
    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`)
    }
1360

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380
    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
    const fromUserName    = this.userSelf()!.id
    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('; '),
    }
1381

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397
    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:        '',
    }
1398

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1399 1400 1401 1402 1403 1404 1405 1406 1407
    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
    }
1408

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 1528 1529
    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 })
  }
1530

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1540
    let destinationId
1541

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1542 1543 1544 1545 1546 1547 1548
    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?')
    }
1549

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 1608 1609
    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
    }
  }
1610

1611
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
1612

1613
export default PuppetPuppeteer