message.ts 25.3 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
2
 *   Wechaty - https://github.com/chatie/wechaty
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.
L
lijiarui 已提交
17
 *   @ignore
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
18
 */
19 20 21 22 23
import * as fs    from 'fs'
import * as path  from 'path'
import {
  Readable,
}                 from 'stream'
M
Mukaiu 已提交
24

25 26
import * as mime  from 'mime'

27
import {
28
  config,
29
  Raven,
L
lijiarui 已提交
30 31
  Sayable,
  log,
32
}                 from './config'
33

Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
34 35
import Contact    from './contact'
import Room       from './room'
36
import Misc       from './misc'
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
37 38
import PuppetWeb  from './puppet-web/puppet-web'
import Bridge     from './puppet-web/bridge'
39

40 41 42 43 44 45
import {
  AppMsgType,
  MsgObj,
  MsgRawObj,
  MsgType,
}                 from './puppet-web/schema'
46

47 48 49 50
export type TypeName =  'attachment'
                      | 'audio'
                      | 'image'
                      | 'video'
51

L
lijiarui 已提交
52 53 54 55 56 57
/**
 * All wechat messages will be encapsulated as a Message.
 *
 * `Message` is `Sayable`,
 * [Example/Ding-Dong-Bot]{@link https://github.com/Chatie/wechaty/blob/master/example/ding-dong-bot.ts}
 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
58
export class Message implements Sayable {
L
lijiarui 已提交
59 60 61
  /**
   * @private
   */
62
  public static counter = 0
L
lijiarui 已提交
63 64 65 66

  /**
   * @private
   */
67
  public _counter: number
68

L
lijiarui 已提交
69 70 71
  /**
   * @private
   */
72 73
  public readonly id: string

L
lijiarui 已提交
74 75 76
  /**
   * @private
   */
77
  public obj = <MsgObj>{}
78

L
lijiarui 已提交
79 80 81
  /**
   * @private
   */
82 83
  public filename(): string {
    throw Error('not a media message')
84 85
  }

H
hcz 已提交
86 87 88
  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
89
  constructor(public rawObj?: MsgRawObj) {
90
    this._counter = Message.counter++
91
    log.silly('Message', 'constructor() SN:%d', this._counter)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
92

93
    if (typeof rawObj === 'string') {
94
      this.rawObj = JSON.parse(rawObj)
95 96
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
97
    this.rawObj = rawObj = rawObj || <MsgRawObj>{}
98
    this.obj = this.parse(rawObj)
99
    this.id = this.obj.id
100
  }
101

L
lijiarui 已提交
102 103 104
  /**
   * @private
   */
105
  // Transform rawObj to local obj
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106 107
  private parse(rawObj): MsgObj {
    const obj: MsgObj = {
108
      id:           rawObj.MsgId,
109 110 111 112 113 114 115
      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 已提交
116
      url:          rawObj.Url || rawObj.MMAppMsgDownloadUrl || rawObj.MMLocationUrl,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
117
    }
118

119
    // FIXME: has there any better method to know the room ID?
120 121 122 123 124 125 126
    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(/^@@/)')
127
        // obj.room = undefined // bug compatible
128
      }
129 130 131
      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
      }
132
    }
133

134
    return obj
135
  }
L
lijiarui 已提交
136 137 138 139

  /**
   * @private
   */
140
  public toString() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
141
    return `Message<${Misc.plainText(this.obj.content)}>`
142
  }
L
lijiarui 已提交
143 144 145 146

  /**
   * @private
   */
147
  public toStringDigest() {
148
    const text = Misc.digestEmoji(this.obj.digest)
149
    return '{' + this.typeEx() + '}' + text
150 151
  }

L
lijiarui 已提交
152 153 154
  /**
   * @private
   */
155
  public toStringEx() {
156
    let s = `${this.constructor.name}#${this._counter}`
157 158 159
    s += '(' + this.getSenderString()
    s += ':' + this.getContentString() + ')'
    return s
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
  }
L
lijiarui 已提交
161 162 163 164

  /**
   * @private
   */
165
  public getSenderString() {
166 167
    const fromName  = Contact.load(this.obj.from).name()
    const roomTopic = this.obj.room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
168
                  ? (':' + Room.load(this.obj.room).topic())
169
                  : ''
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170
    return `<${fromName}${roomTopic}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
171
  }
L
lijiarui 已提交
172 173 174 175

  /**
   * @private
   */
176
  public getContentString() {
177
    let content = Misc.plainText(this.obj.content)
178
    if (content.length > 20) { content = content.substring(0, 17) + '...' }
179 180
    return '{' + this.type() + '}' + content
  }
181

L
lijiarui 已提交
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
  public say(text: string, replyTo?: Contact | Contact[]): Promise<any>

  public say(mediaMessage: MediaMessage, replyTo?: Contact | Contact[]): Promise<any>

  /**
   * Reply a Text or Media File message to the sender.
   *
   * @see {@link https://github.com/Chatie/wechaty/blob/master/example/ding-dong-bot.ts|Example/ding-dong-bot}
   * @param {(string | MediaMessage)} textOrMedia
   * @param {(Contact|Contact[])} [replyTo]
   * @returns {Promise<any>}
   *
   * @example
   * const bot = Wechaty.instance()
   * bot
   * .on('message', async m => {
   *   if (/^ding$/i.test(m.content())) {
   *     await m.say('hello world')
   *     console.log('Bot REPLY: hello world')
   *     await m.say(new MediaMessage(__dirname + '/wechaty.png'))
   *     console.log('Bot REPLY: Image')
   *   }
   * })
   */
  public say(textOrMedia: string | MediaMessage, replyTo?: Contact|Contact[]): Promise<any> {
    /* tslint:disable:no-use-before-declare */
    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)
      }

      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)
      }
    /* tslint:disable:no-use-before-declare */
    } else if (textOrMedia instanceof MediaMessage) {
      m = textOrMedia
      const room = this.room()
      if (room) {
        m.room(room)
      }

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

    return config.puppetInstance()
                  .send(m)
  }

H
hcz 已提交
250
  /**
L
lijiarui 已提交
251
   * @private
H
hcz 已提交
252
   */
253
  public from(contact: Contact): void
L
lijiarui 已提交
254 255 256 257

  /**
   * @private
   */
258
  public from(id: string): void
L
lijiarui 已提交
259

260
  public from(): Contact
L
lijiarui 已提交
261 262 263 264 265

  /**
   * Get the sender from a message.
   * @returns {Contact}
   */
266
  public from(contact?: Contact|string): Contact|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
267
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
268 269
      if (contact instanceof Contact) {
        this.obj.from = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
270
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
271 272 273 274
        this.obj.from = contact
      } else {
        throw new Error('unsupport from param: ' + typeof contact)
      }
275
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
276
    }
277 278 279 280 281 282

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

H
hcz 已提交
285
  /**
L
lijiarui 已提交
286
   * @private
H
hcz 已提交
287
   */
288
  public room(room: Room): void
L
lijiarui 已提交
289 290 291 292

  /**
   * @private
   */
293
  public room(id: string): void
L
lijiarui 已提交
294

295
  public room(): Room|null
L
lijiarui 已提交
296 297 298 299 300 301 302

  /**
   * Get the room from the message.
   * If the message is not in a room, then will return `null`
   *
   * @returns {(Room|null)}
   */
303
  public room(room?: Room|string): Room|null|void {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
304 305 306 307 308 309 310 311
    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)
      }
312
      return
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
313
    }
314 315
    if (this.obj.room) {
      return Room.load(this.obj.room)
316
    }
317
    return null
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
318 319
  }

H
hcz 已提交
320
  /**
L
lijiarui 已提交
321 322 323
   * Get the content of the message
   *
   * @returns {string}
H
hcz 已提交
324
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
325
  public content(): string
L
lijiarui 已提交
326 327 328 329

  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
330 331
  public content(content: string): void

L
lijiarui 已提交
332 333 334 335 336
  /**
   * Get the content of the message
   *
   * @returns {string}
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
337
  public content(content?: string): string|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
338 339
    if (content) {
      this.obj.content = content
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
340
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
341 342 343 344
    }
    return this.obj.content
  }

H
hcz 已提交
345
  /**
L
lijiarui 已提交
346 347
   * Get the type from the message.
   *
348
   * If type is equal to `MsgType.RECALLED`, {@link Message#id} is the msgId of the recalled message.
L
lijiarui 已提交
349 350
   * @see {@link MsgType}
   * @returns {MsgType}
H
hcz 已提交
351
   */
352
  public type(): MsgType {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
353
    return this.obj.type
354 355
  }

H
hcz 已提交
356
  /**
L
lijiarui 已提交
357 358 359 360 361 362
   * Get the typeSub from the message.
   *
   * If message is a location message: `m.type() === MsgType.TEXT && m.typeSub() === MsgType.LOCATION`
   *
   * @see {@link MsgType}
   * @returns {MsgType}
H
hcz 已提交
363
   */
364 365 366 367 368 369 370
  public typeSub(): MsgType {
    if (!this.rawObj) {
      throw new Error('no rawObj')
    }
    return this.rawObj.SubMsgType
  }

H
hcz 已提交
371
  /**
L
lijiarui 已提交
372 373 374 375
   * Get the typeApp from the message.
   *
   * @returns {AppMsgType}
   * @see {@link AppMsgType}
H
hcz 已提交
376
   */
377 378 379 380 381
  public typeApp(): AppMsgType {
    if (!this.rawObj) {
      throw new Error('no rawObj')
    }
    return this.rawObj.AppMsgType
382 383
  }

H
hcz 已提交
384
  /**
L
lijiarui 已提交
385 386 387
   * Get the typeEx from the message.
   *
   * @returns {MsgType}
H
hcz 已提交
388
   */
389
  public typeEx()  { return MsgType[this.obj.type] }
L
lijiarui 已提交
390

H
hcz 已提交
391
  /**
L
lijiarui 已提交
392
   * @private
H
hcz 已提交
393
   */
394
  public count()   { return this._counter }
395

H
hcz 已提交
396
  /**
L
lijiarui 已提交
397 398 399 400 401 402 403
   * Check if a message is sent by self.
   *
   * @returns {boolean} - Return `true` for send from self, `false` for send from others.
   * @example
   * if (message.self()) {
   *  console.log('this message is sent by myself!')
   * }
H
hcz 已提交
404
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
405
  public self(): boolean {
406
    const userId = config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
407 408
                        .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
  /**
   *
   * Get message mentioned contactList.
L
lijiarui 已提交
420 421
   *
   * Message event table as follows
L
lijiarui 已提交
422 423 424 425 426 427 428 429
   *
   * |                                                                            | 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 |  ✘   |        ✘       |     √      |       √         |
   *
L
lijiarui 已提交
430
   * @returns {Contact[]} - Return message mentioned contactList
L
lijiarui 已提交
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
   *
   * @example
   * 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(/^.*?@/, '@')
459 460 461 462 463 464 465 466 467 468
      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 已提交
469 470 471 472 473 474 475
    }

    // 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 已提交
476
      mentionList.map(nameStr => room.memberAll(nameStr))
477
        .filter(contact => !!contact),
L
lijiarui 已提交
478 479 480 481 482 483 484 485
    )

    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
  }

L
lijiarui 已提交
486 487 488
  /**
   * @private
   */
489
  public async ready(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
490
    log.silly('Message', 'ready()')
491

492
    try {
493
      const from  = Contact.load(this.obj.from)
494
      await from.ready()  // Contact from
495

496 497 498
      if (this.obj.to) {
        const to = Contact.load(this.obj.to)
        await to.ready()
499
      }
500

501 502
      if (this.obj.room) {
        const room  = Room.load(this.obj.room)
503
        await room.ready()  // Room member list
504
      }
505

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
516
  /**
L
lijiarui 已提交
517
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
518
   */
519
  public get(prop: string): string {
520
    log.warn('Message', 'DEPRECATED get() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
521

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
529
  /**
L
lijiarui 已提交
530
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
531
   */
532
  public set(prop: string, value: string): this {
533
    log.warn('Message', 'DEPRECATED set() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
534

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

L
lijiarui 已提交
542 543 544
  /**
   * @private
   */
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
  }
L
lijiarui 已提交
549 550 551 552

  /**
   * @private
   */
553
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
554
    console.error('======= dump raw message =======')
555 556 557
    if (!this.rawObj) {
      throw new Error('no this.obj')
    }
558
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj && this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
559 560
  }

L
lijiarui 已提交
561 562 563
  /**
   * @todo add function
   */
564
  public static async find(query) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
565
    return Promise.resolve(new Message(<MsgRawObj>{MsgId: '-1'}))
566 567
  }

L
lijiarui 已提交
568 569 570
  /**
   * @todo add function
   */
571 572
  public static async findAll(query) {
    return Promise.resolve([
L
lijiarui 已提交
573 574
      new Message   (<MsgRawObj>{MsgId: '-2'}),
      new Message (<MsgRawObj>{MsgId: '-3'}),
575
    ])
576 577
  }

L
lijiarui 已提交
578 579 580
  // public to(room: Room): void
  // public to(): Contact|Room
  // public to(contact?: Contact|Room|string): Contact|Room|void {
L
lijiarui 已提交
581

L
lijiarui 已提交
582 583 584 585
  /**
   * @private
   */
  public to(contact: Contact): void
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
586

L
lijiarui 已提交
587
  /**
L
lijiarui 已提交
588
   * @private
L
lijiarui 已提交
589
   */
L
lijiarui 已提交
590
  public to(id: string): void
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
591

L
lijiarui 已提交
592
  public to(): Contact|null // if to is not set, then room must had set
M
Mukaiu 已提交
593

L
lijiarui 已提交
594 595 596 597 598 599 600 601 602 603 604 605 606
  /**
   * Get the destination of the message
   * Message.to() will return null if a message is in a room, use Message.room() to get the room.
   * @returns {(Contact|null)}
   */
  public to(contact?: Contact|string): Contact|Room|null|void {
    if (contact) {
      if (contact instanceof Contact) {
        this.obj.to = contact.id
      } else if (typeof contact === 'string') {
        this.obj.to = contact
      } else {
        throw new Error('unsupport to param ' + typeof contact)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
607
      }
L
lijiarui 已提交
608 609
      return
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
610

L
lijiarui 已提交
611 612 613 614
    // no parameter

    if (!this.obj.to) {
      return null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
615
    }
L
lijiarui 已提交
616 617
    return Contact.load(this.obj.to)
  }
M
Mukaiu 已提交
618

L
lijiarui 已提交
619 620 621 622 623 624 625 626 627 628
  /**
   * Please notice that when we are running Wechaty,
   * if you use the browser that controlled by Wechaty to send attachment files,
   * you will get a zero sized file, because it is not an attachment from the network,
   * but a local data, which is not supported by Wechaty yet.
   *
   * @returns {Promise<Readable>}
   */
  public readyStream(): Promise<Readable> {
    throw Error('abstract method')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
629 630
  }

L
lijiarui 已提交
631 632 633 634 635 636 637 638
  // DEPRECATED: TypeScript ENUM did this for us 201705
  // 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 (李卓桓) 已提交
639
}
640

641
// Message.initType()
642

L
lijiarui 已提交
643 644 645 646
/**
 * Meidia Type Message
 *
 */
M
Mukaiu 已提交
647
export class MediaMessage extends Message {
L
lijiarui 已提交
648 649 650
  /**
   * @private
   */
M
Mukaiu 已提交
651
  private bridge: Bridge
L
lijiarui 已提交
652 653 654 655

  /**
   * @private
   */
M
Mukaiu 已提交
656
  private filePath: string
L
lijiarui 已提交
657 658 659 660

  /**
   * @private
   */
M
Mukaiu 已提交
661
  private fileName: string // 'music'
L
lijiarui 已提交
662 663 664 665

  /**
   * @private
   */
M
Mukaiu 已提交
666 667
  private fileExt: string // 'mp3'

L
lijiarui 已提交
668 669 670
  /**
   * @private
   */
671
  constructor(rawObj: Object)
L
lijiarui 已提交
672 673 674 675

  /**
   * @private
   */
M
Mukaiu 已提交
676 677
  constructor(filePath: string)

678
  constructor(rawObjOrFilePath: Object | string) {
M
Mukaiu 已提交
679 680
    if (typeof rawObjOrFilePath === 'string') {
      super()
M
Mukaiu 已提交
681
      this.filePath = rawObjOrFilePath
M
Mukaiu 已提交
682 683 684 685 686 687 688 689 690 691 692

      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
693
    this.bridge = (config.puppetInstance() as PuppetWeb)
694
                    .bridge
M
Mukaiu 已提交
695 696
  }

697 698 699 700
  /**
   * @private
   */
  public toString() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
701
    return `MediaMessage<${this.filename()}>`
702 703
  }

L
lijiarui 已提交
704 705 706
  /**
   * @private
   */
M
Mukaiu 已提交
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
  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)
780
      Raven.captureException(e)
M
Mukaiu 已提交
781 782 783 784
      throw e
    }
  }

H
hcz 已提交
785
  /**
L
lijiarui 已提交
786 787 788 789 790 791 792 793 794
   * Get the MediaMessage file extension, etc: `jpg`, `gif`, `pdf`, `word` ..
   *
   * @returns {string}
   * @example
   * bot.on('message', async function (m) {
   *   if (m instanceof MediaMessage) {
   *     console.log('media message file name extention is: ' + m.ext())
   *   }
   * })
H
hcz 已提交
795
   */
M
Mukaiu 已提交
796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826
  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
    }
827 828
    log.error('MediaMessage', `ext() got unknown type: ${this.type()}`)
    return String(this.type())
M
Mukaiu 已提交
829 830
  }

831 832 833 834 835 836 837 838
  /**
   * return the MIME Type of this MediaMessage
   *
   */
  public mimeType(): string | null {
    return mime.getType(this.ext())
  }

H
hcz 已提交
839
  /**
L
lijiarui 已提交
840 841 842 843 844 845 846 847 848
   * Get the MediaMessage filename, etc: `how to build a chatbot.pdf`..
   *
   * @returns {string}
   * @example
   * bot.on('message', async function (m) {
   *   if (m instanceof MediaMessage) {
   *     console.log('media message file name is: ' + m.filename())
   *   }
   * })
H
hcz 已提交
849
   */
M
Mukaiu 已提交
850 851 852 853 854 855 856 857 858
  public filename(): string {
    if (this.fileName && this.fileExt) {
      return this.fileName + '.' + this.fileExt
    }

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

859
    let filename = this.rawObj.FileName || this.rawObj.MediaId || this.rawObj.MsgId
M
Mukaiu 已提交
860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876

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

L
lijiarui 已提交
877
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
878
   * Get the read stream for attachment file
L
lijiarui 已提交
879
   */
880
  public async readyStream(): Promise<Readable> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
881 882
    log.verbose('MediaMessage', 'readyStream()')

M
Mukaiu 已提交
883 884
    if (this.filePath)
      return fs.createReadStream(this.filePath)
M
Mukaiu 已提交
885 886 887 888

    try {
      await this.ready()
      // FIXME: decoupling needed
889
      const cookies = await (config.puppetInstance() as PuppetWeb).cookies()
M
Mukaiu 已提交
890 891 892
      if (!this.obj.url) {
        throw new Error('no url')
      }
893
      log.verbose('MediaMessage', 'readyStream() url: %s', this.obj.url)
894
      return Misc.urlStream(this.obj.url, cookies)
M
Mukaiu 已提交
895
    } catch (e) {
896
      log.warn('MediaMessage', 'readyStream() exception: %s', e.stack)
897
      Raven.captureException(e)
M
Mukaiu 已提交
898 899 900
      throw e
    }
  }
901

902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934
  /**
   * save file
   *
   * @param filePath save file
   */
  public async saveFile(filePath: string): Promise<void> {
    if (!filePath) {
      throw new Error('saveFile() filePath is invalid')
    }
    log.silly('MediaMessage', `saveFile() filePath:'${filePath}'`)
    if (fs.existsSync(filePath)) {
      throw new Error('saveFile() file does exist!')
    }
    const writeStream = fs.createWriteStream(filePath)
    let readStream
    try {
      readStream = await this.readyStream()
    } catch (e) {
      log.error('MediaMessage', `saveFile() call readyStream() error: ${e.message}`)
      throw new Error(`saveFile() call readyStream() error: ${e.message}`)
    }
    await new Promise((resolve, reject) => {
      readStream.pipe(writeStream)
      readStream
        .once('end', resolve)
        .once('error', reject)
    })
      .catch(e => {
        log.error('MediaMessage', `saveFile() error: ${e.message}`)
        throw e
      })
  }

935 936 937 938 939 940
  /**
   * Forward the received message.
   *
   * The types of messages that can be forwarded are as follows:
   *
   * The return value of {@link Message#type} matches one of the following types:
L
lijiarui 已提交
941
   * ```
942 943 944 945 946 947 948 949 950 951 952 953
   * MsgType {
   *   TEXT                = 1,
   *   IMAGE               = 3,
   *   VIDEO               = 43,
   *   EMOTICON            = 47,
   *   LOCATION            = 48,
   *   APP                 = 49,
   *   MICROVIDEO          = 62,
   * }
   * ```
   *
   * When the return value of {@link Message#type} is `MsgType.APP`, the return value of {@link Message#typeApp} matches one of the following types:
L
lijiarui 已提交
954
   * ```
955 956 957 958 959 960 961 962
   * AppMsgType {
   *   TEXT                     = 1,
   *   IMG                      = 2,
   *   VIDEO                    = 4,
   *   ATTACH                   = 6,
   *   EMOJI                    = 8,
   * }
   * ```
963 964 965 966 967 968 969 970 971 972 973 974 975 976 977
   * It should be noted that when forwarding ATTACH type message, if the file size is greater than 25Mb, the forwarding will fail.
   * The reason is that the server shields the web wx to download more than 25Mb files with a file size of 0.
   *
   * But if the file is uploaded by you using wechaty, you can forward it.
   * You need to detect the following conditions in the message event, which can be forwarded if it is met.
   *
   * ```javasrcipt
   * .on('message', async m => {
   *   if (m.self() && m.rawObj && m.rawObj.Signature) {
   *     // Filter the contacts you have forwarded
   *     const msg = <MediaMessage> m
   *     await msg.forward()
   *   }
   * })
   * ```
978
   *
979
   * @param {(Sayable | Sayable[])} to Room or Contact
980 981 982 983
   * The recipient of the message, the room, or the contact
   * @returns {Promise<boolean>}
   * @memberof MediaMessage
   */
984 985 986 987 988 989 990
  public async forward(to: Room|Contact): Promise<boolean> {
    try {
      const ret = await config.puppetInstance().forward(this, to)
      return ret
    } catch (e) {
      log.error('Message', 'forward(%s) exception: %s', to, e)
      throw e
991
    }
992
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
993

M
Mukaiu 已提交
994
}
995

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
996
/*
997 998 999 1000 1001
 * 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 (李卓桓) 已提交
1002
 */
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
1003

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1004 1005 1006
export {
  MsgType,
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
1007
export default Message