contact.ts 22.0 KB
Newer Older
1
/**
2
 *   Wechaty - https://github.com/wechaty/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 { instanceToClass }  from 'clone-class'
import { FileBox }          from 'file-box'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
22

23 24 25 26 27 28 29
import {
  ContactGender,
  ContactPayload,
  ContactQueryFilter,
  ContactType,
}                         from 'wechaty-puppet'

30 31
import {
  Accessory,
32
}                   from '../accessory'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
33
import {
34
  log,
35
  qrCodeForChatie,
36
  Raven,
37
}                   from '../config'
38 39
import {
  Sayable,
40
}                   from '../types'
41

42
import { UrlLink }  from './url-link'
Z
zhaoic 已提交
43
import { MiniProgram }  from './mini-program'
44
import { Message } from './message'
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
45

46 47
export const POOL = Symbol('pool')

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
48
/**
L
lijiarui 已提交
49
 * All wechat contacts(friend) will be encapsulated as a Contact.
50
 * [Examples/Contact-Bot]{@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/contact-bot.ts}
51 52
 *
 * @property {string}  id               - Get Contact id.
53
 * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
54
 */
55
export class Contact extends Accessory implements Sayable {
56

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57
  // tslint:disable-next-line:variable-name
58
  public static Type   = ContactType
59
  // tslint:disable-next-line:variable-name
60
  public static Gender = ContactGender
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
61

62
  protected static [POOL]: Map<string, Contact>
63
  protected static get pool () {
64 65
    return this[POOL]
  }
66
  protected static set pool (newPool: Map<string, Contact>) {
67 68 69
    if (this === Contact) {
      throw new Error(
        'The global Contact class can not be used directly!'
70
        + 'See: https://github.com/wechaty/wechaty/issues/1217',
71 72 73 74
      )
    }
    this[POOL] = newPool
  }
75

H
hcz 已提交
76 77
  /**
   * @private
78
   * About the Generic: https://stackoverflow.com/q/43003970/1123955
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
79
   *
L
lijiarui 已提交
80
   * Get Contact by id
81
   * > Tips:
82
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
L
lijiarui 已提交
83 84 85 86 87 88 89 90 91
   *
   * @static
   * @param {string} id
   * @returns {Contact}
   * @example
   * const bot = new Wechaty()
   * await bot.start()
   * const contact = bot.Contact.load('contactId')
   */
92
  public static load<T extends typeof Contact> (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93 94
    this : T,
    id   : string,
95 96
  ): T['prototype'] {
    if (!this.pool) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
97
      log.verbose('Contact', 'load(%s) init pool', id)
98 99
      this.pool = new Map<string, Contact>()
    }
100 101 102
    if (this === Contact) {
      throw new Error(
        'The lgobal Contact class can not be used directly!'
103
        + 'See: https://github.com/wechaty/wechaty/issues/1217',
104 105
      )
    }
106 107
    if (this.pool === Contact.pool) {
      throw new Error('the current pool is equal to the global pool error!')
108
    }
109 110 111
    const existingContact = this.pool.get(id)
    if (existingContact) {
      return existingContact
112
    }
113 114 115

    // 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.
116
    const newContact = new (this as any)(id) as Contact
117

118
    this.pool.set(id, newContact)
119

120
    return newContact
121 122
  }

L
lijiarui 已提交
123
  /**
L
lijiarui 已提交
124
   * The way to search Contact
L
lijiarui 已提交
125
   *
L
lijiarui 已提交
126 127 128
   * @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
129
   * [More Detail]{@link https://github.com/wechaty/wechaty/issues/365}
L
lijiarui 已提交
130 131 132 133
   */

  /**
   * Try to find a contact by filter: {name: string | RegExp} / {alias: string | RegExp}
L
lijiarui 已提交
134
   *
L
lijiarui 已提交
135 136 137 138 139
   * 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 已提交
140
   * @example
L
lijiarui 已提交
141 142 143 144
   * const bot = new Wechaty()
   * await bot.start()
   * const contactFindByName = await bot.Contact.find({ name:"ruirui"} )
   * const contactFindByAlias = await bot.Contact.find({ alias:"lijiarui"} )
L
lijiarui 已提交
145
   */
146
  public static async find<T extends typeof Contact> (
147
    this  : T,
148
    query : string | ContactQueryFilter,
149
  ): Promise<T['prototype'] | null> {
L
lijiarui 已提交
150 151
    log.verbose('Contact', 'find(%s)', JSON.stringify(query))

152
    const contactList = await this.findAll(query)
153 154 155 156 157

    if (!contactList) {
      return null
    }
    if (contactList.length < 1) {
L
lijiarui 已提交
158
      return null
159
    }
L
lijiarui 已提交
160 161

    if (contactList.length > 1) {
162 163 164 165 166 167 168
      log.warn('Contact', 'find() got more than one(%d) result', contactList.length)
    }

    let n = 0
    for (n = 0; n < contactList.length; n++) {
      const contact = contactList[n]
      // use puppet.contactValidate() to confirm double confirm that this contactId is valid.
169 170
      // https://github.com/wechaty/wechaty-puppet-padchat/issues/64
      // https://github.com/wechaty/wechaty/issues/1345
171 172
      const valid = await this.puppet.contactValidate(contact.id)
      if (valid) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
173
        log.verbose('Contact', 'find() confirm contact[#%d] with id=%d is valid result, return it.',
174 175 176
          n,
          contact.id,
        )
177 178 179
        return contact
      } else {
        log.verbose('Contact', 'find() confirm contact[#%d] with id=%d is INVALID result, try next',
180 181 182
          n,
          contact.id,
        )
183
      }
L
lijiarui 已提交
184
    }
185 186
    log.warn('Contact', 'find() got %d contacts but no one is valid.', contactList.length)
    return null
L
lijiarui 已提交
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
  }

  /**
   * 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
L
lijiarui 已提交
202 203
   * const bot = new Wechaty()
   * await bot.start()
204 205 206
   * const contactList = await bot.Contact.findAll()                      // get the contact list of the bot
   * const contactList = await bot.Contact.findAll({ name: 'ruirui' })    // find allof the contacts whose name is 'ruirui'
   * const contactList = await bot.Contact.findAll({ alias: 'lijiarui' }) // find all of the contacts whose alias is 'lijiarui'
L
lijiarui 已提交
207
   */
208
  public static async findAll<T extends typeof Contact> (
209
    this  : T,
210
    query? : string | ContactQueryFilter,
211
  ): Promise<Array<T['prototype']>> {
212
    log.verbose('Contact', 'findAll(%s)', JSON.stringify(query) || '')
L
lijiarui 已提交
213 214

    try {
215
      const contactIdList: string[] = await this.puppet.contactSearch(query)
216
      const contactList = contactIdList.map(id => this.load(id))
L
lijiarui 已提交
217

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
218
      const BATCH_SIZE = 16
219 220
      let   batchIndex = 0

221
      const invalidDict: { [id: string]: true } = {}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
222

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
223
      while (batchIndex * BATCH_SIZE < contactList.length) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
224
        const batchContactList = contactList.slice(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
225 226
          BATCH_SIZE * batchIndex,
          BATCH_SIZE * (batchIndex + 1),
227 228
        )
        await Promise.all(
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
229
          batchContactList.map(
230
            c => c.ready()
231 232 233 234
              .catch(e => {
                log.error('Contact', 'findAll() contact.ready() exception: %s', e.message)
                invalidDict[c.id] = true
              }),
235 236 237 238 239 240
          ),
        )

        batchIndex++
      }

241
      return contactList.filter(contact => !invalidDict[contact.id])
L
lijiarui 已提交
242 243

    } catch (e) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
244
      log.error('Contact', 'this.puppet.contactFindAll() rejected: %s', e.message)
L
lijiarui 已提交
245 246 247 248
      return [] // fail safe
    }
  }

249
  // TODO
250
  public static async delete (contact: Contact): Promise<void> {
ruiruibupt's avatar
ruiruibupt 已提交
251
    log.verbose('Contact', 'static delete(%s)', contact.id)
252 253
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
254
  /**
255
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
256
   * Instance properties
L
lijiarui 已提交
257
   * @private
258
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
259
   */
260
  protected payload?: ContactPayload
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
261

262
  /**
263
   * @hideconstructor
264
   */
265
  constructor (
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
266
    public readonly id: string,
267 268 269
  ) {
    super()
    log.silly('Contact', `constructor(${id})`)
270 271 272 273 274

    // tslint:disable-next-line:variable-name
    const MyClass = instanceToClass(this, Contact)

    if (MyClass === Contact) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
275 276
      throw new Error(
        'Contact class can not be instanciated directly!'
277
        + 'See: https://github.com/wechaty/wechaty/issues/1217',
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
278
      )
279 280 281 282 283
    }

    if (!this.puppet) {
      throw new Error('Contact class can not be instanciated without a puppet!')
    }
284 285 286
  }

  /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
287
   * @private
288
   */
289
  public toString (): string {
290 291 292 293 294 295 296 297 298
    if (!this.payload) {
      return this.constructor.name
    }

    const identity = this.payload.alias
                    || this.payload.name
                    || this.id
                    || 'loading...'

299 300 301
    return `Contact<${identity}>`
  }

302 303 304 305 306
  public async say (text:     string)      : Promise<void | Message>
  public async say (contact:  Contact)     : Promise<void | Message>
  public async say (file:     FileBox)     : Promise<void | Message>
  public async say (mini:     MiniProgram) : Promise<void | Message>
  public async say (url:      UrlLink)     : Promise<void | Message>
L
lijiarui 已提交
307 308

  /**
309
   * > Tips:
310
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
311
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
312
   * @param {(string | Contact | FileBox | UrlLink | MiniProgram)} something
L
lijiarui 已提交
313 314
   * send text, Contact, or file to contact. </br>
   * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file
315
   * @returns {Promise<void | Message>}
L
lijiarui 已提交
316
   * @example
L
lijiarui 已提交
317 318 319 320
   * const bot = new Wechaty()
   * await bot.start()
   * const contact = await bot.Contact.find({name: 'lijiarui'})  // change 'lijiarui' to any of your contact name in wechat
   *
L
lijiarui 已提交
321
   * // 1. send text to contact
L
lijiarui 已提交
322 323
   *
   * await contact.say('welcome to wechaty!')
324
   * const msg = await contact.say('welcome to wechaty!') // only supported by puppet-padplus
L
lijiarui 已提交
325
   *
L
lijiarui 已提交
326
   * // 2. send media file to contact
L
lijiarui 已提交
327 328 329
   *
   * import { FileBox }  from 'file-box'
   * const fileBox1 = FileBox.fromUrl('https://chatie.io/wechaty/images/bot-qr-code.png')
330
   * const fileBox2 = FileBox.fromFile('/tmp/text.txt')
L
lijiarui 已提交
331
   * await contact.say(fileBox1)
332
   * const msg1 = await contact.say(fileBox1) // only supported by puppet-padplus
L
lijiarui 已提交
333
   * await contact.say(fileBox2)
334
   * const msg2 = await contact.say(fileBox2) // only supported by puppet-padplus
L
lijiarui 已提交
335
   *
L
lijiarui 已提交
336
   * // 3. send contact card to contact
L
lijiarui 已提交
337 338
   *
   * const contactCard = bot.Contact.load('contactId')
339
   * const msg = await contact.say(contactCard) // only supported by puppet-padplus
340 341 342 343
   *
   * // 4. send url link to contact
   *
   * const urlLink = new UrlLink ({
L
linyimin 已提交
344 345 346
   *   description : 'WeChat Bot SDK for Individual Account, Powered by TypeScript, Docker, and Love',
   *   thumbnailUrl: 'https://avatars0.githubusercontent.com/u/25162437?s=200&v=4',
   *   title       : 'Welcome to Wechaty',
347
   *   url         : 'https://github.com/wechaty/wechaty',
348 349
   * })
   * await contact.say(urlLink)
350
   * const msg = await contact.say(urlLink) // only supported by puppet-padplus
Z
zhaoic 已提交
351 352 353 354 355 356 357 358 359 360 361 362
   *
   * // 5. send mini program to contact
   *
   * const miniProgram = new MiniProgram ({
   *   username           : 'gh_xxxxxxx',     //get from mp.weixin.qq.com
   *   appid              : '',               //optional, get from mp.weixin.qq.com
   *   title              : '',               //optional
   *   pagepath           : '',               //optional
   *   description        : '',               //optional
   *   thumbnailurl       : '',               //optional
   * })
   * await contact.say(miniProgram)
363
   * const msg = await contact.say(miniProgram) // only supported by puppet-padplus
L
lijiarui 已提交
364
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
365 366 367 368 369 370
  public async say (
    something:  string
              | Contact
              | FileBox
              | MiniProgram
              | UrlLink
371
  ): Promise<void | Message> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
372
    log.verbose('Contact', 'say(%s)', something)
373
    let msgId: string | void
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
374
    if (typeof something === 'string') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
375
      /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
376
       * 1. Text
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
377
       */
378
      msgId = await this.puppet.messageSendText({
379
        contactId: this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
380 381
      }, something)
    } else if (something instanceof Contact) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
382
      /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
383
       * 2. Contact
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
384
       */
385
      msgId = await this.puppet.messageSendContact({
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
386
        contactId: this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
387 388
      }, something.id)
    } else if (something instanceof FileBox) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
389
      /**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
390
       * 3. File
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
391
       */
392
      msgId = await this.puppet.messageSendFile({
393
        contactId: this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
394 395
      }, something)
    } else if (something instanceof UrlLink) {
396 397 398
      /**
       * 4. Link Message
       */
399
      msgId = await this.puppet.messageSendUrl({
400
        contactId : this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
401 402
      }, something.payload)
    } else if (something instanceof MiniProgram) {
Z
zhaoic 已提交
403 404 405
      /**
       * 5. Mini Program
       */
406
      msgId = await this.puppet.messageSendMiniProgram({
Z
zhaoic 已提交
407
        contactId : this.id,
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
408
      }, something.payload)
409
    } else {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
410
      throw new Error('unsupported arg: ' + something)
411
    }
412 413 414 415 416
    if (msgId) {
      const msg = this.wechaty.Message.load(msgId)
      await msg.ready()
      return msg
    }
417
  }
L
lijiarui 已提交
418 419 420 421 422 423 424 425

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

430 431 432
  public async alias ()                  : Promise<null | string>
  public async alias (newAlias:  string) : Promise<void>
  public async alias (empty:     null)   : Promise<void>
L
lijiarui 已提交
433 434 435 436 437 438

  /**
   * 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
439 440 441
   * @returns {(Promise<null | string | void>)}
   * @example <caption> GET the alias for a contact, return {(Promise<string | null>)}</caption>
   * const alias = await contact.alias()
L
lijiarui 已提交
442 443 444 445 446 447 448
   * 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 (李卓桓) 已提交
449 450
   * try {
   *   await contact.alias('lijiarui')
L
lijiarui 已提交
451
   *   console.log(`change ${contact.name()}'s alias successfully!`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
452
   * } catch (e) {
L
lijiarui 已提交
453 454 455 456
   *   console.log(`failed to change ${contact.name()} alias!`)
   * }
   *
   * @example <caption>DELETE the alias for a contact</caption>
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
457 458
   * try {
   *   const oldAlias = await contact.alias(null)
L
lijiarui 已提交
459
   *   console.log(`delete ${contact.name()}'s alias successfully!`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
460 461
   *   console.log('old alias is ${oldAlias}`)
   * } catch (e) {
L
lijiarui 已提交
462 463 464
   *   console.log(`failed to delete ${contact.name()}'s alias!`)
   * }
   */
465
  public async alias (newAlias?: null | string): Promise<null | string | void> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
466
    log.silly('Contact', 'alias(%s)',
467 468 469 470
      newAlias === undefined
        ? ''
        : newAlias,
    )
471

472 473 474 475
    if (!this.payload) {
      throw new Error('no payload')
    }

476
    if (typeof newAlias === 'undefined') {
477
      return this.payload.alias || null
478 479
    }

480 481
    try {
      await this.puppet.contactAlias(this.id, newAlias)
482 483 484
      await this.puppet.contactPayloadDirty(this.id)
      this.payload = await this.puppet.contactPayload(this.id)
      if (newAlias && newAlias !== this.payload.alias) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
485
        log.warn('Contact', 'alias(%s) sync with server fail: set(%s) is not equal to get(%s)',
S
SuperChang 已提交
486
          newAlias,
487 488 489
          newAlias,
          this.payload.alias,
        )
490
      }
491 492 493 494
    } catch (e) {
      log.error('Contact', 'alias(%s) rejected: %s', newAlias, e.message)
      Raven.captureException(e)
    }
495
  }
L
lijiarui 已提交
496

L
lijiarui 已提交
497 498
  /**
   *
L
lijiarui 已提交
499 500
   * @description
   * Should use {@link Contact#friend} instead
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
501
   *
L
lijiarui 已提交
502
   * @deprecated
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
503
   * @private
L
lijiarui 已提交
504
   */
505
  public stranger (): null | boolean {
506 507 508 509
    log.warn('Contact', 'stranger() DEPRECATED. use friend() instead.')
    if (!this.payload) return null
    return !this.friend()
  }
L
lijiarui 已提交
510

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
511 512 513
  /**
   * Check if contact is friend
   *
514
   * > Tips:
515
   * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table)
516
   *
L
lijiarui 已提交
517 518 519 520
   * @returns {boolean | null}
   *
   * <br>True for friend of the bot <br>
   * False for not friend of the bot, null for unknown.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
521 522 523
   * @example
   * const isFriend = contact.friend()
   */
524
  public friend (): null | boolean {
525 526 527 528 529 530
    log.verbose('Contact', 'friend()')
    if (!this.payload) {
      return null
    }
    return this.payload.friend || null
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
531

J
Jas 已提交
532
  /**
L
lijiarui 已提交
533
   * @ignore
L
lijiarui 已提交
534 535
   * @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}
L
lijiarui 已提交
536 537 538 539 540
   */
  /**
   * @description
   * Check if it's a offical account, should use {@link Contact#type} instead
   * @deprecated
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
541
   * @private
J
Jas 已提交
542
   */
543
  public official (): boolean {
544
    log.warn('Contact', 'official() DEPRECATED. use type() instead')
545
    return !!this.payload && (this.payload.type === ContactType.Official)
546
  }
J
Jas 已提交
547 548

  /**
L
lijiarui 已提交
549 550 551
   * @description
   * Check if it's a personal account, should use {@link Contact#type} instead
   * @deprecated
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
552
   * @private
J
Jas 已提交
553
   */
554
  public personal (): boolean {
555
    log.warn('Contact', 'personal() DEPRECATED. use type() instead')
556
    return !!this.payload && this.payload.type === ContactType.Personal
557
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
558

L
lijiarui 已提交
559 560 561 562 563 564 565 566
  /**
   * Enum for ContactType
   * @enum {number}
   * @property {number} Unknown    - ContactType.Unknown    (0) for Unknown
   * @property {number} Personal   - ContactType.Personal   (1) for Personal
   * @property {number} Official   - ContactType.Official   (2) for Official
   */

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
567 568
  /**
   * Return the type of the Contact
L
lijiarui 已提交
569 570
   * > Tips: ContactType is enum here.</br>
   * @returns {ContactType.Unknown | ContactType.Personal | ContactType.Official}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
571 572
   *
   * @example
L
lijiarui 已提交
573 574 575
   * const bot = new Wechaty()
   * await bot.start()
   * const isOfficial = contact.type() === bot.Contact.Type.Official
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
576
   */
577 578 579 580 581
  public type (): ContactType {
    if (!this.payload) {
      throw new Error('no payload')
    }
    return this.payload.type
582
  }
J
Jas 已提交
583

L
lijiarui 已提交
584
  /**
L
lijiarui 已提交
585 586
   * @private
   * TODO
L
lijiarui 已提交
587 588
   * Check if the contact is star contact.
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
589
   * @returns {boolean | null} - True for star friend, False for no star friend.
L
lijiarui 已提交
590 591 592
   * @example
   * const isStar = contact.star()
   */
593
  public star (): null | boolean {
594 595 596 597 598 599 600
    if (!this.payload) {
      return null
    }
    return this.payload.star === undefined
      ? null
      : this.payload.star
  }
L
lijiarui 已提交
601

602 603
  /**
   * Contact gender
L
lijiarui 已提交
604
   * > Tips: ContactGender is enum here. </br>
L
lijiarui 已提交
605
   *
L
lijiarui 已提交
606
   * @returns {ContactGender.Unknown | ContactGender.Male | ContactGender.Female}
L
lijiarui 已提交
607
   * @example
L
lijiarui 已提交
608
   * const gender = contact.gender() === bot.Contact.Gender.Male
L
lijiarui 已提交
609
   */
610
  public gender (): ContactGender {
611 612
    return this.payload
      ? this.payload.gender
613
      : ContactGender.Unknown
614
  }
L
lijiarui 已提交
615 616 617 618

  /**
   * Get the region 'province' from a contact
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
619
   * @returns {string | null}
L
lijiarui 已提交
620 621
   * @example
   * const province = contact.province()
622
   */
623
  public province (): null | string {
624
    return (this.payload && this.payload.province) || null
625
  }
L
lijiarui 已提交
626 627 628 629

  /**
   * Get the region 'city' from a contact
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
630
   * @returns {string | null}
L
lijiarui 已提交
631 632 633
   * @example
   * const city = contact.city()
   */
634
  public city (): null | string {
635
    return (this.payload && this.payload.city) || null
636
  }
637 638 639

  /**
   * Get avatar picture file stream
L
lijiarui 已提交
640
   *
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
641
   * @returns {Promise<FileBox>}
L
lijiarui 已提交
642
   * @example
L
lijiarui 已提交
643
   * // Save avatar to local file like `1-name.jpg`
L
lijiarui 已提交
644 645 646 647 648
   *
   * const file = await contact.avatar()
   * const name = file.name
   * await file.toFile(name, true)
   * console.log(`Contact: ${contact.name()} with avatar file: ${name}`)
649
   */
650
  public async avatar (): Promise<FileBox> {
651 652
    log.verbose('Contact', 'avatar()')

653 654 655 656 657 658 659
    try {
      const fileBox = await this.puppet.contactAvatar(this.id)
      return fileBox
    } catch (e) {
      log.error('Contact', 'avatar() exception: %s', e.message)
      return qrCodeForChatie()
    }
660
  }
661

L
lijiarui 已提交
662
  /**
L
lijiarui 已提交
663 664
   * @description
   * Force reload(re-ready()) data for Contact, use {@link Contact#sync} instead
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
665
   *
L
lijiarui 已提交
666
   * @deprecated
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
667
   * @private
L
lijiarui 已提交
668
   */
669
  public refresh (): Promise<void> {
670 671 672
    log.warn('Contact', 'refresh() DEPRECATED. use sync() instead.')
    return this.sync()
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
673 674

  /**
675
   * Force reload data for Contact, Sync data from lowlevel API again.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
676 677 678 679 680
   *
   * @returns {Promise<this>}
   * @example
   * await contact.sync()
   */
681
  public async sync (): Promise<void> {
682
    await this.ready(true)
683
  }
L
lijiarui 已提交
684

L
lijiarui 已提交
685
  /**
686 687 688 689 690
   * `ready()` is For FrameWork ONLY!
   *
   * Please not to use `ready()` at the user land.
   * If you want to sync data, uyse `sync()` instead.
   *
L
lijiarui 已提交
691 692
   * @private
   */
693 694 695
  public async ready (
    forceSync = false,
  ): Promise<void> {
696
    log.silly('Contact', 'ready() @ %s', this.puppet)
697

698
    if (!forceSync && this.isReady()) { // already ready
699 700 701 702 703
      log.silly('Contact', 'ready() isReady() true')
      return
    }

    try {
704
      if (forceSync) {
705 706
        await this.puppet.contactPayloadDirty(this.id)
      }
707
      this.payload = await this.puppet.contactPayload(this.id)
708
      // log.silly('Contact', `ready() this.puppet.contactPayload(%s) resolved`, this)
709 710

    } catch (e) {
711
      log.verbose('Contact', `ready() this.puppet.contactPayload(%s) exception: %s`,
712 713 714
        this,
        e.message,
      )
715 716 717 718
      Raven.captureException(e)
      throw e
    }
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
719

Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
720 721 722
  /**
   * @private
   */
723
  public isReady (): boolean {
724 725
    return !!(this.payload && this.payload.name)
  }
Huan (李卓桓)'s avatar
wip...  
Huan (李卓桓) 已提交
726

L
lijiarui 已提交
727 728 729 730 731 732 733
  /**
   * Check if contact is self
   *
   * @returns {boolean} True for contact is self, False for contact is others
   * @example
   * const isSelf = contact.self()
   */
734
  public self (): boolean {
735
    const userId = this.puppet.selfId()
736

737
    if (!userId) {
738 739 740
      return false
    }

741
    return this.id === userId
742
  }
743

L
lijiarui 已提交
744
  /**
L
lijiarui 已提交
745
   * Get the weixin number from a contact.
L
lijiarui 已提交
746
   *
L
lijiarui 已提交
747
   * Sometimes cannot get weixin number due to weixin security mechanism, not recommend.
L
lijiarui 已提交
748
   *
L
lijiarui 已提交
749 750
   * @private
   * @returns {string | null}
L
lijiarui 已提交
751
   * @example
L
lijiarui 已提交
752
   * const weixin = contact.weixin()
L
lijiarui 已提交
753
   */
754
  public weixin (): null | string {
755
    return (this.payload && this.payload.weixin) || null
756
  }
757

758
}