room.ts 32.8 KB
Newer Older
1
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
2
 *   Wechaty - https://github.com/chatie/wechaty
3
 *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
4
 *   @copyright 2016-2018 Huan LI <zixia@zixia.net>
5 6 7 8 9 10 11 12 13 14 15 16 17
 *
 *   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 已提交
18
 *   @ignore
19
 */
20 21 22
import {
  instanceToClass,
}                   from 'clone-class'
23 24 25
import {
  FileBox,
}                   from 'file-box'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
26

27 28
import {
  Accessory,
29
}                       from '../accessory'
30
import {
31
  // config,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
32
  FOUR_PER_EM_SPACE,
33 34
  log,
  Raven,
35
}                       from '../config'
36 37
import {
  Sayable,
38
}                       from '../types'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
39

40 41
import { Contact }        from './contact'
import { RoomInvitation } from './room-invitation'
42
import { UrlLink }        from './url-link'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
43

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
44
export const ROOM_EVENT_DICT = {
45
  invite: 'tbw',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
46 47 48 49 50
  join: 'tbw',
  leave: 'tbw',
  topic: 'tbw',
}
export type RoomEventName = keyof typeof ROOM_EVENT_DICT
51

52 53 54 55
import {
  RoomMemberQueryFilter,
  RoomPayload,
  RoomQueryFilter,
56
}                         from 'wechaty-puppet'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
58
/**
L
lijiarui 已提交
59
 * All wechat rooms(groups) will be encapsulated as a Room.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
60
 *
L
lijiarui 已提交
61
 * [Examples/Room-Bot]{@link https://github.com/Chatie/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/room-bot.ts}
62 63 64
 *
 * @property {string}  id               - Get Room id.
 * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
65
 */
66
export class Room extends Accessory implements Sayable {
67

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
68
  protected static pool: Map<string, Room>
69

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
  /**
   * 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')
   */
87
  public static async create (contactList: Contact[], topic?: string): Promise<Room> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
88 89
    log.verbose('Room', 'create(%s, %s)', contactList.join(','), topic)

90 91 92
    if (   !contactList
        || !Array.isArray(contactList)
    ) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93 94 95
      throw new Error('contactList not found')
    }

96 97 98 99
    if (contactList.length < 2) {
      throw new Error('contactList need at least 2 contact to create a new room')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
100
    try {
101 102 103
      const contactIdList = contactList.map(contact => contact.id)
      const roomId = await this.puppet.roomCreate(contactIdList, topic)
      const room = this.load(roomId)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
104 105 106 107 108 109 110 111 112
      return room
    } catch (e) {
      log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
      Raven.captureException(e)
      throw e
    }
  }

  /**
L
lijiarui 已提交
113
   * The filter to find the room:  {topic: string | RegExp}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
114
   *
L
lijiarui 已提交
115 116 117 118 119 120
   * @typedef    RoomQueryFilter
   * @property   {string} topic
   */

  /**
   * Find room by by filter: {topic: string | RegExp}, return all the matched room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
121 122 123 124
   * @static
   * @param {RoomQueryFilter} [query]
   * @returns {Promise<Room[]>}
   * @example
L
lijiarui 已提交
125 126 127 128 129
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in
   * const roomList = await bot.Room.findAll()                    // get the room list of the bot
   * const roomList = await bot.Room.findAll({topic: 'wechaty'})  // find all of the rooms with name 'wechaty'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
130
   */
131
  public static async findAll<T extends typeof Room> (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
132 133
    this   : T,
    query? : RoomQueryFilter,
134
  ): Promise<Array<T['prototype']>> {
135
    log.verbose('Room', 'findAll(%s)', JSON.stringify(query) || '')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
136

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
137
    if (query && !query.topic) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138 139 140
      throw new Error('topicFilter not found')
    }

141 142
    const invalidDict: { [id: string]: true } = {}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
143
    try {
144
      const roomIdList = await this.puppet.roomSearch(query)
145
      const roomList = roomIdList.map(id => this.load(id))
146 147
      await Promise.all(
        roomList.map(
148 149 150 151 152
          room => room.ready()
                      .catch(e => {
                        log.warn('Room', 'findAll() room.ready() rejection: %s', e)
                        invalidDict[room.id] = true
                      })
153 154
        ),
      )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
155

156
      return roomList.filter(room => !invalidDict[room.id])
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
157 158 159

    } catch (e) {
      log.verbose('Room', 'findAll() rejected: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
160
      console.error(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
161 162 163 164 165 166 167 168 169 170
      Raven.captureException(e)
      return [] as Room[] // fail safe
    }
  }

  /**
   * Try to find a room by filter: {topic: string | RegExp}. If get many, return the first one.
   *
   * @param {RoomQueryFilter} query
   * @returns {Promise<Room | null>} If can find the room, return Room, or return null
L
lijiarui 已提交
171 172 173 174 175 176
   * @example
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const roomList = await bot.Room.find()
   * const roomList = await bot.Room.find({topic: 'wechaty'})
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
177
   */
178

179
  public static async find<T extends typeof Room> (
180
    this  : T,
181
    query : string | RoomQueryFilter,
182
  ): Promise<T['prototype'] | null> {
183 184 185 186 187
    log.verbose('Room', 'find(%s)', JSON.stringify(query))

    if (typeof query === 'string') {
      query = { topic: query }
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
188 189

    const roomList = await this.findAll(query)
190
    if (!roomList) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
191 192
      return null
    }
193 194 195 196 197 198 199 200 201 202 203
    if (roomList.length < 1) {
      return null
    }

    if (roomList.length > 1) {
      log.warn('Room', 'find() got more than one(%d) result', roomList.length)
    }

    let n = 0
    for (n = 0; n < roomList.length; n++) {
      const room = roomList[n]
204
      // use puppet.roomValidate() to confirm double confirm that this roomId is valid.
205 206
      // https://github.com/lijiarui/wechaty-puppet-padchat/issues/64
      // https://github.com/Chatie/wechaty/issues/1345
207
      const valid = await this.puppet.roomValidate(room.id)
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
      if (valid) {
        log.verbose('Room', 'find() confirm room[#%d] with id=%d is vlaid result, return it.',
                            n,
                            room.id,
                  )
        return room
      } else {
        log.verbose('Room', 'find() confirm room[#%d] with id=%d is INVALID result, try next',
                            n,
                            room.id,
                    )
      }
    }
    log.warn('Room', 'find() got %d rooms but no one is valid.', roomList.length)
    return null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
223 224 225 226
  }

  /**
   * @private
227
   * About the Generic: https://stackoverflow.com/q/43003970/1123955
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
228
   *
L
lijiarui 已提交
229 230 231 232 233
   * Load room by topic. <br>
   * > Tips: For Web solution, it cannot get the unique topic id,
   * but for other solutions besides web,
   * we can get unique and permanent topic id.
   *
234
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
L
lijiarui 已提交
235 236 237 238 239 240 241 242 243
   * @static
   * @param {string} id
   * @returns {Room}
   * @example
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = bot.Room.load('roomId')
   */
244
  public static load<T extends typeof Room> (
245 246
    this : T,
    id   : string,
247
  ): T['prototype'] {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
248 249 250 251
    if (!this.pool) {
      this.pool = new Map<string, Room>()
    }

252 253 254
    const existingRoom = this.pool.get(id)
    if (existingRoom) {
      return existingRoom
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255
    }
256

257
    const newRoom = new (this as any)(id) as Room
258

259 260
    this.pool.set(id, newRoom)
    return newRoom
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
261 262
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
264
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
265 266 267 268 269
   *
   * Instance Properties
   *
   *
   */
270
  protected payload?: RoomPayload
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
271

272 273
  public readonly id: string  // Room Id

H
hcz 已提交
274 275 276
  /**
   * @private
   */
277
  constructor (
278
    id: string,
279
  ) {
280
    super()
281
    log.silly('Room', `constructor(${id})`)
282

283 284
    this.id = id

285 286 287 288 289 290 291 292 293 294 295
    // tslint:disable-next-line:variable-name
    const MyClass = instanceToClass(this, Room)

    if (MyClass === Room) {
      throw new Error('Room class can not be instanciated directly! See: https://github.com/Chatie/wechaty/issues/1217')
    }

    if (!this.puppet) {
      throw new Error('Room class can not be instanciated without a puppet!')
    }

296
  }
297

H
hcz 已提交
298 299 300
  /**
   * @private
   */
301
  public toString () {
302 303
    if (!this.payload) {
      return this.constructor.name
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
304
    }
305 306

    return `Room<${this.payload.topic || 'loadind...'}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
307
  }
L
lijiarui 已提交
308

309
  public async *[Symbol.asyncIterator] (): AsyncIterableIterator<Contact> {
310
    const memberList = await this.memberList()
311 312 313 314 315
    for (const contact of memberList) {
      yield contact
    }
  }

L
lijiarui 已提交
316
  /**
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
   * @ignore
   * @private
   * @deprecated: Use `sync()` instead
   */
  public async refresh (): Promise<void> {
    await this.sync()
  }

  /**
   * Force reload data for Room, Sync data from lowlevel API again.
   *
   * @returns {Promise<void>}
   * @example
   * await room.sync()
   */
  public async sync (): Promise<void> {
    await this.ready(true)
  }

  /**
   * `ready()` is For FrameWork ONLY!
   *
   * Please not to use `ready()` at the user land.
   * If you want to sync data, uyse `sync()` instead.
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
342
   * @private
L
lijiarui 已提交
343
   */
344
  public async ready (
345
    forceSync = false,
346
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
347
    log.verbose('Room', 'ready()')
348

349
    if (!forceSync && this.isReady()) {
350 351 352
      return
    }

353
    if (forceSync) {
354
      await this.puppet.roomPayloadDirty(this.id)
355
      await this.puppet.roomMemberPayloadDirty(this.id)
356
    }
357 358 359 360 361
    this.payload = await this.puppet.roomPayload(this.id)

    if (!this.payload) {
      throw new Error('ready() no payload')
    }
362 363 364

    const memberIdList = await this.puppet.roomMemberList(this.id)

365
    await Promise.all(
366 367
      memberIdList
        .map(id => this.wechaty.Contact.load(id))
368 369
        .map(contact => {
          contact.ready()
370 371
            .catch(e => {
              log.verbose('Room', 'ready() member.ready() rejection: %s', e)
372 373
            })
        }),
374 375 376 377
    )
  }

  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
378
   * @private
379
   */
380
  public isReady (): boolean {
381
    return !!(this.payload)
382 383
  }

384 385 386 387
  public say (text: string)                     : Promise<void>
  public say (text: string, mention: Contact)   : Promise<void>
  public say (text: string, mention: Contact[]) : Promise<void>
  public say (file: FileBox)                    : Promise<void>
388
  public say (url: UrlLink)                     : Promise<void>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
389 390

  public say (...args: never[]): never
L
lijiarui 已提交
391 392 393

  /**
   * Send message inside Room, if set [replyTo], wechaty will mention the contact as well.
394 395
   * > Tips:
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
L
lijiarui 已提交
396
   *
397
   * @param {(string | Contact | FileBox)} textOrContactOrFileOrUrl - Send `text` or `media file` inside Room. <br>
L
lijiarui 已提交
398 399 400 401 402 403 404 405 406 407
   * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file
   * @param {(Contact | Contact[])} [mention] - Optional parameter, send content inside Room, and mention @replyTo contact or contactList.
   * @returns {Promise<void>}
   *
   * @example
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'wechaty'})
   *
L
lijiarui 已提交
408
   * // 1. Send text inside Room
L
lijiarui 已提交
409 410 411
   *
   * await room.say('Hello world!')
   *
L
lijiarui 已提交
412
   * // 2. Send media file inside Room
L
lijiarui 已提交
413 414 415 416 417
   * import { FileBox }  from 'file-box'
   * const fileBox1 = FileBox.fromUrl('https://chatie.io/wechaty/images/bot-qr-code.png')
   * const fileBox2 = FileBox.fromLocal('/tmp/text.txt')
   * await room.say(fileBox1)
   * await room.say(fileBox2)
L
lijiarui 已提交
418
   *
L
lijiarui 已提交
419
   * // 3. Send Contact Card in a room
L
lijiarui 已提交
420 421 422
   * const contactCard = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member
   * await room.say(contactCard)
   *
L
lijiarui 已提交
423
   * // 4. Send text inside room and mention @mention contact
L
lijiarui 已提交
424
   * const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member
L
lijiarui 已提交
425 426
   * await room.say('Hello world!', contact)
   */
427
  public async say (
428 429
    textOrContactOrFileOrUrl : string | Contact | FileBox | UrlLink,
    mention?                 : Contact | Contact[],
430 431
  ): Promise<void> {
    log.verbose('Room', 'say(%s, %s)',
432
                                  textOrContactOrFileOrUrl,
433 434 435 436
                                  Array.isArray(mention)
                                  ? mention.map(c => c.name()).join(', ')
                                  : mention ? mention.name() : '',
                )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
437
    let text: string
438

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

441
    if (typeof textOrContactOrFileOrUrl === 'string') {
442 443

      if (replyToList.length > 0) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
444 445 446
        // const AT_SEPRATOR = String.fromCharCode(8197)
        const AT_SEPRATOR = FOUR_PER_EM_SPACE

447
        const mentionList = replyToList.map(c => '@' + c.name()).join(AT_SEPRATOR)
448
        text = mentionList + ' ' + textOrContactOrFileOrUrl
449
      } else {
450
        text = textOrContactOrFileOrUrl
451
      }
452
      await this.puppet.messageSendText({
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
453
        contactId : replyToList.length && replyToList[0].id || undefined,
454
        roomId    : this.id,
455
      }, text)
456
    } else if (textOrContactOrFileOrUrl instanceof FileBox) {
457 458
      await this.puppet.messageSendFile({
        roomId: this.id,
459 460
      }, textOrContactOrFileOrUrl)
    } else if (textOrContactOrFileOrUrl instanceof Contact) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
461 462
      await this.puppet.messageSendContact({
        roomId: this.id,
463 464 465 466 467 468 469 470
      }, textOrContactOrFileOrUrl.id)
    } else if (textOrContactOrFileOrUrl instanceof UrlLink) {
      /**
       * 4. Link Message
       */
      await this.puppet.messageSendUrl({
        contactId : this.id
      }, textOrContactOrFileOrUrl.payload)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
471
    } else {
472
      throw new Error('arg unsupported: ' + textOrContactOrFileOrUrl)
473 474
    }
  }
L
lijiarui 已提交
475

476
  public emit (event: 'invite',inviter: Contact, invitation: RoomInvitation)                 : boolean
477 478 479 480
  public emit (event: 'leave', leaverList:   Contact[],  remover?: Contact)                    : boolean
  public emit (event: 'join' , inviteeList:  Contact[] , inviter:  Contact)                    : boolean
  public emit (event: 'topic', topic:        string,     oldTopic: string,   changer: Contact) : boolean
  public emit (event: never, ...args: never[]): never
481

482
  public emit (
483
    event:   RoomEventName,
484
    ...args: any[]
485 486 487 488
  ): boolean {
    return super.emit(event, ...args)
  }

489
  public on (event: 'invite', listener: (this: Room, inviter: Contact, invitation: RoomInvitation) => void)              : this
490 491 492 493
  public on (event: 'leave', listener: (this: Room, leaverList:  Contact[], remover?: 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
  public on (event: never,   ...args: never[])                                                                           : never
L
lijiarui 已提交
494 495 496 497 498 499 500 501 502 503

   /**
    * @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 已提交
504
  /**
L
lijiarui 已提交
505 506 507 508 509
   * @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 已提交
510
   */
L
lijiarui 已提交
511

H
hcz 已提交
512
  /**
L
lijiarui 已提交
513 514 515 516 517 518
   * @listens Room
   * @param   {RoomEventName}      event      - Emit WechatyEvent
   * @param   {RoomEventFunction}  listener   - Depends on the WechatyEvent
   * @return  {this}                          - this for chain
   *
   * @example <caption>Event:join </caption>
L
lijiarui 已提交
519 520 521 522
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your wechat
L
lijiarui 已提交
523
   * if (room) {
L
lijiarui 已提交
524
   *   room.on('join', (room, inviteeList, inviter) => {
L
lijiarui 已提交
525
   *     const nameList = inviteeList.map(c => c.name()).join(',')
L
lijiarui 已提交
526
   *     console.log(`Room got new member ${nameList}, invited by ${inviter}`)
L
lijiarui 已提交
527 528 529 530
   *   })
   * }
   *
   * @example <caption>Event:leave </caption>
L
lijiarui 已提交
531 532 533 534
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your wechat
L
lijiarui 已提交
535
   * if (room) {
L
lijiarui 已提交
536
   *   room.on('leave', (room, leaverList) => {
L
lijiarui 已提交
537
   *     const nameList = leaverList.map(c => c.name()).join(',')
L
lijiarui 已提交
538
   *     console.log(`Room lost member ${nameList}`)
L
lijiarui 已提交
539 540 541 542
   *   })
   * }
   *
   * @example <caption>Event:topic </caption>
L
lijiarui 已提交
543 544 545 546
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your wechat
L
lijiarui 已提交
547
   * if (room) {
L
lijiarui 已提交
548
   *   room.on('topic', (room, topic, oldTopic, changer) => {
L
lijiarui 已提交
549
   *     console.log(`Room topic changed from ${oldTopic} to ${topic} by ${changer.name()}`)
L
lijiarui 已提交
550 551 552
   *   })
   * }
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
553 554 555 556 557 558 559 560 561
   * @example <caption>Event:invite </caption>
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'topic of your room'}) // change `event-room` to any room topic in your wechat
   * if (room) {
   *   room.on('invite', roomInvitation => roomInvitation.accept())
   * }
   *
H
hcz 已提交
562
   */
563
  public on (event: RoomEventName, listener: (...args: any[]) => any): this {
564
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
565

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
566
    super.on(event, listener) // Room is `Sayable`
567
    return this
568 569
  }

H
hcz 已提交
570
  /**
L
lijiarui 已提交
571 572
   * Add contact in a room
   *
573 574 575 576 577
   * > Tips:
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
   * >
   * > see {@link https://github.com/Chatie/wechaty/issues/1441|Web version of WeChat closed group interface}
   *
L
lijiarui 已提交
578
   * @param {Contact} contact
L
lijiarui 已提交
579
   * @returns {Promise<void>}
L
lijiarui 已提交
580
   * @example
L
lijiarui 已提交
581 582 583 584 585
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any contact in your wechat
   * const room = await bot.Room.find({topic: 'wechat'})        // change 'wechat' to any room topic in your wechat
L
lijiarui 已提交
586
   * if (room) {
L
lijiarui 已提交
587 588 589 590
   *   try {
   *      await room.add(contact)
   *   } catch(e) {
   *      console.error(e)
L
lijiarui 已提交
591 592
   *   }
   * }
H
hcz 已提交
593
   */
594
  public async add (contact: Contact): Promise<void> {
595
    log.verbose('Room', 'add(%s)', contact)
596
    await this.puppet.roomAdd(this.id, contact.id)
597
  }
598

H
hcz 已提交
599
  /**
L
lijiarui 已提交
600 601
   * Delete a contact from the room
   * It works only when the bot is the owner of the room
602 603 604 605 606 607
   *
   * > Tips:
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
   * >
   * > see {@link https://github.com/Chatie/wechaty/issues/1441|Web version of WeChat closed group interface}
   *
L
lijiarui 已提交
608
   * @param {Contact} contact
L
lijiarui 已提交
609
   * @returns {Promise<void>}
L
lijiarui 已提交
610
   * @example
L
lijiarui 已提交
611 612 613 614 615
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'wechat'})          // change 'wechat' to any room topic in your wechat
   * const contact = await bot.Contact.find({name: 'lijiarui'})   // change 'lijiarui' to any room member in the room you just set
L
lijiarui 已提交
616
   * if (room) {
L
lijiarui 已提交
617 618 619 620
   *   try {
   *      await room.del(contact)
   *   } catch(e) {
   *      console.error(e)
L
lijiarui 已提交
621 622
   *   }
   * }
H
hcz 已提交
623
   */
624
  public async del (contact: Contact): Promise<void> {
625
    log.verbose('Room', 'del(%s)', contact)
626
    await this.puppet.roomDel(this.id, contact.id)
627
    // this.delLocal(contact)
628 629
  }

630 631
  // private delLocal(contact: Contact): void {
  //   log.verbose('Room', 'delLocal(%s)', contact)
632

633 634 635 636 637 638 639 640 641 642
  //   const memberIdList = this.payload && this.payload.memberIdList
  //   if (memberIdList && memberIdList.length > 0) {
  //     for (let i = 0; i < memberIdList.length; i++) {
  //       if (memberIdList[i] === contact.id) {
  //         memberIdList.splice(i, 1)
  //         break
  //       }
  //     }
  //   }
  // }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
643

H
hcz 已提交
644
  /**
L
lijiarui 已提交
645 646
   * Bot quit the room itself
   *
647 648 649
   * > Tips:
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
   *
L
lijiarui 已提交
650 651 652
   * @returns {Promise<void>}
   * @example
   * await room.quit()
H
hcz 已提交
653
   */
654
  public async quit (): Promise<void> {
655
    log.verbose('Room', 'quit() %s', this)
656
    await this.puppet.roomQuit(this.id)
657
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
658

659 660
  public async topic ()                : Promise<string>
  public async topic (newTopic: string): Promise<void>
661

L
lijiarui 已提交
662 663 664 665
  /**
   * SET/GET topic from the room
   *
   * @param {string} [newTopic] If set this para, it will change room topic.
666
   * @returns {Promise<string | void>}
L
lijiarui 已提交
667 668
   *
   * @example <caption>When you say anything in a room, it will get room topic. </caption>
L
lijiarui 已提交
669
   * const bot = new Wechaty()
L
lijiarui 已提交
670 671 672 673
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
674
   *     const topic = await room.topic()
L
lijiarui 已提交
675 676 677
   *     console.log(`room topic is : ${topic}`)
   *   }
   * })
L
lijiarui 已提交
678
   * .start()
L
lijiarui 已提交
679 680
   *
   * @example <caption>When you say anything in a room, it will change room topic. </caption>
L
lijiarui 已提交
681
   * const bot = new Wechaty()
L
lijiarui 已提交
682 683 684 685
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
L
lijiarui 已提交
686 687
   *     const oldTopic = await room.topic()
   *     await room.topic('change topic to wechaty!')
L
lijiarui 已提交
688 689 690
   *     console.log(`room topic change from ${oldTopic} to ${room.topic()}`)
   *   }
   * })
L
lijiarui 已提交
691
   * .start()
L
lijiarui 已提交
692
   */
693
  public async topic (newTopic?: string): Promise<void | string> {
694 695 696
    log.verbose('Room', 'topic(%s)', newTopic ? newTopic : '')
    if (!this.isReady()) {
      log.warn('Room', 'topic() room not ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
697
      throw new Error('not ready')
698 699 700
    }

    if (typeof newTopic === 'undefined') {
701 702 703 704 705 706 707 708
      if (this.payload && this.payload.topic) {
        return this.payload.topic
      } else {
        const memberIdList = await this.puppet.roomMemberList(this.id)
        const memberList = memberIdList
                            .filter(id => id !== this.puppet.selfId())
                            .map(id => this.wechaty.Contact.load(id))

709
        let defaultTopic = memberList[0] && memberList[0].name() || ''
710 711 712 713 714
        for (let i = 1; i < 3 && memberList[i]; i++) {
          defaultTopic += ',' + memberList[i].name()
        }
        return defaultTopic
      }
715 716 717
    }

    const future = this.puppet
718
        .roomTopic(this.id, newTopic)
719 720 721 722 723 724 725 726 727
        .catch(e => {
          log.warn('Room', 'topic(newTopic=%s) exception: %s',
                            newTopic, e && e.message || e,
                  )
          Raven.captureException(e)
        })

    return future
  }
728

729 730
  public async announce ()             : Promise<string>
  public async announce (text: string) : Promise<void>
731

L
lijiarui 已提交
732 733 734
  /**
   * SET/GET announce from the room
   * > Tips: It only works when bot is the owner of the room.
735 736
   * >
   * > This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
L
lijiarui 已提交
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757
   *
   * @param {string} [text] If set this para, it will change room announce.
   * @returns {(Promise<void | string>)}
   *
   * @example <caption>When you say anything in a room, it will get room announce. </caption>
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'your room'})
   * const announce = await room.announce()
   * console.log(`room announce is : ${announce}`)
   *
   * @example <caption>When you say anything in a room, it will change room announce. </caption>
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'your room'})
   * const oldAnnounce = await room.announce()
   * await room.announce('change announce to wechaty!')
   * console.log(`room announce change from ${oldAnnounce} to ${room.announce()}`)
   */
758
  public async announce (text?: string): Promise<void | string> {
759 760 761 762 763
    log.verbose('Room', 'announce(%s)', text ? text : '')

    if (text) {
      await this.puppet.roomAnnounce(this.id, text)
    } else {
764 765
      const announcement = await this.puppet.roomAnnounce(this.id)
      return announcement
766 767 768
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
769
  /**
L
lijiarui 已提交
770
   * Get QR Code of the Room from the room, which can be used as scan and join the room.
771 772
   * > Tips:
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
L
lijiarui 已提交
773
   * @returns {Promise<string>}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
774
   */
775
  public async qrcode (): Promise<string> {
776
    log.verbose('Room', 'qrcode()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
777 778
    const qrcode = await this.puppet.roomQrcode(this.id)
    return qrcode
779 780
  }

L
lijiarui 已提交
781
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
782
   * Return contact's roomAlias in the room
L
lijiarui 已提交
783
   * @param {Contact} contact
L
lijiarui 已提交
784
   * @returns {Promise<string | null>} - If a contact has an alias in room, return string, otherwise return null
L
lijiarui 已提交
785
   * @example
L
lijiarui 已提交
786
   * const bot = new Wechaty()
L
lijiarui 已提交
787 788 789 790 791
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   const contact = m.from()
   *   if (room) {
L
lijiarui 已提交
792
   *     const alias = await room.alias(contact)
L
lijiarui 已提交
793 794 795
   *     console.log(`${contact.name()} alias is ${alias}`)
   *   }
   * })
L
lijiarui 已提交
796
   * .start()
L
lijiarui 已提交
797
   */
798
  public async alias (contact: Contact): Promise<null | string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
799 800 801 802 803 804 805
    const memberPayload = await this.puppet.roomMemberPayload(this.id, contact.id)

    if (memberPayload && memberPayload.roomAlias) {
      return memberPayload.roomAlias
    }

    return null
806
  }
807

H
hcz 已提交
808
  /**
L
lijiarui 已提交
809 810
   * Same as function alias
   * @param {Contact} contact
L
lijiarui 已提交
811
   * @returns {Promise<string | null>}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
812 813
   * @deprecated: use room.alias() instead
   * @private
H
hcz 已提交
814
   */
815
  public async roomAlias (contact: Contact): Promise<null | string> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
816 817
    log.warn('Room', 'roomAlias() DEPRECATED. use room.alias() instead')
    return this.alias(contact)
818
  }
819

H
hcz 已提交
820
  /**
821
   * Check if the room has member `contact`, the return is a Promise and must be `await`-ed
L
lijiarui 已提交
822 823
   *
   * @param {Contact} contact
L
lijiarui 已提交
824
   * @returns {Promise<boolean>} Return `true` if has contact, else return `false`.
L
lijiarui 已提交
825
   * @example <caption>Check whether 'lijiarui' is in the room 'wechaty'</caption>
L
lijiarui 已提交
826 827 828 829 830
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const contact = await bot.Contact.find({name: 'lijiarui'})   // change 'lijiarui' to any of contact in your wechat
   * const room = await bot.Room.find({topic: 'wechaty'})         // change 'wechaty' to any of the room in your wechat
L
lijiarui 已提交
831
   * if (contact && room) {
832
   *   if (await room.has(contact)) {
L
lijiarui 已提交
833
   *     console.log(`${contact.name()} is in the room wechaty!`)
L
lijiarui 已提交
834
   *   } else {
L
lijiarui 已提交
835
   *     console.log(`${contact.name()} is not in the room wechaty!`)
L
lijiarui 已提交
836 837
   *   }
   * }
H
hcz 已提交
838
   */
839
  public async has (contact: Contact): Promise<boolean> {
840 841 842
    const memberIdList = await this.puppet.roomMemberList(this.id)

    if (!memberIdList) {
843 844
      return false
    }
845 846 847 848

    return memberIdList
            .filter(id => id === contact.id)
            .length > 0
849
  }
L
lijiarui 已提交
850

851
  public async memberAll ()                              : Promise<Contact[]>
852 853
  public async memberAll (name: string)                  : Promise<Contact[]>
  public async memberAll (filter: RoomMemberQueryFilter) : Promise<Contact[]>
854

L
lijiarui 已提交
855 856 857
  /**
   * The way to search member by Room.member()
   *
L
lijiarui 已提交
858
   * @typedef    RoomMemberQueryFilter
L
lijiarui 已提交
859 860 861 862 863 864 865 866 867 868 869
   * @property   {string} name            -Find the contact by wechat name in a room, equal to `Contact.name()`.
   * @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()`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
870
   * - `roomAlias`            the name-string set by user-self in the room, should be called roomAlias
L
lijiarui 已提交
871
   * - `contactAlias`         the name-string set by bot for others, should be called alias, equal to `Contact.alias()`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
872
   * @param {(RoomMemberQueryFilter | string)} query -When use memberAll(name:string), return all matched members, including name, roomAlias, contactAlias
L
lijiarui 已提交
873
   * @returns {Promise<Contact[]>}
L
lijiarui 已提交
874
   */
875
  public async memberAll (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
876
    query?: string | RoomMemberQueryFilter,
877 878
  ): Promise<Contact[]> {
    log.silly('Room', 'memberAll(%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
879
                      JSON.stringify(query) || '',
880 881
              )

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
882 883 884 885
    if (!query) {
      return this.memberList()
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
886 887 888 889
    const contactIdList = await this.puppet.roomMemberSearch(this.id, query)
    const contactList   = contactIdList.map(id => this.wechaty.Contact.load(id))

    return contactList
890
  }
891

892 893
  public async member (name  : string)               : Promise<null | Contact>
  public async member (filter: RoomMemberQueryFilter): Promise<null | Contact>
894

L
lijiarui 已提交
895 896 897
  /**
   * Find all contacts in a room, if get many, return the first one.
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
898
   * @param {(RoomMemberQueryFilter | string)} queryArg -When use member(name:string), return all matched members, including name, roomAlias, contactAlias
L
lijiarui 已提交
899
   * @returns {Promise<null | Contact>}
L
lijiarui 已提交
900 901
   *
   * @example <caption>Find member by name</caption>
L
lijiarui 已提交
902 903 904 905
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'wechaty'})           // change 'wechaty' to any room name in your wechat
L
lijiarui 已提交
906
   * if (room) {
L
lijiarui 已提交
907
   *   const member = await room.member('lijiarui')             // change 'lijiarui' to any room member in your wechat
L
lijiarui 已提交
908
   *   if (member) {
L
lijiarui 已提交
909
   *     console.log(`wechaty room got the member: ${member.name()}`)
L
lijiarui 已提交
910
   *   } else {
L
lijiarui 已提交
911
   *     console.log(`cannot get member in wechaty room!`)
L
lijiarui 已提交
912 913 914 915
   *   }
   * }
   *
   * @example <caption>Find member by MemberQueryFilter</caption>
L
lijiarui 已提交
916 917 918 919
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = await bot.Room.find({topic: 'wechaty'})          // change 'wechaty' to any room name in your wechat
L
lijiarui 已提交
920
   * if (room) {
L
lijiarui 已提交
921
   *   const member = await room.member({name: 'lijiarui'})        // change 'lijiarui' to any room member in your wechat
L
lijiarui 已提交
922
   *   if (member) {
L
lijiarui 已提交
923
   *     console.log(`wechaty room got the member: ${member.name()}`)
L
lijiarui 已提交
924
   *   } else {
L
lijiarui 已提交
925
   *     console.log(`cannot get member in wechaty room!`)
L
lijiarui 已提交
926 927 928
   *   }
   * }
   */
929
  public async member (
930
    queryArg: string | RoomMemberQueryFilter,
931
  ): Promise<null | Contact> {
932 933 934 935 936 937
    log.verbose('Room', 'member(%s)', JSON.stringify(queryArg))

    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') {
938
      memberList =  await this.memberAll(queryArg)
939
    } else {
940
      memberList =  await this.memberAll(queryArg)
941 942 943 944 945 946 947 948 949 950 951
    }

    if (!memberList || !memberList.length) {
      return null
    }

    if (memberList.length > 1) {
      log.warn('Room', 'member(%s) get %d contacts, use the first one by default', JSON.stringify(queryArg), memberList.length)
    }
    return memberList[0]
  }
952

H
hcz 已提交
953
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
954 955
   * @private
   *
L
lijiarui 已提交
956 957
   * Get all room member from the room
   *
L
lijiarui 已提交
958 959 960
   * @returns {Promise<Contact[]>}
   * @example
   * await room.memberList()
H
hcz 已提交
961
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
962
  private async memberList (): Promise<Contact[]> {
963 964 965
    log.verbose('Room', 'memberList()')

    const memberIdList = await this.puppet.roomMemberList(this.id)
966

967
    if (!memberIdList) {
968 969 970
      log.warn('Room', 'memberList() not ready')
      return []
    }
971 972 973 974

    const contactList = memberIdList.map(
      id => this.wechaty.Contact.load(id),
    )
975
    return contactList
976
  }
977

L
lijiarui 已提交
978 979
  /**
   * Get room's owner from the room.
980 981
   * > Tips:
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/Chatie/wechaty/wiki/Puppet#3-puppet-compatible-table)
L
lijiarui 已提交
982
   * @returns {(Contact | null)}
L
lijiarui 已提交
983 984
   * @example
   * const owner = room.owner()
L
lijiarui 已提交
985
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
986
  public owner (): null | Contact {
987 988
    log.info('Room', 'owner()')

989 990 991 992 993
    const ownerId = this.payload && this.payload.ownerId
    if (!ownerId) {
      return null
    }

994
    const owner = this.wechaty.Contact.load(ownerId)
995
    return owner
996
  }
L
lijiarui 已提交
997

998
  public async avatar (): Promise<FileBox> {
999 1000 1001 1002 1003
    log.verbose('Room', 'avatar()')

    return this.puppet.roomAvatar(this.id)
  }

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