@juzi/wechaty
Version:
Wechaty is a RPA SDK for Chatbot Makers.
388 lines (331 loc) • 11.3 kB
text/typescript
/**
* 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.
*
*/
import * as PUPPET from '@juzi/wechaty-puppet'
import type { TagQueryFilter } from '@juzi/wechaty-puppet/dist/esm/src/schemas/tag.js'
import type { Constructor } from 'clone-class'
import { concurrencyExecuter } from 'rx-queue'
import { log } from '../config.js'
import { poolifyMixin } from '../user-mixins/poolify.js'
import assert from 'assert'
import { validationMixin } from '../user-mixins/validation.js'
import {
wechatifyMixin,
} from '../user-mixins/wechatify.js'
import type { ContactInterface } from './contact.js'
import type { TagGroupInterface } from './tag-group.js'
const MixinBase = wechatifyMixin(
poolifyMixin(
Object,
)<TagImplInterface>(),
)
class TagMixin extends MixinBase {
/**
*
* Instance properties
* @ignore
*
*/
payload?: PUPPET.payloads.Tag
/**
* @hideconstructor
*/
constructor (
public readonly id: string,
) {
super()
log.silly('Tag', 'constructor()')
}
type (): PUPPET.types.Tag {
return (this.payload && this.payload.type) || PUPPET.types.Tag.Unspecific
}
name (): string {
return (this.payload && this.payload.name) || ''
}
groupId (): string {
return (this.payload && this.payload.groupId) || ''
}
async group (): Promise<TagGroupInterface | undefined> {
return this.payload?.groupId ? this.wechaty.TagGroup.find({ id: this.payload.groupId }) : undefined
}
static async list (): Promise<TagInterface[]> {
log.verbose('Tag', 'list()')
const tagIdList = await this.wechaty.puppet.tagTagList()
let continuousErrorCount = 0
let totalErrorCount = 0
const totalErrorThreshold = Math.round(tagIdList.length / 5)
const idToTag = async (id: string) => {
if (!this.wechaty.isLoggedIn) {
throw new Error('wechaty not logged in')
}
const result = this.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
}
const CONCURRENCY = 17
const tagIterator = concurrencyExecuter(CONCURRENCY)(idToTag)(tagIdList)
const tagList: TagInterface[] = []
for await (const tag of tagIterator) {
if (tag) {
tagList.push(tag)
}
}
return tagList
}
static async find (filter: TagQueryFilter): Promise<TagInterface | undefined> {
log.silly('Tag', 'find(%s)', JSON.stringify(filter))
if (filter.id) {
const tag = (this.wechaty.Tag as any as typeof TagImpl).load(filter.id)
try {
await tag.ready()
} catch (e) {
this.wechaty.emitError(e)
return undefined
}
return tag
}
if (filter.name) {
const tags = (await this.wechaty.Tag.list()).filter(t => t.name() === filter.name)
if (tags.length > 0) {
return tags[0]
}
}
return undefined
// TODO: use a puppet method to find tag, like how contact and room do it
}
static async findMulti (filters: TagQueryFilter[]): Promise<TagInterface[] | undefined> {
log.silly('Tag', 'find(%s)', JSON.stringify(filters))
const filterIdList = filters.filter(i => !!i.id).map(i => i.id)
const filterNameList = filters.filter(i => !!i.name).map(i => i.name)
const resultSet = new Set<TagInterface>()
if (filterIdList.length) {
const tags = filterIdList.map(i => {
assert(i, 'Should not be undefined')
return (this.wechaty.Tag as any as typeof TagImpl).load(i)
})
try {
await Promise.all(tags.map(i => i.ready()))
} catch (e) {
this.wechaty.emitError(e)
return undefined
}
for (const tag of tags) {
resultSet.add(tag)
}
}
if (filterNameList.length) {
const list = await this.wechaty.Tag.list()
const tagMap = new Map<string, TagInterface>()
// FIXME: There are two tag types in wework, and the names can be repeated
list.forEach(i => tagMap.set(i.name(), i))
for (const name of filterNameList) {
assert(typeof name === 'string', 'Only supported string')
const tag = tagMap.get(name)
if (tag) {
resultSet.add(tag)
}
}
}
return resultSet.size ? Array.from(resultSet) : undefined
// TODO: use a puppet method to find tag, like how contact and room do it
}
/**
* Force reload data for Tag, Sync data from low-level API again.
*
* @returns {Promise<this>}
* @example
* await tag.sync()
*/
async sync (): Promise<void> {
await this.wechaty.puppet.tagPayloadDirty(this.id)
await this.ready(true)
}
/**
* @ignore
*/
isReady (): boolean {
return !!(this.payload && this.payload.name)
}
/**
* `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,
): Promise<void> {
log.silly('Tag', 'ready() @ %s with Tag key="%s"', this.wechaty.puppet, this.id)
if (!forceSync && this.isReady()) { // already ready
log.silly('Tag', 'ready() isReady() true')
return
}
try {
this.payload = await this.wechaty.puppet.tagPayload(this.id)
} catch (e) {
this.wechaty.emitError(e)
log.verbose('Tag', 'ready() this.wechaty.puppet.tagPayload(%s) exception: %s',
this.id,
(e as Error).message,
)
throw e
}
}
async contactList (): Promise<ContactInterface[]> {
log.verbose('Tag', 'contactList() for tag : %s', this)
const contactIds = await this.wechaty.puppet.tagTagContactList(this.id)
const contactPromises = contactIds.map(id => this.wechaty.Contact.find({ id }))
return (await Promise.all(contactPromises)).filter(contact => !!contact) as ContactInterface[]
}
async tag (contacts: ContactInterface | ContactInterface[]): Promise<void> {
log.verbose('Tag', 'tag(%s) for tag : %s', contacts, this)
let contactIds: string[]
if (Array.isArray(contacts)) {
contactIds = contacts.map(c => c.id)
} else {
contactIds = [ contacts.id ]
}
await this.wechaty.puppet.tagContactTagAdd([ this.id ], contactIds)
}
static async createTag (name: string, tagGroup?: TagGroupInterface): Promise<TagInterface | void> {
log.verbose('Tag', 'createTag(%s, %s)', tagGroup, name)
try {
const tagInfoList = await this.wechaty.puppet.tagTagAdd([ name ], tagGroup?.name())
if (tagInfoList?.length) {
const filter : PUPPET.filters.Tag = {
id: tagInfoList[0]?.id,
}
const newTag = await this.find(filter)
return newTag
}
} catch (e) {
this.wechaty.emitError(e)
log.error('Tag', 'createTag() exception: %s', (e as Error).message)
}
}
static async createMultiTag (nameList: string[], tagGroup?: TagGroupInterface): Promise<TagInterface[] | void> {
log.verbose('Tag', 'createMultiTag(%s, %s)', tagGroup, nameList)
try {
const tagInfoList = await this.wechaty.puppet.tagTagAdd(nameList, tagGroup?.name())
if (tagInfoList?.length) {
const filterList : PUPPET.filters.Tag[] = tagInfoList.map(i => ({
id: i.id,
}))
const newTagList = await this.findMulti(filterList)
return newTagList
}
} catch (e) {
this.wechaty.emitError(e)
log.error('Tag', 'createMultiTag() exception: %s', (e as Error).message)
}
}
static async deleteTag (tagInstance: TagInterface): Promise<void> {
log.verbose('Tag', 'deleteTag(%s, %s)', tagInstance)
try {
await this.wechaty.puppet.tagTagDelete([ tagInstance.id ])
} catch (e) {
this.wechaty.emitError(e)
log.error('Tag', 'deleteTag() exception: %s', (e as Error).message)
}
}
static async deleteMultiTag (tagInstances: TagInterface[]): Promise<void> {
log.verbose('Tag', 'deleteMultiTag(%s, %s)', tagInstances)
try {
await this.wechaty.puppet.tagTagDelete(tagInstances.map(i => i.id))
} catch (e) {
this.wechaty.emitError(e)
log.error('Tag', 'deleteMultiTag() exception: %s', (e as Error).message)
}
}
static async modifyTag (tagInstance: TagInterface, tagNewName: string): Promise<TagInterface | void> {
log.verbose('Tag', 'modifyTag(%s, %s)', tagInstance)
try {
const tagNewInfo: PUPPET.types.TagInfo = {
id: tagInstance.id,
name: tagNewName,
}
const tagInfoList = await this.wechaty.puppet.tagTagModify([ tagNewInfo ])
if (tagInfoList?.length) {
const filter : PUPPET.filters.Tag = {
id: tagInfoList[0]?.id,
}
const newTag = await this.find(filter)
return newTag
}
} catch (e) {
this.wechaty.emitError(e)
log.error('Tag', 'modifyTag() exception: %s', (e as Error).message)
}
}
static async modifyMultiTag (
tagInfos: Array<{ tag: TagInterface, newName: string }>,
): Promise<TagInterface[] | void> {
log.verbose('Tag', 'modifyMultiTag(%o)', tagInfos)
try {
const tagNewInfoList: PUPPET.types.TagInfo[] = tagInfos.map(i => ({
id: i.tag.id,
name: i.newName,
}))
const tagInfoList = await this.wechaty.puppet.tagTagModify(tagNewInfoList)
if (tagInfoList?.length) {
const filterList : PUPPET.filters.Tag[] = tagInfoList.map(i => ({
id: i.id,
}))
const newTagList = await this.findMulti(filterList)
return newTagList
}
} catch (e) {
this.wechaty.emitError(e)
log.error('Tag', 'modifyMultiTag() exception: %s', (e as Error).message)
}
}
override toString () {
return `<Tag#${this.name() || this.id}>`
}
}
class TagImplBase extends validationMixin(TagMixin)<TagImplInterface>() {}
interface TagImplInterface extends TagImplBase {}
type TagProtectedProperty =
| 'ready'
type TagInterface = Omit<TagImplInterface, TagProtectedProperty>
class TagImpl extends validationMixin(TagImplBase)<TagInterface>() {}
type TagConstructor = Constructor<
TagImplInterface,
Omit<typeof TagImpl, 'load'>
>
export type {
TagConstructor,
TagProtectedProperty,
TagInterface,
}
export {
TagImpl,
}