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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
13 14 15 16 17
import Config     from './config'
import Contact    from './contact'
import Message    from './message'
import UtilLib    from './util-lib'
import EventScope from './event-scope'
18

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

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
type RoomObj = {
  id:         string
  encryId:    string
  topic:      string
  ownerUin:   number
  memberList: Contact[]
  nickMap:    Map<string, string>
}

type RoomRawMemberList = {
  UserName:     string
  DisplayName:  string
}

type RoomRawObj = {
  UserName:         string
  EncryChatRoomId:  string
  NickName:         string
  OwnerUin:         number
  MemberList:       RoomRawMemberList[]
}

43 44 45 46
type RoomQueryFilter = {
  topic: string | RegExp
}

47 48 49 50 51 52 53
class Room extends EventEmitter {
  private static pool = new Map<string, Room>()

  private dirtyObj: RoomObj // when refresh, use this to save dirty data for query
  private obj:      RoomObj
  private rawObj:   RoomRawObj

54
  constructor(public id: string) {
55
    super()
56
    log.silly('Room', `constructor(${id})`)
57 58 59
    // this.id = id
    // this.obj = {}
    // this.dirtyObj = {}
60 61 62
    // if (!Config.puppetInstance()) {
    //   throw new Error('Config.puppetInstance() not found')
    // }
63
  }
64

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

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

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

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

93 94
    contactGetter = contactGetter || Config.puppetInstance()
                                            .getContact.bind(Config.puppetInstance())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
95
    return contactGetter(this.id)
96
    .then(data => {
97
      log.silly('Room', `contactGetter(${this.id}) resolved`)
98 99
      this.rawObj = data
      this.obj    = this.parse(data)
100
      return this
101 102 103
    })
    .then(_ => {
      return Promise.all(this.obj.memberList.map(c => c.ready()))
104
                    .then(() => this)
105 106
    })
    .catch(e => {
107 108
      log.error('Room', 'contactGetter(%s) exception: %s', this.id, e.message)
      throw e
109 110 111
    })
  }

112 113 114 115
  public on(event: 'leave', listener: (leaver: Contact) => void): this
  public on(event: 'join' , listener: (invitee:      Contact   , inviter: Contact)  => void): this
  public on(event: 'join' , listener: (inviteeList:  Contact[] , inviter: Contact)  => void): this
  public on(event: 'topic', listener: (topic: string, oldTopic: string, changer: Contact) => void): this
116

117
  public on(event: string, listener: Function): this {
118
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
119

120 121 122 123 124 125 126
    /**
     * wrap a room event listener to a global event listener
     */
    const listenerWithRoomArg = (room: Room, ...argList) => {
      return listener.apply(this, argList)
    }

127 128 129 130
    /**
     * every room event must can be mapped to a global event.
     * such as: `join` to `room-join`
     */
131 132
    const globalEventName = 'room-' + event
    const listenerWithScope = EventScope.wrap.call(this, globalEventName, listenerWithRoomArg)
133

134 135 136 137 138 139
    /**
     * bind(listenerWithScope, this):
     * the `this` is for simulate the global room-* event,
     * provide the first argument `room`
     */
    super.on(event, listenerWithScope.bind(listenerWithScope, this))
140
    return this
141 142
  }

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

    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 (李卓桓) 已提交
162
    if (Array.isArray(replyTo)) {
163 164 165 166 167 168 169 170 171 172 173 174
      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 (李卓桓) 已提交
175
  public get(prop): string { return (this.obj && this.obj[prop]) || (this.dirtyObj && this.dirtyObj[prop]) }
176

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

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

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

199 200
  private parseNickMap(memberList): Map<string, string> {
    const nickMap: Map<string, string> = new Map<string, string>()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
201
    let contact, remark
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
202
    if (memberList && memberList.map) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
203
      memberList.forEach(m => {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
204 205 206 207 208 209 210
        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 (李卓桓) 已提交
211
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
212 213
    }
    return nickMap
214 215
  }

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

225
  public add(contact: Contact): Promise<any> {
226
    log.verbose('Room', 'add(%s)', contact)
227 228 229 230 231 232 233 234 235

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

    return Config.puppetInstance()
                  .roomAdd(this, contact)
  }

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

    if (!contact) {
      throw new Error('contact not found')
    }
242 243
    return Config.puppetInstance()
                  .roomDel(this, contact)
244
                  .then(_ => this.delLocal(contact))
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
    if (newTopic) {
      log.verbose('Room', 'topic(%s)', newTopic)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
278

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
279
    if (newTopic) {
280
      Config.puppetInstance().roomTopic(this, newTopic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
281 282
      return newTopic
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
283
    // return this.get('topic')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
284
    return UtilLib.plainText(this.obj && this.obj.topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
285 286
  }

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

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

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

    let user = Config.puppetInstance()
                      .user

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

314
    memberList = memberList.filter(m => m.get('uin') === ownerUin)
315 316 317 318 319 320 321
    if (memberList.length > 0) {
      return memberList[0]
    } else {
      return null
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
322 323 324 325
  /**
   * NickName / DisplayName / RemarkName of member
   */
  public member(name: string): Contact {
326 327
    log.verbose('Room', 'member(%s)', name)

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

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

338 339 340 341 342
    if (idList.length) {
      return Contact.load(idList[0])
    } else {
      return null
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
343 344
  }

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

348
    if (!contactList || !(typeof contactList === 'array')) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
349 350
      throw new Error('contactList not found')
    }
351

352
    return Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
353
                  .roomCreate(contactList, topic)
354 355 356
                  .catch(e => {
                    log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
                    throw e
357
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
358 359
  }

360 361 362 363
  public static findAll(query: RoomQueryFilter): Promise<Room[]> {
    log.silly('Room', 'findAll({ topic: %s })', query.topic)

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

365 366
    if (!topic) {
      throw new Error('topic not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
367 368
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
369 370 371
    // see also Contact.findAll
    let filterFunction: string

372 373 374 375
    if (topic instanceof RegExp) {
      filterFunction = `c => ${topic.toString()}.test(c)`
    } else if (typeof topic === 'string') {
      filterFunction = `c => c === '${topic}'`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
376
    } else {
377
      throw new Error('unsupport topic type')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
378 379
    }

380 381 382 383 384 385
    return Config.puppetInstance()
                  .roomFind(filterFunction)
                  .catch(e => {
                    log.error('Room', '_find() rejected: %s', e.message)
                    return [] // fail safe
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
386 387
  }

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

391 392 393 394 395 396 397 398 399 400 401 402
    return Room.findAll(query)
                .then(roomList => {
                  if (roomList && roomList.length > 0) {
                    return roomList[0]
                  }
                  return null
                })
                .catch(e => {
                  log.error('Room', 'find() rejected: %s', e.message)
                  return null // fail safe
                  // throw e
                })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
403 404
  }

405
  public static load(id: string): Room {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
406 407 408 409 410 411 412 413
    if (!id) { return null }

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

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

416
// Room.init()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
417

418 419
// module.exports = Room
export default Room