UNPKG

@juzi/wechaty

Version:

Wechaty is a RPA SDK for Chatbot Makers.

782 lines 29.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ContactImpl = void 0; /** * Wechaty Chatbot SDK - https://github.com/wechaty/wechaty * * @copyright 2016 Huan LI (李卓桓) <https://github.com/huan>, and * Wechaty Contributors <https://github.com/wechaty>. * * 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. * */ const PUPPET = __importStar(require("@juzi/wechaty-puppet")); const rx_queue_1 = require("rx-queue"); const config_js_1 = require("../config.js"); const mod_js_1 = require("../schemas/mod.js"); const mod_js_2 = require("../user-mixins/mod.js"); const mod_js_3 = require("../sayable/mod.js"); const stringify_filter_js_1 = require("../helper-functions/stringify-filter.js"); const MixinBase = (0, mod_js_2.wechatifyMixin)((0, mod_js_2.poolifyMixin)(mod_js_1.ContactEventEmitter)()); /** * All wechat contacts(friend) will be encapsulated as a Contact. * [Examples/Contact-Bot]{@link https://github.com/wechaty/wechaty/blob/1523c5e02be46ebe2cc172a744b2fbe53351540e/examples/contact-bot.ts} * * @property {string} id - Get Contact id. * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) */ class ContactMixin extends MixinBase { id; static Type = PUPPET.types.Contact; static Gender = PUPPET.types.ContactGender; /** * 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/wechaty/wechaty/issues/365} */ /** * 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. * * @static * @param {string | ContactQueryFilter} query `string` will search `name` & `alias` * @returns {(Promise<undefined | ContactInterface>)} If can find the contact, return Contact, or return null * @example * const bot = new Wechaty() * await bot.start() * const contactFindByName = await bot.Contact.find({ name:"ruirui"} ) * const contactFindByAlias = await bot.Contact.find({ alias:"lijiarui"} ) */ static async find(query) { config_js_1.log.silly('Contact', 'find(%s)', JSON.stringify(query, stringify_filter_js_1.stringifyFilter)); if (typeof query === 'object' && query.id) { let contact; if (this.wechaty.puppet.currentUserId === query.id) { /** * When the contact id is the currentUserId, return a ContactSelfImpl as the Contact */ contact = this.wechaty.ContactSelf.load(query.id); } else { contact = this.wechaty.Contact.load(query.id); } // const contact = (this.wechaty.Contact as any as typeof ContactImpl).load(query.id) try { await contact.ready(); } catch (e) { this.wechaty.emitError(e); return undefined; } return contact; } const contactList = await this.findAll(query); if (contactList.length <= 0) { return; } if (contactList.length > 1) { config_js_1.log.warn('Contact', 'find() got more than 1 result: %d total', contactList.length); } for (const [idx, contact] of contactList.entries()) { // use puppet.contactValidate() to confirm double confirm that this contactId is valid. // https://github.com/wechaty/wechaty-puppet-padchat/issues/64 // https://github.com/wechaty/wechaty/issues/1345 const valid = await this.wechaty.puppet.contactValidate(contact.id); if (valid) { config_js_1.log.silly('Contact', 'find() contact<id=%s> is valid, return it', idx, contact.id); return contact; } else { config_js_1.log.silly('Contact', 'find() contact<id=%s> is invalid, skip it', idx, contact.id); } } config_js_1.log.warn('Contact', 'find() all of %d contacts are invalid', contactList.length); return undefined; } /** * 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 {string | ContactQueryFilter} [queryArg] `string` will search `name` & `alias` * @returns {Promise<ContactInterface[]>} * @example * const bot = new Wechaty() * await bot.start() * const contactList = await bot.Contact.findAll() // get the contact list of the bot * const contactList = await bot.Contact.findAll({ name: 'ruirui' }) // find all of the contacts whose name is 'ruirui' * const contactList = await bot.Contact.findAll({ alias: 'lijiarui' }) // find all of the contacts whose alias is 'lijiarui' */ static async findAll(query) { config_js_1.log.verbose('Contact', 'findAll(%s)', JSON.stringify(query, stringify_filter_js_1.stringifyFilter) || ''); const contactIdList = await this.wechaty.puppet.contactSearch(query); let continuousErrorCount = 0; let totalErrorCount = 0; const totalErrorThreshold = Math.round(contactIdList.length / 5); const idToContact = async (id) => { if (!this.wechaty.isLoggedIn) { throw new Error('wechaty not logged in'); } const result = await this.wechaty.Contact.find({ id }).catch(e => { this.wechaty.emitError(e); continuousErrorCount++; totalErrorCount++; if (continuousErrorCount > 5) { throw new Error('5 continuous errors!'); } if (totalErrorCount > totalErrorThreshold) { throw new Error(`${totalErrorThreshold} total errors!`); } }); continuousErrorCount = 0; return result; }; /** * we need to use concurrencyExecuter to reduce the parallel number of the requests */ const CONCURRENCY = 17; const contactIterator = (0, rx_queue_1.concurrencyExecuter)(CONCURRENCY)(idToContact)(contactIdList); const contactList = []; for await (const contact of contactIterator) { if (contact) { contactList.push(contact); } } return contactList; } static async batchLoadContacts(contactIdList) { if (typeof this.wechaty.puppet.batchContactPayload === 'function') { const contactList = contactIdList.map(id => { if (this.wechaty.puppet.currentUserId === id) { return this.wechaty.ContactSelf.load(id); } else { return this.wechaty.Contact.load(id); } }); const needPayloadSet = new Set(); for (const contact of contactList) { if (!contact.isReady()) { needPayloadSet.add(contact.id); } } if (needPayloadSet.size > 0) { const payloadMap = await this.wechaty.puppet.batchContactPayload(Array.from(needPayloadSet)); for (const contact of contactList) { contact.payload = payloadMap.get(contact.id); } } return contactList; } else { let continuousErrorCount = 0; let totalErrorCount = 0; const totalErrorThreshold = Math.round(contactIdList.length / 5); const idToContact = async (id) => { if (!this.wechaty.isLoggedIn) { throw new Error('wechaty not logged in'); } const result = await this.wechaty.Contact.find({ id }).catch(e => { this.wechaty.emitError(e); continuousErrorCount++; totalErrorCount++; if (continuousErrorCount > 5) { throw new Error('5 continuous errors!'); } if (totalErrorCount > totalErrorThreshold) { throw new Error(`${totalErrorThreshold} total errors!`); } }); continuousErrorCount = 0; return result; }; /** * we need to use concurrencyExecuter to reduce the parallel number of the requests */ const CONCURRENCY = 17; const contactIterator = (0, rx_queue_1.concurrencyExecuter)(CONCURRENCY)(idToContact)(contactIdList); const contactList = []; for await (const contact of contactIterator) { if (contact) { contactList.push(contact); } } return contactList; } } // TODO // eslint-disable-next-line no-use-before-define static async delete(contact) { config_js_1.log.verbose('Contact', 'static delete(%s)', contact.id); await this.wechaty.puppet.contactDelete(contact.id); } /** * * Instance properties * @ignore * */ payload; /** * @hideconstructor */ constructor(id) { super(); this.id = id; config_js_1.log.silly('Contact', `constructor(${id})`); } /** * @ignore */ toString() { if (!this.payload) { return this.constructor.name; } const identity = this.payload.alias || this.payload.name || this.id || 'loading...'; return `Contact<${identity}>`; } /** * > Tips: * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) * * @param {(string | ContactInterface | FileBox | UrlLink | MiniProgram | Location)} sayable * send text, Contact, or file to contact. </br> * You can use {@link https://www.npmjs.com/package/file-box|FileBox} to send file * @returns {Promise<void | MessageInterface>} * @example * 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 * * // 1. send text to contact * * await contact.say('welcome to wechaty!') * const msg = await contact.say('welcome to wechaty!') // only supported by puppet-padplus * * // 2. send media file to contact * * import { FileBox } from 'wechaty' * const fileBox1 = FileBox.fromUrl('https://wechaty.github.io/wechaty/images/bot-qr-code.png') * const fileBox2 = FileBox.fromFile('/tmp/text.txt') * await contact.say(fileBox1) * const msg1 = await contact.say(fileBox1) // only supported by puppet-padplus * await contact.say(fileBox2) * const msg2 = await contact.say(fileBox2) // only supported by puppet-padplus * * // 3. send contact card to contact * * const contactCard = bot.Contact.load('contactId') * const msg = await contact.say(contactCard) // only supported by puppet-padplus * * // 4. send url link to contact * * const urlLink = new UrlLink ({ * 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', * url : 'https://github.com/wechaty/wechaty', * }) * await contact.say(urlLink) * const msg = await contact.say(urlLink) // only supported by puppet-padplus * * // 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) * const msg = await contact.say(miniProgram) // only supported by puppet-padplus * * // 6. send location to contact * const location = new Location ({ * accuracy : 15, * address : '北京市北京市海淀区45 Chengfu Rd', * latitude : 39.995120999999997, * longitude : 116.334154, * name : '东升乡人民政府(海淀区成府路45号)', * }) * await contact.say(location) * const msg = await contact.say(location) */ async say(sayable, options) { config_js_1.log.verbose('Contact', 'say(%s)', sayable); if (options?.mentionList) { config_js_1.log.warn('Contact', 'you cannot mention someone in private conversation!'); delete options.mentionList; } const msgId = await (0, mod_js_3.deliverSayableConversationPuppet)(this.wechaty.puppet)(this.id)(sayable, options); if (msgId) { const msg = await this.wechaty.Message.find({ id: msgId }); if (msg) { return msg; } } } /** * Get the name from a contact * * @returns {string} * @example * const name = contact.name() */ name() { return (this.payload && this.payload.name) || ''; } aka() { return (this.payload && this.payload.aka) || ''; } realName() { return (this.payload && this.payload.realName) || ''; } /** * 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 {(Promise<undefined | string | void>)} * @example <caption> GET the alias for a contact, return {(Promise<string | null>)}</caption> * const alias = await 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> * try { * await contact.alias('lijiarui') * console.log(`change ${contact.name()}'s alias successfully!`) * } catch (e) { * console.log(`failed to change ${contact.name()} alias!`) * } * * @example <caption>DELETE the alias for a contact</caption> * try { * const oldAlias = await contact.alias(null) * console.log(`delete ${contact.name()}'s alias successfully!`) * console.log('old alias is ${oldAlias}`) * } catch (e) { * console.log(`failed to delete ${contact.name()}'s alias!`) * } */ async alias(newAlias) { config_js_1.log.silly('Contact', 'alias(%s)', newAlias === undefined ? '' : newAlias); if (!this.payload) { throw new Error('no payload'); } if (typeof newAlias === 'undefined') { return this.payload.alias; } try { await this.wechaty.puppet.contactAlias(this.id, newAlias); await this.wechaty.puppet.contactPayloadDirty(this.id); /** * In normal puppet, the dirty event handler, onDirty, is a sync function, so the contactPayload will get the new payload * However for wechaty-puppet-service, it uses flashstore to cache payloads, and deleting a cache is an async function * So there is a chance contactPayload will still get old contact */ let maxCheck = 10; let changed = false; while (maxCheck-- > 0 && !changed) { await new Promise(resolve => { setTimeout(resolve, 300); }); this.payload = await this.wechaty.puppet.contactPayload(this.id); const payloadAlias = this.payload.alias || ''; changed = newAlias === payloadAlias; } if (!changed) { throw new Error('failed to modify clias, still got old alias after 10 tries'); } } catch (e) { this.wechaty.emitError(e); config_js_1.log.error('Contact', 'alias(%s) rejected: %s', newAlias, e.message); } } async phone(phoneList) { config_js_1.log.silly('Contact', 'phone(%s)', phoneList === undefined ? '' : JSON.stringify(phoneList)); if (!this.payload) { throw new Error('no payload'); } if (typeof phoneList === 'undefined') { return this.payload.phone; } try { await this.wechaty.puppet.contactPhone(this.id, phoneList); await this.wechaty.puppet.contactPayloadDirty(this.id); this.payload = await this.wechaty.puppet.contactPayload(this.id); } catch (e) { this.wechaty.emitError(e); config_js_1.log.error('Contact', 'phone(%s) rejected: %s', JSON.stringify(phoneList), e.message); } } async corporation(remark) { config_js_1.log.silly('Contact', 'corporation(%s)', remark); if (!this.payload) { throw new Error('no payload'); } if (typeof remark === 'undefined') { return this.payload.corporation; } if (this.payload.type !== PUPPET.types.Contact.Individual) { throw new Error('Can not set corporation remark on non individual contact.'); } try { await this.wechaty.puppet.contactCorporationRemark(this.id, remark); await this.wechaty.puppet.contactPayloadDirty(this.id); this.payload = await this.wechaty.puppet.contactPayload(this.id); } catch (e) { this.wechaty.emitError(e); config_js_1.log.error('Contact', 'corporation(%s) rejected: %s', remark, e.message); } } async description(newDescription) { config_js_1.log.silly('Contact', 'description(%s)', newDescription); if (!this.payload) { throw new Error('no payload'); } if (typeof newDescription === 'undefined') { return this.payload.description; } try { await this.wechaty.puppet.contactDescription(this.id, newDescription); await this.wechaty.puppet.contactPayloadDirty(this.id); this.payload = await this.wechaty.puppet.contactPayload(this.id); } catch (e) { this.wechaty.emitError(e); config_js_1.log.error('Contact', 'description(%s) rejected: %s', newDescription, e.message); } } title() { if (!this.payload) { throw new Error('no payload'); } return this.payload.title || null; } coworker() { if (!this.payload) { throw new Error('no payload'); } return !!this.payload.coworker; } /** * Check if contact is friend * * > Tips: * This function is depending on the Puppet Implementation, see [puppet-compatible-table](https://github.com/wechaty/wechaty/wiki/Puppet#3-puppet-compatible-table) * * @returns {boolean | null} * * <br>True for friend of the bot <br> * False for not friend of the bot, null for unknown. * @example * const isFriend = contact.friend() */ friend() { config_js_1.log.verbose('Contact', 'friend()'); return this.payload?.friend; } /** * 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 */ /** * Return the type of the Contact * > Tips: ContactType is enum here.</br> * @returns {ContactType.Unknown | ContactType.Personal | ContactType.Official} * * @example * const bot = new Wechaty() * await bot.start() * const isOfficial = contact.type() === bot.Contact.Type.Official */ type() { if (!this.payload) { throw new Error('no payload'); } return this.payload.type; } /** * @ignore * TODO: Check if the contact is star contact. * * @returns {boolean | null} - True for star friend, False for no star friend. * @example * const isStar = contact.star() */ star() { return this.payload?.star; } /** * Contact gender * > Tips: ContactGender is enum here. </br> * * @returns {ContactGender.Unknown | ContactGender.Male | ContactGender.Female} * @example * const gender = contact.gender() === bot.Contact.Gender.Male */ gender() { return this.payload ? this.payload.gender : PUPPET.types.ContactGender.Unknown; } /** * Get the region 'province' from a contact * * @returns {string | null} * @example * const province = contact.province() */ province() { return this.payload?.province; } /** * Get the region 'city' from a contact * * @returns {string | null} * @example * const city = contact.city() */ city() { return this.payload?.city; } /** * Get avatar picture file stream * * @returns {Promise<FileBox>} * @example * // Save avatar to local file like `1-name.jpg` * * const file = await contact.avatar() * const name = file.name * await file.toFile(name, true) * console.log(`Contact: ${contact.name()} with avatar file: ${name}`) */ async avatar() { config_js_1.log.verbose('Contact', 'avatar()'); const fileBox = await this.wechaty.puppet.contactAvatar(this.id); return fileBox; } /** * Get all tags of contact * * @returns {Promise<TagInterface[]>} * @example * const tags = await contact.tags() */ async tags() { config_js_1.log.verbose('Contact', 'tags() for %s', this); try { const tagPayloadList = this.payload?.tags || []; const tagListPromises = tagPayloadList.map(tagId => this.wechaty.Tag.find({ id: tagId })); const tagList = await Promise.all(tagListPromises); return tagList.filter(tag => !!tag); } catch (e) { this.wechaty.emitError(e); config_js_1.log.error('Contact', 'tags() exception: %s', e.message); return []; } } /** * Add a Tag */ async tag(tags) { config_js_1.log.verbose('Contact', 'tag(%s) for %s', JSON.stringify(tags), this); if (!Array.isArray(tags)) { tags = [tags]; } const tagIds = tags.map(tag => tag.id); await this.wechaty.puppet.tagContactTagAdd(tagIds, [this.id]); } /** * Remove a Tag */ async tagRemove(tags) { config_js_1.log.verbose('Contact', 'tagRemove(%s) for %s', JSON.stringify(tags), this); if (!Array.isArray(tags)) { tags = [tags]; } const tagIds = tags.map(tag => tag.id); await this.wechaty.puppet.tagContactTagRemove(tagIds, [this.id]); } /** * Force reload data for Contact, Sync data from low-level API again. * * @returns {Promise<this>} * @example * await contact.sync() */ async sync() { await this.wechaty.puppet.contactPayloadDirty(this.id); await this.ready(true); } /** * `ready()` is For FrameWork ONLY! * * Please not to use `ready()` at the user land. * If you want to sync data, use `sync()` instead. * * @ignore */ async ready(forceSync = false) { config_js_1.log.silly('Contact', 'ready() @ %s with id="%s"', this.wechaty.puppet, this.id); if (!forceSync && this.isReady()) { // already ready config_js_1.log.silly('Contact', 'ready() isReady() true'); return; } try { this.payload = await this.wechaty.puppet.contactPayload(this.id); // log.silly('Contact', `ready() this.wechaty.puppet.contactPayload(%s) resolved`, this) } catch (e) { this.wechaty.emitError(e); config_js_1.log.verbose('Contact', 'ready() this.wechaty.puppet.contactPayload(%s) exception: %s', this.id, e.message); throw e; } } /** * Mark the conversation as read * @param { undefined | boolean } hasRead * * @example * const bot = new Wechaty() * const contact = await bot.Contact.find({name: 'xxx'}) * await contact.readMark() */ async readMark(hasRead) { try { if (typeof hasRead === 'undefined') { return this.wechaty.puppet.conversationReadMark(this.id); } else { await this.wechaty.puppet.conversationReadMark(this.id, hasRead); } } catch (e) { this.wechaty.emitError(e); config_js_1.log.error('Contact', 'readMark() exception: %s', e.message); } } /** * @ignore */ isReady() { return !!(this.payload && this.payload.name); } /** * Check if contact is self * * @returns {boolean} True for contact is self, False for contact is others * @example * const isSelf = contact.self() */ self() { return this.id === this.wechaty.puppet.currentUserId; } /** * Get the handle from a contact. * * > A Twitter handle is the username that appears at the end of your unique Twitter URL. * * Sometimes cannot get handle due to the puppet implementation. * * @ignore * @returns {string | null} * @example * const handle = contact.handle() */ handle() { return this.payload?.handle; } /** * Huan(202203): `weixin()` will be removed in v2.0 * @link https://github.com/wechaty/puppet/issues/181 * @deprecated use `handle()` instead */ weixin() { // log.warn('Contact', 'weixin() is deprecated, use `handle()` instead.') // console.error(new Error().stack) return this.payload?.weixin; } additionalInfo() { let additionalInfoObj = {}; if (this.payload?.additionalInfo) { try { additionalInfoObj = JSON.parse(this.payload.additionalInfo); } catch (e) { config_js_1.log.warn('Contact', 'additionalInfo() parse failed, additionalInfo: %s', this.payload.additionalInfo); } } return additionalInfoObj; } async payloadModify(payload) { return this.wechaty.puppet.contactPayloadModify(this.id, payload); } } class ContactImplBase extends (0, mod_js_2.validationMixin)(ContactMixin)() { } class ContactImpl extends (0, mod_js_2.validationMixin)(ContactImplBase)() { } exports.ContactImpl = ContactImpl; //# sourceMappingURL=contact.js.map