From 5565bf00de430cabccb51878f8f3805450e987ee Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 12 Jun 2024 14:45:07 -1000 Subject: [PATCH] refactor(database): :art: Improve database handlers to have more consistent naming and methods --- cli/commands/user/create.ts | 6 +- cli/commands/user/delete.ts | 2 +- cli/commands/user/list.ts | 2 +- cli/commands/user/refetch.ts | 4 +- cli/commands/user/reset.ts | 4 +- database/entities/Federation.ts | 8 +- database/entities/Like.ts | 8 +- database/entities/Notification.ts | 5 +- database/entities/Status.ts | 8 +- database/entities/User.ts | 14 +- packages/database-interface/base.ts | 21 ++ packages/database-interface/note.ts | 196 ++++++++++-------- packages/database-interface/role.ts | 83 +++++--- packages/database-interface/timeline.ts | 11 +- packages/database-interface/user.ts | 125 ++++++----- server/api/api/auth/login/index.test.ts | 6 +- server/api/api/auth/login/index.ts | 9 +- server/api/api/auth/mastodon-login/index.ts | 9 +- server/api/api/auth/reset/index.test.ts | 6 +- server/api/api/v1/accounts/:id/index.test.ts | 12 +- .../api/api/v1/accounts/lookup/index.test.ts | 6 +- .../api/api/v1/accounts/search/index.test.ts | 6 +- .../v1/accounts/update_credentials/index.ts | 2 +- server/api/api/v1/emojis/:id/index.ts | 2 +- server/api/api/v1/notifications/index.test.ts | 2 +- server/api/api/v1/roles/:id/index.test.ts | 14 +- server/api/api/v1/roles/:id/index.ts | 28 +-- server/api/api/v1/roles/index.test.ts | 2 +- server/api/api/v1/roles/index.ts | 2 +- server/api/api/v1/statuses/:id/favourite.ts | 2 +- .../api/v1/statuses/:id/favourited_by.test.ts | 2 +- server/api/api/v1/statuses/:id/index.ts | 4 +- server/api/api/v1/statuses/:id/pin.ts | 7 +- server/api/api/v1/statuses/:id/reblog.ts | 10 +- .../api/v1/statuses/:id/reblogged_by.test.ts | 2 +- server/api/api/v1/statuses/:id/source.ts | 4 +- server/api/api/v1/statuses/:id/unpin.ts | 2 +- server/api/api/v1/statuses/:id/unreblog.ts | 4 +- server/api/api/v1/statuses/index.test.ts | 12 +- server/api/api/v2/instance/index.ts | 2 +- server/api/oauth/authorize/index.ts | 10 +- server/api/users/:uuid/inbox/index.ts | 14 +- server/api/well-known/webfinger/index.ts | 2 +- tests/api/accounts.test.ts | 4 +- tests/oauth.test.ts | 2 +- tests/utils.ts | 4 +- utils/meilisearch.ts | 8 +- 47 files changed, 365 insertions(+), 333 deletions(-) create mode 100644 packages/database-interface/base.ts diff --git a/cli/commands/user/create.ts b/cli/commands/user/create.ts index bab27e71..ba0f8303 100644 --- a/cli/commands/user/create.ts +++ b/cli/commands/user/create.ts @@ -118,13 +118,13 @@ export default class UserCreate extends BaseCommand { !flags.format && this.log( `${chalk.green("✓")} Created user ${chalk.green( - user.getUser().username, + user.data.username, )} with id ${chalk.green(user.id)}`, ); this.log( formatArray( - [user.getUser()], + [user.data], [ "id", "username", @@ -144,7 +144,7 @@ export default class UserCreate extends BaseCommand { flags.format ? link : `\nPassword reset link for ${chalk.bold( - `@${user.getUser().username}`, + `@${user.data.username}`, )}: ${chalk.underline(chalk.blue(link))}\n`, ); diff --git a/cli/commands/user/delete.ts b/cli/commands/user/delete.ts index b8ba1127..da440947 100644 --- a/cli/commands/user/delete.ts +++ b/cli/commands/user/delete.ts @@ -50,7 +50,7 @@ export default class UserDelete extends UserFinderCommand { flags.print && this.log( formatArray( - users.map((u) => u.getUser()), + users.map((u) => u.data), [ "id", "username", diff --git a/cli/commands/user/list.ts b/cli/commands/user/list.ts index 32eb9d1c..55f3416c 100644 --- a/cli/commands/user/list.ts +++ b/cli/commands/user/list.ts @@ -72,7 +72,7 @@ export default class UserList extends BaseCommand { this.log( formatArray( - users.map((u) => u.getUser()), + users.map((u) => u.data), keys, flags.format as "json" | "csv" | undefined, flags["pretty-dates"], diff --git a/cli/commands/user/refetch.ts b/cli/commands/user/refetch.ts index 969e6565..8ece0f30 100644 --- a/cli/commands/user/refetch.ts +++ b/cli/commands/user/refetch.ts @@ -53,7 +53,7 @@ export default class UserRefetch extends UserFinderCommand { flags.print && this.log( formatArray( - users.map((u) => u.getUser()), + users.map((u) => u.data), [ "id", "username", @@ -85,7 +85,7 @@ export default class UserRefetch extends UserFinderCommand { this.log( chalk.bold( `${chalk.red("✗")} Failed to refetch user ${ - user.getUser().username + user.data.username }`, ), ); diff --git a/cli/commands/user/reset.ts b/cli/commands/user/reset.ts index 45468cec..6114e7cd 100644 --- a/cli/commands/user/reset.ts +++ b/cli/commands/user/reset.ts @@ -60,7 +60,7 @@ export default class UserReset extends UserFinderCommand { flags.print && this.log( formatArray( - users.map((u) => u.getUser()), + users.map((u) => u.data), [ "id", "username", @@ -108,7 +108,7 @@ export default class UserReset extends UserFinderCommand { flags.raw ? link : `\nPassword reset link for ${chalk.bold( - `@${user.getUser().username}`, + `@${user.data.username}`, )}: ${chalk.underline(chalk.blue(link))}\n`, ); diff --git a/database/entities/Federation.ts b/database/entities/Federation.ts index 807c114c..9e9a718b 100644 --- a/database/entities/Federation.ts +++ b/database/entities/Federation.ts @@ -15,7 +15,7 @@ export const objectToInboxRequest = async ( author: User, userToSendTo: User, ): Promise => { - if (userToSendTo.isLocal() || !userToSendTo.getUser().endpoints?.inbox) { + if (userToSendTo.isLocal() || !userToSendTo.data.endpoints?.inbox) { throw new Error("UserToSendTo has no inbox or is a local user"); } @@ -25,7 +25,7 @@ export const objectToInboxRequest = async ( const privateKey = await crypto.subtle.importKey( "pkcs8", - Buffer.from(author.getUser().privateKey ?? "", "base64"), + Buffer.from(author.data.privateKey ?? "", "base64"), "Ed25519", false, ["sign"], @@ -33,7 +33,7 @@ export const objectToInboxRequest = async ( const ctor = new SignatureConstructor(privateKey, author.getUri()); - const userInbox = new URL(userToSendTo.getUser().endpoints?.inbox ?? ""); + const userInbox = new URL(userToSendTo.data.endpoints?.inbox ?? ""); const request = new Request(userInbox, { method: "POST", @@ -54,7 +54,7 @@ export const objectToInboxRequest = async ( new LogManager(Bun.stdout).log( LogLevel.DEBUG, "Inbox.Signature", - `Sender public key: ${author.getUser().publicKey}`, + `Sender public key: ${author.data.publicKey}`, ); // Log signed string diff --git a/database/entities/Like.ts b/database/entities/Like.ts index c44c440f..390c1a85 100644 --- a/database/entities/Like.ts +++ b/database/entities/Like.ts @@ -35,12 +35,12 @@ export const createLike = async (user: User, note: Note) => { likerId: user.id, }); - if (note.getAuthor().getUser().instanceId === user.getUser().instanceId) { + if (note.author.data.instanceId === user.data.instanceId) { // Notify the user that their post has been favourited await db.insert(Notifications).values({ accountId: user.id, type: "favourite", - notifiedId: note.getAuthor().id, + notifiedId: note.author.id, noteId: note.id, }); } else { @@ -65,12 +65,12 @@ export const deleteLike = async (user: User, note: Note) => { and( eq(Notifications.accountId, user.id), eq(Notifications.type, "favourite"), - eq(Notifications.notifiedId, note.getAuthor().id), + eq(Notifications.notifiedId, note.author.id), eq(Notifications.noteId, note.id), ), ); - if (user.isLocal() && note.getAuthor().isRemote()) { + if (user.isLocal() && note.author.isRemote()) { // User is local, federate the delete // TODO: Federate this } diff --git a/database/entities/Notification.ts b/database/entities/Notification.ts index 5a4d4f53..693f7e63 100644 --- a/database/entities/Notification.ts +++ b/database/entities/Notification.ts @@ -43,8 +43,7 @@ export const findManyNotifications = async ( output.map(async (notif) => ({ ...notif, account: transformOutputToUserWithRelations(notif.account), - status: - (await Note.fromId(notif.noteId, userId))?.getStatus() ?? null, + status: (await Note.fromId(notif.noteId, userId))?.data ?? null, })), ); }; @@ -59,7 +58,7 @@ export const notificationToAPI = async ( id: notification.id, type: notification.type, status: notification.status - ? await Note.fromStatus(notification.status).toAPI(account) + ? await new Note(notification.status).toAPI(account) : undefined, }; }; diff --git a/database/entities/Status.ts b/database/entities/Status.ts index f6540747..4232715f 100644 --- a/database/entities/Status.ts +++ b/database/entities/Status.ts @@ -323,7 +323,7 @@ export const parseTextMentions = async (text: string): Promise => { export const replaceTextMentions = async (text: string, mentions: User[]) => { let finalText = text; for (const mention of mentions) { - const user = mention.getUser(); + const user = mention.data; // Replace @username and @username@domain if (user.instance) { finalText = finalText.replace( @@ -439,7 +439,7 @@ export const federateNote = async (note: Note) => { // TODO: Add queue system const request = await objectToInboxRequest( note.toLysand(), - note.getAuthor(), + note.author, user, ); @@ -455,9 +455,7 @@ export const federateNote = async (note: Note) => { dualLogger.log( LogLevel.ERROR, "Federation.Status", - `Failed to federate status ${ - note.getStatus().id - } to ${user.getUri()}`, + `Failed to federate status ${note.data.id} to ${user.getUri()}`, ); } } diff --git a/database/entities/User.ts b/database/entities/User.ts index f03b7b7a..f84874eb 100644 --- a/database/entities/User.ts +++ b/database/entities/User.ts @@ -131,8 +131,8 @@ export const followRequestUser = async ( await db .update(Relationships) .set({ - following: isRemote ? false : !followee.getUser().isLocked, - requested: isRemote ? true : followee.getUser().isLocked, + following: isRemote ? false : !followee.data.isLocked, + requested: isRemote ? true : followee.data.isLocked, showingReblogs: reblogs, notifying: notify, languages: languages, @@ -143,8 +143,8 @@ export const followRequestUser = async ( await db .update(Relationships) .set({ - requestedBy: isRemote ? true : followee.getUser().isLocked, - followedBy: isRemote ? false : followee.getUser().isLocked, + requestedBy: isRemote ? true : followee.data.isLocked, + followedBy: isRemote ? false : followee.data.isLocked, }) .where( and( @@ -209,7 +209,7 @@ export const followRequestUser = async ( } else { await db.insert(Notifications).values({ accountId: follower.id, - type: followee.getUser().isLocked ? "follow_request" : "follow", + type: followee.data.isLocked ? "follow_request" : "follow", notifiedId: followee.id, }); } @@ -522,7 +522,7 @@ export const followRequestToLysand = ( throw new Error("Followee must be a remote user"); } - if (!followee.getUser().uri) { + if (!followee.data.uri) { throw new Error("Followee must have a URI in database"); } @@ -550,7 +550,7 @@ export const followAcceptToLysand = ( throw new Error("Followee must be a local user"); } - if (!follower.getUser().uri) { + if (!follower.data.uri) { throw new Error("Follower must have a URI in database"); } diff --git a/packages/database-interface/base.ts b/packages/database-interface/base.ts new file mode 100644 index 00000000..f3e17a6d --- /dev/null +++ b/packages/database-interface/base.ts @@ -0,0 +1,21 @@ +import type { InferModelFromColumns, InferSelectModel } from "drizzle-orm"; +import type { PgTableWithColumns } from "drizzle-orm/pg-core"; + +export abstract class BaseInterface< + // biome-ignore lint/suspicious/noExplicitAny: This is just an extended interface + Table extends PgTableWithColumns, + Columns = InferModelFromColumns, +> { + constructor(public data: Columns) {} + + public abstract save(): Promise; + + public abstract delete(ids: string[]): Promise; + public abstract delete(): Promise; + + public abstract update( + newData: Partial>, + ): Promise; + + public abstract reload(): Promise; +} diff --git a/packages/database-interface/note.ts b/packages/database-interface/note.ts index f7c845f9..cb537602 100644 --- a/packages/database-interface/note.ts +++ b/packages/database-interface/note.ts @@ -35,7 +35,6 @@ import { } from "~/database/entities/Emoji"; import { localObjectURI } from "~/database/entities/Federation"; import { - type Status, type StatusWithRelations, contentToHtml, findManyNotes, @@ -52,30 +51,65 @@ import { import { config } from "~/packages/config-manager"; import type { Attachment as APIAttachment } from "~/types/mastodon/attachment"; import type { Status as APIStatus } from "~/types/mastodon/status"; +import { BaseInterface } from "./base"; import { User } from "./user"; /** * Gives helpers to fetch notes from database in a nice format */ -export class Note { - private constructor(private status: StatusWithRelations) {} +export class Note extends BaseInterface { + async save(): Promise { + return this.update(this.data); + } + + async reload(): Promise { + const reloaded = await Note.fromId(this.data.id); + + if (!reloaded) { + throw new Error("Failed to reload status"); + } + + this.data = reloaded.data; + } + + public static async insert( + data: InferInsertModel, + userRequestingNoteId?: string, + ): Promise { + const inserted = (await db.insert(Notes).values(data).returning())[0]; + + const note = await Note.fromId(inserted.id, userRequestingNoteId); + + if (!note) { + throw new Error("Failed to insert status"); + } + + return note; + } static async fromId( id: string | null, - userId?: string, + userRequestingNoteId?: string, ): Promise { if (!id) return null; - return await Note.fromSql(eq(Notes.id, id), undefined, userId); + return await Note.fromSql( + eq(Notes.id, id), + undefined, + userRequestingNoteId, + ); } - static async fromIds(ids: string[], userId?: string): Promise { + static async fromIds( + ids: string[], + userRequestingNoteId?: string, + ): Promise { return await Note.manyFromSql( inArray(Notes.id, ids), undefined, undefined, undefined, - userId, + userRequestingNoteId, ); } @@ -118,21 +152,19 @@ export class Note { } get id() { - return this.status.id; + return this.data.id; } async getUsersToFederateTo() { // Mentioned users const mentionedUsers = - this.getStatus().mentions.length > 0 + this.data.mentions.length > 0 ? await User.manyFromSql( and( isNotNull(Users.instanceId), inArray( Users.id, - this.getStatus().mentions.map( - (mention) => mention.id, - ), + this.data.mentions.map((mention) => mention.id), ), ), ) @@ -166,24 +198,12 @@ export class Note { return deduplicatedUsersById; } - static fromStatus(status: StatusWithRelations) { - return new Note(status); - } - - static fromStatuses(statuses: StatusWithRelations[]) { - return statuses.map((s) => new Note(s)); - } - isNull() { - return this.status === null; + return this.data === null; } - getStatus() { - return this.status; - } - - getAuthor() { - return new User(this.status.author); + get author() { + return new User(this.data.author); } static async getCount() { @@ -201,7 +221,7 @@ export class Note { async getReplyChildren(userId?: string) { return await Note.manyFromSql( - eq(Notes.replyId, this.status.id), + eq(Notes.replyId, this.data.id), undefined, undefined, undefined, @@ -209,12 +229,8 @@ export class Note { ); } - static async insert(values: InferInsertModel) { - return (await db.insert(Notes).values(values).returning())[0]; - } - async isRemote() { - return this.getAuthor().isRemote(); + return this.author.isRemote(); } async updateFromRemote() { @@ -222,13 +238,13 @@ export class Note { throw new Error("Cannot refetch a local note (it is not remote)"); } - const updated = await Note.saveFromRemote(this.getURI()); + const updated = await Note.saveFromRemote(this.getUri()); if (!updated) { throw new Error("Note not found after update"); } - this.status = updated.getStatus(); + this.data = updated.data; return this; } @@ -324,7 +340,7 @@ export class Note { } } - return await Note.fromId(newNote.id, newNote.authorId); + return await Note.fromId(newNote.id, newNote.data.authorId); } async updateFromData( @@ -347,7 +363,7 @@ export class Note { // Parse emojis and fuse with existing emojis let foundEmojis = emojis; - if (this.getAuthor().isLocal() && htmlContent) { + if (this.author.isLocal() && htmlContent) { const parsedEmojis = await parseEmojis(htmlContent); // Fuse and deduplicate foundEmojis = [...emojis, ...parsedEmojis].filter( @@ -376,14 +392,14 @@ export class Note { // Connect emojis await db .delete(EmojiToNote) - .where(eq(EmojiToNote.noteId, this.status.id)); + .where(eq(EmojiToNote.noteId, this.data.id)); for (const emoji of foundEmojis) { await db .insert(EmojiToNote) .values({ emojiId: emoji.id, - noteId: this.status.id, + noteId: this.data.id, }) .execute(); } @@ -391,13 +407,13 @@ export class Note { // Connect mentions await db .delete(NoteToMentions) - .where(eq(NoteToMentions.noteId, this.status.id)); + .where(eq(NoteToMentions.noteId, this.data.id)); for (const mention of mentions ?? []) { await db .insert(NoteToMentions) .values({ - noteId: this.status.id, + noteId: this.data.id, userId: mention.id, }) .execute(); @@ -410,13 +426,13 @@ export class Note { .set({ noteId: null, }) - .where(eq(Attachments.noteId, this.status.id)); + .where(eq(Attachments.noteId, this.data.id)); if (media_attachments.length > 0) await db .update(Attachments) .set({ - noteId: this.status.id, + noteId: this.data.id, }) .where(inArray(Attachments.id, media_attachments)); } @@ -555,10 +571,10 @@ export class Note { : [], attachments.map((a) => a.id), note.replies_to - ? (await Note.resolve(note.replies_to))?.getStatus().id + ? (await Note.resolve(note.replies_to))?.data.id : undefined, note.quotes - ? (await Note.resolve(note.quotes))?.getStatus().id + ? (await Note.resolve(note.quotes))?.data.id : undefined, ); } @@ -582,10 +598,10 @@ export class Note { ), attachments.map((a) => a.id), note.replies_to - ? (await Note.resolve(note.replies_to))?.getStatus().id + ? (await Note.resolve(note.replies_to))?.data.id : undefined, note.quotes - ? (await Note.resolve(note.quotes))?.getStatus().id + ? (await Note.resolve(note.quotes))?.data.id : undefined, ); @@ -596,27 +612,28 @@ export class Note { return createdNote; } - async delete() { - return ( - await db - .delete(Notes) - .where(eq(Notes.id, this.status.id)) - .returning() - )[0]; + async delete(ids: string[]): Promise; + async delete(): Promise; + async delete(ids?: unknown): Promise { + if (Array.isArray(ids)) { + await db.delete(Notes).where(inArray(Notes.id, ids)); + } else { + await db.delete(Notes).where(eq(Notes.id, this.id)); + } } - async update(newStatus: Partial) { - return ( - await db - .update(Notes) - .set(newStatus) - .where(eq(Notes.id, this.status.id)) - .returning() - )[0]; - } + async update( + newStatus: Partial, + ): Promise { + await db.update(Notes).set(newStatus).where(eq(Notes.id, this.data.id)); - static async deleteMany(ids: string[]) { - return await db.delete(Notes).where(inArray(Notes.id, ids)).returning(); + const updated = await Note.fromId(this.data.id); + + if (!updated) { + throw new Error("Failed to update status"); + } + + return updated.data; } /** @@ -625,10 +642,10 @@ export class Note { * @returns Whether this status is viewable by the user. */ async isViewableByUser(user: User | null) { - if (this.getAuthor().id === user?.id) return true; - if (this.getStatus().visibility === "public") return true; - if (this.getStatus().visibility === "unlisted") return true; - if (this.getStatus().visibility === "private") { + if (this.author.id === user?.id) return true; + if (this.data.visibility === "public") return true; + if (this.data.visibility === "unlisted") return true; + if (this.data.visibility === "private") { return user ? await db.query.Relationships.findFirst({ where: (relationship, { and, eq }) => @@ -641,13 +658,12 @@ export class Note { : false; } return ( - user && - this.getStatus().mentions.find((mention) => mention.id === user.id) + user && this.data.mentions.find((mention) => mention.id === user.id) ); } async toAPI(userFetching?: User | null): Promise { - const data = this.getStatus(); + const data = this.data; // Convert mentions of local users from @username@host to @username const mentionedLocalUsers = data.mentions.filter( @@ -684,7 +700,7 @@ export class Note { id: data.id, in_reply_to_id: data.replyId || null, in_reply_to_account_id: data.reply?.authorId || null, - account: this.getAuthor().toAPI(userFetching?.id === data.authorId), + account: this.author.toAPI(userFetching?.id === data.authorId), created_at: new Date(data.createdAt).toISOString(), application: data.application ? applicationToAPI(data.application) @@ -713,9 +729,9 @@ export class Note { // TODO: Add polls poll: null, reblog: data.reblog - ? await Note.fromStatus( - data.reblog as StatusWithRelations, - ).toAPI(userFetching) + ? await new Note(data.reblog as StatusWithRelations).toAPI( + userFetching, + ) : null, reblogged: data.reblogged, reblogs_count: data.reblogCount, @@ -723,9 +739,9 @@ export class Note { sensitive: data.sensitive, spoiler_text: data.spoilerText, tags: [], - uri: data.uri || this.getURI(), + uri: data.uri || this.getUri(), visibility: data.visibility as APIStatus["visibility"], - url: data.uri || this.getMastoURI(), + url: data.uri || this.getMastoUri(), bookmarked: false, // @ts-expect-error Glitch-SOC extension quote: data.quotingId @@ -737,30 +753,30 @@ export class Note { }; } - getURI() { - return localObjectURI(this.getStatus().id); + getUri() { + return localObjectURI(this.data.id); } - static getURI(id?: string | null) { + static getUri(id?: string | null) { if (!id) return null; return localObjectURI(id); } - getMastoURI() { + getMastoUri() { return new URL( - `/@${this.getAuthor().getUser().username}/${this.id}`, + `/@${this.author.data.username}/${this.id}`, config.http.base_url, ).toString(); } toLysand(): typeof EntityValidator.$Note { - const status = this.getStatus(); + const status = this.data; return { type: "Note", created_at: new Date(status.createdAt).toISOString(), id: status.id, - author: this.getAuthor().getUri(), - uri: this.getURI(), + author: this.author.getUri(), + uri: this.getUri(), content: { "text/html": { content: status.content, @@ -774,8 +790,8 @@ export class Note { ), is_sensitive: status.sensitive, mentions: status.mentions.map((mention) => mention.uri || ""), - quotes: Note.getURI(status.quotingId) ?? undefined, - replies_to: Note.getURI(status.replyId) ?? undefined, + quotes: Note.getUri(status.quotingId) ?? undefined, + replies_to: Note.getUri(status.replyId) ?? undefined, subject: status.spoilerText, visibility: status.visibility as | "public" @@ -799,9 +815,9 @@ export class Note { let currentStatus: Note = this; - while (currentStatus.getStatus().replyId) { + while (currentStatus.data.replyId) { const parent = await Note.fromId( - currentStatus.getStatus().replyId, + currentStatus.data.replyId, fetcher?.id, ); diff --git a/packages/database-interface/role.ts b/packages/database-interface/role.ts index 1a2aeea1..d50a8c00 100644 --- a/packages/database-interface/role.ts +++ b/packages/database-interface/role.ts @@ -11,9 +11,20 @@ import { } from "drizzle-orm"; import { db } from "~/drizzle/db"; import { RoleToUsers, Roles } from "~/drizzle/schema"; +import { BaseInterface } from "./base"; -export class Role { - private constructor(private role: InferSelectModel) {} +export type RoleType = InferSelectModel; + +export class Role extends BaseInterface { + async reload(): Promise { + const reloaded = await Role.fromId(this.data.id); + + if (!reloaded) { + throw new Error("Failed to reload role"); + } + + this.data = reloaded.data; + } public static fromRole(role: InferSelectModel) { return new Role(role); @@ -104,26 +115,44 @@ export class Role { return found.map((s) => new Role(s)); } - public async save( - role: Partial> = this.role, - ) { - return new Role( - ( - await db - .update(Roles) - .set(role) - .where(eq(Roles.id, this.id)) - .returning() - )[0], - ); + async update(newRole: Partial): Promise { + await db.update(Roles).set(newRole).where(eq(Roles.id, this.id)); + + const updated = await Role.fromId(this.data.id); + + if (!updated) { + throw new Error("Failed to update role"); + } + + return updated.data; } - public async delete() { - await db.delete(Roles).where(eq(Roles.id, this.id)); + async save(): Promise { + return this.update(this.data); } - public static async new(role: InferInsertModel) { - return new Role((await db.insert(Roles).values(role).returning())[0]); + async delete(ids: string[]): Promise; + async delete(): Promise; + async delete(ids?: unknown): Promise { + if (Array.isArray(ids)) { + await db.delete(Roles).where(inArray(Roles.id, ids)); + } else { + await db.delete(Roles).where(eq(Roles.id, this.id)); + } + } + + public static async insert( + data: InferInsertModel, + ): Promise { + const inserted = (await db.insert(Roles).values(data).returning())[0]; + + const role = await Role.fromId(inserted.id); + + if (!role) { + throw new Error("Failed to insert role"); + } + + return role; } public async linkUser(userId: string) { @@ -145,22 +174,18 @@ export class Role { } get id() { - return this.role.id; - } - - public getRole() { - return this.role; + return this.data.id; } public toAPI() { return { id: this.id, - name: this.role.name, - permissions: this.role.permissions, - priority: this.role.priority, - description: this.role.description, - visible: this.role.visible, - icon: proxyUrl(this.role.icon), + name: this.data.name, + permissions: this.data.permissions, + priority: this.data.priority, + description: this.data.description, + visible: this.data.visible, + icon: proxyUrl(this.data.icon), }; } } diff --git a/packages/database-interface/timeline.ts b/packages/database-interface/timeline.ts index 29ff202e..a3d09145 100644 --- a/packages/database-interface/timeline.ts +++ b/packages/database-interface/timeline.ts @@ -74,23 +74,20 @@ export class Timeline { switch (this.type) { case TimelineType.NOTE: { const objectBefore = await Note.fromSql( - gt(Notes.id, notes[0].getStatus().id), + gt(Notes.id, notes[0].data.id), ); if (objectBefore) { linkHeader.push( `<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${ - notes[0].getStatus().id + notes[0].data.id }>; rel="prev"`, ); } if (notes.length >= (limit ?? 20)) { const objectAfter = await Note.fromSql( - gt( - Notes.id, - notes[notes.length - 1].getStatus().id, - ), + gt(Notes.id, notes[notes.length - 1].data.id), ); if (objectAfter) { @@ -98,7 +95,7 @@ export class Timeline { `<${urlWithoutQuery}?limit=${ limit ?? 20 }&max_id=${ - notes[notes.length - 1].getStatus().id + notes[notes.length - 1].data.id }>; rel="next"`, ); } diff --git a/packages/database-interface/user.ts b/packages/database-interface/user.ts index 1abef889..997aaacc 100644 --- a/packages/database-interface/user.ts +++ b/packages/database-interface/user.ts @@ -42,14 +42,23 @@ import { import { type Config, config } from "~/packages/config-manager"; import type { Account as APIAccount } from "~/types/mastodon/account"; import type { Mention as APIMention } from "~/types/mastodon/mention"; +import { BaseInterface } from "./base"; import type { Note } from "./note"; import { Role } from "./role"; /** * Gives helpers to fetch users from database in a nice format */ -export class User { - constructor(private user: UserWithRelations) {} +export class User extends BaseInterface { + async reload(): Promise { + const reloaded = await User.fromId(this.data.id); + + if (!reloaded) { + throw new Error("Failed to reload user"); + } + + this.data = reloaded.data; + } static async fromId(id: string | null): Promise { if (!id) return null; @@ -93,15 +102,11 @@ export class User { } get id() { - return this.user.id; - } - - getUser() { - return this.user; + return this.data.id; } isLocal() { - return this.user.instanceId === null; + return this.data.instanceId === null; } isRemote() { @@ -110,8 +115,8 @@ export class User { getUri() { return ( - this.user.uri || - new URL(`/users/${this.user.id}`, config.http.base_url).toString() + this.data.uri || + new URL(`/users/${this.data.id}`, config.http.base_url).toString() ); } @@ -125,12 +130,12 @@ export class User { public getAllPermissions() { return ( - this.user.roles + this.data.roles .flatMap((role) => role.permissions) // Add default permissions .concat(config.permissions.default) // If admin, add admin permissions - .concat(this.user.isAdmin ? config.permissions.admin : []) + .concat(this.data.isAdmin ? config.permissions.admin : []) .reduce((acc, permission) => { if (!acc.includes(permission)) acc.push(permission); return acc; @@ -169,10 +174,14 @@ export class User { )[0].count; } - async delete() { - return ( - await db.delete(Users).where(eq(Users.id, this.id)).returning() - )[0]; + async delete(ids: string[]): Promise; + async delete(): Promise; + async delete(ids?: unknown): Promise { + if (Array.isArray(ids)) { + await db.delete(Users).where(inArray(Users.id, ids)); + } else { + await db.delete(Users).where(eq(Users.id, this.id)); + } } async resetPassword() { @@ -211,17 +220,8 @@ export class User { )[0]; } - async save() { - return ( - await db - .update(Users) - .set({ - ...this.user, - updatedAt: new Date().toISOString(), - }) - .where(eq(Users.id, this.id)) - .returning() - )[0]; + async save(): Promise { + return this.update(this.data); } async updateFromRemote() { @@ -237,7 +237,7 @@ export class User { throw new Error("User not found after update"); } - this.user = updated.getUser(); + this.data = updated.data; return this; } @@ -405,12 +405,12 @@ export class User { * @returns The raw URL for the user's avatar */ getAvatarUrl(config: Config) { - if (!this.user.avatar) + if (!this.data.avatar) return ( config.defaults.avatar || - `https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.user.username}` + `https://api.dicebear.com/8.x/${config.defaults.placeholder_style}/svg?seed=${this.data.username}` ); - return this.user.avatar; + return this.data.avatar; } static async generateKeys() { @@ -494,57 +494,50 @@ export class User { * @returns The raw URL for the user's header */ getHeaderUrl(config: Config) { - if (!this.user.header) return config.defaults.header || ""; - return this.user.header; + if (!this.data.header) return config.defaults.header || ""; + return this.data.header; } getAcct() { return this.isLocal() - ? this.user.username - : `${this.user.username}@${this.user.instance?.baseUrl}`; + ? this.data.username + : `${this.data.username}@${this.data.instance?.baseUrl}`; } static getAcct(isLocal: boolean, username: string, baseUrl?: string) { return isLocal ? username : `${username}@${baseUrl}`; } - async update(data: Partial) { - const updated = ( - await db - .update(Users) - .set({ - ...data, - updatedAt: new Date().toISOString(), - }) - .where(eq(Users.id, this.id)) - .returning() - )[0]; + async update( + newUser: Partial, + ): Promise { + await db.update(Users).set(newUser).where(eq(Users.id, this.id)); - const newUser = await User.fromId(updated.id); + const updated = await User.fromId(this.data.id); - if (!newUser) throw new Error("User not found after update"); - - this.user = newUser.getUser(); + if (!updated) { + throw new Error("Failed to update user"); + } // If something important is updated, federate it if ( - data.username || - data.displayName || - data.note || - data.avatar || - data.header || - data.fields || - data.publicKey || - data.isAdmin || - data.isBot || - data.isLocked || - data.endpoints || - data.isDiscoverable + newUser.username || + newUser.displayName || + newUser.note || + newUser.avatar || + newUser.header || + newUser.fields || + newUser.publicKey || + newUser.isAdmin || + newUser.isBot || + newUser.isLocked || + newUser.endpoints || + newUser.isDiscoverable ) { await this.federateToFollowers(this.toLysand()); } - return this; + return updated.data; } async federateToFollowers(object: typeof EntityValidator.$Entity) { @@ -569,7 +562,7 @@ export class User { } toAPI(isOwnAccount = false): APIAccount { - const user = this.getUser(); + const user = this.data; return { id: user.id, username: user.username, @@ -642,7 +635,7 @@ export class User { throw new Error("Cannot convert remote user to Lysand format"); } - const user = this.getUser(); + const user = this.data; return { id: user.id, @@ -709,7 +702,7 @@ export class User { toMention(): APIMention { return { url: this.getUri(), - username: this.getUser().username, + username: this.data.username, acct: this.getAcct(), id: this.id, }; diff --git a/server/api/api/auth/login/index.test.ts b/server/api/api/auth/login/index.test.ts index 139df0bb..230333c4 100644 --- a/server/api/api/auth/login/index.test.ts +++ b/server/api/api/auth/login/index.test.ts @@ -33,7 +33,7 @@ describe(meta.route, () => { test("should get a JWT with email", async () => { const formData = new FormData(); - formData.append("identifier", users[0]?.getUser().email ?? ""); + formData.append("identifier", users[0]?.data.email ?? ""); formData.append("password", passwords[0]); const response = await sendTestRequest( @@ -72,7 +72,7 @@ describe(meta.route, () => { test("should get a JWT with username", async () => { const formData = new FormData(); - formData.append("identifier", users[0]?.getUser().username ?? ""); + formData.append("identifier", users[0]?.data.username ?? ""); formData.append("password", passwords[0]); const response = await sendTestRequest( @@ -187,7 +187,7 @@ describe(meta.route, () => { test("invalid password", async () => { const formData = new FormData(); - formData.append("identifier", users[0]?.getUser().email ?? ""); + formData.append("identifier", users[0]?.data.email ?? ""); formData.append("password", "password"); const response = await sendTestRequest( diff --git a/server/api/api/auth/login/index.ts b/server/api/api/auth/login/index.ts index 94ed0827..fb30e21e 100644 --- a/server/api/api/auth/login/index.ts +++ b/server/api/api/auth/login/index.ts @@ -108,10 +108,7 @@ export default (app: Hono) => if ( !user || - !(await Bun.password.verify( - password, - user.getUser().password || "", - )) + !(await Bun.password.verify(password, user.data.password || "")) ) return returnError( context.req.query(), @@ -119,13 +116,13 @@ export default (app: Hono) => "Invalid identifier or password", ); - if (user.getUser().passwordResetToken) { + if (user.data.passwordResetToken) { return response(null, 302, { Location: new URL( `${ config.frontend.routes.password_reset }?${new URLSearchParams({ - token: user.getUser().passwordResetToken ?? "", + token: user.data.passwordResetToken ?? "", login_reset: "true", }).toString()}`, config.http.base_url, diff --git a/server/api/api/auth/mastodon-login/index.ts b/server/api/api/auth/mastodon-login/index.ts index d8ac7ddb..70cbcfa1 100644 --- a/server/api/api/auth/mastodon-login/index.ts +++ b/server/api/api/auth/mastodon-login/index.ts @@ -57,20 +57,17 @@ export default (app: Hono) => if ( !user || - !(await Bun.password.verify( - password, - user.getUser().password || "", - )) + !(await Bun.password.verify(password, user.data.password || "")) ) return redirectToLogin("Invalid email or password"); - if (user.getUser().passwordResetToken) { + if (user.data.passwordResetToken) { return response(null, 302, { Location: new URL( `${ config.frontend.routes.password_reset }?${new URLSearchParams({ - token: user.getUser().passwordResetToken ?? "", + token: user.data.passwordResetToken ?? "", login_reset: "true", }).toString()}`, config.http.base_url, diff --git a/server/api/api/auth/reset/index.test.ts b/server/api/api/auth/reset/index.test.ts index d29a1d64..c55895cd 100644 --- a/server/api/api/auth/reset/index.test.ts +++ b/server/api/api/auth/reset/index.test.ts @@ -35,7 +35,7 @@ describe(meta.route, () => { test("should login with normal password", async () => { const formData = new FormData(); - formData.append("identifier", users[0]?.getUser().username ?? ""); + formData.append("identifier", users[0]?.data.username ?? ""); formData.append("password", passwords[0]); const response = await sendTestRequest( @@ -62,7 +62,7 @@ describe(meta.route, () => { const formData = new FormData(); - formData.append("identifier", users[0]?.getUser().username ?? ""); + formData.append("identifier", users[0]?.data.username ?? ""); formData.append("password", passwords[0]); const response = await sendTestRequest( @@ -108,7 +108,7 @@ describe(meta.route, () => { const loginFormData = new FormData(); - loginFormData.append("identifier", users[0]?.getUser().username ?? ""); + loginFormData.append("identifier", users[0]?.data.username ?? ""); loginFormData.append("password", newPassword); const loginResponse = await sendTestRequest( diff --git a/server/api/api/v1/accounts/:id/index.test.ts b/server/api/api/v1/accounts/:id/index.test.ts index 0e3eae6c..be1ce1f8 100644 --- a/server/api/api/v1/accounts/:id/index.test.ts +++ b/server/api/api/v1/accounts/:id/index.test.ts @@ -61,17 +61,17 @@ describe(meta.route, () => { const data = (await response.json()) as APIAccount; expect(data).toMatchObject({ id: users[0].id, - username: users[0].getUser().username, - display_name: users[0].getUser().displayName, + username: users[0].data.username, + display_name: users[0].data.displayName, avatar: expect.any(String), header: expect.any(String), - locked: users[0].getUser().isLocked, - created_at: new Date(users[0].getUser().createdAt).toISOString(), + locked: users[0].data.isLocked, + created_at: new Date(users[0].data.createdAt).toISOString(), followers_count: 0, following_count: 0, statuses_count: 40, - note: users[0].getUser().note, - acct: users[0].getUser().username, + note: users[0].data.note, + acct: users[0].data.username, url: expect.any(String), avatar_static: expect.any(String), header_static: expect.any(String), diff --git a/server/api/api/v1/accounts/lookup/index.test.ts b/server/api/api/v1/accounts/lookup/index.test.ts index c98257d5..a6f1499e 100644 --- a/server/api/api/v1/accounts/lookup/index.test.ts +++ b/server/api/api/v1/accounts/lookup/index.test.ts @@ -16,7 +16,7 @@ describe(meta.route, () => { const response = await sendTestRequest( new Request( new URL( - `${meta.route}?acct=${users[0].getUser().username}`, + `${meta.route}?acct=${users[0].data.username}`, config.http.base_url, ), { @@ -33,8 +33,8 @@ describe(meta.route, () => { expect(data).toEqual( expect.objectContaining({ id: users[0].id, - username: users[0].getUser().username, - display_name: users[0].getUser().displayName, + username: users[0].data.username, + display_name: users[0].data.displayName, avatar: expect.any(String), header: expect.any(String), }), diff --git a/server/api/api/v1/accounts/search/index.test.ts b/server/api/api/v1/accounts/search/index.test.ts index 4f0cdfc1..542c7b01 100644 --- a/server/api/api/v1/accounts/search/index.test.ts +++ b/server/api/api/v1/accounts/search/index.test.ts @@ -16,7 +16,7 @@ describe(meta.route, () => { const response = await sendTestRequest( new Request( new URL( - `${meta.route}?q=${users[0].getUser().username}`, + `${meta.route}?q=${users[0].data.username}`, config.http.base_url, ), { @@ -34,8 +34,8 @@ describe(meta.route, () => { expect.arrayContaining([ expect.objectContaining({ id: users[0].id, - username: users[0].getUser().username, - display_name: users[0].getUser().displayName, + username: users[0].data.username, + display_name: users[0].data.displayName, avatar: expect.any(String), header: expect.any(String), }), diff --git a/server/api/api/v1/accounts/update_credentials/index.ts b/server/api/api/v1/accounts/update_credentials/index.ts index b5bdc60a..6a8e74b8 100644 --- a/server/api/api/v1/accounts/update_credentials/index.ts +++ b/server/api/api/v1/accounts/update_credentials/index.ts @@ -118,7 +118,7 @@ export default (app: Hono) => if (!user) return errorResponse("Unauthorized", 401); - const self = user.getUser(); + const self = user.data; const sanitizedDisplayName = await sanitizedHtmlStrip( display_name ?? "", diff --git a/server/api/api/v1/emojis/:id/index.ts b/server/api/api/v1/emojis/:id/index.ts index 5a70cfd9..35b4e081 100644 --- a/server/api/api/v1/emojis/:id/index.ts +++ b/server/api/api/v1/emojis/:id/index.ts @@ -98,7 +98,7 @@ export default (app: Hono) => // Check if user is admin if ( !user.hasPermission(RolePermissions.MANAGE_EMOJIS) && - emoji.ownerId !== user.getUser().id + emoji.ownerId !== user.data.id ) { return jsonResponse( { diff --git a/server/api/api/v1/notifications/index.test.ts b/server/api/api/v1/notifications/index.test.ts index 864b56c3..aee0eb80 100644 --- a/server/api/api/v1/notifications/index.test.ts +++ b/server/api/api/v1/notifications/index.test.ts @@ -77,7 +77,7 @@ beforeAll(async () => { Authorization: `Bearer ${tokens[1].accessToken}`, }, body: new URLSearchParams({ - status: `@${users[0].getUser().username} test mention`, + status: `@${users[0].data.username} test mention`, visibility: "direct", local_only: "true", }), diff --git a/server/api/api/v1/roles/:id/index.test.ts b/server/api/api/v1/roles/:id/index.test.ts index 8b492ab0..7f589391 100644 --- a/server/api/api/v1/roles/:id/index.test.ts +++ b/server/api/api/v1/roles/:id/index.test.ts @@ -12,7 +12,7 @@ let higherPriorityRole: Role; beforeAll(async () => { // Create new role - role = await Role.new({ + role = await Role.insert({ name: "test", permissions: DEFAULT_ROLES, priority: 2, @@ -27,7 +27,7 @@ beforeAll(async () => { await role.linkUser(users[0].id); // Create new role - roleNotLinked = await Role.new({ + roleNotLinked = await Role.insert({ name: "test2", permissions: ADMIN_ROLES, priority: 0, @@ -39,7 +39,7 @@ beforeAll(async () => { expect(roleNotLinked).toBeDefined(); // Create a role with higher priority than the user's role - higherPriorityRole = await Role.new({ + higherPriorityRole = await Role.insert({ name: "higherPriorityRole", permissions: DEFAULT_ROLES, priority: 3, // Higher priority than the user's role @@ -149,7 +149,7 @@ describe(meta.route, () => { }); test("should assign new role", async () => { - await role.save({ + await role.update({ permissions: [RolePermissions.MANAGE_ROLES], }); @@ -194,7 +194,7 @@ describe(meta.route, () => { icon: expect.any(String), }); - await role.save({ + await role.update({ permissions: [], }); }); @@ -243,7 +243,7 @@ describe(meta.route, () => { test("should return 403 if user tries to add role with higher priority", async () => { // Add MANAGE_ROLES permission to user - await role.save({ + await role.update({ permissions: [RolePermissions.MANAGE_ROLES], }); @@ -268,7 +268,7 @@ describe(meta.route, () => { error: "Cannot assign role 'higherPriorityRole' with priority 3 to user with highest role priority 0", }); - await role.save({ + await role.update({ permissions: [], }); }); diff --git a/server/api/api/v1/roles/:id/index.ts b/server/api/api/v1/roles/:id/index.ts index 1e7b7a38..4e79770d 100644 --- a/server/api/api/v1/roles/:id/index.ts +++ b/server/api/api/v1/roles/:id/index.ts @@ -47,7 +47,7 @@ export default (app: Hono) => const userRoles = await Role.getUserRoles( user.id, - user.getUser().isAdmin, + user.data.isAdmin, ); const role = await Role.fromId(id); @@ -62,22 +62,19 @@ export default (app: Hono) => case "POST": { const userHighestRole = userRoles.reduce((prev, current) => - prev.getRole().priority > current.getRole().priority + prev.data.priority > current.data.priority ? prev : current, ); - if ( - role.getRole().priority > - userHighestRole.getRole().priority - ) { + if (role.data.priority > userHighestRole.data.priority) { return errorResponse( `Cannot assign role '${ - role.getRole().name + role.data.name }' with priority ${ - role.getRole().priority + role.data.priority } to user with highest role priority ${ - userHighestRole.getRole().priority + userHighestRole.data.priority }`, 403, ); @@ -89,22 +86,19 @@ export default (app: Hono) => } case "DELETE": { const userHighestRole = userRoles.reduce((prev, current) => - prev.getRole().priority > current.getRole().priority + prev.data.priority > current.data.priority ? prev : current, ); - if ( - role.getRole().priority > - userHighestRole.getRole().priority - ) { + if (role.data.priority > userHighestRole.data.priority) { return errorResponse( `Cannot remove role '${ - role.getRole().name + role.data.name }' with priority ${ - role.getRole().priority + role.data.priority } from user with highest role priority ${ - userHighestRole.getRole().priority + userHighestRole.data.priority }`, 403, ); diff --git a/server/api/api/v1/roles/index.test.ts b/server/api/api/v1/roles/index.test.ts index 321d3676..18cfe7f9 100644 --- a/server/api/api/v1/roles/index.test.ts +++ b/server/api/api/v1/roles/index.test.ts @@ -10,7 +10,7 @@ let role: Role; beforeAll(async () => { // Create new role - role = await Role.new({ + role = await Role.insert({ name: "test", permissions: ADMIN_ROLES, priority: 0, diff --git a/server/api/api/v1/roles/index.ts b/server/api/api/v1/roles/index.ts index c757957f..8bd19327 100644 --- a/server/api/api/v1/roles/index.ts +++ b/server/api/api/v1/roles/index.ts @@ -29,7 +29,7 @@ export default (app: Hono) => const userRoles = await Role.getUserRoles( user.id, - user.getUser().isAdmin, + user.data.isAdmin, ); return jsonResponse(userRoles.map((r) => r.toAPI())); diff --git a/server/api/api/v1/statuses/:id/favourite.ts b/server/api/api/v1/statuses/:id/favourite.ts index a5fdba29..86926e3b 100644 --- a/server/api/api/v1/statuses/:id/favourite.ts +++ b/server/api/api/v1/statuses/:id/favourite.ts @@ -53,7 +53,7 @@ export default (app: Hono) => const existingLike = await db.query.Likes.findFirst({ where: (like, { and, eq }) => and( - eq(like.likedId, note.getStatus().id), + eq(like.likedId, note.data.id), eq(like.likerId, user.id), ), }); diff --git a/server/api/api/v1/statuses/:id/favourited_by.test.ts b/server/api/api/v1/statuses/:id/favourited_by.test.ts index a244919d..991fba72 100644 --- a/server/api/api/v1/statuses/:id/favourited_by.test.ts +++ b/server/api/api/v1/statuses/:id/favourited_by.test.ts @@ -68,7 +68,7 @@ describe(meta.route, () => { expect(objects.length).toBe(1); for (const [, status] of objects.entries()) { expect(status.id).toBe(users[1].id); - expect(status.username).toBe(users[1].getUser().username); + expect(status.username).toBe(users[1].data.username); } }); }); diff --git a/server/api/api/v1/statuses/:id/index.ts b/server/api/api/v1/statuses/:id/index.ts index c05b4437..32e30ff1 100644 --- a/server/api/api/v1/statuses/:id/index.ts +++ b/server/api/api/v1/statuses/:id/index.ts @@ -103,7 +103,7 @@ export default (app: Hono) => return jsonResponse(await foundStatus.toAPI(user)); } if (context.req.method === "DELETE") { - if (foundStatus.getAuthor().id !== user?.id) { + if (foundStatus.author.id !== user?.id) { return errorResponse("Unauthorized", 401); } @@ -112,7 +112,7 @@ export default (app: Hono) => await foundStatus.delete(); await user.federateToFollowers( - undoFederationRequest(user, foundStatus.getURI()), + undoFederationRequest(user, foundStatus.getUri()), ); return jsonResponse(await foundStatus.toAPI(user), 200); diff --git a/server/api/api/v1/statuses/:id/pin.ts b/server/api/api/v1/statuses/:id/pin.ts index a643196c..5a292c5e 100644 --- a/server/api/api/v1/statuses/:id/pin.ts +++ b/server/api/api/v1/statuses/:id/pin.ts @@ -47,17 +47,14 @@ export default (app: Hono) => if (!foundStatus) return errorResponse("Record not found", 404); - if (foundStatus.getAuthor().id !== user.id) + if (foundStatus.author.id !== user.id) return errorResponse("Unauthorized", 401); if ( await db.query.UserToPinnedNotes.findFirst({ where: (userPinnedNote, { and, eq }) => and( - eq( - userPinnedNote.noteId, - foundStatus.getStatus().id, - ), + eq(userPinnedNote.noteId, foundStatus.data.id), eq(userPinnedNote.userId, user.id), ), }) diff --git a/server/api/api/v1/statuses/:id/reblog.ts b/server/api/api/v1/statuses/:id/reblog.ts index 93818043..5251241e 100644 --- a/server/api/api/v1/statuses/:id/reblog.ts +++ b/server/api/api/v1/statuses/:id/reblog.ts @@ -58,7 +58,7 @@ export default (app: Hono) => const existingReblog = await Note.fromSql( and( eq(Notes.authorId, user.id), - eq(Notes.reblogId, foundStatus.getStatus().id), + eq(Notes.reblogId, foundStatus.data.id), ), ); @@ -68,7 +68,7 @@ export default (app: Hono) => const newReblog = await Note.insert({ authorId: user.id, - reblogId: foundStatus.getStatus().id, + reblogId: foundStatus.data.id, visibility, sensitive: false, updatedAt: new Date().toISOString(), @@ -85,12 +85,12 @@ export default (app: Hono) => return errorResponse("Failed to reblog", 500); } - if (foundStatus.getAuthor().isLocal() && user.isLocal()) { + if (foundStatus.author.isLocal() && user.isLocal()) { await db.insert(Notifications).values({ accountId: user.id, - notifiedId: foundStatus.getAuthor().id, + notifiedId: foundStatus.author.id, type: "reblog", - noteId: newReblog.reblogId, + noteId: newReblog.data.reblogId, }); } diff --git a/server/api/api/v1/statuses/:id/reblogged_by.test.ts b/server/api/api/v1/statuses/:id/reblogged_by.test.ts index 480e24f0..0db898d9 100644 --- a/server/api/api/v1/statuses/:id/reblogged_by.test.ts +++ b/server/api/api/v1/statuses/:id/reblogged_by.test.ts @@ -68,7 +68,7 @@ describe(meta.route, () => { expect(objects.length).toBe(1); for (const [, status] of objects.entries()) { expect(status.id).toBe(users[1].id); - expect(status.username).toBe(users[1].getUser().username); + expect(status.username).toBe(users[1].data.username); } }); }); diff --git a/server/api/api/v1/statuses/:id/source.ts b/server/api/api/v1/statuses/:id/source.ts index a8cf379e..4010f96c 100644 --- a/server/api/api/v1/statuses/:id/source.ts +++ b/server/api/api/v1/statuses/:id/source.ts @@ -51,8 +51,8 @@ export default (app: Hono) => return jsonResponse({ id: status.id, // TODO: Give real source for spoilerText - spoiler_text: status.getStatus().spoilerText, - text: status.getStatus().contentSource, + spoiler_text: status.data.spoilerText, + text: status.data.contentSource, } as APIStatusSource); }, ); diff --git a/server/api/api/v1/statuses/:id/unpin.ts b/server/api/api/v1/statuses/:id/unpin.ts index 76437434..841414f5 100644 --- a/server/api/api/v1/statuses/:id/unpin.ts +++ b/server/api/api/v1/statuses/:id/unpin.ts @@ -46,7 +46,7 @@ export default (app: Hono) => if (!status) return errorResponse("Record not found", 404); - if (status.getAuthor().id !== user.id) + if (status.author.id !== user.id) return errorResponse("Unauthorized", 401); await user.unpin(status); diff --git a/server/api/api/v1/statuses/:id/unreblog.ts b/server/api/api/v1/statuses/:id/unreblog.ts index 6a4f83b2..da64955d 100644 --- a/server/api/api/v1/statuses/:id/unreblog.ts +++ b/server/api/api/v1/statuses/:id/unreblog.ts @@ -53,7 +53,7 @@ export default (app: Hono) => const existingReblog = await Note.fromSql( and( eq(Notes.authorId, user.id), - eq(Notes.reblogId, foundStatus.getStatus().id), + eq(Notes.reblogId, foundStatus.data.id), ), undefined, user?.id, @@ -66,7 +66,7 @@ export default (app: Hono) => await existingReblog.delete(); await user.federateToFollowers( - undoFederationRequest(user, existingReblog.getURI()), + undoFederationRequest(user, existingReblog.getUri()), ); const newNote = await Note.fromId(id, user.id); diff --git a/server/api/api/v1/statuses/index.test.ts b/server/api/api/v1/statuses/index.test.ts index 7b7d890f..fd0ee9b7 100644 --- a/server/api/api/v1/statuses/index.test.ts +++ b/server/api/api/v1/statuses/index.test.ts @@ -320,7 +320,7 @@ describe(meta.route, () => { Authorization: `Bearer ${tokens[0].accessToken}`, }, body: new URLSearchParams({ - status: `Hello, @${users[1].getUser().username}!`, + status: `Hello, @${users[1].data.username}!`, local_only: "true", }), }), @@ -336,8 +336,8 @@ describe(meta.route, () => { expect(object.mentions).toBeArrayOfSize(1); expect(object.mentions[0]).toMatchObject({ id: users[1].id, - username: users[1].getUser().username, - acct: users[1].getUser().username, + username: users[1].data.username, + acct: users[1].data.username, }); }); @@ -349,7 +349,7 @@ describe(meta.route, () => { Authorization: `Bearer ${tokens[0].accessToken}`, }, body: new URLSearchParams({ - status: `Hello, @${users[1].getUser().username}@${ + status: `Hello, @${users[1].data.username}@${ new URL(config.http.base_url).host }!`, local_only: "true", @@ -367,8 +367,8 @@ describe(meta.route, () => { expect(object.mentions).toBeArrayOfSize(1); expect(object.mentions[0]).toMatchObject({ id: users[1].id, - username: users[1].getUser().username, - acct: users[1].getUser().username, + username: users[1].data.username, + acct: users[1].data.username, }); }); }); diff --git a/server/api/api/v2/instance/index.ts b/server/api/api/v2/instance/index.ts index efd95aa2..57d83fa7 100644 --- a/server/api/api/v2/instance/index.ts +++ b/server/api/api/v2/instance/index.ts @@ -92,7 +92,7 @@ export default (app: Hono) => url: null, }, contact: { - email: contactAccount?.getUser().email || null, + email: contactAccount?.data.email || null, account: contactAccount?.toAPI() || null, }, rules: config.signups.rules.map((rule, index) => ({ diff --git a/server/api/oauth/authorize/index.ts b/server/api/oauth/authorize/index.ts index 49a8868c..a3106ccb 100644 --- a/server/api/oauth/authorize/index.ts +++ b/server/api/oauth/authorize/index.ts @@ -250,19 +250,17 @@ export default (app: Hono) => // Include the user's profile information idTokenPayload = { ...idTokenPayload, - name: user.getUser().displayName, - preferred_username: user.getUser().username, + name: user.data.displayName, + preferred_username: user.data.username, picture: user.getAvatarUrl(config), - updated_at: new Date( - user.getUser().updatedAt, - ).toISOString(), + updated_at: new Date(user.data.updatedAt).toISOString(), }; } if (scopeIncludesEmail) { // Include the user's email address idTokenPayload = { ...idTokenPayload, - email: user.getUser().email, + email: user.data.email, // TODO: Add verification system email_verified: true, }; diff --git a/server/api/users/:uuid/inbox/index.ts b/server/api/users/:uuid/inbox/index.ts index fc03c808..96be95a4 100644 --- a/server/api/users/:uuid/inbox/index.ts +++ b/server/api/users/:uuid/inbox/index.ts @@ -148,12 +148,12 @@ export default (app: Hono) => new LogManager(Bun.stdout).log( LogLevel.DEBUG, "Inbox.Signature", - `Sender public key: ${sender.getUser().publicKey}`, + `Sender public key: ${sender.data.publicKey}`, ); } const validator = await SignatureValidator.fromStringKey( - sender.getUser().publicKey, + sender.data.publicKey, ); // If base_url uses https and request uses http, rewrite request to use https @@ -238,8 +238,8 @@ export default (app: Hono) => await db .update(Relationships) .set({ - following: !user.getUser().isLocked, - requested: user.getUser().isLocked, + following: !user.data.isLocked, + requested: user.data.isLocked, showingReblogs: true, notifying: true, languages: [], @@ -250,7 +250,7 @@ export default (app: Hono) => await db .update(Relationships) .set({ - requestedBy: user.getUser().isLocked, + requestedBy: user.data.isLocked, }) .where( and( @@ -261,13 +261,13 @@ export default (app: Hono) => await db.insert(Notifications).values({ accountId: account.id, - type: user.getUser().isLocked + type: user.data.isLocked ? "follow_request" : "follow", notifiedId: user.id, }); - if (!user.getUser().isLocked) { + if (!user.data.isLocked) { await sendFollowAccept(account, user); } diff --git a/server/api/well-known/webfinger/index.ts b/server/api/well-known/webfinger/index.ts index ebdd57b4..036c37ee 100644 --- a/server/api/well-known/webfinger/index.ts +++ b/server/api/well-known/webfinger/index.ts @@ -72,7 +72,7 @@ export default (app: Hono) => return jsonResponse({ subject: `acct:${ - isUuid ? user.id : user.getUser().username + isUuid ? user.id : user.data.username }@${host}`, links: [ diff --git a/tests/api/accounts.test.ts b/tests/api/accounts.test.ts index 8772351f..e9a1cac0 100644 --- a/tests/api/accounts.test.ts +++ b/tests/api/accounts.test.ts @@ -79,7 +79,7 @@ describe("API Tests", () => { const account = (await response.json()) as APIAccount; - expect(account.username).toBe(user.getUser().username); + expect(account.username).toBe(user.data.username); expect(account.bot).toBe(false); expect(account.locked).toBe(false); expect(account.created_at).toBeDefined(); @@ -89,7 +89,7 @@ describe("API Tests", () => { expect(account.note).toBe(""); expect(account.url).toBe( new URL( - `/@${user.getUser().username}`, + `/@${user.data.username}`, config.http.base_url, ).toString(), ); diff --git a/tests/oauth.test.ts b/tests/oauth.test.ts index acaeeba5..a728303d 100644 --- a/tests/oauth.test.ts +++ b/tests/oauth.test.ts @@ -68,7 +68,7 @@ describe("POST /api/auth/login/", () => { test("should get a JWT", async () => { const formData = new FormData(); - formData.append("identifier", users[0]?.getUser().email ?? ""); + formData.append("identifier", users[0]?.data.email ?? ""); formData.append("password", passwords[0]); const response = await sendTestRequest( diff --git a/tests/utils.ts b/tests/utils.ts index 0102d911..8dd53342 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -85,7 +85,7 @@ export const getTestStatuses = async ( user: User, partial?: Partial, ) => { - const statuses: Status[] = []; + const statuses: Note[] = []; for (let i = 0; i < count; i++) { const newStatus = await Note.insert({ @@ -116,5 +116,5 @@ export const getTestStatuses = async ( undefined, user.id, ) - ).map((n) => n.getStatus()); + ).map((n) => n.data); }; diff --git a/utils/meilisearch.ts b/utils/meilisearch.ts index ffa67e7e..0a704f45 100644 --- a/utils/meilisearch.ts +++ b/utils/meilisearch.ts @@ -58,10 +58,10 @@ export const addUserToMeilisearch = async (user: User) => { await meilisearch.index(MeiliIndexType.Accounts).addDocuments([ { id: user.id, - username: user.getUser().username, - displayName: user.getUser().displayName, - note: user.getUser().note, - createdAt: user.getUser().createdAt, + username: user.data.username, + displayName: user.data.displayName, + note: user.data.note, + createdAt: user.data.createdAt, }, ]); };