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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

152
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
153 154 155 156 157
      let ttl = 7
      while (ttl--) {
        const roomRawObj = await contactGetter(this.id) as RoomRawObj

        const currNum = roomRawObj.MemberList && roomRawObj.MemberList.length || 0
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
158
        const prevNum = this.rawObj && this.rawObj.MemberList && this.rawObj.MemberList.length || 0
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176

        log.silly('Room', `ready() contactGetter(%s) MemberList.length:%d at ttl:%d`,
          this.id,
          currNum,
          ttl,
        )

        if (currNum) {
          if (prevNum === currNum) {
            break
          }
          this.rawObj = roomRawObj
        }

        log.verbose('Room', `ready() contactGetter(${this.id}) retry at ttl:%d`, ttl)
        await new Promise(r => setTimeout(r, 1000)) // wait for 1 second
      }

177
      await this.readyAllMembers(this.rawObj.MemberList || [])
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
178
      this.obj = this.parse(this.rawObj)
179 180 181
      if (!this.obj) {
        throw new Error('no this.obj set after contactGetter')
      }
182
      await Promise.all(this.obj.memberList.map(c => c.ready(contactGetter)))
183

184
      return this
185

186
    } catch (e) {
187
      log.error('Room', 'contactGetter(%s) exception: %s', this.id, e.message)
188
      Raven.captureException(e)
189
      throw e
190
    }
191 192
  }

L
lijiarui 已提交
193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253
  public say(mediaMessage: MediaMessage)

  public say(content: string)

  public say(content: string, replyTo: Contact)

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

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

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

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

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

    m.room(this)

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

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

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

258
  public on(event: 'topic', listener: (this: Room, topic: string, oldTopic: string, changer: Contact) => void): this
L
lijiarui 已提交
259 260 261 262 263 264 265 266 267 268 269 270

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

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

H
hcz 已提交
271
  /**
L
lijiarui 已提交
272 273 274 275 276
   * @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 已提交
277
   */
L
lijiarui 已提交
278

H
hcz 已提交
279
  /**
L
lijiarui 已提交
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
   * @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 已提交
311
   */
312
  public on(event: RoomEventName, listener: (...args: any[]) => any): this {
313
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
314

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
315
    super.on(event, listener) // Room is `Sayable`
316
    return this
317 318
  }

L
lijiarui 已提交
319 320 321
  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
322
  public get(prop): string { return (this.obj && this.obj[prop]) || (this.dirtyObj && this.dirtyObj[prop]) }
323

H
hcz 已提交
324 325 326
  /**
   * @private
   */
327
  private parse(rawObj: RoomRawObj): RoomObj | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
328
    if (!rawObj) {
329
      log.warn('Room', 'parse() on a empty rawObj?')
330
      return null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
331
    }
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
332

333 334 335
    const memberList = (rawObj.MemberList || [])
                        .map(m => Contact.load(m.UserName))

L
lijiarui 已提交
336
    const nameMap    = this.parseMap('name', rawObj.MemberList)
337 338
    const roomAliasMap   = this.parseMap('roomAlias', rawObj.MemberList)
    const contactAliasMap   = this.parseMap('contactAlias', rawObj.MemberList)
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
339

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
340
    return {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
341 342 343 344 345
      id:         rawObj.UserName,
      encryId:    rawObj.EncryChatRoomId, // ???
      topic:      rawObj.NickName,
      ownerUin:   rawObj.OwnerUin,
      memberList,
346
      nameMap,
347 348
      roomAliasMap,
      contactAliasMap,
349 350 351
    }
  }

L
lijiarui 已提交
352 353 354
  /**
   * @private
   */
L
lijiarui 已提交
355
  private parseMap(parseContent: NameType, memberList?: RoomRawMember[]): Map<string, string> {
356
    const mapList: Map<string, string> = new Map<string, string>()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
357
    if (memberList && memberList.map) {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
358
      memberList.forEach(member => {
359
        let tmpName: string
360
        const contact = Contact.load(member.UserName)
361
        switch (parseContent) {
ruiruibupt's avatar
2  
ruiruibupt 已提交
362
          case 'name':
363
            tmpName = contact.name()
364
            break
365
          case 'roomAlias':
L
lijiarui 已提交
366
            tmpName = member.DisplayName
367
            break
368 369 370
          case 'contactAlias':
            tmpName = contact.alias() || ''
            break
371 372 373
          default:
            throw new Error('parseMap failed, member not found')
        }
374 375
        /**
         * ISSUE #64 emoji need to be striped
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
376
         * ISSUE #104 never use remark name because sys group message will never use that
ruiruibupt's avatar
#217  
ruiruibupt 已提交
377
         * @rui: Wrong for 'never use remark name because sys group message will never use that', see more in the latest comment in #104
ruiruibupt's avatar
1  
ruiruibupt 已提交
378 379
         * @rui: webwx's NickName here return contactAlias, if not set contactAlias, return name
         * @rui: 2017-7-2 webwx's NickName just ruturn name, no contactAlias
380
         */
381
        mapList[member.UserName] = Misc.stripEmoji(tmpName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
382
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
383
    }
384
    return mapList
385 386
  }

L
lijiarui 已提交
387 388 389
  /**
   * @private
   */
390
  public dumpRaw() {
391
    console.error('======= dump raw Room =======')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
392
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
393
  }
L
lijiarui 已提交
394 395 396 397

  /**
   * @private
   */
398
  public dump() {
399
    console.error('======= dump Room =======')
400 401 402
    if (!this.obj) {
      throw new Error('no this.obj')
    }
403
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj && this.obj[k]}`))
404 405
  }

H
hcz 已提交
406
  /**
L
lijiarui 已提交
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
   * 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 已提交
422
   */
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
423
  public async add(contact: Contact): Promise<number> {
424
    log.verbose('Room', 'add(%s)', contact)
425 426 427 428 429

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

430
    const n = config.puppetInstance()
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
431 432
                      .roomAdd(this, contact)
    return n
433 434
  }

H
hcz 已提交
435
  /**
L
lijiarui 已提交
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
   * 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 已提交
451
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452
  public async del(contact: Contact): Promise<number> {
453
    log.verbose('Room', 'del(%s)', contact.name())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
454 455 456 457

    if (!contact) {
      throw new Error('contact not found')
    }
458
    const n = await config.puppetInstance()
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
459 460
                            .roomDel(this, contact)
                            .then(_ => this.delLocal(contact))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
461
    return n
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
462 463
  }

H
hcz 已提交
464
  /**
L
lijiarui 已提交
465
   * @private
H
hcz 已提交
466
   */
467
  private delLocal(contact: Contact): number {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
468 469
    log.verbose('Room', 'delLocal(%s)', contact)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
470
    const memberList = this.obj && this.obj.memberList
471
    if (!memberList || memberList.length === 0) {
472
      return 0 // already in refreshing
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
473 474 475
    }

    let i
476 477
    for (i = 0; i < memberList.length; i++) {
      if (memberList[i].id === contact.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
478 479 480
        break
      }
    }
481 482
    if (i < memberList.length) {
      memberList.splice(i, 1)
483
      return 1
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
484
    }
485
    return 0
486
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
487

L
lijiarui 已提交
488 489 490
  /**
   * @private
   */
491
  public quit() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
492 493
    throw new Error('wx web not implement yet')
    // WechatyBro.glue.chatroomFactory.quit("@@1c066dfcab4ef467cd0a8da8bec90880035aa46526c44f504a83172a9086a5f7"
494
  }
495

496
  public topic(): string
L
lijiarui 已提交
497

498 499
  public topic(newTopic: string): void

L
lijiarui 已提交
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528
  /**
   * 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()}`)
   *   }
   * })
   */
529
  public topic(newTopic?: string): string | void {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
530
    log.verbose('Room', 'topic(%s)', newTopic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
531
    if (!this.isReady()) {
532
      log.warn('Room', 'topic() room not ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
533 534
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
535 536
    if (typeof newTopic === 'undefined') {
      return Misc.plainText(this.obj ? this.obj.topic : '')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
537
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
538 539 540 541 542 543 544 545 546 547 548 549 550 551 552

    config.puppetInstance()
          .roomTopic(this, newTopic)
          .catch(e => {
            log.warn('Room', 'topic(newTopic=%s) exception: %s',
                              newTopic, e && e.message || e,
                    )
            Raven.captureException(e)
          })

    if (!this.obj) {
      this.obj = <RoomObj>{}
    }
    this.obj['topic'] = newTopic
    return
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
553 554
  }

555 556
  /**
   * should be deprecated
L
lijiarui 已提交
557
   * @private
558
   */
ruiruibupt's avatar
2  
ruiruibupt 已提交
559
  public nick(contact: Contact): string | null {
ruiruibupt's avatar
1  
ruiruibupt 已提交
560
    log.warn('Room', 'nick(Contact) DEPRECATED, use alias(Contact) instead.')
ruiruibupt's avatar
#217  
ruiruibupt 已提交
561
    return this.alias(contact)
562 563
  }

L
lijiarui 已提交
564
  /**
L
lijiarui 已提交
565
   * Return contact's roomAlias in the room, the same as roomAlias
L
lijiarui 已提交
566
   * @param {Contact} contact
L
lijiarui 已提交
567 568 569 570 571 572 573 574 575 576 577 578
   * @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 已提交
579
   */
ruiruibupt's avatar
2  
ruiruibupt 已提交
580
  public alias(contact: Contact): string | null {
581 582 583
    return this.roomAlias(contact)
  }

H
hcz 已提交
584
  /**
L
lijiarui 已提交
585 586 587
   * Same as function alias
   * @param {Contact} contact
   * @returns {(string | null)}
H
hcz 已提交
588
   */
589 590
  public roomAlias(contact: Contact): string | null {
    if (!this.obj || !this.obj.roomAliasMap) {
ruiruibupt's avatar
2  
ruiruibupt 已提交
591
      return null
592
    }
593
    return this.obj.roomAliasMap[contact.id] || null
594 595
  }

H
hcz 已提交
596
  /**
L
lijiarui 已提交
597 598 599 600 601 602 603 604 605 606 607 608 609 610
   * 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 已提交
611
   */
612
  public has(contact: Contact): boolean {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
613
    if (!this.obj || !this.obj.memberList) {
614 615 616 617 618 619 620
      return false
    }
    return this.obj.memberList
                    .filter(c => c.id === contact.id)
                    .length > 0
  }

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

623
  public memberAll(name: string): Contact[]
624

L
lijiarui 已提交
625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646
  /**
   * The way to search member by Room.member()
   *
   * @typedef    MemberQueryFilter
   * @property   {string} name            -Find the contact by wechat name in a room, equal to `Contact.name()`.
   * @property   {string} alias           -Find the contact by alias set by the bot for others in a room, equal to `roomAlias`.
   * @property   {string} roomAlias       -Find the contact by alias set by the bot for others in a room.
   * @property   {string} contactAlias    -Find the contact by alias set by the contact out of a room, equal to `Contact.alias()`.
   * [More Detail]{@link https://github.com/Chatie/wechaty/issues/365}
   */

  /**
   * Find all contacts in a room
   *
   * #### definition
   * - `name`                 the name-string set by user-self, should be called name, equal to `Contact.name()`
   * - `roomAlias` | `alias`  the name-string set by user-self in the room, should be called roomAlias
   * - `contactAlias`         the name-string set by bot for others, should be called alias, equal to `Contact.alias()`
   * @param {(MemberQueryFilter | string)} queryArg -When use memberAll(name:string), return all matched members, including name, roomAlias, contactAlias
   * @returns {Contact[]}
   * @memberof Room
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
647
  public memberAll(queryArg: MemberQueryFilter | string): Contact[] {
648
    if (typeof queryArg === 'string') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667
      //
      // use the following `return` statement to do this job.
      //

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

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

      return ([] as Contact[]).concat(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
668 669
        this.memberAll({name:         queryArg}),
        this.memberAll({roomAlias:    queryArg}),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
670 671
        this.memberAll({contactAlias: queryArg}),
      )
672 673
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
674 675 676
    /**
     * We got filter parameter
     */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
677
    log.silly('Room', 'memberAll({ %s })',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
678 679 680
                      Object.keys(queryArg)
                            .map(k => `${k}: ${queryArg[k]}`)
                            .join(', '),
681 682 683
            )

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
687
    if (!this.obj || !this.obj.memberList) {
688
      log.warn('Room', 'member() not ready')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
689
      return []
690
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
691
    const filterKey            = Object.keys(queryArg)[0]
692 693 694
    /**
     * ISSUE #64 emoji need to be striped
     */
695
    const filterValue: string  = Misc.stripEmoji(Misc.plainText(queryArg[filterKey]))
696 697

    const keyMap = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
698
      contactAlias: 'contactAliasMap',
699 700
      name:         'nameMap',
      alias:        'roomAliasMap',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
701
      roomAlias:    'roomAliasMap',
702 703
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
704 705 706
    const filterMapName = keyMap[filterKey]
    if (!filterMapName) {
      throw new Error('unsupport filter key: ' + filterKey)
707 708 709 710 711
    }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
713
    const filterMap = this.obj[filterMapName]
714
    const idList = Object.keys(filterMap)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
715
                          .filter(id => filterMap[id] === filterValue)
716

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

719
    if (idList.length) {
720
      return idList.map(id => Contact.load(id))
721
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
722
      return []
723
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
724 725
  }

726
  public member(name: string): Contact | null
L
lijiarui 已提交
727

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

L
lijiarui 已提交
730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757
  /**
   * 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()}`)
   *   }
   * }
   */
758 759 760
  public member(queryArg: MemberQueryFilter | string): Contact | null {
    log.verbose('Room', 'member(%s)', JSON.stringify(queryArg))

761 762 763 764 765 766 767 768 769
    let memberList: Contact[]
    // ISSUE #622
    // error TS2345: Argument of type 'string | MemberQueryFilter' is not assignable to parameter of type 'MemberQueryFilter' #622
    if (typeof queryArg === 'string') {
      memberList =  this.memberAll(queryArg)
    } else {
      memberList =  this.memberAll(queryArg)
    }

770 771 772 773 774
    if (!memberList || !memberList.length) {
      return null
    }

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

H
hcz 已提交
780
  /**
L
lijiarui 已提交
781 782 783
   * Get all room member from the room
   *
   * @returns {Contact[]}
H
hcz 已提交
784
   */
785
  public memberList(): Contact[] {
786
    log.verbose('Room', 'memberList')
787 788 789

    if (!this.obj || !this.obj.memberList || this.obj.memberList.length < 1) {
      log.warn('Room', 'memberList() not ready')
790 791 792 793
      log.verbose('Room', 'memberList() trying call refresh() to update')
      this.refresh().then(() => {
        log.verbose('Room', 'memberList() refresh() done')
      })
794
      return []
795 796 797 798
    }
    return this.obj.memberList
  }

H
hcz 已提交
799
  /**
L
lijiarui 已提交
800 801 802 803 804 805 806 807 808 809 810 811 812 813 814
   * 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 已提交
815
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
816
  public static create(contactList: Contact[], topic?: string): Promise<Room> {
817
    log.verbose('Room', 'create(%s, %s)', contactList.join(','), topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
818

Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
819
    if (!contactList || !Array.isArray(contactList)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
820 821
      throw new Error('contactList not found')
    }
822

823
    return config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
824
                  .roomCreate(contactList, topic)
825 826
                  .catch(e => {
                    log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
827
                    Raven.captureException(e)
828
                    throw e
829
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
830 831
  }

H
hcz 已提交
832
  /**
L
lijiarui 已提交
833 834 835 836 837 838 839 840
   * 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 已提交
841
   */
842 843 844 845
  public static async findAll(query?: RoomQueryFilter): Promise<Room[]> {
    if (!query) {
      query = { topic: /.*/ }
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
846
    log.verbose('Room', 'findAll({ topic: %s })', query.topic)
847

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

850 851
    if (!topicFilter) {
      throw new Error('topicFilter not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
852 853
    }

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

856
    if (topicFilter instanceof RegExp) {
857
      filterFunction = `(function (c) { return ${topicFilter.toString()}.test(c) })`
858
    } else if (typeof topicFilter === 'string') {
859
      topicFilter = topicFilter.replace(/'/g, '\\\'')
860
      filterFunction = `(function (c) { return c === '${topicFilter}' })`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
861
    } else {
862
      throw new Error('unsupport topic type')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
863 864
    }

865
    const roomList = await config.puppetInstance()
866 867 868
                                  .roomFind(filterFunction)
                                  .catch(e => {
                                    log.verbose('Room', 'findAll() rejected: %s', e.message)
869
                                    Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
870
                                    return [] as Room[] // fail safe
871 872
                                  })

873 874 875 876
    await Promise.all(roomList.map(room => room.ready()))
    // for (let i = 0; i < roomList.length; i++) {
    //   await roomList[i].ready()
    // }
877 878

    return roomList
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
879 880
  }

881
  /**
L
lijiarui 已提交
882 883
   * Try to find a room by filter: {topic: string | RegExp}. If get many, return the first one.
   *
884 885 886 887
   * @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> {
888
    log.verbose('Room', 'find({ topic: %s })', query.topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
889

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
890 891
    const roomList = await Room.findAll(query)
    if (!roomList || roomList.length < 1) {
892
      return null
893 894
    } else if (roomList.length > 1) {
      log.warn('Room', 'find() got more than one result, return the 1st one.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
895
    }
896
    return roomList[0]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
897 898
  }

L
lijiarui 已提交
899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936
  /**
   * Force reload data for Room
   *
   * @returns {Promise<void>}
   */
  public async refresh(): Promise<void> {
    if (this.isReady()) {
      this.dirtyObj = this.obj
    }
    this.obj = null
    await this.ready()
    return
  }

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

    const user = config.puppetInstance()
                      .user

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

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

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
937
  /**
L
lijiarui 已提交
938
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
939
   */
940 941 942 943
  public static load(id: string): Room {
    if (!id) {
      throw new Error('Room.load() no id')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
944 945 946 947 948 949 950

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
951
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
952 953

export default Room