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

16 17 18
import { Contact }  from './contact'
import { Room }     from './room'
import { UtilLib }  from './util-lib'
19

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
20
export type MsgRawObj = {
21
  MsgId:            string
22 23 24

  MMActualSender:   string // getUserContact(message.MMActualSender,message.MMPeerUserName).isContact()
  MMPeerUserName:   string // message.MsgType == CONF.MSGTYPE_TEXT && message.MMPeerUserName == 'newsapp'
25 26
  ToUserName:       string
  MMActualContent:  string // Content has @id prefix added by wx
27

28
  MMDigest:         string
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
29
  MMDisplayTime:    number  // Javascript timestamp of milliseconds
30 31 32 33 34

  /**
   * MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
   * class="cover" mm-src="{{getMsgImg(message.MsgId,'slave')}}"
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
35
  Url:              string
36 37 38
  MMAppMsgDesc:     string  // class="desc" ng-bind="message.MMAppMsgDesc"

  /**
39 40
   * Attachment
   *
41 42
   * MsgType == MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_ATTACH
   */
43 44 45 46 47 48 49 50
  FileName:         string  // FileName: '钢甲互联项目BP1108.pdf',
  FileSize:         number  // FileSize: '2845701',
  MediaId:          string  // MediaId: '@crypt_b1a45e3f_c21dceb3ac01349...

  MMAppMsgFileExt:      string  // doc, docx ... 'undefined'?
  MMAppMsgFileSize:     string  // '2.7MB',
  MMAppMsgDownloadUrl:  string  // 'https://file.wx.qq.com/cgi-bin/mmwebwx-bin/webwxgetmedia?sender=@4f549c2dafd5ad731afa4d857bf03c10&mediaid=@crypt_b1a45e3f
                                // <a download ng-if="message.MMFileStatus == CONF.MM_SEND_FILE_STATUS_SUCCESS
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
51 52
                                // && (massage.MMStatus == CONF.MSG_SEND_STATUS_SUCC || massage.MMStatus === undefined)
                                // " href="{{message.MMAppMsgDownloadUrl}}">下载</a>
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
  MMUploadProgress: number  // < 100

  /**
   * 模板消息
   * MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_READER_TYPE
   *  item.url
   *  item.title
   *  item.pub_time
   *  item.cover
   *  item.digest
   */
  MMCategory:       any[]  //  item in message.MMCategory

  /**
   * Type
   *
   * MsgType == CONF.MSGTYPE_VOICE : ng-style="{'width':40 + 7*message.VoiceLength/1000}
   */
  MsgType:          number
72 73
  AppMsgType:       AppMsgType  // message.MsgType == CONF.MSGTYPE_APP && message.AppMsgType == CONF.APPMSGTYPE_URL
                                // message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType != CONF.MSGTYPE_LOCATION
74

75
  SubMsgType:       MsgType // "msgType":"{{message.MsgType}}","subType":{{message.SubMsgType||0}},"msgId":"{{message.MsgId}}"
76 77 78 79 80 81 82 83 84

  /**
   * Status-es
   */
  Status:           string
  MMStatus:         number  // img ng-show="message.MMStatus == 1" class="ico_loading"
                            // ng-click="resendMsg(message)" ng-show="message.MMStatus == 5" title="重新发送"
  MMFileStatus:     number  // <p class="loading" ng-show="message.MMStatus == 1 || message.MMFileStatus == CONF.MM_SEND_FILE_STATUS_FAIL">
                            // CONF.MM_SEND_FILE_STATUS_QUEUED, MM_SEND_FILE_STATUS_SENDING
85

86 87 88 89 90
  /**
   * Location
   */
  MMLocationUrl:    string  // ng-if="message.MsgType == CONF.MSGTYPE_TEXT && message.SubMsgType == CONF.MSGTYPE_LOCATION"
                            // <a href="{{message.MMLocationUrl}}" target="_blank">
91 92
                            // 'http://apis.map.qq.com/uri/v1/geocoder?coord=40.075041,116.338994'
  MMLocationDesc:   string  // MMLocationDesc: '北京市昌平区回龙观龙腾苑(五区)内(龙腾街南)',
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

  /**
   * MsgType == CONF.MSGTYPE_EMOTICON
   *
   * getMsgImg(message.MsgId,'big',message)
   */

  /**
   * Image
   *
   *  getMsgImg(message.MsgId,'slave')
   */
  MMImgStyle:       string  // ng-style="message.MMImgStyle"
  MMPreviewSrc:     string  // message.MMPreviewSrc || message.MMThumbSrc || getMsgImg(message.MsgId,'slave')
  MMThumbSrc:       string

  /**
   * Friend Request & ShareCard ?
   *
   * MsgType == CONF.MSGTYPE_SHARECARD" ng-click="showProfile($event,message.RecommendInfo.UserName)
   * MsgType == CONF.MSGTYPE_VERIFYMSG
   */
115
  RecommendInfo?:   RecommendInfo
116 117
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118
export type MsgObj = {
119
  id:       string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
120
  type:     MsgType
121
  from:     string
122
  to?:      string  // if to is not set, then room must be set
123 124 125 126 127 128 129 130 131
  room?:    string
  content:  string
  status:   string
  digest:   string
  date:     string

  url?:     string  // for MessageMedia class
}

132 133
// export type MessageTypeName = 'TEXT' | 'IMAGE' | 'VOICE' | 'VERIFYMSG' | 'POSSIBLEFRIEND_MSG'
// | 'SHARECARD' | 'VIDEO' | 'EMOTICON' | 'LOCATION' | 'APP' | 'VOIPMSG' | 'STATUSNOTIFY'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
134
// | 'VOIPNOTIFY' | 'VOIPINVITE' | 'MICROVIDEO' | 'SYSNOTICE' | 'SYS' | 'RECALLED'
135 136 137

// export type MessageTypeValue = 1 | 3 | 34 | 37 | 40 | 42 | 43 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 62 | 9999 | 10000 | 10002

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138
export type MsgTypeMap = {
139 140 141 142 143
  [index: string]: string|number
  //   MessageTypeName:  MessageTypeValue
  // , MessageTypeValue: MessageTypeName
}

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163
export const enum AppMsgType {
  TEXT                     = 1,
  IMG                      = 2,
  AUDIO                    = 3,
  VIDEO                    = 4,
  URL                      = 5,
  ATTACH                   = 6,
  OPEN                     = 7,
  EMOJI                    = 8,
  VOICE_REMIND             = 9,
  SCAN_GOOD                = 10,
  GOOD                     = 13,
  EMOTION                  = 15,
  CARD_TICKET              = 16,
  REALTIME_SHARE_LOCATION  = 17,
  TRANSFERS                = 2e3,
  RED_ENVELOPES            = 2001,
  READER_TYPE              = 100001,
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
164
export const enum MsgType {
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
  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,
182
  RECALLED            = 10002,
183 184
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
185
export class Message implements Sayable {
186
  public static counter = 0
187
  public _counter: number
188

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
189
  public static TYPE: MsgTypeMap = {
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
    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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212
  protected obj = <MsgObj>{}
213

214
  public readyStream(): Promise<NodeJS.ReadableStream> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
215 216 217
    throw Error('abstract method')
  }

218 219
  public filename(): string {
    throw Error('not a media message')
220 221
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
222
  constructor(public rawObj?: MsgRawObj) {
223
    this._counter = Message.counter++
224
    log.silly('Message', 'constructor() SN:%d', this._counter)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
225

226
    if (typeof rawObj === 'string') {
227
      this.rawObj = JSON.parse(rawObj)
228 229
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
230
    this.rawObj = rawObj = rawObj || <MsgRawObj>{}
231
    this.obj = this.parse(rawObj)
232
    this.id = this.obj.id
233
  }
234

235
  // Transform rawObj to local m
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
236 237
  private parse(rawObj): MsgObj {
    const obj: MsgObj = {
238
      id:           rawObj.MsgId,
239 240 241 242 243 244 245 246
      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
      url:          rawObj.Url || rawObj.MMAppMsgDownloadUrl || rawObj.MMLocationUrl
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
247
    }
248 249

    // FIXME: has ther any better method to know the room ID?
250 251 252 253 254 255 256
    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(/^@@/)')
257
        // obj.room = undefined // bug compatible
258
      }
259 260 261
      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
      }
262
    }
263

264
    return obj
265
  }
266
  public toString() {
267
    return UtilLib.plainText(this.obj.content)
268
  }
269
  public toStringDigest() {
270
    const text = UtilLib.digestEmoji(this.obj.digest)
271
    return '{' + this.typeEx() + '}' + text
272 273
  }

274
  public toStringEx() {
275
    let s = `${this.constructor.name}#${this._counter}`
276 277 278
    s += '(' + this.getSenderString()
    s += ':' + this.getContentString() + ')'
    return s
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
279
  }
280
  public getSenderString() {
281 282 283 284 285 286
    const fromName  = Contact.load(this.obj.from).name()
    const roomTopic = this.obj.room
                  ? Room.load(this.obj.room).topic()
                  : ''
    // return '<' + (name ? name.toStringEx() : '') + roomTopic + '>'
    return `<${fromName}:${roomTopic}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287
  }
288
  public getContentString() {
289
    let content = UtilLib.plainText(this.obj.content)
290
    if (content.length > 20) { content = content.substring(0, 17) + '...' }
291 292
    return '{' + this.type() + '}' + content
  }
293

294 295
  public from(contact: Contact): void
  public from(id: string): void
296
  public from(): Contact
297
  public from(contact?: Contact|string): Contact|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298
    if (contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
299 300
      if (contact instanceof Contact) {
        this.obj.from = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
302 303 304 305
        this.obj.from = contact
      } else {
        throw new Error('unsupport from param: ' + typeof contact)
      }
306
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
307
    }
308 309 310 311 312 313

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

316 317 318
  // public to(room: Room): void
  // public to(): Contact|Room
  // public to(contact?: Contact|Room|string): Contact|Room|void {
319 320
  public to(contact: Contact): void
  public to(id: string): void
321 322 323
  public to(): Contact|null // if to is not set, then room must had set

  public to(contact?: Contact|string): Contact|Room|null|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
324
    if (contact) {
325
      if (contact instanceof Contact) {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
326
        this.obj.to = contact.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327
      } else if (typeof contact === 'string') {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
328 329 330 331
        this.obj.to = contact
      } else {
        throw new Error('unsupport to param ' + typeof contact)
      }
332
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
333
    }
334

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
335 336
    // no parameter

337 338
    if (!this.obj.to) {
      return null
339
    }
340
    return Contact.load(this.obj.to)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
341 342
  }

343 344
  public room(room: Room): void
  public room(id: string): void
345
  public room(): Room|null
346
  public room(room?: Room|string): Room|null|void {
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
347 348 349 350 351 352 353 354
    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)
      }
355
      return
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
356
    }
357 358
    if (this.obj.room) {
      return Room.load(this.obj.room)
359
    }
360
    return null
Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
361 362
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
363 364 365 366
  public content(): string
  public content(content: string): void

  public content(content?: string): string|void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
367 368
    if (content) {
      this.obj.content = content
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
369
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
370 371 372 373
    }
    return this.obj.content
  }

374
  public type(): MsgType {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
375
    return this.obj.type
376 377 378 379 380 381 382 383 384 385 386 387 388 389
  }

  public typeSub(): MsgType {
    if (!this.rawObj) {
      throw new Error('no rawObj')
    }
    return this.rawObj.SubMsgType
  }

  public typeApp(): AppMsgType {
    if (!this.rawObj) {
      throw new Error('no rawObj')
    }
    return this.rawObj.AppMsgType
390 391
  }

392
  public typeEx()  { return Message.TYPE[this.obj.type] }
393
  public count()   { return this._counter }
394

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
395 396 397 398
  public self(): boolean {
    const userId = Config.puppetInstance()
                        .userId

399
    const fromId = this.obj.from
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
400 401 402 403 404 405 406
    if (!userId || !fromId) {
      throw new Error('no user or no from')
    }

    return fromId === userId
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
407 408 409 410 411
  // public ready() {
  //   log.warn('Message', 'ready() DEPRECATED. use load() instead.')
  //   return this.ready()
  // }

412
  public async ready(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
413
    log.silly('Message', 'ready()')
414

415
    try {
416
      const from  = Contact.load(this.obj.from)
417
      await from.ready()                // Contact from
418

419 420 421
      if (this.obj.to) {
        const to = Contact.load(this.obj.to)
        await to.ready()
422
      }
423

424 425
      if (this.obj.room) {
        const room  = Room.load(this.obj.room)
426
        await room.ready()  // Room member list
427
      }
428

429
    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
430
        log.error('Message', 'ready() exception: %s', e.stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
431 432 433
        // console.log(e)
        // this.dump()
        // this.dumpRaw()
434
        throw e
435
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
436 437
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
438 439 440
  /**
   * @deprecated
   */
441
  public get(prop: string): string {
442
    log.warn('Message', 'DEPRECATED get() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
443

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
444 445
    if (!prop || !(prop in this.obj)) {
      const s = '[' + Object.keys(this.obj).join(',') + ']'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
446 447
      throw new Error(`Message.get(${prop}) must be in: ${s}`)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
448
    return this.obj[prop]
449 450
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
451 452 453
  /**
   * @deprecated
   */
454
  public set(prop: string, value: string): this {
455
    log.warn('Message', 'DEPRECATED set() at %s', new Error('stack').stack)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
456

457 458 459
    if (typeof value !== 'string') {
      throw new Error('value must be string, we got: ' + typeof value)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
460
    this.obj[prop] = value
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
461
    return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
462
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
463

464
  public dump() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
465 466
    console.error('======= dump message =======')
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
467
  }
468
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
469
    console.error('======= dump raw message =======')
470
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj && this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
471 472
  }

473
  public static async find(query) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
474
    return Promise.resolve(new Message(<MsgRawObj>{MsgId: '-1'}))
475 476
  }

477 478
  public static async findAll(query) {
    return Promise.resolve([
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
479 480
      new Message   (<MsgRawObj>{MsgId: '-2'})
      , new Message (<MsgRawObj>{MsgId: '-3'})
481
    ])
482 483
  }

484 485 486 487 488 489
  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 (李卓桓) 已提交
490 491 492 493 494

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

    const m = new Message()
495 496 497 498
    const room = this.room()
    if (room) {
      m.room(room)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515

    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)

    }
516
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
517 518 519
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
520
}
521 522

Message.initType()
523

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
524
export * from './message-media'
525

Huan (李卓桓)'s avatar
doc  
Huan (李卓桓) 已提交
526
/*
527 528 529 530 531
 * 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 (李卓桓) 已提交
532
 */