message.ts 15.1 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
 *
 */
9 10 11
import {
    Config
  , RecommendInfo
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
12
  , Sayable
13
  , log
14 15
}               from './config'

16 17 18
import { Contact }  from './contact'
import { Room }     from './room'
import { UtilLib }  from './util-lib'
19

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
20
export type MessageRawObj = {
21
  MsgId:            string
22 23 24

  MMActualSender:   string // getUserContact(message.MMActualSender,message.MMPeerUserName).isContact()
  MMPeerUserName:   string // message.MsgType == CONF.MSGTYPE_TEXT && message.MMPeerUserName == 'newsapp'
25 26
  ToUserName:       string
  MMActualContent:  string // Content has @id prefix added by wx
27

28
  MMDigest:         string
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
29
  MMDisplayTime:    number  // Javascript timestamp of milliseconds
30 31 32 33 34

  /**
   * MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
   * class="cover" mm-src="{{getMsgImg(message.MsgId,'slave')}}"
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
35
  Url:              string
36 37 38
  MMAppMsgDesc:     string  // class="desc" ng-bind="message.MMAppMsgDesc"

  /**
39 40
   * Attachment
   *
41 42
   * MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_ATTACH
   */
43 44 45 46 47 48 49 50
  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
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
51 52
                                // && (massage.MMStatus == CONF.MSG_SEND_STATUS_SUCC || massage.MMStatus === undefined)
                                // " href="{{message.MMAppMsgDownloadUrl}}">下载</a>
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
  MMUploadProgress: number  // < 100

  /**
   * 模板消息
   * MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_READER_TYPE
   *  item.url
   *  item.title
   *  item.pub_time
   *  item.cover
   *  item.digest
   */
  MMCategory:       any[]  //  item in message.MMCategory

  /**
   * Type
   *
   * MsgType == CONF.MSGTYPE_VOICE : ng-style="{'width':40 + 7*message.VoiceLength/1000}
   */
  MsgType:          number
72 73
  AppMsgType:       AppMsgType  // message.MsgType == CONF.MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
                                // message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType != CONF.MSGTYPE_LOCATION
74

75
  SubMsgType:       MsgType // "msgType":"{{message.MsgType}}","subType":{{message.SubMsgType||0}},"msgId":"{{message.MsgId}}"
76 77 78 79 80 81 82 83 84

  /**
   * Status-es
   */
  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
85

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

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

  /**
   * Image
   *
   *  getMsgImg(message.MsgId,'slave')
   */
  MMImgStyle:       string  // ng-style="message.MMImgStyle"
  MMPreviewSrc:     string  // message.MMPreviewSrc || message.MMThumbSrc || getMsgImg(message.MsgId,'slave')
  MMThumbSrc:       string

  /**
   * Friend Request & ShareCard ?
   *
   * MsgType == CONF.MSGTYPE_SHARECARD" ng-click="showProfile($event,message.RecommendInfo.UserName)
   * MsgType == CONF.MSGTYPE_VERIFYMSG
   */
115
  RecommendInfo?:   RecommendInfo
116 117
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118
export type MessageObj = {
119
  id:       string
120
  type:     number
121
  from:     string
122
  to?:      string  // if to is not set, then room must be set
123 124 125 126 127 128 129 130 131
  room?:    string
  content:  string
  status:   string
  digest:   string
  date:     string

  url?:     string  // for MessageMedia class
}

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

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

export type MessageTypeMap = {
  [index: string]: string|number
  //   MessageTypeName:  MessageTypeValue
  // , MessageTypeValue: MessageTypeName
}

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
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,
}

export enum MsgType {
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
  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,
182
  RECALLED            = 10002,
183 184
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
185
export class Message implements Sayable {
186
  public static counter = 0
187
  private _counter: number
188

189
  public static TYPE: MessageTypeMap = {
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
    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
  }

  public readonly id: string

  protected obj = <MessageObj>{}

214
  public readyStream(): Promise<NodeJS.ReadableStream> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
215 216 217
    throw Error('abstract method')
  }

218 219
  public filename(): string {
    throw Error('not a media message')
220 221
  }

222
  constructor(public rawObj?: MessageRawObj) {
223
    this._counter = Message.counter++
224
    log.silly('Message', 'constructor() SN:%d', this._counter)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
225

226
    if (typeof rawObj === 'string') {
227
      this.rawObj = JSON.parse(rawObj)
228 229
    }

230
    this.rawObj = rawObj = rawObj || <MessageRawObj>{}
231
    this.obj = this.parse(rawObj)
232
    this.id = this.obj.id
233
  }
234

235
  // Transform rawObj to local m
236 237
  private parse(rawObj): MessageObj {
    const obj: MessageObj = {
238
      id:           rawObj.MsgId,
239 240 241 242 243 244 245 246
      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
      url:          rawObj.Url || rawObj.MMAppMsgDownloadUrl || rawObj.MMLocationUrl
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
247
    }
248 249

    // FIXME: has ther any better method to know the room ID?
250 251 252 253 254 255 256
    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(/^@@/)')
257
        // obj.room = undefined // bug compatible
258
      }
259 260 261
      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
      }
262
    }
263

264
    return obj
265
  }
266
  public toString() {
267
    return UtilLib.plainText(this.obj.content)
268
  }
269
  public toStringDigest() {
270
    const text = UtilLib.digestEmoji(this.obj.digest)
271
    return '{' + this.typeEx() + '}' + text
272 273
  }

274
  public toStringEx() {
275
    let s = `${this.constructor.name}#${this._counter}`
276 277 278
    s += '(' + this.getSenderString()
    s += ':' + this.getContentString() + ')'
    return s
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
279
  }
280
  public getSenderString() {
281 282 283 284 285
    const name  = Contact.load(this.obj.from)
    const room = this.obj.room
                  ? Room.load(this.obj.room)
                  : null
    return '<' + (name ? name.toStringEx() : '') + (room ? `@${room.toStringEx()}` : '') + '>'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
286
  }
287
  public getContentString() {
288
    let content = UtilLib.plainText(this.obj.content)
289
    if (content.length > 20) { content = content.substring(0, 17) + '...' }
290 291
    return '{' + this.type() + '}' + content
  }
292

293 294
  public from(contact: Contact): void
  public from(id: string): void
295
  public from(): Contact
296
  public from(contact?: Contact|string): Contact|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
298 299
      if (contact instanceof Contact) {
        this.obj.from = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
301 302 303 304
        this.obj.from = contact
      } else {
        throw new Error('unsupport from param: ' + typeof contact)
      }
305
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306
    }
307 308 309 310 311 312

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
334 335
    // no parameter

336 337
    if (!this.obj.to) {
      return null
338
    }
339
    return Contact.load(this.obj.to)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
340 341
  }

342 343
  public room(room: Room): void
  public room(id: string): void
344
  public room(): Room|null
345
  public room(room?: Room|string): Room|null|void {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
346 347 348 349 350 351 352 353
    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)
      }
354
      return
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
355
    }
356 357
    if (this.obj.room) {
      return Room.load(this.obj.room)
358
    }
359
    return null
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
360 361
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
362 363 364 365
  public content(): string
  public content(content: string): void

  public content(content?: string): string|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
366 367
    if (content) {
      this.obj.content = content
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
368
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
369 370 371 372
    }
    return this.obj.content
  }

373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388
  public type(): MsgType {
    return this.obj.type as MsgType
  }

  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
389 390
  }

391
  public typeEx()  { return Message.TYPE[this.obj.type] }
392
  public count()   { return this._counter }
393

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
394 395 396 397
  public self(): boolean {
    const userId = Config.puppetInstance()
                        .userId

398
    const fromId = this.obj.from
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
399 400 401 402 403 404 405
    if (!userId || !fromId) {
      throw new Error('no user or no from')
    }

    return fromId === userId
  }

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

409
    try {
410
      const from  = Contact.load(this.obj.from)
411
      await from.ready()                // Contact from
412

413 414 415
      if (this.obj.to) {
        const to = Contact.load(this.obj.to)
        await to.ready()
416
      }
417

418 419 420 421 422 423
      if (this.obj.room) {
        const room  = Room.load(this.obj.room)
        if (room) {
          await room.ready()  // Room member list
        }
      }
424

425
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
426
        log.error('Message', 'ready() exception: %s', e.stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
427 428 429
        // console.log(e)
        // this.dump()
        // this.dumpRaw()
430
        throw e
431
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
432 433
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
434 435 436
  /**
   * @deprecated
   */
437
  public get(prop: string): string {
438
    log.warn('Message', 'DEPRECATED get() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
439

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
440 441
    if (!prop || !(prop in this.obj)) {
      const s = '[' + Object.keys(this.obj).join(',') + ']'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
442 443
      throw new Error(`Message.get(${prop}) must be in: ${s}`)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
444
    return this.obj[prop]
445 446
  }

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

453 454 455
    if (typeof value !== 'string') {
      throw new Error('value must be string, we got: ' + typeof value)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
456
    this.obj[prop] = value
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
457
    return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
458
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
459

460
  public dump() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
461 462
    console.error('======= dump message =======')
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
463
  }
464
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
465
    console.error('======= dump raw message =======')
466
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj && this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
467 468
  }

469 470
  public static async find(query) {
    return Promise.resolve(new Message(<MessageRawObj>{MsgId: '-1'}))
471 472
  }

473 474 475 476 477
  public static async findAll(query) {
    return Promise.resolve([
      new Message   (<MessageRawObj>{MsgId: '-2'})
      , new Message (<MessageRawObj>{MsgId: '-3'})
    ])
478 479
  }

480 481 482 483 484 485
  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 (李卓桓) 已提交
486 487 488 489 490

  public say(content: string, replyTo?: Contact|Contact[]): Promise<any> {
    log.verbose('Message', 'say(%s, %s)', content, replyTo)

    const m = new Message()
491 492 493 494
    const room = this.room()
    if (room) {
      m.room(room)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511

    if (!replyTo) {
      m.to(this.from())
      m.content(content)

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

    }
512
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
513 514 515
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
516
}
517 518

Message.initType()
519

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
520
export * from './message-media'
521

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
522
/*
523 524 525 526 527
 * 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 (李卓桓) 已提交
528
 */