From 0d8239b39d6641e1e3d147e02f4b21db84dfdde6 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Thu, 2 Jan 2025 19:38:42 +0900 Subject: [PATCH] More accurate shares/likes count --- CHANGES.md | 9 +++++++ package.json | 2 +- pnpm-lock.yaml | 30 ++++++++++++++++++++-- src/federation/account.ts | 2 +- src/federation/inbox.ts | 37 +++++++++++++++++++-------- src/federation/post.ts | 54 ++++++++++++++++++++++++++++++++------- src/pages/accounts.tsx | 2 +- 7 files changed, 112 insertions(+), 24 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index bf35f8fd..9c5e7851 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,15 @@ Version 0.5.0 To be released. + - The number of shares and likes became more accurate. + + - The `Note` objects now have `shares` and `likes` collections with + their `totalItems` numbers. + - When a remote `Note` is persisted, now the `totalItems` numbers of + `shares` and `likes` are also persisted. + - When a `Announce(Note)` or `Undo(Announce(Note))` activity is received, + now it is forwarded to the followers as well if the activity is signed. + Version 0.4.2 ------------- diff --git a/package.json b/package.json index 933b6588..8b5d86fa 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ }, "dependencies": { "@aws-sdk/credential-providers": "^3.716.0", - "@fedify/fedify": "^1.3.3", + "@fedify/fedify": "1.4.0-dev.599", "@fedify/markdown-it-hashtag": "0.2.0", "@fedify/markdown-it-mention": "^0.1.1", "@fedify/postgres": "^0.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9913e681..7a74f903 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^3.716.0 version: 3.716.0(@aws-sdk/client-sso-oidc@3.716.0(@aws-sdk/client-sts@3.716.0)) '@fedify/fedify': - specifier: ^1.3.3 - version: 1.3.3(web-streams-polyfill@3.3.3) + specifier: 1.4.0-dev.599 + version: 1.4.0-dev.599(web-streams-polyfill@3.3.3) '@fedify/markdown-it-hashtag': specifier: 0.2.0 version: 0.2.0 @@ -931,6 +931,10 @@ packages: resolution: {integrity: sha512-8kIcv6TBvTFzpIo+XR9vJtvIFUPK7ZL9NkKO6Vvxzpk8MzGfc+VzfYwephrwgzV32ibynKeJeA0s8n9kzPD8+w==} engines: {bun: '>=1.1.0', deno: '>=2.0.0', node: '>=20.0.0'} + '@fedify/fedify@1.4.0-dev.599': + resolution: {integrity: sha512-irbEcD6MjqG9I64modPl8xUpWbFK5TvcGnIY+QHrQ0ADkGU540HKgPxYWTsV4MVs51qs7fkKM7Fr1eM85eVysQ==} + engines: {bun: '>=1.1.0', deno: '>=2.0.0', node: '>=20.0.0'} + '@fedify/markdown-it-hashtag@0.2.0': resolution: {integrity: sha512-kg8LKcSzemcG356ZBjfV2WyRAIqUbZ1t1XTRgYOlmjv1HNrMaKQiQc6I2zYtZIb+8LKAJbDld7ZScVLCE/RSWQ==} @@ -3651,6 +3655,28 @@ snapshots: transitivePeerDependencies: - web-streams-polyfill + '@fedify/fedify@1.4.0-dev.599(web-streams-polyfill@3.3.3)': + dependencies: + '@deno/shim-crypto': 0.3.1 + '@deno/shim-deno': 0.18.2 + '@hugoalh/http-header-link': 1.0.3 + '@js-temporal/polyfill': 0.4.4 + '@logtape/logtape': 0.8.0 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + '@phensley/language-tag': 1.9.2 + asn1js: 3.0.5 + json-canon: 1.0.1 + jsonld: 8.3.3(web-streams-polyfill@3.3.3) + multibase: 4.0.6 + multicodec: 3.2.1 + pkijs: 3.2.4 + uri-template-router: 0.0.16 + url-template: 3.1.1 + urlpattern-polyfill: 10.0.0 + transitivePeerDependencies: + - web-streams-polyfill + '@fedify/markdown-it-hashtag@0.2.0': dependencies: markdown-it: 14.1.0 diff --git a/src/federation/account.ts b/src/federation/account.ts index 87b548c8..2c2bb3e4 100644 --- a/src/federation/account.ts +++ b/src/federation/account.ts @@ -217,7 +217,7 @@ export async function persistAccountPosts( typeof schema, ExtractTablesWithRelations >, - account: schema.Account, + account: schema.Account & { owner: schema.AccountOwner | null }, fetchPosts: number, baseUrl: URL | string, options: { diff --git a/src/federation/inbox.ts b/src/federation/inbox.ts index ad1471ad..91fa4177 100644 --- a/src/federation/inbox.ts +++ b/src/federation/inbox.ts @@ -416,7 +416,7 @@ export async function onPostShared( ): Promise { const object = await announce.getObject(); if (!isPost(object)) return; - await db.transaction(async (tx) => { + const post = await db.transaction(async (tx) => { const post = await persistSharingPost( tx, announce, @@ -427,11 +427,19 @@ export async function onPostShared( if (post?.sharingId != null) { await updatePostStats(tx, { id: post.sharingId }); } + return post; }); + if (post?.sharing?.account?.owner != null) { + await ctx.forwardActivity( + { username: post.sharing.account.owner.handle }, + "followers", + { skipIfUnsigned: true }, + ); + } } export async function onPostUnshared( - _ctx: InboxContext, + ctx: InboxContext, undo: Undo, ): Promise { const object = await undo.getObject(); @@ -440,7 +448,14 @@ export async function onPostUnshared( const sharer = object.actorId; const originalPost = object.objectId; if (sharer == null || originalPost == null) return; - await db.transaction(async (tx) => { + const original = await db.transaction(async (tx) => { + const original = await tx.query.posts.findFirst({ + with: { + account: { with: { owner: true } }, + }, + where: eq(posts.iri, originalPost.href), + }); + if (original == null) return null; const deleted = await tx .delete(posts) .where( @@ -452,20 +467,22 @@ export async function onPostUnshared( .from(accounts) .where(eq(accounts.iri, sharer.href)), ), - eq( - posts.sharingId, - db - .select({ id: posts.id }) - .from(posts) - .where(eq(posts.iri, originalPost.href)), - ), + eq(posts.sharingId, original.id), ), ) .returning(); if (deleted.length > 0 && deleted[0].sharingId != null) { await updatePostStats(tx, { id: deleted[0].sharingId }); } + return original; }); + if (original?.account.owner != null) { + await ctx.forwardActivity( + { username: original.account.owner.handle }, + "followers", + { skipIfUnsigned: true }, + ); + } } export async function onPostPinned( diff --git a/src/federation/post.ts b/src/federation/post.ts index bf8af302..79984035 100644 --- a/src/federation/post.ts +++ b/src/federation/post.ts @@ -97,11 +97,17 @@ export async function persistPost( options: { contextLoader?: DocumentLoader; documentLoader?: DocumentLoader; - account?: Account; + account?: Account & { owner: AccountOwner | null }; replyTarget?: Post; skipUpdate?: boolean; } = {}, -): Promise<(Post & { mentions: Mention[] }) | null> { +): Promise< + | (Post & { + account: Account & { owner: AccountOwner | null }; + mentions: Mention[]; + }) + | null +> { if (object.id == null) return null; const existingPost = await db.query.posts.findFirst({ with: { account: { with: { owner: true } }, mentions: true }, @@ -218,6 +224,8 @@ export async function persistPost( const to = new Set(object.toIds.map((url) => url.href)); const cc = new Set(object.ccIds.map((url) => url.href)); const replies = await object.getReplies(options); + const shares = await object.getShares(options); + const likes = await object.getLikes(options); const previewLink = object.content == null ? null @@ -259,8 +267,8 @@ export async function persistPost( sensitive: object.sensitive ?? false, url: object.url instanceof Link ? object.url.href?.href : object.url?.href, repliesCount: replies?.totalItems ?? 0, - sharesCount: 0, // TODO - likesCount: 0, // TODO + sharesCount: shares?.totalItems ?? 0, + likesCount: likes?.totalItems ?? 0, published, updated, } as const; @@ -457,7 +465,7 @@ export async function persistPost( mentions: mentionRows, replyTarget: replyTargetObj, }); - return { ...post, mentions: mentionRows }; + return { ...post, account, mentions: mentionRows }; } export async function persistSharingPost( @@ -470,14 +478,25 @@ export async function persistSharingPost( object: ASPost, baseUrl: URL | string, options: { - account?: Account; + account?: Account & { owner: AccountOwner | null }; contextLoader?: DocumentLoader; documentLoader?: DocumentLoader; } = {}, -): Promise { +): Promise< + | (Post & { + account: Account & { owner: AccountOwner | null }; + sharing: + | (Post & { account: Account & { owner: AccountOwner | null } }) + | null; + }) + | null +> { if (announce.id == null) return null; const existingPost = await db.query.posts.findFirst({ - with: { account: { with: { owner: true } } }, + with: { + account: { with: { owner: true } }, + sharing: { with: { account: { with: { owner: true } } } }, + }, where: eq(posts.iri, announce.id.href), }); if (existingPost != null) return existingPost; @@ -531,7 +550,9 @@ export async function persistSharingPost( mentions: [], replyTarget: null, }); - return result[0] ?? null; + return result[0] == null + ? null + : { ...result[0], account, sharing: originalPost }; } export async function persistPollVote( @@ -774,9 +795,24 @@ export function toObject( replyTarget: post.replyTarget == null ? null : new URL(post.replyTarget.iri), replies: new OrderedCollection({ + id: new URL("#replies", post.iri), totalItems: post.replies.length, items: post.replies.map((r) => new URL(r.iri)), }), + shares: + post.sharesCount == null + ? null + : new Collection({ + id: new URL("#shares", post.iri), + totalItems: post.sharesCount, + }), + likes: + post.likesCount == null + ? null + : new Collection({ + id: new URL("#likes", post.iri), + totalItems: post.likesCount, + }), attachments: post.media.map((medium) => medium.type.startsWith("video/") ? new Video({ diff --git a/src/pages/accounts.tsx b/src/pages/accounts.tsx index 0d9d8fe9..633da9b8 100644 --- a/src/pages/accounts.tsx +++ b/src/pages/accounts.tsx @@ -176,7 +176,7 @@ accounts.post("/", async (c) => { await followAccount(tx, fedCtx, { ...account, owner }, following); await persistAccountPosts( tx, - account, + { ...account, owner }, REMOTE_ACTOR_FETCH_POSTS, c.req.url, {