message.ts 9.5 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1 2
/**
 *
3
 * 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 14
}               from './config'

15 16 17 18
import Contact  from './contact'
import Room     from './room'
import UtilLib  from './util-lib'
import log      from './brolog-env'
19

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
20
export type MessageRawObj = {
21
  MsgId:            string
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
22
  MsgType:          number
23 24 25 26 27
  MMActualSender:   string
  ToUserName:       string
  MMActualContent:  string // Content has @id prefix added by wx
  Status:           string
  MMDigest:         string
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
28
  MMDisplayTime:    number  // Javascript timestamp of milliseconds
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
29
  Url:              string
30 31

  RecommendInfo?:   RecommendInfo
32 33
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
34
export type MessageObj = {
35 36 37 38 39 40 41 42 43 44 45 46 47
  id:       string
  type:     string
  from:     string
  to:       string
  room?:    string
  content:  string
  status:   string
  digest:   string
  date:     string

  url?:     string  // for MessageMedia class
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
48
export type MessageType = {
49 50 51
  [index: string]: number|string
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
52
export class Message implements Sayable {
53
  public static counter = 0
54
  private _counter: number
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80

  public static TYPE: MessageType = {
    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>{}

81
  public readyStream(): Promise<NodeJS.ReadableStream> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
82 83 84
    throw Error('abstract method')
  }

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

89
    if (typeof rawObj === 'string') {
90
      this.rawObj = JSON.parse(rawObj)
91 92
    }

93
    this.rawObj = rawObj = rawObj || <MessageRawObj>{}
94
    this.obj = this.parse(rawObj)
95
    this.id = this.obj.id
96
  }
97

98
  // Transform rawObj to local m
99 100
  private parse(rawObj): MessageObj {
    const obj: MessageObj = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
101 102
      id:             rawObj.MsgId
      , type:         rawObj.MsgType
103 104
      , from:         rawObj.MMActualSender
      , to:           rawObj.ToUserName
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
105 106 107
      , content:      rawObj.MMActualContent // Content has @id prefix added by wx
      , status:       rawObj.Status
      , digest:       rawObj.MMDigest
108
      , date:         rawObj.MMDisplayTime  // Javascript timestamp of milliseconds
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
109
    }
110 111

    // FIXME: has ther any better method to know the room ID?
112 113 114 115 116 117 118
    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(/^@@/)')
119
        // obj.room = undefined // bug compatible
120
      }
121 122
    // } else {
    //   obj.room = undefined
123 124
    }
    return obj
125
  }
126
  public toString() {
127
    return UtilLib.plainText(this.obj.content)
128
  }
129
  public toStringDigest() {
130
    const text = UtilLib.digestEmoji(this.obj.digest)
131
    return '{' + this.typeEx() + '}' + text
132 133
  }

134
  public toStringEx() {
135
    let s = `${this.constructor.name}#${this._counter}`
136 137 138
    s += '(' + this.getSenderString()
    s += ':' + this.getContentString() + ')'
    return s
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
139
  }
140
  public getSenderString() {
141 142 143 144 145
    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 (李卓桓) 已提交
146
  }
147
  public getContentString() {
148
    let content = UtilLib.plainText(this.obj.content)
149
    if (content.length > 20) { content = content.substring(0, 17) + '...' }
150 151
    return '{' + this.type() + '}' + content
  }
152

153 154
  public from(contact?: Contact): void
  public from(id?: string): void
155
  public from(): Contact
156
  public from(contact?: Contact|string): Contact|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
157
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
158 159
      if (contact instanceof Contact) {
        this.obj.from = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
161 162 163 164
        this.obj.from = contact
      } else {
        throw new Error('unsupport from param: ' + typeof contact)
      }
165
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
166
    }
167 168 169 170 171 172

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

175 176 177
  public to(contact: Contact): void
  public to(room: Room): void
  public to(id: string): void
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
178
  public to(): Contact|Room
179
  public to(contact?: Contact|Room|string): Contact|Room|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
180
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
181 182
      if (contact instanceof Contact || contact instanceof Room) {
        this.obj.to = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
183
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
184 185 186 187
        this.obj.to = contact
      } else {
        throw new Error('unsupport to param ' + typeof contact)
      }
188
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
189
    }
190 191

    // FIXME: better to identify a room id?
192
    const loadedInstance = /^@@/.test(this.obj.to)
193 194
            ? Room.load(this.obj.to)
            : Contact.load(this.obj.to)
195 196 197 198
    if (!loadedInstance) {
      throw new Error('no to')
    }
    return loadedInstance
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
199 200
  }

201 202
  public room(room: Room): void
  public room(id: string): void
203
  public room(): Room|null
204
  public room(room?: Room|string): Room|null|void {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
205 206 207 208 209 210 211 212
    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)
      }
213
      return
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
214
    }
215
    if (!this.obj.room) {
216
      return null
217
    }
218
    return Room.load(this.obj.room)
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
219 220
  }

221
  public content(content?: string): string {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
222 223 224 225 226 227
    if (content) {
      this.obj.content = content
    }
    return this.obj.content
  }

228
  public type()    { return this.obj.type }
229
  public typeEx()  { return Message.TYPE[this.obj.type] }
230
  public count()   { return this._counter }
231

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
232 233 234 235
  public self(): boolean {
    const userId = Config.puppetInstance()
                        .userId

236
    const fromId = this.obj.from
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
237 238 239 240 241 242 243
    if (!userId || !fromId) {
      throw new Error('no user or no from')
    }

    return fromId === userId
  }

244
  public async ready(): Promise<this> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
245
    log.silly('Message', 'ready()')
246

247
    try {
248 249 250 251
      const from  = Contact.load(this.obj.from)
      const to    = Contact.load(this.obj.to)
      const room  = this.obj.room ? Room.load(this.obj.room) : null

252 253 254
      if (!from || !to) {
        throw new Error('no `from` or no `to`')
      }
255 256 257
      await from.ready()                // Contact from
      await to.ready()                  // Contact to
      if (room) { await room.ready() }  // Room member list
258

259
      return this         // return this for chain
260
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
261
        log.error('Message', 'ready() exception: %s', e.stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
262 263 264
        // console.log(e)
        // this.dump()
        // this.dumpRaw()
265
        throw e
266
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
267 268
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
269 270 271
  /**
   * @deprecated
   */
272
  public get(prop: string): string {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
273 274
    log.warn('Message', 'DEPRECATED get()')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
275 276
    if (!prop || !(prop in this.obj)) {
      const s = '[' + Object.keys(this.obj).join(',') + ']'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
277 278
      throw new Error(`Message.get(${prop}) must be in: ${s}`)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
279
    return this.obj[prop]
280 281
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
282 283 284
  /**
   * @deprecated
   */
285
  public set(prop: string, value: string): this {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
286 287
    log.warn('Message', 'DEPRECATED set()')

288 289 290
    if (typeof value !== 'string') {
      throw new Error('value must be string, we got: ' + typeof value)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
291
    this.obj[prop] = value
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
292
    return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
293
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
294

295
  public dump() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
296 297
    console.error('======= dump message =======')
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298
  }
299
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300
    console.error('======= dump raw message =======')
301
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj && this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
302 303
  }

304 305
  public static async find(query) {
    return Promise.resolve(new Message(<MessageRawObj>{MsgId: '-1'}))
306 307
  }

308 309 310 311 312
  public static async findAll(query) {
    return Promise.resolve([
      new Message   (<MessageRawObj>{MsgId: '-2'})
      , new Message (<MessageRawObj>{MsgId: '-3'})
    ])
313 314
  }

315 316 317 318 319 320
  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 (李卓桓) 已提交
321 322 323 324 325

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

    const m = new Message()
326 327 328 329
    const room = this.room()
    if (room) {
      m.room(room)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346

    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)

    }
347
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
348 349 350
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
351
}
352 353

Message.initType()
354

355
export default Message
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
356
export * from './message-media'
357

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
358
/*
359 360 361 362 363
 * 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 (李卓桓) 已提交
364
 */