contact.ts 10.3 KB
Newer Older
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
1 2 3 4 5 6 7 8
/**
 *
 * wechaty: Wechat for Bot. and for human who talk to bot/robot
 *
 * Licenst: ISC
 * https://github.com/zixia/wechaty
 *
 */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
9 10 11
import {
    Config
  , Sayable
12 13 14 15 16 17
}                     from './config'
import { Message }    from './message'
import { PuppetWeb }  from './puppet-web'
import { UtilLib }    from './util-lib'
import { Wechaty }    from './wechaty'
import { log }        from './brolog-env'
18

19 20
type ContactObj = {
  address:    string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
21 22 23 24
  city:       string
  id:         string
  name:       string
  province:   string
25
  remark:     string|null
26
  sex:        Gender
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
27 28 29 30 31
  signature:  string
  star:       boolean
  stranger:   boolean
  uin:        string
  weixin:     string
32
  avatar:     string  // XXX URL of HeadImgUrl
33 34
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
35
export type ContactRawObj = {
36 37 38
  Alias:        string
  City:         string
  NickName:     string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
39 40
  Province:     string
  RemarkName:   string
41
  Sex:          Gender
42
  Signature:    string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
43 44 45
  StarFriend:   string
  Uin:          string
  UserName:     string
46
  HeadImgUrl:   string
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
47 48

  stranger:     string // assign by injectio.js
49 50
}

51 52 53 54 55 56
export enum Gender {
  Unknown = 0,
  Male    = 1,
  Female  = 2,
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
57
export type ContactQueryFilter = {
58 59
  name?: string | RegExp
  remark?: string | RegExp
60 61
}

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
62
export class Contact implements Sayable {
63 64
  private static pool = new Map<string, Contact>()

65
  private obj: ContactObj | null
66
  private dirtyObj: ContactObj | null
67 68 69
  private rawObj: ContactRawObj

  constructor(public readonly id: string) {
70
    log.silly('Contact', `constructor(${id})`)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
71

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

77 78 79 80 81 82 83
  public toString(): string {
    if (!this.obj) {
      return this.id
    }
    return this.obj.remark || this.obj.name || this.id
  }

84
  public toStringEx() { return `Contact(${this.obj && this.obj.name}[${this.id}])` }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
85

86
  private parse(rawObj: ContactRawObj): ContactObj | null {
87 88 89 90
    if (!rawObj) {
      log.warn('Contact', 'parse() got empty rawObj!')
    }

91
    return !rawObj ? null : {
92
      id:           rawObj.UserName // MMActualSender??? MMPeerUserName??? `getUserContact(message.MMActualSender,message.MMPeerUserName).HeadImgUrl`
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
93
      , uin:        rawObj.Uin    // stable id: 4763975 || getCookie("wxuin")
94
      , weixin:     rawObj.Alias  // Wechat ID
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
95 96 97
      , name:       rawObj.NickName
      , remark:     rawObj.RemarkName
      , sex:        rawObj.Sex
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
98 99 100
      , province:   rawObj.Province
      , city:       rawObj.City
      , signature:  rawObj.Signature
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
101

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

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
104 105
      , star:       !!rawObj.StarFriend
      , stranger:   !!rawObj.stranger // assign by injectio.js
106
      , avatar:     rawObj.HeadImgUrl
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
107 108
    }
  }
109

110 111 112 113 114 115
  public weixin()   { return this.obj && this.obj.weixin || '' }
  public name()     { return UtilLib.plainText(this.obj && this.obj.name || '') }
  public stranger() { return this.obj && this.obj.stranger }
  public star()     { return this.obj && this.obj.star }
  /**
   * Contact gender
116
   * @return Gender.Male(2) | Gender.Female(1) | Gender.Unknown(0)
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
   */
  public gender()   { return this.obj ? this.obj.sex : Gender.Unknown }
  public province() { return this.obj && this.obj.province }
  public city()     { return this.obj && this.obj.city }

  /**
   * Get avatar picture file stream
   */
  public async avatar(): Promise<NodeJS.ReadableStream> {
    if (!this.obj || !this.obj.avatar) {
      throw new Error('Can not get avatar: not ready')
    }

    try {
      const cookies = await (Config.puppetInstance() as PuppetWeb).browser.readCookie()
      return UtilLib.urlStream(this.obj.avatar, cookies)
    } catch (e) {
      log.warn('Contact', 'avatar() exception: %s', e.stack)
      throw e
    }
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
138

139
  public get(prop)  { return this.obj && this.obj[prop] }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
140

141
  public isReady(): boolean {
142
    return !!(this.obj && this.obj.id && this.obj.name !== undefined)
143 144
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
145 146 147 148 149
  // public refresh() {
  //   log.warn('Contact', 'refresh() DEPRECATED. use reload() instead.')
  //   return this.reload()
  // }

150 151 152 153 154 155 156 157
  public async refresh(): Promise<this> {
    if (this.isReady()) {
      this.dirtyObj = this.obj
    }
    this.obj = null
    return this.ready()
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
158 159 160 161 162
  // public ready() {
  //   log.warn('Contact', 'ready() DEPRECATED. use load() instead.')
  //   return this.load()
  // }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
163
  public async ready(contactGetter?: (id: string) => Promise<ContactRawObj>): Promise<this> {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
164
    log.silly('Contact', 'ready(' + (contactGetter ? typeof contactGetter : '') + ')')
165
    if (!this.id) {
166 167
      const e = new Error('ready() call on an un-inited contact')
      throw e
168
    }
169

170
    if (this.isReady()) { // already ready
171 172
      return Promise.resolve(this)
    }
173 174

    if (!contactGetter) {
175 176
      log.silly('Contact', 'get contact via ' + Config.puppetInstance().constructor.name)
      contactGetter = Config.puppetInstance()
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
177 178
                            .getContact.bind(Config.puppetInstance())
    }
179 180 181
    if (!contactGetter) {
      throw new Error('no contatGetter')
    }
182

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
183 184 185 186 187 188
    try {
      const rawObj = await contactGetter(this.id)
      log.silly('Contact', `contactGetter(${this.id}) resolved`)
      this.rawObj = rawObj
      this.obj    = this.parse(rawObj)
      return this
189

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
190 191 192
    } catch (e) {
      log.error('Contact', `contactGetter(${this.id}) exception: %s`, e.message)
      throw e
193 194 195
    }
  }

196
  public dumpRaw() {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
197
    console.error('======= dump raw contact =======')
198
    Object.keys(this.rawObj).forEach(k => console.error(`${k}: ${this.rawObj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
199
  }
200
  public dump()    {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
201
    console.error('======= dump contact =======')
202
    Object.keys(this.obj).forEach(k => console.error(`${k}: ${this.obj && this.obj[k]}`))
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
203
  }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
204

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
205 206 207 208 209 210 211 212 213 214 215 216 217
  public self(): boolean {
    const userId = Config.puppetInstance()
                          .userId

    const selfId = this.id

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

    return selfId === userId
  }

218 219 220 221 222 223 224 225
  /**
   * find contact by `name`(NickName) or `remark`(RemarkName)
   */
  public static findAll(queryArg?: ContactQueryFilter): Promise<Contact[]> {
    let query: ContactQueryFilter
    if (queryArg) {
      query = queryArg
    } else {
226 227
      query = { name: /.*/ }
    }
228

229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
    // log.verbose('Cotnact', 'findAll({ name: %s })', query.name)
    log.verbose('Cotnact', 'findAll({ %s })'
                          , Object.keys(query)
                                  .map(k => `${k}: ${query[k]}`)
                                  .join(', ')
              )

    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',
      remark: 'RemarkName',
    }

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

253 254
    if (!filterValue) {
      throw new Error('filterValue not found')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
255 256
    }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
257 258 259 260 261 262
    /**
     * must be string because we need inject variable value
     * into code as variable name
     */
    let filterFunction: string

263 264 265 266 267
    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 (李卓桓) 已提交
268 269 270 271 272 273 274
    } else {
      throw new Error('unsupport name type')
    }

    return Config.puppetInstance()
                  .contactFind(filterFunction)
                  .catch(e => {
275 276
                    log.error('Contact', 'findAll() rejected: %s', e.message)
                    return [] // fail safe
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
277
                  })
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
278
  }
279

280 281 282
  /**
   * get the remark for contact
   */
283
  public remark(): string | null
284 285
  /**
   * set the remark for contact
Huan (李卓桓)'s avatar
#119  
Huan (李卓桓) 已提交
286
   * @return {Promise<boolean>} A promise to the result. true for success, false for failure
287 288
   */
  public remark(newRemark: string): Promise<boolean>
289 290 291 292
  /**
   * delete the remark for a contact
   */
  public remark(empty: null): Promise<boolean>
293

294
  public remark(newRemark?: string|null): Promise<boolean> | string | null {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
295
    log.silly('Contact', 'remark(%s)', newRemark || '')
296 297

    if (newRemark === undefined) {
298
      return this.obj && this.obj.remark || null
299 300 301 302
    }

    return Config.puppetInstance()
                  .contactRemark(this, newRemark)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
303 304 305 306 307
                  .then(ret => {
                    if (ret) {
                      if (this.obj) {
                        this.obj.remark = newRemark
                      } else {
308
                        log.error('Contact', 'remark() without this.obj?')
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
309 310 311 312 313 314
                      }
                    } else {
                      log.warn('Contact', 'remark(%s) fail', newRemark)
                    }
                    return ret
                  })
315 316 317 318 319 320 321 322 323
                  .catch(e => {
                    log.error('Contact', 'remark(%s) rejected: %s', newRemark, e.message)
                    return false // fail safe
                  })
  }

  /**
   * try to find a contact by filter: {name: string | RegExp}
   */
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
324
  public static async find(query: ContactQueryFilter): Promise<Contact> {
325
    log.verbose('Contact', 'find(%s)', query.name)
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
326

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
327
    const contactList = await Contact.findAll(query)
328
    if (!contactList || !contactList.length) {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
329 330 331
      throw new Error('find not found any contact')
    }
    return contactList[0]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
332 333
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
334
  public static load(id: string): Contact {
335
    if (!id || typeof id !== 'string') {
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
336
      throw new Error('Contact.load(): id not found')
337
    }
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
338

339 340 341 342
    if (!(id in Contact.pool)) {
      Contact.pool[id] = new Contact(id)
    }
    return Contact.pool[id]
Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
343
  }
344

345
  public async say(content: string): Promise<void> {
346 347 348 349 350
    log.verbose('Contact', 'say(%s)', content)

    const wechaty = Wechaty.instance()
    const user = wechaty.user()

351 352 353
    if (!user) {
      throw new Error('no user')
    }
354 355 356 357 358 359 360
    const m = new Message()
    m.from(user)
    m.to(this)
    m.content(content)

    log.silly('Contact', 'say() from: %s to: %s content: %s', user.name(), this.name(), content)

361 362
    await wechaty.send(m)
    return
363 364
  }

Huan (李卓桓)'s avatar
Huan (李卓桓) 已提交
365
}
366

367 368 369 370 371 372 373 374 375 376
// Contact.search = function(options) {
//   if (options.name) {
//     const regex = new RegExp(options.name)
//     return Object.keys(Contact.pool)
//     .filter(k => regex.test(Contact.pool[k].name()))
//     .map(k => Contact.pool[k])
//   }

//   return []
// }