message.ts 23.7 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

Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
20 21 22 23 24
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'
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
  }

L
lijiarui 已提交
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 488
  /**
   *
   * 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 (李卓桓) 已提交
489 490 491 492 493
  // public ready() {
  //   log.warn('Message', 'ready() DEPRECATED. use load() instead.')
  //   return this.ready()
  // }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

M
Mukaiu 已提交
576 577 578 579 580 581 582 583 584 585
  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 (李卓桓) 已提交
586

M
Mukaiu 已提交
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
      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 (李卓桓) 已提交
607 608
      }

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

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

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

Message.initType()
621

M
Mukaiu 已提交
622 623 624 625 626 627
export class MediaMessage extends Message {
  private bridge: Bridge
  private fileStream: NodeJS.ReadableStream
  private fileName: string // 'music'
  private fileExt: string // 'mp3'

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

631
  constructor(rawObjOrFilePath: Object | string) {
M
Mukaiu 已提交
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 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 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812
    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
    }
  }
}
813

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
814
/*
815 816 817 818 819
 * 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 (李卓桓) 已提交
820
 */
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
821 822

export default Message