room.ts 11.2 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 16
import {
    Config
  , Sayable
}                 from './config'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
17 18 19
import Contact    from './contact'
import Message    from './message'
import UtilLib    from './util-lib'
20

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
21
import log        from './brolog-env'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
22

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

32
export type RoomRawMemberList = {
33 34 35 36
  UserName:     string
  DisplayName:  string
}

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

45 46 47
export type RoomEventName = 'join' | 'leave' | 'topic'
                          | 'EVENT_PARAM_ERROR'

48
export type RoomQueryFilter = {
49 50 51
  topic: string | RegExp
}

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

55 56
  private dirtyObj: RoomObj | null // when refresh, use this to save dirty data for query
  private obj:      RoomObj | null
57 58
  private rawObj:   RoomRawObj

59
  constructor(public id: string) {
60
    super()
61
    log.silly('Room', `constructor(${id})`)
62
  }
63

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

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

71
  public async refresh(): Promise<this> {
72 73 74
    if (this.isReady()) {
      this.dirtyObj = this.obj
    }
75
    this.obj = null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
76 77 78
    return this.ready()
  }

79
  public async ready(contactGetter?: (id: string) => Promise<RoomRawObj>): Promise<this> {
80
    log.silly('Room', 'ready(%s)', contactGetter ? contactGetter.constructor.name : '')
81
    if (!this.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
82 83 84 85
      const e = new Error('ready() on a un-inited Room')
      log.warn('Room', e.message)
      return Promise.reject(e)
    } else if (this.isReady()) {
86
      return Promise.resolve(this)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
87
    } else if (this.obj && this.obj.id) {
88
      log.warn('Room', 'ready() has obj.id but memberList empty in room %s. reloading', this.obj.topic)
89
    }
90

91 92 93 94 95 96 97 98
    if (!contactGetter) {
      contactGetter = Config.puppetInstance()
                            .getContact.bind(Config.puppetInstance())
    }
    if (!contactGetter) {
      throw new Error('no contactGetter')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
99
    return contactGetter(this.id)
100
    .then(data => {
101
      log.silly('Room', `contactGetter(${this.id}) resolved`)
102 103
      this.rawObj = data
      this.obj    = this.parse(data)
104
      return this
105 106
    })
    .then(_ => {
107 108 109
      if (!this.obj) {
        throw new Error('no this.obj set after contactGetter')
      }
110
      return Promise.all(this.obj.memberList.map(c => c.ready(contactGetter)))
111
                    .then(() => this)
112 113
    })
    .catch(e => {
114 115
      log.error('Room', 'contactGetter(%s) exception: %s', this.id, e.message)
      throw e
116 117 118
    })
  }

119
  public on(event: 'leave', listener: (this: Sayable, leaver: Contact) => void): this
120
  public on(event: 'join' , listener: (inviteeList: Contact[] , inviter: Contact)  => void): this
121
  public on(event: 'topic', listener: (topic: string, oldTopic: string, changer: Contact) => void): this
122
  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this
123

124
  public on(event: RoomEventName, listener: Function): this {
125
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
126

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
127 128 129 130 131 132 133 134 135 136 137
    // 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`
138
    return this
139 140
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
141
  public say(content: string, replyTo?: Contact|Contact[]): Promise<any> {
142 143 144 145
    log.verbose('Room', 'say(%s, %s)'
                      , content
                      , Array.isArray(replyTo)
                        ? replyTo.map(c => c.name()).join(', ')
146
                        : replyTo ? replyTo.name() : ''
147
    )
148 149 150 151 152 153 154 155 156 157 158 159

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

    if (!replyTo) {
      m.content(content)
      m.to(this)
      return Config.puppetInstance()
                    .send(m)
    }

    let mentionList
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
    if (Array.isArray(replyTo)) {
161 162 163 164 165 166 167 168 169 170 171 172
      m.to(replyTo[0])
      mentionList = replyTo.map(c => '@' + c.name()).join(' ')
    } else {
      m.to(replyTo)
      mentionList = '@' + replyTo.name()
    }

    m.content(mentionList + ' ' + content)
    return Config.puppetInstance()
                  .send(m)
  }

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

175
  private parse(rawObj: RoomRawObj): RoomObj | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
176
    if (!rawObj) {
177
      return null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
178 179
    }
    return {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
180 181 182 183 184
      id:           rawObj.UserName
      , encryId:    rawObj.EncryChatRoomId // ???
      , topic:      rawObj.NickName
      , ownerUin:   rawObj.OwnerUin

185 186
      , memberList: this.parseMemberList(rawObj.MemberList)
      , nickMap:    this.parseNickMap(rawObj.MemberList)
187 188 189
    }
  }

190
  private parseMemberList(memberList) {
191 192 193
    if (!memberList || !memberList.map) {
      return []
    }
194
    return memberList.map(m => Contact.load(m.UserName))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
195 196
  }

197 198
  private parseNickMap(memberList): Map<string, string> {
    const nickMap: Map<string, string> = new Map<string, string>()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
199
    let contact, remark
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
200
    if (memberList && memberList.map) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
201
      memberList.forEach(m => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
202 203 204 205 206 207 208
        contact = Contact.load(m.UserName)
        if (contact) {
          remark = contact.remark()
        } else {
          remark = null
        }
        nickMap[m.UserName] = remark || m.DisplayName || m.NickName
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
209
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
210 211
    }
    return nickMap
212 213
  }

214
  public dumpRaw() {
215
    console.error('======= dump raw Room =======')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
217
  }
218
  public dump() {
219
    console.error('======= dump Room =======')
220
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj && this.obj[k]}`))
221 222
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
223
  public async add(contact: Contact): Promise<any> {
224
    log.verbose('Room', 'add(%s)', contact)
225 226 227 228 229

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
230 231 232
    await Config.puppetInstance()
                .roomAdd(this, contact)
    return
233 234
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
235
  public async del(contact: Contact): Promise<number> {
236
    log.verbose('Room', 'del(%s)', contact.name())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
237 238 239 240

    if (!contact) {
      throw new Error('contact not found')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
241
    const n = await Config.puppetInstance()
242
                  .roomDel(this, contact)
243
                  .then(_ => this.delLocal(contact))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
244
    return n
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
245 246
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
247
  // @private
248
  private delLocal(contact: Contact): number {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
249 250
    log.verbose('Room', 'delLocal(%s)', contact)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
251
    const memberList = this.obj && this.obj.memberList
252
    if (!memberList || memberList.length === 0) {
253
      return 0 // already in refreshing
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
254 255 256
    }

    let i
257 258
    for (i = 0; i < memberList.length; i++) {
      if (memberList[i].id === contact.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
259 260 261
        break
      }
    }
262 263
    if (i < memberList.length) {
      memberList.splice(i, 1)
264
      return 1
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
265
    }
266
    return 0
267
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
268

269
  public quit() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
270 271
    throw new Error('wx web not implement yet')
    // WechatyBro.glue.chatroomFactory.quit("@@1c066dfcab4ef467cd0a8da8bec90880035aa46526c44f504a83172a9086a5f7"
272
  }
273

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
274
  public topic(newTopic?: string): string {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
275 276 277 278
    if (!this.isReady()) {
      throw new Error('room not ready')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
279 280 281
    if (newTopic) {
      log.verbose('Room', 'topic(%s)', newTopic)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
282

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
283
    if (newTopic) {
284
      Config.puppetInstance().roomTopic(this, newTopic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285 286
      return newTopic
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287
    // return this.get('topic')
288
    return UtilLib.plainText(this.obj ? this.obj.topic : '')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
289 290
  }

291
  public nick(contact: Contact): string {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
292
    if (!this.obj || !this.obj.nickMap) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
293 294
      return ''
    }
295 296 297
    return this.obj.nickMap[contact.id]
  }

298
  public has(contact: Contact): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
299
    if (!this.obj || !this.obj.memberList) {
300 301 302 303 304 305 306
      return false
    }
    return this.obj.memberList
                    .filter(c => c.id === contact.id)
                    .length > 0
  }

307
  public owner(): Contact | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
308 309
    const ownerUin = this.obj && this.obj.ownerUin
    let memberList = (this.obj && this.obj.memberList) || []
310 311 312 313 314 315 316 317

    let user = Config.puppetInstance()
                      .user

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

318
    memberList = memberList.filter(m => m.get('uin') === ownerUin)
319 320 321 322 323 324 325
    if (memberList.length > 0) {
      return memberList[0]
    } else {
      return null
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
326 327 328
  /**
   * NickName / DisplayName / RemarkName of member
   */
329
  public member(name: string): Contact | null {
330 331
    log.verbose('Room', 'member(%s)', name)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
332
    if (!this.obj || !this.obj.memberList) {
333
      log.warn('Room', 'member() not ready')
334 335 336 337 338
      return null
    }
    const nickMap = this.obj.nickMap
    const idList = Object.keys(nickMap)
                          .filter(k => nickMap[k] === name)
339 340 341

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

342 343 344 345 346
    if (idList.length) {
      return Contact.load(idList[0])
    } else {
      return null
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
347 348
  }

349 350 351 352 353
  public memberList(): Contact[] {
    log.verbose('Room', 'member(%s)', name)

    if (!this.obj || !this.obj.memberList || this.obj.memberList.length < 1) {
      log.warn('Room', 'memberList() not ready')
354
      return []
355 356 357 358
    }
    return this.obj.memberList
  }

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

Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
362
    if (!contactList || !Array.isArray(contactList)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
363 364
      throw new Error('contactList not found')
    }
365

366
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
367
                  .roomCreate(contactList, topic)
368 369 370
                  .catch(e => {
                    log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
                    throw e
371
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
372 373
  }

374
  public static async findAll(query: RoomQueryFilter): Promise<Room[]> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
375
    log.verbose('Room', 'findAll({ topic: %s })', query.topic)
376 377

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

379 380
    if (!topic) {
      throw new Error('topic not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
381 382
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
383 384 385
    // see also Contact.findAll
    let filterFunction: string

386 387 388 389
    if (topic instanceof RegExp) {
      filterFunction = `c => ${topic.toString()}.test(c)`
    } else if (typeof topic === 'string') {
      filterFunction = `c => c === '${topic}'`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
390
    } else {
391
      throw new Error('unsupport topic type')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
392 393
    }

394 395 396 397 398 399
    return Config.puppetInstance()
                  .roomFind(filterFunction)
                  .catch(e => {
                    log.error('Room', '_find() rejected: %s', e.message)
                    return [] // fail safe
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
400 401
  }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
405 406 407 408
    const roomList = await Room.findAll(query)
    if (!roomList || roomList.length < 1) {
      throw new Error('no room found')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
409
    return roomList[0].ready()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
410 411
  }

412
  public static load(id: string): Room | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
413 414 415 416 417 418 419 420
    if (!id) { return null }

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

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

423
export default Room