message.ts 20.9 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1 2
/**
 *
3
 * Wechaty: * * Wechaty - Wechat for Bot. Connecting ChatBots
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4 5
 *
 * Licenst: ISC
6
 * https://github.com/wechaty/wechaty
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
7 8
 *
 */
M
Mukaiu 已提交
9 10 11 12
import * as moment from 'moment'
import * as fs     from 'fs'
import * as path   from 'path'

13
import {
L
lijiarui 已提交
14 15 16 17 18
  Config,
  RecommendInfo,
  Sayable,
  log,
}  from './config'
19

20 21 22
import { Contact }  from './contact'
import { Room }     from './room'
import { UtilLib }  from './util-lib'
M
Mukaiu 已提交
23 24
import { PuppetWeb }  from './puppet-web/puppet-web'
import { Bridge }     from './puppet-web/bridge'
25

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
26
export type MsgRawObj = {
L
lijiarui 已提交
27
  MsgId:            string,
28

L
lijiarui 已提交
29 30 31 32
  MMActualSender:   string, // getUserContact(message.MMActualSender,message.MMPeerUserName).isContact()
  MMPeerUserName:   string, // message.MsgType == CONF.MSGTYPE_TEXT && message.MMPeerUserName == 'newsapp'
  ToUserName:       string,
  MMActualContent:  string, // Content has @id prefix added by wx
33

L
lijiarui 已提交
34 35
  MMDigest:         string,
  MMDisplayTime:    number,  // Javascript timestamp of milliseconds
36 37 38 39 40

  /**
   * MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
   * class="cover" mm-src="{{getMsgImg(message.MsgId,'slave')}}"
   */
L
lijiarui 已提交
41 42
  Url:              string,
  MMAppMsgDesc:     string,  // class="desc" ng-bind="message.MMAppMsgDesc"
43 44

  /**
45 46
   * Attachment
   *
47 48
   * MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_ATTACH
   */
L
lijiarui 已提交
49 50 51 52 53 54 55 56 57 58 59
  FileName:         string,  // FileName: '钢甲互联项目BP1108.pdf',
  FileSize:         number,  // FileSize: '2845701',
  MediaId:          string,  // MediaId: '@crypt_b1a45e3f_c21dceb3ac01349...

  MMAppMsgFileExt:      string,  // doc, docx ... 'undefined'?
  MMAppMsgFileSize:     string,  // '2.7MB',
  MMAppMsgDownloadUrl:  string,  // 'https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia?sender=@4f549c2dafd5ad731afa4d857bf03c10&mediaid=@crypt_b1a45e3f
                                 // <a download ng-if="message.MMFileStatus == CONF.MM_SEND_FILE_STATUS_SUCCESS
                                 // && (massage.MMStatus == CONF.MSG_SEND_STATUS_SUCC || massage.MMStatus === undefined)
                                 // " href="{{message.MMAppMsgDownloadUrl}}">下载</a>
  MMUploadProgress: number,  // < 100
60 61 62 63 64 65 66 67 68 69

  /**
   * 模板消息
   * MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_READER_TYPE
   *  item.url
   *  item.title
   *  item.pub_time
   *  item.cover
   *  item.digest
   */
L
lijiarui 已提交
70
  MMCategory:       any[],  //  item in message.MMCategory
71 72 73 74 75 76

  /**
   * Type
   *
   * MsgType == CONF.MSGTYPE_VOICE : ng-style="{'width':40 + 7*message.VoiceLength/1000}
   */
L
lijiarui 已提交
77 78 79
  MsgType:          number,
  AppMsgType:       AppMsgType,  // message.MsgType == CONF.MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
                                 // message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType != CONF.MSGTYPE_LOCATION
80

L
lijiarui 已提交
81
  SubMsgType:       MsgType, // "msgType":"{{message.MsgType}}","subType":{{message.SubMsgType||0}},"msgId":"{{message.MsgId}}"
82 83 84 85

  /**
   * Status-es
   */
L
lijiarui 已提交
86 87 88 89 90
  Status:           string,
  MMStatus:         number,  // img ng-show="message.MMStatus == 1" class="ico_loading"
                             // ng-click="resendMsg(message)" ng-show="message.MMStatus == 5" title="重新发送"
  MMFileStatus:     number,  // <p class="loading" ng-show="message.MMStatus == 1 || message.MMFileStatus == CONF.MM_SEND_FILE_STATUS_FAIL">
                             // CONF.MM_SEND_FILE_STATUS_QUEUED, MM_SEND_FILE_STATUS_SENDING
91

92 93 94
  /**
   * Location
   */
L
lijiarui 已提交
95 96 97 98
  MMLocationUrl:    string,  // ng-if="message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType == CONF.MSGTYPE_LOCATION"
                             // <a href="{{message.MMLocationUrl}}" target="_blank">
                             // 'http://apis.map.qq.com/uri/v1/geocoder?coord=40.075041,116.338994'
  MMLocationDesc:   string,  // MMLocationDesc: '北京市昌平区回龙观龙腾苑(五区)内(龙腾街南)',
99 100 101 102 103 104 105 106 107 108 109 110

  /**
   * MsgType == CONF.MSGTYPE_EMOTICON
   *
   * getMsgImg(message.MsgId,'big',message)
   */

  /**
   * Image
   *
   *  getMsgImg(message.MsgId,'slave')
   */
L
lijiarui 已提交
111 112 113
  MMImgStyle:       string,  // ng-style="message.MMImgStyle"
  MMPreviewSrc:     string,  // message.MMPreviewSrc || message.MMThumbSrc || getMsgImg(message.MsgId,'slave')
  MMThumbSrc:       string,
114 115 116 117 118 119 120

  /**
   * Friend Request & ShareCard ?
   *
   * MsgType == CONF.MSGTYPE_SHARECARD" ng-click="showProfile($event,message.RecommendInfo.UserName)
   * MsgType == CONF.MSGTYPE_VERIFYMSG
   */
L
lijiarui 已提交
121
  RecommendInfo?:   RecommendInfo,
122 123
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
124
export type MsgObj = {
L
lijiarui 已提交
125 126 127 128 129 130 131 132 133 134 135
  id:       string,
  type:     MsgType,
  from:     string,
  to?:      string,  // if to is not set, then room must be set
  room?:    string,
  content:  string,
  status:   string,
  digest:   string,
  date:     string,

  url?:     string,  // for MessageMedia class
136 137
}

138 139
// export type MessageTypeName = 'TEXT' | 'IMAGE' | 'VOICE' | 'VERIFYMSG' | 'POSSIBLEFRIEND_MSG'
// | 'SHARECARD' | 'VIDEO' | 'EMOTICON' | 'LOCATION' | 'APP' | 'VOIPMSG' | 'STATUSNOTIFY'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
140
// | 'VOIPNOTIFY' | 'VOIPINVITE' | 'MICROVIDEO' | 'SYSNOTICE' | 'SYS' | 'RECALLED'
141 142 143

// export type MessageTypeValue = 1 | 3 | 34 | 37 | 40 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 62 | 9999 | 10000 | 10002

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
144
export type MsgTypeMap = {
L
lijiarui 已提交
145
  [index: string]: string|number,
146 147 148 149
  //   MessageTypeName:  MessageTypeValue
  // , MessageTypeValue: MessageTypeName
}

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
export const enum AppMsgType {
  TEXT                     = 1,
  IMG                      = 2,
  AUDIO                    = 3,
  VIDEO                    = 4,
  URL                      = 5,
  ATTACH                   = 6,
  OPEN                     = 7,
  EMOJI                    = 8,
  VOICE_REMIND             = 9,
  SCAN_GOOD                = 10,
  GOOD                     = 13,
  EMOTION                  = 15,
  CARD_TICKET              = 16,
  REALTIME_SHARE_LOCATION  = 17,
  TRANSFERS                = 2e3,
  RED_ENVELOPES            = 2001,
  READER_TYPE              = 100001,
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170
export const enum MsgType {
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
  TEXT                = 1,
  IMAGE               = 3,
  VOICE               = 34,
  VERIFYMSG           = 37,
  POSSIBLEFRIEND_MSG  = 40,
  SHARECARD           = 42,
  VIDEO               = 43,
  EMOTICON            = 47,
  LOCATION            = 48,
  APP                 = 49,
  VOIPMSG             = 50,
  STATUSNOTIFY        = 51,
  VOIPNOTIFY          = 52,
  VOIPINVITE          = 53,
  MICROVIDEO          = 62,
  SYSNOTICE           = 9999,
  SYS                 = 10000,
188
  RECALLED            = 10002,
189 190
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
191
export class Message implements Sayable {
192
  public static counter = 0
193
  public _counter: number
194

195 196 197 198 199
  /**
   * a map for:
   *   1. name to id
   *   2. id to name
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
200
  public static TYPE: MsgTypeMap = {
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
    TEXT:               1,
    IMAGE:              3,
    VOICE:              34,
    VERIFYMSG:          37,
    POSSIBLEFRIEND_MSG: 40,
    SHARECARD:          42,
    VIDEO:              43,
    EMOTICON:           47,
    LOCATION:           48,
    APP:                49,
    VOIPMSG:            50,
    STATUSNOTIFY:       51,
    VOIPNOTIFY:         52,
    VOIPINVITE:         53,
    MICROVIDEO:         62,
    SYSNOTICE:          9999,
    SYS:                10000,
L
lijiarui 已提交
218
    RECALLED:           10002,
219 220 221 222
  }

  public readonly id: string

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
223
  protected obj = <MsgObj>{}
224

225
  public readyStream(): Promise<NodeJS.ReadableStream> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
226 227 228
    throw Error('abstract method')
  }

229 230
  public filename(): string {
    throw Error('not a media message')
231 232
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
233
  constructor(public rawObj?: MsgRawObj) {
234
    this._counter = Message.counter++
235
    log.silly('Message', 'constructor() SN:%d', this._counter)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
236

237
    if (typeof rawObj === 'string') {
238
      this.rawObj = JSON.parse(rawObj)
239 240
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
241
    this.rawObj = rawObj = rawObj || <MsgRawObj>{}
242
    this.obj = this.parse(rawObj)
243
    this.id = this.obj.id
244
  }
245

246
  // Transform rawObj to local m
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
247 248
  private parse(rawObj): MsgObj {
    const obj: MsgObj = {
249
      id:           rawObj.MsgId,
250 251 252 253 254 255 256
      type:         rawObj.MsgType,
      from:         rawObj.MMActualSender, // MMPeerUserName
      to:           rawObj.ToUserName,
      content:      rawObj.MMActualContent, // Content has @id prefix added by wx
      status:       rawObj.Status,
      digest:       rawObj.MMDigest,
      date:         rawObj.MMDisplayTime,  // Javascript timestamp of milliseconds
L
lijiarui 已提交
257
      url:          rawObj.Url || rawObj.MMAppMsgDownloadUrl || rawObj.MMLocationUrl,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258
    }
259 260

    // FIXME: has ther any better method to know the room ID?
261 262 263 264 265 266 267
    if (rawObj.MMIsChatRoom) {
      if (/^@@/.test(rawObj.FromUserName)) {
        obj.room =  rawObj.FromUserName // MMPeerUserName always eq FromUserName ?
      } else if (/^@@/.test(rawObj.ToUserName)) {
        obj.room = rawObj.ToUserName
      } else {
        log.error('Message', 'parse found a room message, but neither FromUserName nor ToUserName is a room(/^@@/)')
268
        // obj.room = undefined // bug compatible
269
      }
270 271 272
      if (obj.to && /^@@/.test(obj.to)) { // if a message in room without any specific receiver, then it will set to be `undefined`
        obj.to = undefined
      }
273
    }
274

275
    return obj
276
  }
277
  public toString() {
278
    return UtilLib.plainText(this.obj.content)
279
  }
280
  public toStringDigest() {
281
    const text = UtilLib.digestEmoji(this.obj.digest)
282
    return '{' + this.typeEx() + '}' + text
283 284
  }

285
  public toStringEx() {
286
    let s = `${this.constructor.name}#${this._counter}`
287 288 289
    s += '(' + this.getSenderString()
    s += ':' + this.getContentString() + ')'
    return s
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
290
  }
291
  public getSenderString() {
292 293
    const fromName  = Contact.load(this.obj.from).name()
    const roomTopic = this.obj.room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
294
                  ? (':' + Room.load(this.obj.room).topic())
295
                  : ''
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
296
    return `<${fromName}${roomTopic}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
  }
298
  public getContentString() {
299
    let content = UtilLib.plainText(this.obj.content)
300
    if (content.length > 20) { content = content.substring(0, 17) + '...' }
301 302
    return '{' + this.type() + '}' + content
  }
303

304 305
  public from(contact: Contact): void
  public from(id: string): void
306
  public from(): Contact
307
  public from(contact?: Contact|string): Contact|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
308
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
309 310
      if (contact instanceof Contact) {
        this.obj.from = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
311
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
312 313 314 315
        this.obj.from = contact
      } else {
        throw new Error('unsupport from param: ' + typeof contact)
      }
316
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
317
    }
318 319 320 321 322 323

    const loadedContact = Contact.load(this.obj.from)
    if (!loadedContact) {
      throw new Error('no from')
    }
    return loadedContact
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
324 325
  }

326 327 328
  // public to(room: Room): void
  // public to(): Contact|Room
  // public to(contact?: Contact|Room|string): Contact|Room|void {
329 330
  public to(contact: Contact): void
  public to(id: string): void
331 332 333
  public to(): Contact|null // if to is not set, then room must had set

  public to(contact?: Contact|string): Contact|Room|null|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
334
    if (contact) {
335
      if (contact instanceof Contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
336
        this.obj.to = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
337
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
338 339 340 341
        this.obj.to = contact
      } else {
        throw new Error('unsupport to param ' + typeof contact)
      }
342
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
343
    }
344

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
345 346
    // no parameter

347 348
    if (!this.obj.to) {
      return null
349
    }
350
    return Contact.load(this.obj.to)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
351 352
  }

353 354
  public room(room: Room): void
  public room(id: string): void
355
  public room(): Room|null
356
  public room(room?: Room|string): Room|null|void {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
357 358 359 360 361 362 363 364
    if (room) {
      if (room instanceof Room) {
        this.obj.room = room.id
      } else if (typeof room === 'string') {
        this.obj.room = room
      } else {
        throw new Error('unsupport room param ' + typeof room)
      }
365
      return
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
366
    }
367 368
    if (this.obj.room) {
      return Room.load(this.obj.room)
369
    }
370
    return null
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
371 372
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
373 374 375 376
  public content(): string
  public content(content: string): void

  public content(content?: string): string|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
377 378
    if (content) {
      this.obj.content = content
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
379
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
380 381 382 383
    }
    return this.obj.content
  }

384
  public type(): MsgType {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
385
    return this.obj.type
386 387 388 389 390 391 392 393 394 395 396 397 398 399
  }

  public typeSub(): MsgType {
    if (!this.rawObj) {
      throw new Error('no rawObj')
    }
    return this.rawObj.SubMsgType
  }

  public typeApp(): AppMsgType {
    if (!this.rawObj) {
      throw new Error('no rawObj')
    }
    return this.rawObj.AppMsgType
400 401
  }

402
  public typeEx()  { return Message.TYPE[this.obj.type] }
403
  public count()   { return this._counter }
404

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
405 406 407 408
  public self(): boolean {
    const userId = Config.puppetInstance()
                        .userId

409
    const fromId = this.obj.from
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
410 411 412 413 414 415 416
    if (!userId || !fromId) {
      throw new Error('no user or no from')
    }

    return fromId === userId
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
417 418 419 420 421
  // public ready() {
  //   log.warn('Message', 'ready() DEPRECATED. use load() instead.')
  //   return this.ready()
  // }

422
  public async ready(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
423
    log.silly('Message', 'ready()')
424

425
    try {
426
      const from  = Contact.load(this.obj.from)
427
      await from.ready()                // Contact from
428

429 430 431
      if (this.obj.to) {
        const to = Contact.load(this.obj.to)
        await to.ready()
432
      }
433

434 435
      if (this.obj.room) {
        const room  = Room.load(this.obj.room)
436
        await room.ready()  // Room member list
437
      }
438

439
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
440
        log.error('Message', 'ready() exception: %s', e.stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
441 442 443
        // console.log(e)
        // this.dump()
        // this.dumpRaw()
444
        throw e
445
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
446 447
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
448 449 450
  /**
   * @deprecated
   */
451
  public get(prop: string): string {
452
    log.warn('Message', 'DEPRECATED get() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
453

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
454 455
    if (!prop || !(prop in this.obj)) {
      const s = '[' + Object.keys(this.obj).join(',') + ']'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
456 457
      throw new Error(`Message.get(${prop}) must be in: ${s}`)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
458
    return this.obj[prop]
459 460
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
461 462 463
  /**
   * @deprecated
   */
464
  public set(prop: string, value: string): this {
465
    log.warn('Message', 'DEPRECATED set() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
466

467 468 469
    if (typeof value !== 'string') {
      throw new Error('value must be string, we got: ' + typeof value)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
470
    this.obj[prop] = value
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
471
    return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
472
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
473

474
  public dump() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
475 476
    console.error('======= dump message =======')
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
477
  }
478
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
479
    console.error('======= dump raw message =======')
480
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj && this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
481 482
  }

483
  public static async find(query) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
484
    return Promise.resolve(new Message(<MsgRawObj>{MsgId: '-1'}))
485 486
  }

487 488
  public static async findAll(query) {
    return Promise.resolve([
L
lijiarui 已提交
489 490
      new Message   (<MsgRawObj>{MsgId: '-2'}),
      new Message (<MsgRawObj>{MsgId: '-3'}),
491
    ])
492 493
  }

494 495 496 497 498 499
  public static initType() {
    Object.keys(Message.TYPE).forEach(k => {
      const v = Message.TYPE[k]
      Message.TYPE[v] = k // Message.Type[1] = 'TEXT'
    })
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
500

M
Mukaiu 已提交
501 502
  public say(text: string, replyTo?: Contact | Contact[]): Promise<any>
  public say(mediaMessage: MediaMessage, replyTo?: Contact | Contact[]): Promise<any>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
503

M
Mukaiu 已提交
504 505 506 507 508 509 510 511 512 513
  public say(textOrMedia: string | MediaMessage, replyTo?: Contact|Contact[]): Promise<any> {
    const content = textOrMedia instanceof MediaMessage ? textOrMedia.filename() : textOrMedia
    log.verbose('Message', 'say(%s, %s)', content, replyTo)
    let m
    if (typeof textOrMedia === 'string') {
      m = new Message()
      const room = this.room()
      if (room) {
        m.room(room)
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
514

M
Mukaiu 已提交
515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
      if (!replyTo) {
        m.to(this.from())
        m.content(textOrMedia)

      } else if (this.room()) {
        let mentionList
        if (Array.isArray(replyTo)) {
          m.to(replyTo[0])
          mentionList = replyTo.map(c => '@' + c.name()).join(' ')
        } else {
          m.to(replyTo)
          mentionList = '@' + replyTo.name()
        }
        m.content(mentionList + ' ' + textOrMedia)
      }
    } else if (textOrMedia instanceof MediaMessage) {
      m = textOrMedia
      const room = this.room()
      if (room) {
        m.room(room)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
535 536
      }

M
Mukaiu 已提交
537 538 539
      if (!replyTo) {
        m.to(this.from())
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
540
    }
M
Mukaiu 已提交
541

542
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
543 544 545
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
546
}
547 548

Message.initType()
549

M
Mukaiu 已提交
550 551 552 553 554 555
export class MediaMessage extends Message {
  private bridge: Bridge
  private fileStream: NodeJS.ReadableStream
  private fileName: string // 'music'
  private fileExt: string // 'mp3'

556
  constructor(rawObj: Object)
M
Mukaiu 已提交
557 558
  constructor(filePath: string)

559
  constructor(rawObjOrFilePath: Object | string) {
M
Mukaiu 已提交
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 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 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740
    if (typeof rawObjOrFilePath === 'string') {
      super()
      this.fileStream = fs.createReadStream(rawObjOrFilePath)

      const pathInfo = path.parse(rawObjOrFilePath)
      this.fileName = pathInfo.name
      this.fileExt = pathInfo.ext.replace(/^\./, '')
    } else if (rawObjOrFilePath instanceof Object) {
      super(rawObjOrFilePath as any)
    } else {
      throw new Error('not supported construct param')
    }

    // FIXME: decoupling needed
    this.bridge = (Config.puppetInstance() as PuppetWeb)
      .bridge
  }

  public async ready(): Promise<void> {
    log.silly('MediaMessage', 'ready()')

    try {
      await super.ready()

      let url: string|null = null
      switch (this.type()) {
        case MsgType.EMOTICON:
          url = await this.bridge.getMsgEmoticon(this.id)
          break
        case MsgType.IMAGE:
          url = await this.bridge.getMsgImg(this.id)
          break
        case MsgType.VIDEO:
        case MsgType.MICROVIDEO:
          url = await this.bridge.getMsgVideo(this.id)
          break
        case MsgType.VOICE:
          url = await this.bridge.getMsgVoice(this.id)
          break

        case MsgType.APP:
          if (!this.rawObj) {
            throw new Error('no rawObj')
          }
          switch (this.typeApp()) {
            case AppMsgType.ATTACH:
              if (!this.rawObj.MMAppMsgDownloadUrl) {
                throw new Error('no MMAppMsgDownloadUrl')
              }
              // had set in Message
              // url = this.rawObj.MMAppMsgDownloadUrl
              break

            case AppMsgType.URL:
            case AppMsgType.READER_TYPE:
              if (!this.rawObj.Url) {
                throw new Error('no Url')
              }
              // had set in Message
              // url = this.rawObj.Url
              break

            default:
              const e = new Error('ready() unsupported typeApp(): ' + this.typeApp())
              log.warn('MediaMessage', e.message)
              this.dumpRaw()
              throw e
          }
          break

        case MsgType.TEXT:
          if (this.typeSub() === MsgType.LOCATION) {
            url = await this.bridge.getMsgPublicLinkImg(this.id)
          }
          break

        default:
          throw new Error('not support message type for MediaMessage')
      }

      if (!url) {
        if (!this.obj.url) {
          throw new Error('no obj.url')
        }
        url = this.obj.url
      }

      this.obj.url = url

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

  public ext(): string {
    if (this.fileExt)
      return this.fileExt

    switch (this.type()) {
      case MsgType.EMOTICON:
        return 'gif'

      case MsgType.IMAGE:
        return 'jpg'

      case MsgType.VIDEO:
      case MsgType.MICROVIDEO:
        return 'mp4'

      case MsgType.VOICE:
        return 'mp3'

      case MsgType.APP:
        switch (this.typeApp()) {
          case AppMsgType.URL:
            return 'url' // XXX
        }
        break

      case MsgType.TEXT:
        if (this.typeSub() === MsgType.LOCATION) {
          return 'jpg'
        }
        break
    }
    throw new Error('not support type: ' + this.type())
  }

  public filename(): string {
    if (this.fileName && this.fileExt) {
      return this.fileName + '.' + this.fileExt
    }

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

    const objFileName = this.rawObj.FileName || this.rawObj.MediaId || this.rawObj.MsgId

    let filename  = moment().format('YYYY-MM-DD HH:mm:ss')
                    + ' #' + this._counter
                    + ' ' + this.getSenderString()
                    + ' ' + objFileName

    filename = filename.replace(/ /g, '_')

    const re = /\.[a-z0-9]{1,7}$/i
    if (!re.test(filename)) {
      const ext = this.rawObj.MMAppMsgFileExt || this.ext()
      filename += '.' + ext
    }
    return filename
  }

  // private getMsgImg(id: string): Promise<string> {
  //   return this.bridge.getMsgImg(id)
  //   .catch(e => {
  //     log.warn('MediaMessage', 'getMsgImg(%d) exception: %s', id, e.message)
  //     throw e
  //   })
  // }

  public async readyStream(): Promise<NodeJS.ReadableStream> {
    if (this.fileStream)
      return this.fileStream

    try {
      await this.ready()
      // FIXME: decoupling needed
      const cookies = await (Config.puppetInstance() as PuppetWeb).browser.readCookie()
      if (!this.obj.url) {
        throw new Error('no url')
      }
      return UtilLib.urlStream(this.obj.url, cookies)
    } catch (e) {
      log.warn('MediaMessage', 'stream() exception: %s', e.stack)
      throw e
    }
  }
}
741

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
742
/*
743 744 745 746 747
 * join room in mac client: https://support.weixin.qq.com/cgi-bin/
 * mmsupport-bin/addchatroombyinvite
 * ?ticket=AUbv%2B4GQA1Oo65ozlIqRNw%3D%3D&exportkey=AS9GWEg4L82fl3Y8e2OeDbA%3D
 * &lang=en&pass_ticket=T6dAZXE27Y6R29%2FFppQPqaBlNwZzw9DAN5RJzzzqeBA%3D
 * &wechat_real_lang=en
Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
748
 */