From 3c3814a3c125536a45612b9f1e12facd02d2f63a Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 8 May 2024 11:51:47 -1000 Subject: [PATCH] fix(api): :bug: Fix favourited attribute not being correct on serialized API notes --- database/entities/Status.ts | 156 +++------------- package.json | 280 ++++++++++++++-------------- packages/database-interface/note.ts | 87 +++++---- 3 files changed, 213 insertions(+), 310 deletions(-) diff --git a/database/entities/Status.ts b/database/entities/Status.ts index 06806f48..4bff89cb 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -38,9 +38,7 @@ import type { Application } from "./Application"; import { attachmentFromLysand } from "./Attachment"; import { type EmojiWithInstance, fetchEmoji } from "./Emoji"; import { objectToInboxRequest } from "./Federation"; -import type { Like } from "./Like"; import { - type UserType, type UserWithInstance, type UserWithRelations, resolveWebFinger, @@ -57,7 +55,6 @@ export type StatusWithRelations = Status & { attachments: InferSelectModel[]; reblog: StatusWithoutRecursiveRelations | null; emojis: EmojiWithInstance[]; - likes: Like[]; reply: Status | null; quote: Status | null; application: Application | null; @@ -71,21 +68,6 @@ export type StatusWithoutRecursiveRelations = Omit< "reply" | "quote" | "reblog" >; -export const noteExtras = { - reblogCount: - sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes".id)`.as( - "reblog_count", - ), - likeCount: - sql`(SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes".id)`.as( - "like_count", - ), - replyCount: - sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes".id)`.as( - "reply_count", - ), -}; - /** * Wrapper against the Status object to make it easier to work with * @param query @@ -98,10 +80,7 @@ export const findManyNotes = async ( ...query, with: { ...query?.with, - attachments: { - where: (attachment, { eq }) => - eq(attachment.noteId, sql`"Notes"."id"`), - }, + attachments: true, emojis: { with: { emoji: { @@ -158,14 +137,36 @@ export const findManyNotes = async ( }, }, extras: { - ...noteExtras, + reblogCount: + sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes_reblog".id)`.as( + "reblog_count", + ), + likeCount: + sql`(SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes_reblog".id)`.as( + "like_count", + ), + replyCount: + sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes_reblog".id)`.as( + "reply_count", + ), }, }, reply: true, quote: true, }, extras: { - ...noteExtras, + reblogCount: + sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."reblogId" = "Notes".id)`.as( + "reblog_count", + ), + likeCount: + sql`(SELECT COUNT(*) FROM "Likes" WHERE "Likes"."likedId" = "Notes".id)`.as( + "like_count", + ), + replyCount: + sql`(SELECT COUNT(*) FROM "Notes" WHERE "Notes"."replyId" = "Notes".id)`.as( + "reply_count", + ), ...query?.extras, }, }); @@ -196,113 +197,6 @@ export const findManyNotes = async ( })); }; -export const findFirstNote = async ( - query: Parameters[0], -): Promise => { - const output = await db.query.Notes.findFirst({ - ...query, - with: { - ...query?.with, - attachments: { - where: (attachment, { eq }) => - eq(attachment.noteId, sql`"Notes"."id"`), - }, - emojis: { - with: { - emoji: { - with: { - instance: true, - }, - }, - }, - }, - author: { - with: { - ...userRelations, - }, - extras: userExtrasTemplate("Notes_author"), - }, - mentions: { - with: { - user: { - with: { - instance: true, - }, - }, - }, - }, - reblog: { - with: { - attachments: true, - emojis: { - with: { - emoji: { - with: { - instance: true, - }, - }, - }, - }, - likes: true, - application: true, - mentions: { - with: { - user: { - with: userRelations, - extras: userExtrasTemplate( - "Notes_reblog_mentions_user", - ), - }, - }, - }, - author: { - with: { - ...userRelations, - }, - extras: userExtrasTemplate("Notes_reblog_author"), - }, - }, - extras: { - ...noteExtras, - }, - }, - reply: true, - quote: true, - }, - extras: { - ...noteExtras, - ...query?.extras, - }, - }); - - if (!output) return null; - - return { - ...output, - author: transformOutputToUserWithRelations(output.author), - mentions: output.mentions.map((mention) => ({ - ...mention.user, - endpoints: mention.user.endpoints, - })), - emojis: (output.emojis ?? []).map((emoji) => emoji.emoji), - reblog: output.reblog && { - ...output.reblog, - author: transformOutputToUserWithRelations(output.reblog.author), - mentions: output.reblog.mentions.map((mention) => ({ - ...mention.user, - endpoints: mention.user.endpoints, - })), - emojis: (output.reblog.emojis ?? []).map((emoji) => emoji.emoji), - reblogCount: Number(output.reblog.reblogCount), - likeCount: Number(output.reblog.likeCount), - replyCount: Number(output.reblog.replyCount), - }, - reblogCount: Number(output.reblogCount), - likeCount: Number(output.likeCount), - replyCount: Number(output.replyCount), - }; -}; - export const resolveNote = async ( uri?: string, providedNote?: Lysand.Note, diff --git a/package.json b/package.json index 2fd6337d..d575b9db 100644 --- a/package.json +++ b/package.json @@ -1,143 +1,143 @@ { - "name": "lysand", - "module": "index.ts", - "type": "module", - "version": "0.5.0", - "description": "A project to build a federated social network", - "author": { - "email": "contact@cpluspatch.com", - "name": "CPlusPatch", - "url": "https://cpluspatch.com" - }, - "bugs": { - "url": "https://github.com/lysand-org/lysand/issues" - }, - "icon": "https://github.com/lysand-org/lysand", - "license": "AGPL-3.0-or-later", - "keywords": ["federated", "activitypub", "bun"], - "workspaces": ["packages/*"], - "maintainers": [ - { - "email": "contact@cpluspatch.com", - "name": "CPlusPatch", - "url": "https://cpluspatch.com" + "name": "lysand", + "module": "index.ts", + "type": "module", + "version": "0.5.0", + "description": "A project to build a federated social network", + "author": { + "email": "contact@cpluspatch.com", + "name": "CPlusPatch", + "url": "https://cpluspatch.com" + }, + "bugs": { + "url": "https://github.com/lysand-org/lysand/issues" + }, + "icon": "https://github.com/lysand-org/lysand", + "license": "AGPL-3.0-or-later", + "keywords": ["federated", "activitypub", "bun"], + "workspaces": ["packages/*"], + "maintainers": [ + { + "email": "contact@cpluspatch.com", + "name": "CPlusPatch", + "url": "https://cpluspatch.com" + } + ], + "repository": { + "type": "git", + "url": "git+https://github.com/lysand-org/lysand.git" + }, + "private": true, + "scripts": { + "dev": "bun run --hot index.ts", + "start": "NODE_ENV=production bun run dist/index.js --prod", + "lint": "bunx @biomejs/biome check .", + "build": "bun run build.ts", + "cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs,glitch,glitch-dev --exclude-ext sql,log,pem", + "wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-", + "cli": "bun run cli/index.ts", + "prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'" + }, + "trustedDependencies": [ + "@biomejs/biome", + "@fortawesome/fontawesome-common-types", + "@fortawesome/free-regular-svg-icons", + "@fortawesome/free-solid-svg-icons", + "es5-ext", + "esbuild", + "json-editor-vue", + "msgpackr-extract", + "nuxt-app", + "sharp", + "vue-demi" + ], + "oclif": { + "bin": "cli", + "dirname": "cli", + "commands": { + "strategy": "explicit", + "target": "./cli/index", + "identifier": "commands" + }, + "additionalHelpFlags": ["-h"], + "additionalVersionFlags": ["-v"], + "plugins": [], + "description": "CLI to interface with the Lysand project", + "topicSeparator": " ", + "topics": { + "user": { + "description": "Manage users" + } + }, + "theme": "./cli/theme.json", + "flexibleTaxonomy": true + }, + "devDependencies": { + "@biomejs/biome": "^1.7.0", + "@types/cli-progress": "^3.11.5", + "@types/cli-table": "^0.3.4", + "@types/html-to-text": "^9.0.4", + "@types/ioredis": "^5.0.0", + "@types/jsonld": "^1.5.13", + "@types/markdown-it-container": "^2.0.10", + "@types/mime-types": "^2.1.4", + "@types/pg": "^8.11.5", + "@types/qs": "^6.9.15", + "bun-types": "latest", + "drizzle-kit": "^0.20.14", + "oclif": "^4.10.4", + "ts-prune": "^0.10.3", + "typescript": "latest" + }, + "peerDependencies": { + "typescript": "^5.3.2" + }, + "dependencies": { + "@inquirer/confirm": "^3.1.6", + "@inquirer/input": "^2.1.6", + "@oclif/core": "^3.26.6", + "cli-progress": "^3.12.0", + "ora": "^8.0.1", + "table": "^6.8.2", + "uqr": "^0.1.2", + "@hackmd/markdown-it-task-lists": "^2.1.4", + "@hono/zod-validator": "^0.2.1", + "@json2csv/plainjs": "^7.0.6", + "@tufjs/canonical-json": "^2.0.0", + "blurhash": "^2.0.5", + "bullmq": "^5.7.1", + "chalk": "^5.3.0", + "cli-parser": "workspace:*", + "cli-table": "^0.3.11", + "config-manager": "workspace:*", + "drizzle-orm": "^0.30.7", + "extract-zip": "^2.0.1", + "hono": "^4.3.2", + "html-to-text": "^9.0.5", + "ioredis": "^5.3.2", + "ip-matching": "^2.1.2", + "iso-639-1": "^3.1.0", + "jose": "^5.2.4", + "linkify-html": "^4.1.3", + "linkify-string": "^4.1.3", + "linkifyjs": "^4.1.3", + "log-manager": "workspace:*", + "magic-regexp": "^0.8.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^8.6.7", + "markdown-it-container": "^4.0.0", + "markdown-it-toc-done-right": "^4.2.0", + "media-manager": "workspace:*", + "meilisearch": "^0.39.0", + "mime-types": "^2.1.35", + "oauth4webapi": "^2.4.0", + "pg": "^8.11.5", + "qs": "^6.12.1", + "sharp": "^0.33.3", + "string-comparison": "^1.3.0", + "stringify-entities": "^4.0.4", + "xss": "^1.0.15", + "zod": "^3.22.4", + "zod-validation-error": "^3.2.0" } - ], - "repository": { - "type": "git", - "url": "git+https://github.com/lysand-org/lysand.git" - }, - "private": true, - "scripts": { - "dev": "bun run --hot index.ts", - "start": "NODE_ENV=production bun run dist/index.js --prod", - "lint": "bunx @biomejs/biome check .", - "build": "bun run build.ts", - "cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs,glitch,glitch-dev --exclude-ext sql,log,pem", - "wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-", - "cli": "bun run cli/index.ts", - "prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'" - }, - "trustedDependencies": [ - "@biomejs/biome", - "@fortawesome/fontawesome-common-types", - "@fortawesome/free-regular-svg-icons", - "@fortawesome/free-solid-svg-icons", - "es5-ext", - "esbuild", - "json-editor-vue", - "msgpackr-extract", - "nuxt-app", - "sharp", - "vue-demi" - ], - "oclif": { - "bin": "cli", - "dirname": "cli", - "commands": { - "strategy": "explicit", - "target": "./cli/index", - "identifier": "commands" - }, - "additionalHelpFlags": ["-h"], - "additionalVersionFlags": ["-v"], - "plugins": [], - "description": "CLI to interface with the Lysand project", - "topicSeparator": " ", - "topics": { - "user": { - "description": "Manage users" - } - }, - "theme": "./cli/theme.json", - "flexibleTaxonomy": true - }, - "devDependencies": { - "@biomejs/biome": "^1.7.0", - "@types/cli-progress": "^3.11.5", - "@types/cli-table": "^0.3.4", - "@types/html-to-text": "^9.0.4", - "@types/ioredis": "^5.0.0", - "@types/jsonld": "^1.5.13", - "@types/markdown-it-container": "^2.0.10", - "@types/mime-types": "^2.1.4", - "@types/pg": "^8.11.5", - "@types/qs": "^6.9.15", - "bun-types": "latest", - "drizzle-kit": "^0.20.14", - "oclif": "^4.10.4", - "ts-prune": "^0.10.3", - "typescript": "latest" - }, - "peerDependencies": { - "typescript": "^5.3.2" - }, - "dependencies": { - "@inquirer/confirm": "^3.1.6", - "@inquirer/input": "^2.1.6", - "@oclif/core": "^3.26.6", - "cli-progress": "^3.12.0", - "ora": "^8.0.1", - "table": "^6.8.2", - "uqr": "^0.1.2", - "@hackmd/markdown-it-task-lists": "^2.1.4", - "@hono/zod-validator": "^0.2.1", - "@json2csv/plainjs": "^7.0.6", - "@tufjs/canonical-json": "^2.0.0", - "blurhash": "^2.0.5", - "bullmq": "^5.7.1", - "chalk": "^5.3.0", - "cli-parser": "workspace:*", - "cli-table": "^0.3.11", - "config-manager": "workspace:*", - "drizzle-orm": "^0.30.7", - "extract-zip": "^2.0.1", - "hono": "^4.3.2", - "html-to-text": "^9.0.5", - "ioredis": "^5.3.2", - "ip-matching": "^2.1.2", - "iso-639-1": "^3.1.0", - "jose": "^5.2.4", - "linkify-html": "^4.1.3", - "linkify-string": "^4.1.3", - "linkifyjs": "^4.1.3", - "log-manager": "workspace:*", - "magic-regexp": "^0.8.0", - "markdown-it": "^14.1.0", - "markdown-it-anchor": "^8.6.7", - "markdown-it-container": "^4.0.0", - "markdown-it-toc-done-right": "^4.2.0", - "media-manager": "workspace:*", - "meilisearch": "^0.39.0", - "mime-types": "^2.1.35", - "oauth4webapi": "^2.4.0", - "pg": "^8.11.5", - "qs": "^6.12.1", - "sharp": "^0.33.3", - "string-comparison": "^1.3.0", - "stringify-entities": "^4.0.4", - "xss": "^1.0.15", - "zod": "^3.22.4", - "zod-validation-error": "^3.2.0" - } } diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index 3a395734..191ec3bf 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -32,7 +32,6 @@ import { type Status, type StatusWithRelations, contentToHtml, - findFirstNote, findManyNotes, } from "~database/entities/Status"; import { db } from "~drizzle/db"; @@ -69,13 +68,14 @@ export class Note { sql: SQL | undefined, orderBy: SQL | undefined = desc(Notes.id), ) { - const found = await findFirstNote({ + const found = await findManyNotes({ where: sql, orderBy, + limit: 1, }); - if (!found) return null; - return new Note(found); + if (!found[0]) return null; + return new Note(found[0]); } static async manyFromSql( @@ -413,35 +413,47 @@ export class Note { async toAPI(userFetching?: User | null): Promise { const data = this.getStatus(); - const wasPinnedByUser = userFetching - ? !!(await db.query.UserToPinnedNotes.findFirst({ - where: (relation, { and, eq }) => - and( - eq(relation.noteId, data.id), - eq(relation.userId, userFetching?.id), - ), - })) - : false; - const wasRebloggedByUser = userFetching - ? !!(await Note.fromSql( - and( - eq(Notes.authorId, userFetching?.id), - eq(Notes.reblogId, data.id), - ), - )) - : false; - - const wasMutedByUser = userFetching - ? !!(await db.query.Relationships.findFirst({ - where: (relationship, { and, eq }) => - and( - eq(relationship.ownerId, userFetching.id), - eq(relationship.subjectId, data.authorId), - eq(relationship.muting, true), - ), - })) - : false; + const [pinnedByUser, rebloggedByUser, mutedByUser, likedByUser] = ( + await Promise.all([ + userFetching + ? db.query.UserToPinnedNotes.findFirst({ + where: (relation, { and, eq }) => + and( + eq(relation.noteId, data.id), + eq(relation.userId, userFetching?.id), + ), + }) + : false, + userFetching + ? Note.fromSql( + and( + eq(Notes.authorId, userFetching?.id), + eq(Notes.reblogId, data.id), + ), + ) + : false, + userFetching + ? db.query.Relationships.findFirst({ + where: (relationship, { and, eq }) => + and( + eq(relationship.ownerId, userFetching.id), + eq(relationship.subjectId, data.authorId), + eq(relationship.muting, true), + ), + }) + : false, + userFetching + ? db.query.Likes.findFirst({ + where: (like, { and, eq }) => + and( + eq(like.likedId, data.id), + eq(like.likerId, userFetching.id), + ), + }) + : false, + ]) + ).map((r) => !!r); // Convert mentions of local users from @username@host to @username const mentionedLocalUsers = data.mentions.filter( @@ -476,10 +488,7 @@ export class Note { card: null, content: replacedContent, emojis: data.emojis.map((emoji) => emojiToAPI(emoji)), - // FIXME: data.likes is always empty - favourited: !!(data.likes ?? []).find( - (like) => like.likerId === userFetching?.id, - ), + favourited: likedByUser, favourites_count: data.likeCount, media_attachments: (data.attachments ?? []).map( (a) => attachmentToAPI(a) as APIAttachment, @@ -495,8 +504,8 @@ export class Note { username: mention.username, })), language: null, - muted: wasMutedByUser, - pinned: wasPinnedByUser, + muted: mutedByUser, + pinned: pinnedByUser, // TODO: Add polls poll: null, reblog: data.reblog @@ -504,7 +513,7 @@ export class Note { data.reblog as StatusWithRelations, ).toAPI(userFetching) : null, - reblogged: wasRebloggedByUser, + reblogged: rebloggedByUser, reblogs_count: data.reblogCount, replies_count: data.replyCount, sensitive: data.sensitive,