UNPKG

@juzi/wechaty

Version:

Wechaty is a RPA SDK for Chatbot Makers.

637 lines (518 loc) 15.8 kB
/** * 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. * */ /** * Issue #2245 - New Wechaty User Module (WUM): * `Post` for supporting Moments, Channel, Tweet, Weibo, Facebook feeds, etc. * * @see https://github.com/wechaty/wechaty/issues/2245#issuecomment-914886835 */ import * as PUPPET from '@juzi/wechaty-puppet' import { log } from '@juzi/wechaty-puppet' import type { Constructor, } from 'clone-class' import { validationMixin, wechatifyMixinBase, } from '../user-mixins/mod.js' import type { Sayable } from '../sayable/mod.js' import { sayableToPayload, payloadToSayableWechaty, } from '../sayable/mod.js' import { ContactImpl, ContactInterface } from './contact.js' import { concurrencyExecuter } from 'rx-queue' import type { LocationInterface } from './location.js' interface Tap { contact: ContactInterface type: PUPPET.types.Tap, date: Date } class PostBuilder { payload: PUPPET.payloads.PostClient /** * Wechaty Sayable List */ sayableList: Sayable[] = [] /** * Huan(202201): why use Impl as a parameter? */ static new (Impl: typeof PostMixin) { return new this(Impl) } protected constructor ( protected Impl: typeof PostMixin, ) { this.payload = { sayableList: [], // Puppet Sayable Payload List type: PUPPET.types.Post.Unspecified, } } add (sayable: Sayable): this { this.sayableList.push(sayable) return this } type (type: PUPPET.types.Post): this { this.payload.type = type return this } reply (post: PostInterface): this { if (!post.id) { throw new Error('can not link to a post without id: ' + JSON.stringify(post)) } this.payload.parentId = post.payload.id this.payload.rootId = post.payload.rootId || post.payload.id return this } location (location: LocationInterface) { this.payload.location = { ...location.payload, } } visible (contactList: ContactInterface[]) { const contactIds = contactList.map(contact => { if (!ContactImpl.valid(contact)) { log.warn(`expect contact instance but ${contact} is not a contact`) return '' } return contact.id }) this.payload.visibleList = contactIds.filter(id => !!id) } async build (): Promise<PostInterface> { const sayablePayloadListNested = await Promise.all( this.sayableList.map(sayableToPayload), ) this.payload.sayableList = sayablePayloadListNested.filter(Boolean) as PUPPET.payloads.Sayable[] return this.Impl.create(this.payload) } } class PostMixin extends wechatifyMixinBase() { static builder (): PostBuilder { return PostBuilder.new(this) } /** * * Create * */ static create ( payload: PUPPET.payloads.PostClient, ): PostInterface { log.verbose('Post', 'create()') return new this(payload) } static load (id: string): PostInterface { log.verbose('Post', 'static load(%s)', id) /** * Must NOT use `Post` at here * MUST use `this` at here * * because the class will be `cloneClass`-ed */ const post = new this(id) return post } static async find ( filter: PUPPET.filters.Post, ): Promise<undefined | PostInterface> { log.verbose('Post', 'find(%s)', JSON.stringify(filter), ) if (filter.id) { const post = this.wechaty.Post.load(filter.id) await post.ready() return post } const [ postList ] = await this.findAll(filter, { pageSize: 1 }) if (postList.length > 0) { return postList[0] } return undefined } static async findAll ( filter : PUPPET.filters.Post, pagination? : PUPPET.filters.PaginationRequest, ): Promise<[ postList : PostInterface[], nextPageToken? : string, ]> { log.verbose('Post', 'findAll(%s%s)', JSON.stringify(filter), pagination ? ', ' + JSON.stringify(pagination) : '', ) const { nextPageToken, response: postIdList, } = await this.wechaty.puppet.postSearch( filter, pagination, ) const idToPost = async (id: string) => this.wechaty.Post.find({ id }) .catch(e => this.wechaty.emitError(e)) /** * we need to use concurrencyExecuter to reduce the parallel number of the requests */ const CONCURRENCY = 17 const postIterator = concurrencyExecuter(CONCURRENCY)(idToPost)(postIdList) const postList: PostInterface[] = [] for await (const post of postIterator) { if (post) { postList.push(post) } } return [ postList, nextPageToken ] } protected _payload?: PUPPET.payloads.Post get payload (): PUPPET.payloads.Post { if (!this._payload) { throw new Error('no payload, need to call `ready()` first.') } return this._payload } readonly id?: string /* * @hideconstructor */ constructor ( idOrPayload: string | PUPPET.payloads.Post, ) { super() log.verbose('Post', 'constructor(%s)', typeof idOrPayload === 'string' ? idOrPayload : JSON.stringify(idOrPayload.id), ) if (typeof idOrPayload === 'string') { this.id = idOrPayload } else { this._payload = idOrPayload this.id = idOrPayload.id } } counter (): PUPPET.payloads.PostServer['counter'] { return { children : 0, descendant : 0, taps : {}, ...(PUPPET.payloads.isPostServer(this.payload) && this.payload.counter), } } async author (): Promise<ContactInterface> { log.silly('Post', 'author()') if (PUPPET.payloads.isPostClient(this.payload)) { return this.wechaty.currentUser } const author = await this.wechaty.Contact.find({ id: this.payload.contactId }) if (!author) { throw new Error('no author for id: ' + this.payload.contactId) } return author } async root (): Promise<undefined | PostInterface> { log.silly('Post', 'root()') if (!this.payload.rootId) { return undefined } const post = this.wechaty.Post.load(this.payload.rootId) await post.ready() return post } async parent (): Promise<undefined | PostInterface> { log.silly('Post', 'parent()') if (!this.payload.parentId) { return undefined } const post = this.wechaty.Post.load(this.payload.parentId) await post.ready() return post } async sync (): Promise<void> { log.silly('Post', 'sync()') if (!this.id) { throw new Error('no post id found') } this._payload = await this.wechaty.puppet.postPayload(this.id) } async ready (): Promise<void> { log.silly('Post', 'ready()') if (!this.id) { throw new Error('no post id found') } if (this._payload) { return } await this.sync() } async * [Symbol.asyncIterator] (): AsyncIterableIterator<Sayable> { log.verbose('Post', '[Symbol.asyncIterator]()') const payloadToSayable = payloadToSayableWechaty(this.wechaty) if (PUPPET.payloads.isPostServer(this.payload)) { for (const sayableId of this.payload.sayableList) { const sayable = await this.getSayableWithId(sayableId) if (sayable) { yield sayable } } } else { // client for (const sayablePayload of this.payload.sayableList) { const sayable = await payloadToSayable(sayablePayload) if (sayable) { yield sayable } } } } async getSayableWithIndex (sayableIndex: number) { log.verbose('Post', 'getSayableWithIndex(%s)', sayableIndex) const payloadToSayable = payloadToSayableWechaty(this.wechaty) if (PUPPET.payloads.isPostServer(this.payload)) { const sayablePayload = await this.wechaty.puppet.postPayloadSayable(this.id!, this.payload.sayableList[sayableIndex]!) const sayable = await payloadToSayable(sayablePayload) return sayable } else { const sayablePayload = this.payload.sayableList[sayableIndex] if (sayablePayload) { const sayable = await payloadToSayable(sayablePayload) return sayable } else { throw new Error(`post has no sayable with index ${sayableIndex}`) } } } async getSayableWithId (id: string) { log.verbose('Post', 'getSayableWithId(%s)', id) if (PUPPET.payloads.isPostServer(this.payload)) { const payloadToSayable = payloadToSayableWechaty(this.wechaty) const sayablePayload = await this.wechaty.puppet.postPayloadSayable(this.id!, id) const sayable = await payloadToSayable(sayablePayload) return sayable } else { throw new Error('client post sayable has no Id') } } async * children ( filter: PUPPET.filters.Post = {}, ): AsyncIterableIterator<PostInterface> { log.verbose('Post', '*children(%s)', Object.keys(filter).length ? JSON.stringify(filter) : '') const pagination: PUPPET.filters.PaginationRequest = { pageSize: 100, } const parentIdFilter = { ...filter, parentId: this.id, } let [ postList, nextPageToken ] = await this.wechaty.Post.findAll( parentIdFilter, pagination, ) while (true) { yield * postList postList.length = 0 if (!nextPageToken) { break } [ postList, nextPageToken ] = await this.wechaty.Post.findAll( parentIdFilter, { ...pagination, pageToken: nextPageToken, }, ) } } async * descendants ( filter: PUPPET.filters.Post = {}, ): AsyncIterableIterator<PostInterface> { log.verbose('Post', '*descendants(%s)', Object.keys(filter).length ? JSON.stringify(filter) : '') for await (const post of this.children(filter)) { yield post yield * post.descendants(filter) } } async * likes ( filter: PUPPET.filters.Post = {}, ): AsyncIterableIterator<Tap> { log.verbose('Post', '*likes(%s)', Object.keys(filter).length ? JSON.stringify(filter) : '') return this.taps({ ...filter, type: PUPPET.types.Tap.Like, }) } async * taps ( filter: PUPPET.filters.Tap = {}, ): AsyncIterableIterator<Tap> { log.verbose('Post', '*taps(%s)', Object.keys(filter).length ? JSON.stringify(filter) : '') const pagination: PUPPET.filters.PaginationRequest = {} let [ tapList, nextPageToken ] = await this.tapFind( filter, pagination, ) while (true) { yield * tapList tapList.length = 0 if (!nextPageToken) { break } [ tapList, nextPageToken ] = await this.tapFind( filter, { ...pagination, pageToken: nextPageToken }, ) } } async reply ( sayable: | Exclude<Sayable, PostInterface> | Exclude<Sayable, PostInterface>[], ): Promise<void | PostInterface> { log.verbose('Post', 'reply(%s)', sayable) if (!this.id) { console.error('You can only call `reply()` on received posts, but it seems that you are trying to call reply on a post created from local.') throw new Error('no post id found') } const builder = this.wechaty.Post.builder() if (Array.isArray(sayable)) { sayable.forEach(s => builder.add(s)) } else { builder.add(sayable) } const post = await builder .reply(this) .build() const postId = await this.wechaty.puppet.postPublish(post.payload) if (postId) { const newPost = this.wechaty.Post.load(postId) await newPost.ready() return newPost } } async like (status: boolean) : Promise<void> async like () : Promise<boolean> async like (status?: boolean): Promise<void | boolean> { log.verbose('Post', 'like(%s)', typeof status === 'undefined' ? '' : status) if (typeof status === 'undefined') { return this.tap( PUPPET.types.Tap.Like, ) } else { return this.tap( PUPPET.types.Tap.Like, status, ) } } /** * Return Date if the bot has tapped the post, otherwise return undefined */ async tap (type: PUPPET.types.Tap) : Promise<boolean> async tap (type: PUPPET.types.Tap, status: boolean) : Promise<void> async tap ( type : PUPPET.types.Tap, status? : boolean, ): Promise<void | boolean> { log.verbose('Post', 'tap(%s%s)', PUPPET.types.Tap[type], typeof status === 'undefined' ? '' : ', ' + status, ) if (!this.id) { throw new Error('can not tap for post without id') } return this.wechaty.puppet.tap(this.id, type, status) } async tapFind ( filter : PUPPET.filters.Tap, pagination? : PUPPET.filters.PaginationRequest, ): Promise<[ tapList : Tap[], nextPageToken? : string, ]> { log.verbose('Post', 'tapFind()') if (!this.id) { throw new Error('can not get tapFind for client created post') } const { nextPageToken, response, } = await this.wechaty.puppet.tapSearch( this.id, filter, pagination, ) const tapList: Tap[] = [] for (const [ type, data ] of Object.entries(response)) { for (const [ i, contactId ] of data.contactId.entries()) { const contact = await this.wechaty.Contact.find({ id: contactId }) if (!contact) { log.warn('Post', 'tapFind() contact not found for id: %s', contactId) continue } const timestamp = data.timestamp[i] const date = timestamp ? new Date(timestamp) : new Date() tapList.push({ contact, date, type: Number(type) as PUPPET.types.Tap, }) } } return [ tapList, nextPageToken ] } location (): LocationInterface | undefined { log.verbose('Post', 'location()') if (!this.payload.location) { log.warn('this post has no location info') return } return new this.wechaty.Location(this.payload.location) } async visibleList (): Promise<ContactInterface[]> { log.verbose('Post', 'visibleList()') if (!this.payload.visibleList) { return [] } const contactIdList: string[] = this.payload.visibleList const idToContact = async (id: string) => this.wechaty.Contact.find({ id }).catch(e => this.wechaty.emitError(e)) /** * we need to use concurrencyExecuter to reduce the parallel number of the requests */ const CONCURRENCY = 17 const contactIterator = concurrencyExecuter(CONCURRENCY)(idToContact)(contactIdList) const contactList: ContactInterface[] = [] for await (const contact of contactIterator) { if (contact) { contactList.push(contact) } } return contactList } } class PostImpl extends validationMixin(PostMixin)<PostInterface>() {} interface PostInterface extends PostImpl {} type PostConstructor = Constructor< PostInterface, typeof PostImpl > export type { PostConstructor, PostInterface, } export { PostBuilder, PostImpl, }