room.ts 30.7 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 29
import {
  Message,
  MediaMessage,
30 31 32
}                       from './message'
import Misc             from './misc'
import PuppetAccessory  from './puppet-accessory'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
33

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

45
type NameType = 'name' | 'alias' | 'roomAlias' | 'contactAlias'
46

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

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

62 63 64
export type RoomEventName = 'join'
                          | 'leave'
                          | 'topic'
65 66
                          | 'EVENT_PARAM_ERROR'

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

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

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

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

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

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

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

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

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

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

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

151
    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
152 153 154 155 156
      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 (李卓桓) 已提交
157
        const prevNum = this.rawObj && this.rawObj.MemberList && this.rawObj.MemberList.length || 0
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
158 159 160 161 162 163 164 165 166

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

        if (currNum) {
          if (prevNum === currNum) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
167
            log.verbose('Room', `ready() contactGetter(${this.id}) done at ttl:%d`, ttl)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
168 169 170 171 172
            break
          }
          this.rawObj = roomRawObj
        }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
173
        log.silly('Room', `ready() contactGetter(${this.id}) retry at ttl:%d`, ttl)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
174 175 176
        await new Promise(r => setTimeout(r, 1000)) // wait for 1 second
      }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
177
      await this.readyAllMembers(this.rawObj && 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
  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()
234
      m.puppet = this.puppet
L
lijiarui 已提交
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250

      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)

251 252
    return this.puppet // config.puppetInstance()
                .send(m)
L
lijiarui 已提交
253 254
  }

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

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

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

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

H
hcz 已提交
280
  /**
L
lijiarui 已提交
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 311
   * @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 已提交
312
   */
313
  public on(event: RoomEventName, listener: (...args: any[]) => any): this {
314
    log.verbose('Room', 'on(%s, %s)', event, typeof listener)
315

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

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

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

334
    const memberList = (rawObj.MemberList || [])
335 336 337 338 339
                        .map(m => {
                          const c = Contact.load(m.UserName)
                          c.puppet = this.puppet
                          return c
                        })
340

L
lijiarui 已提交
341
    const nameMap    = this.parseMap('name', rawObj.MemberList)
342 343
    const roomAliasMap   = this.parseMap('roomAlias', rawObj.MemberList)
    const contactAliasMap   = this.parseMap('contactAlias', rawObj.MemberList)
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
344

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

L
lijiarui 已提交
357 358 359
  /**
   * @private
   */
L
lijiarui 已提交
360
  private parseMap(parseContent: NameType, memberList?: RoomRawMember[]): Map<string, string> {
361
    const mapList: Map<string, string> = new Map<string, string>()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
362
    if (memberList && memberList.map) {
Huan (李卓桓)'s avatar
#104  
Huan (李卓桓) 已提交
363
      memberList.forEach(member => {
364
        let tmpName: string
365
        const contact = Contact.load(member.UserName)
366 367
        contact.puppet = this.puppet

368
        switch (parseContent) {
ruiruibupt's avatar
2  
ruiruibupt 已提交
369
          case 'name':
370
            tmpName = contact.name()
371
            break
372
          case 'roomAlias':
L
lijiarui 已提交
373
            tmpName = member.DisplayName
374
            break
375 376 377
          case 'contactAlias':
            tmpName = contact.alias() || ''
            break
378 379 380
          default:
            throw new Error('parseMap failed, member not found')
        }
381 382
        /**
         * ISSUE #64 emoji need to be striped
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
383
         * ISSUE #104 never use remark name because sys group message will never use that
ruiruibupt's avatar
#217  
ruiruibupt 已提交
384
         * @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 已提交
385 386
         * @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
387
         */
388
        mapList[member.UserName] = Misc.stripEmoji(tmpName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
389
      })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
390
    }
391
    return mapList
392 393
  }

L
lijiarui 已提交
394 395 396
  /**
   * @private
   */
397
  public dumpRaw() {
398
    console.error('======= dump raw Room =======')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
399
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
400
  }
L
lijiarui 已提交
401 402 403 404

  /**
   * @private
   */
405
  public dump() {
406
    console.error('======= dump Room =======')
407 408 409
    if (!this.obj) {
      throw new Error('no this.obj')
    }
410
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj && this.obj[k]}`))
411 412
  }

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

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

437 438
    const n = this.puppet // config.puppetInstance()
                  .roomAdd(this, contact)
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
439
    return n
440 441
  }

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

    if (!contact) {
      throw new Error('contact not found')
    }
465 466 467
    const n = await this.puppet // config.puppetInstance()
                        .roomDel(this, contact)
                        .then(_ => this.delLocal(contact))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
468
    return n
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
469 470
  }

H
hcz 已提交
471
  /**
L
lijiarui 已提交
472
   * @private
H
hcz 已提交
473
   */
474
  private delLocal(contact: Contact): number {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
475 476
    log.verbose('Room', 'delLocal(%s)', contact)

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
477
    const memberList = this.obj && this.obj.memberList
478
    if (!memberList || memberList.length === 0) {
479
      return 0 // already in refreshing
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
480 481 482
    }

    let i
483 484
    for (i = 0; i < memberList.length; i++) {
      if (memberList[i].id === contact.id) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
485 486 487
        break
      }
    }
488 489
    if (i < memberList.length) {
      memberList.splice(i, 1)
490
      return 1
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
491
    }
492
    return 0
493
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
494

L
lijiarui 已提交
495 496 497
  /**
   * @private
   */
498
  public quit() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
499 500
    throw new Error('wx web not implement yet')
    // WechatyBro.glue.chatroomFactory.quit("@@1c066dfcab4ef467cd0a8da8bec90880035aa46526c44f504a83172a9086a5f7"
501
  }
502

503
  public topic(): string
L
lijiarui 已提交
504

505 506
  public topic(newTopic: string): void

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
542 543
    if (typeof newTopic === 'undefined') {
      return Misc.plainText(this.obj ? this.obj.topic : '')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
544
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
545

546 547 548 549 550 551 552 553
    this.puppet // config.puppetInstance()
        .roomTopic(this, newTopic)
        .catch(e => {
          log.warn('Room', 'topic(newTopic=%s) exception: %s',
                            newTopic, e && e.message || e,
                  )
          Raven.captureException(e)
        })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
554 555 556 557 558 559

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

562 563
  /**
   * should be deprecated
L
lijiarui 已提交
564
   * @private
565
   */
ruiruibupt's avatar
2  
ruiruibupt 已提交
566
  public nick(contact: Contact): string | null {
ruiruibupt's avatar
1  
ruiruibupt 已提交
567
    log.warn('Room', 'nick(Contact) DEPRECATED, use alias(Contact) instead.')
ruiruibupt's avatar
#217  
ruiruibupt 已提交
568
    return this.alias(contact)
569 570
  }

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

H
hcz 已提交
591
  /**
L
lijiarui 已提交
592 593 594
   * Same as function alias
   * @param {Contact} contact
   * @returns {(string | null)}
H
hcz 已提交
595
   */
596 597
  public roomAlias(contact: Contact): string | null {
    if (!this.obj || !this.obj.roomAliasMap) {
ruiruibupt's avatar
2  
ruiruibupt 已提交
598
      return null
599
    }
600
    return this.obj.roomAliasMap[contact.id] || null
601 602
  }

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

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

630
  public memberAll(name: string): Contact[]
631

L
lijiarui 已提交
632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653
  /**
   * 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 (李卓桓) 已提交
654
  public memberAll(queryArg: MemberQueryFilter | string): Contact[] {
655
    if (typeof queryArg === 'string') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674
      //
      // 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 (李卓桓) 已提交
675 676
        this.memberAll({name:         queryArg}),
        this.memberAll({roomAlias:    queryArg}),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
677 678
        this.memberAll({contactAlias: queryArg}),
      )
679 680
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
681 682 683
    /**
     * We got filter parameter
     */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
684
    log.silly('Room', 'memberAll({ %s })',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
685 686 687
                      Object.keys(queryArg)
                            .map(k => `${k}: ${queryArg[k]}`)
                            .join(', '),
688 689 690
            )

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

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

    const keyMap = {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
705
      contactAlias: 'contactAliasMap',
706 707
      name:         'nameMap',
      alias:        'roomAliasMap',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
708
      roomAlias:    'roomAliasMap',
709 710
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
711 712 713
    const filterMapName = keyMap[filterKey]
    if (!filterMapName) {
      throw new Error('unsupport filter key: ' + filterKey)
714 715 716 717 718
    }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
720
    const filterMap = this.obj[filterMapName]
721
    const idList = Object.keys(filterMap)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
722
                          .filter(id => filterMap[id] === filterValue)
723

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

726
    if (idList.length) {
727 728 729 730 731
      return idList.map(id => {
        const c = Contact.load(id)
        c.puppet = this.puppet
        return c
      })
732
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
733
      return []
734
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
735 736
  }

737
  public member(name: string): Contact | null
L
lijiarui 已提交
738

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

L
lijiarui 已提交
741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768
  /**
   * 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()}`)
   *   }
   * }
   */
769 770 771
  public member(queryArg: MemberQueryFilter | string): Contact | null {
    log.verbose('Room', 'member(%s)', JSON.stringify(queryArg))

772 773 774 775 776 777 778 779 780
    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)
    }

781 782 783 784 785
    if (!memberList || !memberList.length) {
      return null
    }

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

H
hcz 已提交
791
  /**
L
lijiarui 已提交
792 793 794
   * Get all room member from the room
   *
   * @returns {Contact[]}
H
hcz 已提交
795
   */
796
  public memberList(): Contact[] {
797
    log.verbose('Room', 'memberList')
798 799 800

    if (!this.obj || !this.obj.memberList || this.obj.memberList.length < 1) {
      log.warn('Room', 'memberList() not ready')
801 802 803 804
      log.verbose('Room', 'memberList() trying call refresh() to update')
      this.refresh().then(() => {
        log.verbose('Room', 'memberList() refresh() done')
      })
805
      return []
806 807 808 809
    }
    return this.obj.memberList
  }

H
hcz 已提交
810
  /**
L
lijiarui 已提交
811 812 813 814 815 816 817 818 819 820 821 822 823 824 825
   * 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 已提交
826
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
827
  public static create(contactList: Contact[], topic?: string): Promise<Room> {
828
    log.verbose('Room', 'create(%s, %s)', contactList.join(','), topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
829

Huan (李卓桓)'s avatar
bug fix  
Huan (李卓桓) 已提交
830
    if (!contactList || !Array.isArray(contactList)) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
831 832
      throw new Error('contactList not found')
    }
833

834 835 836 837 838 839 840
    return this.puppet // config.puppetInstance()
                .roomCreate(contactList, topic)
                .catch(e => {
                  log.error('Room', 'create() exception: %s', e && e.stack || e.message || e)
                  Raven.captureException(e)
                  throw e
                })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
841 842
  }

H
hcz 已提交
843
  /**
L
lijiarui 已提交
844 845 846 847 848 849 850 851
   * 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 已提交
852
   */
853 854 855 856
  public static async findAll(query?: RoomQueryFilter): Promise<Room[]> {
    if (!query) {
      query = { topic: /.*/ }
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
857
    log.verbose('Room', 'findAll({ topic: %s })', query.topic)
858

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

861 862
    if (!topicFilter) {
      throw new Error('topicFilter not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
863 864
    }

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

867
    if (topicFilter instanceof RegExp) {
868
      filterFunction = `(function (c) { return ${topicFilter.toString()}.test(c) })`
869
    } else if (typeof topicFilter === 'string') {
870
      topicFilter = topicFilter.replace(/'/g, '\\\'')
871
      filterFunction = `(function (c) { return c === '${topicFilter}' })`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
872
    } else {
873
      throw new Error('unsupport topic type')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
874 875
    }

876 877 878 879 880 881 882
    const roomList = await this.puppet // config.puppetInstance()
                                .roomFind(filterFunction)
                                .catch(e => {
                                  log.verbose('Room', 'findAll() rejected: %s', e.message)
                                  Raven.captureException(e)
                                  return [] as Room[] // fail safe
                                })
883

884 885 886 887
    await Promise.all(roomList.map(room => room.ready()))
    // for (let i = 0; i < roomList.length; i++) {
    //   await roomList[i].ready()
    // }
888 889

    return roomList
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
890 891
  }

892
  /**
L
lijiarui 已提交
893 894
   * Try to find a room by filter: {topic: string | RegExp}. If get many, return the first one.
   *
895 896 897 898
   * @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> {
899
    log.verbose('Room', 'find({ topic: %s })', query.topic)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
900

901
    const roomList = await this.findAll(query)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
902
    if (!roomList || roomList.length < 1) {
903
      return null
904 905
    } else if (roomList.length > 1) {
      log.warn('Room', 'find() got more than one result, return the 1st one.')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
906
    }
907
    return roomList[0]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
908 909
  }

L
lijiarui 已提交
910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
  /**
   * 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

933
    const user = this.puppet // config.puppetInstance()
L
lijiarui 已提交
934 935 936 937 938 939 940
                      .user

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

    if (this.rawObj.ChatRoomOwner) {
941 942 943
      const c = Contact.load(this.rawObj.ChatRoomOwner)
      c.puppet = this.puppet
      return c
L
lijiarui 已提交
944 945 946 947 948 949
    }

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
950
  /**
L
lijiarui 已提交
951
   * @private
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
952
   */
953 954 955 956
  public static load(id: string): Room {
    if (!id) {
      throw new Error('Room.load() no id')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
957

958 959
    if (id in this.pool) {
      return this.pool[id]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
960
    }
961
    return this.pool[id] = new this(id)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
962 963
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
964
}
Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
965 966

export default Room