room.ts 31.0 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}
59 60 61
 *
 * @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 (李卓桓) 已提交
62
 */
63
export class Room extends Accessory implements Sayable {
64

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
  /**
   * 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 {
92 93 94
      const contactIdList = contactList.map(contact => contact.id)
      const roomId = await this.puppet.roomCreate(contactIdList, topic)
      const room = this.load(roomId)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
95 96 97 98 99 100 101 102 103
      return room
    } catch (e) {
      log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
      Raven.captureException(e)
      throw e
    }
  }

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

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

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

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

      return roomList

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

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

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

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

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

244 245 246
    const existingRoom = this.pool.get(id)
    if (existingRoom) {
      return existingRoom
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
247
    }
248

249 250
    const newRoom = new (this as any)(id) as Room
    // newRoom.payload = this.puppet.cacheRoomPayload.get(id)
251

252 253
    this.pool.set(id, newRoom)
    return newRoom
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
254 255
  }

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

268 269 270
    const readyPayload = this.puppet.roomPayloadCache(this.id)
    return readyPayload
  }
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
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301
  public toString() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
302
    if (this.payload && this.payload.topic) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
303
      return `Room<${this.payload.topic}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
304
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
305
    return `Room<${this.id || ''}>`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306
  }
L
lijiarui 已提交
307

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
323
    if (!dirty && this.isReady()) {
324 325 326
      return
    }

327 328 329 330
    if (dirty) {
      await this.puppet.roomPayloadDirty(this.id)
    }
    await this.puppet.roomPayload(this.id)
331 332 333

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

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

  /**
   * @private
   */
  public isReady(): boolean {
350
    return !!(this.payload)
351 352 353 354 355
  }

  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 (李卓桓) 已提交
356
  public say(file: FileBox)                    : Promise<void>
357
  public say(text: never, ...args: never[])    : never
L
lijiarui 已提交
358 359 360

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
408
    if (typeof textOrContactOrFile === 'string') {
409 410

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

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

436
  public emit(event: 'leave', leaverList:   Contact[],  remover?: Contact)                    : boolean
437 438 439 440 441 442
  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,
443
    ...args: any[]
444 445 446 447
  ): boolean {
    return super.emit(event, ...args)
  }

448 449 450 451
  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 已提交
452 453 454 455 456 457 458 459 460 461

   /**
    * @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 已提交
462
  /**
L
lijiarui 已提交
463 464 465 466 467
   * @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 已提交
468
   */
L
lijiarui 已提交
469

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
515
    super.on(event, listener) // Room is `Sayable`
516
    return this
517 518
  }

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

H
hcz 已提交
548
  /**
L
lijiarui 已提交
549 550
   * Delete a contact from the room
   * It works only when the bot is the owner of the room
551 552 553 554 555 556
   *
   * > 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 已提交
557
   * @param {Contact} contact
L
lijiarui 已提交
558
   * @returns {Promise<void>}
L
lijiarui 已提交
559
   * @example
L
lijiarui 已提交
560 561 562 563 564
   * 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 已提交
565
   * if (room) {
L
lijiarui 已提交
566 567 568 569
   *   try {
   *      await room.del(contact)
   *   } catch(e) {
   *      console.error(e)
L
lijiarui 已提交
570 571
   *   }
   * }
H
hcz 已提交
572
   */
573 574
  public async del(contact: Contact): Promise<void> {
    log.verbose('Room', 'del(%s)', contact)
575
    await this.puppet.roomDel(this.id, contact.id)
576
    // this.delLocal(contact)
577 578
  }

579 580
  // private delLocal(contact: Contact): void {
  //   log.verbose('Room', 'delLocal(%s)', contact)
581

582 583 584 585 586 587 588 589 590 591
  //   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 (李卓桓) 已提交
592

H
hcz 已提交
593
  /**
L
lijiarui 已提交
594 595
   * Bot quit the room itself
   *
596 597 598
   * > 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 已提交
599 600 601
   * @returns {Promise<void>}
   * @example
   * await room.quit()
H
hcz 已提交
602
   */
603 604
  public async quit(): Promise<void> {
    log.verbose('Room', 'quit() %s', this)
605
    await this.puppet.roomQuit(this.id)
606
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
607

608 609
  public async topic()                : Promise<string>
  public async topic(newTopic: string): Promise<void>
610

L
lijiarui 已提交
611 612 613 614
  /**
   * SET/GET topic from the room
   *
   * @param {string} [newTopic] If set this para, it will change room topic.
615
   * @returns {Promise<string | void>}
L
lijiarui 已提交
616 617
   *
   * @example <caption>When you say anything in a room, it will get room topic. </caption>
L
lijiarui 已提交
618
   * const bot = new Wechaty()
L
lijiarui 已提交
619 620 621 622
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
623
   *     const topic = await room.topic()
L
lijiarui 已提交
624 625 626
   *     console.log(`room topic is : ${topic}`)
   *   }
   * })
L
lijiarui 已提交
627
   * .start()
L
lijiarui 已提交
628 629
   *
   * @example <caption>When you say anything in a room, it will change room topic. </caption>
L
lijiarui 已提交
630
   * const bot = new Wechaty()
L
lijiarui 已提交
631 632 633 634
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
L
lijiarui 已提交
635 636
   *     const oldTopic = await room.topic()
   *     await room.topic('change topic to wechaty!')
L
lijiarui 已提交
637 638 639
   *     console.log(`room topic change from ${oldTopic} to ${room.topic()}`)
   *   }
   * })
L
lijiarui 已提交
640
   * .start()
L
lijiarui 已提交
641
   */
642
  public async topic(newTopic?: string): Promise<void | string> {
643 644 645
    log.verbose('Room', 'topic(%s)', newTopic ? newTopic : '')
    if (!this.isReady()) {
      log.warn('Room', 'topic() room not ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
646
      throw new Error('not ready')
647 648 649
    }

    if (typeof newTopic === 'undefined') {
650 651 652 653 654 655 656 657
      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))

658
        let defaultTopic = memberList[0] && memberList[0].name() || ''
659 660 661 662 663
        for (let i = 1; i < 3 && memberList[i]; i++) {
          defaultTopic += ',' + memberList[i].name()
        }
        return defaultTopic
      }
664 665 666
    }

    const future = this.puppet
667
        .roomTopic(this.id, newTopic)
668 669 670 671 672 673 674 675 676
        .catch(e => {
          log.warn('Room', 'topic(newTopic=%s) exception: %s',
                            newTopic, e && e.message || e,
                  )
          Raven.captureException(e)
        })

    return future
  }
677

678 679 680
  public async announce()             : Promise<string>
  public async announce(text: string) : Promise<void>

L
lijiarui 已提交
681 682 683
  /**
   * SET/GET announce from the room
   * > Tips: It only works when bot is the owner of the room.
684 685
   * >
   * > 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 已提交
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706
   *
   * @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()}`)
   */
707 708 709 710 711 712 713 714 715 716
  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 (李卓桓) 已提交
717
  /**
L
lijiarui 已提交
718
   * Get QR Code of the Room from the room, which can be used as scan and join the room.
719 720
   * > 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 已提交
721
   * @returns {Promise<string>}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
722
   */
723 724
  public async qrcode(): Promise<string> {
    log.verbose('Room', 'qrcode()')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
725 726
    const qrcode = await this.puppet.roomQrcode(this.id)
    return qrcode
727 728
  }

L
lijiarui 已提交
729
  /**
L
lijiarui 已提交
730
   * Return contact's roomAlias in the room, the same as roomAlias
L
lijiarui 已提交
731
   * @param {Contact} contact
L
lijiarui 已提交
732
   * @returns {Promise<string | null>} - If a contact has an alias in room, return string, otherwise return null
L
lijiarui 已提交
733
   * @example
L
lijiarui 已提交
734
   * const bot = new Wechaty()
L
lijiarui 已提交
735 736 737 738 739
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   const contact = m.from()
   *   if (room) {
L
lijiarui 已提交
740
   *     const alias = await room.alias(contact)
L
lijiarui 已提交
741 742 743
   *     console.log(`${contact.name()} alias is ${alias}`)
   *   }
   * })
L
lijiarui 已提交
744
   * .start()
L
lijiarui 已提交
745
   */
746
  public async alias(contact: Contact): Promise<null | string> {
747 748
    return this.roomAlias(contact)
  }
749

H
hcz 已提交
750
  /**
L
lijiarui 已提交
751 752
   * Same as function alias
   * @param {Contact} contact
L
lijiarui 已提交
753
   * @returns {Promise<string | null>}
H
hcz 已提交
754
   */
755
  public async roomAlias(contact: Contact): Promise<null | string> {
756

757
    const memberPayload = await this.puppet.roomMemberPayload(this.id, contact.id)
758 759 760

    if (memberPayload && memberPayload.roomAlias) {
      return memberPayload.roomAlias
761
    }
762 763

    return null
764
  }
765

H
hcz 已提交
766
  /**
767
   * Check if the room has member `contact`, the return is a Promise and must be `await`-ed
L
lijiarui 已提交
768 769
   *
   * @param {Contact} contact
L
lijiarui 已提交
770
   * @returns {Promise<boolean>} Return `true` if has contact, else return `false`.
L
lijiarui 已提交
771
   * @example <caption>Check whether 'lijiarui' is in the room 'wechaty'</caption>
L
lijiarui 已提交
772 773 774 775 776
   * 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 已提交
777
   * if (contact && room) {
778
   *   if (await room.has(contact)) {
L
lijiarui 已提交
779
   *     console.log(`${contact.name()} is in the room wechaty!`)
L
lijiarui 已提交
780
   *   } else {
L
lijiarui 已提交
781
   *     console.log(`${contact.name()} is not in the room wechaty!`)
L
lijiarui 已提交
782 783
   *   }
   * }
H
hcz 已提交
784
   */
785 786 787 788
  public async has(contact: Contact): Promise<boolean> {
    const memberIdList = await this.puppet.roomMemberList(this.id)

    if (!memberIdList) {
789 790
      return false
    }
791 792 793 794

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

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

L
lijiarui 已提交
800 801 802
  /**
   * The way to search member by Room.member()
   *
L
lijiarui 已提交
803
   * @typedef    RoomMemberQueryFilter
L
lijiarui 已提交
804 805 806 807 808 809 810 811 812 813 814
   * @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 (李卓桓) 已提交
815
   * - `roomAlias`            the name-string set by user-self in the room, should be called roomAlias
L
lijiarui 已提交
816
   * - `contactAlias`         the name-string set by bot for others, should be called alias, equal to `Contact.alias()`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
817
   * @param {(RoomMemberQueryFilter | string)} query -When use memberAll(name:string), return all matched members, including name, roomAlias, contactAlias
L
lijiarui 已提交
818
   * @returns {Promise<Contact[]>}
L
lijiarui 已提交
819
   */
820
  public async memberAll(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
821
    query: string | RoomMemberQueryFilter,
822 823
  ): Promise<Contact[]> {
    log.silly('Room', 'memberAll(%s)',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
824
                      JSON.stringify(query),
825 826
              )

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
827 828 829 830
    const contactIdList = await this.puppet.roomMemberSearch(this.id, query)
    const contactList   = contactIdList.map(id => this.wechaty.Contact.load(id))

    return contactList
831
  }
832

833 834
  public async member(name  : string)               : Promise<null | Contact>
  public async member(filter: RoomMemberQueryFilter): Promise<null | Contact>
835

L
lijiarui 已提交
836 837 838
  /**
   * Find all contacts in a room, if get many, return the first one.
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
839
   * @param {(RoomMemberQueryFilter | string)} queryArg -When use member(name:string), return all matched members, including name, roomAlias, contactAlias
L
lijiarui 已提交
840
   * @returns {Promise<null | Contact>}
L
lijiarui 已提交
841 842
   *
   * @example <caption>Find member by name</caption>
L
lijiarui 已提交
843 844 845 846
   * 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 已提交
847
   * if (room) {
L
lijiarui 已提交
848
   *   const member = await room.member('lijiarui')             // change 'lijiarui' to any room member in your wechat
L
lijiarui 已提交
849
   *   if (member) {
L
lijiarui 已提交
850
   *     console.log(`wechaty room got the member: ${member.name()}`)
L
lijiarui 已提交
851
   *   } else {
L
lijiarui 已提交
852
   *     console.log(`cannot get member in wechaty room!`)
L
lijiarui 已提交
853 854 855 856
   *   }
   * }
   *
   * @example <caption>Find member by MemberQueryFilter</caption>
L
lijiarui 已提交
857 858 859 860
   * 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 已提交
861
   * if (room) {
L
lijiarui 已提交
862
   *   const member = await room.member({name: 'lijiarui'})        // change 'lijiarui' to any room member in your wechat
L
lijiarui 已提交
863
   *   if (member) {
L
lijiarui 已提交
864
   *     console.log(`wechaty room got the member: ${member.name()}`)
L
lijiarui 已提交
865
   *   } else {
L
lijiarui 已提交
866
   *     console.log(`cannot get member in wechaty room!`)
L
lijiarui 已提交
867 868 869
   *   }
   * }
   */
870
  public async member(
871
    queryArg: string | RoomMemberQueryFilter,
872
  ): Promise<null | Contact> {
873 874 875 876 877 878
    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') {
879
      memberList =  await this.memberAll(queryArg)
880
    } else {
881
      memberList =  await this.memberAll(queryArg)
882 883 884 885 886 887 888 889 890 891 892
    }

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

H
hcz 已提交
894
  /**
L
lijiarui 已提交
895 896
   * Get all room member from the room
   *
L
lijiarui 已提交
897 898 899
   * @returns {Promise<Contact[]>}
   * @example
   * await room.memberList()
H
hcz 已提交
900
   */
901 902 903 904
  public async memberList(): Promise<Contact[]> {
    log.verbose('Room', 'memberList()')

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

906
    if (!memberIdList) {
907 908 909
      log.warn('Room', 'memberList() not ready')
      return []
    }
910 911 912 913

    const contactList = memberIdList.map(
      id => this.wechaty.Contact.load(id),
    )
914
    return contactList
915
  }
916

L
lijiarui 已提交
917
  /**
L
lijiarui 已提交
918
   * @ignore
L
lijiarui 已提交
919
   */
920 921 922
  public async refresh(): Promise<void> {
    return this.sync()
  }
L
lijiarui 已提交
923

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
924
  /**
L
lijiarui 已提交
925
   * Force reload data for Room, Sync data for Room
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
926 927
   *
   * @returns {Promise<void>}
L
lijiarui 已提交
928 929
   * @example
   * await room.sync()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
930
   */
931
  public async sync(): Promise<void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
932
    await this.ready(true)
933
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
934

L
lijiarui 已提交
935 936
  /**
   * Get room's owner from the room.
937 938
   * > 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 已提交
939
   * @returns {(Contact | null)}
L
lijiarui 已提交
940 941
   * @example
   * const owner = room.owner()
L
lijiarui 已提交
942
   */
943 944 945
  public owner(): Contact | null {
    log.info('Room', 'owner()')

946 947 948 949 950
    const ownerId = this.payload && this.payload.ownerId
    if (!ownerId) {
      return null
    }

951
    const owner = this.wechaty.Contact.load(ownerId)
952
    return owner
953
  }
L
lijiarui 已提交
954

955 956 957 958 959 960
  public async avatar(): Promise<FileBox> {
    log.verbose('Room', 'avatar()')

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
961
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
962 963

export default Room