contact.ts 21.3 KB
Newer Older
1
/**
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
2
 *   Wechaty - https://github.com/chatie/wechaty
3
 *
L
lijiarui 已提交
4
 *   @copyright 2016-2017 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
import {
21
  config,
22
  Raven,
L
lijiarui 已提交
23
  Sayable,
24
  log,
25
}                     from './config'
M
Mukaiu 已提交
26 27 28 29
import {
  Message,
  MediaMessage,
}                     from './message'
30 31 32
import { PuppetWeb }  from './puppet-web'
import { UtilLib }    from './util-lib'
import { Wechaty }    from './wechaty'
33

34
export interface ContactObj {
L
lijiarui 已提交
35 36 37 38 39 40 41 42 43 44 45 46 47
  address:    string,
  city:       string,
  id:         string,
  name:       string,
  province:   string,
  alias:      string|null,
  sex:        Gender,
  signature:  string,
  star:       boolean,
  stranger:   boolean,
  uin:        string,
  weixin:     string,
  avatar:     string,  // XXX URL of HeadImgUrl
J
Jas 已提交
48 49
  official:   boolean,
  special:    boolean,
50 51
}

52
export interface ContactRawObj {
L
lijiarui 已提交
53 54 55 56 57 58 59 60 61 62 63 64 65
  Alias:        string,
  City:         string,
  NickName:     string,
  Province:     string,
  RemarkName:   string,
  Sex:          Gender,
  Signature:    string,
  StarFriend:   string,
  Uin:          string,
  UserName:     string,
  HeadImgUrl:   string,

  stranger:     string, // assign by injectio.js
J
Jas 已提交
66
  VerifyFlag:   number,
67 68
}

L
lijiarui 已提交
69 70
/**
 * Enum for Gender values.
L
lijiarui 已提交
71
 *
L
lijiarui 已提交
72
 * @enum {number}
L
lijiarui 已提交
73 74 75
 * @property {number} Unknown   - 0 for Unknown
 * @property {number} Male      - 1 for Male
 * @property {number} Female    - 2 for Female
L
lijiarui 已提交
76
 */
77 78 79 80 81 82
export enum Gender {
  Unknown = 0,
  Male    = 1,
  Female  = 2,
}

83
export interface ContactQueryFilter {
L
lijiarui 已提交
84 85
  name?:   string | RegExp,
  alias?:  string | RegExp,
86
  // remark is DEPRECATED
L
lijiarui 已提交
87
  remark?: string | RegExp,
88 89
}

J
Jas 已提交
90 91
/**
 * @see https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3848
L
lijiarui 已提交
92
 * @ignore
J
Jas 已提交
93 94 95 96 97 98 99 100
 */
const specialContactList: string[] = [
  'weibo', 'qqmail', 'fmessage', 'tmessage', 'qmessage', 'qqsync', 'floatbottle',
  'lbsapp', 'shakeapp', 'medianote', 'qqfriend', 'readerapp', 'blogapp', 'facebookapp',
  'masssendapp', 'meishiapp', 'feedsapp', 'voip', 'blogappweixin', 'weixin', 'brandsessionholder',
  'weixinreminder', 'wxid_novlwrv3lqwv11', 'gh_22b87fa7cb3c', 'officialaccounts', 'notification_messages',
]

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
101
/**
L
lijiarui 已提交
102
 * All wechat contacts(friend) will be encapsulated as a Contact.
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
103
 *
L
lijiarui 已提交
104 105
 * `Contact` is `Sayable`,
 * [Example/Contact-Bot]{@link https://github.com/Chatie/wechaty/blob/master/example/contact-bot.ts}
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
106
 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
107
export class Contact implements Sayable {
108 109
  private static pool = new Map<string, Contact>()

110
  public obj: ContactObj | null
111
  private dirtyObj: ContactObj | null
112 113
  private rawObj: ContactRawObj

H
hcz 已提交
114 115 116
  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
117 118 119
  constructor(
    public readonly id: string,
  ) {
120
    log.silly('Contact', `constructor(${id})`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
121

122 123 124
    if (typeof id !== 'string') {
      throw new Error('id must be string. found: ' + typeof id)
    }
125 126
  }

L
lijiarui 已提交
127 128 129
  /**
   * @private
   */
130 131 132 133
  public toString(): string {
    if (!this.obj) {
      return this.id
    }
134
    return this.obj.alias || this.obj.name || this.id
135 136
  }

L
lijiarui 已提交
137 138 139
  /**
   * @private
   */
140
  public toStringEx() { return `Contact(${this.obj && this.obj.name}[${this.id}])` }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
141

L
lijiarui 已提交
142 143 144
  /**
   * @private
   */
145
  private parse(rawObj: ContactRawObj): ContactObj | null {
146
    if (!rawObj || !rawObj.UserName) {
147 148 149
      log.warn('Contact', 'parse() got empty rawObj!')
    }

150
    return !rawObj ? null : {
L
lijiarui 已提交
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165
      id:         rawObj.UserName, // MMActualSender??? MMPeerUserName??? `getUserContact(message.MMActualSender,message.MMPeerUserName).HeadImgUrl`
      uin:        rawObj.Uin,    // stable id: 4763975 || getCookie("wxuin")
      weixin:     rawObj.Alias,  // Wechat ID
      name:       rawObj.NickName,
      alias:      rawObj.RemarkName,
      sex:        rawObj.Sex,
      province:   rawObj.Province,
      city:       rawObj.City,
      signature:  rawObj.Signature,

      address:    rawObj.Alias, // XXX: need a stable address for user

      star:       !!rawObj.StarFriend,
      stranger:   !!rawObj.stranger, // assign by injectio.js
      avatar:     rawObj.HeadImgUrl,
J
Jas 已提交
166 167 168
      /**
       * @see 1. https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3243
       * @see 2. https://github.com/Urinx/WeixinBot/blob/master/README.md
L
lijiarui 已提交
169
       * @ignore
J
Jas 已提交
170 171 172 173 174
       */
      // tslint:disable-next-line
      official:      !!rawObj.UserName && !rawObj.UserName.startsWith('@@') && !!(rawObj.VerifyFlag & 8),
      /**
       * @see 1. https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3246
L
lijiarui 已提交
175
       * @ignore
J
Jas 已提交
176 177
       */
      special:       specialContactList.indexOf(rawObj.UserName) > -1 || /@qqim$/.test(rawObj.UserName),
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
178 179
    }
  }
180

L
lijiarui 已提交
181
  /**
L
lijiarui 已提交
182 183
   * Get the weixin number from a contact.
   *
L
lijiarui 已提交
184 185
   * Sometimes cannot get weixin number due to weixin security mechanism, not recommend.
   *
L
lijiarui 已提交
186 187
   * @deprecated
   * @returns {string | null}
L
lijiarui 已提交
188 189 190
   * @example
   * const weixin = contact.weixin()
   */
191 192 193 194
  public weixin(): string | null {
    const wxId = this.obj && this.obj.weixin || null
    if (!wxId) {
      log.info('Contact', `weixin() is not able to always work, it's limited by Tencent API`)
195 196
      log.info('Contact', 'weixin() If you want to track a contact between sessions, see FAQ at')
      log.info('Contact', 'https://github.com/Chatie/wechaty/wiki/FAQ#1-how-to-get-the-permanent-id-for-a-contact')
197 198 199
    }
    return wxId
  }
L
lijiarui 已提交
200 201 202 203 204 205 206 207

  /**
   * Get the name from a contact
   *
   * @returns {string}
   * @example
   * const name = contact.name()
   */
208
  public name()     { return UtilLib.plainText(this.obj && this.obj.name || '') }
L
lijiarui 已提交
209 210 211 212

  /**
   * Check if contact is stranger
   *
L
lijiarui 已提交
213
   * @returns {boolean | null} - True for not friend of the bot, False for friend of the bot, null for unknown.
L
lijiarui 已提交
214 215 216 217 218 219 220 221
   * @example
   * const isStranger = contact.stranger()
   */
  public stranger(): boolean|null {
    if (!this.obj) return null
    return this.obj.stranger
  }

J
Jas 已提交
222 223 224
  /**
   * Check if it's a offical account
   *
L
lijiarui 已提交
225 226 227
   * @returns {boolean|null} - True for official account, Flase for contact is not a official account, null for unknown
   * @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 已提交
228 229 230 231 232 233 234 235 236 237
   * @example
   * const isOfficial = contact.official()
   */
  public official(): boolean {
    return !!this.obj && this.obj.official
  }

  /**
   * Check if it's a special contact
   *
L
lijiarui 已提交
238 239 240 241 242
   * The contact who's id in following list will be identify as a special contact
   * `weibo`, `qqmail`, `fmessage`, `tmessage`, `qmessage`, `qqsync`, `floatbottle`,
   * `lbsapp`, `shakeapp`, `medianote`, `qqfriend`, `readerapp`, `blogapp`, `facebookapp`,
   * `masssendapp`, `meishiapp`, `feedsapp`, `voip`, `blogappweixin`, `weixin`, `brandsessionholder`,
   * `weixinreminder`, `wxid_novlwrv3lqwv11`, `gh_22b87fa7cb3c`, `officialaccounts`, `notification_messages`,
J
Jas 已提交
243
   *
L
lijiarui 已提交
244 245
   * @see {@link https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3848|webwxApp.js#L3848}
   * @see {@link https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3246|webwxApp.js#L3246}
J
Jas 已提交
246 247 248 249 250 251 252 253 254 255 256
   * @returns {boolean|null} True for brand, Flase for contact is not a brand
   * @example
   * const isSpecial = contact.special()
   */
  public special(): boolean {
    return !!this.obj && this.obj.special
  }

  /**
   * Check if it's a personal account
   *
L
lijiarui 已提交
257
   * @returns {boolean|null} - True for personal account, Flase for contact is not a personal account
J
Jas 已提交
258 259 260 261 262 263 264
   * @example
   * const isPersonal = contact.personal()
   */
  public personal(): boolean {
    return !this.official()
  }

L
lijiarui 已提交
265 266 267
  /**
   * Check if the contact is star contact.
   *
L
lijiarui 已提交
268
   * @returns {boolean} - True for star friend, False for no star friend.
L
lijiarui 已提交
269 270 271 272 273 274 275 276
   * @example
   * const isStar = contact.star()
   */
  public star(): boolean|null {
    if (!this.obj) return null
    return this.obj.star
  }

277 278
  /**
   * Contact gender
L
lijiarui 已提交
279
   *
L
lijiarui 已提交
280
   * @returns {Gender.Male(2)|Gender.Female(1)|Gender.Unknown(0)}
L
lijiarui 已提交
281 282 283 284 285 286 287 288 289 290 291
   * @example
   * const gender = contact.gender()
   */
  public gender(): Gender   { return this.obj ? this.obj.sex : Gender.Unknown }

  /**
   * Get the region 'province' from a contact
   *
   * @returns {string | undefined}
   * @example
   * const province = contact.province()
292 293
   */
  public province() { return this.obj && this.obj.province }
L
lijiarui 已提交
294 295 296 297 298 299 300 301

  /**
   * Get the region 'city' from a contact
   *
   * @returns {string | undefined}
   * @example
   * const city = contact.city()
   */
302 303 304 305
  public city()     { return this.obj && this.obj.city }

  /**
   * Get avatar picture file stream
L
lijiarui 已提交
306 307 308 309 310 311 312 313
   *
   * @returns {Promise<NodeJS.ReadableStream>}
   * @example
   * const avatarFileName = contact.name() + `.jpg`
   * const avatarReadStream = await contact.avatar()
   * const avatarWriteStream = createWriteStream(avatarFileName)
   * avatarReadStream.pipe(avatarWriteStream)
   * log.info('Bot', 'Contact: %s: %s with avatar file: %s', contact.weixin(), contact.name(), avatarFileName)
314 315
   */
  public async avatar(): Promise<NodeJS.ReadableStream> {
316 317
    log.verbose('Contact', 'avatar()')

318 319 320 321 322
    if (!this.obj || !this.obj.avatar) {
      throw new Error('Can not get avatar: not ready')
    }

    try {
323
      const hostname = await (config.puppetInstance() as PuppetWeb).browser.hostname()
324
      const avatarUrl = `http://${hostname}${this.obj.avatar}`
325
      const cookies = await (config.puppetInstance() as PuppetWeb).browser.readCookie()
326 327 328 329 330
      log.silly('Contact', 'avatar() url: %s', avatarUrl)

      return UtilLib.urlStream(avatarUrl, cookies)
    } catch (err) {
      log.warn('Contact', 'avatar() exception: %s', err.stack)
331
      Raven.captureException(err)
332
      throw err
333 334
    }
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
335

L
lijiarui 已提交
336 337 338
  /**
   * @private
   */
339
  public get(prop)  { return this.obj && this.obj[prop] }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
340

L
lijiarui 已提交
341 342 343
  /**
   * @private
   */
344
  public isReady(): boolean {
345
    return !!(this.obj && this.obj.id && this.obj.name)
346 347
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
348 349 350 351 352
  // public refresh() {
  //   log.warn('Contact', 'refresh() DEPRECATED. use reload() instead.')
  //   return this.reload()
  // }

L
lijiarui 已提交
353 354 355 356 357 358 359
  /**
   * Force reload data for Contact
   *
   * @returns {Promise<this>}
   * @example
   * await contact.refresh()
   */
360 361 362 363 364 365 366 367
  public async refresh(): Promise<this> {
    if (this.isReady()) {
      this.dirtyObj = this.obj
    }
    this.obj = null
    return this.ready()
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
368 369 370 371 372
  // public ready() {
  //   log.warn('Contact', 'ready() DEPRECATED. use load() instead.')
  //   return this.load()
  // }

L
lijiarui 已提交
373 374 375
  /**
   * @private
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
376
  public async ready(contactGetter?: (id: string) => Promise<ContactRawObj>): Promise<this> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
377
    log.silly('Contact', 'ready(' + (contactGetter ? typeof contactGetter : '') + ')')
378
    if (!this.id) {
379 380
      const e = new Error('ready() call on an un-inited contact')
      throw e
381
    }
382

383
    if (this.isReady()) { // already ready
384 385
      return Promise.resolve(this)
    }
386 387

    if (!contactGetter) {
388 389 390
      log.silly('Contact', 'get contact via ' + config.puppetInstance().constructor.name)
      contactGetter = config.puppetInstance()
                            .getContact.bind(config.puppetInstance())
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
391
    }
392 393 394
    if (!contactGetter) {
      throw new Error('no contatGetter')
    }
395

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
396 397 398 399 400 401
    try {
      const rawObj = await contactGetter(this.id)
      log.silly('Contact', `contactGetter(${this.id}) resolved`)
      this.rawObj = rawObj
      this.obj    = this.parse(rawObj)
      return this
402

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
403 404
    } catch (e) {
      log.error('Contact', `contactGetter(${this.id}) exception: %s`, e.message)
405
      Raven.captureException(e)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
406
      throw e
407 408 409
    }
  }

L
lijiarui 已提交
410 411 412
  /**
   * @private
   */
413
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
414
    console.error('======= dump raw contact =======')
415
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
416
  }
L
lijiarui 已提交
417

L
lijiarui 已提交
418 419 420
  /**
   * @private
   */
421
  public dump()    {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
422
    console.error('======= dump contact =======')
423 424 425
    if (!this.obj) {
      throw new Error('no this.obj')
    }
426
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj && this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
427
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
428

L
lijiarui 已提交
429 430 431 432 433 434 435
  /**
   * Check if contact is self
   *
   * @returns {boolean} True for contact is self, False for contact is others
   * @example
   * const isSelf = contact.self()
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
436
  public self(): boolean {
437
    const userId = config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
438 439 440 441 442 443 444 445 446 447 448
                          .userId

    const selfId = this.id

    if (!userId || !selfId) {
      throw new Error('no user or no self id')
    }

    return selfId === userId
  }

449
  /**
L
lijiarui 已提交
450 451 452 453 454 455 456 457 458 459
   * The way to search Contact
   *
   * @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}
   */

  /**
   * Find contact by `name` or `alias`
L
lijiarui 已提交
460 461 462 463
   *
   * If use Contact.findAll() get the contact list of the bot.
   *
   * #### definition
L
lijiarui 已提交
464 465
   * - `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
L
lijiarui 已提交
466 467 468 469 470
   *
   * @static
   * @param {ContactQueryFilter} [queryArg]
   * @returns {Promise<Contact[]>}
   * @example
L
lijiarui 已提交
471 472 473
   * 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'
474
   */
ruiruibupt's avatar
3  
ruiruibupt 已提交
475
  public static async findAll(queryArg?: ContactQueryFilter): Promise<Contact[]> {
476 477
    let query: ContactQueryFilter
    if (queryArg) {
ruiruibupt's avatar
3  
ruiruibupt 已提交
478
      if (queryArg.remark) {
479
        log.warn('Contact', 'Contact.findAll({remark:%s}) DEPRECATED, use Contact.findAll({alias:%s}) instead.', queryArg.remark, queryArg.remark)
ruiruibupt's avatar
3  
ruiruibupt 已提交
480
        query = { alias: queryArg.remark}
ruiruibupt's avatar
#217  
ruiruibupt 已提交
481 482 483
      } else {
        query = queryArg
      }
484
    } else {
485 486
      query = { name: /.*/ }
    }
487

488
    // log.verbose('Cotnact', 'findAll({ name: %s })', query.name)
L
lijiarui 已提交
489 490
    log.verbose('Cotnact', 'findAll({ %s })',
                            Object.keys(query)
491
                                  .map(k => `${k}: ${query[k]}`)
L
lijiarui 已提交
492
                                  .join(', '),
493 494 495 496 497 498 499 500 501 502 503
              )

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

    let filterKey                     = Object.keys(query)[0]
    let filterValue: string | RegExp  = query[filterKey]

    const keyMap = {
      name:   'NickName',
504
      alias:  'RemarkName',
505 506 507 508 509 510
    }

    filterKey = keyMap[filterKey]
    if (!filterKey) {
      throw new Error('unsupport filter key')
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
511

512 513
    if (!filterValue) {
      throw new Error('filterValue not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
514 515
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
516 517 518 519 520 521
    /**
     * must be string because we need inject variable value
     * into code as variable name
     */
    let filterFunction: string

522 523 524 525 526
    if (filterValue instanceof RegExp) {
      filterFunction = `(function (c) { return ${filterValue.toString()}.test(c.${filterKey}) })`
    } else if (typeof filterValue === 'string') {
      filterValue = filterValue.replace(/'/g, '\\\'')
      filterFunction = `(function (c) { return c.${filterKey} === '${filterValue}' })`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
527 528 529 530
    } else {
      throw new Error('unsupport name type')
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
531 532 533 534 535 536 537 538 539 540 541
    try {
      const contactList = await config.puppetInstance()
                                  .contactFind(filterFunction)

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

    } catch (e) {
      log.error('Contact', 'findAll() rejected: %s', e.message)
      return [] // fail safe
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
542
  }
543

544
  public alias(): string | null
L
lijiarui 已提交
545

546
  public alias(newAlias: string): Promise<boolean>
L
lijiarui 已提交
547

548
  public alias(empty: null): Promise<boolean>
549

550 551 552
  /**
   * GET / SET / DELETE the alias for a contact
   *
L
lijiarui 已提交
553 554
   * Tests show it will failed if set alias too frequently(60 times in one minute).
   * @param {(none | string | null)} newAlias
555
   * @returns {(string | null | Promise<boolean>)}
L
lijiarui 已提交
556
   * @example <caption> GET the alias for a contact, return {(string | null)}</caption>
557 558 559 560 561 562 563
   * 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)
   * }
   *
L
lijiarui 已提交
564
   * @example <caption>SET the alias for a contact</caption>
565 566 567 568
   * const ret = await contact.alias('lijiarui')
   * if (ret) {
   *   console.log(`change ${contact.name()}'s alias successfully!`)
   * } else {
L
lijiarui 已提交
569
   *   console.log(`failed to change ${contact.name()} alias!`)
570 571
   * }
   *
L
lijiarui 已提交
572
   * @example <caption>DELETE the alias for a contact</caption>
573 574 575 576 577 578 579
   * const ret = await contact.alias(null)
   * if (ret) {
   *   console.log(`delete ${contact.name()}'s alias successfully!`)
   * } else {
   *   console.log(`failed to delete ${contact.name()}'s alias!`)
   * }
   */
580 581
  public alias(newAlias?: string|null): Promise<boolean> | string | null {
    log.silly('Contact', 'alias(%s)', newAlias || '')
582

583 584
    if (newAlias === undefined) {
      return this.obj && this.obj.alias || null
585 586
    }

587
    return config.puppetInstance()
588
                  .contactAlias(this, newAlias)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
589 590 591
                  .then(ret => {
                    if (ret) {
                      if (this.obj) {
592
                        this.obj.alias = newAlias
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
593
                      } else {
594
                        log.error('Contact', 'alias() without this.obj?')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
595 596
                      }
                    } else {
597
                      log.warn('Contact', 'alias(%s) fail', newAlias)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
598 599 600
                    }
                    return ret
                  })
601
                  .catch(e => {
602
                    log.error('Contact', 'alias(%s) rejected: %s', newAlias, e.message)
603
                    Raven.captureException(e)
604 605 606 607
                    return false // fail safe
                  })
  }

L
lijiarui 已提交
608 609 610
  /**
   * @private
   */
ruiruibupt's avatar
#217  
ruiruibupt 已提交
611
  // function should be deprecated
612 613 614 615
  public remark(newRemark?: string|null): Promise<boolean> | string | null {
    log.warn('Contact', 'remark(%s) DEPRECATED, use alias(%s) instead.')
    log.silly('Contact', 'remark(%s)', newRemark || '')

ruiruibupt's avatar
2  
ruiruibupt 已提交
616 617 618 619 620 621 622
    switch (newRemark) {
      case undefined:
        return this.alias()
      case null:
        return this.alias(null)
      default:
        return this.alias(newRemark)
623 624 625
    }
  }

626
  /**
L
lijiarui 已提交
627 628 629 630
   * Try to find a contact by filter: {name: string | RegExp} / {alias: string | RegExp}
   *
   * Find contact by name or alias, if the result more than one, return the first one.
   *
L
lijiarui 已提交
631
   * @static
632
   * @param {ContactQueryFilter} query
L
lijiarui 已提交
633 634 635 636
   * @returns {(Promise<Contact | null>)} If can find the contact, return Contact, or return null
   * @example
   * const contactFindByName = await Contact.find({ name:"ruirui"} )
   * const contactFindByAlias = await Contact.find({ alias:"lijiarui"} )
637
   */
638
  public static async find(query: ContactQueryFilter): Promise<Contact | null> {
ruiruibupt's avatar
1  
ruiruibupt 已提交
639
    log.verbose('Contact', 'find(%s)', JSON.stringify(query))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
640

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
641
    const contactList = await Contact.findAll(query)
642
    if (!contactList || !contactList.length) {
643
      return null
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
644
    }
645 646 647 648

    if (contactList.length > 1) {
      log.warn('Contact', 'function find(%s) get %d contacts, use the first one by default', JSON.stringify(query), contactList.length)
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
649
    return contactList[0]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
650 651
  }

L
lijiarui 已提交
652
  /**
L
lijiarui 已提交
653
   * @private
L
lijiarui 已提交
654
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
655
  public static load(id: string): Contact {
656
    if (!id || typeof id !== 'string') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
657
      throw new Error('Contact.load(): id not found')
658
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
659

660 661 662 663
    if (!(id in Contact.pool)) {
      Contact.pool[id] = new Contact(id)
    }
    return Contact.pool[id]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
664
  }
665

L
lijiarui 已提交
666
  /**
L
lijiarui 已提交
667
   * Sent Text to contact
L
lijiarui 已提交
668
   *
L
lijiarui 已提交
669
   * @param {string} text
L
lijiarui 已提交
670
   */
M
Mukaiu 已提交
671
  public async say(text: string)
L
lijiarui 已提交
672 673 674 675 676 677 678

  /**
   * Send Media File to Contact
   *
   * @param {MediaMessage} mediaMessage
   * @memberof Contact
   */
M
Mukaiu 已提交
679 680
  public async say(mediaMessage: MediaMessage)

L
lijiarui 已提交
681 682 683 684 685 686 687 688 689 690
  /**
   * Send Text or Media File to Contact.
   *
   * @param {(string | MediaMessage)} textOrMedia
   * @returns {Promise<boolean>}
   * @example
   * const contact = await Contact.find({name: 'lijiarui'})         // change 'lijiarui' to any of your contact name in wechat
   * await contact.say('welcome to wechaty!')
   * await contact.say(new MediaMessage(__dirname + '/wechaty.png') // put the filePath you want to send here
   */
691
  public async say(textOrMedia: string | MediaMessage): Promise<boolean> {
M
Mukaiu 已提交
692
    const content = textOrMedia instanceof MediaMessage ? textOrMedia.filename() : textOrMedia
693 694
    log.verbose('Contact', 'say(%s)', content)

695 696
    const bot = Wechaty.instance()
    const user = bot.self()
697

698 699 700
    if (!user) {
      throw new Error('no user')
    }
M
Mukaiu 已提交
701 702 703 704 705 706 707 708 709
    let m
    if (typeof textOrMedia === 'string') {
      m = new Message()
      m.content(textOrMedia)
    } else if (textOrMedia instanceof MediaMessage) {
      m = textOrMedia
    } else {
      throw new Error('not support args')
    }
710 711 712 713
    m.from(user)
    m.to(this)
    log.silly('Contact', 'say() from: %s to: %s content: %s', user.name(), this.name(), content)

714
    return await bot.send(m)
715 716
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
717
}
718

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