room.ts 29.2 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 23 24 25
import {
  FileBox,
}                   from 'file-box'
import {
  instanceToClass,
}                   from 'clone-class'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
26

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

38 39 40
import {
  Contact,
}               from './contact'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
41

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
55
/**
L
lijiarui 已提交
56
 * All wechat rooms(groups) will be encapsulated as a Room.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57
 *
L
lijiarui 已提交
58
 * [Examples/Room-Bot]{@link https://github.com/Chatie/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/room-bot.ts}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
59
 */
60
export class Room extends Accessory implements Sayable {
61

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

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

    if (!contactList || !Array.isArray(contactList)) {
      throw new Error('contactList not found')
    }

    try {
89 90 91
      const contactIdList = contactList.map(contact => contact.id)
      const roomId = await this.puppet.roomCreate(contactIdList, topic)
      const room = this.load(roomId)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
92 93 94 95 96 97 98 99 100
      return room
    } catch (e) {
      log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
      Raven.captureException(e)
      throw e
    }
  }

  /**
L
lijiarui 已提交
101
   * The filter to find the room:  {topic: string | RegExp}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
102
   *
L
lijiarui 已提交
103 104 105 106 107 108
   * @typedef    RoomQueryFilter
   * @property   {string} topic
   */

  /**
   * Find room by by filter: {topic: string | RegExp}, return all the matched room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
109 110 111 112
   * @static
   * @param {RoomQueryFilter} [query]
   * @returns {Promise<Room[]>}
   * @example
L
lijiarui 已提交
113 114 115 116 117
   * 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 (李卓桓) 已提交
118
   */
119 120 121 122
  public static async findAll<T extends typeof Room>(
    this  : T,
    query : RoomQueryFilter = { topic: /.*/ },
  ): Promise<T['prototype'][]> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
123
    log.verbose('Room', 'findAll()', JSON.stringify(query))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
124 125 126 127 128 129

    if (!query.topic) {
      throw new Error('topicFilter not found')
    }

    try {
130
      const roomIdList = await this.puppet.roomSearch(query)
131
      const roomList = roomIdList.map(id => this.load(id))
132 133 134 135 136 137 138 139 140 141 142
      await Promise.all(
        roomList.map(
          room => {
            try {
              return room.ready()
            } catch (e) {
              return {} as any
            }
          },
        ),
      )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
143 144 145 146 147

      return roomList

    } catch (e) {
      log.verbose('Room', 'findAll() rejected: %s', e.message)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
148
      console.error(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
149 150 151 152 153 154 155 156 157 158
      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 已提交
159 160 161 162 163 164
   * @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 (李卓桓) 已提交
165
   */
166

167 168
  public static async find<T extends typeof Room>(
    this  : T,
169
    query : string | RoomQueryFilter,
170
  ): Promise<T['prototype'] | null> {
171 172 173 174 175
    log.verbose('Room', 'find(%s)', JSON.stringify(query))

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

    const roomList = await this.findAll(query)
178
    if (!roomList) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
179 180
      return null
    }
181 182 183 184 185 186 187 188 189 190 191
    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]
192
      // use puppet.roomValidate() to confirm double confirm that this roomId is valid.
193 194
      // https://github.com/lijiarui/wechaty-puppet-padchat/issues/64
      // https://github.com/Chatie/wechaty/issues/1345
195
      const valid = await this.puppet.roomValidate(room.id)
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
      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 (李卓桓) 已提交
211 212 213 214
  }

  /**
   * @private
215
   * About the Generic: https://stackoverflow.com/q/43003970/1123955
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216
   */
L
lijiarui 已提交
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
  /**
   * 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.
   *
   * @static
   * @param {string} id
   * @returns {Room}
   * @example
   * const bot = new Wechaty()
   * await bot.start()
   * // after logged in...
   * const room = bot.Room.load('roomId')
   */
232
  public static load<T extends typeof Room>(
233 234
    this : T,
    id   : string,
235
  ): T['prototype'] {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
236 237 238 239
    if (!this.pool) {
      this.pool = new Map<string, Room>()
    }

240 241 242
    const existingRoom = this.pool.get(id)
    if (existingRoom) {
      return existingRoom
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
243
    }
244

245 246
    const newRoom = new (this as any)(id) as Room
    // newRoom.payload = this.puppet.cacheRoomPayload.get(id)
247

248 249
    this.pool.set(id, newRoom)
    return newRoom
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
250 251
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
252
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
253
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
254 255 256 257 258
   *
   * Instance Properties
   *
   *
   */
259
  protected get payload(): undefined | RoomPayload {
260 261 262 263
    if (!this.id) {
      return undefined
    }

264 265 266
    const readyPayload = this.puppet.roomPayloadCache(this.id)
    return readyPayload
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
267

268 269
  public readonly id: string  // Room Id

H
hcz 已提交
270 271 272
  /**
   * @private
   */
273
  constructor(
274
    id: string,
275
  ) {
276
    super()
277
    log.silly('Room', `constructor(${id})`)
278

279 280
    this.id = id

281 282 283 284 285 286 287 288 289 290 291
    // 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!')
    }

292
  }
293

H
hcz 已提交
294 295 296
  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
297
  public toString() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298
    if (this.payload && this.payload.topic) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
299
      return `Room<${this.payload.topic}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
300
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301
    return `Room<${this.id || ''}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
302
  }
L
lijiarui 已提交
303

304 305
  public async *[Symbol.asyncIterator](): AsyncIterableIterator<Contact> {
    const memberList = await this.memberList()
306 307 308 309 310
    for (const contact of memberList) {
      yield contact
    }
  }

L
lijiarui 已提交
311 312 313
  /**
   * @private
   */
314
  public async ready(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
315
    dirty = false,
316
  ): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
317
    log.verbose('Room', 'ready()')
318

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
319
    if (!dirty && this.isReady()) {
320 321 322
      return
    }

323 324 325 326
    if (dirty) {
      await this.puppet.roomPayloadDirty(this.id)
    }
    await this.puppet.roomPayload(this.id)
327 328 329

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

330
    await Promise.all(
331 332
      memberIdList
        .map(id => this.wechaty.Contact.load(id))
333 334 335 336 337 338
        .map(contact => {
          contact.ready()
            .catch(() => {
              //
            })
        }),
339 340 341 342 343 344 345
    )
  }

  /**
   * @private
   */
  public isReady(): boolean {
346
    return !!(this.payload)
347 348 349 350 351
  }

  public say(text: string)                     : Promise<void>
  public say(text: string, mention: Contact)   : Promise<void>
  public say(text: string, mention: Contact[]) : Promise<void>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
352
  public say(file: FileBox)                    : Promise<void>
353
  public say(text: never, ...args: never[])    : never
L
lijiarui 已提交
354 355 356 357

  /**
   * Send message inside Room, if set [replyTo], wechaty will mention the contact as well.
   *
L
lijiarui 已提交
358 359 360 361 362 363 364 365 366 367 368
   * @param {(string | Contact | FileBox)} textOrContactOrFile - Send `text` or `media file` inside Room. <br>
   * 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 已提交
369
   * // 1. Send text inside Room
L
lijiarui 已提交
370 371 372
   *
   * await room.say('Hello world!')
   *
L
lijiarui 已提交
373
   * // 2. Send media file inside Room
L
lijiarui 已提交
374 375 376 377 378
   * 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 已提交
379
   *
L
lijiarui 已提交
380
   * // 3. Send Contact Card in a room
L
lijiarui 已提交
381 382 383
   * const contactCard = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member
   * await room.say(contactCard)
   *
L
lijiarui 已提交
384
   * // 4. Send text inside room and mention @mention contact
L
lijiarui 已提交
385
   * const contact = await bot.Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member
L
lijiarui 已提交
386 387
   * await room.say('Hello world!', contact)
   */
388
  public async say(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
389 390
    textOrContactOrFile : string | Contact | FileBox,
    mention?            : Contact | Contact[],
391 392
  ): Promise<void> {
    log.verbose('Room', 'say(%s, %s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
393
                                  textOrContactOrFile,
394 395 396 397
                                  Array.isArray(mention)
                                  ? mention.map(c => c.name()).join(', ')
                                  : mention ? mention.name() : '',
                )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
398
    let text: string
399

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
402
    if (typeof textOrContactOrFile === 'string') {
403 404

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

408
        const mentionList = replyToList.map(c => '@' + c.name()).join(AT_SEPRATOR)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
409
        text = mentionList + ' ' + textOrContactOrFile
410
      } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
411
        text = textOrContactOrFile
412
      }
413
      await this.puppet.messageSendText({
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
414 415
        roomId    : this.id,
        contactId : replyToList.length && replyToList[0].id || undefined,
416
      }, text)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
417
    } else if (textOrContactOrFile instanceof FileBox) {
418 419
      await this.puppet.messageSendFile({
        roomId: this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
420 421 422 423 424
      }, textOrContactOrFile)
    } else if (textOrContactOrFile instanceof Contact) {
      await this.puppet.messageSendContact({
        roomId: this.id,
      }, textOrContactOrFile.id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
425 426
    } else {
      throw new Error('arg unsupported')
427 428
    }
  }
L
lijiarui 已提交
429

430
  public emit(event: 'leave', leaverList:   Contact[],  remover?: Contact)                    : boolean
431 432 433 434 435 436
  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

  public emit(
    event:   RoomEventName,
437
    ...args: any[]
438 439 440 441
  ): boolean {
    return super.emit(event, ...args)
  }

442 443 444 445
  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 已提交
446 447 448 449 450 451 452 453 454 455

   /**
    * @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 已提交
456
  /**
L
lijiarui 已提交
457 458 459 460 461
   * @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 已提交
462
   */
L
lijiarui 已提交
463

H
hcz 已提交
464
  /**
L
lijiarui 已提交
465 466 467 468 469 470
   * @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 已提交
471 472 473 474
   * 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 已提交
475 476 477
   * if (room) {
   *   room.on('join', (room: Room, inviteeList: Contact[], inviter: Contact) => {
   *     const nameList = inviteeList.map(c => c.name()).join(',')
L
lijiarui 已提交
478
   *     console.log(`Room got new member ${nameList}, invited by ${inviter}`)
L
lijiarui 已提交
479 480 481 482
   *   })
   * }
   *
   * @example <caption>Event:leave </caption>
L
lijiarui 已提交
483 484 485 486
   * 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 已提交
487 488 489
   * if (room) {
   *   room.on('leave', (room: Room, leaverList: Contact[]) => {
   *     const nameList = leaverList.map(c => c.name()).join(',')
L
lijiarui 已提交
490
   *     console.log(`Room lost member ${nameList}`)
L
lijiarui 已提交
491 492 493 494
   *   })
   * }
   *
   * @example <caption>Event:topic </caption>
L
lijiarui 已提交
495 496 497 498
   * 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 已提交
499 500
   * if (room) {
   *   room.on('topic', (room: Room, topic: string, oldTopic: string, changer: Contact) => {
L
lijiarui 已提交
501
   *     console.log(`Room topic changed from ${oldTopic} to ${topic} by ${changer.name()}`)
L
lijiarui 已提交
502 503 504
   *   })
   * }
   *
H
hcz 已提交
505
   */
506
  public on(event: RoomEventName, listener: (...args: any[]) => any): this {
507
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
508

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
509
    super.on(event, listener) // Room is `Sayable`
510
    return this
511 512
  }

H
hcz 已提交
513
  /**
L
lijiarui 已提交
514 515 516
   * Add contact in a room
   *
   * @param {Contact} contact
L
lijiarui 已提交
517
   * @returns {Promise<void>}
L
lijiarui 已提交
518
   * @example
L
lijiarui 已提交
519 520 521 522 523
   * 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 已提交
524
   * if (room) {
L
lijiarui 已提交
525 526 527 528
   *   try {
   *      await room.add(contact)
   *   } catch(e) {
   *      console.error(e)
L
lijiarui 已提交
529 530
   *   }
   * }
H
hcz 已提交
531
   */
532 533
  public async add(contact: Contact): Promise<void> {
    log.verbose('Room', 'add(%s)', contact)
534
    await this.puppet.roomAdd(this.id, contact.id)
535
  }
536

H
hcz 已提交
537
  /**
L
lijiarui 已提交
538 539 540
   * Delete a contact from the room
   * It works only when the bot is the owner of the room
   * @param {Contact} contact
L
lijiarui 已提交
541
   * @returns {Promise<void>}
L
lijiarui 已提交
542
   * @example
L
lijiarui 已提交
543 544 545 546 547
   * 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 已提交
548
   * if (room) {
L
lijiarui 已提交
549 550 551 552
   *   try {
   *      await room.del(contact)
   *   } catch(e) {
   *      console.error(e)
L
lijiarui 已提交
553 554
   *   }
   * }
H
hcz 已提交
555
   */
556 557
  public async del(contact: Contact): Promise<void> {
    log.verbose('Room', 'del(%s)', contact)
558
    await this.puppet.roomDel(this.id, contact.id)
559
    // this.delLocal(contact)
560 561
  }

562 563
  // private delLocal(contact: Contact): void {
  //   log.verbose('Room', 'delLocal(%s)', contact)
564

565 566 567 568 569 570 571 572 573 574
  //   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 (李卓桓) 已提交
575

H
hcz 已提交
576
  /**
L
lijiarui 已提交
577 578 579 580 581
   * Bot quit the room itself
   *
   * @returns {Promise<void>}
   * @example
   * await room.quit()
H
hcz 已提交
582
   */
583 584
  public async quit(): Promise<void> {
    log.verbose('Room', 'quit() %s', this)
585
    await this.puppet.roomQuit(this.id)
586
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
587

588 589
  public async topic()                : Promise<string>
  public async topic(newTopic: string): Promise<void>
590

L
lijiarui 已提交
591 592 593 594
  /**
   * SET/GET topic from the room
   *
   * @param {string} [newTopic] If set this para, it will change room topic.
595
   * @returns {Promise<string | void>}
L
lijiarui 已提交
596 597
   *
   * @example <caption>When you say anything in a room, it will get room topic. </caption>
L
lijiarui 已提交
598
   * const bot = new Wechaty()
L
lijiarui 已提交
599 600 601 602
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
603
   *     const topic = await room.topic()
L
lijiarui 已提交
604 605 606
   *     console.log(`room topic is : ${topic}`)
   *   }
   * })
L
lijiarui 已提交
607
   * .start()
L
lijiarui 已提交
608 609
   *
   * @example <caption>When you say anything in a room, it will change room topic. </caption>
L
lijiarui 已提交
610
   * const bot = new Wechaty()
L
lijiarui 已提交
611 612 613 614
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
L
lijiarui 已提交
615 616
   *     const oldTopic = await room.topic()
   *     await room.topic('change topic to wechaty!')
L
lijiarui 已提交
617 618 619
   *     console.log(`room topic change from ${oldTopic} to ${room.topic()}`)
   *   }
   * })
L
lijiarui 已提交
620
   * .start()
L
lijiarui 已提交
621
   */
622
  public async topic(newTopic?: string): Promise<void | string> {
623 624 625
    log.verbose('Room', 'topic(%s)', newTopic ? newTopic : '')
    if (!this.isReady()) {
      log.warn('Room', 'topic() room not ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
626
      throw new Error('not ready')
627 628 629
    }

    if (typeof newTopic === 'undefined') {
630 631 632 633 634 635 636 637
      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))

638
        let defaultTopic = memberList[0] && memberList[0].name() || ''
639 640 641 642 643
        for (let i = 1; i < 3 && memberList[i]; i++) {
          defaultTopic += ',' + memberList[i].name()
        }
        return defaultTopic
      }
644 645 646
    }

    const future = this.puppet
647
        .roomTopic(this.id, newTopic)
648 649 650 651 652 653 654 655 656
        .catch(e => {
          log.warn('Room', 'topic(newTopic=%s) exception: %s',
                            newTopic, e && e.message || e,
                  )
          Raven.captureException(e)
        })

    return future
  }
657

658 659 660
  public async announce()             : Promise<string>
  public async announce(text: string) : Promise<void>

L
lijiarui 已提交
661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
  /**
   * SET/GET announce from the room
   * > Tips: It only works when bot is the owner of the room.
   *
   * @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()}`)
   */
685 686 687 688 689 690 691 692 693 694
  public async announce(text?: string): Promise<void | string> {
    log.verbose('Room', 'announce(%s)', text ? text : '')

    if (text) {
      await this.puppet.roomAnnounce(this.id, text)
    } else {
      return await this.puppet.roomAnnounce(this.id)
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
695
  /**
L
lijiarui 已提交
696 697 698
   * Get QR Code of the Room from the room, which can be used as scan and join the room.
   *
   * @returns {Promise<string>}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
699
   */
700 701
  public async qrcode(): Promise<string> {
    log.verbose('Room', 'qrcode()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
702 703
    const qrcode = await this.puppet.roomQrcode(this.id)
    return qrcode
704 705
  }

L
lijiarui 已提交
706
  /**
L
lijiarui 已提交
707
   * Return contact's roomAlias in the room, the same as roomAlias
L
lijiarui 已提交
708
   * @param {Contact} contact
L
lijiarui 已提交
709
   * @returns {Promise<string | null>} - If a contact has an alias in room, return string, otherwise return null
L
lijiarui 已提交
710
   * @example
L
lijiarui 已提交
711
   * const bot = new Wechaty()
L
lijiarui 已提交
712 713 714 715 716
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   const contact = m.from()
   *   if (room) {
L
lijiarui 已提交
717
   *     const alias = await room.alias(contact)
L
lijiarui 已提交
718 719 720
   *     console.log(`${contact.name()} alias is ${alias}`)
   *   }
   * })
L
lijiarui 已提交
721
   * .start()
L
lijiarui 已提交
722
   */
723
  public async alias(contact: Contact): Promise<null | string> {
724 725
    return this.roomAlias(contact)
  }
726

H
hcz 已提交
727
  /**
L
lijiarui 已提交
728 729
   * Same as function alias
   * @param {Contact} contact
L
lijiarui 已提交
730
   * @returns {Promise<string | null>}
H
hcz 已提交
731
   */
732
  public async roomAlias(contact: Contact): Promise<null | string> {
733

734
    const memberPayload = await this.puppet.roomMemberPayload(this.id, contact.id)
735 736 737

    if (memberPayload && memberPayload.roomAlias) {
      return memberPayload.roomAlias
738
    }
739 740

    return null
741
  }
742

H
hcz 已提交
743
  /**
744
   * Check if the room has member `contact`, the return is a Promise and must be `await`-ed
L
lijiarui 已提交
745 746
   *
   * @param {Contact} contact
L
lijiarui 已提交
747
   * @returns {Promise<boolean>} Return `true` if has contact, else return `false`.
L
lijiarui 已提交
748
   * @example <caption>Check whether 'lijiarui' is in the room 'wechaty'</caption>
L
lijiarui 已提交
749 750 751 752 753
   * 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 已提交
754
   * if (contact && room) {
755
   *   if (await room.has(contact)) {
L
lijiarui 已提交
756
   *     console.log(`${contact.name()} is in the room wechaty!`)
L
lijiarui 已提交
757
   *   } else {
L
lijiarui 已提交
758
   *     console.log(`${contact.name()} is not in the room wechaty!`)
L
lijiarui 已提交
759 760
   *   }
   * }
H
hcz 已提交
761
   */
762 763 764 765
  public async has(contact: Contact): Promise<boolean> {
    const memberIdList = await this.puppet.roomMemberList(this.id)

    if (!memberIdList) {
766 767
      return false
    }
768 769 770 771

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
774 775
  public async memberAll(name: string)                  : Promise<Contact[]>
  public async memberAll(filter: RoomMemberQueryFilter) : Promise<Contact[]>
776

L
lijiarui 已提交
777 778 779
  /**
   * The way to search member by Room.member()
   *
L
lijiarui 已提交
780
   * @typedef    RoomMemberQueryFilter
L
lijiarui 已提交
781 782 783 784 785 786 787 788 789 790 791
   * @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 (李卓桓) 已提交
792
   * - `roomAlias`            the name-string set by user-self in the room, should be called roomAlias
L
lijiarui 已提交
793
   * - `contactAlias`         the name-string set by bot for others, should be called alias, equal to `Contact.alias()`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
794
   * @param {(RoomMemberQueryFilter | string)} query -When use memberAll(name:string), return all matched members, including name, roomAlias, contactAlias
L
lijiarui 已提交
795
   * @returns {Promise<Contact[]>}
L
lijiarui 已提交
796
   */
797
  public async memberAll(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
798
    query: string | RoomMemberQueryFilter,
799 800
  ): Promise<Contact[]> {
    log.silly('Room', 'memberAll(%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
801
                      JSON.stringify(query),
802 803
              )

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
804 805 806 807
    const contactIdList = await this.puppet.roomMemberSearch(this.id, query)
    const contactList   = contactIdList.map(id => this.wechaty.Contact.load(id))

    return contactList
808
  }
809

810 811
  public async member(name  : string)               : Promise<null | Contact>
  public async member(filter: RoomMemberQueryFilter): Promise<null | Contact>
812

L
lijiarui 已提交
813 814 815
  /**
   * Find all contacts in a room, if get many, return the first one.
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
816
   * @param {(RoomMemberQueryFilter | string)} queryArg -When use member(name:string), return all matched members, including name, roomAlias, contactAlias
L
lijiarui 已提交
817
   * @returns {Promise<null | Contact>}
L
lijiarui 已提交
818 819
   *
   * @example <caption>Find member by name</caption>
L
lijiarui 已提交
820 821 822 823
   * 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 已提交
824
   * if (room) {
L
lijiarui 已提交
825
   *   const member = await room.member('lijiarui')             // change 'lijiarui' to any room member in your wechat
L
lijiarui 已提交
826
   *   if (member) {
L
lijiarui 已提交
827
   *     console.log(`wechaty room got the member: ${member.name()}`)
L
lijiarui 已提交
828
   *   } else {
L
lijiarui 已提交
829
   *     console.log(`cannot get member in wechaty room!`)
L
lijiarui 已提交
830 831 832 833
   *   }
   * }
   *
   * @example <caption>Find member by MemberQueryFilter</caption>
L
lijiarui 已提交
834 835 836 837
   * 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 已提交
838
   * if (room) {
L
lijiarui 已提交
839
   *   const member = await room.member({name: 'lijiarui'})        // change 'lijiarui' to any room member in your wechat
L
lijiarui 已提交
840
   *   if (member) {
L
lijiarui 已提交
841
   *     console.log(`wechaty room got the member: ${member.name()}`)
L
lijiarui 已提交
842
   *   } else {
L
lijiarui 已提交
843
   *     console.log(`cannot get member in wechaty room!`)
L
lijiarui 已提交
844 845 846
   *   }
   * }
   */
847
  public async member(
848
    queryArg: string | RoomMemberQueryFilter,
849
  ): Promise<null | Contact> {
850 851 852 853 854 855
    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') {
856
      memberList =  await this.memberAll(queryArg)
857
    } else {
858
      memberList =  await this.memberAll(queryArg)
859 860 861 862 863 864 865 866 867 868 869
    }

    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]
  }
870

H
hcz 已提交
871
  /**
L
lijiarui 已提交
872 873
   * Get all room member from the room
   *
L
lijiarui 已提交
874 875 876
   * @returns {Promise<Contact[]>}
   * @example
   * await room.memberList()
H
hcz 已提交
877
   */
878 879 880 881
  public async memberList(): Promise<Contact[]> {
    log.verbose('Room', 'memberList()')

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

883
    if (!memberIdList) {
884 885 886
      log.warn('Room', 'memberList() not ready')
      return []
    }
887 888 889 890

    const contactList = memberIdList.map(
      id => this.wechaty.Contact.load(id),
    )
891
    return contactList
892
  }
893

L
lijiarui 已提交
894
  /**
L
lijiarui 已提交
895
   * @ignore
L
lijiarui 已提交
896
   */
897 898 899
  public async refresh(): Promise<void> {
    return this.sync()
  }
L
lijiarui 已提交
900

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
901
  /**
L
lijiarui 已提交
902
   * Force reload data for Room, Sync data for Room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
903 904
   *
   * @returns {Promise<void>}
L
lijiarui 已提交
905 906
   * @example
   * await room.sync()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
907
   */
908
  public async sync(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
909
    await this.ready(true)
910
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
911

L
lijiarui 已提交
912 913
  /**
   * Get room's owner from the room.
L
lijiarui 已提交
914
   *
L
lijiarui 已提交
915
   * @returns {(Contact | null)}
L
lijiarui 已提交
916 917
   * @example
   * const owner = room.owner()
L
lijiarui 已提交
918
   */
919 920 921
  public owner(): Contact | null {
    log.info('Room', 'owner()')

922 923 924 925 926
    const ownerId = this.payload && this.payload.ownerId
    if (!ownerId) {
      return null
    }

927
    const owner = this.wechaty.Contact.load(ownerId)
928
    return owner
929
  }
L
lijiarui 已提交
930

931 932 933 934 935 936
  public async avatar(): Promise<FileBox> {
    log.verbose('Room', 'avatar()')

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
937
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
938 939

export default Room