message.ts 9.4 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): Contact
  public from(id?: string): Contact
155
  public from(): Contact
156
  public from(contact?: Contact|string): Contact {
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)
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
165
    }
166 167 168 169 170 171

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

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

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

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

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

225
  public type()    { return this.obj.type }
226
  public typeEx()  { return Message.TYPE[this.obj.type] }
227
  public count()   { return this._counter }
228

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
229 230 231 232 233 234 235 236 237 238 239 240
  public self(): boolean {
    const userId = Config.puppetInstance()
                        .userId

    const fromId = this.obj.id
    if (!userId || !fromId) {
      throw new Error('no user or no from')
    }

    return fromId === userId
  }

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

244
    try {
245 246 247 248
      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

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

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

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

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

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

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

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

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

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

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

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

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

    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)

    }
344
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
345 346 347
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
348
}
349 350

Message.initType()
351

352
export default Message
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
353
export * from './message-media'
354

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