room.ts 16.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
import {
21
  // config,
22
  Raven,
L
lijiarui 已提交
23 24
  Sayable,
  log,
25 26
}                       from './config'
import Contact          from './contact'
M
Mukaiu 已提交
27 28
import {
  MediaMessage,
29 30
}                       from './message'
import PuppetAccessory  from './puppet-accessory'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
31

32 33 34
export type RoomEventName = 'join'
                          | 'leave'
                          | 'topic'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
35
                          | never
36

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
37
export type MemberQueryNameType = 'name' | 'roomAlias' | 'contactAlias'
38

39
export interface MemberQueryFilter {
40 41 42
  name?:         string,
  roomAlias?:    string,
  contactAlias?: string,
43 44
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
45 46 47 48
export interface RoomQueryFilter {
  topic: string | RegExp,
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
49
/**
L
lijiarui 已提交
50
 * All wechat rooms(groups) will be encapsulated as a Room.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
51
 *
L
lijiarui 已提交
52
 * `Room` is `Sayable`,
53
 * [Examples/Room-Bot]{@link https://github.com/Chatie/wechaty/blob/master/examples/room-bot.ts}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
54
 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
55
export abstract class Room extends PuppetAccessory implements Sayable {
56 57
  private static pool = new Map<string, Room>()

H
hcz 已提交
58 59 60
  /**
   * @private
   */
61
  constructor(public id: string) {
62
    super()
63
    log.silly('Room', `constructor(${id})`)
64
  }
65

H
hcz 已提交
66 67 68
  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
69
  public toString()    { return `@Room<${this.topic()}>` }
L
lijiarui 已提交
70 71 72 73

  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
74
  public abstract async ready(): Promise<Room>
L
lijiarui 已提交
75

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
76 77 78 79 80
  public abstract say(mediaMessage: MediaMessage)         : Promise<void>
  public abstract say(content: string)                    : Promise<void>
  public abstract say(content: string, replyTo: Contact)  : Promise<void>
  public abstract say(content: string, replyTo: Contact[]): Promise<void>
  public abstract say(content: never, ...args: never[])   : Promise<never>
L
lijiarui 已提交
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
  /**
   * Send message inside Room, if set [replyTo], wechaty will mention the contact as well.
   *
   * @param {(string | MediaMessage)} textOrMedia - Send `text` or `media file` inside Room.
   * @param {(Contact | Contact[])} [replyTo] - Optional parameter, send content inside Room, and mention @replyTo contact or contactList.
   * @returns {Promise<boolean>}
   * If bot send message successfully, it will return true. If the bot failed to send for blocking or any other reason, it will return false
   *
   * @example <caption>Send text inside Room</caption>
   * const room = await Room.find({name: 'wechaty'})        // change 'wechaty' to any of your room in wechat
   * await room.say('Hello world!')
   *
   * @example <caption>Send media file inside Room</caption>
   * const room = await Room.find({name: 'wechaty'})        // change 'wechaty' to any of your room in wechat
   * await room.say(new MediaMessage('/test.jpg'))          // put the filePath you want to send here
   *
   * @example <caption>Send text inside Room, and mention @replyTo contact</caption>
   * const contact = await Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any of the room member
   * const room = await Room.find({name: 'wechaty'})        // change 'wechaty' to any of your room in wechat
   * await room.say('Hello world!', contact)
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
102
  public abstract say(textOrMedia: string | MediaMessage, replyTo?: Contact|Contact[]): Promise<void>
L
lijiarui 已提交
103

104 105 106
  public on(event: 'leave', listener: (this: Room, leaver: 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
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
107
  public on(event: never, listener: never): never
L
lijiarui 已提交
108 109 110 111 112 113 114 115 116 117

   /**
    * @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 已提交
118
  /**
L
lijiarui 已提交
119 120 121 122 123
   * @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 已提交
124
   */
L
lijiarui 已提交
125

H
hcz 已提交
126
  /**
L
lijiarui 已提交
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
   * @listens Room
   * @param   {RoomEventName}      event      - Emit WechatyEvent
   * @param   {RoomEventFunction}  listener   - Depends on the WechatyEvent
   * @return  {this}                          - this for chain
   *
   * @example <caption>Event:join </caption>
   * const room = await Room.find({topic: 'event-room'}) // change `event-room` to any room topic in your wechat
   * if (room) {
   *   room.on('join', (room: Room, inviteeList: Contact[], inviter: Contact) => {
   *     const nameList = inviteeList.map(c => c.name()).join(',')
   *     console.log(`Room ${room.topic()} got new member ${nameList}, invited by ${inviter}`)
   *   })
   * }
   *
   * @example <caption>Event:leave </caption>
   * const room = await Room.find({topic: 'event-room'}) // change `event-room` to any room topic in your wechat
   * if (room) {
   *   room.on('leave', (room: Room, leaverList: Contact[]) => {
   *     const nameList = leaverList.map(c => c.name()).join(',')
   *     console.log(`Room ${room.topic()} lost member ${nameList}`)
   *   })
   * }
   *
   * @example <caption>Event:topic </caption>
   * const room = await Room.find({topic: 'event-room'}) // change `event-room` to any room topic in your wechat
   * if (room) {
   *   room.on('topic', (room: Room, topic: string, oldTopic: string, changer: Contact) => {
   *     console.log(`Room ${room.topic()} topic changed from ${oldTopic} to ${topic} by ${changer.name()}`)
   *   })
   * }
   *
H
hcz 已提交
158
   */
159
  public on(event: RoomEventName, listener: (...args: any[]) => any): this {
160
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
161

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
162
    super.on(event, listener) // Room is `Sayable`
163
    return this
164 165
  }

H
hcz 已提交
166
  /**
L
lijiarui 已提交
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
   * Add contact in a room
   *
   * @param {Contact} contact
   * @returns {Promise<number>}
   * @example
   * const contact = await Contact.find({name: 'lijiarui'}) // change 'lijiarui' to any contact in your wechat
   * const room = await Room.find({topic: 'wechat'})        // change 'wechat' to any room topic in your wechat
   * if (room) {
   *   const result = await room.add(contact)
   *   if (result) {
   *     console.log(`add ${contact.name()} to ${room.topic()} successfully! `)
   *   } else{
   *     console.log(`failed to add ${contact.name()} to ${room.topic()}! `)
   *   }
   * }
H
hcz 已提交
182
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
183
  public abstract async add(contact: Contact): Promise<void>
184

H
hcz 已提交
185
  /**
L
lijiarui 已提交
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
   * Delete a contact from the room
   * It works only when the bot is the owner of the room
   * @param {Contact} contact
   * @returns {Promise<number>}
   * @example
   * const room = await Room.find({topic: 'wechat'})          // change 'wechat' to any room topic in your wechat
   * const contact = await Contact.find({name: 'lijiarui'})   // change 'lijiarui' to any room member in the room you just set
   * if (room) {
   *   const result = await room.del(contact)
   *   if (result) {
   *     console.log(`remove ${contact.name()} from ${room.topic()} successfully! `)
   *   } else{
   *     console.log(`failed to remove ${contact.name()} from ${room.topic()}! `)
   *   }
   * }
H
hcz 已提交
201
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
202
  public abstract async del(contact: Contact): Promise<void>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
203

H
hcz 已提交
204
  /**
L
lijiarui 已提交
205
   * @private
H
hcz 已提交
206
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
207
  public abstract quit(): Promise<void>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
208

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
209 210
  public abstract topic(): string
  public abstract topic(newTopic: string): Promise<void>
211

L
lijiarui 已提交
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
  /**
   * SET/GET topic from the room
   *
   * @param {string} [newTopic] If set this para, it will change room topic.
   * @returns {(string | void)}
   *
   * @example <caption>When you say anything in a room, it will get room topic. </caption>
   * const bot = Wechaty.instance()
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
   *     const topic = room.topic()
   *     console.log(`room topic is : ${topic}`)
   *   }
   * })
   *
   * @example <caption>When you say anything in a room, it will change room topic. </caption>
   * const bot = Wechaty.instance()
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   if (room) {
   *     const oldTopic = room.topic()
   *     room.topic('change topic to wechaty!')
   *     console.log(`room topic change from ${oldTopic} to ${room.topic()}`)
   *   }
   * })
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
241
  public abstract topic(newTopic?: string): string | Promise<void>
242

L
lijiarui 已提交
243
  /**
L
lijiarui 已提交
244
   * Return contact's roomAlias in the room, the same as roomAlias
L
lijiarui 已提交
245
   * @param {Contact} contact
L
lijiarui 已提交
246 247 248 249 250 251 252 253 254 255 256 257
   * @returns {string | null} - If a contact has an alias in room, return string, otherwise return null
   * @example
   * const bot = Wechaty.instance()
   * bot
   * .on('message', async m => {
   *   const room = m.room()
   *   const contact = m.from()
   *   if (room) {
   *     const alias = room.alias(contact)
   *     console.log(`${contact.name()} alias is ${alias}`)
   *   }
   * })
L
lijiarui 已提交
258
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
259
  public abstract alias(contact: Contact): string | null
260

H
hcz 已提交
261
  /**
L
lijiarui 已提交
262 263 264
   * Same as function alias
   * @param {Contact} contact
   * @returns {(string | null)}
H
hcz 已提交
265
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
266
  public abstract roomAlias(contact: Contact): string | null
267

H
hcz 已提交
268
  /**
L
lijiarui 已提交
269 270 271 272 273 274 275 276 277 278 279 280 281 282
   * Check if the room has member `contact`.
   *
   * @param {Contact} contact
   * @returns {boolean} Return `true` if has contact, else return `false`.
   * @example <caption>Check whether 'lijiarui' is in the room 'wechaty'</caption>
   * const contact = await Contact.find({name: 'lijiarui'})   // change 'lijiarui' to any of contact in your wechat
   * const room = await Room.find({topic: 'wechaty'})         // change 'wechaty' to any of the room in your wechat
   * if (contact && room) {
   *   if (room.has(contact)) {
   *     console.log(`${contact.name()} is in the room ${room.topic()}!`)
   *   } else {
   *     console.log(`${contact.name()} is not in the room ${room.topic()} !`)
   *   }
   * }
H
hcz 已提交
283
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
284
  public abstract has(contact: Contact): boolean
L
lijiarui 已提交
285

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
286 287
  public abstract memberAll(filter: MemberQueryFilter): Contact[]
  public abstract memberAll(name: string): Contact[]
288

L
lijiarui 已提交
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
  /**
   * The way to search member by Room.member()
   *
   * @typedef    MemberQueryFilter
   * @property   {string} name            -Find the contact by wechat name in a room, equal to `Contact.name()`.
   * @property   {string} 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 (李卓桓) 已提交
304
   * - `roomAlias`            the name-string set by user-self in the room, should be called roomAlias
L
lijiarui 已提交
305 306 307 308 309
   * - `contactAlias`         the name-string set by bot for others, should be called alias, equal to `Contact.alias()`
   * @param {(MemberQueryFilter | string)} queryArg -When use memberAll(name:string), return all matched members, including name, roomAlias, contactAlias
   * @returns {Contact[]}
   * @memberof Room
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
310
  public abstract memberAll(queryArg: MemberQueryFilter | string): Contact[]
311

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
312 313
  public abstract member(name: string): Contact | null
  public abstract member(filter: MemberQueryFilter): Contact | null
314

L
lijiarui 已提交
315 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 342
  /**
   * Find all contacts in a room, if get many, return the first one.
   *
   * @param {(MemberQueryFilter | string)} queryArg -When use member(name:string), return all matched members, including name, roomAlias, contactAlias
   * @returns {(Contact | null)}
   *
   * @example <caption>Find member by name</caption>
   * const room = await Room.find({topic: 'wechaty'})           // change 'wechaty' to any room name in your wechat
   * if (room) {
   *   const member = room.member('lijiarui')                   // change 'lijiarui' to any room member in your wechat
   *   if (member) {
   *     console.log(`${room.topic()} got the member: ${member.name()}`)
   *   } else {
   *     console.log(`cannot get member in room: ${room.topic()}`)
   *   }
   * }
   *
   * @example <caption>Find member by MemberQueryFilter</caption>
   * const room = await Room.find({topic: 'wechaty'})          // change 'wechaty' to any room name in your wechat
   * if (room) {
   *   const member = room.member({name: 'lijiarui'})          // change 'lijiarui' to any room member in your wechat
   *   if (member) {
   *     console.log(`${room.topic()} got the member: ${member.name()}`)
   *   } else {
   *     console.log(`cannot get member in room: ${room.topic()}`)
   *   }
   * }
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
343
  public abstract member(queryArg: MemberQueryFilter | string): Contact | null
344

H
hcz 已提交
345
  /**
L
lijiarui 已提交
346 347 348
   * Get all room member from the room
   *
   * @returns {Contact[]}
H
hcz 已提交
349
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
350
  public abstract memberList(): Contact[]
351

H
hcz 已提交
352
  /**
L
lijiarui 已提交
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
   * Create a new room.
   *
   * @static
   * @param {Contact[]} contactList
   * @param {string} [topic]
   * @returns {Promise<Room>}
   * @example <caption>Creat a room with 'lijiarui' and 'juxiaomi', the room topic is 'ding - created'</caption>
   * const helperContactA = await Contact.find({ name: 'lijiarui' })  // change 'lijiarui' to any contact in your wechat
   * const helperContactB = await Contact.find({ name: 'juxiaomi' })  // change 'juxiaomi' to any contact in your wechat
   * const contactList = [helperContactA, helperContactB]
   * console.log('Bot', 'contactList: %s', contactList.join(','))
   * const room = await Room.create(contactList, 'ding')
   * console.log('Bot', 'createDingRoom() new ding room created: %s', room)
   * await room.topic('ding - created')
   * await room.say('ding - created')
H
hcz 已提交
368
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
369
  public static async create(contactList: Contact[], topic?: string): Promise<Room> {
370
    log.verbose('Room', 'create(%s, %s)', contactList.join(','), topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
371

Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
372
    if (!contactList || !Array.isArray(contactList)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
373 374
      throw new Error('contactList not found')
    }
375

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
376 377 378 379 380 381 382 383
    try {
      const room = await this.puppet.roomCreate(contactList, topic)
      return room
    } catch (e) {
      log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
      Raven.captureException(e)
      throw e
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
384 385
  }

H
hcz 已提交
386
  /**
L
lijiarui 已提交
387 388 389 390 391 392 393 394
   * Find room by topic, return all the matched room
   *
   * @static
   * @param {RoomQueryFilter} [query]
   * @returns {Promise<Room[]>}
   * @example
   * const roomList = await Room.findAll()                    // get the room list of the bot
   * const roomList = await Room.findAll({name: 'wechaty'})   // find all of the rooms with name 'wechaty'
H
hcz 已提交
395
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
396 397 398
  public static async findAll(
    query: RoomQueryFilter = { topic: /.*/ },
  ): Promise<Room[]> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
399
    log.verbose('Room', 'findAll({ topic: %s })', query.topic)
400

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
401
    if (!query.topic) {
402
      throw new Error('topicFilter not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
403 404
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
405 406 407
    try {
      const roomList = await this.puppet.roomFindAll(query)
      await Promise.all(roomList.map(room => room.ready()))
408

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
409
      return roomList
410

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
411 412 413 414 415
    } catch (e) {
      log.verbose('Room', 'findAll() rejected: %s', e.message)
      Raven.captureException(e)
      return [] as Room[] // fail safe
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
416 417
  }

418
  /**
L
lijiarui 已提交
419 420
   * Try to find a room by filter: {topic: string | RegExp}. If get many, return the first one.
   *
421 422 423 424
   * @param {RoomQueryFilter} query
   * @returns {Promise<Room | null>} If can find the room, return Room, or return null
   */
  public static async find(query: RoomQueryFilter): Promise<Room | null> {
425
    log.verbose('Room', 'find({ topic: %s })', query.topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
426

427
    const roomList = await this.findAll(query)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
428
    if (!roomList || roomList.length < 1) {
429
      return null
430 431
    } else if (roomList.length > 1) {
      log.warn('Room', 'find() got more than one result, return the 1st one.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
432
    }
433
    return roomList[0]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
434 435
  }

L
lijiarui 已提交
436 437 438 439 440
  /**
   * Force reload data for Room
   *
   * @returns {Promise<void>}
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
441
  public abstract async refresh(): Promise<void>
L
lijiarui 已提交
442 443 444 445 446 447 448

  /**
   * @private
   * Get room's owner from the room.
   * Not recommend, because cannot always get the owner
   * @returns {(Contact | null)}
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
449
  public abstract owner(): Contact | null
L
lijiarui 已提交
450

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
451
  /**
L
lijiarui 已提交
452
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
453
   */
454 455 456 457
  public static load(id: string): Room {
    if (!id) {
      throw new Error('Room.load() no id')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
458

459 460
    if (id in this.pool) {
      return this.pool[id]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
461
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
462
    return this.pool[id] = new (this as any)(id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
463 464
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
465
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
466 467

export default Room