contact.ts 15.6 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
 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
20 21
import { FileBox } from 'file-box'

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
22
import {
23
  log,
24
  Raven,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
25
  Sayable,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
26 27
}                       from './config'
import PuppetAccessory  from './puppet-accessory'
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
28 29

import Message          from './message'
30

L
lijiarui 已提交
31 32
/**
 * Enum for Gender values.
L
lijiarui 已提交
33
 *
L
lijiarui 已提交
34
 * @enum {number}
L
lijiarui 已提交
35 36 37
 * @property {number} Unknown   - 0 for Unknown
 * @property {number} Male      - 1 for Male
 * @property {number} Female    - 2 for Female
L
lijiarui 已提交
38
 */
39
export enum Gender {
40 41 42
  Unknown = 0,
  Male    = 1,
  Female  = 2,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
43 44 45
}

export enum ContactType {
46 47 48
  Unknown = 0,
  Personal,
  Official,
49 50
}

51
export interface ContactQueryFilter {
L
lijiarui 已提交
52 53
  name?:   string | RegExp,
  alias?:  string | RegExp,
54 55
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
export interface ContactPayload {
  gender:     Gender,
  type:       ContactType,

  address?:   string,
  alias?:     string | null,
  avatar?:    string,
  city?:      string,
  friend?:    boolean,
  name?:      string,
  province?:  string,
  signature?: string,
  star?:      boolean,
  weixin?:    string,
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
72
/**
L
lijiarui 已提交
73
 * All wechat contacts(friend) will be encapsulated as a Contact.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
74
 *
L
lijiarui 已提交
75
 * `Contact` is `Sayable`,
76
 * [Examples/Contact-Bot]{@link https://github.com/Chatie/wechaty/blob/master/examples/contact-bot.ts}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
77
 */
78
export class Contact extends PuppetAccessory implements Sayable {
79

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
80 81 82 83
  // tslint:disable-next-line:variable-name
  public static Type    = ContactType
  public static Gender  = Gender

84 85
  protected static readonly pool = new Map<string, Contact>()

H
hcz 已提交
86 87
  /**
   * @private
88
   * About the Generic: https://stackoverflow.com/q/43003970/1123955
H
hcz 已提交
89
   */
90
  public static load<T extends typeof Contact>(this: T, id: string): T['prototype'] {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
91 92
    if (!id || typeof id !== 'string') {
      throw new Error('Contact.load(): id not found')
93
    }
94

95 96 97
    const existingContact = this.pool.get(id)
    if (existingContact) {
      return existingContact
98
    }
99 100 101 102 103 104

    // when we call `load()`, `this` should already be extend-ed a child class.
    // so we force `this as any` at here to make the call.
    const newContact = new (this as any)(id)
    Contact.pool.set(id, newContact)
    return newContact
105 106
  }

L
lijiarui 已提交
107
  /**
L
lijiarui 已提交
108
   * The way to search Contact
L
lijiarui 已提交
109
   *
L
lijiarui 已提交
110 111 112 113 114 115 116 117
   * @typedef    ContactQueryFilter
   * @property   {string} name    - The name-string set by user-self, should be called name
   * @property   {string} alias   - The name-string set by bot for others, should be called alias
   * [More Detail]{@link https://github.com/Chatie/wechaty/issues/365}
   */

  /**
   * Try to find a contact by filter: {name: string | RegExp} / {alias: string | RegExp}
L
lijiarui 已提交
118
   *
L
lijiarui 已提交
119 120 121 122 123
   * Find contact by name or alias, if the result more than one, return the first one.
   *
   * @static
   * @param {ContactQueryFilter} query
   * @returns {(Promise<Contact | null>)} If can find the contact, return Contact, or return null
L
lijiarui 已提交
124
   * @example
L
lijiarui 已提交
125 126
   * const contactFindByName = await Contact.find({ name:"ruirui"} )
   * const contactFindByAlias = await Contact.find({ alias:"lijiarui"} )
L
lijiarui 已提交
127
   */
128 129 130 131
  public static async find<T extends typeof Contact>(
    this  : T,
    query : ContactQueryFilter,
  ): Promise<T['prototype'] | null> {
L
lijiarui 已提交
132 133
    log.verbose('Contact', 'find(%s)', JSON.stringify(query))

134
    const contactList = await this.findAll(query)
L
lijiarui 已提交
135 136
    if (!contactList || !contactList.length) {
      return null
137
    }
L
lijiarui 已提交
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161

    if (contactList.length > 1) {
      log.warn('Contact', 'function find(%s) get %d contacts, use the first one by default', JSON.stringify(query), contactList.length)
    }
    return contactList[0]
  }

  /**
   * Find contact by `name` or `alias`
   *
   * If use Contact.findAll() get the contact list of the bot.
   *
   * #### definition
   * - `name`   the name-string set by user-self, should be called name
   * - `alias`  the name-string set by bot for others, should be called alias
   *
   * @static
   * @param {ContactQueryFilter} [queryArg]
   * @returns {Promise<Contact[]>}
   * @example
   * const contactList = await Contact.findAll()                    // get the contact list of the bot
   * const contactList = await Contact.findAll({name: 'ruirui'})    // find allof the contacts whose name is 'ruirui'
   * const contactList = await Contact.findAll({alias: 'lijiarui'}) // find all of the contacts whose alias is 'lijiarui'
   */
162 163 164 165
  public static async findAll<T extends typeof Contact>(
    this  : T,
    query : ContactQueryFilter = { name: /.*/ },
  ): Promise<T['prototype'][]> {
L
lijiarui 已提交
166 167 168
    // log.verbose('Cotnact', 'findAll({ name: %s })', query.name)
    log.verbose('Cotnact', 'findAll({ %s })',
                            Object.keys(query)
169
                                  .map((k: keyof ContactQueryFilter) => `${k}: ${query[k]}`)
L
lijiarui 已提交
170 171 172 173 174 175 176 177
                                  .join(', '),
              )

    if (Object.keys(query).length !== 1) {
      throw new Error('query only support one key. multi key support is not availble now.')
    }

    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
178
      const contactList: Contact[] = await this.puppet.contactFindAll(query)
L
lijiarui 已提交
179 180 181 182 183

      await Promise.all(contactList.map(c => c.ready()))
      return contactList

    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
184
      log.error('Contact', 'this.puppet.contactFindAll() rejected: %s', e.message)
L
lijiarui 已提交
185 186 187 188
      return [] // fail safe
    }
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
189 190 191 192 193
  /**
   * Instance properties
   */
  protected payload?: ContactPayload

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
  /**
   * @private
   */
  constructor(
    public readonly id: string,
  ) {
    super()
    log.silly('Contact', `constructor(${id})`)
  }

  /**
   * @private
   */
  public toString(): string {
    const identity = this.alias() || this.name() || this.id
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
209
    return `Contact<${identity}>`
210 211
  }

L
lijiarui 已提交
212 213 214 215
  /**
   * Sent Text to contact
   *
   * @param {string} text
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
216 217 218 219 220 221 222
   * @example
   * const contact = await Contact.find({name: 'lijiarui'})         // change 'lijiarui' to any of your contact name in wechat
   * try {
   *   await contact.say('welcome to wechaty!')
   * } catch (e) {
   *   console.error(e)
   * }
L
lijiarui 已提交
223
   */
224
  public async say(text: string): Promise<void>
L
lijiarui 已提交
225 226 227 228

  /**
   * Send Media File to Contact
   *
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
229
   * @param {Message} Message
L
lijiarui 已提交
230 231
   * @example
   * const contact = await Contact.find({name: 'lijiarui'})         // change 'lijiarui' to any of your contact name in wechat
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
232
   * try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
233
   *   await contact.say(bot.Message.create(__dirname + '/wechaty.png') // put the filePath you want to send here
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
234 235 236
   * } catch (e) {
   *   console.error(e)
   * }
L
lijiarui 已提交
237
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
238
  public async say(file: FileBox): Promise<void>
239

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
240 241
  public async say(textOrFile: string | FileBox): Promise<void> {
    log.verbose('Contact', 'say(%s)', textOrFile)
242

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
243 244 245 246 247 248 249 250 251 252 253
    let msg: Message
    if (typeof textOrFile === 'string') {
      msg = Message.createMO({
        text : textOrFile,
        to   : this,
      })
    } else if (textOrFile instanceof FileBox) {
      msg = Message.createMO({
        to   : this,
        file : textOrFile,
      })
254
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255
      throw new Error('unsupported')
256
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
257

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
258
    msg.puppet = this.puppet
259 260

    log.silly('Contact', 'say() from: %s to: %s content: %s',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
261
                                  this.puppet.userSelf(),
262
                                  this,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
263
                                  msg,
264
              )
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
265
    await this.puppet.messageSend(msg)
266
  }
L
lijiarui 已提交
267 268 269 270 271 272 273 274

  /**
   * Get the name from a contact
   *
   * @returns {string}
   * @example
   * const name = contact.name()
   */
275 276 277
  public name(): string {
    return this.payload && this.payload.name || ''
  }
L
lijiarui 已提交
278

279 280 281
  public alias()                  : null | string
  public alias(newAlias:  string) : Promise<void>
  public alias(empty:     null)   : Promise<void>
L
lijiarui 已提交
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297

  /**
   * GET / SET / DELETE the alias for a contact
   *
   * Tests show it will failed if set alias too frequently(60 times in one minute).
   * @param {(none | string | null)} newAlias
   * @returns {(string | null | Promise<boolean>)}
   * @example <caption> GET the alias for a contact, return {(string | null)}</caption>
   * const alias = contact.alias()
   * if (alias === null) {
   *   console.log('You have not yet set any alias for contact ' + contact.name())
   * } else {
   *   console.log('You have already set an alias for contact ' + contact.name() + ':' + alias)
   * }
   *
   * @example <caption>SET the alias for a contact</caption>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
298 299
   * try {
   *   await contact.alias('lijiarui')
L
lijiarui 已提交
300
   *   console.log(`change ${contact.name()}'s alias successfully!`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
301
   * } catch (e) {
L
lijiarui 已提交
302 303 304 305
   *   console.log(`failed to change ${contact.name()} alias!`)
   * }
   *
   * @example <caption>DELETE the alias for a contact</caption>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
306 307
   * try {
   *   const oldAlias = await contact.alias(null)
L
lijiarui 已提交
308
   *   console.log(`delete ${contact.name()}'s alias successfully!`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
309 310
   *   console.log('old alias is ${oldAlias}`)
   * } catch (e) {
L
lijiarui 已提交
311 312 313
   *   console.log(`failed to delete ${contact.name()}'s alias!`)
   * }
   */
314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
  public alias(newAlias?: null | string): null | string | Promise<void> {
    log.verbose('Contact', 'alias(%s)',
                            newAlias === undefined
                              ? ''
                              : newAlias,
                )

    if (typeof newAlias === 'undefined') {
      return this.payload && this.payload.alias || null
    }

    const future = this.puppet.contactAlias(this, newAlias)

    future
      .then(() => this.payload!.alias = newAlias)
      .catch(e => {
        log.error('Contact', 'alias(%s) rejected: %s', newAlias, e.message)
        Raven.captureException(e)
      })

    return future
  }
L
lijiarui 已提交
336

L
lijiarui 已提交
337 338 339
  /**
   * Check if contact is stranger
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
340 341
   * @deprecated use friend() instead
   *
L
lijiarui 已提交
342
   * @returns {boolean | null} - True for not friend of the bot, False for friend of the bot, null for unknown.
L
lijiarui 已提交
343 344 345
   * @example
   * const isStranger = contact.stranger()
   */
346 347 348 349 350
  public stranger(): null | boolean {
    log.warn('Contact', 'stranger() DEPRECATED. use friend() instead.')
    if (!this.payload) return null
    return !this.friend()
  }
L
lijiarui 已提交
351

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
352 353 354 355 356 357 358
  /**
   * Check if contact is friend
   *
   * @returns {boolean | null} - True for friend of the bot, False for not friend of the bot, null for unknown.
   * @example
   * const isFriend = contact.friend()
   */
359 360 361 362 363 364 365
  public friend(): null | boolean {
    log.verbose('Contact', 'friend()')
    if (!this.payload) {
      return null
    }
    return this.payload.friend || null
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
366

J
Jas 已提交
367 368 369
  /**
   * Check if it's a offical account
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
370 371
   * @deprecated use type() instead
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
372
   * @returns {boolean | null} - True for official account, Flase for contact is not a official account, null for unknown
L
lijiarui 已提交
373 374
   * @see {@link https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3243|webwxApp.js#L324}
   * @see {@link https://github.com/Urinx/WeixinBot/blob/master/README.md|Urinx/WeixinBot/README}
J
Jas 已提交
375 376 377
   * @example
   * const isOfficial = contact.official()
   */
378 379
  public official(): boolean {
    log.warn('Contact', 'official() DEPRECATED. use type() instead')
380
    return !!this.payload && this.payload.type === ContactType.Official
381
  }
J
Jas 已提交
382 383 384 385

  /**
   * Check if it's a personal account
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
386 387 388
   * @deprecated use type() instead
   *
   * @returns {boolean} - True for personal account, Flase for contact is not a personal account
J
Jas 已提交
389 390 391
   * @example
   * const isPersonal = contact.personal()
   */
392 393 394 395
  public personal(): boolean {
    log.warn('Contact', 'personal() DEPRECATED. use type() instead')
    return !this.official()
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
396 397 398 399 400 401 402 403

  /**
   * Return the type of the Contact
   *
   * @returns ContactType - Contact.Type.PERSONAL for personal account, Contact.Type.OFFICIAL for official account
   * @example
   * const isOfficial = contact.type() === Contact.Type.OFFICIAL
   */
404 405 406
  public type(): ContactType {
    return this.payload!.type
  }
J
Jas 已提交
407

L
lijiarui 已提交
408 409 410
  /**
   * Check if the contact is star contact.
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
411
   * @returns {boolean | null} - True for star friend, False for no star friend.
L
lijiarui 已提交
412 413 414
   * @example
   * const isStar = contact.star()
   */
415 416 417 418 419 420 421 422
  public star(): null | boolean {
    if (!this.payload) {
      return null
    }
    return this.payload.star === undefined
      ? null
      : this.payload.star
  }
L
lijiarui 已提交
423

424 425
  /**
   * Contact gender
L
lijiarui 已提交
426
   *
427
   * @returns {Gender.Male(2)|Gender.Female(1)|Gender.Unknown(0)}
L
lijiarui 已提交
428 429 430
   * @example
   * const gender = contact.gender()
   */
431 432 433
  public gender(): Gender {
    return this.payload
      ? this.payload.gender
434
      : Gender.Unknown
435
  }
L
lijiarui 已提交
436 437 438 439

  /**
   * Get the region 'province' from a contact
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
440
   * @returns {string | null}
L
lijiarui 已提交
441 442
   * @example
   * const province = contact.province()
443
   */
444 445 446
  public province(): null | string {
    return this.payload && this.payload.province || null
  }
L
lijiarui 已提交
447 448 449 450

  /**
   * Get the region 'city' from a contact
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
451
   * @returns {string | null}
L
lijiarui 已提交
452 453 454
   * @example
   * const city = contact.city()
   */
455 456 457
  public city(): null | string {
    return this.payload && this.payload.city || null
  }
458 459 460

  /**
   * Get avatar picture file stream
L
lijiarui 已提交
461
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
462
   * @returns {Promise<FileBox>}
L
lijiarui 已提交
463 464
   * @example
   * const avatarFileName = contact.name() + `.jpg`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
465
   * const fileBox = await contact.avatar()
L
lijiarui 已提交
466
   * const avatarWriteStream = createWriteStream(avatarFileName)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
467
   * fileBox.pipe(avatarWriteStream)
L
lijiarui 已提交
468
   * log.info('Bot', 'Contact: %s: %s with avatar file: %s', contact.weixin(), contact.name(), avatarFileName)
469
   */
470
  // TODO: use File to replace ReadableStream
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
471
  public async avatar(): Promise<FileBox> {
472 473 474 475
    log.verbose('Contact', 'avatar()')

    return this.puppet.contactAvatar(this)
  }
476

L
lijiarui 已提交
477
  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
478
   * Force reload(re-ready()) data for Contact
L
lijiarui 已提交
479
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
480 481
   * @deprecated use sync() instead
   *
L
lijiarui 已提交
482 483 484 485
   * @returns {Promise<this>}
   * @example
   * await contact.refresh()
   */
486 487 488 489
  public refresh(): Promise<void> {
    log.warn('Contact', 'refresh() DEPRECATED. use sync() instead.')
    return this.sync()
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
490 491 492 493 494 495 496 497

  /**
   * sycc data for Contact
   *
   * @returns {Promise<this>}
   * @example
   * await contact.sync()
   */
498 499 500 501 502 503 504 505
  public async sync(): Promise<void> {
    // TODO: make sure the contact.* works when we are refreshing the data
    // if (this.isReady()) {
    //   this.dirtyObj = this.obj
    // }
    this.payload = undefined
    await this.ready()
  }
L
lijiarui 已提交
506

L
lijiarui 已提交
507 508 509
  /**
   * @private
   */
510 511 512 513 514 515 516 517 518
  public async ready(): Promise<void> {
    log.silly('Contact', 'ready()')

    if (this.isReady()) { // already ready
      log.silly('Contact', 'ready() isReady() true')
      return
    }

    try {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
519
      this.payload = await this.puppet.contactPayload(this.id)
520 521 522 523 524 525 526 527 528 529 530 531
      log.silly('Contact', `ready() this.puppet.contactPayload(%s) resolved`, this)
      // console.log(this.payload)

    } catch (e) {
      log.error('Contact', `ready() this.puppet.contactPayload(%s) exception: %s`,
                            this,
                            e.message,
                )
      Raven.captureException(e)
      throw e
    }
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
532

Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
533 534 535
  /**
   * @private
   */
536 537 538
  public isReady(): boolean {
    return !!(this.payload && this.payload.name)
  }
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
539

L
lijiarui 已提交
540 541 542 543 544 545 546
  /**
   * Check if contact is self
   *
   * @returns {boolean} True for contact is self, False for contact is others
   * @example
   * const isSelf = contact.self()
   */
547 548 549 550 551 552 553 554 555
  public self(): boolean {
    const user = this.puppet.userSelf()

    if (!user) {
      return false
    }

    return this.id === user.id
  }
556

L
lijiarui 已提交
557
  /**
L
lijiarui 已提交
558
   * Get the weixin number from a contact.
L
lijiarui 已提交
559
   *
L
lijiarui 已提交
560
   * Sometimes cannot get weixin number due to weixin security mechanism, not recommend.
L
lijiarui 已提交
561
   *
L
lijiarui 已提交
562 563
   * @private
   * @returns {string | null}
L
lijiarui 已提交
564
   * @example
L
lijiarui 已提交
565
   * const weixin = contact.weixin()
L
lijiarui 已提交
566
   */
567 568 569
  public weixin(): null | string {
    return this.payload && this.payload.weixin || null
  }
570

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
571
}
572

Huan (李卓桓)'s avatar
merge  
Huan (李卓桓) 已提交
573
export default Contact