message.ts 9.1 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++
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
87

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

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

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

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

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

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

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

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

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

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

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

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

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

231 232
    // return co.call(this, function* () {
    try {
233 234 235 236
      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

237 238 239
      if (!from || !to) {
        throw new Error('no `from` or no `to`')
      }
240 241 242
      await from.ready()                // Contact from
      await to.ready()                  // Contact to
      if (room) { await room.ready() }  // Room member list
243

244
      return this         // return this for chain
245 246
    // }).catch(e => { // Exception
    } catch (e) {
247
        log.error('Message', 'ready() exception: %s', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
248 249 250
        // console.log(e)
        // this.dump()
        // this.dumpRaw()
251
        throw e
252
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
253 254
  }

255
  public get(prop: string): string {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
256 257
    if (!prop || !(prop in this.obj)) {
      const s = '[' + Object.keys(this.obj).join(',') + ']'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258 259
      throw new Error(`Message.get(${prop}) must be in: ${s}`)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
260
    return this.obj[prop]
261 262
  }

263 264 265 266
  public set(prop: string, value: string): this {
    if (typeof value !== 'string') {
      throw new Error('value must be string, we got: ' + typeof value)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
267
    this.obj[prop] = value
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
268
    return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
269
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
270

271
  public dump() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
272 273
    console.error('======= dump message =======')
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
274
  }
275
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
276
    console.error('======= dump raw message =======')
277
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj && this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
278 279
  }

280 281
  public static async find(query) {
    return Promise.resolve(new Message(<MessageRawObj>{MsgId: '-1'}))
282 283
  }

284 285 286 287 288
  public static async findAll(query) {
    return Promise.resolve([
      new Message   (<MessageRawObj>{MsgId: '-2'})
      , new Message (<MessageRawObj>{MsgId: '-3'})
    ])
289 290
  }

291 292 293 294 295 296
  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 (李卓桓) 已提交
297 298 299 300 301

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

    const m = new Message()
302 303 304 305
    const room = this.room()
    if (room) {
      m.room(room)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322

    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)

    }
323
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
324 325 326
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327
}
328 329

Message.initType()
330

331
export default Message
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
332
export * from './message-media'
333

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
334
/*
335 336 337 338 339
 * 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 (李卓桓) 已提交
340
 */