From fb3591bd3fc2a8e440520efdb1cc6c77afa1d6d9 Mon Sep 17 00:00:00 2001 From: jlachor Date: Tue, 22 Jul 2025 10:35:56 +0200 Subject: [PATCH 01/15] Create Blog Logic interfaces --- packages/blog-logic/interfaces.ts | 159 ++++++++++++++++++ .../optimistic-actions-interfaces.ts | 54 ++++++ packages/blog-logic/optimistic_actions.ts | 136 +++++++++++++++ packages/blog-logic/rest-api.md | 25 +++ packages/blog-logic/wordpress-reference.ts | 85 ++++++++++ 5 files changed, 459 insertions(+) create mode 100644 packages/blog-logic/interfaces.ts create mode 100644 packages/blog-logic/optimistic-actions-interfaces.ts create mode 100644 packages/blog-logic/optimistic_actions.ts create mode 100644 packages/blog-logic/rest-api.md create mode 100644 packages/blog-logic/wordpress-reference.ts diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts new file mode 100644 index 0000000..a5c2323 --- /dev/null +++ b/packages/blog-logic/interfaces.ts @@ -0,0 +1,159 @@ +// WORK IN PROGRESS +import type { TAccountName, IOnlineSignatureProvider } from "@hiveio/wax"; +import { type Observer } from "../../src/types/subscribable"; + +export interface IPagination { + page: number; + pageSize: number; +} + +export interface ICommonFilters { + readonly startTime?: Date; + readonly endTime?: Date; +} + +export interface IVotesFilters extends ICommonFilters { + readonly isUpvote?: boolean; + readonly voterName?: string; + readonly sortBy?: "date" | "weight" | "voter"; +} + +export interface IPostCommentsFilters extends ICommonFilters { + readonly sortBy?: "date" | "votes" | "trending" | "permlink"; + readonly positiveVotes?: boolean; + readonly tags?: string[]; +} + +export interface ICommunityFilters extends ICommonFilters { + readonly byName?: string; + readonly tags?: string[]; +} + +export interface IAccountIdentity { + readonly name: string; +} + +export interface ICommunityIdentity { + readonly name: string; +} + +/** + * Represents a set of data uniquely identifying a post or reply object. + */ +export interface IPostCommentIdentity { + readonly author: IAccountIdentity; + readonly id: string; +} + +export interface IVote { + readonly weight: number; + readonly upvote: boolean; + readonly voter: string; +} + +export interface ICommunity extends ICommunityIdentity { + readonly title: string; + readonly about: string; + readonly admins: string[]; + readonly avatarUrl: string; + readonly creationDate: Date; + readonly subscribersCount: number; + readonly authorsCount: number; + readonly pendingCount: number; + getSlug(): string; +} +export interface IAccount extends IAccountIdentity { + readonly creationDate: Date; + readonly commentCount: number; + readonly lastActivity: Date; + readonly postCount: number; + readonly registeredDate: Date; + readonly description: string; + readonly avatar: string; + readonly url: string; + readonly name: string; + getSlug(): string; +} + +/** + * Common representation of a post and reply objects + */ +export interface IComment extends IPostCommentIdentity { + readonly publishedAt: Date; + readonly updatedAt: Date; + readonly author: IAccountIdentity; + + enumReplies(filter: IPostCommentsFilters, pagination: IPagination): Iterable; + enumMentionedAccounts(): Iterable; + enumVotes(filter: IPostCommentsFilters, pagination: IPagination): Iterable; + getContent(): string; + wasVotedByUser(userName: IAccountIdentity): boolean; + getCommensCount(): number; + + /** + * Allows to generate a slug for the comment, which can be used in URLs or as a unique identifier. + */ + generateSlug(): string; +}; + +/** + * Represents a reply to a post or another reply object. + */ +export interface IReply extends IComment { + readonly parent: IPostCommentIdentity; +} + +export interface ISession { + +} + +/** + * Represents a post (article) published on the platform. + */ +export interface IPost extends IComment { + readonly title: string; + readonly summary: string; + readonly tags: string[]; + readonly community?: ICommunityIdentity; + + getTitleImage(): string; +} + +export interface ILoginSession { + readonly authenticatedAccount: TAccountName; + readonly sessionId: string; + + logout(): Promise +} + +export interface IAuthenticationProvider { + login(account: TAccountName, signatureProvider: IOnlineSignatureProvider, directLogin: boolean, sessionTimeout: number): Promise; +} + +export interface IActiveBloggingPlatform { + readonly session: ILoginSession; + // Add callbacks + + post(body: string, tags: string[], title?: string, observer?: Partial>): Promise; + comment(postOrComment: IPostCommentIdentity, body: string, tags: string[], title?: string, observer?: Partial>): Promise; + vote(postOrComment: IPostCommentIdentity, upvote: boolean, weight: number, observer?: Partial>): Promise; + reblog(postOrComment: IPostCommentIdentity): Promise; + deletePost(postOrComment: IPostCommentIdentity): Promise; + editPost(postOrComment: IPostCommentIdentity, body: string, tags: string[], title?: string, observer?: Partial>): Promise; + deleteComment(postOrComment: IPostCommentIdentity): Promise; + editComment(postOrComment: IPostCommentIdentity, body: string, tags: string[], title?: string, observer?: Partial>): Promise; + followBlog(authorOrCommunity: IAccountIdentity | ICommunityIdentity): Promise; +} + +export interface IBloggingPlatform { + viewerContext?: IAccountIdentity; + communityContext?: ICommunityIdentity; + enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Iterable; + configureViewContext(accontName: IAccountIdentity, communityName?: ICommunityIdentity): void; + enumCommunities(filter: ICommunityFilters, pagination: IPagination): Iterable + + authorize(provider: IAuthenticationProvider): Promise; +} + +// UI integration with mock data + diff --git a/packages/blog-logic/optimistic-actions-interfaces.ts b/packages/blog-logic/optimistic-actions-interfaces.ts new file mode 100644 index 0000000..2493c5c --- /dev/null +++ b/packages/blog-logic/optimistic-actions-interfaces.ts @@ -0,0 +1,54 @@ +import {TAccountName} from "@hiveio/wax"; + +export enum TActionState { + /// Indicates that the action is started and pending + PENDING, + /// Indicates that the action was processed (i.e. by L1 chain layer), but potentially not yet completed + PROCESSED, + /// Indicates that the action was processed and completed (i.e. by L2 chain layer) + COMPLETED, + /// Indicates that the action was rejected L1 chain layer + REJECTED, + /// Indicates that the action didn't change expected state in specified time (i.e. L2 chain layer didn't complete it) + TIMEOUT +}; + +export interface IApplicationMutableProperty { + value(): T; + + /** + * Indicates whether the action is still pending or has been completed. + * @returns {TActionState} The current state of the action, COMPLETED value means action is settled on the backend side. + */ + isSettled(): TActionState; +}; + +export interface IAccountListEntry { + readonly account: IApplicationMutableProperty; +}; + +export interface IFollowListEntry extends IAccountListEntry { + readonly isFollowedBlog: IApplicationMutableProperty; + readonly isMuted: IApplicationMutableProperty; +}; + +export interface IApplicationMutableList { + isSettled(): TActionState; + /** + * Returns the number of entries in the list. Not settled, means that some upcoming change is processing. + */ + count(): IApplicationMutableProperty; + entries(): Iterable; +}; + +/** + * Represents the application state of the follow list specific to given account + */ +export interface IFollowListState extends IApplicationMutableList { +}; + +export interface IBlacklistedUserListState extends IApplicationMutableList { +}; + +export interface IMutedUserListState extends IApplicationMutableList { +}; diff --git a/packages/blog-logic/optimistic_actions.ts b/packages/blog-logic/optimistic_actions.ts new file mode 100644 index 0000000..3465f98 --- /dev/null +++ b/packages/blog-logic/optimistic_actions.ts @@ -0,0 +1,136 @@ +import WorkerBee from "@hiveio/workerbee"; +import type { IWorkerBee, Observer } from "@hiveio/workerbee"; +import {ITransaction, IOnlineSignatureProvider, IHiveChainInterface, FollowOperation, TAccountName} from "@hiveio/wax"; +import BeekeeperProvider from "@hiveio/wax-signers-beekeeper"; +import Beekeeper from "@hiveio/beekeeper"; +import { + TActionState, + IFollowListState +} from "./optimistic-actions-interfaces"; +class ChainDeferredActions { + public constructor(private readonly bot: IWorkerBee) { + } + + public async followBlog(signatureProvider: IOnlineSignatureProvider, workingAccount: TAccountName, blog: TAccountName, observer: Partial>): Promise { + const tx = await this.bot.chain.createTransaction(); + tx.pushOperation(new FollowOperation().followBlog(workingAccount, blog)); + await tx.sign(signatureProvider); + + let checksCount = 2; + + /// todo missing implementation + const followList: IFollowListState = {} as IFollowListState; + + const internalObserver: Partial> = { + next: (state: TActionState) => { + /// todo update followList state object + observer.next?.(followList); + }, + error: (err: Error) => { + observer.error?.(err); + } + }; + + + await this.deferredActionStateIndicator(tx, internalObserver, async (): Promise => { + /// todo: call the follow_api here to check if the follow was successful + /// now emulate some delay in L2 processing layer + return --checksCount === 0; + }) + } + + private async deferredActionStateIndicator(tx: ITransaction, observer: Partial>, l2Acceptor?: () => Promise): Promise { + observer.next?.(TActionState.PENDING); + + let blockMargin = 3; + + return new Promise((resolve, reject) => { + const onL1Accept = () => { + observer.next?.(TActionState.PROCESSED); + + if( l2Acceptor === undefined) { + observer.next?.(TActionState.COMPLETED); + resolve(); + return; + } + + /// can be replaced with timeout, but it does not matter + const listener = this.bot.observe.onBlock().subscribe({ + next(blockData) { + l2Acceptor() + .then((result) => { + if (result) { + listener.unsubscribe(); + observer.next?.(TActionState.COMPLETED); + resolve(); + } else { + if(--blockMargin == 0) { + listener.unsubscribe(); + observer.next?.(TActionState.TIMEOUT); + reject(new Error(`Block: ${blockData.block.number} L2 layer didn't complete action in expected time`)); + } + } + }) + .catch((err) => { + observer.error?.(err); + reject(err); + }); + }, + error(val) { + listener.unsubscribe(); + reject(val); + } + }); + + }; + + this.bot.broadcast(tx) + .then(onL1Accept) + .catch((err) => { + observer.next?.(TActionState.REJECTED); + reject(err); + }); + }); + }; + + +}; + +const log = (message: string): void => { + const date = new Date(); + const formattedDate = date.toISOString(); + console.log(`[${formattedDate}] ${message}`); +}; + +const beekeepperInstance = await Beekeeper(); + +const walletName = 'myWallet'; +const walletPassword = 'myPassword'; + +const workingAccount = 'small.minion'; +const mysecretkey = '5J...'; // Replace with your actual secret key +const blogAccount = 'medium.minion'; + +const session = beekeepperInstance.createSession('xxx'); +const unlockedWallet = session.hasWallet('myWallet') ? session.openWallet(walletName).unlock(walletPassword) : (await session.createWallet(walletName, walletPassword, false)).wallet; + +await unlockedWallet.importKey(mysecretkey); + +const bot = new WorkerBee(); +await bot.start(); + +const signatureProvider: IOnlineSignatureProvider = await BeekeeperProvider.for(unlockedWallet, workingAccount, 'posting', bot.chain); + +const optimisticUI = new ChainDeferredActions(bot); + +const follow = await optimisticUI.followBlog(signatureProvider, workingAccount, blogAccount, { + next: (data: IFollowListState) => { + log(`Follow action state: ${TActionState[data.isSettled()]}`); + }, + error: (err: Error) => { + log(`Follow action error: ${err.message}`); + } +}); + +bot.stop(); +bot.delete(); diff --git a/packages/blog-logic/rest-api.md b/packages/blog-logic/rest-api.md new file mode 100644 index 0000000..86bca45 --- /dev/null +++ b/packages/blog-logic/rest-api.md @@ -0,0 +1,25 @@ +// Work in progress + +# POSTS + +GET posts?(filters, pagination) +GET posts/{post-id} +POST posts +PUT posts/{post-id} +DELETE posts/{post-id} +GET posts/{post-id}/comments?(filters, pagination) +GET posts/{post-id}/comments/{comment-id} +POST posts/{post-id}/comments +PUT posts/{post-id}/comments/{comment-id} +DELETE posts/{post-id}/comments/{comment-id} +POST posts/{post-id}/vote + +# USER + +GET users/{user-id}/profile +GET users/{user-id}/posts?(filters, pagination) + +# COMMUNITY + +GET communities/{community-id}/profile +GET communities/{community-id}/posts?(filters, pagination) diff --git a/packages/blog-logic/wordpress-reference.ts b/packages/blog-logic/wordpress-reference.ts new file mode 100644 index 0000000..9867048 --- /dev/null +++ b/packages/blog-logic/wordpress-reference.ts @@ -0,0 +1,85 @@ +// https://developer.wordpress.org/rest-api/reference/ + +// REST API docs for wordpress + +export interface WPPost { + id: number; + date: string; + date_gmt: string; + guid: Rendered; + modified: string; + modified_gmt: string; + slug: string; + status: "publish" | "future" | "draft" | "pending" | "private"; + type: string; + link: string; + title: Rendered; + content: RenderedProtected; + excerpt: RenderedProtected; + author: number; + featured_media: number; + comment_status: "open" | "closed"; + ping_status: "open" | "closed"; + sticky: boolean; + template: string; + format: string; + meta: Record; + categories: number[]; + tags: number[]; + + // Optional: Embedded or custom fields + _links?: WPLinks; + _embedded?: any; // Add specific types if using _embed +} + +export interface Rendered { + rendered: string; +} + +export interface RenderedProtected extends Rendered { + protected: boolean; +} + +export interface WPLinks { + self: WPLink[]; + collection: WPLink[]; + about: WPLink[]; + author: WPLink[]; + replies?: WPLink[]; + version_history?: WPLink[]; + "wp:featuredmedia"?: WPLink[]; + "wp:attachment"?: WPLink[]; + "wp:term"?: WPLink[]; + curies?: WPCurie[]; +} + +export interface WPLink { + href: string; +} + +export interface WPCurie { + name: string; + href: string; + templated: boolean; +} + +export interface WPComment { + id: number; + post: number; // Post ID this comment is attached to + parent: number; // Parent comment ID (if it's a reply) + author: number; // User ID (0 if anonymous) + author_name: string; + author_email: string; + author_url: string; + date: string; + date_gmt: string; + content: Rendered; + link: string; + status: string; // Usually 'approved' or 'hold' + type: string; // Usually '' (empty string for normal comment) + author_ip: string; + author_user_agent: string; + meta: Record; + + _links?: WPLinks; +} -- GitLab From e45fbdef577e7cc29579bf97fb202661a68108cf Mon Sep 17 00:00:00 2001 From: jlachor Date: Thu, 14 Aug 2025 12:24:09 +0200 Subject: [PATCH 02/15] WordPress rest API prototype --- examples/wordpress-rest-api/.npmrc | 1 + examples/wordpress-rest-api/example-config.ts | 9 + examples/wordpress-rest-api/hash-utils.ts | 8 + examples/wordpress-rest-api/hive.ts | 129 ++ examples/wordpress-rest-api/hiveToWpMap.ts | 129 ++ .../wordpress-rest-api/mocks/categories.ts | 13 + examples/wordpress-rest-api/mocks/comments.ts | 74 + examples/wordpress-rest-api/mocks/posts.ts | 157 +++ examples/wordpress-rest-api/mocks/tags.ts | 22 + examples/wordpress-rest-api/mocks/users.ts | 16 + examples/wordpress-rest-api/package.json | 21 + examples/wordpress-rest-api/pnpm-lock.yaml | 1255 +++++++++++++++++ examples/wordpress-rest-api/readme.md | 17 + .../wordpress-rest-api/wordpress-rest-api.ts | 125 ++ examples/wordpress-rest-api/wp-reference.ts | 97 ++ packages/blog-logic/interfaces.ts | 16 +- packages/blog-logic/wordpress-reference.ts | 158 ++- pnpm-lock.yaml | 261 ++-- 18 files changed, 2369 insertions(+), 139 deletions(-) create mode 120000 examples/wordpress-rest-api/.npmrc create mode 100644 examples/wordpress-rest-api/example-config.ts create mode 100644 examples/wordpress-rest-api/hash-utils.ts create mode 100644 examples/wordpress-rest-api/hive.ts create mode 100644 examples/wordpress-rest-api/hiveToWpMap.ts create mode 100644 examples/wordpress-rest-api/mocks/categories.ts create mode 100644 examples/wordpress-rest-api/mocks/comments.ts create mode 100644 examples/wordpress-rest-api/mocks/posts.ts create mode 100644 examples/wordpress-rest-api/mocks/tags.ts create mode 100644 examples/wordpress-rest-api/mocks/users.ts create mode 100644 examples/wordpress-rest-api/package.json create mode 100644 examples/wordpress-rest-api/pnpm-lock.yaml create mode 100644 examples/wordpress-rest-api/readme.md create mode 100644 examples/wordpress-rest-api/wordpress-rest-api.ts create mode 100644 examples/wordpress-rest-api/wp-reference.ts diff --git a/examples/wordpress-rest-api/.npmrc b/examples/wordpress-rest-api/.npmrc new file mode 120000 index 0000000..cba44bb --- /dev/null +++ b/examples/wordpress-rest-api/.npmrc @@ -0,0 +1 @@ +../../.npmrc \ No newline at end of file diff --git a/examples/wordpress-rest-api/example-config.ts b/examples/wordpress-rest-api/example-config.ts new file mode 100644 index 0000000..3994c95 --- /dev/null +++ b/examples/wordpress-rest-api/example-config.ts @@ -0,0 +1,9 @@ +export const wordPressExampleConfig = { + postLimit: 10, + observer: "hive.blog", + sort: "created", // "trending" / "hot" / "created" / "promoted" / "payout" / "payout_comments" / "muted" + startAuthor: "", + startPermlink: "", + postTag: "hive-148441", + defaultPort: 4000, +} \ No newline at end of file diff --git a/examples/wordpress-rest-api/hash-utils.ts b/examples/wordpress-rest-api/hash-utils.ts new file mode 100644 index 0000000..bacb48f --- /dev/null +++ b/examples/wordpress-rest-api/hash-utils.ts @@ -0,0 +1,8 @@ +export const simpleHash = (str): number => { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + } + return (hash >>> 0); + }; \ No newline at end of file diff --git a/examples/wordpress-rest-api/hive.ts b/examples/wordpress-rest-api/hive.ts new file mode 100644 index 0000000..b3230e7 --- /dev/null +++ b/examples/wordpress-rest-api/hive.ts @@ -0,0 +1,129 @@ +import { + TWaxApiRequest +} from '@hiveio/wax'; + +export interface IGetPostHeader { + author: string; + permlink: string; + category: string; + depth: number; +} + +export interface JsonMetadata { + image: string; + links?: string[]; + flow?: { + pictures: { + caption: string; + id: number; + mime: string; + name: string; + tags: string[]; + url: string; + }[]; + tags: string[]; + }; + images: string[]; + author: string | undefined; + tags?: string[]; + description?: string | null; + app?: string; + canonical_url?: string; + format?: string; + original_author?: string; + original_permlink?: string; + summary?: string; +} + +export interface EntryVote { + voter: string; + rshares: number; +} + +export interface EntryBeneficiaryRoute { + account: string; + weight: number; +} + +export interface EntryStat { + flag_weight: number; + gray: boolean; + hide: boolean; + total_votes: number; + is_pinned?: boolean; +} + + +export interface Entry { + active_votes: EntryVote[]; + author: string; + author_payout_value: string; + author_reputation: number; + author_role?: string; + author_title?: string; + beneficiaries: EntryBeneficiaryRoute[]; + blacklists: string[]; + body: string; + category: string; + children: number; + community?: string; + community_title?: string; + created: string; + total_votes?: number; + curator_payout_value: string; + depth: number; + is_paidout: boolean; + json_metadata: JsonMetadata; + max_accepted_payout: string; + net_rshares: number; + parent_author?: string; + parent_permlink?: string; + payout: number; + payout_at: string; + pending_payout_value: string; + percent_hbd: number; + permlink: string; + post_id: number; + id?: number; + promoted: string; + reblogged_by?: string[]; + replies: Array; + stats?: EntryStat; + title: string; + updated: string; + url: string; + original_entry?: Entry; +} + +export type ExtendedNodeApi = { + bridge: { + get_post_header: TWaxApiRequest<{ author: string; permlink: string }, IGetPostHeader>; + get_post: TWaxApiRequest<{ author: string; permlink: string; observer: string }, Entry | null>; + get_discussion: TWaxApiRequest< + { author: string; permlink: string; observer?: string }, + Record | null + >; + get_ranked_posts: TWaxApiRequest< + { + sort: string; + tag: string; + start_author: string; + start_permlink: string; + limit: number; + observer: string; + }, + Entry[] | null + >; + get_account_posts: TWaxApiRequest< + { + sort: string; + account: string; + start_author: string; + start_permlink: string; + limit: number; + observer: string; + }, + Entry[] | null + >; + }; +}; \ No newline at end of file diff --git a/examples/wordpress-rest-api/hiveToWpMap.ts b/examples/wordpress-rest-api/hiveToWpMap.ts new file mode 100644 index 0000000..c4e62ee --- /dev/null +++ b/examples/wordpress-rest-api/hiveToWpMap.ts @@ -0,0 +1,129 @@ +import { simpleHash } from "./hash-utils"; +import { Entry } from "./hive"; +import { WPComment, WPPost, WPTag, WPTerm } from "./wp-reference"; +import { DefaultRenderer } from "@hiveio/content-renderer"; + +const renderer = new DefaultRenderer({ + baseUrl: "https://hive.blog/", + breaks: true, + skipSanitization: false, + allowInsecureScriptTags: false, + addNofollowToLinks: true, + doNotShowImages: false, + assetsWidth: 640, + assetsHeight: 480, + imageProxyFn: (url: string) => url, + usertagUrlFn: (account: string) => "https://hive.blog/@" + account, + hashtagUrlFn: (hashtag: string) => "https://hive.blog/trending/" + hashtag, + isLinkSafeFn: (url: string) => true, + addExternalCssClassToMatchingLinksFn: (url: string) => true, + ipfsPrefix: "https://ipfs.io/ipfs/" // IPFS gateway to display ipfs images +}); + +const mapWpTerm = (termName: string, type: "tag" | "category"): WPTerm => { + const termId = simpleHash(termName); + const taxonomy: string = type === "tag" ? "post_tag" : "category"; + const wpTerm: WPTerm = { + id: termId, + link: `http://localhost/${type}/${termName}/`, + name: termName, + slug: termName.toLocaleLowerCase(), + taxonomy, + }; + return wpTerm; +} + + +export const mapHivePostToWpPost = (hivePost: Entry, wpId: number, accountId: number): WPPost => { + const slug = `${hivePost.author}_${hivePost.permlink}`; + const tags = hivePost.json_metadata?.tags || []; + const wpTermTags = tags.map((tag) => mapWpTerm(tag, "tag")); + const community = hivePost.community_title; + const wpTermCategory = community ? [mapWpTerm(community, "category")] : []; + const renderedBody = renderer.render(hivePost.body); + const wpExcerpt = renderedBody.replace(/<[^>]+>/g, '').substring(0, 100); + const wpPost: WPPost = { + id:wpId, + slug, + date: new Date(hivePost.created).toISOString(), + date_gmt: new Date(hivePost.created).toISOString(), + modified: new Date(hivePost.updated).toISOString(), + modified_gmt: new Date(hivePost.updated).toISOString(), + status: "publish", + type: "post", + link: `http://host/${slug}/`, + title: { rendered: hivePost.title }, + content: { rendered: renderedBody, protected: false }, + excerpt: { rendered: wpExcerpt, protected: false }, + author: accountId, + featured_media: 0, + comment_status: "open", + ping_status: "open", + sticky: false, + template: "", + format: "standard", + meta: {}, + categories: [community ? simpleHash(community) : 0], + tags: tags.map((tags) => simpleHash(tags)), + guid: { rendered: `http://host/?p=${wpId}` }, + class_list: [`category-${community}`], + _embedded: { + replies: [], + author: [{ + id: accountId, + name: hivePost.author, + url: `https://hive.blog/@${hivePost.author}`, + description: "", + link: `https://hive.blog/@${hivePost.author}`, + slug: hivePost.author, + avatar_urls: { + 24: `https://images.hive.blog/u/${hivePost.author}/avatar`, + 48: `https://images.hive.blog/u/${hivePost.author}/avatar`, + 96: `https://images.hive.blog/u/${hivePost.author}/avatar` + } + }], + "wp:term": [...wpTermTags.map((wpTerm) => [wpTerm]), ...wpTermCategory.map((wpTerm) => [wpTerm])] + } + }; + return wpPost +} + +export const mapHiveCommentToWPComment = (hiveComment: Entry, commentId: number, mainPostId: number, authorId: number): WPComment => { + const parentId = simpleHash(`${hiveComment.parent_author}_${hiveComment.parent_permlink}`); + const renderedBody = renderer.render(hiveComment.body); + const wpComment: WPComment = { + id: commentId, + post: mainPostId, + parent: parentId === mainPostId ? 0 : parentId, // There is no id for parent post + author: authorId, + author_name: hiveComment.author, + author_url: `https://hive.blog/@${hiveComment.author}`, + date: new Date(hiveComment.created).toISOString(), + date_gmt: new Date(hiveComment.created).toISOString(), + content: { rendered: renderedBody }, + link: `http://host/${hiveComment.parent_author}_${hiveComment.parent_permlink}/#comment-${commentId}`, + status: "approved", + type: "comment", + meta: [], + author_avatar_urls: { + 24: `https://images.hive.blog/u/${hiveComment.author}/avatar`, + 48: `https://images.hive.blog/u/${hiveComment.author}/avatar`, + 96: `https://images.hive.blog/u/${hiveComment.author}/avatar` + } + } + return wpComment; +} + +// For later use +export const mapHiveTagsToWpTags = (tagSlug: string): WPTag => { + return { + id: 1, + count: 1, + description: "", + link: `http://localhost/tag/${tagSlug}/`, + name: tagSlug, + slug: tagSlug, + taxonomy: "post_tag", + meta: [] + } +} diff --git a/examples/wordpress-rest-api/mocks/categories.ts b/examples/wordpress-rest-api/mocks/categories.ts new file mode 100644 index 0000000..039ed46 --- /dev/null +++ b/examples/wordpress-rest-api/mocks/categories.ts @@ -0,0 +1,13 @@ +export const categoryHive = [ + { + id: 3, + count: 1, + description: "About Hive blockchain", + link: "http://localhost/category/hive/", + name: "Hive", + slug: "hive", + taxonomy: "category", + parent: 0, + meta: [] + } +]; diff --git a/examples/wordpress-rest-api/mocks/comments.ts b/examples/wordpress-rest-api/mocks/comments.ts new file mode 100644 index 0000000..465aa22 --- /dev/null +++ b/examples/wordpress-rest-api/mocks/comments.ts @@ -0,0 +1,74 @@ +export const comments1 = [ + { + id: 1, + post: 1, + parent: 0, + author: 0, + author_name: "A WordPress Commenter", + author_url: "https://wordpress.org/", + date: "2025-08-25T11:10:15", + date_gmt: "2025-08-25T11:10:15", + content: { + rendered: "

Just a test comment

\n" + }, + link: "http://localhost/hello-world/#comment-1", + status: "approved", + type: "comment", + author_avatar_urls: { + 24: "https://secure.gravatar.com/avatar/8e1606e6fba450a9362af43874c1b2dfad34c782e33d0a51e1b46c18a2a567dd?s=24&d=mm&r=g", + 48: "https://secure.gravatar.com/avatar/8e1606e6fba450a9362af43874c1b2dfad34c782e33d0a51e1b46c18a2a567dd?s=48&d=mm&r=g", + 96: "https://secure.gravatar.com/avatar/8e1606e6fba450a9362af43874c1b2dfad34c782e33d0a51e1b46c18a2a567dd?s=96&d=mm&r=g" + }, + meta: [] + } +]; + +export const comments2 = [] + +export const comments3 = [ + { + id: 3, + post: 9, + parent: 2, + author: 1, + author_name: "wordpress", + author_url: "http://localhost", + date: "2025-08-28T08:34:48", + date_gmt: "2025-08-28T08:34:48", + content: { + rendered: "

Testowa odpowiedź w języku polskim.

\n" + }, + link: "http://localhost/best-mock-post/#comment-3", + status: "approved", + type: "comment", + author_avatar_urls: { + 24: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=24&d=mm&r=g", + 48: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=48&d=mm&r=g", + 96: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=96&d=mm&r=g" + }, + meta: [] + }, + { + id: 2, + post: 9, + parent: 0, + author: 1, + author_name: "wordpress", + author_url: "http://localhost", + date: "2025-08-25T11:40:48", + date_gmt: "2025-08-25T11:40:48", + content: { + rendered: "

Testowy komentarz w języku polskim.

\n

Best page

\n" + }, + link: "http://localhost/best-mock-post/#comment-2", + status: "approved", + type: "comment", + author_avatar_urls: { + 24: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=24&d=mm&r=g", + 48: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=48&d=mm&r=g", + 96: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=96&d=mm&r=g" + }, + meta: [] + } +] + diff --git a/examples/wordpress-rest-api/mocks/posts.ts b/examples/wordpress-rest-api/mocks/posts.ts new file mode 100644 index 0000000..c55cb19 --- /dev/null +++ b/examples/wordpress-rest-api/mocks/posts.ts @@ -0,0 +1,157 @@ +import { WPPost } from "../wp-reference"; + +export const post1: WPPost = { + id: 1, + date: "2025-08-25T11:10:15", + date_gmt: "2025-08-25T11:10:15", + guid: { + rendered: "http://localhost/?p=1" + }, + modified: "2025-08-25T11:10:15", + modified_gmt: "2025-08-25T11:10:15", + slug: "hello-world", + status: "publish", + type: "post", + link: "http://localhost/hello-world/", + title: { + rendered: "Hello world!" + }, + content: { + rendered: "\n

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

\n", + protected: false + }, + excerpt: { + rendered: "

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

\n", + protected: false + }, + author: 1, + featured_media: 0, + comment_status: "open", + ping_status: "open", + sticky: false, + template: "", + format: "standard", + meta: { + footnotes: "" + }, + categories: [ + 1 + ], + tags: [ + 11 + ], + class_list: [ + "post-1", + "post", + "type-post", + "status-publish", + "format-standard", + "hentry", + "category-uncategorized" + ] +} + +export const post2: WPPost = { + id: 6, + date: "2025-08-25T11:11:36", + date_gmt: "2025-08-25T11:11:36", + guid: { + rendered: "http://localhost/?p=6" + }, + modified: "2025-08-25T11:11:36", + modified_gmt: "2025-08-25T11:11:36", + slug: "test-post", + status: "publish", + type: "post", + link: "http://localhost/test-post/", + title: { + rendered: "Test post" + }, + content: { + rendered: "\n

I’m going to add simplest mock post for WP.

\n", + protected: false + }, + excerpt: { + rendered: "

I’m going to add simplest mock post for WP.

\n", + protected: false + }, + author: 1, + featured_media: 0, + comment_status: "open", + ping_status: "open", + sticky: false, + template: "", + format: "standard", + meta: { + footnotes: "" + }, + categories: [ + 1 + ], + tags: [], + class_list: [ + "post-6", + "post", + "type-post", + "status-publish", + "format-standard", + "hentry", + "category-uncategorized" + ] +} + +export const post3: WPPost = { + id: 9, + date: "2025-08-25T11:12:17", + date_gmt: "2025-08-25T11:12:17", + guid: { + rendered: "http://localhost/?p=9" + }, + modified: "2025-08-27T10:38:16", + modified_gmt: "2025-08-27T10:38:16", + slug: "best-mock-post", + status: "publish", + type: "post", + link: "http://localhost/best-mock-post/", + title: { + rendered: "Best Mock Post" + }, + content: { + rendered: "\n

I don’t have enough patience for this crap

\n\n\n\n

Best page:

\n\n\n\n

https://youtube.com

\n\n\n\n
\"\"
\n\n\n\n

\n", + protected: false + }, + excerpt: { + rendered: "

I don’t have enough patience for this crap /n Best page: https://youtube.com

\n", + protected: false + }, + author: 1, + featured_media: 0, + comment_status: "open", + ping_status: "open", + sticky: false, + template: "", + format: "standard", + meta: { + footnotes: "" + }, + categories: [ + 3 + ], + tags: [ + 4 + ], + class_list: [ + "post-9", + "post", + "type-post", + "status-publish", + "format-standard", + "hentry", + "category-hive", + "tag-mock-rock" + ] +} + +export const allPosts = [post3, post2, post1]; + + diff --git a/examples/wordpress-rest-api/mocks/tags.ts b/examples/wordpress-rest-api/mocks/tags.ts new file mode 100644 index 0000000..87e58d5 --- /dev/null +++ b/examples/wordpress-rest-api/mocks/tags.ts @@ -0,0 +1,22 @@ +export const tagMock = [ + { + id: 4, + count: 1, + description: "All things about mock", + link: "http://localhost/tag/mock-rock/", + name: "Mock rocks", + slug: "mock-rock", + taxonomy: "post_tag", + meta: [] + }, + { + id: 11, + count: 1, + description: "About front end's programming language", + link: "http://localhost/tag/javascript/", + name: "JavaScript", + slug: "javascript", + taxonomy: "post_tag", + meta: [] + } +]; diff --git a/examples/wordpress-rest-api/mocks/users.ts b/examples/wordpress-rest-api/mocks/users.ts new file mode 100644 index 0000000..481ffb6 --- /dev/null +++ b/examples/wordpress-rest-api/mocks/users.ts @@ -0,0 +1,16 @@ +export const userWordPress = [ + { + id: 1, + name: "wordpress", + url: "http://localhost", + description: "", + link: "http://localhost/author/wordpress/", + slug: "wordpress", + avatar_urls: { + 24: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=24&d=mm&r=g", + 48: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=48&d=mm&r=g", + 96: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=96&d=mm&r=g" + }, + meta: [] + } +]; diff --git a/examples/wordpress-rest-api/package.json b/examples/wordpress-rest-api/package.json new file mode 100644 index 0000000..c66e2cc --- /dev/null +++ b/examples/wordpress-rest-api/package.json @@ -0,0 +1,21 @@ +{ + "name": "wordpress-rest-api", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "private": false, + "contributors": [], + "scripts": { + "dev": "tsx ./wordpress-rest-api.ts" + }, + "dependencies": { + "@hiveio/content-renderer": "^2.3.1", + "@hiveio/wax": "1.27.6-rc10-stable.250804212246", + "@hiveio/workerbee": "file:../..", + "cors": "^2.8.5", + "express": "^5.1.0" + }, + "devDependencies": { + "tsx": "^4.20.5" + } +} diff --git a/examples/wordpress-rest-api/pnpm-lock.yaml b/examples/wordpress-rest-api/pnpm-lock.yaml new file mode 100644 index 0000000..68934c2 --- /dev/null +++ b/examples/wordpress-rest-api/pnpm-lock.yaml @@ -0,0 +1,1255 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hiveio/content-renderer': + specifier: ^2.3.1 + version: 2.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@hiveio/wax': + specifier: 1.27.6-rc10-stable.250804212246 + version: 1.27.6-rc10-stable.250804212246 + '@hiveio/workerbee': + specifier: file:../.. + version: file:../.. + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^5.1.0 + version: 5.1.0 + devDependencies: + tsx: + specifier: ^4.20.5 + version: 4.20.5 + +packages: + + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hiveio/content-renderer@2.3.1': + resolution: {integrity: sha512-YE8Lj4LycKxig+2z8VQXNiedQq0lx0pQk9q6USsbHZIFaZePWvkZAJAFy2q9qmLFH5Ca5Nu/TGTiZFQbmbLOVQ==, tarball: https://registry.npmjs.org/@hiveio/content-renderer/-/content-renderer-2.3.1.tgz} + engines: {node: '>=20'} + + '@hiveio/wax@1.27.6-rc10-stable.250804212246': + resolution: {integrity: sha1-qZYJ4ot0Lgy3C0fbeS1WFoxmFEY=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax/-/@hiveio/wax-1.27.6-rc10-stable.250804212246.tgz} + engines: {node: ^20.11 || >= 21.2} + + '@hiveio/workerbee@file:../..': + resolution: {directory: ../.., type: directory} + engines: {node: ^20.11 || >= 21.2} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + autolinker@3.16.2: + resolution: {integrity: sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + ow@0.13.2: + resolution: {integrity: sha512-9wvr+q+ZTDRvXDjL6eDOdFe5WUl/wa5sntf9kAolxqSpkBqaIObwLgFCGXSJASFw+YciXnOVtDWpxXa9cqV94A==} + engines: {node: '>=6'} + + ow@0.28.2: + resolution: {integrity: sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==} + engines: {node: '>=12'} + + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-twitter-embed@4.0.4: + resolution: {integrity: sha512-2JIL7qF+U62zRzpsh6SZDXNI3hRNVYf5vOZ1WRcMvwKouw+xC00PuFaD0aEp2wlyGaZ+f4x2VvX+uDadFQ3HVA==} + engines: {node: '>=10'} + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + remarkable@2.0.1: + resolution: {integrity: sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==} + engines: {node: '>= 6.0.0'} + hasBin: true + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-html@2.13.0: + resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + scriptjs@2.5.9: + resolution: {integrity: sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg==} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-custom-error@3.3.1: + resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.5: + resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-fest@0.5.2: + resolution: {integrity: sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==} + engines: {node: '>=6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript-chained-error@1.6.0: + resolution: {integrity: sha512-DUH9Cf8AggyReFexCoVYofKqkLp95sRNZRMa7kQL4xpZd+eB6bmgD0YwD37aDwjupFjfCyYjkC0JGT/zc9JG8w==} + engines: {node: '>=12'} + + universe-log@5.2.0: + resolution: {integrity: sha512-8jSMiXIcm0Ea/N3zCj2Kl+BIihCV7ydvshAgGaDDsLgOSSchiDORUMmSYKMH3FXeKaUr6dKd7e1eScBUpe2+3Q==} + engines: {node: '>=8'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vali-date@1.0.0: + resolution: {integrity: sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==} + engines: {node: '>=0.10.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.25.9': + optional: true + + '@esbuild/android-arm64@0.25.9': + optional: true + + '@esbuild/android-arm@0.25.9': + optional: true + + '@esbuild/android-x64@0.25.9': + optional: true + + '@esbuild/darwin-arm64@0.25.9': + optional: true + + '@esbuild/darwin-x64@0.25.9': + optional: true + + '@esbuild/freebsd-arm64@0.25.9': + optional: true + + '@esbuild/freebsd-x64@0.25.9': + optional: true + + '@esbuild/linux-arm64@0.25.9': + optional: true + + '@esbuild/linux-arm@0.25.9': + optional: true + + '@esbuild/linux-ia32@0.25.9': + optional: true + + '@esbuild/linux-loong64@0.25.9': + optional: true + + '@esbuild/linux-mips64el@0.25.9': + optional: true + + '@esbuild/linux-ppc64@0.25.9': + optional: true + + '@esbuild/linux-riscv64@0.25.9': + optional: true + + '@esbuild/linux-s390x@0.25.9': + optional: true + + '@esbuild/linux-x64@0.25.9': + optional: true + + '@esbuild/netbsd-arm64@0.25.9': + optional: true + + '@esbuild/netbsd-x64@0.25.9': + optional: true + + '@esbuild/openbsd-arm64@0.25.9': + optional: true + + '@esbuild/openbsd-x64@0.25.9': + optional: true + + '@esbuild/openharmony-arm64@0.25.9': + optional: true + + '@esbuild/sunos-x64@0.25.9': + optional: true + + '@esbuild/win32-arm64@0.25.9': + optional: true + + '@esbuild/win32-ia32@0.25.9': + optional: true + + '@esbuild/win32-x64@0.25.9': + optional: true + + '@hiveio/content-renderer@2.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@xmldom/xmldom': 0.8.10 + ow: 0.28.2 + react-twitter-embed: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + remarkable: 2.0.1 + sanitize-html: 2.13.0 + typescript-chained-error: 1.6.0 + universe-log: 5.2.0 + transitivePeerDependencies: + - react + - react-dom + + '@hiveio/wax@1.27.6-rc10-stable.250804212246': + dependencies: + events: 3.3.0 + + '@hiveio/workerbee@file:../..': + dependencies: + '@hiveio/wax': 1.27.6-rc10-stable.250804212246 + + '@sindresorhus/is@4.6.0': {} + + '@xmldom/xmldom@0.8.10': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + autolinker@3.16.2: + dependencies: + tslib: 2.8.1 + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + clean-stack@2.2.0: {} + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deepmerge@4.3.1: {} + + depd@2.0.0: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-prop@6.0.1: + dependencies: + is-obj: 2.0.0 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + entities@4.5.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.9: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + etag@1.8.1: {} + + events@3.3.0: {} + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-obj@2.0.0: {} + + is-plain-object@5.0.0: {} + + is-promise@4.0.0: {} + + js-tokens@4.0.0: {} + + lodash.isequal@4.5.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + ow@0.13.2: + dependencies: + type-fest: 0.5.2 + + ow@0.28.2: + dependencies: + '@sindresorhus/is': 4.6.0 + callsites: 3.1.0 + dot-prop: 6.0.1 + lodash.isequal: 4.5.0 + vali-date: 1.0.0 + + parse-srcset@1.0.2: {} + + parseurl@1.3.3: {} + + path-to-regexp@8.3.0: {} + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-twitter-embed@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + scriptjs: 2.5.9 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + remarkable@2.0.1: + dependencies: + argparse: 1.0.10 + autolinker: 3.16.2 + + resolve-pkg-maps@1.0.0: {} + + router@2.2.0: + dependencies: + debug: 4.4.1 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sanitize-html@2.13.0: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.5.6 + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + scriptjs@2.5.9: {} + + send@1.2.0: + dependencies: + debug: 4.4.1 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + sprintf-js@1.0.3: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + toidentifier@1.0.1: {} + + ts-custom-error@3.3.1: {} + + tslib@2.8.1: {} + + tsx@4.20.5: + dependencies: + esbuild: 0.25.9 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + type-fest@0.5.2: {} + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typescript-chained-error@1.6.0: + dependencies: + clean-stack: 2.2.0 + deepmerge: 4.3.1 + ts-custom-error: 3.3.1 + + universe-log@5.2.0: + dependencies: + ow: 0.13.2 + typescript-chained-error: 1.6.0 + + unpipe@1.0.0: {} + + vali-date@1.0.0: {} + + vary@1.1.2: {} + + wrappy@1.0.2: {} diff --git a/examples/wordpress-rest-api/readme.md b/examples/wordpress-rest-api/readme.md new file mode 100644 index 0000000..0c9d82d --- /dev/null +++ b/examples/wordpress-rest-api/readme.md @@ -0,0 +1,17 @@ +# Hive to WordPress example + +This project maps data from Hive and returns it as proper WordPress API. It will be later use by custom front end (like Frontity) to display posts and comments. + +### Instalation + +* Get project and get submodules. +* Go into `example/wordpress-rest-api`. +* `pnpm install`. +* Change `example-config.ts` file to get the data you want to see on main page. +* `pnpm run dev` for deployment. Rest API should be ready to work with your front end. + +### About + +This example is still a work in progress. It was made to be used with Frontity especially, but the API can connect to other part of WordPress infrastructure. + +Tags, categories and authors are off so far. The things you can do with this API is to display posts list, single posts and comments for given post. \ No newline at end of file diff --git a/examples/wordpress-rest-api/wordpress-rest-api.ts b/examples/wordpress-rest-api/wordpress-rest-api.ts new file mode 100644 index 0000000..772a408 --- /dev/null +++ b/examples/wordpress-rest-api/wordpress-rest-api.ts @@ -0,0 +1,125 @@ +import express, { Request, Response } from "express"; +import cors from "cors"; +import { createHiveChain } from "@hiveio/wax"; +import { Entry, ExtendedNodeApi } from "./hive"; +import { mapHiveCommentToWPComment, mapHivePostToWpPost, mapHiveTagsToWpTags } from "./hiveToWpMap"; +import { WPComment, WPPost } from "./wp-reference"; +import { simpleHash } from "./hash-utils"; +import { wordPressExampleConfig } from "./example-config"; + +const hiveChain = await createHiveChain(); +const extendedHiveChain = hiveChain.extend(); + +const app = express(); +const PORT = wordPressExampleConfig.defaultPort; + +// Middleware to parse JSON +app.use(express.json()); +app.use(cors()); +const apiRouter = express.Router(); + +const idToStringMap = new Map(); + +const getAuthorPermlinkFromSlug = (slug: string): {author: string, permlink: string} => { + const splitedSlug = slug.split("_"); + const author = splitedSlug[0]; + splitedSlug.shift(); + const permlink = splitedSlug.join("_"); + return { + author, + permlink + } +} + +const mapAndAddPostsToMap = (posts: Entry[]): WPPost[] => { + const mappedPosts: WPPost[] = [] + posts.forEach((post) => { + const postId = simpleHash(`${post.author}_${post.permlink}`); + const authorId = simpleHash(post.author); + idToStringMap.set(postId, `${post.author}_${post.permlink}`).set(authorId, post.author); + mappedPosts.push(mapHivePostToWpPost(post, postId, authorId)); + }); + return mappedPosts; +} + + +apiRouter.get("/posts", async (req: Request, res: Response) => { + // Default WP call for devtools + if (req.query.slug === "com.chrome.devtools.json") res.json([]); + // Single post + else if (!!req.query.slug) { + const {author, permlink} = getAuthorPermlinkFromSlug(req.query.slug as string); + const authorPermlinkHash = simpleHash(req.query.slug); + const authorHash = simpleHash(author); + idToStringMap.set(authorPermlinkHash, req.query.slug as string).set(authorHash, author); + const result = await extendedHiveChain.api.bridge.get_post({author, permlink, observer: "hive.blog"}); + if (result) { + res.json(mapHivePostToWpPost(result, authorPermlinkHash, authorHash)); + } else { + res.status(404).json({ error: "Post not found" }); + } + // Posts list + } else { + const result = await extendedHiveChain.api.bridge.get_ranked_posts({ + limit: wordPressExampleConfig.postLimit, + sort: wordPressExampleConfig.sort, + observer: wordPressExampleConfig.observer, + start_author: wordPressExampleConfig.startAuthor, + start_permlink: wordPressExampleConfig.startPermlink, + tag: wordPressExampleConfig.postTag + }); + if (result) { + res.json(mapAndAddPostsToMap(result)); + } + } + +}); + +apiRouter.get("/comments", async (req: Request, res: Response) => { + const postId = Number(req.query.post); + const postParent = idToStringMap.get(postId); + if (postParent) { + const result = await extendedHiveChain.api.bridge.get_discussion({author: postParent.split("_")[0], permlink: postParent.split("_").slice(1).join("_")}); + if (result) { + const wpComments: WPComment[] = [] + Object.entries(result).forEach(([authorPermlink, comment]) => { + if (comment.parent_author && comment.parent_permlink) { + const wpAuthorPermlink = authorPermlink.replace("/", "_"); + const wpComment = mapHiveCommentToWPComment(comment, simpleHash(wpAuthorPermlink), postId, simpleHash(comment.author)); + wpComments.push(wpComment) + } + }); + res.json(wpComments) + } + } else { + res.json([]); + } +}); + +apiRouter.get("/tags", (req: Request, res: Response) => { + res.json([]); +}); + +apiRouter.get("/categories", (req: Request, res: Response) => { + res.json([]); +}); + +apiRouter.get("/users", (req: Request, res: Response) => { + res.json([]); +}); + +apiRouter.get("/media", (req: Request, res: Response) => { + res.json([]); +}); + +apiRouter.get("/pages", (req: Request, res: Response) => { + res.json([]); +}); + +// Mount router with prefix +app.use("/wp-json/wp/v2", apiRouter); + +app.listen(PORT, () => { + // eslint-disable-next-line no-console + console.log(`🚀 Server running at http://localhost:${PORT}`); +}); diff --git a/examples/wordpress-rest-api/wp-reference.ts b/examples/wordpress-rest-api/wp-reference.ts new file mode 100644 index 0000000..af779eb --- /dev/null +++ b/examples/wordpress-rest-api/wp-reference.ts @@ -0,0 +1,97 @@ +export interface Rendered { + rendered: string; +} + +export interface RenderedProtected extends Rendered { + protected: boolean; +} + +export interface WPPost { + id: number; + date: string; // Date + date_gmt: string; // Date + guid: Rendered; + modified: string; // Date + modified_gmt: string; // Date + slug: string; + status: "publish" | "future" | "draft" | "pending" | "private"; + type: "post"; + link: string; + title: Rendered; + content: RenderedProtected; + excerpt: RenderedProtected; + author: number; + featured_media: number; + comment_status: "open" | "closed"; + ping_status: "open" | "closed"; + sticky: boolean; + template: string; + format: "standard" | "aside" | "chat" | "gallery" | "link" | "image" | "quote" | "status" | "video" | "audio"; + meta: Record; + categories: number[]; + tags: number[]; + class_list: string[]; + _embedded?: { + replies: WPComment[][]; + author: Array<{ + id: number; + name: string; + url: string; + description: string; + link: string; + slug: string; + avatar_urls: { + 24: string; + 48: string; + 96: string; + }; + }>; + "wp:term": WPTerm[][]; + }; +} +export interface WPComment { + id: number; + post: number; + parent: number; + author: number; + author_name: string; + author_url: string; + date: string; // Date + date_gmt: string; // Date + content: { + rendered: string + }; + link: string; + status: "approved"; + type: "comment"; + author_avatar_urls: { + 24: string; + 48: string; + 96: string + }; + meta: []; +} + +export interface Content { + rendered: string +} + +export interface WPTag { + id: number, + count: number, + description: string, + link: string, + name: string, + slug: string, + taxonomy: string, + meta: [] + +} + +export interface WPTerm { + id: number; + link: string; + name: string; + slug: string; + taxonomy: string; +} \ No newline at end of file diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts index a5c2323..cd2db66 100644 --- a/packages/blog-logic/interfaces.ts +++ b/packages/blog-logic/interfaces.ts @@ -10,6 +10,7 @@ export interface IPagination { export interface ICommonFilters { readonly startTime?: Date; readonly endTime?: Date; + order?: "asc" | "desc"; } export interface IVotesFilters extends ICommonFilters { @@ -19,9 +20,14 @@ export interface IVotesFilters extends ICommonFilters { } export interface IPostCommentsFilters extends ICommonFilters { - readonly sortBy?: "date" | "votes" | "trending" | "permlink"; + readonly sortBy?: "author" | "date" | "id" | "include" | "modified" | "parent" | "relevance" | "slug" | "include_slugs" | "title"; readonly positiveVotes?: boolean; readonly tags?: string[]; + readonly modificationStartTime?: Date; + readonly modificationEndTime?: Date; + readonly author?: string; + readonly searchInText?: string; + readonly slug?: string | string[]; } export interface ICommunityFilters extends ICommonFilters { @@ -42,7 +48,7 @@ export interface ICommunityIdentity { */ export interface IPostCommentIdentity { readonly author: IAccountIdentity; - readonly id: string; + readonly permlink: string; } export interface IVote { @@ -88,7 +94,9 @@ export interface IComment extends IPostCommentIdentity { enumVotes(filter: IPostCommentsFilters, pagination: IPagination): Iterable; getContent(): string; wasVotedByUser(userName: IAccountIdentity): boolean; - getCommensCount(): number; + getCommentsCount(): number; + getParent(): IPostCommentIdentity; + getTopPost(): IPostCommentIdentity; /** * Allows to generate a slug for the comment, which can be used in URLs or as a unique identifier. @@ -148,6 +156,8 @@ export interface IActiveBloggingPlatform { export interface IBloggingPlatform { viewerContext?: IAccountIdentity; communityContext?: ICommunityIdentity; + getPost(postId: IPostCommentIdentity): IPost; + getComment(commentId: IPostCommentIdentity): IComment; enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Iterable; configureViewContext(accontName: IAccountIdentity, communityName?: ICommunityIdentity): void; enumCommunities(filter: ICommunityFilters, pagination: IPagination): Iterable diff --git a/packages/blog-logic/wordpress-reference.ts b/packages/blog-logic/wordpress-reference.ts index 9867048..6f3666b 100644 --- a/packages/blog-logic/wordpress-reference.ts +++ b/packages/blog-logic/wordpress-reference.ts @@ -4,11 +4,11 @@ export interface WPPost { id: number; - date: string; - date_gmt: string; + date: Date; + date_gmt: Date; guid: Rendered; - modified: string; - modified_gmt: string; + modified: Date; + modified_gmt: Date; slug: string; status: "publish" | "future" | "draft" | "pending" | "private"; type: string; @@ -22,7 +22,7 @@ export interface WPPost { ping_status: "open" | "closed"; sticky: boolean; template: string; - format: string; + format: "standard" | "aside" | "chat" | "gallery" | "link" | "image" | "quote" | "status" | "video" | "audio"; meta: Record; categories: number[]; tags: number[]; @@ -71,11 +71,11 @@ export interface WPComment { author_name: string; author_email: string; author_url: string; - date: string; - date_gmt: string; + date: Date; + date_gmt: Date; content: Rendered; link: string; - status: string; // Usually 'approved' or 'hold' + status: "publish" | "future" | "draft" | "pending" | "private"; type: string; // Usually '' (empty string for normal comment) author_ip: string; author_user_agent: string; @@ -83,3 +83,145 @@ export interface WPComment { _links?: WPLinks; } + +export interface WPGetPostsParams { + context?: "view" | "embed" | "edit"; + page?: number; + per_page?: number; + search?: string; + after?: Date; // ISO 8601 date + modified_after?: Date; // ISO 8601 date + before?: Date; // ISO 8601 date + modified_before?: Date; // ISO 8601 date + author?: number | number[]; + author_exclude?: number | number[]; + exclude?: number | number[]; + include?: number | number[]; + offset?: number; + order?: "asc" | "desc"; + orderby?: "author" | "date" | "id" | "include" | "modified" | "parent" | "relevance" | "slug" | "include_slugs" | "title"; + search_columns?: string[]; + slug?: string | string[]; + status?: string | string[]; + _fields?: string[]; +} + +export interface WPUser { + id: number; + username?: string; + name?: string; + first_name?: string; + last_name?: string; + email?: string + url?: string; + description?: string; + link: string; + locale?: string; + nickname?: string; + slug?: string; + registered_date: string; + roles?: string[]; + capabilities?: Record; + extra_capabilities?: Record; + avatar_urls: WPLink; + meta?: Record; +} + +export interface WPGetCommentsParams { + /** + * Scope under which the request is made; determines which fields appear in the response. + * One of: 'view', 'embed', 'edit' + * Default: 'view' + */ + context?: "view" | "embed" | "edit"; + + /** Current page of pagination. Default: 1 */ + page?: number; + + /** Maximum number of items per page. Default: 10 */ + per_page?: number; + + /** Limit results to comments matching this search string */ + search?: string; + + /** Limit response to comments published after this ISO-8601 date-time */ + after?: string; + + /** Limit response to comments published before this ISO-8601 date-time */ + before?: string; + + /** Limit result set to comments assigned to specific user IDs (requires authorization) */ + author?: number[]; + + /** Exclude comments assigned to specific user IDs (requires authorization) */ + author_exclude?: number[]; + + /** Limit result set to comments from a specific author email (requires authorization) */ + author_email?: string; + + /** Ensure result set excludes specific comment IDs */ + exclude?: number[]; + + /** Limit result set to specific comment IDs */ + include?: number[]; + + /** Offset the result set by a specific number of items */ + offset?: number; + + /** Order by ascending or descending. Default: 'desc' */ + order?: "asc" | "desc"; + + /** + * Attribute to sort by. + * Default: 'date_gmt' + * One of: 'date', 'date_gmt', 'id', 'include', 'post', 'parent', 'type' + */ + orderby?: "date" | "date_gmt" | "id" | "include" | "post" | "parent" | "type"; + + /** Limit result set to comments with specific parent IDs */ + parent?: number[]; + + /** Exclude comments with specific parent IDs */ + parent_exclude?: number[]; + + /** Limit result set to comments assigned to specific post IDs */ + post?: number[]; + + /** + * Limit result set to comments with a specific status (requires authorization). + * Example statuses include: 'approve' + * Default: 'approve' + */ + status?: string; + + /** + * Limit result set to comments of a specific type (requires authorization). + * Example: 'comment' + * Default: 'comment' + */ + type?: string; + + /** Password for password-protected posts */ + password?: string; +} + +export interface WPCreatePostPayload { + date?: string; + date_gmt?: string; + slug?: string; + status?: "publish" | "future" | "draft" | "pending" | "private"; + password?: string; + title?: { rendered?: string; raw?: string }; + content?: { rendered?: string; raw?: string }; + author?: number; + excerpt?: { rendered?: string; raw?: string }; + featured_media?: number; + comment_status?: "open" | "closed"; + ping_status?: "open" | "closed"; + format?: "standard" | "aside" | "chat" | "gallery" | "link" | "image" | "quote" | "status" | "video" | "audio"; + sticky?: boolean; + template?: string; + categories?: number[]; + tags?: number[]; + meta?: Record; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 23d2b21..c37730c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,7 +62,7 @@ catalogs: version: 2.8.1 tsx: specifier: 4.19.3 - version: 4.19.2 + version: 4.19.3 typescript: specifier: 5.7.3 version: 5.7.3 @@ -176,7 +176,7 @@ importers: version: 2.8.1 tsx: specifier: catalog:typescript-toolset - version: 4.19.2 + version: 4.19.3 typedoc: specifier: catalog:typedoc-toolset version: 0.27.3(typescript@5.7.3) @@ -204,146 +204,158 @@ packages: resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} engines: {node: '>=6.9.0'} - '@esbuild/aix-ppc64@0.23.1': - resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.23.1': - resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.23.1': - resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.23.1': - resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.23.1': - resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.23.1': - resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.23.1': - resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.23.1': - resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.23.1': - resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.23.1': - resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.23.1': - resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.23.1': - resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.23.1': - resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.23.1': - resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.23.1': - resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.23.1': - resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.23.1': - resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.23.1': - resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.23.1': - resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.23.1': - resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.23.1': - resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.23.1': - resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.23.1': - resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.23.1': - resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1283,8 +1295,8 @@ packages: es6-promise@4.2.8: resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - esbuild@0.23.1: - resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} hasBin: true @@ -1792,9 +1804,6 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-core-module@2.15.1: resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} engines: {node: '>= 0.4'} @@ -2460,10 +2469,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - qs@6.11.2: - resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} - engines: {node: '>=0.6'} - qs@6.14.0: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} @@ -2854,8 +2859,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsx@4.19.2: - resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + tsx@4.19.3: + resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -3070,76 +3075,82 @@ snapshots: '@babel/helper-validator-identifier@7.25.9': optional: true - '@esbuild/aix-ppc64@0.23.1': + '@esbuild/aix-ppc64@0.25.9': + optional: true + + '@esbuild/android-arm64@0.25.9': + optional: true + + '@esbuild/android-arm@0.25.9': optional: true - '@esbuild/android-arm64@0.23.1': + '@esbuild/android-x64@0.25.9': optional: true - '@esbuild/android-arm@0.23.1': + '@esbuild/darwin-arm64@0.25.9': optional: true - '@esbuild/android-x64@0.23.1': + '@esbuild/darwin-x64@0.25.9': optional: true - '@esbuild/darwin-arm64@0.23.1': + '@esbuild/freebsd-arm64@0.25.9': optional: true - '@esbuild/darwin-x64@0.23.1': + '@esbuild/freebsd-x64@0.25.9': optional: true - '@esbuild/freebsd-arm64@0.23.1': + '@esbuild/linux-arm64@0.25.9': optional: true - '@esbuild/freebsd-x64@0.23.1': + '@esbuild/linux-arm@0.25.9': optional: true - '@esbuild/linux-arm64@0.23.1': + '@esbuild/linux-ia32@0.25.9': optional: true - '@esbuild/linux-arm@0.23.1': + '@esbuild/linux-loong64@0.25.9': optional: true - '@esbuild/linux-ia32@0.23.1': + '@esbuild/linux-mips64el@0.25.9': optional: true - '@esbuild/linux-loong64@0.23.1': + '@esbuild/linux-ppc64@0.25.9': optional: true - '@esbuild/linux-mips64el@0.23.1': + '@esbuild/linux-riscv64@0.25.9': optional: true - '@esbuild/linux-ppc64@0.23.1': + '@esbuild/linux-s390x@0.25.9': optional: true - '@esbuild/linux-riscv64@0.23.1': + '@esbuild/linux-x64@0.25.9': optional: true - '@esbuild/linux-s390x@0.23.1': + '@esbuild/netbsd-arm64@0.25.9': optional: true - '@esbuild/linux-x64@0.23.1': + '@esbuild/netbsd-x64@0.25.9': optional: true - '@esbuild/netbsd-x64@0.23.1': + '@esbuild/openbsd-arm64@0.25.9': optional: true - '@esbuild/openbsd-arm64@0.23.1': + '@esbuild/openbsd-x64@0.25.9': optional: true - '@esbuild/openbsd-x64@0.23.1': + '@esbuild/openharmony-arm64@0.25.9': optional: true - '@esbuild/sunos-x64@0.23.1': + '@esbuild/sunos-x64@0.25.9': optional: true - '@esbuild/win32-arm64@0.23.1': + '@esbuild/win32-arm64@0.25.9': optional: true - '@esbuild/win32-ia32@0.23.1': + '@esbuild/win32-ia32@0.25.9': optional: true - '@esbuild/win32-x64@0.23.1': + '@esbuild/win32-x64@0.25.9': optional: true '@eslint-community/eslint-utils@4.4.0(eslint@9.20.1(jiti@2.4.2))': @@ -3687,7 +3698,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.3.4 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -4281,32 +4292,34 @@ snapshots: es6-promise@4.2.8: {} - esbuild@0.23.1: + esbuild@0.25.9: optionalDependencies: - '@esbuild/aix-ppc64': 0.23.1 - '@esbuild/android-arm': 0.23.1 - '@esbuild/android-arm64': 0.23.1 - '@esbuild/android-x64': 0.23.1 - '@esbuild/darwin-arm64': 0.23.1 - '@esbuild/darwin-x64': 0.23.1 - '@esbuild/freebsd-arm64': 0.23.1 - '@esbuild/freebsd-x64': 0.23.1 - '@esbuild/linux-arm': 0.23.1 - '@esbuild/linux-arm64': 0.23.1 - '@esbuild/linux-ia32': 0.23.1 - '@esbuild/linux-loong64': 0.23.1 - '@esbuild/linux-mips64el': 0.23.1 - '@esbuild/linux-ppc64': 0.23.1 - '@esbuild/linux-riscv64': 0.23.1 - '@esbuild/linux-s390x': 0.23.1 - '@esbuild/linux-x64': 0.23.1 - '@esbuild/netbsd-x64': 0.23.1 - '@esbuild/openbsd-arm64': 0.23.1 - '@esbuild/openbsd-x64': 0.23.1 - '@esbuild/sunos-x64': 0.23.1 - '@esbuild/win32-arm64': 0.23.1 - '@esbuild/win32-ia32': 0.23.1 - '@esbuild/win32-x64': 0.23.1 + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 escalade@3.2.0: {} @@ -4558,7 +4571,7 @@ snapshots: foreground-child@3.3.0: dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 signal-exit: 4.1.0 forwarded@0.2.0: {} @@ -4801,7 +4814,7 @@ snapshots: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -4835,7 +4848,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.1 transitivePeerDependencies: - supports-color @@ -4927,10 +4940,6 @@ snapshots: is-callable@1.2.7: {} - is-core-module@2.13.1: - dependencies: - hasown: 2.0.0 - is-core-module@2.15.1: dependencies: hasown: 2.0.2 @@ -5377,7 +5386,7 @@ snapshots: normalize-package-data@5.0.0: dependencies: hosted-git-info: 6.1.1 - is-core-module: 2.13.1 + is-core-module: 2.15.1 semver: 7.6.3 validate-npm-package-license: 3.0.4 @@ -5653,10 +5662,6 @@ snapshots: punycode@2.3.1: {} - qs@6.11.2: - dependencies: - side-channel: 1.0.4 - qs@6.14.0: dependencies: side-channel: 1.1.0 @@ -5970,7 +5975,7 @@ snapshots: socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 - debug: 4.3.4 + debug: 4.4.1 socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -6148,9 +6153,9 @@ snapshots: tslib@2.8.1: {} - tsx@4.19.2: + tsx@4.19.3: dependencies: - esbuild: 0.23.1 + esbuild: 0.25.9 get-tsconfig: 4.10.0 optionalDependencies: fsevents: 2.3.3 @@ -6158,7 +6163,7 @@ snapshots: tuf-js@1.1.7: dependencies: '@tufjs/models': 1.0.4 - debug: 4.3.4 + debug: 4.4.1 make-fetch-happen: 11.1.1 transitivePeerDependencies: - supports-color @@ -6264,7 +6269,7 @@ snapshots: union@0.5.0: dependencies: - qs: 6.11.2 + qs: 6.14.0 unique-filename@2.0.1: dependencies: -- GitLab From 6b7a5a7b5aa29c27a6ce1d1a8681d192cff2b7a6 Mon Sep 17 00:00:00 2001 From: jlachor Date: Mon, 8 Sep 2025 13:48:26 +0200 Subject: [PATCH 03/15] Implement first version of Blog Logic --- examples/wordpress-rest-api/example-config.ts | 12 +- examples/wordpress-rest-api/hiveToWpMap.ts | 81 ++++++++++ .../wordpress-rest-api/wordpress-rest-api.ts | 71 ++++++--- packages/blog-logic/BloggingPlatform.ts | 65 ++++++++ packages/blog-logic/Comment.ts | 60 ++++++++ packages/blog-logic/Post.ts | 74 ++++++++++ packages/blog-logic/Reply.ts | 19 +++ packages/blog-logic/interfaces.ts | 56 ++++--- packages/blog-logic/wax.ts | 139 ++++++++++++++++++ 9 files changed, 523 insertions(+), 54 deletions(-) create mode 100644 packages/blog-logic/BloggingPlatform.ts create mode 100644 packages/blog-logic/Comment.ts create mode 100644 packages/blog-logic/Post.ts create mode 100644 packages/blog-logic/Reply.ts create mode 100644 packages/blog-logic/wax.ts diff --git a/examples/wordpress-rest-api/example-config.ts b/examples/wordpress-rest-api/example-config.ts index 3994c95..a418e88 100644 --- a/examples/wordpress-rest-api/example-config.ts +++ b/examples/wordpress-rest-api/example-config.ts @@ -1,4 +1,14 @@ -export const wordPressExampleConfig = { +export interface WordPressConfig { + postLimit: number; + observer: string; + sort: "trending" | "hot" | "created" | "promoted" | "payout" | "payout_comments" | "muted"; + startAuthor: string; + startPermlink: string; + postTag: string; + defaultPort: number; +} + +export const wordPressExampleConfig: WordPressConfig = { postLimit: 10, observer: "hive.blog", sort: "created", // "trending" / "hot" / "created" / "promoted" / "payout" / "payout_comments" / "muted" diff --git a/examples/wordpress-rest-api/hiveToWpMap.ts b/examples/wordpress-rest-api/hiveToWpMap.ts index c4e62ee..b711801 100644 --- a/examples/wordpress-rest-api/hiveToWpMap.ts +++ b/examples/wordpress-rest-api/hiveToWpMap.ts @@ -1,3 +1,4 @@ +import { IPost, IReply } from "../../packages/blog-logic/interfaces"; import { simpleHash } from "./hash-utils"; import { Entry } from "./hive"; import { WPComment, WPPost, WPTag, WPTerm } from "./wp-reference"; @@ -88,6 +89,60 @@ export const mapHivePostToWpPost = (hivePost: Entry, wpId: number, accountId: nu return wpPost } +export const mapIPostToWpPost = async (hivePost: IPost, wpId: number, accountId: number): Promise => { + const slug = hivePost.generateSlug(); + const tags = hivePost?.tags || []; + const wpTermTags = tags.map((tag) => mapWpTerm(tag, "tag")); + const community = hivePost.community?.name; + const wpTermCategory = community ? [mapWpTerm(community, "category")] : []; + const renderedBody = renderer.render(await hivePost.getContent()); + const wpExcerpt = renderedBody.replace(/<[^>]+>/g, '').substring(0, 100); + const wpPost: WPPost = { + id:wpId, + slug, + date: new Date(hivePost.publishedAt).toISOString(), + date_gmt: new Date(hivePost.publishedAt).toISOString(), + modified: new Date(hivePost.updatedAt).toISOString(), + modified_gmt: new Date(hivePost.updatedAt).toISOString(), + status: "publish", + type: "post", + link: `http://host/${slug}/`, + title: { rendered: hivePost.title }, + content: { rendered: renderedBody, protected: false }, + excerpt: { rendered: wpExcerpt, protected: false }, + author: accountId, + featured_media: 0, + comment_status: "open", + ping_status: "open", + sticky: false, + template: "", + format: "standard", + meta: {}, + categories: [community ? simpleHash(community) : 0], + tags: tags.map((tags) => simpleHash(tags)), + guid: { rendered: `http://host/?p=${wpId}` }, + class_list: [`category-${community}`], + _embedded: { + replies: [], + author: [{ + id: accountId, + name: hivePost.author.name, + url: `https://hive.blog/@${hivePost.author}`, + description: "", + link: `https://hive.blog/@${hivePost.author}`, + slug: hivePost.author.name, + avatar_urls: { + 24: `https://images.hive.blog/u/${hivePost.author}/avatar`, + 48: `https://images.hive.blog/u/${hivePost.author}/avatar`, + 96: `https://images.hive.blog/u/${hivePost.author}/avatar` + } + }], + "wp:term": [...wpTermTags.map((wpTerm) => [wpTerm]), ...wpTermCategory.map((wpTerm) => [wpTerm])] + } + }; + return wpPost +} + export const mapHiveCommentToWPComment = (hiveComment: Entry, commentId: number, mainPostId: number, authorId: number): WPComment => { const parentId = simpleHash(`${hiveComment.parent_author}_${hiveComment.parent_permlink}`); const renderedBody = renderer.render(hiveComment.body); @@ -114,6 +169,32 @@ export const mapHiveCommentToWPComment = (hiveComment: Entry, commentId: number, return wpComment; } +export const mapIReplyToWPComment = async (hiveComment: IReply, commentId: number, mainPostId: number, authorId: number): Promise => { + const parentId = simpleHash(`${hiveComment.parent.author}_${hiveComment.parent.permlink}`); + const renderedBody = renderer.render(await hiveComment.getContent()); + const wpComment: WPComment = { + id: commentId, + post: mainPostId, + parent: parentId === mainPostId ? 0 : parentId, // There is no id for parent post + author: authorId, + author_name: hiveComment.author.name, + author_url: `https://hive.blog/@${hiveComment.author}`, + date: new Date(hiveComment.publishedAt).toISOString(), + date_gmt: new Date(hiveComment.publishedAt).toISOString(), + content: { rendered: renderedBody }, + link: `http://host/${hiveComment.parent.author.name}_${hiveComment.parent.permlink}/#comment-${commentId}`, + status: "approved", + type: "comment", + meta: [], + author_avatar_urls: { + 24: `https://images.hive.blog/u/${hiveComment.author}/avatar`, + 48: `https://images.hive.blog/u/${hiveComment.author}/avatar`, + 96: `https://images.hive.blog/u/${hiveComment.author}/avatar` + } + } + return wpComment; +} + // For later use export const mapHiveTagsToWpTags = (tagSlug: string): WPTag => { return { diff --git a/examples/wordpress-rest-api/wordpress-rest-api.ts b/examples/wordpress-rest-api/wordpress-rest-api.ts index 772a408..18b1abb 100644 --- a/examples/wordpress-rest-api/wordpress-rest-api.ts +++ b/examples/wordpress-rest-api/wordpress-rest-api.ts @@ -1,11 +1,13 @@ import express, { Request, Response } from "express"; import cors from "cors"; -import { createHiveChain } from "@hiveio/wax"; +import { BlogPostOperation, createHiveChain } from "@hiveio/wax"; import { Entry, ExtendedNodeApi } from "./hive"; -import { mapHiveCommentToWPComment, mapHivePostToWpPost, mapHiveTagsToWpTags } from "./hiveToWpMap"; +import { mapHiveCommentToWPComment, mapHivePostToWpPost, mapHiveTagsToWpTags, mapIPostToWpPost, mapIReplyToWPComment } from "./hiveToWpMap"; import { WPComment, WPPost } from "./wp-reference"; import { simpleHash } from "./hash-utils"; import { wordPressExampleConfig } from "./example-config"; +import { IPost, IReply } from "../../packages/blog-logic/interfaces"; +import { BloggingPlaform } from "../../packages/blog-logic/BloggingPlatform"; const hiveChain = await createHiveChain(); const extendedHiveChain = hiveChain.extend(); @@ -20,6 +22,9 @@ const apiRouter = express.Router(); const idToStringMap = new Map(); +const posts: IPost[] = []; +const bloggingPlatform: BloggingPlaform = new BloggingPlaform(); + const getAuthorPermlinkFromSlug = (slug: string): {author: string, permlink: string} => { const splitedSlug = slug.split("_"); const author = splitedSlug[0]; @@ -43,6 +48,19 @@ const mapAndAddPostsToMap = (posts: Entry[]): WPPost[] => { } +const mapAndAddtoMapPosts = async (posts: IPost[]): Promise => { + const mappedPosts: WPPost[] = []; + posts.forEach(async (post) => { + const postId = simpleHash(`${post.author.name}_${post.permlink}`); + const authorId = simpleHash(post.author.name); + idToStringMap.set(postId, `${post.author.name}_${post.permlink}`).set(authorId, post.author.name); + posts.push(post); + mappedPosts.push(await mapIPostToWpPost(post, postId, authorId)); + }); + return await mappedPosts; +} + + apiRouter.get("/posts", async (req: Request, res: Response) => { // Default WP call for devtools if (req.query.slug === "com.chrome.devtools.json") res.json([]); @@ -52,24 +70,27 @@ apiRouter.get("/posts", async (req: Request, res: Response) => { const authorPermlinkHash = simpleHash(req.query.slug); const authorHash = simpleHash(author); idToStringMap.set(authorPermlinkHash, req.query.slug as string).set(authorHash, author); - const result = await extendedHiveChain.api.bridge.get_post({author, permlink, observer: "hive.blog"}); - if (result) { - res.json(mapHivePostToWpPost(result, authorPermlinkHash, authorHash)); + const post = await bloggingPlatform.getPost({author: {name: author}, permlink}); + posts.push(post); + if (post) { + res.json(await mapIPostToWpPost(post, authorPermlinkHash, authorHash)); } else { res.status(404).json({ error: "Post not found" }); } // Posts list } else { - const result = await extendedHiveChain.api.bridge.get_ranked_posts({ + const posts = await bloggingPlatform.enumPosts({ limit: wordPressExampleConfig.postLimit, sort: wordPressExampleConfig.sort, - observer: wordPressExampleConfig.observer, - start_author: wordPressExampleConfig.startAuthor, - start_permlink: wordPressExampleConfig.startPermlink, + startAuthor: wordPressExampleConfig.startAuthor, + startPermlink: wordPressExampleConfig.startPermlink, tag: wordPressExampleConfig.postTag - }); - if (result) { - res.json(mapAndAddPostsToMap(result)); + }, { + page: 1, + pageSize: 10 + }) as IPost[]; + if (posts) { + res.json(await mapAndAddtoMapPosts(posts)); } } @@ -79,17 +100,21 @@ apiRouter.get("/comments", async (req: Request, res: Response) => { const postId = Number(req.query.post); const postParent = idToStringMap.get(postId); if (postParent) { - const result = await extendedHiveChain.api.bridge.get_discussion({author: postParent.split("_")[0], permlink: postParent.split("_").slice(1).join("_")}); - if (result) { - const wpComments: WPComment[] = [] - Object.entries(result).forEach(([authorPermlink, comment]) => { - if (comment.parent_author && comment.parent_permlink) { - const wpAuthorPermlink = authorPermlink.replace("/", "_"); - const wpComment = mapHiveCommentToWPComment(comment, simpleHash(wpAuthorPermlink), postId, simpleHash(comment.author)); - wpComments.push(wpComment) - } - }); - res.json(wpComments) + const {author, permlink} = getAuthorPermlinkFromSlug(postParent); + const post = posts.find((post) => post.author.name === author && post.permlink === permlink); + if (post) { + const replies = await post.enumReplies({}, {page: 1, pageSize: 10}) as IReply[]; + if (replies) { + const wpComments: WPComment[] = [] + Object.entries(replies).forEach( async ([authorPermlink, reply]) => { + if (reply.author.name && reply.permlink) { + const wpAuthorPermlink = authorPermlink.replace("/", "_"); + const wpComment = await mapIReplyToWPComment(reply, simpleHash(wpAuthorPermlink), postId, simpleHash(reply.author.name)); + wpComments.push(wpComment) + } + }); + res.json(wpComments) + } } } else { res.json([]); diff --git a/packages/blog-logic/BloggingPlatform.ts b/packages/blog-logic/BloggingPlatform.ts new file mode 100644 index 0000000..f0d808f --- /dev/null +++ b/packages/blog-logic/BloggingPlatform.ts @@ -0,0 +1,65 @@ +import { TWaxExtended } from "@hiveio/wax"; +import { IAccountIdentity, + IBloggingPlatform, + ICommunity, + ICommunityFilters, + IPagination, + IPost, + IPostCommentIdentity, + IPostCommentsFilters +} from "./interfaces"; +import { Post } from "./Post"; +import { ExtendedNodeApi, getWax } from "./wax"; + +export class BloggingPlaform implements IBloggingPlatform { + public viewerContext: IAccountIdentity; + + private chain?: TWaxExtended + + private initializeChain = async () => { + if (!this.chain) + this.chain = await getWax(); + } + + + public constructor() { + this.viewerContext = {name: "hive.blog"}; // Set default + this.initializeChain(); + } + + public configureViewContext(accontName: IAccountIdentity): void { + this.viewerContext = accontName; + } + + + public async getPost(postId: IPostCommentIdentity): Promise { + if (!this.chain) + await this.initializeChain(); + const postData = await this.chain?.api.bridge.get_post({author: postId.author.name, permlink: postId.permlink, observer: this.viewerContext.name }); + if (!postData) + throw new Error("Post not found"); + return new Post(postId, postData!); + } + + public async enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise> { + return await []; + } + + public async enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Promise> { + if (!this.chain) + await this.initializeChain(); + const posts = await this.chain?.api.bridge.get_ranked_posts({ + limit: filter.limit, + sort: filter.sort, + observer: this.viewerContext.name, + start_author: filter.startAuthor, + start_permlink: filter.startPermlink, + tag: filter.tag + }); + if (!posts) + throw new Error("Posts not found"); + return posts?.map((post) => new Post({author: {name: post.author}, permlink: post.permlink}, post)) + } + + +} \ No newline at end of file diff --git a/packages/blog-logic/Comment.ts b/packages/blog-logic/Comment.ts new file mode 100644 index 0000000..b2131b3 --- /dev/null +++ b/packages/blog-logic/Comment.ts @@ -0,0 +1,60 @@ +import { TWaxExtended } from "@hiveio/wax"; +import { IAccount, IAccountIdentity, IComment, ICommunityIdentity, IPagination, IPost, IPostCommentIdentity, IPostCommentsFilters, IReply, IVote } from "./interfaces"; +import { Entry, ExtendedNodeApi, getWax } from "./wax"; +import { Reply } from "./Reply"; + +export class Comment implements IComment { + + protected chain: TWaxExtended + + public author: IAccountIdentity; + public permlink: string; + public publishedAt: Date; + public updatedAt: Date; + + + protected content?: string; + protected votes?: Iterable; + + private initializeChain = async () => { + if (!this.chain) + this.chain = await getWax(); + + } + + public constructor(authorPermlink: IPostCommentIdentity, postCommentData?: Entry) { + this.initializeChain(); + this.author = authorPermlink.author; + this.permlink = authorPermlink.permlink; + + if(postCommentData) { + this.publishedAt = new Date(postCommentData.created); + this.updatedAt = new Date(postCommentData.updated); + this.content = postCommentData.body; + } + } + + public generateSlug(): string { + return `${this.author.name}_${this.permlink}`; + } + + public async enumMentionedAccounts(): Promise> { + return []; + } + + public async getContent(): Promise { + if (this.content) + return this.content; + await this.chain.api.bridge.get_post({author: this.author.name, permlink: this.permlink, observer: "hive.blog"}); + return this.content || ""; + } + + public async enumVotes(filter: IPostCommentsFilters, pagination: IPagination): Promise> { + return []; + } + + public async wasVotedByUser(userName: IAccountIdentity): Promise { + return false; + } + +} \ No newline at end of file diff --git a/packages/blog-logic/Post.ts b/packages/blog-logic/Post.ts new file mode 100644 index 0000000..b224e4d --- /dev/null +++ b/packages/blog-logic/Post.ts @@ -0,0 +1,74 @@ +import { Comment } from "./Comment"; +import { ICommunityIdentity, IPagination, IPost, IPostCommentIdentity, IPostCommentsFilters, IReply } from "./interfaces"; +import { Reply } from "./Reply"; +import { Entry } from "./wax"; + +export class Post extends Comment implements IPost { + + public title: string; + public tags: string[]; + public community?: ICommunityIdentity | undefined; + public summary: string; + + private replies?: IReply[]; + + public constructor(authorPermlink: IPostCommentIdentity, postData?: Entry) { + super(authorPermlink, postData); + if (postData) { + this.title = postData.title; + this.tags = postData.json_metadata?.tags || []; + this.summary = postData.json_metadata?.description || ""; + this.community = postData.community ? {name: postData.community} : undefined; + } + } + + private async fetchReplies(): Promise { + if (!this.replies) { + const repliesData = await this.chain.api.bridge.get_discussion({ + author: this.author.name, + permlink: this.permlink, + observer: "hive.blog", + }); // Temporary hive.blog; + if (!repliesData) + throw "No replies"; + const replies = Object.entries(repliesData)?.map( + ([authorPermlink, reply]) => + new Reply( + { author: { name: reply.author }, permlink: reply.permlink }, + { + author: { name: reply.parent_author || "" }, + permlink: reply.parent_permlink || "", + }, + { author: this.author, permlink: this.permlink }, + reply + ) + ); + this.replies = replies; + return replies; + } + return this.replies; + } + + public async getContent(): Promise { + if (this.content) + return this.content; + await this.chain.api.bridge.get_post({author: this.author.name, permlink: this.permlink, observer: "hive.blog"}); + return this.content || ""; + } + + public getTitleImage(): string { + return ""; + } + + public async enumReplies(filter: IPostCommentsFilters, pagination: IPagination): Promise> { + if (this.replies) return this.replies; + return await this.fetchReplies(); + } + + public async getCommentsCount(): Promise { + if (this.replies) return this.replies.length; + + return (await this.fetchReplies()).length; + } + +} \ No newline at end of file diff --git a/packages/blog-logic/Reply.ts b/packages/blog-logic/Reply.ts new file mode 100644 index 0000000..7507532 --- /dev/null +++ b/packages/blog-logic/Reply.ts @@ -0,0 +1,19 @@ +import { Comment } from "./Comment"; +import { IPostCommentIdentity, IReply } from "./interfaces"; +import { Entry } from "./wax"; + + +export class Reply extends Comment implements IReply { + + public parent: IPostCommentIdentity; + public topPost: IPostCommentIdentity; + + + public constructor(authorPermlink: IPostCommentIdentity, parent: IPostCommentIdentity, topPost: IPostCommentIdentity, replyData: Entry) { + super(authorPermlink, replyData); + this.parent = parent; + this.topPost = topPost; + } + + +} diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts index cd2db66..5c3119e 100644 --- a/packages/blog-logic/interfaces.ts +++ b/packages/blog-logic/interfaces.ts @@ -20,14 +20,11 @@ export interface IVotesFilters extends ICommonFilters { } export interface IPostCommentsFilters extends ICommonFilters { - readonly sortBy?: "author" | "date" | "id" | "include" | "modified" | "parent" | "relevance" | "slug" | "include_slugs" | "title"; - readonly positiveVotes?: boolean; - readonly tags?: string[]; - readonly modificationStartTime?: Date; - readonly modificationEndTime?: Date; - readonly author?: string; - readonly searchInText?: string; - readonly slug?: string | string[]; + readonly limit: number; + readonly sort: "trending" | "hot" | "created" | "promoted" | "payout" | "payout_comments" | "muted"; + readonly startAuthor: string; + readonly startPermlink: string; + readonly tag: string; } export interface ICommunityFilters extends ICommonFilters { @@ -87,16 +84,14 @@ export interface IAccount extends IAccountIdentity { export interface IComment extends IPostCommentIdentity { readonly publishedAt: Date; readonly updatedAt: Date; - readonly author: IAccountIdentity; - enumReplies(filter: IPostCommentsFilters, pagination: IPagination): Iterable; - enumMentionedAccounts(): Iterable; - enumVotes(filter: IPostCommentsFilters, pagination: IPagination): Iterable; - getContent(): string; - wasVotedByUser(userName: IAccountIdentity): boolean; - getCommentsCount(): number; - getParent(): IPostCommentIdentity; - getTopPost(): IPostCommentIdentity; + + enumMentionedAccounts(): Promise>; + enumVotes(filter: IPostCommentsFilters, pagination: IPagination): Promise>; + getContent(): Promise; + wasVotedByUser(userName: IAccountIdentity): Promise; + + /** * Allows to generate a slug for the comment, which can be used in URLs or as a unique identifier. @@ -108,7 +103,8 @@ export interface IComment extends IPostCommentIdentity { * Represents a reply to a post or another reply object. */ export interface IReply extends IComment { - readonly parent: IPostCommentIdentity; + parent: IPostCommentIdentity; + topPost: IPostCommentIdentity; } export interface ISession { @@ -119,11 +115,13 @@ export interface ISession { * Represents a post (article) published on the platform. */ export interface IPost extends IComment { - readonly title: string; - readonly summary: string; - readonly tags: string[]; - readonly community?: ICommunityIdentity; + title: string; + summary: string; + tags: string[]; + community?: ICommunityIdentity; + getCommentsCount(): Promise; + enumReplies(filter: ICommonFilters, pagination: IPagination): Promise>; getTitleImage(): string; } @@ -155,14 +153,12 @@ export interface IActiveBloggingPlatform { export interface IBloggingPlatform { viewerContext?: IAccountIdentity; - communityContext?: ICommunityIdentity; - getPost(postId: IPostCommentIdentity): IPost; - getComment(commentId: IPostCommentIdentity): IComment; - enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Iterable; - configureViewContext(accontName: IAccountIdentity, communityName?: ICommunityIdentity): void; - enumCommunities(filter: ICommunityFilters, pagination: IPagination): Iterable - - authorize(provider: IAuthenticationProvider): Promise; + getPost(postId: IPostCommentIdentity): Promise; + enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Promise>; + configureViewContext(accontName: IAccountIdentity): void; + enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise> + + // authorize(provider: IAuthenticationProvider): Promise; } // UI integration with mock data diff --git a/packages/blog-logic/wax.ts b/packages/blog-logic/wax.ts new file mode 100644 index 0000000..151b9e3 --- /dev/null +++ b/packages/blog-logic/wax.ts @@ -0,0 +1,139 @@ +import { createHiveChain, TWaxExtended, + TWaxApiRequest +} from "@hiveio/wax"; + + +export interface IGetPostHeader { + author: string; + permlink: string; + category: string; + depth: number; +} + +export interface JsonMetadata { + image: string; + links?: string[]; + flow?: { + pictures: { + caption: string; + id: number; + mime: string; + name: string; + tags: string[]; + url: string; + }[]; + tags: string[]; + }; + images: string[]; + author: string | undefined; + tags?: string[]; + description?: string | null; + app?: string; + canonical_url?: string; + format?: string; + original_author?: string; + original_permlink?: string; + summary?: string; +} + +export interface EntryVote { + voter: string; + rshares: number; +} + +export interface EntryBeneficiaryRoute { + account: string; + weight: number; +} + +export interface EntryStat { + flag_weight: number; + gray: boolean; + hide: boolean; + total_votes: number; + is_pinned?: boolean; +} + + +export interface Entry { + active_votes: EntryVote[]; + author: string; + author_payout_value: string; + author_reputation: number; + author_role?: string; + author_title?: string; + beneficiaries: EntryBeneficiaryRoute[]; + blacklists: string[]; + body: string; + category: string; + children: number; + community?: string; + community_title?: string; + created: string; + total_votes?: number; + curator_payout_value: string; + depth: number; + is_paidout: boolean; + json_metadata: JsonMetadata; + max_accepted_payout: string; + net_rshares: number; + parent_author?: string; + parent_permlink?: string; + payout: number; + payout_at: string; + pending_payout_value: string; + percent_hbd: number; + permlink: string; + post_id: number; + id?: number; + promoted: string; + reblogged_by?: string[]; + replies: Array; + stats?: EntryStat; + title: string; + updated: string; + url: string; + original_entry?: Entry; +} + +export type ExtendedNodeApi = { + bridge: { + get_post_header: TWaxApiRequest<{ author: string; permlink: string }, IGetPostHeader>; + get_post: TWaxApiRequest<{ author: string; permlink: string; observer: string }, Entry | null>; + get_discussion: TWaxApiRequest< + { author: string; permlink: string; observer?: string }, + Record | null + >; + get_ranked_posts: TWaxApiRequest< + { + sort: string; + tag: string; + start_author: string; + start_permlink: string; + limit: number; + observer: string; + }, + Entry[] | null + >; + get_account_posts: TWaxApiRequest< + { + sort: string; + account: string; + start_author: string; + start_permlink: string; + limit: number; + observer: string; + }, + Entry[] | null + >; + }; +}; + +let chain: Promise>; + +export const getWax = () => { + if (!chain) + return chain = createHiveChain().then(chain => chain.extend()); + + return chain; +}; -- GitLab From c99650c432cce5544f71645c176b9a2ff90d3822 Mon Sep 17 00:00:00 2001 From: jlachor Date: Mon, 15 Sep 2025 14:08:53 +0200 Subject: [PATCH 04/15] QA and upgrades in Blog Logic --- examples/wordpress-rest-api/hiveToWpMap.ts | 90 ++----------------- .../wordpress-rest-api/wordpress-rest-api.ts | 58 ++++++------ packages/blog-logic/BloggingPlatform.ts | 12 +-- packages/blog-logic/Comment.ts | 33 ++++--- packages/blog-logic/Post.ts | 46 +++++----- packages/blog-logic/Reply.ts | 17 ++-- packages/blog-logic/Vote.ts | 14 +++ packages/blog-logic/interfaces.ts | 12 +-- packages/blog-logic/utils.ts | 7 ++ packages/blog-logic/wax.ts | 15 ++++ 10 files changed, 134 insertions(+), 170 deletions(-) create mode 100644 packages/blog-logic/Vote.ts create mode 100644 packages/blog-logic/utils.ts diff --git a/examples/wordpress-rest-api/hiveToWpMap.ts b/examples/wordpress-rest-api/hiveToWpMap.ts index b711801..de9d554 100644 --- a/examples/wordpress-rest-api/hiveToWpMap.ts +++ b/examples/wordpress-rest-api/hiveToWpMap.ts @@ -35,65 +35,11 @@ const mapWpTerm = (termName: string, type: "tag" | "category"): WPTerm => { } -export const mapHivePostToWpPost = (hivePost: Entry, wpId: number, accountId: number): WPPost => { - const slug = `${hivePost.author}_${hivePost.permlink}`; - const tags = hivePost.json_metadata?.tags || []; - const wpTermTags = tags.map((tag) => mapWpTerm(tag, "tag")); - const community = hivePost.community_title; - const wpTermCategory = community ? [mapWpTerm(community, "category")] : []; - const renderedBody = renderer.render(hivePost.body); - const wpExcerpt = renderedBody.replace(/<[^>]+>/g, '').substring(0, 100); - const wpPost: WPPost = { - id:wpId, - slug, - date: new Date(hivePost.created).toISOString(), - date_gmt: new Date(hivePost.created).toISOString(), - modified: new Date(hivePost.updated).toISOString(), - modified_gmt: new Date(hivePost.updated).toISOString(), - status: "publish", - type: "post", - link: `http://host/${slug}/`, - title: { rendered: hivePost.title }, - content: { rendered: renderedBody, protected: false }, - excerpt: { rendered: wpExcerpt, protected: false }, - author: accountId, - featured_media: 0, - comment_status: "open", - ping_status: "open", - sticky: false, - template: "", - format: "standard", - meta: {}, - categories: [community ? simpleHash(community) : 0], - tags: tags.map((tags) => simpleHash(tags)), - guid: { rendered: `http://host/?p=${wpId}` }, - class_list: [`category-${community}`], - _embedded: { - replies: [], - author: [{ - id: accountId, - name: hivePost.author, - url: `https://hive.blog/@${hivePost.author}`, - description: "", - link: `https://hive.blog/@${hivePost.author}`, - slug: hivePost.author, - avatar_urls: { - 24: `https://images.hive.blog/u/${hivePost.author}/avatar`, - 48: `https://images.hive.blog/u/${hivePost.author}/avatar`, - 96: `https://images.hive.blog/u/${hivePost.author}/avatar` - } - }], - "wp:term": [...wpTermTags.map((wpTerm) => [wpTerm]), ...wpTermCategory.map((wpTerm) => [wpTerm])] - } - }; - return wpPost -} - export const mapIPostToWpPost = async (hivePost: IPost, wpId: number, accountId: number): Promise => { const slug = hivePost.generateSlug(); const tags = hivePost?.tags || []; const wpTermTags = tags.map((tag) => mapWpTerm(tag, "tag")); - const community = hivePost.community?.name; + const community = hivePost.communityTitle; const wpTermCategory = community ? [mapWpTerm(community, "category")] : []; const renderedBody = renderer.render(await hivePost.getContent()); const wpExcerpt = renderedBody.replace(/<[^>]+>/g, '').substring(0, 100); @@ -126,11 +72,11 @@ export const mapIPostToWpPost = async (hivePost: IPost, wpId: number, accountId: replies: [], author: [{ id: accountId, - name: hivePost.author.name, + name: hivePost.author, url: `https://hive.blog/@${hivePost.author}`, description: "", link: `https://hive.blog/@${hivePost.author}`, - slug: hivePost.author.name, + slug: hivePost.author, avatar_urls: { 24: `https://images.hive.blog/u/${hivePost.author}/avatar`, 48: `https://images.hive.blog/u/${hivePost.author}/avatar`, @@ -143,32 +89,6 @@ export const mapIPostToWpPost = async (hivePost: IPost, wpId: number, accountId: return wpPost } -export const mapHiveCommentToWPComment = (hiveComment: Entry, commentId: number, mainPostId: number, authorId: number): WPComment => { - const parentId = simpleHash(`${hiveComment.parent_author}_${hiveComment.parent_permlink}`); - const renderedBody = renderer.render(hiveComment.body); - const wpComment: WPComment = { - id: commentId, - post: mainPostId, - parent: parentId === mainPostId ? 0 : parentId, // There is no id for parent post - author: authorId, - author_name: hiveComment.author, - author_url: `https://hive.blog/@${hiveComment.author}`, - date: new Date(hiveComment.created).toISOString(), - date_gmt: new Date(hiveComment.created).toISOString(), - content: { rendered: renderedBody }, - link: `http://host/${hiveComment.parent_author}_${hiveComment.parent_permlink}/#comment-${commentId}`, - status: "approved", - type: "comment", - meta: [], - author_avatar_urls: { - 24: `https://images.hive.blog/u/${hiveComment.author}/avatar`, - 48: `https://images.hive.blog/u/${hiveComment.author}/avatar`, - 96: `https://images.hive.blog/u/${hiveComment.author}/avatar` - } - } - return wpComment; -} - export const mapIReplyToWPComment = async (hiveComment: IReply, commentId: number, mainPostId: number, authorId: number): Promise => { const parentId = simpleHash(`${hiveComment.parent.author}_${hiveComment.parent.permlink}`); const renderedBody = renderer.render(await hiveComment.getContent()); @@ -177,12 +97,12 @@ export const mapIReplyToWPComment = async (hiveComment: IReply, commentId: numbe post: mainPostId, parent: parentId === mainPostId ? 0 : parentId, // There is no id for parent post author: authorId, - author_name: hiveComment.author.name, + author_name: hiveComment.author, author_url: `https://hive.blog/@${hiveComment.author}`, date: new Date(hiveComment.publishedAt).toISOString(), date_gmt: new Date(hiveComment.publishedAt).toISOString(), content: { rendered: renderedBody }, - link: `http://host/${hiveComment.parent.author.name}_${hiveComment.parent.permlink}/#comment-${commentId}`, + link: `http://host/${hiveComment.parent.author}_${hiveComment.parent.permlink}/#comment-${commentId}`, status: "approved", type: "comment", meta: [], diff --git a/examples/wordpress-rest-api/wordpress-rest-api.ts b/examples/wordpress-rest-api/wordpress-rest-api.ts index 18b1abb..f22a276 100644 --- a/examples/wordpress-rest-api/wordpress-rest-api.ts +++ b/examples/wordpress-rest-api/wordpress-rest-api.ts @@ -1,8 +1,8 @@ import express, { Request, Response } from "express"; import cors from "cors"; -import { BlogPostOperation, createHiveChain } from "@hiveio/wax"; -import { Entry, ExtendedNodeApi } from "./hive"; -import { mapHiveCommentToWPComment, mapHivePostToWpPost, mapHiveTagsToWpTags, mapIPostToWpPost, mapIReplyToWPComment } from "./hiveToWpMap"; +import { createHiveChain } from "@hiveio/wax"; +import { ExtendedNodeApi } from "./hive"; +import { mapIPostToWpPost, mapIReplyToWPComment } from "./hiveToWpMap"; import { WPComment, WPPost } from "./wp-reference"; import { simpleHash } from "./hash-utils"; import { wordPressExampleConfig } from "./example-config"; @@ -10,7 +10,6 @@ import { IPost, IReply } from "../../packages/blog-logic/interfaces"; import { BloggingPlaform } from "../../packages/blog-logic/BloggingPlatform"; const hiveChain = await createHiveChain(); -const extendedHiveChain = hiveChain.extend(); const app = express(); const PORT = wordPressExampleConfig.defaultPort; @@ -22,8 +21,9 @@ const apiRouter = express.Router(); const idToStringMap = new Map(); -const posts: IPost[] = []; +let posts: IPost[] = []; const bloggingPlatform: BloggingPlaform = new BloggingPlaform(); +bloggingPlatform.configureViewContext({name: wordPressExampleConfig.observer}); const getAuthorPermlinkFromSlug = (slug: string): {author: string, permlink: string} => { const splitedSlug = slug.split("_"); @@ -36,28 +36,29 @@ const getAuthorPermlinkFromSlug = (slug: string): {author: string, permlink: str } } -const mapAndAddPostsToMap = (posts: Entry[]): WPPost[] => { - const mappedPosts: WPPost[] = [] - posts.forEach((post) => { +const mapAndAddtoMapPosts = async (posts: IPost[]): Promise => { + const mappedPosts: WPPost[] = []; + posts.forEach(async (post) => { const postId = simpleHash(`${post.author}_${post.permlink}`); const authorId = simpleHash(post.author); idToStringMap.set(postId, `${post.author}_${post.permlink}`).set(authorId, post.author); - mappedPosts.push(mapHivePostToWpPost(post, postId, authorId)); + posts.push(post); + mappedPosts.push(await mapIPostToWpPost(post, postId, authorId)); }); - return mappedPosts; + return await mappedPosts; } -const mapAndAddtoMapPosts = async (posts: IPost[]): Promise => { - const mappedPosts: WPPost[] = []; - posts.forEach(async (post) => { - const postId = simpleHash(`${post.author.name}_${post.permlink}`); - const authorId = simpleHash(post.author.name); - idToStringMap.set(postId, `${post.author.name}_${post.permlink}`).set(authorId, post.author.name); - posts.push(post); - mappedPosts.push(await mapIPostToWpPost(post, postId, authorId)); +const mapReplies = async (replies: IReply[], postId: number) : Promise => { + const wpComments: WPComment[] = [] + replies.forEach( async (reply) => { + if (reply.author && reply.permlink) { + const wpAuthorPermlink = reply.generateSlug(); + const wpComment = await mapIReplyToWPComment(reply, simpleHash(wpAuthorPermlink), postId, simpleHash(reply.author)); + wpComments.push(wpComment) + } }); - return await mappedPosts; + return await wpComments; } @@ -70,7 +71,7 @@ apiRouter.get("/posts", async (req: Request, res: Response) => { const authorPermlinkHash = simpleHash(req.query.slug); const authorHash = simpleHash(author); idToStringMap.set(authorPermlinkHash, req.query.slug as string).set(authorHash, author); - const post = await bloggingPlatform.getPost({author: {name: author}, permlink}); + const post = await bloggingPlatform.getPost({author: author, permlink}); posts.push(post); if (post) { res.json(await mapIPostToWpPost(post, authorPermlinkHash, authorHash)); @@ -79,7 +80,7 @@ apiRouter.get("/posts", async (req: Request, res: Response) => { } // Posts list } else { - const posts = await bloggingPlatform.enumPosts({ + const newPosts = await bloggingPlatform.enumPosts({ limit: wordPressExampleConfig.postLimit, sort: wordPressExampleConfig.sort, startAuthor: wordPressExampleConfig.startAuthor, @@ -90,6 +91,7 @@ apiRouter.get("/posts", async (req: Request, res: Response) => { pageSize: 10 }) as IPost[]; if (posts) { + posts = [...posts, ...newPosts]; res.json(await mapAndAddtoMapPosts(posts)); } } @@ -101,19 +103,11 @@ apiRouter.get("/comments", async (req: Request, res: Response) => { const postParent = idToStringMap.get(postId); if (postParent) { const {author, permlink} = getAuthorPermlinkFromSlug(postParent); - const post = posts.find((post) => post.author.name === author && post.permlink === permlink); + const post = posts.find((post) => post.author === author && post.permlink === permlink); if (post) { - const replies = await post.enumReplies({}, {page: 1, pageSize: 10}) as IReply[]; + const replies = await post.enumReplies({}, {page: 1, pageSize: 1000}) as IReply[]; if (replies) { - const wpComments: WPComment[] = [] - Object.entries(replies).forEach( async ([authorPermlink, reply]) => { - if (reply.author.name && reply.permlink) { - const wpAuthorPermlink = authorPermlink.replace("/", "_"); - const wpComment = await mapIReplyToWPComment(reply, simpleHash(wpAuthorPermlink), postId, simpleHash(reply.author.name)); - wpComments.push(wpComment) - } - }); - res.json(wpComments) + res.json(await mapReplies(replies, postId)) } } } else { diff --git a/packages/blog-logic/BloggingPlatform.ts b/packages/blog-logic/BloggingPlatform.ts index f0d808f..eda2b10 100644 --- a/packages/blog-logic/BloggingPlatform.ts +++ b/packages/blog-logic/BloggingPlatform.ts @@ -9,6 +9,7 @@ import { IAccountIdentity, IPostCommentsFilters } from "./interfaces"; import { Post } from "./Post"; +import { paginateData } from "./utils"; import { ExtendedNodeApi, getWax } from "./wax"; export class BloggingPlaform implements IBloggingPlatform { @@ -35,10 +36,10 @@ export class BloggingPlaform implements IBloggingPlatform { public async getPost(postId: IPostCommentIdentity): Promise { if (!this.chain) await this.initializeChain(); - const postData = await this.chain?.api.bridge.get_post({author: postId.author.name, permlink: postId.permlink, observer: this.viewerContext.name }); + const postData = await this.chain?.api.bridge.get_post({author: postId.author, permlink: postId.permlink, observer: this.viewerContext.name }); if (!postData) throw new Error("Post not found"); - return new Post(postId, postData!); + return new Post(postId, this, postData!); } public async enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise> { @@ -58,8 +59,7 @@ export class BloggingPlaform implements IBloggingPlatform { }); if (!posts) throw new Error("Posts not found"); - return posts?.map((post) => new Post({author: {name: post.author}, permlink: post.permlink}, post)) + const paginatedPosts = paginateData(posts, pagination); + return paginatedPosts?.map((post) => new Post({author: post.author, permlink: post.permlink}, this, post)) } - - -} \ No newline at end of file +} diff --git a/packages/blog-logic/Comment.ts b/packages/blog-logic/Comment.ts index b2131b3..357a474 100644 --- a/packages/blog-logic/Comment.ts +++ b/packages/blog-logic/Comment.ts @@ -1,13 +1,14 @@ import { TWaxExtended } from "@hiveio/wax"; -import { IAccount, IAccountIdentity, IComment, ICommunityIdentity, IPagination, IPost, IPostCommentIdentity, IPostCommentsFilters, IReply, IVote } from "./interfaces"; +import { IAccountIdentity, IBloggingPlatform, IComment, ICommonFilters, IPagination, IPostCommentIdentity, IVote } from "./interfaces"; +import { paginateData } from "./utils"; +import { Vote } from "./Vote"; import { Entry, ExtendedNodeApi, getWax } from "./wax"; -import { Reply } from "./Reply"; export class Comment implements IComment { protected chain: TWaxExtended - public author: IAccountIdentity; + public author: string; public permlink: string; public publishedAt: Date; public updatedAt: Date; @@ -15,17 +16,19 @@ export class Comment implements IComment { protected content?: string; protected votes?: Iterable; + protected BloggingPlatform: IBloggingPlatform; + private initializeChain = async () => { if (!this.chain) this.chain = await getWax(); - } - public constructor(authorPermlink: IPostCommentIdentity, postCommentData?: Entry) { + public constructor(authorPermlink: IPostCommentIdentity, bloggingPlatform: IBloggingPlatform, postCommentData?: Entry, ) { this.initializeChain(); this.author = authorPermlink.author; this.permlink = authorPermlink.permlink; + this.BloggingPlatform = bloggingPlatform if(postCommentData) { this.publishedAt = new Date(postCommentData.created); @@ -35,26 +38,30 @@ export class Comment implements IComment { } public generateSlug(): string { - return `${this.author.name}_${this.permlink}`; + return `${this.author}_${this.permlink}`; } public async enumMentionedAccounts(): Promise> { - return []; + return await []; } public async getContent(): Promise { if (this.content) return this.content; - await this.chain.api.bridge.get_post({author: this.author.name, permlink: this.permlink, observer: "hive.blog"}); + await this.chain.api.bridge.get_post({author: this.author, permlink: this.permlink, observer: "hive.blog"}); return this.content || ""; } - public async enumVotes(filter: IPostCommentsFilters, pagination: IPagination): Promise> { - return []; + public async enumVotes(filter: ICommonFilters, pagination: IPagination): Promise> { + const votesData = await this.chain.api.condenser_api.get_active_votes([this.author, this.permlink]); + const votes = votesData.map((vote) => new Vote(vote)); + this.votes = votes; + return paginateData(votes, pagination); } - public async wasVotedByUser(userName: IAccountIdentity): Promise { - return false; + public async wasVotedByUser(userName: string): Promise { + if (!this.votes) await this.enumVotes({}, {page: 1, pageSize: 100}); // Temporary pagination before fix + return !!Array.from(this.votes || []).find((vote) => vote.voter === userName) } -} \ No newline at end of file +} diff --git a/packages/blog-logic/Post.ts b/packages/blog-logic/Post.ts index b224e4d..25a40ff 100644 --- a/packages/blog-logic/Post.ts +++ b/packages/blog-logic/Post.ts @@ -1,48 +1,51 @@ import { Comment } from "./Comment"; -import { ICommunityIdentity, IPagination, IPost, IPostCommentIdentity, IPostCommentsFilters, IReply } from "./interfaces"; +import { IBloggingPlatform, ICommunityIdentity, IPagination, IPost, IPostCommentIdentity, IPostCommentsFilters, IReply } from "./interfaces"; import { Reply } from "./Reply"; +import { paginateData } from "./utils"; import { Entry } from "./wax"; export class Post extends Comment implements IPost { public title: string; public tags: string[]; - public community?: ICommunityIdentity | undefined; + public community?: ICommunityIdentity; public summary: string; + public communityTitle?: string; private replies?: IReply[]; - public constructor(authorPermlink: IPostCommentIdentity, postData?: Entry) { - super(authorPermlink, postData); - if (postData) { - this.title = postData.title; - this.tags = postData.json_metadata?.tags || []; - this.summary = postData.json_metadata?.description || ""; - this.community = postData.community ? {name: postData.community} : undefined; - } + public constructor(authorPermlink: IPostCommentIdentity, bloggingPlatform: IBloggingPlatform, postData: Entry) { + super(authorPermlink, bloggingPlatform, postData); + this.title = postData.title; + this.tags = postData.json_metadata?.tags || []; + this.summary = postData.json_metadata?.description || ""; + this.community = postData.community ? {name: postData.community} : undefined; + this.communityTitle = postData.community_title } private async fetchReplies(): Promise { if (!this.replies) { const repliesData = await this.chain.api.bridge.get_discussion({ - author: this.author.name, + author: this.author, permlink: this.permlink, - observer: "hive.blog", + observer: this.BloggingPlatform.viewerContext.name, }); // Temporary hive.blog; if (!repliesData) throw "No replies"; - const replies = Object.entries(repliesData)?.map( - ([authorPermlink, reply]) => + const filteredReplies = Object.values(repliesData).filter((rawReply) => !!rawReply.parent_author) + const replies = filteredReplies?.map( + (reply) => new Reply( - { author: { name: reply.author }, permlink: reply.permlink }, + { author: reply.author, permlink: reply.permlink }, + this.BloggingPlatform, { - author: { name: reply.parent_author || "" }, + author: reply.parent_author || "", permlink: reply.parent_permlink || "", }, { author: this.author, permlink: this.permlink }, reply ) - ); + ) this.replies = replies; return replies; } @@ -52,17 +55,18 @@ export class Post extends Comment implements IPost { public async getContent(): Promise { if (this.content) return this.content; - await this.chain.api.bridge.get_post({author: this.author.name, permlink: this.permlink, observer: "hive.blog"}); + await this.chain.api.bridge.get_post({author: this.author, permlink: this.permlink, observer: this.BloggingPlatform.viewerContext.name}); return this.content || ""; } public getTitleImage(): string { + // The logic is complicated here, it wil be added later. return ""; } public async enumReplies(filter: IPostCommentsFilters, pagination: IPagination): Promise> { - if (this.replies) return this.replies; - return await this.fetchReplies(); + if (this.replies) return paginateData(this.replies, pagination); + return paginateData(await this.fetchReplies(), pagination); } public async getCommentsCount(): Promise { @@ -71,4 +75,4 @@ export class Post extends Comment implements IPost { return (await this.fetchReplies()).length; } -} \ No newline at end of file +} diff --git a/packages/blog-logic/Reply.ts b/packages/blog-logic/Reply.ts index 7507532..1bf4c89 100644 --- a/packages/blog-logic/Reply.ts +++ b/packages/blog-logic/Reply.ts @@ -1,19 +1,20 @@ import { Comment } from "./Comment"; -import { IPostCommentIdentity, IReply } from "./interfaces"; +import { IBloggingPlatform, IPostCommentIdentity, IReply } from "./interfaces"; import { Entry } from "./wax"; - export class Reply extends Comment implements IReply { - public parent: IPostCommentIdentity; public topPost: IPostCommentIdentity; - - public constructor(authorPermlink: IPostCommentIdentity, parent: IPostCommentIdentity, topPost: IPostCommentIdentity, replyData: Entry) { - super(authorPermlink, replyData); + public constructor( + authorPermlink: IPostCommentIdentity, + bloggingPlatform: IBloggingPlatform, + parent: IPostCommentIdentity, + topPost: IPostCommentIdentity, + replyData: Entry + ) { + super(authorPermlink, bloggingPlatform, replyData); this.parent = parent; this.topPost = topPost; } - - } diff --git a/packages/blog-logic/Vote.ts b/packages/blog-logic/Vote.ts new file mode 100644 index 0000000..d07ce0e --- /dev/null +++ b/packages/blog-logic/Vote.ts @@ -0,0 +1,14 @@ +import { IVote } from "./interfaces"; +import { VoteData } from "./wax"; + +export class Vote implements IVote { + public upvote: boolean; + public voter: string; + public weight: number; + + public constructor(voteData: VoteData) { + this.upvote = voteData.weight > 0; + this.voter = voteData.voter; + this.weight = voteData.weight; + } +} diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts index 5c3119e..d551b3d 100644 --- a/packages/blog-logic/interfaces.ts +++ b/packages/blog-logic/interfaces.ts @@ -44,7 +44,7 @@ export interface ICommunityIdentity { * Represents a set of data uniquely identifying a post or reply object. */ export interface IPostCommentIdentity { - readonly author: IAccountIdentity; + readonly author: string; readonly permlink: string; } @@ -87,9 +87,9 @@ export interface IComment extends IPostCommentIdentity { enumMentionedAccounts(): Promise>; - enumVotes(filter: IPostCommentsFilters, pagination: IPagination): Promise>; + enumVotes(filter: ICommonFilters, pagination: IPagination): Promise>; getContent(): Promise; - wasVotedByUser(userName: IAccountIdentity): Promise; + wasVotedByUser(userName: string): Promise; @@ -119,6 +119,7 @@ export interface IPost extends IComment { summary: string; tags: string[]; community?: ICommunityIdentity; + communityTitle?: string; getCommentsCount(): Promise; enumReplies(filter: ICommonFilters, pagination: IPagination): Promise>; @@ -152,11 +153,12 @@ export interface IActiveBloggingPlatform { } export interface IBloggingPlatform { - viewerContext?: IAccountIdentity; + viewerContext: IAccountIdentity; getPost(postId: IPostCommentIdentity): Promise; enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Promise>; configureViewContext(accontName: IAccountIdentity): void; - enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise> + enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise>; + // To do: add getAccount method later // authorize(provider: IAuthenticationProvider): Promise; } diff --git a/packages/blog-logic/utils.ts b/packages/blog-logic/utils.ts new file mode 100644 index 0000000..a472229 --- /dev/null +++ b/packages/blog-logic/utils.ts @@ -0,0 +1,7 @@ +import { IPagination } from "./interfaces"; + +export const paginateData = (data: any[], pagination: IPagination): any[] => { + const {page, pageSize} = pagination + const startIndex = (page - 1) * pageSize; + return data.slice(startIndex, startIndex + pageSize); +} \ No newline at end of file diff --git a/packages/blog-logic/wax.ts b/packages/blog-logic/wax.ts index 151b9e3..dbe0190 100644 --- a/packages/blog-logic/wax.ts +++ b/packages/blog-logic/wax.ts @@ -96,6 +96,18 @@ export interface Entry { original_entry?: Entry; } +export interface VoteData { + percent: number; + reputation: number; + rshares: number; + time: string; + timestamp?: number; + voter: string; + weight: number; + reward?: number; +} + + export type ExtendedNodeApi = { bridge: { get_post_header: TWaxApiRequest<{ author: string; permlink: string }, IGetPostHeader>; @@ -127,6 +139,9 @@ export type ExtendedNodeApi = { Entry[] | null >; }; + condenser_api: { + get_active_votes: TWaxApiRequest; + } }; let chain: Promise>; -- GitLab From a1d8c778bb3ec352dd1d6d886b84eef2d19ba781 Mon Sep 17 00:00:00 2001 From: jlachor Date: Wed, 24 Sep 2025 15:29:20 +0200 Subject: [PATCH 05/15] Expand and upgrade Blog Logic, add Community and Account --- examples/wordpress-rest-api/hiveToWpMap.ts | 5 +- .../wordpress-rest-api/wordpress-rest-api.ts | 15 +-- packages/blog-logic/Account.ts | 29 +++++ packages/blog-logic/BloggingPlatform.ts | 63 +++++++++- packages/blog-logic/Comment.ts | 62 ++++++---- packages/blog-logic/Community.ts | 34 ++++++ packages/blog-logic/Post.ts | 53 +++++--- packages/blog-logic/interfaces.ts | 17 ++- packages/blog-logic/wax.ts | 114 +++++++++++++++++- 9 files changed, 325 insertions(+), 67 deletions(-) create mode 100644 packages/blog-logic/Account.ts create mode 100644 packages/blog-logic/Community.ts diff --git a/examples/wordpress-rest-api/hiveToWpMap.ts b/examples/wordpress-rest-api/hiveToWpMap.ts index de9d554..40ea2df 100644 --- a/examples/wordpress-rest-api/hiveToWpMap.ts +++ b/examples/wordpress-rest-api/hiveToWpMap.ts @@ -36,13 +36,14 @@ const mapWpTerm = (termName: string, type: "tag" | "category"): WPTerm => { export const mapIPostToWpPost = async (hivePost: IPost, wpId: number, accountId: number): Promise => { - const slug = hivePost.generateSlug(); + const slug = hivePost.getSlug(); const tags = hivePost?.tags || []; const wpTermTags = tags.map((tag) => mapWpTerm(tag, "tag")); const community = hivePost.communityTitle; const wpTermCategory = community ? [mapWpTerm(community, "category")] : []; const renderedBody = renderer.render(await hivePost.getContent()); - const wpExcerpt = renderedBody.replace(/<[^>]+>/g, '').substring(0, 100); + const titleImage = hivePost.getTitleImage(); + const wpExcerpt = renderer.render(`${titleImage} \n \n ${renderedBody.replace(/<[^>]+>/g, '').substring(0, 100)}...`); const wpPost: WPPost = { id:wpId, slug, diff --git a/examples/wordpress-rest-api/wordpress-rest-api.ts b/examples/wordpress-rest-api/wordpress-rest-api.ts index f22a276..ba13867 100644 --- a/examples/wordpress-rest-api/wordpress-rest-api.ts +++ b/examples/wordpress-rest-api/wordpress-rest-api.ts @@ -38,26 +38,26 @@ const getAuthorPermlinkFromSlug = (slug: string): {author: string, permlink: str const mapAndAddtoMapPosts = async (posts: IPost[]): Promise => { const mappedPosts: WPPost[] = []; - posts.forEach(async (post) => { + await Promise.all(posts.map(async (post) => { const postId = simpleHash(`${post.author}_${post.permlink}`); const authorId = simpleHash(post.author); idToStringMap.set(postId, `${post.author}_${post.permlink}`).set(authorId, post.author); posts.push(post); mappedPosts.push(await mapIPostToWpPost(post, postId, authorId)); - }); - return await mappedPosts; + })); + return mappedPosts; } const mapReplies = async (replies: IReply[], postId: number) : Promise => { - const wpComments: WPComment[] = [] - replies.forEach( async (reply) => { + const wpComments: WPComment[] = []; + await Promise.all( replies.map( async (reply) => { if (reply.author && reply.permlink) { - const wpAuthorPermlink = reply.generateSlug(); + const wpAuthorPermlink = reply.getSlug(); const wpComment = await mapIReplyToWPComment(reply, simpleHash(wpAuthorPermlink), postId, simpleHash(reply.author)); wpComments.push(wpComment) } - }); + })); return await wpComments; } @@ -103,6 +103,7 @@ apiRouter.get("/comments", async (req: Request, res: Response) => { const postParent = idToStringMap.get(postId); if (postParent) { const {author, permlink} = getAuthorPermlinkFromSlug(postParent); + // Delete array of posts, get replies here. const post = posts.find((post) => post.author === author && post.permlink === permlink); if (post) { const replies = await post.enumReplies({}, {page: 1, pageSize: 1000}) as IReply[]; diff --git a/packages/blog-logic/Account.ts b/packages/blog-logic/Account.ts new file mode 100644 index 0000000..f7f6c4b --- /dev/null +++ b/packages/blog-logic/Account.ts @@ -0,0 +1,29 @@ +import { IAccount } from "./interfaces"; +import { FullAccount } from "./wax"; + +export class Account implements IAccount { + public readonly name: string; + public readonly creationDate: Date; + public readonly postCount: number; + public readonly lastActivity: Date; + public readonly registeredDate: Date; + public readonly description: string; + public readonly avatar: string; + + public constructor(accountData: FullAccount) { + this.name = accountData.name; + this.avatar = JSON.parse(accountData.posting_json_metadata)?.profile.profile_image || ""; + this.creationDate = new Date(accountData.created); + this.postCount = accountData.post_count; + this.lastActivity = new Date(accountData.last_post); + this.registeredDate = new Date(accountData.created); + this.description = accountData.profile?.about || ""; + } + + /** + * Get standard WordPress slug for account. + */ + public getSlug(): string { + return this.name; + } +} diff --git a/packages/blog-logic/BloggingPlatform.ts b/packages/blog-logic/BloggingPlatform.ts index eda2b10..7b60028 100644 --- a/packages/blog-logic/BloggingPlatform.ts +++ b/packages/blog-logic/BloggingPlatform.ts @@ -1,5 +1,7 @@ import { TWaxExtended } from "@hiveio/wax"; -import { IAccountIdentity, +import { Account } from "./Account"; +import { Community } from "./Community"; +import { IAccount, IAccountIdentity, IBloggingPlatform, ICommunity, ICommunityFilters, @@ -15,40 +17,66 @@ import { ExtendedNodeApi, getWax } from "./wax"; export class BloggingPlaform implements IBloggingPlatform { public viewerContext: IAccountIdentity; - private chain?: TWaxExtended + private chain?: TWaxExtended; + private initializeChain = async () => { if (!this.chain) this.chain = await getWax(); } + // Add initilaize chain to constructor + + public overwrittenGetTitleImage?: () => string; public constructor() { this.viewerContext = {name: "hive.blog"}; // Set default this.initializeChain(); } + /** + * Change the observer for blog. + * @param accontName account name or community. + */ public configureViewContext(accontName: IAccountIdentity): void { this.viewerContext = accontName; } + /** + * Get single post idetified by author/permlink. + * @param postId + * @returns post object + */ public async getPost(postId: IPostCommentIdentity): Promise { - if (!this.chain) - await this.initializeChain(); + await this.initializeChain(); const postData = await this.chain?.api.bridge.get_post({author: postId.author, permlink: postId.permlink, observer: this.viewerContext.name }); if (!postData) throw new Error("Post not found"); return new Post(postId, this, postData!); } + /** + * Enumarate all communities for given filters + * @param filter + * @param pagination + * @returns iterable of community objects + */ public async enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise> { + await this.initializeChain(); + const communities = await this.chain?.api.bridge.list_communities({observer: this.viewerContext.name, sort: filter.sort, query: filter.query}); + if (communities) return paginateData(communities.map((community) => new Community(community)), pagination); return await []; } + /** + * Get posts for selected filters. + * @param filter + * @param pagination + * @returns iterable of posts + */ public async enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Promise> { - if (!this.chain) - await this.initializeChain(); + await this.initializeChain(); const posts = await this.chain?.api.bridge.get_ranked_posts({ limit: filter.limit, sort: filter.sort, @@ -62,4 +90,27 @@ export class BloggingPlaform implements IBloggingPlatform { const paginatedPosts = paginateData(posts, pagination); return paginatedPosts?.map((post) => new Post({author: post.author, permlink: post.permlink}, this, post)) } + + /** + * Get account object for given account name. + * @param accontName + * @returns account object + */ + public async getAccount(accontName: string): Promise { + await this.initializeChain(); + const account = await this.chain?.api.condenser_api.get_accounts([[accontName]]); + if (!account) + throw new Error("Account not found"); + return new Account(account[0]); + } + + // Section for overwritting methods + + /** + * It allows other project to overwrite title image method with their custom implementation. + * @param callbackMethod custom method for getting title image. + */ + public overwriteGetTitleImage(callbackMethod: () => string) { + this.overwriteGetTitleImage = callbackMethod; + } } diff --git a/packages/blog-logic/Comment.ts b/packages/blog-logic/Comment.ts index 357a474..ff778d1 100644 --- a/packages/blog-logic/Comment.ts +++ b/packages/blog-logic/Comment.ts @@ -1,12 +1,12 @@ import { TWaxExtended } from "@hiveio/wax"; -import { IAccountIdentity, IBloggingPlatform, IComment, ICommonFilters, IPagination, IPostCommentIdentity, IVote } from "./interfaces"; +import { IBloggingPlatform, IComment, ICommonFilters, IPagination, IPostCommentIdentity, IVote } from "./interfaces"; import { paginateData } from "./utils"; import { Vote } from "./Vote"; import { Entry, ExtendedNodeApi, getWax } from "./wax"; export class Comment implements IComment { - protected chain: TWaxExtended + protected chain?: TWaxExtended public author: string; public permlink: string; @@ -16,51 +16,71 @@ export class Comment implements IComment { protected content?: string; protected votes?: Iterable; - protected BloggingPlatform: IBloggingPlatform; + protected bloggingPlatform: IBloggingPlatform; - private initializeChain = async () => { + protected initializeChain = async () => { if (!this.chain) this.chain = await getWax(); } - public constructor(authorPermlink: IPostCommentIdentity, bloggingPlatform: IBloggingPlatform, postCommentData?: Entry, ) { + // Refactor blogginPlatform and data into dataProvider with promises. + public constructor(authorPermlink: IPostCommentIdentity, bloggingPlatform: IBloggingPlatform, postCommentData: Entry, ) { this.initializeChain(); this.author = authorPermlink.author; this.permlink = authorPermlink.permlink; - this.BloggingPlatform = bloggingPlatform - - if(postCommentData) { - this.publishedAt = new Date(postCommentData.created); - this.updatedAt = new Date(postCommentData.updated); - this.content = postCommentData.body; - } + this.bloggingPlatform = bloggingPlatform; + this.publishedAt = new Date(postCommentData.created); + this.updatedAt = new Date(postCommentData.updated); + this.content = postCommentData.body; } - public generateSlug(): string { + /** + * Create standard slug that can be used in Wordpress. It's generated by adding "_" between author and permlink. + */ + public getSlug(): string { return `${this.author}_${this.permlink}`; } - public async enumMentionedAccounts(): Promise> { - return await []; + /** + * Get list of all mentioned accounts as strings. + */ + /* eslint-disable-next-line require-await */ + public async enumMentionedAccounts(): Promise> { + const regex = /@[a-z0-9.-]+\b/g; // Alphanumeric with . and -, but not on the end. + return this.content?.match(regex) ?? []; } + /** + * Get full body of comment or post. + */ + /* eslint-disable-next-line require-await */ public async getContent(): Promise { - if (this.content) - return this.content; - await this.chain.api.bridge.get_post({author: this.author, permlink: this.permlink, observer: "hive.blog"}); - return this.content || ""; + return this.content || "" } + /** + * Return all votes for given comment. + * @param filter Standard filters for date and order. + * @param pagination + * @returns Iterable of Votes. + */ public async enumVotes(filter: ICommonFilters, pagination: IPagination): Promise> { - const votesData = await this.chain.api.condenser_api.get_active_votes([this.author, this.permlink]); + this.initializeChain(); + // Get rid of condenser API + const votesData = await this.chain!.api.condenser_api.get_active_votes([this.author, this.permlink]); const votes = votesData.map((vote) => new Vote(vote)); this.votes = votes; return paginateData(votes, pagination); } + /** + * Check if this post was voted by selecred user. + * @param userName + */ public async wasVotedByUser(userName: string): Promise { - if (!this.votes) await this.enumVotes({}, {page: 1, pageSize: 100}); // Temporary pagination before fix + this.initializeChain(); + if (!this.votes) await this.enumVotes({}, {page: 1, pageSize: 10000}); // Temporary pagination before fix return !!Array.from(this.votes || []).find((vote) => vote.voter === userName) } diff --git a/packages/blog-logic/Community.ts b/packages/blog-logic/Community.ts new file mode 100644 index 0000000..021991b --- /dev/null +++ b/packages/blog-logic/Community.ts @@ -0,0 +1,34 @@ +import { ICommunity } from "./interfaces"; +import { CommunityData } from "./wax"; + +export class Community implements ICommunity { + public readonly name: string; + public readonly title: string; + public readonly about: string; + public readonly admins: string[]; + public readonly avatarUrl: string; + public readonly creationDate: Date; + public readonly subscribersCount: number; + public readonly authorsCount: number; + public readonly pendingCount: number; + + public constructor(communityData: CommunityData) { + this.name = communityData.name; + this.title = communityData.title; + this.about = communityData.about; + this.admins = communityData.admins || []; + this.avatarUrl = communityData.avatar_url; + this.creationDate = new Date(communityData.created_at); + this.subscribersCount = communityData.subscribers; + this.authorsCount = communityData.num_authors; + this.pendingCount = communityData.num_pending; + + } + + /** + * Get standard WordPress slug. It treats community as category. + */ + public getSlug(): string { + return this.title + } +} diff --git a/packages/blog-logic/Post.ts b/packages/blog-logic/Post.ts index 25a40ff..5607d39 100644 --- a/packages/blog-logic/Post.ts +++ b/packages/blog-logic/Post.ts @@ -12,7 +12,8 @@ export class Post extends Comment implements IPost { public summary: string; public communityTitle?: string; - private replies?: IReply[]; + private replies?: Iterable; + private postImage?: string; public constructor(authorPermlink: IPostCommentIdentity, bloggingPlatform: IBloggingPlatform, postData: Entry) { super(authorPermlink, bloggingPlatform, postData); @@ -21,15 +22,21 @@ export class Post extends Comment implements IPost { this.summary = postData.json_metadata?.description || ""; this.community = postData.community ? {name: postData.community} : undefined; this.communityTitle = postData.community_title + this.postImage = postData.json_metadata.image[0]; } - private async fetchReplies(): Promise { + /** + * Fetch and return all replies for post. Do pagination later. + * @returns iterable of replies + */ + private async fetchReplies(): Promise> { + this.initializeChain(); if (!this.replies) { - const repliesData = await this.chain.api.bridge.get_discussion({ + const repliesData = await this.chain!.api.bridge.get_discussion({ author: this.author, permlink: this.permlink, - observer: this.BloggingPlatform.viewerContext.name, - }); // Temporary hive.blog; + observer: this.bloggingPlatform.viewerContext.name, + }); if (!repliesData) throw "No replies"; const filteredReplies = Object.values(repliesData).filter((rawReply) => !!rawReply.parent_author) @@ -37,7 +44,7 @@ export class Post extends Comment implements IPost { (reply) => new Reply( { author: reply.author, permlink: reply.permlink }, - this.BloggingPlatform, + this.bloggingPlatform, { author: reply.parent_author || "", permlink: reply.parent_permlink || "", @@ -52,27 +59,35 @@ export class Post extends Comment implements IPost { return this.replies; } - public async getContent(): Promise { - if (this.content) - return this.content; - await this.chain.api.bridge.get_post({author: this.author, permlink: this.permlink, observer: this.BloggingPlatform.viewerContext.name}); - return this.content || ""; - } - + /** + * Get title image from post content. + * @returns Link to title image + */ public getTitleImage(): string { - // The logic is complicated here, it wil be added later. - return ""; + if (this.bloggingPlatform.overwrittenGetTitleImage) return this.bloggingPlatform.overwrittenGetTitleImage() + return this.postImage || "" } + /** + * Enum replies for given post. + * @param filter + * @param pagination + * @returns iterable of replies objects + */ public async enumReplies(filter: IPostCommentsFilters, pagination: IPagination): Promise> { - if (this.replies) return paginateData(this.replies, pagination); - return paginateData(await this.fetchReplies(), pagination); + this.initializeChain(); + if (this.replies) return paginateData(Array.from(this.replies), pagination); + return paginateData(await this.fetchReplies() as IReply[], pagination); } + /** + * Get number of comments (replies) for given post. + */ public async getCommentsCount(): Promise { - if (this.replies) return this.replies.length; + this.initializeChain(); + if (this.replies) return Array.from(this.replies).length; - return (await this.fetchReplies()).length; + return (Array.from(await this.fetchReplies())).length; } } diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts index d551b3d..bb7b8b1 100644 --- a/packages/blog-logic/interfaces.ts +++ b/packages/blog-logic/interfaces.ts @@ -28,8 +28,8 @@ export interface IPostCommentsFilters extends ICommonFilters { } export interface ICommunityFilters extends ICommonFilters { - readonly byName?: string; - readonly tags?: string[]; + readonly sort: string; + readonly query: string } export interface IAccountIdentity { @@ -67,14 +67,11 @@ export interface ICommunity extends ICommunityIdentity { } export interface IAccount extends IAccountIdentity { readonly creationDate: Date; - readonly commentCount: number; readonly lastActivity: Date; readonly postCount: number; readonly registeredDate: Date; readonly description: string; readonly avatar: string; - readonly url: string; - readonly name: string; getSlug(): string; } @@ -86,17 +83,15 @@ export interface IComment extends IPostCommentIdentity { readonly updatedAt: Date; - enumMentionedAccounts(): Promise>; + enumMentionedAccounts(): Promise>; enumVotes(filter: ICommonFilters, pagination: IPagination): Promise>; getContent(): Promise; wasVotedByUser(userName: string): Promise; - - /** * Allows to generate a slug for the comment, which can be used in URLs or as a unique identifier. */ - generateSlug(): string; + getSlug(): string; }; /** @@ -150,6 +145,7 @@ export interface IActiveBloggingPlatform { deleteComment(postOrComment: IPostCommentIdentity): Promise; editComment(postOrComment: IPostCommentIdentity, body: string, tags: string[], title?: string, observer?: Partial>): Promise; followBlog(authorOrCommunity: IAccountIdentity | ICommunityIdentity): Promise; + getAccount(accountName: string): Promise; } export interface IBloggingPlatform { @@ -160,6 +156,9 @@ export interface IBloggingPlatform { enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise>; // To do: add getAccount method later + overwrittenGetTitleImage?: () => string; + overwriteGetTitleImage(callback: () => string): void; + // authorize(provider: IAuthenticationProvider): Promise; } diff --git a/packages/blog-logic/wax.ts b/packages/blog-logic/wax.ts index dbe0190..86112ac 100644 --- a/packages/blog-logic/wax.ts +++ b/packages/blog-logic/wax.ts @@ -1,7 +1,83 @@ -import { createHiveChain, TWaxExtended, - TWaxApiRequest +import { + createHiveChain, + TWaxExtended, + TWaxApiRequest, + ApiAuthority, + NaiAsset } from "@hiveio/wax"; +export interface AccountProfile { + about?: string; + cover_image?: string; + location?: string; + blacklist_description?: string; + muted_list_description?: string; + name?: string; + profile_image?: string; + website?: string; + pinned?: string; + witness_description?: string; + witness_owner?: string; +} +export interface AccountFollowStats { + follower_count: number; + following_count: number; + account: string; +} + +export interface FullAccount { + vesting_balance: string | NaiAsset; + name: string; + owner: ApiAuthority; + active: ApiAuthority; + posting: ApiAuthority; + memo_key: string; + post_count: number; + created: string; + reputation: string | number; + json_metadata: string; + posting_json_metadata: string; + last_vote_time: string; + last_post: string; + reward_hbd_balance: string; + reward_vesting_hive: string; + reward_hive_balance: string; + reward_vesting_balance: string; + governance_vote_expiration_ts: string; + balance: string; + vesting_shares: string; + hbd_balance: string; + savings_balance: string; + savings_hbd_balance: string; + savings_hbd_seconds: string; + savings_hbd_last_interest_payment: string; + savings_hbd_seconds_last_update: string; + next_vesting_withdrawal: string; + delegated_vesting_shares: string; + received_vesting_shares: string; + vesting_withdraw_rate: string; + to_withdraw: number; + withdrawn: number; + witness_votes: string[]; + proxy: string; + proxied_vsf_votes: number[] | string[]; + voting_manabar: { + current_mana: string | number; + last_update_time: number; + }; + voting_power: number; + downvote_manabar: { + current_mana: string | number; + last_update_time: number; + }; + profile?: AccountProfile; + follow_stats?: AccountFollowStats; + __loaded?: true; + proxyVotes?: Array; + // Temporary properties for UI purposes + _temporary?: boolean; +} + export interface IGetPostHeader { author: string; @@ -11,7 +87,7 @@ export interface IGetPostHeader { } export interface JsonMetadata { - image: string; + image: string[]; links?: string[]; flow?: { pictures: { @@ -107,6 +183,33 @@ export interface VoteData { reward?: number; } +export interface CommunityData { + about: string; + admins?: string[]; + avatar_url: string; + created_at: string; + description: string; + flag_text: string; + id: number; + is_nsfw: boolean; + lang: string; + name: string; + num_authors: number; + num_pending: number; + subscribers: number; + sum_pending: number; + settings?: object; + team: string[][]; + title: string; + type_id: number; + context: { + role: string; + subscribed: boolean; + title: string; + _temporary?: boolean; + }; + _temporary?: boolean; +} export type ExtendedNodeApi = { bridge: { @@ -138,9 +241,14 @@ export type ExtendedNodeApi = { }, Entry[] | null >; + list_communities: TWaxApiRequest< + { sort: string; query?: string | null; observer: string }, + CommunityData[] | null + >; }; condenser_api: { get_active_votes: TWaxApiRequest; + get_accounts: TWaxApiRequest<[string[]], FullAccount[]>; } }; -- GitLab From 3dd3b9ac9e2eb5227d4a0ab3bc15279b89626e1d Mon Sep 17 00:00:00 2001 From: jlachor Date: Tue, 30 Sep 2025 11:03:08 +0200 Subject: [PATCH 06/15] Get rid of condenser API --- packages/blog-logic/Account.ts | 10 +-- packages/blog-logic/BloggingPlatform.ts | 10 +-- packages/blog-logic/Comment.ts | 10 +-- packages/blog-logic/Vote.ts | 8 +-- packages/blog-logic/interfaces.ts | 8 +-- packages/blog-logic/wax.ts | 84 +++++++++++++++++++++++-- 6 files changed, 100 insertions(+), 30 deletions(-) diff --git a/packages/blog-logic/Account.ts b/packages/blog-logic/Account.ts index f7f6c4b..7b6d082 100644 --- a/packages/blog-logic/Account.ts +++ b/packages/blog-logic/Account.ts @@ -1,5 +1,5 @@ import { IAccount } from "./interfaces"; -import { FullAccount } from "./wax"; +import { AccountDetails } from "./wax"; export class Account implements IAccount { public readonly name: string; @@ -10,14 +10,14 @@ export class Account implements IAccount { public readonly description: string; public readonly avatar: string; - public constructor(accountData: FullAccount) { + public constructor(accountData: AccountDetails) { this.name = accountData.name; this.avatar = JSON.parse(accountData.posting_json_metadata)?.profile.profile_image || ""; this.creationDate = new Date(accountData.created); - this.postCount = accountData.post_count; - this.lastActivity = new Date(accountData.last_post); + this.postCount = 0; // In this API not available. + this.lastActivity = new Date(); // In this API not available. this.registeredDate = new Date(accountData.created); - this.description = accountData.profile?.about || ""; + this.description = JSON.parse(accountData.posting_json_metadata)?.about || ""; } /** diff --git a/packages/blog-logic/BloggingPlatform.ts b/packages/blog-logic/BloggingPlatform.ts index 7b60028..7de3c38 100644 --- a/packages/blog-logic/BloggingPlatform.ts +++ b/packages/blog-logic/BloggingPlatform.ts @@ -1,4 +1,4 @@ -import { TWaxExtended } from "@hiveio/wax"; +import { TWaxExtended, TWaxRestExtended } from "@hiveio/wax"; import { Account } from "./Account"; import { Community } from "./Community"; import { IAccount, IAccountIdentity, @@ -12,12 +12,12 @@ import { IAccount, IAccountIdentity, } from "./interfaces"; import { Post } from "./Post"; import { paginateData } from "./utils"; -import { ExtendedNodeApi, getWax } from "./wax"; +import { ExtendedNodeApi, ExtendedRestApi, getWax } from "./wax"; export class BloggingPlaform implements IBloggingPlatform { public viewerContext: IAccountIdentity; - private chain?: TWaxExtended; + private chain?: TWaxExtended>; private initializeChain = async () => { @@ -98,10 +98,10 @@ export class BloggingPlaform implements IBloggingPlatform { */ public async getAccount(accontName: string): Promise { await this.initializeChain(); - const account = await this.chain?.api.condenser_api.get_accounts([[accontName]]); + const account = await this.chain?.restApi["hafbe-api"].accounts.account({accountName: accontName}); if (!account) throw new Error("Account not found"); - return new Account(account[0]); + return new Account(account); } // Section for overwritting methods diff --git a/packages/blog-logic/Comment.ts b/packages/blog-logic/Comment.ts index ff778d1..9ea4bee 100644 --- a/packages/blog-logic/Comment.ts +++ b/packages/blog-logic/Comment.ts @@ -1,5 +1,5 @@ import { TWaxExtended } from "@hiveio/wax"; -import { IBloggingPlatform, IComment, ICommonFilters, IPagination, IPostCommentIdentity, IVote } from "./interfaces"; +import { IBloggingPlatform, IComment, IPagination, IPostCommentIdentity, IVote, IVotesFilters } from "./interfaces"; import { paginateData } from "./utils"; import { Vote } from "./Vote"; import { Entry, ExtendedNodeApi, getWax } from "./wax"; @@ -65,11 +65,11 @@ export class Comment implements IComment { * @param pagination * @returns Iterable of Votes. */ - public async enumVotes(filter: ICommonFilters, pagination: IPagination): Promise> { + public async enumVotes(filter: IVotesFilters, pagination: IPagination): Promise> { this.initializeChain(); // Get rid of condenser API - const votesData = await this.chain!.api.condenser_api.get_active_votes([this.author, this.permlink]); - const votes = votesData.map((vote) => new Vote(vote)); + const votesData = await this.chain!.api.database_api.list_votes({limit: filter.limit, order: filter.votesSort, start: null}); + const votes = votesData.votes.map((vote) => new Vote(vote)); this.votes = votes; return paginateData(votes, pagination); } @@ -80,7 +80,7 @@ export class Comment implements IComment { */ public async wasVotedByUser(userName: string): Promise { this.initializeChain(); - if (!this.votes) await this.enumVotes({}, {page: 1, pageSize: 10000}); // Temporary pagination before fix + if (!this.votes) await this.enumVotes({limit: 10000, votesSort: "by_comment_voter"}, {page: 1, pageSize: 10000}); // Temporary pagination before fix return !!Array.from(this.votes || []).find((vote) => vote.voter === userName) } diff --git a/packages/blog-logic/Vote.ts b/packages/blog-logic/Vote.ts index d07ce0e..bfaf3ff 100644 --- a/packages/blog-logic/Vote.ts +++ b/packages/blog-logic/Vote.ts @@ -1,14 +1,14 @@ import { IVote } from "./interfaces"; -import { VoteData } from "./wax"; +import { IVoteListItem } from "./wax"; export class Vote implements IVote { public upvote: boolean; public voter: string; public weight: number; - public constructor(voteData: VoteData) { - this.upvote = voteData.weight > 0; + public constructor(voteData: IVoteListItem) { + this.upvote = Number(voteData.weight) > 0 this.voter = voteData.voter; - this.weight = voteData.weight; + this.weight = Number(voteData.weight); } } diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts index bb7b8b1..8596606 100644 --- a/packages/blog-logic/interfaces.ts +++ b/packages/blog-logic/interfaces.ts @@ -14,11 +14,9 @@ export interface ICommonFilters { } export interface IVotesFilters extends ICommonFilters { - readonly isUpvote?: boolean; - readonly voterName?: string; - readonly sortBy?: "date" | "weight" | "voter"; + readonly limit: number; + readonly votesSort: "by_comment_voter" | "by_voter_comment"; } - export interface IPostCommentsFilters extends ICommonFilters { readonly limit: number; readonly sort: "trending" | "hot" | "created" | "promoted" | "payout" | "payout_comments" | "muted"; @@ -84,7 +82,7 @@ export interface IComment extends IPostCommentIdentity { enumMentionedAccounts(): Promise>; - enumVotes(filter: ICommonFilters, pagination: IPagination): Promise>; + enumVotes(filter: IVotesFilters, pagination: IPagination): Promise>; getContent(): Promise; wasVotedByUser(userName: string): Promise; diff --git a/packages/blog-logic/wax.ts b/packages/blog-logic/wax.ts index 86112ac..9b48bca 100644 --- a/packages/blog-logic/wax.ts +++ b/packages/blog-logic/wax.ts @@ -3,7 +3,8 @@ import { TWaxExtended, TWaxApiRequest, ApiAuthority, - NaiAsset + NaiAsset, + TWaxRestExtended } from "@hiveio/wax"; export interface AccountProfile { @@ -211,6 +212,59 @@ export interface CommunityData { _temporary?: boolean; } +export interface IVoteListItem { + id: number; + voter: string; + author: string; + permlink: string; + weight: string; + rshares: number; + vote_percent: number; + last_update: string; + num_changes: number; +} + +export interface AccountDetails { + id: number; + name: string; + can_vote: boolean; + mined: boolean; + proxy: string; + recovery_account: string; + last_account_recovery: Date; + created: Date; + reputation: number; + json_metadata: string; + posting_json_metadata: string; + profile_image: string; + hbd_balance: number; + balance: number; + vesting_shares: string; + vesting_balance: number; + hbd_saving_balance: number; + savings_balance: number; + savings_withdraw_requests: number; + reward_hbd_balance: number; + reward_hive_balance: number; + reward_vesting_balance: string; + reward_vesting_hive: number; + posting_rewards: string; + curation_rewards: string; + delegated_vesting_shares: string; + received_vesting_shares: string; + proxied_vsf_votes: number[] | string[]; + withdrawn: string; + vesting_withdraw_rate: string; + to_withdraw: string; + withdraw_routes: number; + delayed_vests: string; + witness_votes: string[]; + witnesses_voted_for: number; + ops_count: number; + is_witness: boolean; + governanceTs: any; +} + export type ExtendedNodeApi = { bridge: { get_post_header: TWaxApiRequest<{ author: string; permlink: string }, IGetPostHeader>; @@ -246,17 +300,35 @@ export type ExtendedNodeApi = { CommunityData[] | null >; }; - condenser_api: { - get_active_votes: TWaxApiRequest; - get_accounts: TWaxApiRequest<[string[]], FullAccount[]>; + database_api: { + list_votes: TWaxApiRequest< + { + start: [string, string, string] | null; + limit: number; + order: "by_comment_voter" | "by_voter_comment"; + }, + { votes: IVoteListItem[] } + >; + } +}; + +export type ExtendedRestApi = { + "hafbe-api": { + accounts: { + account: { + params: { accountName: string }; + result: AccountDetails, + urlPath: "{accountName}", + }, + } } }; -let chain: Promise>; +let chain: Promise>>; export const getWax = () => { if (!chain) - return chain = createHiveChain().then(chain => chain.extend()); + return chain = createHiveChain().then(chain => chain.extend().extendRest({})); return chain; }; -- GitLab From 9db62a568491ceca8aede7e6bd9bc81bae55173d Mon Sep 17 00:00:00 2001 From: jlachor Date: Tue, 30 Sep 2025 14:33:05 +0200 Subject: [PATCH 07/15] Update readme --- examples/wordpress-rest-api/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/wordpress-rest-api/readme.md b/examples/wordpress-rest-api/readme.md index 0c9d82d..0d25ed5 100644 --- a/examples/wordpress-rest-api/readme.md +++ b/examples/wordpress-rest-api/readme.md @@ -6,7 +6,7 @@ This project maps data from Hive and returns it as proper WordPress API. It will * Get project and get submodules. * Go into `example/wordpress-rest-api`. -* `pnpm install`. +* `pnpm install --ignore-workspace`. * Change `example-config.ts` file to get the data you want to see on main page. * `pnpm run dev` for deployment. Rest API should be ready to work with your front end. -- GitLab From beecf053dedbd41ea769159f05135eca22e4df01 Mon Sep 17 00:00:00 2001 From: jlachor Date: Tue, 30 Sep 2025 14:38:53 +0200 Subject: [PATCH 08/15] Fix title images error --- packages/blog-logic/Post.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/blog-logic/Post.ts b/packages/blog-logic/Post.ts index 5607d39..17baade 100644 --- a/packages/blog-logic/Post.ts +++ b/packages/blog-logic/Post.ts @@ -22,7 +22,7 @@ export class Post extends Comment implements IPost { this.summary = postData.json_metadata?.description || ""; this.community = postData.community ? {name: postData.community} : undefined; this.communityTitle = postData.community_title - this.postImage = postData.json_metadata.image[0]; + this.postImage = postData.json_metadata.image?.[0]; } /** -- GitLab From 56fa361188f80fcebdad212d277bb13b2cc3a71c Mon Sep 17 00:00:00 2001 From: jlachor Date: Tue, 7 Oct 2025 11:01:14 +0200 Subject: [PATCH 09/15] Fixes after review --- examples/wordpress-rest-api/example-config.ts | 2 + examples/wordpress-rest-api/hive.ts | 1 + examples/wordpress-rest-api/hiveToWpMap.ts | 9 +- .../wordpress-rest-api/mocks/categories.ts | 13 -- examples/wordpress-rest-api/mocks/comments.ts | 74 --------- examples/wordpress-rest-api/mocks/posts.ts | 157 ------------------ examples/wordpress-rest-api/mocks/tags.ts | 22 --- examples/wordpress-rest-api/mocks/users.ts | 16 -- packages/blog-logic/Account.ts | 4 +- packages/blog-logic/BloggingPlatform.ts | 13 +- packages/blog-logic/Comment.ts | 10 +- packages/blog-logic/Community.ts | 4 +- packages/blog-logic/Post.ts | 7 +- packages/blog-logic/rest-api.md | 25 --- packages/blog-logic/utils.ts | 4 +- src/chain-observers/filters/post-mention.ts | 2 +- .../providers/mention-provider.ts | 2 +- 17 files changed, 32 insertions(+), 333 deletions(-) delete mode 100644 examples/wordpress-rest-api/mocks/categories.ts delete mode 100644 examples/wordpress-rest-api/mocks/comments.ts delete mode 100644 examples/wordpress-rest-api/mocks/posts.ts delete mode 100644 examples/wordpress-rest-api/mocks/tags.ts delete mode 100644 examples/wordpress-rest-api/mocks/users.ts delete mode 100644 packages/blog-logic/rest-api.md diff --git a/examples/wordpress-rest-api/example-config.ts b/examples/wordpress-rest-api/example-config.ts index a418e88..d9a7fe0 100644 --- a/examples/wordpress-rest-api/example-config.ts +++ b/examples/wordpress-rest-api/example-config.ts @@ -6,6 +6,7 @@ export interface WordPressConfig { startPermlink: string; postTag: string; defaultPort: number; + host: string; } export const wordPressExampleConfig: WordPressConfig = { @@ -16,4 +17,5 @@ export const wordPressExampleConfig: WordPressConfig = { startPermlink: "", postTag: "hive-148441", defaultPort: 4000, + host: "http://localhost", } \ No newline at end of file diff --git a/examples/wordpress-rest-api/hive.ts b/examples/wordpress-rest-api/hive.ts index b3230e7..6d967d1 100644 --- a/examples/wordpress-rest-api/hive.ts +++ b/examples/wordpress-rest-api/hive.ts @@ -2,6 +2,7 @@ import { TWaxApiRequest } from '@hiveio/wax'; + export interface IGetPostHeader { author: string; permlink: string; diff --git a/examples/wordpress-rest-api/hiveToWpMap.ts b/examples/wordpress-rest-api/hiveToWpMap.ts index 40ea2df..202a204 100644 --- a/examples/wordpress-rest-api/hiveToWpMap.ts +++ b/examples/wordpress-rest-api/hiveToWpMap.ts @@ -1,4 +1,5 @@ import { IPost, IReply } from "../../packages/blog-logic/interfaces"; +import { wordPressExampleConfig } from "./example-config"; import { simpleHash } from "./hash-utils"; import { Entry } from "./hive"; import { WPComment, WPPost, WPTag, WPTerm } from "./wp-reference"; @@ -26,7 +27,7 @@ const mapWpTerm = (termName: string, type: "tag" | "category"): WPTerm => { const taxonomy: string = type === "tag" ? "post_tag" : "category"; const wpTerm: WPTerm = { id: termId, - link: `http://localhost/${type}/${termName}/`, + link: `${wordPressExampleConfig.host}/${type}/${termName}/`, name: termName, slug: termName.toLocaleLowerCase(), taxonomy, @@ -53,7 +54,7 @@ export const mapIPostToWpPost = async (hivePost: IPost, wpId: number, accountId: modified_gmt: new Date(hivePost.updatedAt).toISOString(), status: "publish", type: "post", - link: `http://host/${slug}/`, + link: `${wordPressExampleConfig.host}/${slug}/`, title: { rendered: hivePost.title }, content: { rendered: renderedBody, protected: false }, excerpt: { rendered: wpExcerpt, protected: false }, @@ -67,7 +68,7 @@ export const mapIPostToWpPost = async (hivePost: IPost, wpId: number, accountId: meta: {}, categories: [community ? simpleHash(community) : 0], tags: tags.map((tags) => simpleHash(tags)), - guid: { rendered: `http://host/?p=${wpId}` }, + guid: { rendered: `${wordPressExampleConfig.host}/?p=${wpId}` }, class_list: [`category-${community}`], _embedded: { replies: [], @@ -122,7 +123,7 @@ export const mapHiveTagsToWpTags = (tagSlug: string): WPTag => { id: 1, count: 1, description: "", - link: `http://localhost/tag/${tagSlug}/`, + link: `${wordPressExampleConfig.host}/tag/${tagSlug}/`, name: tagSlug, slug: tagSlug, taxonomy: "post_tag", diff --git a/examples/wordpress-rest-api/mocks/categories.ts b/examples/wordpress-rest-api/mocks/categories.ts deleted file mode 100644 index 039ed46..0000000 --- a/examples/wordpress-rest-api/mocks/categories.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const categoryHive = [ - { - id: 3, - count: 1, - description: "About Hive blockchain", - link: "http://localhost/category/hive/", - name: "Hive", - slug: "hive", - taxonomy: "category", - parent: 0, - meta: [] - } -]; diff --git a/examples/wordpress-rest-api/mocks/comments.ts b/examples/wordpress-rest-api/mocks/comments.ts deleted file mode 100644 index 465aa22..0000000 --- a/examples/wordpress-rest-api/mocks/comments.ts +++ /dev/null @@ -1,74 +0,0 @@ -export const comments1 = [ - { - id: 1, - post: 1, - parent: 0, - author: 0, - author_name: "A WordPress Commenter", - author_url: "https://wordpress.org/", - date: "2025-08-25T11:10:15", - date_gmt: "2025-08-25T11:10:15", - content: { - rendered: "

Just a test comment

\n" - }, - link: "http://localhost/hello-world/#comment-1", - status: "approved", - type: "comment", - author_avatar_urls: { - 24: "https://secure.gravatar.com/avatar/8e1606e6fba450a9362af43874c1b2dfad34c782e33d0a51e1b46c18a2a567dd?s=24&d=mm&r=g", - 48: "https://secure.gravatar.com/avatar/8e1606e6fba450a9362af43874c1b2dfad34c782e33d0a51e1b46c18a2a567dd?s=48&d=mm&r=g", - 96: "https://secure.gravatar.com/avatar/8e1606e6fba450a9362af43874c1b2dfad34c782e33d0a51e1b46c18a2a567dd?s=96&d=mm&r=g" - }, - meta: [] - } -]; - -export const comments2 = [] - -export const comments3 = [ - { - id: 3, - post: 9, - parent: 2, - author: 1, - author_name: "wordpress", - author_url: "http://localhost", - date: "2025-08-28T08:34:48", - date_gmt: "2025-08-28T08:34:48", - content: { - rendered: "

Testowa odpowiedź w języku polskim.

\n" - }, - link: "http://localhost/best-mock-post/#comment-3", - status: "approved", - type: "comment", - author_avatar_urls: { - 24: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=24&d=mm&r=g", - 48: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=48&d=mm&r=g", - 96: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=96&d=mm&r=g" - }, - meta: [] - }, - { - id: 2, - post: 9, - parent: 0, - author: 1, - author_name: "wordpress", - author_url: "http://localhost", - date: "2025-08-25T11:40:48", - date_gmt: "2025-08-25T11:40:48", - content: { - rendered: "

Testowy komentarz w języku polskim.

\n

Best page

\n" - }, - link: "http://localhost/best-mock-post/#comment-2", - status: "approved", - type: "comment", - author_avatar_urls: { - 24: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=24&d=mm&r=g", - 48: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=48&d=mm&r=g", - 96: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=96&d=mm&r=g" - }, - meta: [] - } -] - diff --git a/examples/wordpress-rest-api/mocks/posts.ts b/examples/wordpress-rest-api/mocks/posts.ts deleted file mode 100644 index c55cb19..0000000 --- a/examples/wordpress-rest-api/mocks/posts.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { WPPost } from "../wp-reference"; - -export const post1: WPPost = { - id: 1, - date: "2025-08-25T11:10:15", - date_gmt: "2025-08-25T11:10:15", - guid: { - rendered: "http://localhost/?p=1" - }, - modified: "2025-08-25T11:10:15", - modified_gmt: "2025-08-25T11:10:15", - slug: "hello-world", - status: "publish", - type: "post", - link: "http://localhost/hello-world/", - title: { - rendered: "Hello world!" - }, - content: { - rendered: "\n

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

\n", - protected: false - }, - excerpt: { - rendered: "

Welcome to WordPress. This is your first post. Edit or delete it, then start writing!

\n", - protected: false - }, - author: 1, - featured_media: 0, - comment_status: "open", - ping_status: "open", - sticky: false, - template: "", - format: "standard", - meta: { - footnotes: "" - }, - categories: [ - 1 - ], - tags: [ - 11 - ], - class_list: [ - "post-1", - "post", - "type-post", - "status-publish", - "format-standard", - "hentry", - "category-uncategorized" - ] -} - -export const post2: WPPost = { - id: 6, - date: "2025-08-25T11:11:36", - date_gmt: "2025-08-25T11:11:36", - guid: { - rendered: "http://localhost/?p=6" - }, - modified: "2025-08-25T11:11:36", - modified_gmt: "2025-08-25T11:11:36", - slug: "test-post", - status: "publish", - type: "post", - link: "http://localhost/test-post/", - title: { - rendered: "Test post" - }, - content: { - rendered: "\n

I’m going to add simplest mock post for WP.

\n", - protected: false - }, - excerpt: { - rendered: "

I’m going to add simplest mock post for WP.

\n", - protected: false - }, - author: 1, - featured_media: 0, - comment_status: "open", - ping_status: "open", - sticky: false, - template: "", - format: "standard", - meta: { - footnotes: "" - }, - categories: [ - 1 - ], - tags: [], - class_list: [ - "post-6", - "post", - "type-post", - "status-publish", - "format-standard", - "hentry", - "category-uncategorized" - ] -} - -export const post3: WPPost = { - id: 9, - date: "2025-08-25T11:12:17", - date_gmt: "2025-08-25T11:12:17", - guid: { - rendered: "http://localhost/?p=9" - }, - modified: "2025-08-27T10:38:16", - modified_gmt: "2025-08-27T10:38:16", - slug: "best-mock-post", - status: "publish", - type: "post", - link: "http://localhost/best-mock-post/", - title: { - rendered: "Best Mock Post" - }, - content: { - rendered: "\n

I don’t have enough patience for this crap

\n\n\n\n

Best page:

\n\n\n\n

https://youtube.com

\n\n\n\n
\"\"
\n\n\n\n

\n", - protected: false - }, - excerpt: { - rendered: "

I don’t have enough patience for this crap /n Best page: https://youtube.com

\n", - protected: false - }, - author: 1, - featured_media: 0, - comment_status: "open", - ping_status: "open", - sticky: false, - template: "", - format: "standard", - meta: { - footnotes: "" - }, - categories: [ - 3 - ], - tags: [ - 4 - ], - class_list: [ - "post-9", - "post", - "type-post", - "status-publish", - "format-standard", - "hentry", - "category-hive", - "tag-mock-rock" - ] -} - -export const allPosts = [post3, post2, post1]; - - diff --git a/examples/wordpress-rest-api/mocks/tags.ts b/examples/wordpress-rest-api/mocks/tags.ts deleted file mode 100644 index 87e58d5..0000000 --- a/examples/wordpress-rest-api/mocks/tags.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const tagMock = [ - { - id: 4, - count: 1, - description: "All things about mock", - link: "http://localhost/tag/mock-rock/", - name: "Mock rocks", - slug: "mock-rock", - taxonomy: "post_tag", - meta: [] - }, - { - id: 11, - count: 1, - description: "About front end's programming language", - link: "http://localhost/tag/javascript/", - name: "JavaScript", - slug: "javascript", - taxonomy: "post_tag", - meta: [] - } -]; diff --git a/examples/wordpress-rest-api/mocks/users.ts b/examples/wordpress-rest-api/mocks/users.ts deleted file mode 100644 index 481ffb6..0000000 --- a/examples/wordpress-rest-api/mocks/users.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const userWordPress = [ - { - id: 1, - name: "wordpress", - url: "http://localhost", - description: "", - link: "http://localhost/author/wordpress/", - slug: "wordpress", - avatar_urls: { - 24: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=24&d=mm&r=g", - 48: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=48&d=mm&r=g", - 96: "https://secure.gravatar.com/avatar/8267cafaf605e0677af79986f57c6dff9a5ab053477841b190e57b98d16248d5?s=96&d=mm&r=g" - }, - meta: [] - } -]; diff --git a/packages/blog-logic/Account.ts b/packages/blog-logic/Account.ts index 7b6d082..6a22277 100644 --- a/packages/blog-logic/Account.ts +++ b/packages/blog-logic/Account.ts @@ -13,10 +13,10 @@ export class Account implements IAccount { public constructor(accountData: AccountDetails) { this.name = accountData.name; this.avatar = JSON.parse(accountData.posting_json_metadata)?.profile.profile_image || ""; - this.creationDate = new Date(accountData.created); + this.creationDate = new Date(`${accountData.created}Z`); this.postCount = 0; // In this API not available. this.lastActivity = new Date(); // In this API not available. - this.registeredDate = new Date(accountData.created); + this.registeredDate = new Date(`${accountData.created}Z`); this.description = JSON.parse(accountData.posting_json_metadata)?.about || ""; } diff --git a/packages/blog-logic/BloggingPlatform.ts b/packages/blog-logic/BloggingPlatform.ts index 7de3c38..52dc0df 100644 --- a/packages/blog-logic/BloggingPlatform.ts +++ b/packages/blog-logic/BloggingPlatform.ts @@ -1,4 +1,5 @@ import { TWaxExtended, TWaxRestExtended } from "@hiveio/wax"; +import { WorkerBeeError } from "../../src/errors"; import { Account } from "./Account"; import { Community } from "./Community"; import { IAccount, IAccountIdentity, @@ -12,7 +13,7 @@ import { IAccount, IAccountIdentity, } from "./interfaces"; import { Post } from "./Post"; import { paginateData } from "./utils"; -import { ExtendedNodeApi, ExtendedRestApi, getWax } from "./wax"; +import { Entry, ExtendedNodeApi, ExtendedRestApi, getWax } from "./wax"; export class BloggingPlaform implements IBloggingPlatform { public viewerContext: IAccountIdentity; @@ -52,7 +53,7 @@ export class BloggingPlaform implements IBloggingPlatform { await this.initializeChain(); const postData = await this.chain?.api.bridge.get_post({author: postId.author, permlink: postId.permlink, observer: this.viewerContext.name }); if (!postData) - throw new Error("Post not found"); + throw new WorkerBeeError("Post not found"); return new Post(postId, this, postData!); } @@ -65,7 +66,7 @@ export class BloggingPlaform implements IBloggingPlatform { public async enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise> { await this.initializeChain(); const communities = await this.chain?.api.bridge.list_communities({observer: this.viewerContext.name, sort: filter.sort, query: filter.query}); - if (communities) return paginateData(communities.map((community) => new Community(community)), pagination); + if (communities) return paginateData(communities.map((community) => new Community(community)), pagination); return await []; } @@ -86,8 +87,8 @@ export class BloggingPlaform implements IBloggingPlatform { tag: filter.tag }); if (!posts) - throw new Error("Posts not found"); - const paginatedPosts = paginateData(posts, pagination); + throw new WorkerBeeError("Posts not found"); + const paginatedPosts = paginateData(posts, pagination); return paginatedPosts?.map((post) => new Post({author: post.author, permlink: post.permlink}, this, post)) } @@ -100,7 +101,7 @@ export class BloggingPlaform implements IBloggingPlatform { await this.initializeChain(); const account = await this.chain?.restApi["hafbe-api"].accounts.account({accountName: accontName}); if (!account) - throw new Error("Account not found"); + throw new WorkerBeeError("Account not found"); return new Account(account); } diff --git a/packages/blog-logic/Comment.ts b/packages/blog-logic/Comment.ts index 9ea4bee..0f341ba 100644 --- a/packages/blog-logic/Comment.ts +++ b/packages/blog-logic/Comment.ts @@ -30,8 +30,8 @@ export class Comment implements IComment { this.author = authorPermlink.author; this.permlink = authorPermlink.permlink; this.bloggingPlatform = bloggingPlatform; - this.publishedAt = new Date(postCommentData.created); - this.updatedAt = new Date(postCommentData.updated); + this.publishedAt = new Date(`${postCommentData.created}Z`); + this.updatedAt = new Date(`${postCommentData.updated}Z`); this.content = postCommentData.body; } @@ -47,7 +47,7 @@ export class Comment implements IComment { */ /* eslint-disable-next-line require-await */ public async enumMentionedAccounts(): Promise> { - const regex = /@[a-z0-9.-]+\b/g; // Alphanumeric with . and -, but not on the end. + const regex = /@[a-z]+[a-z0-9.-]+[a-z0-9]+\b/g; // Alphanumeric with . and -, but not on the end. return this.content?.match(regex) ?? []; } @@ -71,7 +71,7 @@ export class Comment implements IComment { const votesData = await this.chain!.api.database_api.list_votes({limit: filter.limit, order: filter.votesSort, start: null}); const votes = votesData.votes.map((vote) => new Vote(vote)); this.votes = votes; - return paginateData(votes, pagination); + return paginateData(votes, pagination); } /** @@ -81,7 +81,7 @@ export class Comment implements IComment { public async wasVotedByUser(userName: string): Promise { this.initializeChain(); if (!this.votes) await this.enumVotes({limit: 10000, votesSort: "by_comment_voter"}, {page: 1, pageSize: 10000}); // Temporary pagination before fix - return !!Array.from(this.votes || []).find((vote) => vote.voter === userName) + return Array.from(this.votes || []).some((vote) => vote.voter === userName) } } diff --git a/packages/blog-logic/Community.ts b/packages/blog-logic/Community.ts index 021991b..d69bce7 100644 --- a/packages/blog-logic/Community.ts +++ b/packages/blog-logic/Community.ts @@ -18,7 +18,7 @@ export class Community implements ICommunity { this.about = communityData.about; this.admins = communityData.admins || []; this.avatarUrl = communityData.avatar_url; - this.creationDate = new Date(communityData.created_at); + this.creationDate = new Date(`${communityData.created_at}Z`); this.subscribersCount = communityData.subscribers; this.authorsCount = communityData.num_authors; this.pendingCount = communityData.num_pending; @@ -29,6 +29,6 @@ export class Community implements ICommunity { * Get standard WordPress slug. It treats community as category. */ public getSlug(): string { - return this.title + return this.title.toLowerCase().replace(/\s+/g, "-"); } } diff --git a/packages/blog-logic/Post.ts b/packages/blog-logic/Post.ts index 17baade..9a4edbe 100644 --- a/packages/blog-logic/Post.ts +++ b/packages/blog-logic/Post.ts @@ -1,3 +1,4 @@ +import { WorkerBeeError } from "../../src/errors"; import { Comment } from "./Comment"; import { IBloggingPlatform, ICommunityIdentity, IPagination, IPost, IPostCommentIdentity, IPostCommentsFilters, IReply } from "./interfaces"; import { Reply } from "./Reply"; @@ -38,7 +39,7 @@ export class Post extends Comment implements IPost { observer: this.bloggingPlatform.viewerContext.name, }); if (!repliesData) - throw "No replies"; + throw new WorkerBeeError("No replies"); const filteredReplies = Object.values(repliesData).filter((rawReply) => !!rawReply.parent_author) const replies = filteredReplies?.map( (reply) => @@ -76,8 +77,8 @@ export class Post extends Comment implements IPost { */ public async enumReplies(filter: IPostCommentsFilters, pagination: IPagination): Promise> { this.initializeChain(); - if (this.replies) return paginateData(Array.from(this.replies), pagination); - return paginateData(await this.fetchReplies() as IReply[], pagination); + if (this.replies) return paginateData(Array.from(this.replies), pagination); + return paginateData(await this.fetchReplies() as IReply[], pagination); } /** diff --git a/packages/blog-logic/rest-api.md b/packages/blog-logic/rest-api.md deleted file mode 100644 index 86bca45..0000000 --- a/packages/blog-logic/rest-api.md +++ /dev/null @@ -1,25 +0,0 @@ -// Work in progress - -# POSTS - -GET posts?(filters, pagination) -GET posts/{post-id} -POST posts -PUT posts/{post-id} -DELETE posts/{post-id} -GET posts/{post-id}/comments?(filters, pagination) -GET posts/{post-id}/comments/{comment-id} -POST posts/{post-id}/comments -PUT posts/{post-id}/comments/{comment-id} -DELETE posts/{post-id}/comments/{comment-id} -POST posts/{post-id}/vote - -# USER - -GET users/{user-id}/profile -GET users/{user-id}/posts?(filters, pagination) - -# COMMUNITY - -GET communities/{community-id}/profile -GET communities/{community-id}/posts?(filters, pagination) diff --git a/packages/blog-logic/utils.ts b/packages/blog-logic/utils.ts index a472229..f7e6b2a 100644 --- a/packages/blog-logic/utils.ts +++ b/packages/blog-logic/utils.ts @@ -1,7 +1,7 @@ import { IPagination } from "./interfaces"; -export const paginateData = (data: any[], pagination: IPagination): any[] => { +export const paginateData = (data: T[], pagination: IPagination): T[] => { const {page, pageSize} = pagination const startIndex = (page - 1) * pageSize; return data.slice(startIndex, startIndex + pageSize); -} \ No newline at end of file +} diff --git a/src/chain-observers/filters/post-mention.ts b/src/chain-observers/filters/post-mention.ts index d389f62..592c205 100644 --- a/src/chain-observers/filters/post-mention.ts +++ b/src/chain-observers/filters/post-mention.ts @@ -28,7 +28,7 @@ export class PostMentionFilter extends FilterBase { for(const { operation: { body } } of (operationsPerType.comment_operation ?? [])) { // Use regex to find all account mentions in the form of @username - const mentionRegex = /@([a-z0-9.-]+)/gi; + const mentionRegex = /@[a-z]+[a-z0-9.-]+[a-z0-9]+\b/g; let match: RegExpExecArray | null; while ((match = mentionRegex.exec(body)) !== null) { const mentionedAccount = match[1] as TAccountName; diff --git a/src/chain-observers/providers/mention-provider.ts b/src/chain-observers/providers/mention-provider.ts index 36910d6..c2059e0 100644 --- a/src/chain-observers/providers/mention-provider.ts +++ b/src/chain-observers/providers/mention-provider.ts @@ -47,7 +47,7 @@ export class MentionedAccountProvider = Ar postMetadataSet.add(postHash); - const mentionRegex = /@([a-z0-9.-]+)/gi; + const mentionRegex = /@[a-z]+[a-z0-9.-]+[a-z0-9]+\b/g; let match: RegExpExecArray | null; let foundMention = false; while ((match = mentionRegex.exec(operation.body)) !== null && !foundMention) { -- GitLab From 4d0bcc89f87607c963643f2c129340788e68fc5a Mon Sep 17 00:00:00 2001 From: jlachor Date: Tue, 7 Oct 2025 13:47:23 +0200 Subject: [PATCH 10/15] Implement Data Provider in Blog Logic --- .../wordpress-rest-api/wordpress-rest-api.ts | 20 ++- packages/blog-logic/Account.ts | 7 +- packages/blog-logic/BloggingPlatform.ts | 60 ++----- packages/blog-logic/Comment.ts | 44 +++--- packages/blog-logic/Community.ts | 7 +- packages/blog-logic/DataProvider.ts | 146 ++++++++++++++++++ packages/blog-logic/Post.ts | 77 +++------ packages/blog-logic/Reply.ts | 16 +- packages/blog-logic/Vote.ts | 9 +- packages/blog-logic/interfaces.ts | 6 +- 10 files changed, 240 insertions(+), 152 deletions(-) create mode 100644 packages/blog-logic/DataProvider.ts diff --git a/examples/wordpress-rest-api/wordpress-rest-api.ts b/examples/wordpress-rest-api/wordpress-rest-api.ts index ba13867..3f99663 100644 --- a/examples/wordpress-rest-api/wordpress-rest-api.ts +++ b/examples/wordpress-rest-api/wordpress-rest-api.ts @@ -8,6 +8,8 @@ import { simpleHash } from "./hash-utils"; import { wordPressExampleConfig } from "./example-config"; import { IPost, IReply } from "../../packages/blog-logic/interfaces"; import { BloggingPlaform } from "../../packages/blog-logic/BloggingPlatform"; +import { DataProvider } from "../../packages/blog-logic/DataProvider"; +import { getWax } from "../../packages/blog-logic/wax"; const hiveChain = await createHiveChain(); @@ -21,9 +23,9 @@ const apiRouter = express.Router(); const idToStringMap = new Map(); -let posts: IPost[] = []; -const bloggingPlatform: BloggingPlaform = new BloggingPlaform(); -bloggingPlatform.configureViewContext({name: wordPressExampleConfig.observer}); +const chain = await getWax() +const dataProvider = new DataProvider(chain); +dataProvider.bloggingPlatform.configureViewContext({name: wordPressExampleConfig.observer}); const getAuthorPermlinkFromSlug = (slug: string): {author: string, permlink: string} => { const splitedSlug = slug.split("_"); @@ -71,8 +73,7 @@ apiRouter.get("/posts", async (req: Request, res: Response) => { const authorPermlinkHash = simpleHash(req.query.slug); const authorHash = simpleHash(author); idToStringMap.set(authorPermlinkHash, req.query.slug as string).set(authorHash, author); - const post = await bloggingPlatform.getPost({author: author, permlink}); - posts.push(post); + const post = await dataProvider.bloggingPlatform.getPost({author: author, permlink}); if (post) { res.json(await mapIPostToWpPost(post, authorPermlinkHash, authorHash)); } else { @@ -80,7 +81,7 @@ apiRouter.get("/posts", async (req: Request, res: Response) => { } // Posts list } else { - const newPosts = await bloggingPlatform.enumPosts({ + const newPosts = await dataProvider.bloggingPlatform.enumPosts({ limit: wordPressExampleConfig.postLimit, sort: wordPressExampleConfig.sort, startAuthor: wordPressExampleConfig.startAuthor, @@ -90,10 +91,7 @@ apiRouter.get("/posts", async (req: Request, res: Response) => { page: 1, pageSize: 10 }) as IPost[]; - if (posts) { - posts = [...posts, ...newPosts]; - res.json(await mapAndAddtoMapPosts(posts)); - } + res.json(await mapAndAddtoMapPosts(newPosts)); } }); @@ -104,7 +102,7 @@ apiRouter.get("/comments", async (req: Request, res: Response) => { if (postParent) { const {author, permlink} = getAuthorPermlinkFromSlug(postParent); // Delete array of posts, get replies here. - const post = posts.find((post) => post.author === author && post.permlink === permlink); + const post = await dataProvider.bloggingPlatform.getPost({author, permlink}) if (post) { const replies = await post.enumReplies({}, {page: 1, pageSize: 1000}) as IReply[]; if (replies) { diff --git a/packages/blog-logic/Account.ts b/packages/blog-logic/Account.ts index 6a22277..c3b1e3c 100644 --- a/packages/blog-logic/Account.ts +++ b/packages/blog-logic/Account.ts @@ -1,5 +1,6 @@ +import { WorkerBeeError } from "../../src/errors"; +import { DataProvider } from "./DataProvider"; import { IAccount } from "./interfaces"; -import { AccountDetails } from "./wax"; export class Account implements IAccount { public readonly name: string; @@ -10,7 +11,9 @@ export class Account implements IAccount { public readonly description: string; public readonly avatar: string; - public constructor(accountData: AccountDetails) { + public constructor(accountName: string, dataProvider: DataProvider) { + const accountData = dataProvider.getAccount(accountName); + if(!accountData) throw new WorkerBeeError("No account"); this.name = accountData.name; this.avatar = JSON.parse(accountData.posting_json_metadata)?.profile.profile_image || ""; this.creationDate = new Date(`${accountData.created}Z`); diff --git a/packages/blog-logic/BloggingPlatform.ts b/packages/blog-logic/BloggingPlatform.ts index 52dc0df..4a2cb20 100644 --- a/packages/blog-logic/BloggingPlatform.ts +++ b/packages/blog-logic/BloggingPlatform.ts @@ -1,7 +1,6 @@ -import { TWaxExtended, TWaxRestExtended } from "@hiveio/wax"; -import { WorkerBeeError } from "../../src/errors"; import { Account } from "./Account"; import { Community } from "./Community"; +import { DataProvider } from "./DataProvider"; import { IAccount, IAccountIdentity, IBloggingPlatform, ICommunity, @@ -9,30 +8,19 @@ import { IAccount, IAccountIdentity, IPagination, IPost, IPostCommentIdentity, - IPostCommentsFilters + IPostFilters } from "./interfaces"; import { Post } from "./Post"; -import { paginateData } from "./utils"; -import { Entry, ExtendedNodeApi, ExtendedRestApi, getWax } from "./wax"; export class BloggingPlaform implements IBloggingPlatform { + private dataProvider: DataProvider; public viewerContext: IAccountIdentity; - private chain?: TWaxExtended>; - - - private initializeChain = async () => { - if (!this.chain) - this.chain = await getWax(); - } - // Add initilaize chain to constructor - - public overwrittenGetTitleImage?: () => string; - public constructor() { + public constructor(dataProvider: DataProvider) { this.viewerContext = {name: "hive.blog"}; // Set default - this.initializeChain(); + this.dataProvider = dataProvider; } /** @@ -50,11 +38,8 @@ export class BloggingPlaform implements IBloggingPlatform { * @returns post object */ public async getPost(postId: IPostCommentIdentity): Promise { - await this.initializeChain(); - const postData = await this.chain?.api.bridge.get_post({author: postId.author, permlink: postId.permlink, observer: this.viewerContext.name }); - if (!postData) - throw new WorkerBeeError("Post not found"); - return new Post(postId, this, postData!); + await this.dataProvider.fetchPost(postId); + return new Post(postId, this.dataProvider); } /** @@ -64,10 +49,8 @@ export class BloggingPlaform implements IBloggingPlatform { * @returns iterable of community objects */ public async enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise> { - await this.initializeChain(); - const communities = await this.chain?.api.bridge.list_communities({observer: this.viewerContext.name, sort: filter.sort, query: filter.query}); - if (communities) return paginateData(communities.map((community) => new Community(community)), pagination); - return await []; + const communitiesIds = await this.dataProvider.enumCommunities(filter, pagination); + return communitiesIds.map((communityId) => new Community(communityId, this.dataProvider)); } /** @@ -76,20 +59,9 @@ export class BloggingPlaform implements IBloggingPlatform { * @param pagination * @returns iterable of posts */ - public async enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Promise> { - await this.initializeChain(); - const posts = await this.chain?.api.bridge.get_ranked_posts({ - limit: filter.limit, - sort: filter.sort, - observer: this.viewerContext.name, - start_author: filter.startAuthor, - start_permlink: filter.startPermlink, - tag: filter.tag - }); - if (!posts) - throw new WorkerBeeError("Posts not found"); - const paginatedPosts = paginateData(posts, pagination); - return paginatedPosts?.map((post) => new Post({author: post.author, permlink: post.permlink}, this, post)) + public async enumPosts(filter: IPostFilters, pagination: IPagination): Promise> { + const postsIds = await this.dataProvider.enumPosts(filter, pagination); + return postsIds.map((post) => new Post({author: post.author, permlink: post.permlink}, this.dataProvider)) } /** @@ -98,11 +70,8 @@ export class BloggingPlaform implements IBloggingPlatform { * @returns account object */ public async getAccount(accontName: string): Promise { - await this.initializeChain(); - const account = await this.chain?.restApi["hafbe-api"].accounts.account({accountName: accontName}); - if (!account) - throw new WorkerBeeError("Account not found"); - return new Account(account); + await this.dataProvider.fetchAccount(accontName); + return new Account(accontName, this.dataProvider); } // Section for overwritting methods @@ -115,3 +84,4 @@ export class BloggingPlaform implements IBloggingPlatform { this.overwriteGetTitleImage = callbackMethod; } } + diff --git a/packages/blog-logic/Comment.ts b/packages/blog-logic/Comment.ts index 0f341ba..b41bdb5 100644 --- a/packages/blog-logic/Comment.ts +++ b/packages/blog-logic/Comment.ts @@ -1,8 +1,8 @@ import { TWaxExtended } from "@hiveio/wax"; -import { IBloggingPlatform, IComment, IPagination, IPostCommentIdentity, IVote, IVotesFilters } from "./interfaces"; -import { paginateData } from "./utils"; +import { DataProvider } from "./DataProvider"; +import { IComment, IPagination, IPostCommentIdentity, IVote, IVotesFilters } from "./interfaces"; import { Vote } from "./Vote"; -import { Entry, ExtendedNodeApi, getWax } from "./wax"; +import { ExtendedNodeApi } from "./wax"; export class Comment implements IComment { @@ -16,23 +16,22 @@ export class Comment implements IComment { protected content?: string; protected votes?: Iterable; - protected bloggingPlatform: IBloggingPlatform; - - - protected initializeChain = async () => { - if (!this.chain) - this.chain = await getWax(); - } + protected dataProvider: DataProvider; // Refactor blogginPlatform and data into dataProvider with promises. - public constructor(authorPermlink: IPostCommentIdentity, bloggingPlatform: IBloggingPlatform, postCommentData: Entry, ) { - this.initializeChain(); + public constructor(authorPermlink: IPostCommentIdentity, dataProvider: DataProvider) { this.author = authorPermlink.author; this.permlink = authorPermlink.permlink; - this.bloggingPlatform = bloggingPlatform; - this.publishedAt = new Date(`${postCommentData.created}Z`); - this.updatedAt = new Date(`${postCommentData.updated}Z`); - this.content = postCommentData.body; + this.dataProvider = dataProvider; + const post = dataProvider.getComment(authorPermlink); + this.publishedAt = new Date(post?.created || ""); + this.updatedAt = new Date(post?.updated || ""); + this.content = post?.body; + + } + + protected getCommentId(): IPostCommentIdentity { + return {author: this.author, permlink: this.permlink}; } /** @@ -66,12 +65,9 @@ export class Comment implements IComment { * @returns Iterable of Votes. */ public async enumVotes(filter: IVotesFilters, pagination: IPagination): Promise> { - this.initializeChain(); // Get rid of condenser API - const votesData = await this.chain!.api.database_api.list_votes({limit: filter.limit, order: filter.votesSort, start: null}); - const votes = votesData.votes.map((vote) => new Vote(vote)); - this.votes = votes; - return paginateData(votes, pagination); + const voters = await this.dataProvider.enumVotes(this.getCommentId(), filter, pagination); + return voters.map((voter) => new Vote(this.getCommentId(), voter, this.dataProvider)); } /** @@ -79,9 +75,9 @@ export class Comment implements IComment { * @param userName */ public async wasVotedByUser(userName: string): Promise { - this.initializeChain(); - if (!this.votes) await this.enumVotes({limit: 10000, votesSort: "by_comment_voter"}, {page: 1, pageSize: 10000}); // Temporary pagination before fix - return Array.from(this.votes || []).some((vote) => vote.voter === userName) + let voters = this.dataProvider.getVoters(this.getCommentId()); + if (!voters) voters = await this.dataProvider.enumVotes(this.getCommentId(), {limit: 1000, votesSort: "by_comment_voter"}, {page: 1, pageSize: 10000}); + return voters.some((voter) => voter === userName) } } diff --git a/packages/blog-logic/Community.ts b/packages/blog-logic/Community.ts index d69bce7..84f32b1 100644 --- a/packages/blog-logic/Community.ts +++ b/packages/blog-logic/Community.ts @@ -1,5 +1,6 @@ +import { WorkerBeeError } from "../../src/errors"; +import { DataProvider } from "./DataProvider"; import { ICommunity } from "./interfaces"; -import { CommunityData } from "./wax"; export class Community implements ICommunity { public readonly name: string; @@ -12,7 +13,9 @@ export class Community implements ICommunity { public readonly authorsCount: number; public readonly pendingCount: number; - public constructor(communityData: CommunityData) { + public constructor(communityName: string, dataProvider: DataProvider) { + const communityData = dataProvider.getCommunity(communityName); + if (!communityData) throw new WorkerBeeError("No community"); this.name = communityData.name; this.title = communityData.title; this.about = communityData.about; diff --git a/packages/blog-logic/DataProvider.ts b/packages/blog-logic/DataProvider.ts new file mode 100644 index 0000000..debb0fd --- /dev/null +++ b/packages/blog-logic/DataProvider.ts @@ -0,0 +1,146 @@ +import { TWaxExtended, TWaxRestExtended } from "@hiveio/wax"; +import { WorkerBeeError } from "../../src/errors"; +import { BloggingPlaform } from "./BloggingPlatform"; +import { ICommonFilters, ICommunityFilters, IPagination, IPostCommentIdentity, IPostFilters, IVotesFilters } from "./interfaces"; +import { paginateData } from "./utils"; +import { AccountDetails, CommunityData, Entry, ExtendedNodeApi, ExtendedRestApi, IVoteListItem } from "./wax"; + +/** + * Main class to call all of Blog Logic. The class is responsible for making instances of Blog Logic's objects and + * getting and caching all the necessary data for them. + */ +export class DataProvider { + public chain: TWaxExtended>; + public bloggingPlatform: BloggingPlaform; + + private comments: Map = new Map(); + private repliesByPostId: Map = new Map(); + private accounts: Map = new Map(); + private communities: Map = new Map(); + private votesByCommentsAndVoter: Map> = new Map(); + + + public constructor(chain: TWaxExtended>) { + this.chain = chain; + this.bloggingPlatform = new BloggingPlaform(this); + } + + /** + * For keeping universal author-permlink strings as a map key. The string is done the same way as in WP API. + */ + private convertCommentIdToHash(commentId: IPostCommentIdentity): string { + return `${commentId.author}_${commentId.permlink}` + } + + public getComment(postId: IPostCommentIdentity): Entry | null { + return this.comments.get(this.convertCommentIdToHash(postId)) || null; + } + + public async fetchPost(postId: IPostCommentIdentity): Promise { + const fetchedPostData = await this.chain.api.bridge.get_post({ + author: postId.author, + permlink: postId.permlink, + observer: this.bloggingPlatform.viewerContext.name, + }); + if (!fetchedPostData) + throw new Error("Post not found"); + this.comments.set(this.convertCommentIdToHash(postId), fetchedPostData); + } + + public async enumPosts(filter: IPostFilters, pagination: IPagination): Promise { + const posts = await this.chain.api.bridge.get_ranked_posts({ + limit: filter.limit, + sort: filter.sort, + observer: this.bloggingPlatform.viewerContext.name, + start_author: filter.startAuthor, + start_permlink: filter.startPermlink, + tag: filter.tag + }); + if (!posts) + throw new WorkerBeeError("Posts not found"); + const paginatedPosts = paginateData(posts, pagination); + paginatedPosts.forEach((post) => { + const postId = {author: post.author, permlink: post.permlink} + this.comments.set(this.convertCommentIdToHash(postId), post); + }) + return paginatedPosts.map((post) => ({author: post.author, permlink: post.permlink})); + } + + public getRepliesIdsByPost(postId: IPostCommentIdentity): IPostCommentIdentity[] | null { + return this.repliesByPostId.get(this.convertCommentIdToHash(postId)) || null; + } + + public async enumReplies(postId: IPostCommentIdentity, filter: ICommonFilters, pagination: IPagination): Promise { + const replies = await this.chain!.api.bridge.get_discussion({ + author: postId.author, + permlink: postId.permlink, + observer: this.bloggingPlatform.viewerContext.name, + }); + if (!replies) throw WorkerBeeError; + const filteredReplies = Object.values(replies).filter((rawReply) => !!rawReply.parent_author); + const repliesIds: IPostCommentIdentity[] = []; + filteredReplies.forEach((reply) => { + const replyId = { + author: reply.author, + permlink: reply.permlink + } + repliesIds.push(replyId); + this.comments.set(this.convertCommentIdToHash(replyId), reply); + }) + this.repliesByPostId.set(this.convertCommentIdToHash(postId), repliesIds); + + return paginateData(filteredReplies, pagination).map((reply) => ({author: reply.author, permlink: reply.permlink})); + } + + public getAccount(accountName: string): AccountDetails | null { + return this.accounts.get(accountName) || null; + } + + public async fetchAccount(accountName: string): Promise { + const account = await this.chain.restApi["hafbe-api"].accounts.account({accountName: accountName}); + if (!account) + throw new Error("Account not found"); + this.accounts.set(accountName, account); + } + + public getCommunity(communityName: string): CommunityData | null { + return this.communities.get(communityName) || null; + } + public async enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise { + const communities = await this.chain.api.bridge.list_communities({ + observer: this.bloggingPlatform.viewerContext.name, + sort: filter.sort, + query: filter.query, + }); + const communitiesNames: string[] = []; + if (communities) + communities.forEach((community) => { + this.communities.set(community.name, community); + communitiesNames.push(community.name); + }) + return paginateData(communitiesNames, pagination); + } + + public getVote(commentId: IPostCommentIdentity, voter: string): IVoteListItem | null { + return this.votesByCommentsAndVoter.get(this.convertCommentIdToHash(commentId))?.get(voter) || null; + } + + public getVoters(commentId: IPostCommentIdentity): string[] | null { + const votesMap = this.votesByCommentsAndVoter.get(this.convertCommentIdToHash(commentId)); + const votes = Array.from(votesMap?.keys() || []); + return votes || null; + } + + public async enumVotes(commentId: IPostCommentIdentity, filter: IVotesFilters, pagination: IPagination): Promise { + const votesData = (await this.chain!.api.database_api.list_votes({limit: filter.limit, order: filter.votesSort, start: [commentId.author, commentId.permlink, ""]})).votes; + const votersForComment: string[] = []; + const votesByVoters: Map = new Map(); + votesData.forEach((voteData) => { + votersForComment.push(voteData.voter); + votesByVoters.set(voteData.voter, voteData); + }) + this.votesByCommentsAndVoter.set(this.convertCommentIdToHash(commentId), votesByVoters); + return paginateData(votersForComment, pagination); + } + +} diff --git a/packages/blog-logic/Post.ts b/packages/blog-logic/Post.ts index 9a4edbe..cc9b463 100644 --- a/packages/blog-logic/Post.ts +++ b/packages/blog-logic/Post.ts @@ -1,9 +1,7 @@ -import { WorkerBeeError } from "../../src/errors"; import { Comment } from "./Comment"; -import { IBloggingPlatform, ICommunityIdentity, IPagination, IPost, IPostCommentIdentity, IPostCommentsFilters, IReply } from "./interfaces"; +import { DataProvider } from "./DataProvider"; +import { ICommonFilters, ICommunityIdentity, IPagination, IPost, IPostCommentIdentity, IReply } from "./interfaces"; import { Reply } from "./Reply"; -import { paginateData } from "./utils"; -import { Entry } from "./wax"; export class Post extends Comment implements IPost { @@ -13,59 +11,27 @@ export class Post extends Comment implements IPost { public summary: string; public communityTitle?: string; - private replies?: Iterable; private postImage?: string; - public constructor(authorPermlink: IPostCommentIdentity, bloggingPlatform: IBloggingPlatform, postData: Entry) { - super(authorPermlink, bloggingPlatform, postData); - this.title = postData.title; - this.tags = postData.json_metadata?.tags || []; - this.summary = postData.json_metadata?.description || ""; - this.community = postData.community ? {name: postData.community} : undefined; - this.communityTitle = postData.community_title - this.postImage = postData.json_metadata.image?.[0]; - } + public constructor(authorPermlink: IPostCommentIdentity, dataProvider: DataProvider) { + super(authorPermlink, dataProvider); + const post = dataProvider.getComment(authorPermlink); + this.title = post?.title || ""; + this.tags = post?.json_metadata?.tags || []; + this.summary = post?.json_metadata?.description || ""; + this.community = post?.community ? {name: post.community} : undefined; + this.communityTitle = post?.community_title + this.postImage = post?.json_metadata.image?.[0]; - /** - * Fetch and return all replies for post. Do pagination later. - * @returns iterable of replies - */ - private async fetchReplies(): Promise> { - this.initializeChain(); - if (!this.replies) { - const repliesData = await this.chain!.api.bridge.get_discussion({ - author: this.author, - permlink: this.permlink, - observer: this.bloggingPlatform.viewerContext.name, - }); - if (!repliesData) - throw new WorkerBeeError("No replies"); - const filteredReplies = Object.values(repliesData).filter((rawReply) => !!rawReply.parent_author) - const replies = filteredReplies?.map( - (reply) => - new Reply( - { author: reply.author, permlink: reply.permlink }, - this.bloggingPlatform, - { - author: reply.parent_author || "", - permlink: reply.parent_permlink || "", - }, - { author: this.author, permlink: this.permlink }, - reply - ) - ) - this.replies = replies; - return replies; - } - return this.replies; } + /** * Get title image from post content. * @returns Link to title image */ public getTitleImage(): string { - if (this.bloggingPlatform.overwrittenGetTitleImage) return this.bloggingPlatform.overwrittenGetTitleImage() + if (this.dataProvider.bloggingPlatform.overwrittenGetTitleImage) return this.dataProvider.bloggingPlatform.overwrittenGetTitleImage() return this.postImage || "" } @@ -75,20 +41,21 @@ export class Post extends Comment implements IPost { * @param pagination * @returns iterable of replies objects */ - public async enumReplies(filter: IPostCommentsFilters, pagination: IPagination): Promise> { - this.initializeChain(); - if (this.replies) return paginateData(Array.from(this.replies), pagination); - return paginateData(await this.fetchReplies() as IReply[], pagination); + public async enumReplies(filter: ICommonFilters, pagination: IPagination): Promise> { + const postId = {author: this.author, permlink: this.permlink}; + const repliesIds = await this.dataProvider.enumReplies(postId, filter, pagination) || []; + return repliesIds.map((replyId) => new Reply(replyId, this.dataProvider, postId)); } /** * Get number of comments (replies) for given post. */ public async getCommentsCount(): Promise { - this.initializeChain(); - if (this.replies) return Array.from(this.replies).length; - - return (Array.from(await this.fetchReplies())).length; + const postId = {author: this.author, permlink: this.permlink}; + let repliesIds = this.dataProvider.getRepliesIdsByPost(postId); + if (!repliesIds) + repliesIds = await this.dataProvider.enumReplies(postId, {}, {page: 1, pageSize: 10000}); + return repliesIds.length; } } diff --git a/packages/blog-logic/Reply.ts b/packages/blog-logic/Reply.ts index 1bf4c89..335565d 100644 --- a/packages/blog-logic/Reply.ts +++ b/packages/blog-logic/Reply.ts @@ -1,6 +1,6 @@ import { Comment } from "./Comment"; -import { IBloggingPlatform, IPostCommentIdentity, IReply } from "./interfaces"; -import { Entry } from "./wax"; +import { DataProvider } from "./DataProvider"; +import { IPostCommentIdentity, IReply } from "./interfaces"; export class Reply extends Comment implements IReply { public parent: IPostCommentIdentity; @@ -8,13 +8,13 @@ export class Reply extends Comment implements IReply { public constructor( authorPermlink: IPostCommentIdentity, - bloggingPlatform: IBloggingPlatform, - parent: IPostCommentIdentity, - topPost: IPostCommentIdentity, - replyData: Entry + dataProvider: DataProvider, + topPost: IPostCommentIdentity ) { - super(authorPermlink, bloggingPlatform, replyData); - this.parent = parent; + super(authorPermlink, dataProvider); + const reply = dataProvider.getComment(authorPermlink); + + this.parent = {author: reply?.parent_author || "", permlink: reply?.parent_permlink || ""}; this.topPost = topPost; } } diff --git a/packages/blog-logic/Vote.ts b/packages/blog-logic/Vote.ts index bfaf3ff..e55154e 100644 --- a/packages/blog-logic/Vote.ts +++ b/packages/blog-logic/Vote.ts @@ -1,12 +1,15 @@ -import { IVote } from "./interfaces"; -import { IVoteListItem } from "./wax"; +import { WorkerBeeError } from "../../src/errors"; +import { DataProvider } from "./DataProvider"; +import { IPostCommentIdentity, IVote } from "./interfaces"; export class Vote implements IVote { public upvote: boolean; public voter: string; public weight: number; - public constructor(voteData: IVoteListItem) { + public constructor(parentId: IPostCommentIdentity, voter: string, dataProvider: DataProvider) { + const voteData = dataProvider.getVote(parentId, voter); + if(!voteData) throw new WorkerBeeError("No account"); this.upvote = Number(voteData.weight) > 0 this.voter = voteData.voter; this.weight = Number(voteData.weight); diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts index 8596606..9d95957 100644 --- a/packages/blog-logic/interfaces.ts +++ b/packages/blog-logic/interfaces.ts @@ -17,7 +17,7 @@ export interface IVotesFilters extends ICommonFilters { readonly limit: number; readonly votesSort: "by_comment_voter" | "by_voter_comment"; } -export interface IPostCommentsFilters extends ICommonFilters { +export interface IPostFilters extends ICommonFilters { readonly limit: number; readonly sort: "trending" | "hot" | "created" | "promoted" | "payout" | "payout_comments" | "muted"; readonly startAuthor: string; @@ -149,9 +149,11 @@ export interface IActiveBloggingPlatform { export interface IBloggingPlatform { viewerContext: IAccountIdentity; getPost(postId: IPostCommentIdentity): Promise; - enumPosts(filter: IPostCommentsFilters, pagination: IPagination): Promise>; + enumPosts(filter: IPostFilters, pagination: IPagination): Promise>; configureViewContext(accontName: IAccountIdentity): void; enumCommunities(filter: ICommunityFilters, pagination: IPagination): Promise>; + getAccount(accontName: string): Promise; + // To do: add getAccount method later overwrittenGetTitleImage?: () => string; -- GitLab From 3f5183576e2320f55eaa09840fee653a1b93f700 Mon Sep 17 00:00:00 2001 From: jlachor Date: Tue, 14 Oct 2025 14:17:02 +0200 Subject: [PATCH 11/15] Implement parrent comment to Vote class --- packages/blog-logic/Vote.ts | 2 ++ packages/blog-logic/interfaces.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/packages/blog-logic/Vote.ts b/packages/blog-logic/Vote.ts index e55154e..74a8ae1 100644 --- a/packages/blog-logic/Vote.ts +++ b/packages/blog-logic/Vote.ts @@ -3,6 +3,7 @@ import { DataProvider } from "./DataProvider"; import { IPostCommentIdentity, IVote } from "./interfaces"; export class Vote implements IVote { + public parentComment: IPostCommentIdentity; public upvote: boolean; public voter: string; public weight: number; @@ -13,5 +14,6 @@ export class Vote implements IVote { this.upvote = Number(voteData.weight) > 0 this.voter = voteData.voter; this.weight = Number(voteData.weight); + this.parentComment = {author: voteData.author, permlink: voteData.permlink}; } } diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts index 9d95957..2742f2c 100644 --- a/packages/blog-logic/interfaces.ts +++ b/packages/blog-logic/interfaces.ts @@ -50,6 +50,7 @@ export interface IVote { readonly weight: number; readonly upvote: boolean; readonly voter: string; + readonly parentComment: IPostCommentIdentity } export interface ICommunity extends ICommunityIdentity { -- GitLab From 4474f02a82a6bbb60b1e25bddc4b4f978021492e Mon Sep 17 00:00:00 2001 From: jlachor Date: Thu, 16 Oct 2025 13:46:51 +0200 Subject: [PATCH 12/15] Implement docs for Blog Logic --- packages/blog-logic/readme.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/blog-logic/readme.md diff --git a/packages/blog-logic/readme.md b/packages/blog-logic/readme.md new file mode 100644 index 0000000..9ca0111 --- /dev/null +++ b/packages/blog-logic/readme.md @@ -0,0 +1,29 @@ +# Blog Logic + +Blog Logic is a library that makes getting, keeping and preparing Hive's data for blogging much simpler process. You can use logical, carefully prepared interfaces to handle the blog data you need, like posts, replies, accounts and other. + +### Technical requirement + +Because in the future development we plan using the Workerbee for getting data, it's part of Workerbee's repo. Other things necessary for functionality of this library is Wax for data fetching. + +### Current data entities + +* Comments - parent class for both Posts and Replies. +* Posts - an equivalent of real live blog post, not a reply for any existing one. +* Reply - in nested structure. It can be a direct reply to other reply. We keep identification of top post for any Reply. +* Vote - representation of single vote for given comment. +* Community - for Hive's community with its details. +* Account - details about an user or the author of given post. + +### Data Provider + +The class to feed all the other classes with data is Data Provider. Its implementation is necessary for proper working of the rest of classes. It is responsible for fetching and caching all the data required by other entities. Then they can ask Data Provider for data and map then into their own interfaces. + +The implementation of Data Provider is quite elastic. In the future we're going to use Workerbee there and some system to cache data in a better way. + +### How to use + +1. Import files or interfaces you want to use. You can just start with current version of Data Provider. +2. Create new object of Data Provider class, putting Wax's chain into contructor. +3. Use Data Provider's Blogging Platform class to get any data you want, preprepared for the nice, logical interface. You can call their methods and they'll help you with managing blog data. + -- GitLab From 0be4297b696e8f5de0344a9d474f98efea625b64 Mon Sep 17 00:00:00 2001 From: jlachor Date: Tue, 21 Oct 2025 10:28:49 +0200 Subject: [PATCH 13/15] Better formatting in Data Provider --- packages/blog-logic/DataProvider.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/blog-logic/DataProvider.ts b/packages/blog-logic/DataProvider.ts index debb0fd..284f604 100644 --- a/packages/blog-logic/DataProvider.ts +++ b/packages/blog-logic/DataProvider.ts @@ -132,7 +132,13 @@ export class DataProvider { } public async enumVotes(commentId: IPostCommentIdentity, filter: IVotesFilters, pagination: IPagination): Promise { - const votesData = (await this.chain!.api.database_api.list_votes({limit: filter.limit, order: filter.votesSort, start: [commentId.author, commentId.permlink, ""]})).votes; + const votesData = ( + await this.chain!.api.database_api.list_votes({ + limit: filter.limit, + order: filter.votesSort, + start: [commentId.author, commentId.permlink, ""], + }) + ).votes; const votersForComment: string[] = []; const votesByVoters: Map = new Map(); votesData.forEach((voteData) => { -- GitLab From cabd7d3f38e847cd2831bae3843c2445b69d7e09 Mon Sep 17 00:00:00 2001 From: jlachor Date: Wed, 22 Oct 2025 14:09:19 +0200 Subject: [PATCH 14/15] Use jsonrpc API types --- package.json | 3 ++- packages/blog-logic/wax.ts | 5 ++++- pnpm-lock.yaml | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 258d6da..21ec54c 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,8 @@ "typedoc": "catalog:typedoc-toolset", "typedoc-gitlab-wiki-theme": "catalog:typedoc-toolset", "typedoc-plugin-markdown": "catalog:typedoc-toolset", - "typescript": "catalog:typescript-toolset" + "typescript": "catalog:typescript-toolset", + "@hiveio/wax-api-jsonrpc": "^1.27.12" }, "dependencies": { "@hiveio/wax": "1.27.6-rc10-stable.250804212246" diff --git a/packages/blog-logic/wax.ts b/packages/blog-logic/wax.ts index 9b48bca..cd0f472 100644 --- a/packages/blog-logic/wax.ts +++ b/packages/blog-logic/wax.ts @@ -6,6 +6,9 @@ import { NaiAsset, TWaxRestExtended } from "@hiveio/wax"; +import WaxExtendedData from '@hiveio/wax-api-jsonrpc'; + + export interface AccountProfile { about?: string; @@ -328,7 +331,7 @@ let chain: Promise { if (!chain) - return chain = createHiveChain().then(chain => chain.extend().extendRest({})); + return chain = createHiveChain().then(chain => chain.extend(WaxExtendedData).extendRest({})); return chain; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c37730c..beb4881 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: '@hiveio/beekeeper': specifier: 1.27.11 version: 1.27.11 + '@hiveio/wax-api-jsonrpc': + specifier: ^1.27.12 + version: 1.27.12 '@playwright/test': specifier: catalog:playwright-toolset version: 1.50.1 @@ -417,6 +420,9 @@ packages: resolution: {integrity: sha1-0v2gSYFhN5MLQzBHIYikUyksu/k=, tarball: https://gitlab.syncad.com/api/v4/projects/198/packages/npm/@hiveio/beekeeper/-/@hiveio/beekeeper-1.27.11.tgz} engines: {node: ^20.11 || >= 21.2} + '@hiveio/wax-api-jsonrpc@1.27.12': + resolution: {integrity: sha512-yHYr9DAVwMz9FkKFt7jZtbwX/oYzTcr+pP2JY1BfqOPekNcV5/zJYSh+AsKlw0U14HSGiknMa5lS8SbrM6Lorg==} + '@hiveio/wax@1.27.6-rc10-stable.250804212246': resolution: {integrity: sha1-qZYJ4ot0Lgy3C0fbeS1WFoxmFEY=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax/-/@hiveio/wax-1.27.6-rc10-stable.250804212246.tgz} engines: {node: ^20.11 || >= 21.2} @@ -3213,6 +3219,8 @@ snapshots: '@hiveio/beekeeper@1.27.11': {} + '@hiveio/wax-api-jsonrpc@1.27.12': {} + '@hiveio/wax@1.27.6-rc10-stable.250804212246': dependencies: events: 3.3.0 -- GitLab From 0188b1348b378947b767c7615d991a3eb0e0a305 Mon Sep 17 00:00:00 2001 From: jlachor Date: Thu, 23 Oct 2025 11:47:07 +0200 Subject: [PATCH 15/15] Use chain types from external library --- .../wordpress-rest-api/wordpress-rest-api.ts | 7 + package.json | 3 +- packages/blog-logic/.npmrc | 1 + packages/blog-logic/Comment.ts | 4 - packages/blog-logic/DataProvider.ts | 28 +- packages/blog-logic/interfaces.ts | 3 - packages/blog-logic/package.json | 16 + packages/blog-logic/wax.ts | 329 +----------------- pnpm-lock.yaml | 38 +- 9 files changed, 77 insertions(+), 352 deletions(-) create mode 120000 packages/blog-logic/.npmrc create mode 100644 packages/blog-logic/package.json diff --git a/examples/wordpress-rest-api/wordpress-rest-api.ts b/examples/wordpress-rest-api/wordpress-rest-api.ts index 3f99663..fc207cc 100644 --- a/examples/wordpress-rest-api/wordpress-rest-api.ts +++ b/examples/wordpress-rest-api/wordpress-rest-api.ts @@ -114,6 +114,13 @@ apiRouter.get("/comments", async (req: Request, res: Response) => { } }); +apiRouter.get("/test-votes", async (req: Request, res: Response) => { + const voteTestId = {author: "theycallmedan", permlink: "i-have-returned"}; + const post = await dataProvider.bloggingPlatform.getPost(voteTestId); + const votes = await post.enumVotes({limit: 1000, votesSort: "by_comment_voter"}, {page: 1, pageSize: 1000}); + res.json(votes); +}) + apiRouter.get("/tags", (req: Request, res: Response) => { res.json([]); }); diff --git a/package.json b/package.json index 21ec54c..258d6da 100644 --- a/package.json +++ b/package.json @@ -82,8 +82,7 @@ "typedoc": "catalog:typedoc-toolset", "typedoc-gitlab-wiki-theme": "catalog:typedoc-toolset", "typedoc-plugin-markdown": "catalog:typedoc-toolset", - "typescript": "catalog:typescript-toolset", - "@hiveio/wax-api-jsonrpc": "^1.27.12" + "typescript": "catalog:typescript-toolset" }, "dependencies": { "@hiveio/wax": "1.27.6-rc10-stable.250804212246" diff --git a/packages/blog-logic/.npmrc b/packages/blog-logic/.npmrc new file mode 120000 index 0000000..cba44bb --- /dev/null +++ b/packages/blog-logic/.npmrc @@ -0,0 +1 @@ +../../.npmrc \ No newline at end of file diff --git a/packages/blog-logic/Comment.ts b/packages/blog-logic/Comment.ts index b41bdb5..ffafe95 100644 --- a/packages/blog-logic/Comment.ts +++ b/packages/blog-logic/Comment.ts @@ -1,13 +1,9 @@ -import { TWaxExtended } from "@hiveio/wax"; import { DataProvider } from "./DataProvider"; import { IComment, IPagination, IPostCommentIdentity, IVote, IVotesFilters } from "./interfaces"; import { Vote } from "./Vote"; -import { ExtendedNodeApi } from "./wax"; export class Comment implements IComment { - protected chain?: TWaxExtended - public author: string; public permlink: string; public publishedAt: Date; diff --git a/packages/blog-logic/DataProvider.ts b/packages/blog-logic/DataProvider.ts index 284f604..57c6a75 100644 --- a/packages/blog-logic/DataProvider.ts +++ b/packages/blog-logic/DataProvider.ts @@ -1,26 +1,27 @@ -import { TWaxExtended, TWaxRestExtended } from "@hiveio/wax"; +import {HafbeTypesAccount} from "@hiveio/wax-api-hafbe" +import {Community as CommunityData, PostBridgeApi, ActiveVotesDatabaseApi} from "@hiveio/wax-api-jsonrpc"; import { WorkerBeeError } from "../../src/errors"; import { BloggingPlaform } from "./BloggingPlatform"; import { ICommonFilters, ICommunityFilters, IPagination, IPostCommentIdentity, IPostFilters, IVotesFilters } from "./interfaces"; import { paginateData } from "./utils"; -import { AccountDetails, CommunityData, Entry, ExtendedNodeApi, ExtendedRestApi, IVoteListItem } from "./wax"; +import { WaxExtendedChain } from "./wax"; /** * Main class to call all of Blog Logic. The class is responsible for making instances of Blog Logic's objects and * getting and caching all the necessary data for them. */ export class DataProvider { - public chain: TWaxExtended>; + public chain: WaxExtendedChain; public bloggingPlatform: BloggingPlaform; - private comments: Map = new Map(); + private comments: Map = new Map(); private repliesByPostId: Map = new Map(); - private accounts: Map = new Map(); + private accounts: Map = new Map(); private communities: Map = new Map(); - private votesByCommentsAndVoter: Map> = new Map(); + private votesByCommentsAndVoter: Map> = new Map(); - public constructor(chain: TWaxExtended>) { + public constructor(chain: WaxExtendedChain) { this.chain = chain; this.bloggingPlatform = new BloggingPlaform(this); } @@ -32,7 +33,7 @@ export class DataProvider { return `${commentId.author}_${commentId.permlink}` } - public getComment(postId: IPostCommentIdentity): Entry | null { + public getComment(postId: IPostCommentIdentity): PostBridgeApi | null { return this.comments.get(this.convertCommentIdToHash(postId)) || null; } @@ -49,11 +50,8 @@ export class DataProvider { public async enumPosts(filter: IPostFilters, pagination: IPagination): Promise { const posts = await this.chain.api.bridge.get_ranked_posts({ - limit: filter.limit, sort: filter.sort, observer: this.bloggingPlatform.viewerContext.name, - start_author: filter.startAuthor, - start_permlink: filter.startPermlink, tag: filter.tag }); if (!posts) @@ -92,12 +90,12 @@ export class DataProvider { return paginateData(filteredReplies, pagination).map((reply) => ({author: reply.author, permlink: reply.permlink})); } - public getAccount(accountName: string): AccountDetails | null { + public getAccount(accountName: string): HafbeTypesAccount | null { return this.accounts.get(accountName) || null; } public async fetchAccount(accountName: string): Promise { - const account = await this.chain.restApi["hafbe-api"].accounts.account({accountName: accountName}); + const account = await this.chain.restApi.hafbeApi.accounts.accountName({accountName: accountName}); if (!account) throw new Error("Account not found"); this.accounts.set(accountName, account); @@ -121,7 +119,7 @@ export class DataProvider { return paginateData(communitiesNames, pagination); } - public getVote(commentId: IPostCommentIdentity, voter: string): IVoteListItem | null { + public getVote(commentId: IPostCommentIdentity, voter: string): ActiveVotesDatabaseApi | null { return this.votesByCommentsAndVoter.get(this.convertCommentIdToHash(commentId))?.get(voter) || null; } @@ -140,7 +138,7 @@ export class DataProvider { }) ).votes; const votersForComment: string[] = []; - const votesByVoters: Map = new Map(); + const votesByVoters: Map = new Map(); votesData.forEach((voteData) => { votersForComment.push(voteData.voter); votesByVoters.set(voteData.voter, voteData); diff --git a/packages/blog-logic/interfaces.ts b/packages/blog-logic/interfaces.ts index 2742f2c..4885020 100644 --- a/packages/blog-logic/interfaces.ts +++ b/packages/blog-logic/interfaces.ts @@ -18,10 +18,7 @@ export interface IVotesFilters extends ICommonFilters { readonly votesSort: "by_comment_voter" | "by_voter_comment"; } export interface IPostFilters extends ICommonFilters { - readonly limit: number; readonly sort: "trending" | "hot" | "created" | "promoted" | "payout" | "payout_comments" | "muted"; - readonly startAuthor: string; - readonly startPermlink: string; readonly tag: string; } diff --git a/packages/blog-logic/package.json b/packages/blog-logic/package.json new file mode 100644 index 0000000..31a272e --- /dev/null +++ b/packages/blog-logic/package.json @@ -0,0 +1,16 @@ +{ + "name": "hive-blog-logic", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "private": false, + "contributors": [], + "dependencies": { + "@hiveio/wax": "1.27.6-rc10-stable.250804212246", + "@hiveio/wax-api-hafbe": "1.27.12", + "@hiveio/wax-api-jsonrpc": "^1.27.12" + }, + "devDependencies": { + "tsx": "^4.20.5" + } +} diff --git a/packages/blog-logic/wax.ts b/packages/blog-logic/wax.ts index cd0f472..7f81b41 100644 --- a/packages/blog-logic/wax.ts +++ b/packages/blog-logic/wax.ts @@ -1,337 +1,18 @@ import { createHiveChain, TWaxExtended, - TWaxApiRequest, - ApiAuthority, - NaiAsset, TWaxRestExtended } from "@hiveio/wax"; -import WaxExtendedData from '@hiveio/wax-api-jsonrpc'; +import HafbeExtendedData from "@hiveio/wax-api-hafbe"; +import WaxExtendedData from "@hiveio/wax-api-jsonrpc"; +export type WaxExtendedChain = TWaxExtended>; - -export interface AccountProfile { - about?: string; - cover_image?: string; - location?: string; - blacklist_description?: string; - muted_list_description?: string; - name?: string; - profile_image?: string; - website?: string; - pinned?: string; - witness_description?: string; - witness_owner?: string; -} -export interface AccountFollowStats { - follower_count: number; - following_count: number; - account: string; -} - -export interface FullAccount { - vesting_balance: string | NaiAsset; - name: string; - owner: ApiAuthority; - active: ApiAuthority; - posting: ApiAuthority; - memo_key: string; - post_count: number; - created: string; - reputation: string | number; - json_metadata: string; - posting_json_metadata: string; - last_vote_time: string; - last_post: string; - reward_hbd_balance: string; - reward_vesting_hive: string; - reward_hive_balance: string; - reward_vesting_balance: string; - governance_vote_expiration_ts: string; - balance: string; - vesting_shares: string; - hbd_balance: string; - savings_balance: string; - savings_hbd_balance: string; - savings_hbd_seconds: string; - savings_hbd_last_interest_payment: string; - savings_hbd_seconds_last_update: string; - next_vesting_withdrawal: string; - delegated_vesting_shares: string; - received_vesting_shares: string; - vesting_withdraw_rate: string; - to_withdraw: number; - withdrawn: number; - witness_votes: string[]; - proxy: string; - proxied_vsf_votes: number[] | string[]; - voting_manabar: { - current_mana: string | number; - last_update_time: number; - }; - voting_power: number; - downvote_manabar: { - current_mana: string | number; - last_update_time: number; - }; - profile?: AccountProfile; - follow_stats?: AccountFollowStats; - __loaded?: true; - proxyVotes?: Array; - // Temporary properties for UI purposes - _temporary?: boolean; -} - - -export interface IGetPostHeader { - author: string; - permlink: string; - category: string; - depth: number; -} - -export interface JsonMetadata { - image: string[]; - links?: string[]; - flow?: { - pictures: { - caption: string; - id: number; - mime: string; - name: string; - tags: string[]; - url: string; - }[]; - tags: string[]; - }; - images: string[]; - author: string | undefined; - tags?: string[]; - description?: string | null; - app?: string; - canonical_url?: string; - format?: string; - original_author?: string; - original_permlink?: string; - summary?: string; -} - -export interface EntryVote { - voter: string; - rshares: number; -} - -export interface EntryBeneficiaryRoute { - account: string; - weight: number; -} - -export interface EntryStat { - flag_weight: number; - gray: boolean; - hide: boolean; - total_votes: number; - is_pinned?: boolean; -} - - -export interface Entry { - active_votes: EntryVote[]; - author: string; - author_payout_value: string; - author_reputation: number; - author_role?: string; - author_title?: string; - beneficiaries: EntryBeneficiaryRoute[]; - blacklists: string[]; - body: string; - category: string; - children: number; - community?: string; - community_title?: string; - created: string; - total_votes?: number; - curator_payout_value: string; - depth: number; - is_paidout: boolean; - json_metadata: JsonMetadata; - max_accepted_payout: string; - net_rshares: number; - parent_author?: string; - parent_permlink?: string; - payout: number; - payout_at: string; - pending_payout_value: string; - percent_hbd: number; - permlink: string; - post_id: number; - id?: number; - promoted: string; - reblogged_by?: string[]; - replies: Array; - stats?: EntryStat; - title: string; - updated: string; - url: string; - original_entry?: Entry; -} - -export interface VoteData { - percent: number; - reputation: number; - rshares: number; - time: string; - timestamp?: number; - voter: string; - weight: number; - reward?: number; -} - -export interface CommunityData { - about: string; - admins?: string[]; - avatar_url: string; - created_at: string; - description: string; - flag_text: string; - id: number; - is_nsfw: boolean; - lang: string; - name: string; - num_authors: number; - num_pending: number; - subscribers: number; - sum_pending: number; - settings?: object; - team: string[][]; - title: string; - type_id: number; - context: { - role: string; - subscribed: boolean; - title: string; - _temporary?: boolean; - }; - _temporary?: boolean; -} - -export interface IVoteListItem { - id: number; - voter: string; - author: string; - permlink: string; - weight: string; - rshares: number; - vote_percent: number; - last_update: string; - num_changes: number; -} - -export interface AccountDetails { - id: number; - name: string; - can_vote: boolean; - mined: boolean; - proxy: string; - recovery_account: string; - last_account_recovery: Date; - created: Date; - reputation: number; - json_metadata: string; - posting_json_metadata: string; - profile_image: string; - hbd_balance: number; - balance: number; - vesting_shares: string; - vesting_balance: number; - hbd_saving_balance: number; - savings_balance: number; - savings_withdraw_requests: number; - reward_hbd_balance: number; - reward_hive_balance: number; - reward_vesting_balance: string; - reward_vesting_hive: number; - posting_rewards: string; - curation_rewards: string; - delegated_vesting_shares: string; - received_vesting_shares: string; - proxied_vsf_votes: number[] | string[]; - withdrawn: string; - vesting_withdraw_rate: string; - to_withdraw: string; - withdraw_routes: number; - delayed_vests: string; - witness_votes: string[]; - witnesses_voted_for: number; - ops_count: number; - is_witness: boolean; - governanceTs: any; -} - -export type ExtendedNodeApi = { - bridge: { - get_post_header: TWaxApiRequest<{ author: string; permlink: string }, IGetPostHeader>; - get_post: TWaxApiRequest<{ author: string; permlink: string; observer: string }, Entry | null>; - get_discussion: TWaxApiRequest< - { author: string; permlink: string; observer?: string }, - Record | null - >; - get_ranked_posts: TWaxApiRequest< - { - sort: string; - tag: string; - start_author: string; - start_permlink: string; - limit: number; - observer: string; - }, - Entry[] | null - >; - get_account_posts: TWaxApiRequest< - { - sort: string; - account: string; - start_author: string; - start_permlink: string; - limit: number; - observer: string; - }, - Entry[] | null - >; - list_communities: TWaxApiRequest< - { sort: string; query?: string | null; observer: string }, - CommunityData[] | null - >; - }; - database_api: { - list_votes: TWaxApiRequest< - { - start: [string, string, string] | null; - limit: number; - order: "by_comment_voter" | "by_voter_comment"; - }, - { votes: IVoteListItem[] } - >; - } -}; - -export type ExtendedRestApi = { - "hafbe-api": { - accounts: { - account: { - params: { accountName: string }; - result: AccountDetails, - urlPath: "{accountName}", - }, - } - } -}; - -let chain: Promise>>; +let chain: Promise; export const getWax = () => { if (!chain) - return chain = createHiveChain().then(chain => chain.extend(WaxExtendedData).extendRest({})); + return chain = createHiveChain().then(chain => chain.extend(WaxExtendedData).extendRest(HafbeExtendedData)); return chain; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index beb4881..cb1c9d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,9 +87,6 @@ importers: '@hiveio/beekeeper': specifier: 1.27.11 version: 1.27.11 - '@hiveio/wax-api-jsonrpc': - specifier: ^1.27.12 - version: 1.27.12 '@playwright/test': specifier: catalog:playwright-toolset version: 1.50.1 @@ -193,6 +190,22 @@ importers: specifier: catalog:typescript-toolset version: 5.7.3 + packages/blog-logic: + dependencies: + '@hiveio/wax': + specifier: 1.27.6-rc10-stable.250804212246 + version: 1.27.6-rc10-stable.250804212246 + '@hiveio/wax-api-hafbe': + specifier: 1.27.12 + version: 1.27.12 + '@hiveio/wax-api-jsonrpc': + specifier: ^1.27.12 + version: 1.27.12 + devDependencies: + tsx: + specifier: ^4.20.5 + version: 4.20.6 + packages: '@aashutoshrathi/word-wrap@1.2.6': @@ -420,8 +433,11 @@ packages: resolution: {integrity: sha1-0v2gSYFhN5MLQzBHIYikUyksu/k=, tarball: https://gitlab.syncad.com/api/v4/projects/198/packages/npm/@hiveio/beekeeper/-/@hiveio/beekeeper-1.27.11.tgz} engines: {node: ^20.11 || >= 21.2} + '@hiveio/wax-api-hafbe@1.27.12': + resolution: {integrity: sha1-2cLBu1vRPRoeIeyLKizlkuWcZnI=, tarball: https://gitlab.syncad.com/api/v4/projects/358/packages/npm/@hiveio/wax-api-hafbe/-/@hiveio/wax-api-hafbe-1.27.12.tgz} + '@hiveio/wax-api-jsonrpc@1.27.12': - resolution: {integrity: sha512-yHYr9DAVwMz9FkKFt7jZtbwX/oYzTcr+pP2JY1BfqOPekNcV5/zJYSh+AsKlw0U14HSGiknMa5lS8SbrM6Lorg==} + resolution: {integrity: sha1-2zVZzJHNCBIfPZI3qC1xa7gj0v0=, tarball: https://gitlab.syncad.com/api/v4/projects/198/packages/npm/@hiveio/wax-api-jsonrpc/-/@hiveio/wax-api-jsonrpc-1.27.12.tgz} '@hiveio/wax@1.27.6-rc10-stable.250804212246': resolution: {integrity: sha1-qZYJ4ot0Lgy3C0fbeS1WFoxmFEY=, tarball: https://gitlab.syncad.com/api/v4/projects/419/packages/npm/@hiveio/wax/-/@hiveio/wax-1.27.6-rc10-stable.250804212246.tgz} @@ -2870,6 +2886,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + tuf-js@1.1.7: resolution: {integrity: sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3219,6 +3240,8 @@ snapshots: '@hiveio/beekeeper@1.27.11': {} + '@hiveio/wax-api-hafbe@1.27.12': {} + '@hiveio/wax-api-jsonrpc@1.27.12': {} '@hiveio/wax@1.27.6-rc10-stable.250804212246': @@ -6168,6 +6191,13 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tsx@4.20.6: + dependencies: + esbuild: 0.25.9 + get-tsconfig: 4.10.0 + optionalDependencies: + fsevents: 2.3.3 + tuf-js@1.1.7: dependencies: '@tufjs/models': 1.0.4 -- GitLab