room.ts 15.1 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1 2 3 4 5 6 7
/**
 *
 * wechaty: Wechat for Bot. and for human who talk to bot/robot
 *
 * Licenst: ISC
 * https://github.com/zixia/wechaty
 *
8 9
 * Add/Del/Topic: https://github.com/wechaty/wechaty/issues/32
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
10
 */
11
import { EventEmitter } from 'events'
12

13 14 15
import {
    Config
  , Sayable
16
  , log
17
}                 from './config'
18 19 20
import { Contact }    from './contact'
import { Message }    from './message'
import { UtilLib }    from './util-lib'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
21

22 23 24 25 26 27
type RoomObj = {
  id:         string
  encryId:    string
  topic:      string
  ownerUin:   number
  memberList: Contact[]
28 29 30
  nameMap:      Map<string, string>
  aliasMap:     Map<string, string>
  roomAliasMap: Map<string, string>
31 32
}

33
type NameType = 'name' | 'alias' | 'roomAlias'
34

35
export type RoomRawMember = {
36
  UserName:     string
37
  NickName:     string
38 39 40
  DisplayName:  string
}

41
export type RoomRawObj = {
42 43 44 45
  UserName:         string
  EncryChatRoomId:  string
  NickName:         string
  OwnerUin:         number
46
  ChatRoomOwner:    string
47
  MemberList:       RoomRawMember[]
48 49
}

50 51 52
export type RoomEventName = 'join'
                          | 'leave'
                          | 'topic'
53 54
                          | 'EVENT_PARAM_ERROR'

55
export type RoomQueryFilter = {
56 57 58
  topic: string | RegExp
}

59
export type MemberQueryFilter = {
60 61 62
  name?:      string
  alias?:     string
  roomAlias?: string
63 64
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
65
export class Room extends EventEmitter implements Sayable {
66 67
  private static pool = new Map<string, Room>()

68 69
  private dirtyObj: RoomObj | null // when refresh, use this to save dirty data for query
  private obj:      RoomObj | null
70 71
  private rawObj:   RoomRawObj

72
  constructor(public id: string) {
73
    super()
74
    log.silly('Room', `constructor(${id})`)
75
  }
76

77
  public toString()    { return this.id }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
78
  public toStringEx()  { return `Room(${this.obj && this.obj.topic}[${this.id}])` }
79

80
  public isReady(): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
81
    return !!(this.obj && this.obj.memberList && this.obj.memberList.length)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
82 83
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
84 85 86 87 88
  // public refresh() {
  //   log.warn('Room', 'refresh() DEPRECATED. use reload() instead.')
  //   return this.reload()
  // }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
89
  public async refresh(): Promise<void> {
90 91 92
    if (this.isReady()) {
      this.dirtyObj = this.obj
    }
93
    this.obj = null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
94 95
    await this.ready()
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
96 97
  }

98 99 100 101 102 103 104 105
  private async readyAllMembers(memberList: RoomRawMember[]): Promise<void> {
    for (let member of memberList) {
      let contact = Contact.load(member.UserName)
      await contact.ready()
    }
    return
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106 107 108 109 110
  // public ready(contactGetter?: (id: string) => Promise<any>) {
  //   log.warn('Room', 'ready() DEPRECATED. use load() instad.')
  //   return this.load(contactGetter)
  // }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
111
  public async ready(contactGetter?: (id: string) => Promise<any>): Promise<void> {
112
    log.silly('Room', 'ready(%s)', contactGetter ? contactGetter.constructor.name : '')
113
    if (!this.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114 115
      const e = new Error('ready() on a un-inited Room')
      log.warn('Room', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
116
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
117
    } else if (this.isReady()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
118
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
119
    } else if (this.obj && this.obj.id) {
120
      log.warn('Room', 'ready() has obj.id but memberList empty in room %s. reloading', this.obj.topic)
121
    }
122

123 124 125 126 127 128 129 130
    if (!contactGetter) {
      contactGetter = Config.puppetInstance()
                            .getContact.bind(Config.puppetInstance())
    }
    if (!contactGetter) {
      throw new Error('no contactGetter')
    }

131 132
    try {
      const data = await contactGetter(this.id)
133
      log.silly('Room', `contactGetter(${this.id}) resolved`)
134
      this.rawObj = data
135 136
      await this.readyAllMembers(this.rawObj.MemberList)
      this.obj    = this.parse(this.rawObj)
137 138 139
      if (!this.obj) {
        throw new Error('no this.obj set after contactGetter')
      }
140
      await Promise.all(this.obj.memberList.map(c => c.ready(contactGetter)))
141

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
142
      return
143

144
    } catch (e) {
145 146
      log.error('Room', 'contactGetter(%s) exception: %s', this.id, e.message)
      throw e
147
    }
148 149
  }

150 151 152
  public on(event: 'leave', listener: (this: Room, leaver: Contact) => void): this
  public on(event: 'join' , listener: (this: Room, inviteeList: Contact[] , inviter: Contact)  => void): this
  public on(event: 'topic', listener: (this: Room, topic: string, oldTopic: string, changer: Contact) => void): this
153
  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this
154

155
  public on(event: RoomEventName, listener: Function): this {
156
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
157

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
158 159 160 161 162 163 164 165 166 167 168
    // const thisWithSay = {
    //   say: (content: string) => {
    //     return Config.puppetInstance()
    //                   .say(content)
    //   }
    // }
    // super.on(event, function() {
    //   return listener.apply(thisWithSay, arguments)
    // })

    super.on(event, listener) // Room is `Sayable`
169
    return this
170 171
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
172 173 174
  public say(content: string): Promise<any>
  public say(content: string, replyTo: Contact): Promise<void>
  public say(content: string, replyTo: Contact[]): Promise<void>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
175

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
176
  public say(content: string, replyTo?: Contact|Contact[]): Promise<void> {
177 178 179 180
    log.verbose('Room', 'say(%s, %s)'
                      , content
                      , Array.isArray(replyTo)
                        ? replyTo.map(c => c.name()).join(', ')
181
                        : replyTo ? replyTo.name() : ''
182
    )
183 184 185 186

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
187
    const replyToList: Contact[] = [].concat(replyTo as any || [])
188

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
189 190 191 192 193 194 195
    if (replyToList.length > 0) {
      const mentionList = replyToList.map(c => '@' + c.name()).join(' ')
      m.content(mentionList + ' ' + content)
    } else {
      m.content(content)
    }
    // m.to(replyToList[0])
196 197 198 199 200

    return Config.puppetInstance()
                  .send(m)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
201
  public get(prop): string { return (this.obj && this.obj[prop]) || (this.dirtyObj && this.dirtyObj[prop]) }
202

203
  private parse(rawObj: RoomRawObj): RoomObj | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
204
    if (!rawObj) {
205
      log.warn('Room', 'parse() on a empty rawObj?')
206
      return null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
207
    }
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
208

209 210 211 212
    const memberList    = this.parseMemberList(rawObj.MemberList)
    const nameMap       = this.parseMap(rawObj.MemberList, 'name')
    const aliasMap      = this.parseMap(rawObj.MemberList, 'alias')
    const roomAliasMap  = this.parseMap(rawObj.MemberList, 'roomAlias')
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
213

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
214
    return {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
215 216 217 218
      id:         rawObj.UserName,
      encryId:    rawObj.EncryChatRoomId, // ???
      topic:      rawObj.NickName,
      ownerUin:   rawObj.OwnerUin,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
219

Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
220
      memberList,
221 222 223
      nameMap,
      aliasMap,
      roomAliasMap,
224 225 226
    }
  }

227 228
  private parseMemberList(rawMemberList: RoomRawMember[]): Contact[] {
    if (!rawMemberList || !rawMemberList.map) {
229 230
      return []
    }
231
    return rawMemberList.map(m => Contact.load(m.UserName))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
232
  }
233 234
  private parseMap(memberList: RoomRawMember[], parseContent: NameType): Map<string, string> {
    const mapList: Map<string, string> = new Map<string, string>()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
235
    if (memberList && memberList.map) {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
236
      memberList.forEach(member => {
237 238 239
        let tmpName: string
        let contact = Contact.load(member.UserName)
        switch (parseContent) {
240
          case 'name':
241 242
            tmpName = contact.name()
            break
243 244
          case 'alias':
            tmpName = contact.alias() || ''
245
            break
246
          case 'roomAlias':
247 248 249 250 251
            tmpName = member.DisplayName
            break
          default:
            throw new Error('parseMap failed, member not found')
        }
252 253
        /**
         * ISSUE #64 emoji need to be striped
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
254
         * ISSUE #104 never use remark name because sys group message will never use that
ruiruibupt's avatar
#217  
ruiruibupt 已提交
255
         * @rui: Wrong for 'never use remark name because sys group message will never use that', see more in the latest comment in #104
256
         * @rui: cannot use argument NickName because it mix real name and alias
257
         */
258
        mapList[member.UserName] = UtilLib.stripEmoji(tmpName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
259
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
260
    }
261
    return mapList
262 263
  }

264
  public dumpRaw() {
265
    console.error('======= dump raw Room =======')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
266
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
267
  }
268
  public dump() {
269
    console.error('======= dump Room =======')
270
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj && this.obj[k]}`))
271 272
  }

Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
273
  public async add(contact: Contact): Promise<number> {
274
    log.verbose('Room', 'add(%s)', contact)
275 276 277 278 279

    if (!contact) {
      throw new Error('contact not found')
    }

Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
280 281 282
    const n = Config.puppetInstance()
                      .roomAdd(this, contact)
    return n
283 284
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285
  public async del(contact: Contact): Promise<number> {
286
    log.verbose('Room', 'del(%s)', contact.name())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287 288 289 290

    if (!contact) {
      throw new Error('contact not found')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
291
    const n = await Config.puppetInstance()
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
292 293
                            .roomDel(this, contact)
                            .then(_ => this.delLocal(contact))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
294
    return n
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
295 296
  }

297
  private delLocal(contact: Contact): number {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298 299
    log.verbose('Room', 'delLocal(%s)', contact)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300
    const memberList = this.obj && this.obj.memberList
301
    if (!memberList || memberList.length === 0) {
302
      return 0 // already in refreshing
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
303 304 305
    }

    let i
306 307
    for (i = 0; i < memberList.length; i++) {
      if (memberList[i].id === contact.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
308 309 310
        break
      }
    }
311 312
    if (i < memberList.length) {
      memberList.splice(i, 1)
313
      return 1
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
314
    }
315
    return 0
316
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
317

318
  public quit() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
319 320
    throw new Error('wx web not implement yet')
    // WechatyBro.glue.chatroomFactory.quit("@@1c066dfcab4ef467cd0a8da8bec90880035aa46526c44f504a83172a9086a5f7"
321
  }
322

323 324 325 326 327 328 329 330 331 332
  /**
   * get topic
   */
  public topic(): string
  /**
   * set topic
   */
  public topic(newTopic: string): void

  public topic(newTopic?: string): string | void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
333
    if (!this.isReady()) {
334
      log.warn('Room', 'topic() room not ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
335 336
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
337 338
    if (newTopic) {
      log.verbose('Room', 'topic(%s)', newTopic)
339
      Config.puppetInstance().roomTopic(this, newTopic)
340 341 342 343 344 345 346 347 348
                              .catch(e => {
                                log.warn('Room', 'topic(newTopic=%s) exception: %s',
                                                  newTopic, e && e.message || e
                                )
                              })
      if (!this.obj) {
        this.obj = <RoomObj>{}
      }
      Object.assign(this.obj, { topic: newTopic })
349
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
350
    }
351
    return UtilLib.plainText(this.obj ? this.obj.topic : '')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
352 353
  }

354
  // should be deprecated
355
  public nick(contact: Contact): string {
356
    log.warn('Room', 'nick(Contact) DEPRECATED, use name(Contact) instead.')
ruiruibupt's avatar
#217  
ruiruibupt 已提交
357
    return this.alias(contact)
358 359
  }

ruiruibupt's avatar
#217  
ruiruibupt 已提交
360
  public alias(contact: Contact): string {
361 362 363 364
    if (!this.obj || !this.obj.nameMap) {
      return ''
    }
    return this.obj.roomAliasMap[contact.id] || this.obj.nameMap[contact.id]
365 366
  }

367
  public has(contact: Contact): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
368
    if (!this.obj || !this.obj.memberList) {
369 370 371 372 373 374 375
      return false
    }
    return this.obj.memberList
                    .filter(c => c.id === contact.id)
                    .length > 0
  }

376
  public owner(): Contact | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
377 378
    const ownerUin = this.obj && this.obj.ownerUin
    let memberList = (this.obj && this.obj.memberList) || []
379 380 381 382 383 384 385 386

    let user = Config.puppetInstance()
                      .user

    if (user && user.get('uin') === ownerUin) {
      return user
    }

387
    memberList = memberList.filter(m => m.get('uin') === ownerUin)
388 389 390
    if (memberList.length > 0) {
      return memberList[0]
    }
J
jaslin 已提交
391 392

    if (this.rawObj.ChatRoomOwner) {
393 394 395 396
      return Contact.load(this.rawObj.ChatRoomOwner)
    }

    return null
397 398
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
399
  /**
400
   * find member by `name` / `roomAlias` / `alias`
401
   * when use member(name:string), find all name by default
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
402
   */
403 404 405 406 407 408

  public member(filter: MemberQueryFilter): Contact | null
  public member(name: string): Contact | null

  public member(queryArg: MemberQueryFilter | string): Contact | null {
    if (typeof queryArg === 'string') {
409
      return this.member({alias: queryArg}) || this.member({roomAlias: queryArg}) || this.member({name: queryArg})
410 411 412 413 414 415 416 417 418 419 420
    }

    log.silly('Room', 'member({ %s })'
                        , Object.keys(queryArg)
                                .map(k => `${k}: ${queryArg[k]}`)
                                .join(', ')
            )

    if (Object.keys(queryArg).length !== 1) {
      throw new Error('Room member find quaryArg only support one key. multi key support is not availble now.')
    }
421

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
422
    if (!this.obj || !this.obj.memberList) {
423
      log.warn('Room', 'member() not ready')
424 425
      return null
    }
426
    let filterKey            = Object.keys(queryArg)[0]
427 428 429
    /**
     * ISSUE #64 emoji need to be striped
     */
430 431 432
    let filterValue: string  = UtilLib.stripEmoji(queryArg[filterKey])

    const keyMap = {
433 434 435
      name:       'nameMap',
      alias:      'aliasMap',
      roomAlias:  'roomAliasMap'
436 437 438 439 440 441 442 443 444 445
    }

    filterKey = keyMap[filterKey]
    if (!filterKey) {
      throw new Error('unsupport filter key')
    }

    if (!filterValue) {
      throw new Error('filterValue not found')
    }
446

447 448 449
    const filterMap = this.obj[filterKey]
    const idList = Object.keys(filterMap)
                          .filter(k => filterMap[k] === filterValue)
450

451
    log.silly('Room', 'member() check %s: %s', filterKey, filterValue)
452

453 454 455 456 457
    if (idList.length) {
      return Contact.load(idList[0])
    } else {
      return null
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
458 459
  }

460
  public memberList(): Contact[] {
461
    log.verbose('Room', 'memberList')
462 463 464

    if (!this.obj || !this.obj.memberList || this.obj.memberList.length < 1) {
      log.warn('Room', 'memberList() not ready')
465
      return []
466 467 468 469
    }
    return this.obj.memberList
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
470
  public static create(contactList: Contact[], topic?: string): Promise<Room> {
471
    log.verbose('Room', 'create(%s, %s)', contactList.join(','), topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
472

Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
473
    if (!contactList || !Array.isArray(contactList)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
474 475
      throw new Error('contactList not found')
    }
476

477
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
478
                  .roomCreate(contactList, topic)
479 480 481
                  .catch(e => {
                    log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
                    throw e
482
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
483 484
  }

485 486 487 488
  public static async findAll(query?: RoomQueryFilter): Promise<Room[]> {
    if (!query) {
      query = { topic: /.*/ }
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
489
    log.verbose('Room', 'findAll({ topic: %s })', query.topic)
490

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
491
    let topicFilter = query.topic
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
492

493 494
    if (!topicFilter) {
      throw new Error('topicFilter not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
495 496
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
497 498
    let filterFunction: string

499
    if (topicFilter instanceof RegExp) {
500
      filterFunction = `(function (c) { return ${topicFilter.toString()}.test(c) })`
501
    } else if (typeof topicFilter === 'string') {
502
      topicFilter = topicFilter.replace(/'/g, '\\\'')
503
      filterFunction = `(function (c) { return c === '${topicFilter}' })`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
504
    } else {
505
      throw new Error('unsupport topic type')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
506 507
    }

508 509 510
    return Config.puppetInstance()
                  .roomFind(filterFunction)
                  .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
511
                    log.verbose('Room', 'findAll() rejected: %s', e.message)
512 513
                    return [] // fail safe
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
514 515
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
516
  public static async find(query: RoomQueryFilter): Promise<Room> {
517
    log.verbose('Room', 'find({ topic: %s })', query.topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
518

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
519 520 521 522
    const roomList = await Room.findAll(query)
    if (!roomList || roomList.length < 1) {
      throw new Error('no room found')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
523 524 525
    const room = roomList[0]
    await room.ready()
    return room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
526 527
  }

528 529 530 531
  public static load(id: string): Room {
    if (!id) {
      throw new Error('Room.load() no id')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
532 533 534 535 536 537 538

    if (id in Room.pool) {
      return Room.pool[id]
    }
    return Room.pool[id] = new Room(id)
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
539
}