message.ts 23.5 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
import * as fs     from 'fs'
import * as path   from 'path'

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

Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
19 20 21 22 23
import Contact    from './contact'
import Room       from './room'
import UtilLib    from './util-lib'
import PuppetWeb  from './puppet-web/puppet-web'
import Bridge     from './puppet-web/bridge'
24

25
export interface MsgRawObj {
L
lijiarui 已提交
26
  MsgId:            string,
27

L
lijiarui 已提交
28 29 30 31
  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
32

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

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

  /**
44 45
   * Attachment
   *
46 47
   * MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_ATTACH
   */
L
lijiarui 已提交
48 49 50 51 52 53 54 55 56 57 58
  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
59 60 61 62 63 64 65 66 67 68

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

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

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

  /**
   * Status-es
   */
L
lijiarui 已提交
85 86 87 88 89
  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
90

91 92 93
  /**
   * Location
   */
L
lijiarui 已提交
94 95 96 97
  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: '北京市昌平区回龙观龙腾苑(五区)内(龙腾街南)',
98 99 100 101 102 103 104 105 106 107 108 109

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

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

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

123
export interface MsgObj {
L
lijiarui 已提交
124 125 126 127 128 129 130 131 132 133 134
  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
135 136
}

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

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

143
export interface MsgTypeMap {
L
lijiarui 已提交
144
  [index: string]: string|number,
145 146 147 148
  //   MessageTypeName:  MessageTypeValue
  // , MessageTypeValue: MessageTypeName
}

149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
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 (李卓桓) 已提交
169
export const enum MsgType {
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
  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,
187
  RECALLED            = 10002,
188 189
}

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

194 195 196 197 198
  /**
   * a map for:
   *   1. name to id
   *   2. id to name
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
199
  public static TYPE: MsgTypeMap = {
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
    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 已提交
217
    RECALLED:           10002,
218 219 220 221
  }

  public readonly id: string

222
  public obj = <MsgObj>{}
223

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

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

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

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

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

245
  // Transform rawObj to local m
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
246 247
  private parse(rawObj): MsgObj {
    const obj: MsgObj = {
248
      id:           rawObj.MsgId,
249 250 251 252 253 254 255
      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 已提交
256
      url:          rawObj.Url || rawObj.MMAppMsgDownloadUrl || rawObj.MMLocationUrl,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
257
    }
258 259

    // FIXME: has ther any better method to know the room ID?
260 261 262 263 264 265 266
    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(/^@@/)')
267
        // obj.room = undefined // bug compatible
268
      }
269 270 271
      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
      }
272
    }
273

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

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

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

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

325 326 327
  // public to(room: Room): void
  // public to(): Contact|Room
  // public to(contact?: Contact|Room|string): Contact|Room|void {
328 329
  public to(contact: Contact): void
  public to(id: string): void
330 331 332
  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 (李卓桓) 已提交
333
    if (contact) {
334
      if (contact instanceof Contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
335
        this.obj.to = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
336
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
337 338 339 340
        this.obj.to = contact
      } else {
        throw new Error('unsupport to param ' + typeof contact)
      }
341
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
342
    }
343

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

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

352 353
  public room(room: Room): void
  public room(id: string): void
354
  public room(): Room|null
355
  public room(room?: Room|string): Room|null|void {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
356 357 358 359 360 361 362 363
    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)
      }
364
      return
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
365
    }
366 367
    if (this.obj.room) {
      return Room.load(this.obj.room)
368
    }
369
    return null
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
370 371
  }

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

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

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

  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
399 400
  }

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

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

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

    return fromId === userId
  }

L
lijiarui 已提交
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487
  /**
   *
   * Get message mentioned contactList.
   * message event table as follows
   *
   * |                                                                            | Web  |  Mac PC Client | iOS Mobile |  android Mobile |
   * | :---                                                                       | :--: |     :----:     |   :---:    |     :---:       |
   * | [You were mentioned] tip ([有人@我]的提示)                                   |  ✘   |        √       |     √      |       √         |
   * | Identify magic code (8197) by copy & paste in mobile                       |  ✘   |        √       |     √      |       ✘         |
   * | Identify magic code (8197) by programming                                  |  ✘   |        ✘       |     ✘      |       ✘         |
   * | Identify two contacts with the same roomAlias by [You were  mentioned] tip |  ✘   |        ✘       |     √      |       √         |
   *
   * @returns {Contact[]} return message mentioned contactList
   *
   * @example
   * ```ts
   * const contactList = message.mentioned()
   * console.log(contactList)
   * ```
   */
  public mentioned(): Contact[] {
    let contactList: Contact[] = []
    const room = this.room()
    if (this.type() !== MsgType.TEXT || !room ) {
      return contactList
    }

    // define magic code `8197` to identify @xxx
    const AT_SEPRATOR = String.fromCharCode(8197)

    const atList = this.content().split(AT_SEPRATOR)

    if (atList.length === 0) return contactList

    // Using `filter(e => e.indexOf('@') > -1)` to filter the string without `@`
    const rawMentionedList = atList
      .filter(str => str.includes('@'))
      .map(str => multipleAt(str))
      .filter(str => !!str) // filter blank string

    // convert 'hello@a@b@c' to [ 'c', 'b@c', 'a@b@c' ]
    function multipleAt(str: string) {
      str = str.replace(/^.*?@/, '@')
        let name = ''
        const nameList: string[] = []
        str.split('@')
          .filter(mentionName => !!mentionName)
          .reverse()
          .forEach(mentionName => {
            name = mentionName + '@' + name
            nameList.push(name.slice(0, -1)) // get rid of the `@` at beginning
          })
        return nameList
    }

    // flatten array, see http://stackoverflow.com/a/10865042/1123955
    const mentionList = [].concat.apply([], rawMentionedList)
    log.verbose('Message', 'mentioned(%s),get mentionList: %s', this.content(), JSON.stringify(mentionList))

    contactList = [].concat.apply([],
      mentionList.map(member => {
        room.memberAll(member)
      })
      .filter(contact => !!contact),
    )

    if (contactList.length === 0) {
      log.warn(`Message`, `message.mentioned() can not found member using room.member() from mentionList, metion string: ${JSON.stringify(mentionList)}`)
    }
    return contactList
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
488 489 490 491 492
  // public ready() {
  //   log.warn('Message', 'ready() DEPRECATED. use load() instead.')
  //   return this.ready()
  // }

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

496
    try {
497
      const from  = Contact.load(this.obj.from)
498
      await from.ready()                // Contact from
499

500 501 502
      if (this.obj.to) {
        const to = Contact.load(this.obj.to)
        await to.ready()
503
      }
504

505 506
      if (this.obj.room) {
        const room  = Room.load(this.obj.room)
507
        await room.ready()  // Room member list
508
      }
509

510
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
511
        log.error('Message', 'ready() exception: %s', e.stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
512 513 514
        // console.log(e)
        // this.dump()
        // this.dumpRaw()
515
        throw e
516
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
517 518
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
519 520 521
  /**
   * @deprecated
   */
522
  public get(prop: string): string {
523
    log.warn('Message', 'DEPRECATED get() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
524

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
525 526
    if (!prop || !(prop in this.obj)) {
      const s = '[' + Object.keys(this.obj).join(',') + ']'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
527 528
      throw new Error(`Message.get(${prop}) must be in: ${s}`)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
529
    return this.obj[prop]
530 531
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
532 533 534
  /**
   * @deprecated
   */
535
  public set(prop: string, value: string): this {
536
    log.warn('Message', 'DEPRECATED set() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
537

538 539 540
    if (typeof value !== 'string') {
      throw new Error('value must be string, we got: ' + typeof value)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
541
    this.obj[prop] = value
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
542
    return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
543
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
544

545
  public dump() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
546 547
    console.error('======= dump message =======')
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
548
  }
549
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
550
    console.error('======= dump raw message =======')
551
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj && this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
552 553
  }

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

558 559
  public static async findAll(query) {
    return Promise.resolve([
L
lijiarui 已提交
560 561
      new Message   (<MsgRawObj>{MsgId: '-2'}),
      new Message (<MsgRawObj>{MsgId: '-3'}),
562
    ])
563 564
  }

565 566 567 568 569 570
  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 (李卓桓) 已提交
571

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

M
Mukaiu 已提交
575
  public say(textOrMedia: string | MediaMessage, replyTo?: Contact|Contact[]): Promise<any> {
576
    /* tslint:disable:no-use-before-declare */
M
Mukaiu 已提交
577 578 579 580 581 582 583 584 585
    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 (李卓桓) 已提交
586

M
Mukaiu 已提交
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
      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)
      }
602
    /* tslint:disable:no-use-before-declare */
M
Mukaiu 已提交
603 604 605 606 607
    } else if (textOrMedia instanceof MediaMessage) {
      m = textOrMedia
      const room = this.room()
      if (room) {
        m.room(room)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
608 609
      }

M
Mukaiu 已提交
610 611 612
      if (!replyTo) {
        m.to(this.from())
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
613
    }
M
Mukaiu 已提交
614

615
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
616 617 618
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
619
}
620 621

Message.initType()
622

M
Mukaiu 已提交
623 624
export class MediaMessage extends Message {
  private bridge: Bridge
M
Mukaiu 已提交
625
  private filePath: string
M
Mukaiu 已提交
626 627 628
  private fileName: string // 'music'
  private fileExt: string // 'mp3'

629
  constructor(rawObj: Object)
M
Mukaiu 已提交
630 631
  constructor(filePath: string)

632
  constructor(rawObjOrFilePath: Object | string) {
M
Mukaiu 已提交
633 634
    if (typeof rawObjOrFilePath === 'string') {
      super()
M
Mukaiu 已提交
635
      this.filePath = rawObjOrFilePath
M
Mukaiu 已提交
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 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770

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

771
    let filename = this.rawObj.FileName || this.rawObj.MediaId || this.rawObj.MsgId
M
Mukaiu 已提交
772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789

    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> {
M
Mukaiu 已提交
790 791
    if (this.filePath)
      return fs.createReadStream(this.filePath)
M
Mukaiu 已提交
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806

    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
    }
  }
}
807

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
808
/*
809 810 811 812 813
 * 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 (李卓桓) 已提交
814
 */
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
815 816

export default Message