fix(api): 🐛 Fix favourited attribute not being correct on serialized API notes

This commit is contained in:
Jesse Wierzbinski 2024-05-08 11:51:47 -10:00
parent 5fcbcd0f07
commit 3c3814a3c1
No known key found for this signature in database
3 changed files with 213 additions and 310 deletions

View file

@ -38,9 +38,7 @@ import type { Application } from "./Application";
import { attachmentFromLysand } from "./Attachment"; import { attachmentFromLysand } from "./Attachment";
import { type EmojiWithInstance, fetchEmoji } from "./Emoji"; import { type EmojiWithInstance, fetchEmoji } from "./Emoji";
import { objectToInboxRequest } from "./Federation"; import { objectToInboxRequest } from "./Federation";
import type { Like } from "./Like";
import { import {
type UserType,
type UserWithInstance, type UserWithInstance,
type UserWithRelations, type UserWithRelations,
resolveWebFinger, resolveWebFinger,
@ -57,7 +55,6 @@ export type StatusWithRelations = Status & {
attachments: InferSelectModel<typeof Attachments>[]; attachments: InferSelectModel<typeof Attachments>[];
reblog: StatusWithoutRecursiveRelations | null; reblog: StatusWithoutRecursiveRelations | null;
emojis: EmojiWithInstance[]; emojis: EmojiWithInstance[];
likes: Like[];
reply: Status | null; reply: Status | null;
quote: Status | null; quote: Status | null;
application: Application | null; application: Application | null;
@ -71,21 +68,6 @@ export type StatusWithoutRecursiveRelations = Omit<
"reply" | "quote" | "reblog" "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 * Wrapper against the Status object to make it easier to work with
* @param query * @param query
@ -98,10 +80,7 @@ export const findManyNotes = async (
...query, ...query,
with: { with: {
...query?.with, ...query?.with,
attachments: { attachments: true,
where: (attachment, { eq }) =>
eq(attachment.noteId, sql`"Notes"."id"`),
},
emojis: { emojis: {
with: { with: {
emoji: { emoji: {
@ -158,14 +137,36 @@ export const findManyNotes = async (
}, },
}, },
extras: { 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, reply: true,
quote: true, quote: true,
}, },
extras: { 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, ...query?.extras,
}, },
}); });
@ -196,113 +197,6 @@ export const findManyNotes = async (
})); }));
}; };
export const findFirstNote = async (
query: Parameters<typeof db.query.Notes.findFirst>[0],
): Promise<StatusWithRelations | null> => {
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 ( export const resolveNote = async (
uri?: string, uri?: string,
providedNote?: Lysand.Note, providedNote?: Lysand.Note,

View file

@ -1,143 +1,143 @@
{ {
"name": "lysand", "name": "lysand",
"module": "index.ts", "module": "index.ts",
"type": "module", "type": "module",
"version": "0.5.0", "version": "0.5.0",
"description": "A project to build a federated social network", "description": "A project to build a federated social network",
"author": { "author": {
"email": "contact@cpluspatch.com", "email": "contact@cpluspatch.com",
"name": "CPlusPatch", "name": "CPlusPatch",
"url": "https://cpluspatch.com" "url": "https://cpluspatch.com"
}, },
"bugs": { "bugs": {
"url": "https://github.com/lysand-org/lysand/issues" "url": "https://github.com/lysand-org/lysand/issues"
}, },
"icon": "https://github.com/lysand-org/lysand", "icon": "https://github.com/lysand-org/lysand",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"keywords": ["federated", "activitypub", "bun"], "keywords": ["federated", "activitypub", "bun"],
"workspaces": ["packages/*"], "workspaces": ["packages/*"],
"maintainers": [ "maintainers": [
{ {
"email": "contact@cpluspatch.com", "email": "contact@cpluspatch.com",
"name": "CPlusPatch", "name": "CPlusPatch",
"url": "https://cpluspatch.com" "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"
}
} }

View file

@ -32,7 +32,6 @@ import {
type Status, type Status,
type StatusWithRelations, type StatusWithRelations,
contentToHtml, contentToHtml,
findFirstNote,
findManyNotes, findManyNotes,
} from "~database/entities/Status"; } from "~database/entities/Status";
import { db } from "~drizzle/db"; import { db } from "~drizzle/db";
@ -69,13 +68,14 @@ export class Note {
sql: SQL<unknown> | undefined, sql: SQL<unknown> | undefined,
orderBy: SQL<unknown> | undefined = desc(Notes.id), orderBy: SQL<unknown> | undefined = desc(Notes.id),
) { ) {
const found = await findFirstNote({ const found = await findManyNotes({
where: sql, where: sql,
orderBy, orderBy,
limit: 1,
}); });
if (!found) return null; if (!found[0]) return null;
return new Note(found); return new Note(found[0]);
} }
static async manyFromSql( static async manyFromSql(
@ -413,35 +413,47 @@ export class Note {
async toAPI(userFetching?: User | null): Promise<APIStatus> { async toAPI(userFetching?: User | null): Promise<APIStatus> {
const data = this.getStatus(); 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 const [pinnedByUser, rebloggedByUser, mutedByUser, likedByUser] = (
? !!(await Note.fromSql( await Promise.all([
and( userFetching
eq(Notes.authorId, userFetching?.id), ? db.query.UserToPinnedNotes.findFirst({
eq(Notes.reblogId, data.id), where: (relation, { and, eq }) =>
), and(
)) eq(relation.noteId, data.id),
: false; eq(relation.userId, userFetching?.id),
),
const wasMutedByUser = userFetching })
? !!(await db.query.Relationships.findFirst({ : false,
where: (relationship, { and, eq }) => userFetching
and( ? Note.fromSql(
eq(relationship.ownerId, userFetching.id), and(
eq(relationship.subjectId, data.authorId), eq(Notes.authorId, userFetching?.id),
eq(relationship.muting, true), eq(Notes.reblogId, data.id),
), ),
})) )
: false; : 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 // Convert mentions of local users from @username@host to @username
const mentionedLocalUsers = data.mentions.filter( const mentionedLocalUsers = data.mentions.filter(
@ -476,10 +488,7 @@ export class Note {
card: null, card: null,
content: replacedContent, content: replacedContent,
emojis: data.emojis.map((emoji) => emojiToAPI(emoji)), emojis: data.emojis.map((emoji) => emojiToAPI(emoji)),
// FIXME: data.likes is always empty favourited: likedByUser,
favourited: !!(data.likes ?? []).find(
(like) => like.likerId === userFetching?.id,
),
favourites_count: data.likeCount, favourites_count: data.likeCount,
media_attachments: (data.attachments ?? []).map( media_attachments: (data.attachments ?? []).map(
(a) => attachmentToAPI(a) as APIAttachment, (a) => attachmentToAPI(a) as APIAttachment,
@ -495,8 +504,8 @@ export class Note {
username: mention.username, username: mention.username,
})), })),
language: null, language: null,
muted: wasMutedByUser, muted: mutedByUser,
pinned: wasPinnedByUser, pinned: pinnedByUser,
// TODO: Add polls // TODO: Add polls
poll: null, poll: null,
reblog: data.reblog reblog: data.reblog
@ -504,7 +513,7 @@ export class Note {
data.reblog as StatusWithRelations, data.reblog as StatusWithRelations,
).toAPI(userFetching) ).toAPI(userFetching)
: null, : null,
reblogged: wasRebloggedByUser, reblogged: rebloggedByUser,
reblogs_count: data.reblogCount, reblogs_count: data.reblogCount,
replies_count: data.replyCount, replies_count: data.replyCount,
sensitive: data.sensitive, sensitive: data.sensitive,