<commit_before>import {
  Config,
  Sayable,
}                     from './config'
import {
  Message,
  MediaMessage,
}                     from './message'
import { PuppetWeb }  from './puppet-web'
import { UtilLib }    from './util-lib'
import { Wechaty }    from './wechaty'
import { log }        from './brolog-env'

interface ContactObj {
  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
  official:   boolean,
  special:    boolean,
}

export interface ContactRawObj {
  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
  VerifyFlag:   number,
}

/**
 * Enum for Gender values.
 * @enum {number}
 */
export enum Gender {
  Unknown = 0,
  Male    = 1,
  Female  = 2,
}

export interface ContactQueryFilter {
  name?:   string | RegExp,
  alias?:  string | RegExp,
  // DEPRECATED
  remark?: string | RegExp,
}

/**
 * @see https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3848
 */
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',
]

/**
 * Class Contact
 *
 * `Contact` is `Sayable`
 */
export class Contact implements Sayable {
  private static pool = new Map<string, Contact>()

  private obj: ContactObj | null
  private dirtyObj: ContactObj | null
  private rawObj: ContactRawObj

  constructor(public readonly id: string) {
    log.silly('Contact', `constructor(${id})`)

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

  public toString(): string {
    if (!this.obj) {
      return this.id
    }
    return this.obj.alias || this.obj.name || this.id
  }

  public toStringEx() { return `Contact(${this.obj && this.obj.name}[${this.id}])` }

  private parse(rawObj: ContactRawObj): ContactObj | null {
    if (!rawObj || !rawObj.UserName) {
      log.warn('Contact', 'parse() got empty rawObj!')
    }

    return !rawObj ? null : {
      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,
      /**
       * @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
       */
      // 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
       */
      special:       specialContactList.indexOf(rawObj.UserName) > -1 || /@qqim$/.test(rawObj.UserName),
    }
  }

  /**
   * Get the weixin number from a contact
   * Sometimes cannot get weixin number due to weixin security mechanism, not recommend.
   * @returns {string | null}
   *
   * @example
   * ```ts
   * const weixin = contact.weixin()
   * ```
   */
  public weixin()   { return this.obj && this.obj.weixin || null }

  /**
   * Get the name from a contact
   *
   * @returns {string}
   *
   * @example
   * ```ts
   * const name = contact.name()
   * ```
   */
  public name()     { return UtilLib.plainText(this.obj && this.obj.name || '') }

  /**
   * Check if contact is stranger
   *
   * @returns {boolean | null} True for not friend of the bot, False for friend of the bot, null for cannot get the info.
   *
   * @example
   * ```ts
   * const isStranger = contact.stranger()
   * ```
   */
  public stranger(): boolean|null {
    if (!this.obj) return null
    return this.obj.stranger<commit_msg>fix(contact) better warn message if weixin() can not get id from a contact<commit_after>import {
  Config,
  Sayable,
}                     from './config'
import {
  Message,
  MediaMessage,
}                     from './message'
import { PuppetWeb }  from './puppet-web'
import { UtilLib }    from './util-lib'
import { Wechaty }    from './wechaty'
import { log }        from './brolog-env'

interface ContactObj {
  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
  official:   boolean,
  special:    boolean,
}

export interface ContactRawObj {
  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
  VerifyFlag:   number,
}

/**
 * Enum for Gender values.
 * @enum {number}
 */
export enum Gender {
  Unknown = 0,
  Male    = 1,
  Female  = 2,
}

export interface ContactQueryFilter {
  name?:   string | RegExp,
  alias?:  string | RegExp,
  // DEPRECATED
  remark?: string | RegExp,
}

/**
 * @see https://github.com/Chatie/webwx-app-tracker/blob/7c59d35c6ea0cff38426a4c5c912a086c4c512b2/formatted/webwxApp.js#L3848
 */
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',
]

/**
 * Class Contact
 *
 * `Contact` is `Sayable`
 */
export class Contact implements Sayable {
  private static pool = new Map<string, Contact>()

  private obj: ContactObj | null
  private dirtyObj: ContactObj | null
  private rawObj: ContactRawObj

  constructor(public readonly id: string) {
    log.silly('Contact', `constructor(${id})`)

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

  public toString(): string {
    if (!this.obj) {
      return this.id
    }
    return this.obj.alias || this.obj.name || this.id
  }

  public toStringEx() { return `Contact(${this.obj && this.obj.name}[${this.id}])` }

  private parse(rawObj: ContactRawObj): ContactObj | null {
    if (!rawObj || !rawObj.UserName) {
      log.warn('Contact', 'parse() got empty rawObj!')
    }

    return !rawObj ? null : {
      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,
      /**
       * @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
       */
      // 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
       */
      special:       specialContactList.indexOf(rawObj.UserName) > -1 || /@qqim$/.test(rawObj.UserName),
    }
  }

  /**
   * Get the weixin number from a contact
   * Sometimes cannot get weixin number due to weixin security mechanism, not recommend.
   * @returns {string | null}
   *
   * @example
   * ```ts
   * const weixin = contact.weixin()
   * ```
   */
  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`)
      log.silly('Contact', 'weixin() If you want to track a contact between sessions, see FAQ at')
      log.silly('Contact', 'https://github.com/Chatie/wechaty/wiki/FAQ#1-how-to-get-the-permanent-id-for-a-contact')
    }
    return wxId
  }

  /**
   * Get the name from a contact
   *
   * @returns {string}
   *
   * @example
   * ```ts
   * const name = contact.name()
   * ```
   */
  public name()     { return UtilLib.plainText(this.obj && this.obj.name || '') }

  /**
   * Check if contact is stranger
   *
   * @returns {boolean | null} True for not friend of the bot, False for friend of the bot, null for cannot get the info.
   *
   * @example
   * ```ts
   * const isStranger = contact.stranger()
   * ```
   */
  public stranger(): boolean|null {
    if (!this.obj) return null
    return this.obj.stranger
{'fraction_non_alphanumeric': 0.1382051527631453, 'fraction_numerical': 0.021358107460971172, 'mean_word_length': 2.608154803040774, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 7, 'https://': 9, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4641225', 'n_tokens_mistral': 3745, 'n_tokens_neox': 3595, 'n_words': 825}