message.ts 24.1 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1
/**
2
 *   Chatie - https://github.com/chatie
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
3
 *
4
 *   Copyright 2016-2017 Huan LI <zixia@zixia.net>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
5
 *
6 7 8 9 10 11 12 13 14 15 16
 *   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
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   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.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
17 18
 *
 */
M
Mukaiu 已提交
19 20 21
import * as fs     from 'fs'
import * as path   from 'path'

22
import {
L
lijiarui 已提交
23 24 25 26
  Config,
  RecommendInfo,
  Sayable,
  log,
27
}                 from './config'
28

Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
29 30 31 32 33
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'
34

35
export interface MsgRawObj {
L
lijiarui 已提交
36
  MsgId:            string,
37

L
lijiarui 已提交
38 39 40 41
  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
42

L
lijiarui 已提交
43 44
  MMDigest:         string,
  MMDisplayTime:    number,  // Javascript timestamp of milliseconds
45 46 47 48 49

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

  /**
54 55
   * Attachment
   *
56 57
   * MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_ATTACH
   */
L
lijiarui 已提交
58 59 60 61 62 63 64 65 66 67 68
  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
69 70 71 72 73 74 75 76 77 78

  /**
   * 模板消息
   * MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_READER_TYPE
   *  item.url
   *  item.title
   *  item.pub_time
   *  item.cover
   *  item.digest
   */
L
lijiarui 已提交
79
  MMCategory:       any[],  //  item in message.MMCategory
80 81 82 83 84 85

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

L
lijiarui 已提交
90
  SubMsgType:       MsgType, // "msgType":"{{message.MsgType}}","subType":{{message.SubMsgType||0}},"msgId":"{{message.MsgId}}"
91 92 93 94

  /**
   * Status-es
   */
L
lijiarui 已提交
95 96 97 98 99
  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
100

101 102 103
  /**
   * Location
   */
L
lijiarui 已提交
104 105 106 107
  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: '北京市昌平区回龙观龙腾苑(五区)内(龙腾街南)',
108 109 110 111 112 113 114 115 116 117 118 119

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

  /**
   * Image
   *
   *  getMsgImg(message.MsgId,'slave')
   */
L
lijiarui 已提交
120 121 122
  MMImgStyle:       string,  // ng-style="message.MMImgStyle"
  MMPreviewSrc:     string,  // message.MMPreviewSrc || message.MMThumbSrc || getMsgImg(message.MsgId,'slave')
  MMThumbSrc:       string,
123 124 125 126 127 128 129

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

133
export interface MsgObj {
L
lijiarui 已提交
134 135 136 137 138 139 140 141 142 143 144
  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
145 146
}

147 148
// export type MessageTypeName = 'TEXT' | 'IMAGE' | 'VOICE' | 'VERIFYMSG' | 'POSSIBLEFRIEND_MSG'
// | 'SHARECARD' | 'VIDEO' | 'EMOTICON' | 'LOCATION' | 'APP' | 'VOIPMSG' | 'STATUSNOTIFY'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
149
// | 'VOIPNOTIFY' | 'VOIPINVITE' | 'MICROVIDEO' | 'SYSNOTICE' | 'SYS' | 'RECALLED'
150 151 152

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

153
export interface MsgTypeMap {
L
lijiarui 已提交
154
  [index: string]: string|number,
155 156 157 158
  //   MessageTypeName:  MessageTypeValue
  // , MessageTypeValue: MessageTypeName
}

159
export enum AppMsgType {
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
  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,
}

179
export enum MsgType {
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
  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,
197
  RECALLED            = 10002,
198 199
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
200
export class Message implements Sayable {
201
  public static counter = 0
202
  public _counter: number
203

204
  // DEPRECATED: TypeScript ENUM did this for us 201705
205 206 207 208 209
  /**
   * a map for:
   *   1. name to id
   *   2. id to name
   */
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
  // public static TYPE: MsgTypeMap = {
  //   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,
  //   RECALLED:           10002,
  // }
230 231 232

  public readonly id: string

233
  public obj = <MsgObj>{}
234

235
  public readyStream(): Promise<NodeJS.ReadableStream> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
236 237 238
    throw Error('abstract method')
  }

239 240
  public filename(): string {
    throw Error('not a media message')
241 242
  }

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

247
    if (typeof rawObj === 'string') {
248
      this.rawObj = JSON.parse(rawObj)
249 250
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
251
    this.rawObj = rawObj = rawObj || <MsgRawObj>{}
252
    this.obj = this.parse(rawObj)
253
    this.id = this.obj.id
254
  }
255

256
  // Transform rawObj to local obj
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
257 258
  private parse(rawObj): MsgObj {
    const obj: MsgObj = {
259
      id:           rawObj.MsgId,
260 261 262 263 264 265 266
      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 已提交
267
      url:          rawObj.Url || rawObj.MMAppMsgDownloadUrl || rawObj.MMLocationUrl,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
268
    }
269

270
    // FIXME: has there any better method to know the room ID?
271 272 273 274 275 276 277
    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(/^@@/)')
278
        // obj.room = undefined // bug compatible
279
      }
280 281 282
      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
      }
283
    }
284

285
    return obj
286
  }
287
  public toString() {
288
    return UtilLib.plainText(this.obj.content)
289
  }
290
  public toStringDigest() {
291
    const text = UtilLib.digestEmoji(this.obj.digest)
292
    return '{' + this.typeEx() + '}' + text
293 294
  }

295
  public toStringEx() {
296
    let s = `${this.constructor.name}#${this._counter}`
297 298 299
    s += '(' + this.getSenderString()
    s += ':' + this.getContentString() + ')'
    return s
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300
  }
301
  public getSenderString() {
302 303
    const fromName  = Contact.load(this.obj.from).name()
    const roomTopic = this.obj.room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
304
                  ? (':' + Room.load(this.obj.room).topic())
305
                  : ''
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306
    return `<${fromName}${roomTopic}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
307
  }
308
  public getContentString() {
309
    let content = UtilLib.plainText(this.obj.content)
310
    if (content.length > 20) { content = content.substring(0, 17) + '...' }
311 312
    return '{' + this.type() + '}' + content
  }
313

314 315
  public from(contact: Contact): void
  public from(id: string): void
316
  public from(): Contact
317
  public from(contact?: Contact|string): Contact|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
318
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
319 320
      if (contact instanceof Contact) {
        this.obj.from = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
321
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
322 323 324 325
        this.obj.from = contact
      } else {
        throw new Error('unsupport from param: ' + typeof contact)
      }
326
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327
    }
328 329 330 331 332 333

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

336 337 338
  // public to(room: Room): void
  // public to(): Contact|Room
  // public to(contact?: Contact|Room|string): Contact|Room|void {
339 340
  public to(contact: Contact): void
  public to(id: string): void
341 342 343
  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 (李卓桓) 已提交
344
    if (contact) {
345
      if (contact instanceof Contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
346
        this.obj.to = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
347
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
348 349 350 351
        this.obj.to = contact
      } else {
        throw new Error('unsupport to param ' + typeof contact)
      }
352
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
353
    }
354

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
355 356
    // no parameter

357 358
    if (!this.obj.to) {
      return null
359
    }
360
    return Contact.load(this.obj.to)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
361 362
  }

363 364
  public room(room: Room): void
  public room(id: string): void
365
  public room(): Room|null
366
  public room(room?: Room|string): Room|null|void {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
367 368 369 370 371 372 373 374
    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)
      }
375
      return
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
376
    }
377 378
    if (this.obj.room) {
      return Room.load(this.obj.room)
379
    }
380
    return null
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
381 382
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
383 384 385 386
  public content(): string
  public content(content: string): void

  public content(content?: string): string|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
387 388
    if (content) {
      this.obj.content = content
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
389
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
390 391 392 393
    }
    return this.obj.content
  }

394
  public type(): MsgType {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
395
    return this.obj.type
396 397 398 399 400 401 402 403 404 405 406 407 408 409
  }

  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
410 411
  }

412
  public typeEx()  { return MsgType[this.obj.type] }
413
  public count()   { return this._counter }
414

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
415 416 417 418
  public self(): boolean {
    const userId = Config.puppetInstance()
                        .userId

419
    const fromId = this.obj.from
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
420 421 422 423 424 425 426
    if (!userId || !fromId) {
      throw new Error('no user or no from')
    }

    return fromId === userId
  }

L
lijiarui 已提交
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
  /**
   *
   * 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(/^.*?@/, '@')
470 471 472 473 474 475 476 477 478 479
      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
L
lijiarui 已提交
480 481 482 483 484 485 486
    }

    // 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([],
Y
Yu Zhan 已提交
487
      mentionList.map(nameStr => room.memberAll(nameStr))
L
lijiarui 已提交
488 489 490 491 492 493 494 495 496
      .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
  }

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

500
    try {
501
      const from  = Contact.load(this.obj.from)
502
      await from.ready()  // Contact from
503

504 505 506
      if (this.obj.to) {
        const to = Contact.load(this.obj.to)
        await to.ready()
507
      }
508

509 510
      if (this.obj.room) {
        const room  = Room.load(this.obj.room)
511
        await room.ready()  // Room member list
512
      }
513

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

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

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

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

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

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

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

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

569
  // DEPRECATED: TypeScript ENUM did this for us 201705
570 571 572 573 574 575
  // 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 (李卓桓) 已提交
576

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

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

M
Mukaiu 已提交
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)
      }
607
    /* tslint:disable:no-use-before-declare */
M
Mukaiu 已提交
608 609 610 611 612
    } else if (textOrMedia instanceof MediaMessage) {
      m = textOrMedia
      const room = this.room()
      if (room) {
        m.room(room)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
613 614
      }

M
Mukaiu 已提交
615 616 617
      if (!replyTo) {
        m.to(this.from())
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
618
    }
M
Mukaiu 已提交
619

620
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
621 622 623
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
624
}
625

626
// Message.initType()
627

M
Mukaiu 已提交
628 629
export class MediaMessage extends Message {
  private bridge: Bridge
M
Mukaiu 已提交
630
  private filePath: string
M
Mukaiu 已提交
631 632 633
  private fileName: string // 'music'
  private fileExt: string // 'mp3'

634
  constructor(rawObj: Object)
M
Mukaiu 已提交
635 636
  constructor(filePath: string)

637
  constructor(rawObjOrFilePath: Object | string) {
M
Mukaiu 已提交
638 639
    if (typeof rawObjOrFilePath === 'string') {
      super()
M
Mukaiu 已提交
640
      this.filePath = rawObjOrFilePath
M
Mukaiu 已提交
641 642 643 644 645 646 647 648 649 650 651 652

      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)
653
                    .bridge
M
Mukaiu 已提交
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
  }

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

776
    let filename = this.rawObj.FileName || this.rawObj.MediaId || this.rawObj.MsgId
M
Mukaiu 已提交
777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794

    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 已提交
795 796
    if (this.filePath)
      return fs.createReadStream(this.filePath)
M
Mukaiu 已提交
797 798 799 800 801 802 803 804 805 806 807 808 809 810 811

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

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

export default Message