puppet-web.ts 28.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
import cloneClass   from 'clone-class'
import {
  ThrottleQueue,
}                   from 'rx-queue'
23 24 25 26 27
import {
  Watchdog,
  WatchdogFood,
}                   from 'watchdog'

28
import {
29
  config,
30 31
  log,
  Raven,
32 33
}                   from '../config'
import Contact      from '../contact'
M
Mukaiu 已提交
34 35 36
import {
  Message,
  MediaMessage,
37 38
}                   from '../message'
import Profile      from '../profile'
39 40
import {
  Puppet,
41
  PuppetOptions,
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
42
  ScanData,
43 44 45
}                   from '../puppet'
import Room         from '../room'
import Misc         from '../misc'
46 47
import {
  Bridge,
48
  Cookie,
49 50
}                   from './bridge'
import Event        from './event'
51 52

import {
53
  MediaData,
54 55 56
  MsgRawObj,
  MediaType,
}                     from './schema'
57

M
Mukaiu 已提交
58 59 60
import * as request from 'request'
import * as bl from 'bl'

61 62
export type PuppetFoodType = 'scan' | 'ding'
export type ScanFoodType   = 'scan' | 'login' | 'logout'
63

64
export class PuppetWeb extends Puppet {
65
  public bridge   : Bridge
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
66
  public scanInfo : ScanData | null
67

68 69
  public puppetWatchdog : Watchdog<PuppetFoodType>
  public scanWatchdog   : Watchdog<ScanFoodType>
70

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

73 74 75 76
  constructor(
    public options: PuppetOptions,
  ) {
    super(options)
77
    this.fileId = 0
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
78

79
    const PUPPET_TIMEOUT  = 1 * 60 * 1000  // 1 minute
80
    this.puppetWatchdog   = new Watchdog<PuppetFoodType>(PUPPET_TIMEOUT, 'PuppetWeb')
81

82
    const SCAN_TIMEOUT  = 2 * 60 * 1000 // 2 minutes
83
    this.scanWatchdog   = new Watchdog<ScanFoodType>(SCAN_TIMEOUT, 'Scan')
84 85
  }

86 87 88
  public toString() {
    return `PuppetWeb<${this.options.profile.name}>`
  }
89

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

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

95
    try {
96 97
      this.initWatchdogForPuppet()
      this.initWatchdogForScan()
98 99

      this.bridge = await this.initBridge(this.options.profile)
100 101
      log.verbose('PuppetWeb', 'initBridge() done')

102 103 104 105
      /**
       *  state must set to `live`
       *  before feed Watchdog
       */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106
      this.state.on(true)
107

108
      const food: WatchdogFood = {
L
lijiarui 已提交
109 110
        data: 'inited',
        timeout: 2 * 60 * 1000, // 2 mins for first login
111 112
      }
      this.emit('watchdog', food)
113

114 115
      const throttleQueue = new ThrottleQueue(5 * 60 * 1000)
      this.on('heartbeat', data => throttleQueue.next(data))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
116
      throttleQueue.subscribe(async data => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
117
        log.verbose('Wechaty', 'start() throttleQueue.subscribe() new item: %s', data)
118 119 120
        await this.saveCookie()
      })

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
121
      log.verbose('PuppetWeb', 'start() done')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
122
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
123

124
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
125
      log.error('PuppetWeb', 'start() exception: %s', e)
126

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
127
      this.state.off(true)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
128
      this.emit('error', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
129
      await this.stop()
130

131
      Raven.captureException(e)
132
      throw e
133
    }
134 135
  }

136
  public initWatchdogForPuppet(): void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
137 138
    log.verbose('PuppetWeb', 'initWatchdogForPuppet()')

139
    const puppet = this
140
    const dog    = this.puppetWatchdog
141

142 143 144
    // clean the dog because this could be re-inited
    dog.removeAllListeners()

145
    puppet.on('watchdog', food => dog.feed(food))
146
    dog.on('feed', food => {
147
      log.silly('PuppetWeb', 'initWatchdogForPuppet() dog.on(feed, food={type=%s, data=%s})', food.type, food.data)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
148
      // feed the dog, heartbeat the puppet.
149 150 151
      puppet.emit('heartbeat', food.data)
    })

152 153 154
    dog.on('reset', async (food, timeout) => {
      log.warn('PuppetWeb', 'initWatchdogForPuppet() dog.on(reset) last food:%s, timeout:%s',
                            food.data, timeout)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
155
      try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
156 157
        await this.stop()
        await this.start()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
158 159 160
      } catch (e) {
        puppet.emit('error', e)
      }
161 162 163 164 165 166 167 168 169 170 171
    })
  }

  /**
   * 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 {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
172 173
    log.verbose('PuppetWeb', 'initWatchdogForScan()')

174
    const puppet = this
175
    const dog    = this.scanWatchdog
176

177 178 179
    // clean the dog because this could be re-inited
    dog.removeAllListeners()

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
    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 (李卓桓) 已提交
200 201 202
    dog.on('reset', async (food, timePast) => {
      log.warn('PuppetWeb', 'initScanWatchdog() on(reset) lastFood: %s, timePast: %s',
                            food.data, timePast)
203 204 205
      try {
        await this.bridge.reload()
      } catch (e) {
206
        log.error('PuppetWeb', 'initScanWatchdog() on(reset) exception: %s', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
207 208 209 210 211 212 213 214 215
        try {
          log.error('PuppetWeb', 'initScanWatchdog() on(reset) try to recover by bridge.{quit,init}()', e)
          await this.bridge.quit()
          await this.bridge.init()
          log.error('PuppetWeb', 'initScanWatchdog() on(reset) recover successful')
        } catch (e) {
          log.error('PuppetWeb', 'initScanWatchdog() on(reset) recover FAIL: %s', e)
          this.emit('error', e)
        }
216 217 218 219
      }
    })
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
220
  public async stop(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
221
    log.verbose('PuppetWeb', 'quit()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
222

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
223 224 225 226
    if (this.state.off()) {
      log.warn('PuppetWeb', 'quit() is called on a OFF puppet. await ready(off) and return.')
      await this.state.ready('off')
      return
227 228
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
229
    log.verbose('PuppetWeb', 'quit() make watchdog sleep before do quit')
230
    this.puppetWatchdog.sleep()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
231
    this.scanWatchdog.sleep()
232

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
233
    this.state.off('pending')
234

235
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
236 237 238
      await this.bridge.quit()
      // register the removeListeners micro task at then end of the task queue
      setImmediate(() => this.bridge.removeAllListeners())
239
    } catch (e) {
240
      log.error('PuppetWeb', 'quit() exception: %s', e.message)
241
      Raven.captureException(e)
242
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
243
    } finally {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
244
      this.state.off(true)
245
    }
246 247
  }

248
  public async initBridge(profile: Profile): Promise<Bridge> {
249
    log.verbose('PuppetWeb', 'initBridge()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
250

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
251
    if (this.state.off()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252 253 254
      const e = new Error('initBridge() found targetState != live, no init anymore')
      log.warn('PuppetWeb', e.message)
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
256

257
    const head = config.head
258 259 260 261 262 263
    // we have to set this.bridge right now,
    // because the Event.onXXX might arrive while we are initializing.
    this.bridge = new Bridge({
      head,
      profile,
    })
264

265
    this.bridge.on('ding'     , Event.onDing.bind(this))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
266
    this.bridge.on('error'    , e => this.emit('error', e))
267 268 269 270 271
    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))
272
    this.bridge.on('unload'   , Event.onUnload.bind(this))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
273

274
    try {
275
      await this.bridge.init()
276
    } catch (e) {
277
      log.error('PuppetWeb', 'initBridge() exception: %s', e.message)
278
      await this.bridge.quit().catch(console.error)
279 280 281 282
      this.emit('error', e)

      Raven.captureException(e)
      throw e
283
    }
284 285

    return this.bridge
286 287
  }

288
  public logined(): boolean {
289 290
    log.warn('PuppetWeb', 'logined() DEPRECATED. use logonoff() instead.')
    return this.logonoff()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
291 292
  }

293
  public logonoff(): boolean {
294 295
    return !!(this.user)
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
296

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298
   * get self contact
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
299
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300 301
  public self(): Contact {
    log.verbose('PuppetWeb', 'self()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
302

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
303 304
    if (this.user) {
      return this.user
305
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306
    throw new Error('PuppetWeb.self() no this.user')
307
  }
308

M
Mukaiu 已提交
309 310
  private async getBaseRequest(): Promise<any> {
    try {
311
      const json = await this.bridge.getBaseRequest()
312
      const obj = JSON.parse(json)
M
Mukaiu 已提交
313 314 315
      return obj.BaseRequest
    } catch (e) {
      log.error('PuppetWeb', 'send() exception: %s', e.message)
316
      Raven.captureException(e)
M
Mukaiu 已提交
317 318 319
      throw e
    }
  }
320

321
  private async uploadMedia(mediaMessage: MediaMessage, toUserName: string): Promise<MediaData> {
M
Mukaiu 已提交
322 323 324
    if (!mediaMessage)
      throw new Error('require mediaMessage')

325
    const filename = mediaMessage.filename()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
326
    const ext      = mediaMessage.ext()
M
Mukaiu 已提交
327

328
    // const contentType = Misc.mime(ext)
329 330 331 332 333
    // const contentType = mime.getType(ext)
    const contentType = mediaMessage.mimeType()
    if (!contentType) {
      throw new Error('no MIME Type found on mediaMessage: ' + mediaMessage.filename())
    }
M
Mukaiu 已提交
334 335 336 337 338 339 340
    let mediatype: MediaType

    switch (ext) {
      case 'bmp':
      case 'jpeg':
      case 'jpg':
      case 'png':
M
Mukaiu 已提交
341
      case 'gif':
342
        mediatype = MediaType.IMAGE
M
Mukaiu 已提交
343 344
        break
      case 'mp4':
345
        mediatype = MediaType.VIDEO
M
Mukaiu 已提交
346 347
        break
      default:
348
        mediatype = MediaType.ATTACHMENT
M
Mukaiu 已提交
349 350
    }

351 352
    const readStream = await mediaMessage.readyStream()
    const buffer = <Buffer>await new Promise((resolve, reject) => {
M
Mukaiu 已提交
353 354 355 356 357 358
      readStream.pipe(bl((err, data) => {
        if (err) reject(err)
        else resolve(data)
      }))
    })

M
Mukaiu 已提交
359 360
    // Sending video files is not allowed to exceed 20MB
    // https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L1115
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
361 362 363 364 365 366 367 368
    const MAX_FILE_SIZE   = 100 * 1024 * 1024
    const LARGE_FILE_SIZE = 25 * 1024 * 1024
    const MAX_VIDEO_SIZE  = 20 * 1024 * 1024

    if (mediatype === MediaType.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`)
369
    }
M
Mukaiu 已提交
370

371
    const md5 = Misc.md5(buffer)
M
Mukaiu 已提交
372

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
373 374 375 376 377 378
    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')
379
    const webwxDataTicket = first && first.value
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
380 381 382
    const size            = buffer.length
    const fromUserName    = this.self().id
    const id              = 'WU_FILE_' + this.fileId
383
    this.fileId++
M
Mukaiu 已提交
384

385
    const hostname = await this.bridge.hostname()
386 387 388 389 390 391 392 393
    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('; '),
    }

    log.silly('PuppetWeb', 'uploadMedia() headers:%s', JSON.stringify(headers))

394
    const uploadMediaRequest = {
395 396 397 398 399
      BaseRequest:   baseRequest,
      FileMd5:       md5,
      FromUserName:  fromUserName,
      ToUserName:    toUserName,
      UploadType:    2,
M
Mukaiu 已提交
400
      ClientMediaId: +new Date,
401
      MediaType:     MediaType.ATTACHMENT,
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
      StartPos:      0,
      DataLen:       size,
      TotalLen:      size,
      Signature:     '',
      AESKey:        '',
    }

    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
M
Mukaiu 已提交
417 418
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
419
    const mediaData = {
420
      ToUserName: toUserName,
421 422 423 424 425
      MediaId:    '',
      FileName:   filename,
      FileSize:   size,
      FileMd5:    md5,
      MMFileExt:  ext,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
426
    } as MediaData
427 428 429

    // 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
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
430
    if (size > LARGE_FILE_SIZE) {
431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
      let ret
      try {
        ret = <any> await new Promise((resolve, reject) => {
          const r = {
            url: `https://${hostname}${checkUploadUrl}`,
            headers,
            json: checkData,
          }
          request.post(r, function (err, res, body) {
            try {
              if (err) {
                reject(err)
              } else {
                let obj = body
                if (typeof body !== 'object') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
446 447 448 449
                  log.silly('PuppetWeb', 'updateMedia() typeof body = %s', typeof body)
                  try {
                    obj = JSON.parse(body)
                  } catch (e) {
450
                    log.error('PuppetWeb', 'updateMedia() body = %s', body)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
451 452 453
                    log.error('PuppetWeb', 'updateMedia() exception: %s', e)
                    this.emit('error', e)
                  }
454 455 456 457 458 459 460
                }
                if (typeof obj !== 'object' || obj.BaseResponse.Ret !== 0) {
                  const errMsg = obj.BaseResponse || 'api return err'
                  log.silly('PuppetWeb', 'uploadMedia() checkUpload err:%s \nreq:%s\nret:%s', JSON.stringify(errMsg), JSON.stringify(r), body)
                  reject(new Error('chackUpload err:' + JSON.stringify(errMsg)))
                }
                resolve({
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
461 462
                  Signature : obj.Signature,
                  AESKey    : obj.AESKey,
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
                })
              }
            } catch (e) {
              reject(e)
            }
          })
        })
      } catch (e) {
        log.error('PuppetWeb', 'uploadMedia() checkUpload exception: %s', e.message)
        throw e
      }
      if (!ret.Signature) {
        log.error('PuppetWeb', '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
484 485
    }

Huan (李卓桓)'s avatar
add log  
Huan (李卓桓) 已提交
486 487 488
    log.verbose('PuppetWeb', 'uploadMedia() webwx_data_ticket: %s', webwxDataTicket)
    log.verbose('PuppetWeb', 'uploadMedia() pass_ticket: %s', passTicket)

489
    const formData = {
490
      id,
M
Mukaiu 已提交
491 492 493
      name: filename,
      type: contentType,
      lastModifiedDate: Date().toString(),
494
      size,
M
Mukaiu 已提交
495 496 497
      mediatype,
      uploadmediarequest: JSON.stringify(uploadMediaRequest),
      webwx_data_ticket: webwxDataTicket,
498
      pass_ticket: passTicket || '',
M
Mukaiu 已提交
499 500 501 502 503 504 505 506 507
      filename: {
        value: buffer,
        options: {
          filename,
          contentType,
          size,
        },
      },
    }
508 509 510
    let mediaId
    try {
      mediaId = <string>await new Promise((resolve, reject) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
511
        try {
512 513 514 515 516 517 518 519 520 521 522 523 524 525
          request.post({
            url: uploadMediaUrl + '?f=json',
            headers,
            formData,
          }, function (err, res, body) {
            if (err) { reject(err) }
            else {
              let obj = body
              if (typeof body !== 'object') {
                obj = JSON.parse(body)
              }
              resolve(obj.MediaId || '')
            }
          })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
526
        } catch (e) {
527
          reject(e)
M
Mukaiu 已提交
528 529
        }
      })
530 531 532 533
    } catch (e) {
      log.error('PuppetWeb', 'uploadMedia() uploadMedia exception: %s', e.message)
      throw new Error('uploadMedia err: ' + e.message)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
534
    if (!mediaId) {
535 536
      log.error('PuppetWeb', 'uploadMedia(): upload fail')
      throw new Error('PuppetWeb.uploadMedia(): upload fail')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
537
    }
538
    return Object.assign(mediaData, { MediaId: mediaId as string })
M
Mukaiu 已提交
539 540
  }

541
  public async sendMedia(message: MediaMessage): Promise<boolean> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
542
    const to   = message.to()
M
Mukaiu 已提交
543
    const room = message.room()
544 545

    let destinationId
546 547

    if (room) {
548
      destinationId = room.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
549 550
    } else {
      if (!to) {
551
        throw new Error('PuppetWeb.sendMedia(): message with neither room nor to?')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
552 553
      }
      destinationId = to.id
554 555
    }

556
    let mediaData: MediaData
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
557 558
    const rawObj = message.rawObj as MsgRawObj
    if (!rawObj.MediaId) {
559 560
      try {
        mediaData = await this.uploadMedia(message, destinationId)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
561
        message.rawObj = Object.assign(rawObj, mediaData)
562 563 564 565 566 567 568
        log.silly('PuppetWeb', 'Upload completed, new rawObj:%s', JSON.stringify(message.rawObj))
      } catch (e) {
        log.error('PuppetWeb', 'sendMedia() exception: %s', e.message)
        return false
      }
    } else {
      // To support forward file
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
569
      log.silly('PuppetWeb', 'skip upload file, rawObj:%s', JSON.stringify(rawObj))
570
      mediaData = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
571 572 573 574 575 576
        ToUserName : destinationId,
        MediaId    : rawObj.MediaId,
        MsgType    : rawObj.MsgType,
        FileName   : rawObj.FileName,
        FileSize   : rawObj.FileSize,
        MMFileExt  : rawObj.MMFileExt,
577
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
578 579
      if (rawObj.Signature) {
        mediaData.Signature = rawObj.Signature
580 581
      }
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
582 583 584 585 586
    // console.log('mediaData.MsgType', mediaData.MsgType)
    // console.log('rawObj.MsgType', message.rawObj && message.rawObj.MsgType)

    mediaData.MsgType = Misc.msgType(message.ext())
    log.silly('PuppetWeb', 'sendMedia() destination: %s, mediaId: %s, MsgType; %s)',
M
Mukaiu 已提交
587
      destinationId,
588
      mediaData.MediaId,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
589
      mediaData.MsgType,
590
    )
591
    let ret = false
592
    try {
593
      ret = await this.bridge.sendMedia(mediaData)
Huan (李卓桓)'s avatar
linting  
Huan (李卓桓) 已提交
594
    } catch (e) {
595
      log.error('PuppetWeb', 'sendMedia() exception: %s', e.message)
596
      Raven.captureException(e)
597
      return false
598
    }
599
    return ret
M
Mukaiu 已提交
600 601
  }

602
  /**
603
   * TODO: Test this function if it could work...
604 605 606
   */
  // public async forward(baseData: MsgRawObj, patchData: MsgRawObj): Promise<boolean> {
  public async forward(message: MediaMessage, sendTo: Contact | Room): Promise<boolean> {
607

608 609 610 611
    log.silly('PuppetWeb', 'forward() to: %s, message: %s)',
      sendTo, message.filename(),
      // patchData.ToUserName,
      // patchData.MMActualContent,
612
    )
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660

    if (!message.rawObj) {
      throw new Error('no rawObj')
    }

    let m = Object.assign({}, message.rawObj)
    const newMsg = <MsgRawObj>{}
    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')
    // }
    if (m.FileSize >= largeFileSize && !m.Signature) {
      // 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.')
      return false
    }

    newMsg.FromUserName         = this.userId || ''
    newMsg.isTranspond          = true
    newMsg.MsgIdBeforeTranspond = m.MsgIdBeforeTranspond || m.MsgId
    newMsg.MMSourceMsgId        = m.MsgId
    // 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
    newMsg.Content      = Misc.unescapeHtml(m.Content.replace(/^@\w+:<br\/>/, '')).replace(/^[\w\-]+:<br\/>/, '')
    newMsg.MMIsChatRoom = sendTo instanceof Room ? true : false

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

    m = Object.assign(m, newMsg)
    // 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)
    // }
    newMsg.ToUserName = sendTo.id
    // ret = await config.puppetInstance().forward(m, newMsg)
    // return ret
    const baseData  = m
    const patchData = newMsg

661 662 663 664 665 666 667 668 669 670 671
    let ret = false
    try {
      ret = await this.bridge.forward(baseData, patchData)
    } catch (e) {
      log.error('PuppetWeb', 'forward() exception: %s', e.message)
      Raven.captureException(e)
      throw e
    }
    return ret
  }

672
   public async send(message: Message | MediaMessage): Promise<boolean> {
M
Mukaiu 已提交
673 674 675 676 677 678 679 680 681 682 683 684 685 686
    const to   = message.to()
    const room = message.room()

    let destinationId

    if (room) {
      destinationId = room.id
    } else {
      if (!to) {
        throw new Error('PuppetWeb.send(): message with neither room nor to?')
      }
      destinationId = to.id
    }

687 688
    let ret = false

M
Mukaiu 已提交
689
    if (message instanceof MediaMessage) {
690
      ret = await this.sendMedia(message)
M
Mukaiu 已提交
691 692 693 694 695 696 697 698 699
    } else {
      const content = message.content()

      log.silly('PuppetWeb', 'send() destination: %s, content: %s)',
        destinationId,
        content,
      )

      try {
700
        ret = await this.bridge.send(destinationId, content)
M
Mukaiu 已提交
701 702
      } catch (e) {
        log.error('PuppetWeb', 'send() exception: %s', e.message)
703
        Raven.captureException(e)
M
Mukaiu 已提交
704 705 706
        throw e
      }
    }
707
    return ret
M
Mukaiu 已提交
708
  }
709

710 711
  /**
   * Bot say...
712
   * send to `self` for notice / log
713
   */
714
  public async say(content: string): Promise<boolean> {
715
    if (!this.logonoff()) {
716 717 718
      throw new Error('can not say before login')
    }

719 720 721 722 723
    if (!content) {
      log.warn('PuppetWeb', 'say(%s) can not say nothing', content)
      return false
    }

724 725 726 727 728 729 730 731 732
    if (!this.user) {
      log.warn('PuppetWeb', 'say(%s) can not say because no user', content)
      this.emit('error', new Error('no this.user for PuppetWeb'))
      return false
    }

    // const m = new Message()
    // m.to('filehelper')
    // m.content(content)
733

734 735
    // return await this.send(m)
    return await this.user.say(content)
736 737
  }

738 739 740
  /**
   * logout from browser, then server will emit `logout` event
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
741
  public async logout(): Promise<void> {
742 743 744
    log.verbose('PuppetWeb', 'logout()')

    const data = this.user || this.userId || ''
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
745
    this.userId = this.user = undefined
746

747 748
    try {
      await this.bridge.logout()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
749
      this.emit('logout', data)
750 751
    } catch (e) {
      log.error('PuppetWeb', 'logout() exception: %s', e.message)
752
      Raven.captureException(e)
753 754
      throw e
    }
755 756
  }

757
  public async getContact(id: string): Promise<object> {
758 759
    try {
      return await this.bridge.getContact(id)
Huan (李卓桓)'s avatar
linting  
Huan (李卓桓) 已提交
760
    } catch (e) {
761
      log.error('PuppetWeb', 'getContact(%d) exception: %s', id, e.message)
762
      Raven.captureException(e)
763
      throw e
764
    }
765
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
766

767 768 769 770 771
  public async ding(data?: any): Promise<string> {
    try {
      return await this.bridge.ding(data)
    } catch (e) {
      log.warn('PuppetWeb', 'ding(%s) rejected: %s', data, e.message)
772
      Raven.captureException(e)
773
      throw e
774 775
    }
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
776

777
  public async contactAlias(contact: Contact, remark: string|null): Promise<boolean> {
778 779 780
    try {
      const ret = await this.bridge.contactRemark(contact.id, remark)
      if (!ret) {
L
lijiarui 已提交
781 782
        log.warn('PuppetWeb', 'contactRemark(%s, %s) bridge.contactRemark() return false',
                              contact.id, remark,
783 784 785 786 787
        )
      }
      return ret

    } catch (e) {
788
      log.warn('PuppetWeb', 'contactRemark(%s, %s) rejected: %s', contact.id, remark, e.message)
789
      Raven.captureException(e)
790 791 792 793
      throw e
    }
  }

794 795 796
  public async contactFind(filterFunc: string): Promise<Contact[]> {
    try {
      const idList = await this.bridge.contactFind(filterFunc)
797 798 799 800 801
      return idList.map(id => {
        const c = Contact.load(id)
        c.puppet = this
        return c
      })
802 803 804 805 806
    } catch (e) {
      log.warn('PuppetWeb', 'contactFind(%s) rejected: %s', filterFunc, e.message)
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
807 808
  }

809 810 811
  public async roomFind(filterFunc: string): Promise<Room[]> {
    try {
      const idList = await this.bridge.roomFind(filterFunc)
812 813 814 815 816
      return idList.map(id => {
        const r = Room.load(id)
        r.puppet = this
        return r
      })
817 818 819 820 821
    } catch (e) {
      log.warn('PuppetWeb', 'roomFind(%s) rejected: %s', filterFunc, e.message)
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
822 823
  }

824
  public async roomDel(room: Room, contact: Contact): Promise<number> {
825 826
    const roomId    = room.id
    const contactId = contact.id
827 828 829 830 831 832 833
    try {
      return await this.bridge.roomDelMember(roomId, contactId)
    } catch (e) {
      log.warn('PuppetWeb', 'roomDelMember(%s, %d) rejected: %s', roomId, contactId, e.message)
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
834 835
  }

836
  public async roomAdd(room: Room, contact: Contact): Promise<number> {
837 838
    const roomId    = room.id
    const contactId = contact.id
839 840 841 842 843 844 845
    try {
      return await this.bridge.roomAddMember(roomId, contactId)
    } catch (e) {
      log.warn('PuppetWeb', 'roomAddMember(%s) rejected: %s', contact, e.message)
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
846
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
847

848
  public async roomTopic(room: Room, topic: string): Promise<string> {
849
    if (!room || typeof topic === 'undefined') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
850
      return Promise.reject(new Error('room or topic not found'))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
851
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
852 853

    const roomId = room.id
854 855 856 857 858 859 860
    try {
      return await this.bridge.roomModTopic(roomId, topic)
    } catch (e) {
      log.warn('PuppetWeb', 'roomTopic(%s) rejected: %s', topic, e.message)
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
861 862
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
863
  public async roomCreate(contactList: Contact[], topic: string): Promise<Room> {
864
    if (!contactList || ! contactList.map) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
865 866 867
      throw new Error('contactList not found')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
868
    const contactIdList = contactList.map(c => c.id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
869

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
870 871
    try {
      const roomId = await this.bridge.roomCreate(contactIdList, topic)
872
      if (!roomId) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
873 874
        throw new Error('PuppetWeb.roomCreate() roomId "' + roomId + '" not found')
      }
875 876 877
      const r = Room.load(roomId)
      r.puppet = this
      return r
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
878 879 880

    } catch (e) {
      log.warn('PuppetWeb', 'roomCreate(%s, %s) rejected: %s', contactIdList.join(','), topic, e.message)
881
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
882 883
      throw e
    }
884 885 886 887 888
  }

  /**
   * FriendRequest
   */
889
  public async friendRequestSend(contact: Contact, hello: string): Promise<boolean> {
890 891
    if (!contact) {
      throw new Error('contact not found')
892
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
893

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
894 895 896 897
    try {
      return await this.bridge.verifyUserRequest(contact.id, hello)
    } catch (e) {
      log.warn('PuppetWeb', 'bridge.verifyUserRequest(%s, %s) rejected: %s', contact.id, hello, e.message)
898
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
899 900
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
901 902
  }

903
  public async friendRequestAccept(contact: Contact, ticket: string): Promise<boolean> {
904 905
    if (!contact || !ticket) {
      throw new Error('contact or ticket not found')
906 907
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
908 909 910 911
    try {
      return await this.bridge.verifyUserOk(contact.id, ticket)
    } catch (e) {
      log.warn('PuppetWeb', 'bridge.verifyUserOk(%s, %s) rejected: %s', contact.id, ticket, e.message)
912
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
913 914
      throw e
    }
915
  }
916 917 918 919 920 921

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

925 926 927 928
    // tslint:disable-next-line:variable-name
    const MyContact = cloneClass(Contact)
    MyContact.puppet = this

929
    async function stable(done: Function): Promise<void> {
Huan (李卓桓)'s avatar
add log  
Huan (李卓桓) 已提交
930
      log.silly('PuppetWeb', 'readyStable() stable() counter=%d', counter)
931 932

      const contactList = await MyContact.findAll()
933
      if (counter === contactList.length) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
934
        log.verbose('PuppetWeb', 'readyStable() stable() READY counter=%d', counter)
935
        return done()
936 937
      }
      counter = contactList.length
938
      setTimeout(() => stable(done), 1000)
939 940 941 942
        .unref()
    }

    return new Promise<void>((resolve, reject) => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
943 944 945 946 947 948
      const timer = setTimeout(() => {
        log.warn('PuppetWeb', 'readyStable() stable() reject at counter=%d', counter)
        return reject(new Error('timeout after 60 seconds'))
      }, 60 * 1000)
      timer.unref()

949
      const done = () => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
950
        clearTimeout(timer)
951
        return resolve()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
952
      }
953

954
      return stable(done)
955 956 957
    })

  }
958

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
959 960 961 962 963 964
  /**
   * https://www.chatie.io:8080/api
   * location.hostname = www.chatie.io
   * location.host = www.chatie.io:8080
   * See: https://stackoverflow.com/a/11379802/1123955
   */
965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987
  public async hostname(): Promise<string> {
    try {
      const name = await this.bridge.hostname()
      if (!name) {
        throw new Error('no hostname found')
      }
      return name
    } catch (e) {
      log.error('PuppetWeb', 'hostname() exception:%s', e)
      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()
  }
988
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
989 990

export default PuppetWeb