room.ts 12.5 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 28 29 30
type RoomObj = {
  id:         string
  encryId:    string
  topic:      string
  ownerUin:   number
  memberList: Contact[]
  nickMap:    Map<string, string>
}

31
export type RoomRawMember = {
32
  UserName:     string
33
  NickName:     string
34 35 36
  DisplayName:  string
}

37
export type RoomRawObj = {
38 39 40 41
  UserName:         string
  EncryChatRoomId:  string
  NickName:         string
  OwnerUin:         number
42
  ChatRoomOwner:    string
43
  MemberList:       RoomRawMember[]
44 45
}

46 47 48
export type RoomEventName = 'join'
                          | 'leave'
                          | 'topic'
49 50
                          | 'EVENT_PARAM_ERROR'

51
export type RoomQueryFilter = {
52 53 54
  topic: string | RegExp
}

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

58 59
  private dirtyObj: RoomObj | null // when refresh, use this to save dirty data for query
  private obj:      RoomObj | null
60 61
  private rawObj:   RoomRawObj

62
  constructor(public id: string) {
63
    super()
64
    log.silly('Room', `constructor(${id})`)
65
  }
66

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

70
  public isReady(): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
71
    return !!(this.obj && this.obj.memberList && this.obj.memberList.length)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
72 73
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
74 75 76 77 78
  // public refresh() {
  //   log.warn('Room', 'refresh() DEPRECATED. use reload() instead.')
  //   return this.reload()
  // }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
79
  public async refresh(): Promise<void> {
80 81 82
    if (this.isReady()) {
      this.dirtyObj = this.obj
    }
83
    this.obj = null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
84 85
    await this.ready()
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
86 87
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
88 89 90 91 92
  // public ready(contactGetter?: (id: string) => Promise<any>) {
  //   log.warn('Room', 'ready() DEPRECATED. use load() instad.')
  //   return this.load(contactGetter)
  // }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93
  public async ready(contactGetter?: (id: string) => Promise<any>): Promise<void> {
94
    log.silly('Room', 'ready(%s)', contactGetter ? contactGetter.constructor.name : '')
95
    if (!this.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
96 97
      const e = new Error('ready() on a un-inited Room')
      log.warn('Room', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
98
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
99
    } else if (this.isReady()) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
100
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
101
    } else if (this.obj && this.obj.id) {
102
      log.warn('Room', 'ready() has obj.id but memberList empty in room %s. reloading', this.obj.topic)
103
    }
104

105 106 107 108 109 110 111 112
    if (!contactGetter) {
      contactGetter = Config.puppetInstance()
                            .getContact.bind(Config.puppetInstance())
    }
    if (!contactGetter) {
      throw new Error('no contactGetter')
    }

113 114
    try {
      const data = await contactGetter(this.id)
115
      log.silly('Room', `contactGetter(${this.id}) resolved`)
116 117
      this.rawObj = data
      this.obj    = this.parse(data)
118

119 120 121
      if (!this.obj) {
        throw new Error('no this.obj set after contactGetter')
      }
122
      await Promise.all(this.obj.memberList.map(c => c.ready(contactGetter)))
123

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
124
      return
125

126
    } catch (e) {
127 128
      log.error('Room', 'contactGetter(%s) exception: %s', this.id, e.message)
      throw e
129
    }
130 131
  }

132 133 134
  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
135
  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this
136

137
  public on(event: RoomEventName, listener: Function): this {
138
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
139

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
140 141 142 143 144 145 146 147 148 149 150
    // 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`
151
    return this
152 153
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
154 155 156
  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 (李卓桓) 已提交
157

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
158
  public say(content: string, replyTo?: Contact|Contact[]): Promise<void> {
159 160 161 162
    log.verbose('Room', 'say(%s, %s)'
                      , content
                      , Array.isArray(replyTo)
                        ? replyTo.map(c => c.name()).join(', ')
163
                        : replyTo ? replyTo.name() : ''
164
    )
165 166 167 168

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
171 172 173 174 175 176 177
    if (replyToList.length > 0) {
      const mentionList = replyToList.map(c => '@' + c.name()).join(' ')
      m.content(mentionList + ' ' + content)
    } else {
      m.content(content)
    }
    // m.to(replyToList[0])
178 179 180 181 182

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

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

185
  private parse(rawObj: RoomRawObj): RoomObj | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
186
    if (!rawObj) {
187
      log.warn('Room', 'parse() on a empty rawObj?')
188
      return null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
189
    }
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
190 191

    const memberList  = this.parseMemberList(rawObj.MemberList)
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
192
    const nickMap     = this.parseNickMap(rawObj.MemberList)
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
193

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
194
    return {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
195 196 197 198
      id:         rawObj.UserName,
      encryId:    rawObj.EncryChatRoomId, // ???
      topic:      rawObj.NickName,
      ownerUin:   rawObj.OwnerUin,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
199

Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
200 201
      memberList,
      nickMap,
202 203 204
    }
  }

205 206
  private parseMemberList(rawMemberList: RoomRawMember[]): Contact[] {
    if (!rawMemberList || !rawMemberList.map) {
207 208
      return []
    }
209
    return rawMemberList.map(m => Contact.load(m.UserName))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
210 211
  }

Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
212
  private parseNickMap(memberList: RoomRawMember[]): Map<string, string> {
213
    const nickMap: Map<string, string> = new Map<string, string>()
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
214

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
215
    if (memberList && memberList.map) {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
216
      memberList.forEach(member => {
217 218
        /**
         * ISSUE #64 emoji need to be striped
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
219
         * ISSUE #104 never use remark name because sys group message will never use that
220
         */
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
221 222
        nickMap[member.UserName] = UtilLib.stripEmoji(
          member.DisplayName || member.NickName
223
        )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
224
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
225 226
    }
    return nickMap
227 228
  }

229
  public dumpRaw() {
230
    console.error('======= dump raw Room =======')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
231
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
232
  }
233
  public dump() {
234
    console.error('======= dump Room =======')
235
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj && this.obj[k]}`))
236 237
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
238
  public async add(contact: Contact): Promise<any> {
239
    log.verbose('Room', 'add(%s)', contact)
240 241 242 243 244

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
245 246 247
    await Config.puppetInstance()
                .roomAdd(this, contact)
    return
248 249
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
250
  public async del(contact: Contact): Promise<number> {
251
    log.verbose('Room', 'del(%s)', contact.name())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252 253 254 255

    if (!contact) {
      throw new Error('contact not found')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
256
    const n = await Config.puppetInstance()
257
                  .roomDel(this, contact)
258
                  .then(_ => this.delLocal(contact))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
259
    return n
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
260 261
  }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
265
    const memberList = this.obj && this.obj.memberList
266
    if (!memberList || memberList.length === 0) {
267
      return 0 // already in refreshing
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
268 269 270
    }

    let i
271 272
    for (i = 0; i < memberList.length; i++) {
      if (memberList[i].id === contact.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
273 274 275
        break
      }
    }
276 277
    if (i < memberList.length) {
      memberList.splice(i, 1)
278
      return 1
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
279
    }
280
    return 0
281
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
282

283
  public quit() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
284 285
    throw new Error('wx web not implement yet')
    // WechatyBro.glue.chatroomFactory.quit("@@1c066dfcab4ef467cd0a8da8bec90880035aa46526c44f504a83172a9086a5f7"
286
  }
287

288 289 290 291 292 293 294 295 296 297
  /**
   * get topic
   */
  public topic(): string
  /**
   * set topic
   */
  public topic(newTopic: string): void

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
302 303
    if (newTopic) {
      log.verbose('Room', 'topic(%s)', newTopic)
304
      Config.puppetInstance().roomTopic(this, newTopic)
305 306 307 308 309 310 311 312 313
                              .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 })
314
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
315
    }
316
    return UtilLib.plainText(this.obj ? this.obj.topic : '')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
317 318
  }

319
  public nick(contact: Contact): string {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
320
    if (!this.obj || !this.obj.nickMap) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
321 322
      return ''
    }
323 324 325
    return this.obj.nickMap[contact.id]
  }

326
  public has(contact: Contact): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327
    if (!this.obj || !this.obj.memberList) {
328 329 330 331 332 333 334
      return false
    }
    return this.obj.memberList
                    .filter(c => c.id === contact.id)
                    .length > 0
  }

335
  public owner(): Contact | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
336 337
    const ownerUin = this.obj && this.obj.ownerUin
    let memberList = (this.obj && this.obj.memberList) || []
338 339 340 341 342 343 344 345

    let user = Config.puppetInstance()
                      .user

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

346
    memberList = memberList.filter(m => m.get('uin') === ownerUin)
347 348 349
    if (memberList.length > 0) {
      return memberList[0]
    }
J
jaslin 已提交
350 351

    if (this.rawObj.ChatRoomOwner) {
352 353 354 355
      return Contact.load(this.rawObj.ChatRoomOwner)
    }

    return null
356 357
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
358 359 360
  /**
   * NickName / DisplayName / RemarkName of member
   */
361
  public member(name: string): Contact | null {
362 363
    log.verbose('Room', 'member(%s)', name)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
364
    if (!this.obj || !this.obj.memberList) {
365
      log.warn('Room', 'member() not ready')
366 367
      return null
    }
368

369 370 371 372
    /**
     * ISSUE #64 emoji need to be striped
     */
    name = UtilLib.stripEmoji(name)
373

374 375 376
    const nickMap = this.obj.nickMap
    const idList = Object.keys(nickMap)
                          .filter(k => nickMap[k] === name)
377 378 379

    log.silly('Room', 'member() check nickMap: %s', JSON.stringify(nickMap))

380 381 382 383 384
    if (idList.length) {
      return Contact.load(idList[0])
    } else {
      return null
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
385 386
  }

387
  public memberList(): Contact[] {
388
    log.verbose('Room', 'memberList')
389 390 391

    if (!this.obj || !this.obj.memberList || this.obj.memberList.length < 1) {
      log.warn('Room', 'memberList() not ready')
392
      return []
393 394 395 396
    }
    return this.obj.memberList
  }

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

Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
400
    if (!contactList || !Array.isArray(contactList)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
401 402
      throw new Error('contactList not found')
    }
403

404
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
405
                  .roomCreate(contactList, topic)
406 407 408
                  .catch(e => {
                    log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
                    throw e
409
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
410 411
  }

412 413 414 415
  public static async findAll(query?: RoomQueryFilter): Promise<Room[]> {
    if (!query) {
      query = { topic: /.*/ }
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
416
    log.verbose('Room', 'findAll({ topic: %s })', query.topic)
417

418
    const topicFilter = query.topic
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
419

420 421
    if (!topicFilter) {
      throw new Error('topicFilter not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
422 423
    }

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

426
    if (topicFilter instanceof RegExp) {
427
      filterFunction = `(function (c) { return ${topicFilter.toString()}.test(c) })`
428
    } else if (typeof topicFilter === 'string') {
429
      filterFunction = `(function (c) { return c === '${topicFilter}' })`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
430
    } else {
431
      throw new Error('unsupport topic type')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
432 433
    }

434 435 436
    return Config.puppetInstance()
                  .roomFind(filterFunction)
                  .catch(e => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
437
                    log.verbose('Room', 'findAll() rejected: %s', e.message)
438 439
                    return [] // fail safe
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
440 441
  }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
445 446 447 448
    const roomList = await Room.findAll(query)
    if (!roomList || roomList.length < 1) {
      throw new Error('no room found')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
449 450 451
    const room = roomList[0]
    await room.ready()
    return room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452 453
  }

454 455 456 457
  public static load(id: string): Room {
    if (!id) {
      throw new Error('Room.load() no id')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
458 459 460 461 462 463 464

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

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