puppet-puppeteer.ts 50.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,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
43
  ScanData,
44
}                     from '../puppet/'
45
import {
46
  config,
47 48
  log,
  Raven,
49 50 51 52
}                     from '../config'
import Profile        from '../profile'
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 74
  Contact,
  ContactPayload,
  ContactQueryFilter,
  Gender,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
75
}                             from '../contact'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
76
import {
77
  MessageDirection,
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'
87 88 89
// import {
//   FriendRequest,
// }                             from '../puppet/friend-request'
M
Mukaiu 已提交
90

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
91 92
export type PuppetFoodType = 'scan' | 'ding'
export type ScanFoodType   = 'scan' | 'login' | 'logout'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93

94
export class PuppetPuppeteer extends Puppet {
95
  public bridge   : Bridge
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
96
  public scanInfo?: ScanData
97

98
  public scanWatchdog: Watchdog<ScanFoodType>
99

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

102 103 104
  constructor(
    public options: PuppetOptions,
  ) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
105
    super(options)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106

107
    // this.fileId = 0
108

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

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

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

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

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

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

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

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

150
      log.verbose('PuppetPuppeteer', 'start() done')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
151
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
152

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

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

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

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

168 169
    const puppet = this

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
183
    this.watchdog.on('reset', async (food, timeout) => {
184
      log.warn('PuppetPuppeteer', 'initWatchdogForPuppet() dog.on(reset) last food:%s, timeout:%s',
185
                            food.data, timeout)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
186
      try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
187 188
        await this.stop()
        await this.start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
189 190 191
      } catch (e) {
        puppet.emit('error', e)
      }
192 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
   */
  public initWatchdogForScan(): void {
203
    log.verbose('PuppetPuppeteer', 'initWatchdogForScan()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
204

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

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

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

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

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

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

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

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

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

287
    const head = config.head
288 289 290 291 292 293
    // we have to set this.bridge right now,
    // because the Event.onXXX might arrive while we are initializing.
    this.bridge = new Bridge({
      head,
      profile,
    })
294

295
    this.bridge.on('ding'     , Event.onDing.bind(this))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
296
    this.bridge.on('error'    , e => this.emit('error', e))
297 298 299 300 301
    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))
302
    this.bridge.on('unload'   , Event.onUnload.bind(this))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
303

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

      Raven.captureException(e)
      throw e
313
    }
314 315

    return this.bridge
316 317
  }

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

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

328
    const from: Contact     = this.Contact.load(rawPayload.MMActualSender)  // MMPeerUserName
329 330
    const text: string      = rawPayload.MMActualContent                    // Content has @id prefix added by wx
    const date: Date        = new Date(rawPayload.MMDisplayTime)            // Javascript timestamp of milliseconds
331 332 333 334 335 336 337

    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)) {
338
        room = this.Room.load(rawPayload.FromUserName) // MMPeerUserName always eq FromUserName ?
339
      } else if (/^@@/.test(rawPayload.ToUserName)) {
340
        room = this.Room.load(rawPayload.ToUserName)
341 342 343 344
      } else {
        throw new Error('parse found a room message, but neither FromUserName nor ToUserName is a room(/^@@/)')
      }
      room = room
M
Mukaiu 已提交
345 346
    }

347 348
    if (rawPayload.ToUserName) {
      if (!/^@@/.test(rawPayload.ToUserName)) { // if a message in room without any specific receiver, then it will set to be `undefined`
349
        to = this.Contact.load(rawPayload.ToUserName)
350
      }
351 352
    }

353
    const file: FileBox | undefined = undefined
M
Mukaiu 已提交
354

355
    const type: MessageType = this.messageTypeFromWeb(rawPayload.MsgType)
356

357 358 359 360
    const fromId = from && from.id
    const roomId = room && room.id
    const toId   = to   && to.id

361 362 363
    const payload: MessagePayload = {
      direction: MessageDirection.MT,
      type,
364 365 366
      fromId,
      toId,
      roomId,
367 368 369
      text,
      date,
      file,
370 371
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
372 373 374
    if (type !== MessageType.Text && type !== MessageType.Unknown) {
      payload.file = await this.messageRawPayloadFile(rawPayload)
    }
M
Mukaiu 已提交
375

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
376 377 378
    return payload
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 420 421 422 423 424 425 426 427 428 429 430 431 432
  private async messageRawPayloadFile(
    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('; '),
    }

    const fileBox = FileBox.fromRemote(url, filename, headers)

    return fileBox
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
433
  private messageTypeFromWeb(webMsgType: WebMessageType): MessageType {
434
    switch (webMsgType) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
435
      case WebMessageType.TEXT:
436 437
        return MessageType.Text

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
438 439
      case WebMessageType.EMOTICON:
      case WebMessageType.IMAGE:
440 441
        return MessageType.Image

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
442
      case WebMessageType.VOICE:
443
        return MessageType.Audio
444

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
445 446
      case WebMessageType.MICROVIDEO:
      case WebMessageType.VIDEO:
447
        return MessageType.Video
448

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
449
      case WebMessageType.TEXT:
450 451 452
        return MessageType.Text

      case WebMessageType.SYS:
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
453
        // FriendRequest is a SYS message
454 455
        // FIXME: should we use better message type at here???
        return MessageType.Text
456 457 458 459 460 461 462 463 464 465 466 467 468

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

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

483 484 485 486
    log.silly('PuppetPuppeteer', 'forward(receiver=%s, messageId=%s)',
                                  receiver,
                                  messageId,
              )
487

488
    let rawPayload = await this.messageRawPayload(messageId)
489 490

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
492
    const newMsg = <WebMessageRawPayload>{}
493 494 495 496 497 498 499 500 501 502 503 504 505
    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')
    // }
506
    if (rawPayload.FileSize >= largeFileSize && !rawPayload.Signature) {
507 508
      // 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 (李卓桓) 已提交
509
      throw new Error('forward() Due to webWx restrictions, more than 25MB of files can not be downloaded and can not be forwarded.')
510 511
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
512
    newMsg.FromUserName         = this.userId || ''
513
    newMsg.isTranspond          = true
514 515
    newMsg.MsgIdBeforeTranspond = rawPayload.MsgIdBeforeTranspond || rawPayload.MsgId
    newMsg.MMSourceMsgId        = rawPayload.MsgId
516
    // 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
517
    newMsg.Content      = Misc.unescapeHtml(rawPayload.Content.replace(/^@\w+:<br\/>/, '')).replace(/^[\w\-]+:<br\/>/, '')
518
    newMsg.MMIsChatRoom = receiver instanceof Room ? true : false
519 520 521

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

522
    rawPayload = Object.assign(rawPayload, newMsg)
523 524 525 526 527
    // 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)
    // }
528
    newMsg.ToUserName = receiver.contactId || receiver.roomId as string
529 530
    // ret = await config.puppetInstance().forward(m, newMsg)
    // return ret
531
    const baseData  = rawPayload
532 533
    const patchData = newMsg

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

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

    let destinationId

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

562 563 564 565
    log.silly('PuppetPuppeteer', 'messageSendText() destination: %s, text: %s)',
                                  destinationId,
                                  text,
              )
M
Mukaiu 已提交
566

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
576
  public async login(user: Contact): Promise<void> {
577
    log.warn('PuppetPuppeteer', 'login(%s)', user)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
578
    this.userId = user.id
579 580 581
    this.emit('login', user)
  }

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

588 589 590 591 592
    const user = this.userSelf()
    if (!user) {
      log.warn('PuppetPuppeteer', 'logout() without self()')
      return
    }
593

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

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

  }

  public async contactRawPayloadParser(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
625
    rawPayload: WebContactRawPayload,
626
  ): Promise<ContactPayload> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
627 628 629 630 631 632
    log.verbose('PuppetPuppeteer', 'contactParseRawPayload(Object.keys(payload).length=%d)',
                                    Object.keys(rawPayload).length,
                )
    if (!Object.keys(rawPayload).length) {
      log.error('PuppetPuppeteer', 'contactParseRawPayload() got empty rawPayload!')
      return {
633
        gender: Gender.Unknown,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
634
        type:   Contact.Type.Unknown,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
635
      }
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 685
  public async contactAvatar(contactId: string): Promise<FileBox> {
    const payload = await this.contactPayload(contactId)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
686 687 688 689 690 691 692
    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
693
      const cookieList = await this.cookies()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
694 695
      log.silly('PuppeteerContact', 'avatar() url: %s', avatarUrl)

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

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

707 708 709 710 711 712
      return FileBox.fromRemote(
        avatarUrl,
        contact.name() || 'unknown' + '-avatar.jpg',
        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: keyof ContactQueryFilter) => `${k}: ${query[k]}`)
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
   */
1100 1101 1102 1103 1104 1105 1106 1107
  public async hostname(): Promise<string> {
    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 1114 1115 1116 1117 1118 1119 1120 1121 1122
      this.emit('error', e)
      throw e
    }
  }

  public async cookies(): Promise<Cookie[]> {
    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 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
  /**
   *
   *
   *
   *
   *
   *  THE FOLLOWING COMMENT OUTED CODE
   *
   *  IS: TO BE MERGE
   *
   *
   *
   *
   *
   *
   *
   */

  // public async readyMedia(): Promise<this> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1159 1160 1161 1162
  private async messageRawPayloadToUrl(
    rawPayload: WebMessageRawPayload,
  ): Promise<null | string> {
    log.silly('PuppetPuppeteer', 'readyMedia()')
1163

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1167
    try {
1168

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186
      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
1187

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214
        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
1215

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1216 1217 1218 1219 1220 1221
        case WebMessageType.TEXT:
          if (rawPayload.SubMsgType === WebMessageType.LOCATION) {
            // type = MessageType.Image
            url = await this.bridge.getMsgPublicLinkImg(rawPayload.MsgId)
          }
          break
1222

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253
        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

  }
1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 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

  // public async readyStream(): Promise<Readable> {
  //   log.verbose('PuppetPuppeteer', 'readyStream()')

  //   /**
  //    * 1. local file
  //    */
  //   try {
  //     const filename = this.filename()
  //     if (filename) {
  //       return fs.createReadStream(filename)
  //     }
  //   } catch (e) {
  //     // no filename
  //   }

  //   /**
  //    * 2. remote url
  //    */
  //   try {
  //     await this.ready()
  //     // FIXME: decoupling needed
  //     const cookies = await (this.puppet as any as PuppetPuppeteer).cookies()
  //     if (!this.payload.url) {
  //       throw new Error('no url')
  //     }
  //     log.verbose('PuppetPuppeteer', 'readyStream() url: %s', this.payload.url)
  //     return Misc.urlStream(this.payload.url, cookies)
  //   } catch (e) {
  //     log.warn('PuppetPuppeteer', 'readyStream() exception: %s', e.stack)
  //     Raven.captureException(e)
  //     throw e
  //   }
  // }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302
  private filename(
    rawPayload: WebMessageRawPayload,
  ): null | string {
    log.verbose('PuppetPuppeteer', 'filename()')

    // if (this.parsedPath) {
    //   // https://nodejs.org/api/path.html#path_path_parse_path
    //   const filename = path.join(
    //     this.parsedPath!.dir  || '',
    //     this.parsedPath!.base || '',
    //   )
    //   log.silly('PuppetPuppeteer', 'filename()=%s, build from parsedPath', filename)
    //   return filename
    // }
1303

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1304
    let filename = rawPayload.FileName || rawPayload.MediaId || rawPayload.MsgId
1305

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1306 1307 1308 1309 1310
    const re = /\.[a-z0-9]{1,7}$/i
    if (!re.test(filename)) {
      const ext = rawPayload.MMAppMsgFileExt || this.extname(rawPayload)
      filename += '.' + ext
    }
1311

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1312 1313 1314
    log.silly('PuppetPuppeteer', 'filename()=%s, build from rawPayload', filename)
    return filename
  }
1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336

  // public ext(): string {
  //   const fileExt = this.extFromFile()
  //   if (fileExt) {
  //     return fileExt
  //   }

  //   const typeExt = this.extFromType()
  //   if (typeExt) {
  //     return typeExt
  //   }

  //   throw new Error('unknown ext()')
  // }

  // private extFromFile(): string | null {
  //   if (this.parsedPath && this.parsedPath.ext) {
  //     return this.parsedPath.ext
  //   }
  //   return null
  // }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1337 1338 1339 1340
  private extname(
    rawPayload: WebMessageRawPayload,
  ): string {
    let ext: string
1341

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388
    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

  }
1389 1390 1391 1392 1393 1394 1395 1396 1397 1398

  // /**
  //  * return the MIME Type of this MediaMessage
  //  *
  //  */
  // public mimeType(): string | null {
  //   // getType support both 'js' & '.js' as arg
  //   return mime.getType(this.ext())
  // }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427
    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
    }
1428

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1429 1430 1431 1432 1433 1434
    const buffer = <Buffer>await new Promise((resolve, reject) => {
      file.pipe(bl((err: Error, data: Buffer) => {
        if (err) reject(err)
        else resolve(data)
      }))
    })
1435

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1436 1437 1438 1439 1440
    // 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
1441

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1442 1443 1444 1445 1446
    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`)
    }
1447

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467
    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('; '),
    }
1468

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484
    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:        '',
    }
1485

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1486 1487 1488 1489 1490 1491 1492 1493 1494
    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
    }
1495

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
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 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 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 1608 1609 1610 1611 1612 1613 1614 1615 1616
    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 })
  }
1617

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1618 1619 1620 1621 1622 1623 1624 1625
  public async messageSendFile(
    receiver : Receiver,
    file     : FileBox,
  ): Promise<void> {
    log.verbose('PuppetPuppeteer', 'messageSendFile(receiver=%s, file=%s)',
                                    receiver,
                                    file.toString(),
                )
1626

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1627
    let destinationId
1628

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1629 1630 1631 1632 1633 1634 1635
    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?')
    }
1636

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1637 1638
    let mediaData: WebMessageMediaPayload
    let rawPayload = {} as WebMessageRawPayload
1639

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696
    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
    }
  }
1697

1698
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
1699

1700
export {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1701
  WebRoomRawPayload,
1702
}
1703
export default PuppetPuppeteer