message.ts 8.6 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 12 13
import {
    Config
  , RecommendInfo
}               from './config'

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

19 20 21 22 23 24 25 26 27
type MessageRawObj = {
  MsgId:            string
  MsgType:          string
  MMActualSender:   string
  ToUserName:       string
  MMActualContent:  string // Content has @id prefix added by wx
  Status:           string
  MMDigest:         string
  MMDisplayTime:    string  // Javascript timestamp of milliseconds
28 29

  RecommendInfo?:   RecommendInfo
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
}

type MessageObj = {
  id:       string
  type:     string
  from:     string
  to:       string
  room?:    string
  content:  string
  status:   string
  digest:   string
  date:     string

  url?:     string  // for MessageMedia class
}

type MessageType = {
  [index: string]: number|string
}

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

  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>{}

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

83
  constructor(public rawObj?: MessageRawObj) {
84
    this._counter = Message.counter++
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
85

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

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

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

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

131
  public toStringEx() {
132
    let s = `${this.constructor.name}#${this._counter}`
133 134 135
    s += '(' + this.getSenderString()
    s += ':' + this.getContentString() + ')'
    return s
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
136
  }
137
  public getSenderString() {
138
    const name  = Contact.load(this.obj.from).toStringEx()
139 140
    const room = this.obj.room ? Room.load(this.obj.room).toStringEx() : null
    return '<' + name + (room ? `@${room}` : '') + '>'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
141
  }
142
  public getContentString() {
143
    let content = UtilLib.plainText(this.obj.content)
144
    if (content.length > 20) { content = content.substring(0, 17) + '...' }
145 146
    return '{' + this.type() + '}' + content
  }
147

148 149
  public from(contact?: Contact): Contact
  public from(id?: string): Contact
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
150
  public from(contact?: Contact|string): Contact {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
151
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
152 153
      if (contact instanceof Contact) {
        this.obj.from = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
155 156 157 158
        this.obj.from = contact
      } else {
        throw new Error('unsupport from param: ' + typeof contact)
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159 160 161 162
    }
    return this.obj.from ? Contact.load(this.obj.from) : null
  }

163 164 165 166
  public to(contact?: Contact): Contact
  public to(room?: Room): Room
  public to(id?: string): Contact|Room
  public to(contact?: Contact|Room|string): Contact|Room {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
167
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
168 169
      if (contact instanceof Contact || contact instanceof Room) {
        this.obj.to = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
170
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
171 172 173 174
        this.obj.to = contact
      } else {
        throw new Error('unsupport to param ' + typeof contact)
      }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
175
    }
176 177 178 179 180 181 182 183
    if (!this.obj.to) {
      return null
    }

    // FIXME: better to identify a room id?
    return /^@@/.test(this.obj.to)
            ? Room.load(this.obj.to)
            : Contact.load(this.obj.to)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
184 185
  }

186 187
  public room(room?: Room): Room
  public room(id?: string): Room
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
188 189 190 191 192 193 194 195 196 197 198 199 200
  public room(room?: Room|string): Room {
    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)
      }
    }
    return this.obj.room ? Room.load(this.obj.room) : null
  }

201
  public content(content?: string) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
202 203 204 205 206 207
    if (content) {
      this.obj.content = content
    }
    return this.obj.content
  }

208
  public type()    { return this.obj.type }
209
  public typeEx()  { return Message.TYPE[this.obj.type] }
210
  public count()   { return this._counter }
211

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

215 216
    // return co.call(this, function* () {
    try {
217 218 219 220
      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

221 222 223
      await from.ready()                // Contact from
      await to.ready()                  // Contact to
      if (room) { await room.ready() }  // Room member list
224

225
      return this         // return this for chain
226 227
    // }).catch(e => { // Exception
    } catch (e) {
228
        log.error('Message', 'ready() exception: %s', e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
229 230 231
        // console.log(e)
        // this.dump()
        // this.dumpRaw()
232
        throw e
233
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
234 235
  }

236
  public get(prop: string): string {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
237 238
    if (!prop || !(prop in this.obj)) {
      const s = '[' + Object.keys(this.obj).join(',') + ']'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
239 240
      throw new Error(`Message.get(${prop}) must be in: ${s}`)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
241
    return this.obj[prop]
242 243
  }

244 245 246 247
  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 (李卓桓) 已提交
248
    this.obj[prop] = value
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
249
    return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
250
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
251

252
  public dump() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
253 254
    console.error('======= dump message =======')
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255
  }
256
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
257
    console.error('======= dump raw message =======')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
259 260
  }

261 262
  public static async find(query) {
    return Promise.resolve(new Message(<MessageRawObj>{MsgId: '-1'}))
263 264
  }

265 266 267 268 269
  public static async findAll(query) {
    return Promise.resolve([
      new Message   (<MessageRawObj>{MsgId: '-2'})
      , new Message (<MessageRawObj>{MsgId: '-3'})
    ])
270 271
  }

272 273 274 275 276 277
  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 (李卓桓) 已提交
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300

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

    const m = new Message()
    m.room(this.room())

    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)

    }
301
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
302 303 304
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
305
}
306 307

Message.initType()
308

309
export default Message
310 311
export { Message }
export { MediaMessage } from './message-media'
312

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
313
/*
314 315 316 317 318
 * 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 (李卓桓) 已提交
319
 */