room.ts 29.5 KB
Newer Older
1
/**
L
lijiarui 已提交
2
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
3
 *   Wechaty - https://github.com/chatie/wechaty
4
 *
5
 *   @copyright 2016-2017 Huan LI <zixia@zixia.net>
6 7 8 9 10 11 12 13 14 15 16 17 18
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
L
lijiarui 已提交
19
 *   @ignore
20
 */
21
import { EventEmitter } from 'events'
22

23
import {
24
  config,
25
  Raven,
L
lijiarui 已提交
26 27
  Sayable,
  log,
28 29
}                 from './config'
import Contact    from './contact'
M
Mukaiu 已提交
30 31 32
import {
  Message,
  MediaMessage,
33 34
}                 from './message'
import Misc       from './misc'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
35

36
interface RoomObj {
37 38 39 40 41 42 43 44
  id:               string,
  encryId:          string,
  topic:            string,
  ownerUin:         number,
  memberList:       Contact[],
  nameMap:          Map<string, string>,
  roomAliasMap:     Map<string, string>,
  contactAliasMap:  Map<string, string>,
45 46
}

47
type NameType = 'name' | 'alias' | 'roomAlias' | 'contactAlias'
48

49
export interface RoomRawMember {
L
lijiarui 已提交
50 51 52
  UserName:     string,
  NickName:     string,
  DisplayName:  string,
53 54
}

55
export interface RoomRawObj {
L
lijiarui 已提交
56 57 58 59 60
  UserName:         string,
  EncryChatRoomId:  string,
  NickName:         string,
  OwnerUin:         number,
  ChatRoomOwner:    string,
61
  MemberList?:      RoomRawMember[],
62 63
}

64 65 66
export type RoomEventName = 'join'
                          | 'leave'
                          | 'topic'
67 68
                          | 'EVENT_PARAM_ERROR'

69
export interface RoomQueryFilter {
L
lijiarui 已提交
70
  topic: string | RegExp,
71 72
}

73
export interface MemberQueryFilter {
74 75 76 77
  name?:         string,
  alias?:        string,
  roomAlias?:    string,
  contactAlias?: string,
78 79
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
80
/**
L
lijiarui 已提交
81
 * All wechat rooms(groups) will be encapsulated as a Room.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
82
 *
L
lijiarui 已提交
83 84
 * `Room` is `Sayable`,
 * [Example/Room-Bot]{@link https://github.com/Chatie/wechaty/blob/master/example/room-bot.ts}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
85
 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
86
export class Room extends EventEmitter implements Sayable {
87 88
  private static pool = new Map<string, Room>()

89 90
  private dirtyObj: RoomObj | null // when refresh, use this to save dirty data for query
  private obj:      RoomObj | null
91 92
  private rawObj:   RoomRawObj

H
hcz 已提交
93 94 95
  /**
   * @private
   */
96
  constructor(public id: string) {
97
    super()
98
    log.silly('Room', `constructor(${id})`)
99
  }
100

H
hcz 已提交
101 102 103
  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
104
  public toString()    { return `Room<${this.topic()}>` }
L
lijiarui 已提交
105 106 107 108

  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
109
  public toStringEx()  { return `Room(${this.obj && this.obj.topic}[${this.id}])` }
110

L
lijiarui 已提交
111 112 113
  /**
   * @private
   */
114
  public isReady(): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
115
    return !!(this.obj && this.obj.memberList && this.obj.memberList.length)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
116 117
  }

L
lijiarui 已提交
118 119 120
  /**
   * @private
   */
121
  private async readyAllMembers(memberList: RoomRawMember[]): Promise<void> {
122 123
    for (const member of memberList) {
      const contact = Contact.load(member.UserName)
124
      await contact.ready()
125 126 127 128
    }
    return
  }

L
lijiarui 已提交
129 130 131
  /**
   * @private
   */
132
  public async ready(contactGetter?: (id: string) => Promise<any>): Promise<Room> {
133
    log.silly('Room', 'ready(%s)', contactGetter ? contactGetter.constructor.name : '')
134
    if (!this.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
135 136
      const e = new Error('ready() on a un-inited Room')
      log.warn('Room', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
137
      throw e
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138
    } else if (this.isReady()) {
139
      return this
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
140
    } else if (this.obj && this.obj.id) {
141
      log.warn('Room', 'ready() has obj.id but memberList empty in room %s. reloading', this.obj.topic)
142
    }
143

144
    if (!contactGetter) {
145 146
      contactGetter = config.puppetInstance()
                            .getContact.bind(config.puppetInstance())
147 148 149 150 151
    }
    if (!contactGetter) {
      throw new Error('no contactGetter')
    }

152 153
    try {
      const data = await contactGetter(this.id)
154
      log.silly('Room', `contactGetter(${this.id}) resolved`)
155
      this.rawObj = data
156
      await this.readyAllMembers(this.rawObj.MemberList || [])
157
      this.obj    = this.parse(this.rawObj)
158 159 160
      if (!this.obj) {
        throw new Error('no this.obj set after contactGetter')
      }
161
      await Promise.all(this.obj.memberList.map(c => c.ready(contactGetter)))
162

163
      return this
164

165
    } catch (e) {
166
      log.error('Room', 'contactGetter(%s) exception: %s', this.id, e.message)
167
      Raven.captureException(e)
168
      throw e
169
    }
170 171
  }

L
lijiarui 已提交
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
  public say(mediaMessage: MediaMessage)

  public say(content: string)

  public say(content: string, replyTo: Contact)

  public say(content: string, replyTo: Contact[])

  /**
   * Send message inside Room, if set [replyTo], wechaty will mention the contact as well.
   *
   * @param {(string | MediaMessage)} textOrMedia - Send `text` or `media file` inside Room.
   * @param {(Contact | Contact[])} [replyTo] - Optional parameter, send content inside Room, and mention @replyTo contact or contactList.
   * @returns {Promise<boolean>}
   * If bot send message successfully, it will return true. If the bot failed to send for blocking or any other reason, it will return false
   *
   * @example <caption>Send text inside Room</caption>
   * const room = await Room.find({name: 'wechaty'})        // change 'wechaty' to any of your room in wechat
   * await room.say('Hello world!')
   *
   * @example <caption>Send media file inside Room</caption>
   * const room = await Room.find({name: 'wechaty'})        // change 'wechaty' to any of your room in wechat
   * await room.say(new MediaMessage('/test.jpg'))          // put the filePath you want to send here
   *
   * @example <caption>Send text inside Room, and mention @replyTo contact</caption>
   * const contact = await Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member
   * const room = await Room.find({name: 'wechaty'})        // change 'wechaty' to any of your room in wechat
   * await room.say('Hello world!', contact)
   */
  public say(textOrMedia: string | MediaMessage, replyTo?: Contact|Contact[]): Promise<boolean> {
    const content = textOrMedia instanceof MediaMessage ? textOrMedia.filename() : textOrMedia
    log.verbose('Room', 'say(%s, %s)',
                        content,
                        Array.isArray(replyTo)
                        ? replyTo.map(c => c.name()).join(', ')
                        : replyTo ? replyTo.name() : '',
    )

    let m
    if (typeof textOrMedia === 'string') {
      m = new Message()

      const replyToList: Contact[] = [].concat(replyTo as any || [])

      if (replyToList.length > 0) {
        const AT_SEPRATOR = String.fromCharCode(8197)
        const mentionList = replyToList.map(c => '@' + c.name()).join(AT_SEPRATOR)
        m.content(mentionList + ' ' + content)
      } else {
        m.content(content)
      }
      // m.to(replyToList[0])
    } else
      m = textOrMedia

    m.room(this)

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

233
  public on(event: 'leave', listener: (this: Room, leaver: Contact) => void): this
L
lijiarui 已提交
234

235
  public on(event: 'join' , listener: (this: Room, inviteeList: Contact[] , inviter: Contact)  => void): this
L
lijiarui 已提交
236

237
  public on(event: 'topic', listener: (this: Room, topic: string, oldTopic: string, changer: Contact) => void): this
L
lijiarui 已提交
238 239 240 241 242 243 244 245 246 247 248 249

  public on(event: 'EVENT_PARAM_ERROR', listener: () => void): this

   /**
    * @desc       Room Class Event Type
    * @typedef    RoomEventName
    * @property   {string}  join  - Emit when anyone join any room.
    * @property   {string}  topic - Get topic event, emitted when someone change room topic.
    * @property   {string}  leave - Emit when anyone leave the room.<br>
    *                               If someone leaves the room by themselves, wechat will not notice other people in the room, so the bot will never get the "leave" event.
    */

H
hcz 已提交
250
  /**
L
lijiarui 已提交
251 252 253 254 255
   * @desc       Room Class Event Function
   * @typedef    RoomEventFunction
   * @property   {Function} room-join       - (this: Room, inviteeList: Contact[] , inviter: Contact)  => void
   * @property   {Function} room-topic      - (this: Room, topic: string, oldTopic: string, changer: Contact) => void
   * @property   {Function} room-leave      - (this: Room, leaver: Contact) => void
H
hcz 已提交
256
   */
L
lijiarui 已提交
257

H
hcz 已提交
258
  /**
L
lijiarui 已提交
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289
   * @listens Room
   * @param   {RoomEventName}      event      - Emit WechatyEvent
   * @param   {RoomEventFunction}  listener   - Depends on the WechatyEvent
   * @return  {this}                          - this for chain
   *
   * @example <caption>Event:join </caption>
   * const room = await Room.find({topic: 'event-room'}) // change `event-room` to any room topic in your wechat
   * if (room) {
   *   room.on('join', (room: Room, inviteeList: Contact[], inviter: Contact) => {
   *     const nameList = inviteeList.map(c => c.name()).join(',')
   *     console.log(`Room ${room.topic()} got new member ${nameList}, invited by ${inviter}`)
   *   })
   * }
   *
   * @example <caption>Event:leave </caption>
   * const room = await Room.find({topic: 'event-room'}) // change `event-room` to any room topic in your wechat
   * if (room) {
   *   room.on('leave', (room: Room, leaverList: Contact[]) => {
   *     const nameList = leaverList.map(c => c.name()).join(',')
   *     console.log(`Room ${room.topic()} lost member ${nameList}`)
   *   })
   * }
   *
   * @example <caption>Event:topic </caption>
   * const room = await Room.find({topic: 'event-room'}) // change `event-room` to any room topic in your wechat
   * if (room) {
   *   room.on('topic', (room: Room, topic: string, oldTopic: string, changer: Contact) => {
   *     console.log(`Room ${room.topic()} topic changed from ${oldTopic} to ${topic} by ${changer.name()}`)
   *   })
   * }
   *
H
hcz 已提交
290
   */
291
  public on(event: RoomEventName, listener: (...args: any[]) => any): this {
292
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
293

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
294
    super.on(event, listener) // Room is `Sayable`
295
    return this
296 297
  }

L
lijiarui 已提交
298 299 300
  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301
  public get(prop): string { return (this.obj && this.obj[prop]) || (this.dirtyObj && this.dirtyObj[prop]) }
302

H
hcz 已提交
303 304 305
  /**
   * @private
   */
306
  private parse(rawObj: RoomRawObj): RoomObj | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
307
    if (!rawObj) {
308
      log.warn('Room', 'parse() on a empty rawObj?')
309
      return null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
310
    }
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
311

312 313 314
    const memberList = (rawObj.MemberList || [])
                        .map(m => Contact.load(m.UserName))

L
lijiarui 已提交
315
    const nameMap    = this.parseMap('name', rawObj.MemberList)
316 317
    const roomAliasMap   = this.parseMap('roomAlias', rawObj.MemberList)
    const contactAliasMap   = this.parseMap('contactAlias', rawObj.MemberList)
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
318

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
319
    return {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
320 321 322 323 324
      id:         rawObj.UserName,
      encryId:    rawObj.EncryChatRoomId, // ???
      topic:      rawObj.NickName,
      ownerUin:   rawObj.OwnerUin,
      memberList,
325
      nameMap,
326 327
      roomAliasMap,
      contactAliasMap,
328 329 330
    }
  }

L
lijiarui 已提交
331 332 333
  /**
   * @private
   */
L
lijiarui 已提交
334
  private parseMap(parseContent: NameType, memberList?: RoomRawMember[]): Map<string, string> {
335
    const mapList: Map<string, string> = new Map<string, string>()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
336
    if (memberList && memberList.map) {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
337
      memberList.forEach(member => {
338
        let tmpName: string
339
        const contact = Contact.load(member.UserName)
340
        switch (parseContent) {
ruiruibupt's avatar
2  
ruiruibupt 已提交
341
          case 'name':
342
            tmpName = contact.name()
343
            break
344
          case 'roomAlias':
L
lijiarui 已提交
345
            tmpName = member.DisplayName
346
            break
347 348 349
          case 'contactAlias':
            tmpName = contact.alias() || ''
            break
350 351 352
          default:
            throw new Error('parseMap failed, member not found')
        }
353 354
        /**
         * ISSUE #64 emoji need to be striped
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
355
         * ISSUE #104 never use remark name because sys group message will never use that
ruiruibupt's avatar
#217  
ruiruibupt 已提交
356
         * @rui: Wrong for 'never use remark name because sys group message will never use that', see more in the latest comment in #104
ruiruibupt's avatar
1  
ruiruibupt 已提交
357 358
         * @rui: webwx's NickName here return contactAlias, if not set contactAlias, return name
         * @rui: 2017-7-2 webwx's NickName just ruturn name, no contactAlias
359
         */
360
        mapList[member.UserName] = Misc.stripEmoji(tmpName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
361
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
362
    }
363
    return mapList
364 365
  }

L
lijiarui 已提交
366 367 368
  /**
   * @private
   */
369
  public dumpRaw() {
370
    console.error('======= dump raw Room =======')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
371
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
372
  }
L
lijiarui 已提交
373 374 375 376

  /**
   * @private
   */
377
  public dump() {
378
    console.error('======= dump Room =======')
379 380 381
    if (!this.obj) {
      throw new Error('no this.obj')
    }
382
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj && this.obj[k]}`))
383 384
  }

H
hcz 已提交
385
  /**
L
lijiarui 已提交
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
   * Add contact in a room
   *
   * @param {Contact} contact
   * @returns {Promise<number>}
   * @example
   * const contact = await Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any contact in your wechat
   * const room = await Room.find({topic: 'wechat'})        // change 'wechat' to any room topic in your wechat
   * if (room) {
   *   const result = await room.add(contact)
   *   if (result) {
   *     console.log(`add ${contact.name()} to ${room.topic()} successfully! `)
   *   } else{
   *     console.log(`failed to add ${contact.name()} to ${room.topic()}! `)
   *   }
   * }
H
hcz 已提交
401
   */
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
402
  public async add(contact: Contact): Promise<number> {
403
    log.verbose('Room', 'add(%s)', contact)
404 405 406 407 408

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

409
    const n = config.puppetInstance()
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
410 411
                      .roomAdd(this, contact)
    return n
412 413
  }

H
hcz 已提交
414
  /**
L
lijiarui 已提交
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
   * Delete a contact from the room
   * It works only when the bot is the owner of the room
   * @param {Contact} contact
   * @returns {Promise<number>}
   * @example
   * const room = await Room.find({topic: 'wechat'})          // change 'wechat' to any room topic in your wechat
   * const contact = await Contact.find({name: 'lijiarui'})   // change 'lijiarui' to any room member in the room you just set
   * if (room) {
   *   const result = await room.del(contact)
   *   if (result) {
   *     console.log(`remove ${contact.name()} from ${room.topic()} successfully! `)
   *   } else{
   *     console.log(`failed to remove ${contact.name()} from ${room.topic()}! `)
   *   }
   * }
H
hcz 已提交
430
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
431
  public async del(contact: Contact): Promise<number> {
432
    log.verbose('Room', 'del(%s)', contact.name())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
433 434 435 436

    if (!contact) {
      throw new Error('contact not found')
    }
437
    const n = await config.puppetInstance()
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
438 439
                            .roomDel(this, contact)
                            .then(_ => this.delLocal(contact))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
440
    return n
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
441 442
  }

H
hcz 已提交
443
  /**
L
lijiarui 已提交
444
   * @private
H
hcz 已提交
445
   */
446
  private delLocal(contact: Contact): number {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
447 448
    log.verbose('Room', 'delLocal(%s)', contact)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
449
    const memberList = this.obj && this.obj.memberList
450
    if (!memberList || memberList.length === 0) {
451
      return 0 // already in refreshing
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452 453 454
    }

    let i
455 456
    for (i = 0; i < memberList.length; i++) {
      if (memberList[i].id === contact.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
457 458 459
        break
      }
    }
460 461
    if (i < memberList.length) {
      memberList.splice(i, 1)
462
      return 1
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
463
    }
464
    return 0
465
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
466

L
lijiarui 已提交
467 468 469
  /**
   * @private
   */
470
  public quit() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
471 472
    throw new Error('wx web not implement yet')
    // WechatyBro.glue.chatroomFactory.quit("@@1c066dfcab4ef467cd0a8da8bec90880035aa46526c44f504a83172a9086a5f7"
473
  }
474

475
  public topic(): string
L
lijiarui 已提交
476

477 478
  public topic(newTopic: string): void

L
lijiarui 已提交
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
  /**
   * SET/GET topic from the room
   *
   * @param {string} [newTopic] If set this para, it will change room topic.
   * @returns {(string | void)}
   *
   * @example <caption>When you say anything in a room, it will get room topic. </caption>
   * const bot = Wechaty.instance()
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
   *     const topic = room.topic()
   *     console.log(`room topic is : ${topic}`)
   *   }
   * })
   *
   * @example <caption>When you say anything in a room, it will change room topic. </caption>
   * const bot = Wechaty.instance()
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
   *     const oldTopic = room.topic()
   *     room.topic('change topic to wechaty!')
   *     console.log(`room topic change from ${oldTopic} to ${room.topic()}`)
   *   }
   * })
   */
508
  public topic(newTopic?: string): string | void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
509
    if (!this.isReady()) {
510
      log.warn('Room', 'topic() room not ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
511 512
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
513 514
    if (newTopic) {
      log.verbose('Room', 'topic(%s)', newTopic)
515
      config.puppetInstance()
516 517 518 519 520
            .roomTopic(this, newTopic)
            .catch(e => {
              log.warn('Room', 'topic(newTopic=%s) exception: %s',
                                newTopic, e && e.message || e,
                      )
521
              Raven.captureException(e)
522
            })
523 524 525 526
      if (!this.obj) {
        this.obj = <RoomObj>{}
      }
      Object.assign(this.obj, { topic: newTopic })
527
      return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
528
    }
529
    return Misc.plainText(this.obj ? this.obj.topic : '')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
530 531
  }

532 533
  /**
   * should be deprecated
L
lijiarui 已提交
534
   * @private
535
   */
ruiruibupt's avatar
2  
ruiruibupt 已提交
536
  public nick(contact: Contact): string | null {
ruiruibupt's avatar
1  
ruiruibupt 已提交
537
    log.warn('Room', 'nick(Contact) DEPRECATED, use alias(Contact) instead.')
ruiruibupt's avatar
#217  
ruiruibupt 已提交
538
    return this.alias(contact)
539 540
  }

L
lijiarui 已提交
541
  /**
L
lijiarui 已提交
542
   * Return contact's roomAlias in the room, the same as roomAlias
L
lijiarui 已提交
543
   * @param {Contact} contact
L
lijiarui 已提交
544 545 546 547 548 549 550 551 552 553 554 555
   * @returns {string | null} - If a contact has an alias in room, return string, otherwise return null
   * @example
   * const bot = Wechaty.instance()
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   const contact = m.from()
   *   if (room) {
   *     const alias = room.alias(contact)
   *     console.log(`${contact.name()} alias is ${alias}`)
   *   }
   * })
L
lijiarui 已提交
556
   */
ruiruibupt's avatar
2  
ruiruibupt 已提交
557
  public alias(contact: Contact): string | null {
558 559 560
    return this.roomAlias(contact)
  }

H
hcz 已提交
561
  /**
L
lijiarui 已提交
562 563 564
   * Same as function alias
   * @param {Contact} contact
   * @returns {(string | null)}
H
hcz 已提交
565
   */
566 567
  public roomAlias(contact: Contact): string | null {
    if (!this.obj || !this.obj.roomAliasMap) {
ruiruibupt's avatar
2  
ruiruibupt 已提交
568
      return null
569
    }
570
    return this.obj.roomAliasMap[contact.id] || null
571 572
  }

H
hcz 已提交
573
  /**
L
lijiarui 已提交
574 575 576 577 578 579 580 581 582 583 584 585 586 587
   * Check if the room has member `contact`.
   *
   * @param {Contact} contact
   * @returns {boolean} Return `true` if has contact, else return `false`.
   * @example <caption>Check whether 'lijiarui' is in the room 'wechaty'</caption>
   * const contact = await Contact.find({name: 'lijiarui'})   // change 'lijiarui' to any of contact in your wechat
   * const room = await Room.find({topic: 'wechaty'})         // change 'wechaty' to any of the room in your wechat
   * if (contact && room) {
   *   if (room.has(contact)) {
   *     console.log(`${contact.name()} is in the room ${room.topic()}!`)
   *   } else {
   *     console.log(`${contact.name()} is not in the room ${room.topic()} !`)
   *   }
   * }
H
hcz 已提交
588
   */
589
  public has(contact: Contact): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
590
    if (!this.obj || !this.obj.memberList) {
591 592 593 594 595 596 597
      return false
    }
    return this.obj.memberList
                    .filter(c => c.id === contact.id)
                    .length > 0
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
598
  public memberAll(filter: MemberQueryFilter): Contact[]
L
lijiarui 已提交
599

600
  public memberAll(name: string): Contact[]
601

L
lijiarui 已提交
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623
  /**
   * The way to search member by Room.member()
   *
   * @typedef    MemberQueryFilter
   * @property   {string} name            -Find the contact by wechat name in a room, equal to `Contact.name()`.
   * @property   {string} alias           -Find the contact by alias set by the bot for others in a room, equal to `roomAlias`.
   * @property   {string} roomAlias       -Find the contact by alias set by the bot for others in a room.
   * @property   {string} contactAlias    -Find the contact by alias set by the contact out of a room, equal to `Contact.alias()`.
   * [More Detail]{@link https://github.com/Chatie/wechaty/issues/365}
   */

  /**
   * Find all contacts in a room
   *
   * #### definition
   * - `name`                 the name-string set by user-self, should be called name, equal to `Contact.name()`
   * - `roomAlias` | `alias`  the name-string set by user-self in the room, should be called roomAlias
   * - `contactAlias`         the name-string set by bot for others, should be called alias, equal to `Contact.alias()`
   * @param {(MemberQueryFilter | string)} queryArg -When use memberAll(name:string), return all matched members, including name, roomAlias, contactAlias
   * @returns {Contact[]}
   * @memberof Room
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
624
  public memberAll(queryArg: MemberQueryFilter | string): Contact[] {
625
    if (typeof queryArg === 'string') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644
      //
      // use the following `return` statement to do this job.
      //

      // const nameList = this.memberAll({name: queryArg})
      // const roomAliasList = this.memberAll({roomAlias: queryArg})
      // const contactAliasList = this.memberAll({contactAlias: queryArg})

      // if (nameList) {
      //   contactList = contactList.concat(nameList)
      // }
      // if (roomAliasList) {
      //   contactList = contactList.concat(roomAliasList)
      // }
      // if (contactAliasList) {
      //   contactList = contactList.concat(contactAliasList)
      // }

      return ([] as Contact[]).concat(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
645 646
        this.memberAll({name:         queryArg}),
        this.memberAll({roomAlias:    queryArg}),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
647 648
        this.memberAll({contactAlias: queryArg}),
      )
649 650
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
651 652 653
    /**
     * We got filter parameter
     */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
654
    log.silly('Room', 'memberAll({ %s })',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
655 656 657
                      Object.keys(queryArg)
                            .map(k => `${k}: ${queryArg[k]}`)
                            .join(', '),
658 659 660
            )

    if (Object.keys(queryArg).length !== 1) {
ruiruibupt's avatar
1  
ruiruibupt 已提交
661
      throw new Error('Room member find queryArg only support one key. multi key support is not availble now.')
662
    }
663

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
664
    if (!this.obj || !this.obj.memberList) {
665
      log.warn('Room', 'member() not ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
666
      return []
667
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
668
    const filterKey            = Object.keys(queryArg)[0]
669 670 671
    /**
     * ISSUE #64 emoji need to be striped
     */
672
    const filterValue: string  = Misc.stripEmoji(Misc.plainText(queryArg[filterKey]))
673 674

    const keyMap = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
675
      contactAlias: 'contactAliasMap',
676 677
      name:         'nameMap',
      alias:        'roomAliasMap',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
678
      roomAlias:    'roomAliasMap',
679 680
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
681 682 683
    const filterMapName = keyMap[filterKey]
    if (!filterMapName) {
      throw new Error('unsupport filter key: ' + filterKey)
684 685 686 687 688
    }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
690
    const filterMap = this.obj[filterMapName]
691
    const idList = Object.keys(filterMap)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
692
                          .filter(id => filterMap[id] === filterValue)
693

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
694
    log.silly('Room', 'memberAll() check %s from %s: %s', filterValue, filterKey, JSON.stringify(filterMap))
695

696
    if (idList.length) {
697
      return idList.map(id => Contact.load(id))
698
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
699
      return []
700
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
701 702
  }

703
  public member(name: string): Contact | null
L
lijiarui 已提交
704

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
705
  public member(filter: MemberQueryFilter): Contact | null
706

L
lijiarui 已提交
707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
  /**
   * Find all contacts in a room, if get many, return the first one.
   *
   * @param {(MemberQueryFilter | string)} queryArg -When use member(name:string), return all matched members, including name, roomAlias, contactAlias
   * @returns {(Contact | null)}
   *
   * @example <caption>Find member by name</caption>
   * const room = await Room.find({topic: 'wechaty'})           // change 'wechaty' to any room name in your wechat
   * if (room) {
   *   const member = room.member('lijiarui')                   // change 'lijiarui' to any room member in your wechat
   *   if (member) {
   *     console.log(`${room.topic()} got the member: ${member.name()}`)
   *   } else {
   *     console.log(`cannot get member in room: ${room.topic()}`)
   *   }
   * }
   *
   * @example <caption>Find member by MemberQueryFilter</caption>
   * const room = await Room.find({topic: 'wechaty'})          // change 'wechaty' to any room name in your wechat
   * if (room) {
   *   const member = room.member({name: 'lijiarui'})          // change 'lijiarui' to any room member in your wechat
   *   if (member) {
   *     console.log(`${room.topic()} got the member: ${member.name()}`)
   *   } else {
   *     console.log(`cannot get member in room: ${room.topic()}`)
   *   }
   * }
   */
735 736 737
  public member(queryArg: MemberQueryFilter | string): Contact | null {
    log.verbose('Room', 'member(%s)', JSON.stringify(queryArg))

738 739 740 741 742 743 744 745 746
    let memberList: Contact[]
    // ISSUE #622
    // error TS2345: Argument of type 'string | MemberQueryFilter' is not assignable to parameter of type 'MemberQueryFilter' #622
    if (typeof queryArg === 'string') {
      memberList =  this.memberAll(queryArg)
    } else {
      memberList =  this.memberAll(queryArg)
    }

747 748 749 750 751
    if (!memberList || !memberList.length) {
      return null
    }

    if (memberList.length > 1) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
752
      log.warn('Room', 'member(%s) get %d contacts, use the first one by default', JSON.stringify(queryArg), memberList.length)
753 754 755 756
    }
    return memberList[0]
  }

H
hcz 已提交
757
  /**
L
lijiarui 已提交
758 759 760
   * Get all room member from the room
   *
   * @returns {Contact[]}
H
hcz 已提交
761
   */
762
  public memberList(): Contact[] {
763
    log.verbose('Room', 'memberList')
764 765 766

    if (!this.obj || !this.obj.memberList || this.obj.memberList.length < 1) {
      log.warn('Room', 'memberList() not ready')
767 768 769 770
      log.verbose('Room', 'memberList() trying call refresh() to update')
      this.refresh().then(() => {
        log.verbose('Room', 'memberList() refresh() done')
      })
771
      return []
772 773 774 775
    }
    return this.obj.memberList
  }

H
hcz 已提交
776
  /**
L
lijiarui 已提交
777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
   * Create a new room.
   *
   * @static
   * @param {Contact[]} contactList
   * @param {string} [topic]
   * @returns {Promise<Room>}
   * @example <caption>Creat a room with 'lijiarui' and 'juxiaomi', the room topic is 'ding - created'</caption>
   * const helperContactA = await Contact.find({ name: 'lijiarui' })  // change 'lijiarui' to any contact in your wechat
   * const helperContactB = await Contact.find({ name: 'juxiaomi' })  // change 'juxiaomi' to any contact in your wechat
   * const contactList = [helperContactA, helperContactB]
   * console.log('Bot', 'contactList: %s', contactList.join(','))
   * const room = await Room.create(contactList, 'ding')
   * console.log('Bot', 'createDingRoom() new ding room created: %s', room)
   * await room.topic('ding - created')
   * await room.say('ding - created')
H
hcz 已提交
792
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
793
  public static create(contactList: Contact[], topic?: string): Promise<Room> {
794
    log.verbose('Room', 'create(%s, %s)', contactList.join(','), topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
795

Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
796
    if (!contactList || !Array.isArray(contactList)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
797 798
      throw new Error('contactList not found')
    }
799

800
    return config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
801
                  .roomCreate(contactList, topic)
802 803
                  .catch(e => {
                    log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
804
                    Raven.captureException(e)
805
                    throw e
806
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
807 808
  }

H
hcz 已提交
809
  /**
L
lijiarui 已提交
810 811 812 813 814 815 816 817
   * Find room by topic, return all the matched room
   *
   * @static
   * @param {RoomQueryFilter} [query]
   * @returns {Promise<Room[]>}
   * @example
   * const roomList = await Room.findAll()                    // get the room list of the bot
   * const roomList = await Room.findAll({name: 'wechaty'})   // find all of the rooms with name 'wechaty'
H
hcz 已提交
818
   */
819 820 821 822
  public static async findAll(query?: RoomQueryFilter): Promise<Room[]> {
    if (!query) {
      query = { topic: /.*/ }
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
823
    log.verbose('Room', 'findAll({ topic: %s })', query.topic)
824

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

827 828
    if (!topicFilter) {
      throw new Error('topicFilter not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
829 830
    }

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

833
    if (topicFilter instanceof RegExp) {
834
      filterFunction = `(function (c) { return ${topicFilter.toString()}.test(c) })`
835
    } else if (typeof topicFilter === 'string') {
836
      topicFilter = topicFilter.replace(/'/g, '\\\'')
837
      filterFunction = `(function (c) { return c === '${topicFilter}' })`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
838
    } else {
839
      throw new Error('unsupport topic type')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
840 841
    }

842
    const roomList = await config.puppetInstance()
843 844 845
                                  .roomFind(filterFunction)
                                  .catch(e => {
                                    log.verbose('Room', 'findAll() rejected: %s', e.message)
846
                                    Raven.captureException(e)
847 848 849
                                    return [] // fail safe
                                  })

850 851 852 853
    await Promise.all(roomList.map(room => room.ready()))
    // for (let i = 0; i < roomList.length; i++) {
    //   await roomList[i].ready()
    // }
854 855

    return roomList
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
856 857
  }

858
  /**
L
lijiarui 已提交
859 860
   * Try to find a room by filter: {topic: string | RegExp}. If get many, return the first one.
   *
861 862 863 864
   * @param {RoomQueryFilter} query
   * @returns {Promise<Room | null>} If can find the room, return Room, or return null
   */
  public static async find(query: RoomQueryFilter): Promise<Room | null> {
865
    log.verbose('Room', 'find({ topic: %s })', query.topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
866

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
867 868
    const roomList = await Room.findAll(query)
    if (!roomList || roomList.length < 1) {
869
      return null
870 871
    } else if (roomList.length > 1) {
      log.warn('Room', 'find() got more than one result, return the 1st one.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
872
    }
873
    return roomList[0]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
874 875
  }

L
lijiarui 已提交
876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913
  /**
   * Force reload data for Room
   *
   * @returns {Promise<void>}
   */
  public async refresh(): Promise<void> {
    if (this.isReady()) {
      this.dirtyObj = this.obj
    }
    this.obj = null
    await this.ready()
    return
  }

  /**
   * @private
   * Get room's owner from the room.
   * Not recommend, because cannot always get the owner
   * @returns {(Contact | null)}
   */
  public owner(): Contact | null {
    const ownerUin = this.obj && this.obj.ownerUin

    const user = config.puppetInstance()
                      .user

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

    if (this.rawObj.ChatRoomOwner) {
      return Contact.load(this.rawObj.ChatRoomOwner)
    }

    log.info('Room', 'owner() is limited by Tencent API, sometimes work sometimes not')
    return null
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
914
  /**
L
lijiarui 已提交
915
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
916
   */
917 918 919 920
  public static load(id: string): Room {
    if (!id) {
      throw new Error('Room.load() no id')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
921 922 923 924 925 926 927

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
928
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
929 930

export default Room