refactor(database): 🎨 Improve database handlers to have more consistent naming and methods

This commit is contained in:
Jesse Wierzbinski 2024-06-12 14:45:07 -10:00
parent a6159b9d55
commit 5565bf00de
No known key found for this signature in database
47 changed files with 365 additions and 333 deletions

View file

@ -118,13 +118,13 @@ export default class UserCreate extends BaseCommand<typeof UserCreate> {
!flags.format && !flags.format &&
this.log( this.log(
`${chalk.green("✓")} Created user ${chalk.green( `${chalk.green("✓")} Created user ${chalk.green(
user.getUser().username, user.data.username,
)} with id ${chalk.green(user.id)}`, )} with id ${chalk.green(user.id)}`,
); );
this.log( this.log(
formatArray( formatArray(
[user.getUser()], [user.data],
[ [
"id", "id",
"username", "username",
@ -144,7 +144,7 @@ export default class UserCreate extends BaseCommand<typeof UserCreate> {
flags.format flags.format
? link ? link
: `\nPassword reset link for ${chalk.bold( : `\nPassword reset link for ${chalk.bold(
`@${user.getUser().username}`, `@${user.data.username}`,
)}: ${chalk.underline(chalk.blue(link))}\n`, )}: ${chalk.underline(chalk.blue(link))}\n`,
); );

View file

@ -50,7 +50,7 @@ export default class UserDelete extends UserFinderCommand<typeof UserDelete> {
flags.print && flags.print &&
this.log( this.log(
formatArray( formatArray(
users.map((u) => u.getUser()), users.map((u) => u.data),
[ [
"id", "id",
"username", "username",

View file

@ -72,7 +72,7 @@ export default class UserList extends BaseCommand<typeof UserList> {
this.log( this.log(
formatArray( formatArray(
users.map((u) => u.getUser()), users.map((u) => u.data),
keys, keys,
flags.format as "json" | "csv" | undefined, flags.format as "json" | "csv" | undefined,
flags["pretty-dates"], flags["pretty-dates"],

View file

@ -53,7 +53,7 @@ export default class UserRefetch extends UserFinderCommand<typeof UserRefetch> {
flags.print && flags.print &&
this.log( this.log(
formatArray( formatArray(
users.map((u) => u.getUser()), users.map((u) => u.data),
[ [
"id", "id",
"username", "username",
@ -85,7 +85,7 @@ export default class UserRefetch extends UserFinderCommand<typeof UserRefetch> {
this.log( this.log(
chalk.bold( chalk.bold(
`${chalk.red("✗")} Failed to refetch user ${ `${chalk.red("✗")} Failed to refetch user ${
user.getUser().username user.data.username
}`, }`,
), ),
); );

View file

@ -60,7 +60,7 @@ export default class UserReset extends UserFinderCommand<typeof UserReset> {
flags.print && flags.print &&
this.log( this.log(
formatArray( formatArray(
users.map((u) => u.getUser()), users.map((u) => u.data),
[ [
"id", "id",
"username", "username",
@ -108,7 +108,7 @@ export default class UserReset extends UserFinderCommand<typeof UserReset> {
flags.raw flags.raw
? link ? link
: `\nPassword reset link for ${chalk.bold( : `\nPassword reset link for ${chalk.bold(
`@${user.getUser().username}`, `@${user.data.username}`,
)}: ${chalk.underline(chalk.blue(link))}\n`, )}: ${chalk.underline(chalk.blue(link))}\n`,
); );

View file

@ -15,7 +15,7 @@ export const objectToInboxRequest = async (
author: User, author: User,
userToSendTo: User, userToSendTo: User,
): Promise<Request> => { ): Promise<Request> => {
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"); 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( const privateKey = await crypto.subtle.importKey(
"pkcs8", "pkcs8",
Buffer.from(author.getUser().privateKey ?? "", "base64"), Buffer.from(author.data.privateKey ?? "", "base64"),
"Ed25519", "Ed25519",
false, false,
["sign"], ["sign"],
@ -33,7 +33,7 @@ export const objectToInboxRequest = async (
const ctor = new SignatureConstructor(privateKey, author.getUri()); 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, { const request = new Request(userInbox, {
method: "POST", method: "POST",
@ -54,7 +54,7 @@ export const objectToInboxRequest = async (
new LogManager(Bun.stdout).log( new LogManager(Bun.stdout).log(
LogLevel.DEBUG, LogLevel.DEBUG,
"Inbox.Signature", "Inbox.Signature",
`Sender public key: ${author.getUser().publicKey}`, `Sender public key: ${author.data.publicKey}`,
); );
// Log signed string // Log signed string

View file

@ -35,12 +35,12 @@ export const createLike = async (user: User, note: Note) => {
likerId: user.id, 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 // Notify the user that their post has been favourited
await db.insert(Notifications).values({ await db.insert(Notifications).values({
accountId: user.id, accountId: user.id,
type: "favourite", type: "favourite",
notifiedId: note.getAuthor().id, notifiedId: note.author.id,
noteId: note.id, noteId: note.id,
}); });
} else { } else {
@ -65,12 +65,12 @@ export const deleteLike = async (user: User, note: Note) => {
and( and(
eq(Notifications.accountId, user.id), eq(Notifications.accountId, user.id),
eq(Notifications.type, "favourite"), eq(Notifications.type, "favourite"),
eq(Notifications.notifiedId, note.getAuthor().id), eq(Notifications.notifiedId, note.author.id),
eq(Notifications.noteId, note.id), eq(Notifications.noteId, note.id),
), ),
); );
if (user.isLocal() && note.getAuthor().isRemote()) { if (user.isLocal() && note.author.isRemote()) {
// User is local, federate the delete // User is local, federate the delete
// TODO: Federate this // TODO: Federate this
} }

View file

@ -43,8 +43,7 @@ export const findManyNotifications = async (
output.map(async (notif) => ({ output.map(async (notif) => ({
...notif, ...notif,
account: transformOutputToUserWithRelations(notif.account), account: transformOutputToUserWithRelations(notif.account),
status: status: (await Note.fromId(notif.noteId, userId))?.data ?? null,
(await Note.fromId(notif.noteId, userId))?.getStatus() ?? null,
})), })),
); );
}; };
@ -59,7 +58,7 @@ export const notificationToAPI = async (
id: notification.id, id: notification.id,
type: notification.type, type: notification.type,
status: notification.status status: notification.status
? await Note.fromStatus(notification.status).toAPI(account) ? await new Note(notification.status).toAPI(account)
: undefined, : undefined,
}; };
}; };

View file

@ -323,7 +323,7 @@ export const parseTextMentions = async (text: string): Promise<User[]> => {
export const replaceTextMentions = async (text: string, mentions: User[]) => { export const replaceTextMentions = async (text: string, mentions: User[]) => {
let finalText = text; let finalText = text;
for (const mention of mentions) { for (const mention of mentions) {
const user = mention.getUser(); const user = mention.data;
// Replace @username and @username@domain // Replace @username and @username@domain
if (user.instance) { if (user.instance) {
finalText = finalText.replace( finalText = finalText.replace(
@ -439,7 +439,7 @@ export const federateNote = async (note: Note) => {
// TODO: Add queue system // TODO: Add queue system
const request = await objectToInboxRequest( const request = await objectToInboxRequest(
note.toLysand(), note.toLysand(),
note.getAuthor(), note.author,
user, user,
); );
@ -455,9 +455,7 @@ export const federateNote = async (note: Note) => {
dualLogger.log( dualLogger.log(
LogLevel.ERROR, LogLevel.ERROR,
"Federation.Status", "Federation.Status",
`Failed to federate status ${ `Failed to federate status ${note.data.id} to ${user.getUri()}`,
note.getStatus().id
} to ${user.getUri()}`,
); );
} }
} }

View file

@ -131,8 +131,8 @@ export const followRequestUser = async (
await db await db
.update(Relationships) .update(Relationships)
.set({ .set({
following: isRemote ? false : !followee.getUser().isLocked, following: isRemote ? false : !followee.data.isLocked,
requested: isRemote ? true : followee.getUser().isLocked, requested: isRemote ? true : followee.data.isLocked,
showingReblogs: reblogs, showingReblogs: reblogs,
notifying: notify, notifying: notify,
languages: languages, languages: languages,
@ -143,8 +143,8 @@ export const followRequestUser = async (
await db await db
.update(Relationships) .update(Relationships)
.set({ .set({
requestedBy: isRemote ? true : followee.getUser().isLocked, requestedBy: isRemote ? true : followee.data.isLocked,
followedBy: isRemote ? false : followee.getUser().isLocked, followedBy: isRemote ? false : followee.data.isLocked,
}) })
.where( .where(
and( and(
@ -209,7 +209,7 @@ export const followRequestUser = async (
} else { } else {
await db.insert(Notifications).values({ await db.insert(Notifications).values({
accountId: follower.id, accountId: follower.id,
type: followee.getUser().isLocked ? "follow_request" : "follow", type: followee.data.isLocked ? "follow_request" : "follow",
notifiedId: followee.id, notifiedId: followee.id,
}); });
} }
@ -522,7 +522,7 @@ export const followRequestToLysand = (
throw new Error("Followee must be a remote user"); 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"); 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"); 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"); throw new Error("Follower must have a URI in database");
} }

View file

@ -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<any>,
Columns = InferModelFromColumns<Table["_"]["columns"]>,
> {
constructor(public data: Columns) {}
public abstract save(): Promise<Columns>;
public abstract delete(ids: string[]): Promise<void>;
public abstract delete(): Promise<void>;
public abstract update(
newData: Partial<InferSelectModel<Table>>,
): Promise<Columns>;
public abstract reload(): Promise<void>;
}

View file

@ -35,7 +35,6 @@ import {
} from "~/database/entities/Emoji"; } from "~/database/entities/Emoji";
import { localObjectURI } from "~/database/entities/Federation"; import { localObjectURI } from "~/database/entities/Federation";
import { import {
type Status,
type StatusWithRelations, type StatusWithRelations,
contentToHtml, contentToHtml,
findManyNotes, findManyNotes,
@ -52,30 +51,65 @@ import {
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import type { Attachment as APIAttachment } from "~/types/mastodon/attachment"; import type { Attachment as APIAttachment } from "~/types/mastodon/attachment";
import type { Status as APIStatus } from "~/types/mastodon/status"; import type { Status as APIStatus } from "~/types/mastodon/status";
import { BaseInterface } from "./base";
import { User } from "./user"; import { User } from "./user";
/** /**
* Gives helpers to fetch notes from database in a nice format * Gives helpers to fetch notes from database in a nice format
*/ */
export class Note { export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
private constructor(private status: StatusWithRelations) {} async save(): Promise<StatusWithRelations> {
return this.update(this.data);
}
async reload(): Promise<void> {
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<typeof Notes>,
userRequestingNoteId?: string,
): Promise<Note> {
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( static async fromId(
id: string | null, id: string | null,
userId?: string, userRequestingNoteId?: string,
): Promise<Note | null> { ): Promise<Note | null> {
if (!id) return null; 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<Note[]> { static async fromIds(
ids: string[],
userRequestingNoteId?: string,
): Promise<Note[]> {
return await Note.manyFromSql( return await Note.manyFromSql(
inArray(Notes.id, ids), inArray(Notes.id, ids),
undefined, undefined,
undefined, undefined,
undefined, undefined,
userId, userRequestingNoteId,
); );
} }
@ -118,21 +152,19 @@ export class Note {
} }
get id() { get id() {
return this.status.id; return this.data.id;
} }
async getUsersToFederateTo() { async getUsersToFederateTo() {
// Mentioned users // Mentioned users
const mentionedUsers = const mentionedUsers =
this.getStatus().mentions.length > 0 this.data.mentions.length > 0
? await User.manyFromSql( ? await User.manyFromSql(
and( and(
isNotNull(Users.instanceId), isNotNull(Users.instanceId),
inArray( inArray(
Users.id, Users.id,
this.getStatus().mentions.map( this.data.mentions.map((mention) => mention.id),
(mention) => mention.id,
),
), ),
), ),
) )
@ -166,24 +198,12 @@ export class Note {
return deduplicatedUsersById; return deduplicatedUsersById;
} }
static fromStatus(status: StatusWithRelations) {
return new Note(status);
}
static fromStatuses(statuses: StatusWithRelations[]) {
return statuses.map((s) => new Note(s));
}
isNull() { isNull() {
return this.status === null; return this.data === null;
} }
getStatus() { get author() {
return this.status; return new User(this.data.author);
}
getAuthor() {
return new User(this.status.author);
} }
static async getCount() { static async getCount() {
@ -201,7 +221,7 @@ export class Note {
async getReplyChildren(userId?: string) { async getReplyChildren(userId?: string) {
return await Note.manyFromSql( return await Note.manyFromSql(
eq(Notes.replyId, this.status.id), eq(Notes.replyId, this.data.id),
undefined, undefined,
undefined, undefined,
undefined, undefined,
@ -209,12 +229,8 @@ export class Note {
); );
} }
static async insert(values: InferInsertModel<typeof Notes>) {
return (await db.insert(Notes).values(values).returning())[0];
}
async isRemote() { async isRemote() {
return this.getAuthor().isRemote(); return this.author.isRemote();
} }
async updateFromRemote() { async updateFromRemote() {
@ -222,13 +238,13 @@ export class Note {
throw new Error("Cannot refetch a local note (it is not remote)"); 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) { if (!updated) {
throw new Error("Note not found after update"); throw new Error("Note not found after update");
} }
this.status = updated.getStatus(); this.data = updated.data;
return this; 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( async updateFromData(
@ -347,7 +363,7 @@ export class Note {
// Parse emojis and fuse with existing emojis // Parse emojis and fuse with existing emojis
let foundEmojis = emojis; let foundEmojis = emojis;
if (this.getAuthor().isLocal() && htmlContent) { if (this.author.isLocal() && htmlContent) {
const parsedEmojis = await parseEmojis(htmlContent); const parsedEmojis = await parseEmojis(htmlContent);
// Fuse and deduplicate // Fuse and deduplicate
foundEmojis = [...emojis, ...parsedEmojis].filter( foundEmojis = [...emojis, ...parsedEmojis].filter(
@ -376,14 +392,14 @@ export class Note {
// Connect emojis // Connect emojis
await db await db
.delete(EmojiToNote) .delete(EmojiToNote)
.where(eq(EmojiToNote.noteId, this.status.id)); .where(eq(EmojiToNote.noteId, this.data.id));
for (const emoji of foundEmojis) { for (const emoji of foundEmojis) {
await db await db
.insert(EmojiToNote) .insert(EmojiToNote)
.values({ .values({
emojiId: emoji.id, emojiId: emoji.id,
noteId: this.status.id, noteId: this.data.id,
}) })
.execute(); .execute();
} }
@ -391,13 +407,13 @@ export class Note {
// Connect mentions // Connect mentions
await db await db
.delete(NoteToMentions) .delete(NoteToMentions)
.where(eq(NoteToMentions.noteId, this.status.id)); .where(eq(NoteToMentions.noteId, this.data.id));
for (const mention of mentions ?? []) { for (const mention of mentions ?? []) {
await db await db
.insert(NoteToMentions) .insert(NoteToMentions)
.values({ .values({
noteId: this.status.id, noteId: this.data.id,
userId: mention.id, userId: mention.id,
}) })
.execute(); .execute();
@ -410,13 +426,13 @@ export class Note {
.set({ .set({
noteId: null, noteId: null,
}) })
.where(eq(Attachments.noteId, this.status.id)); .where(eq(Attachments.noteId, this.data.id));
if (media_attachments.length > 0) if (media_attachments.length > 0)
await db await db
.update(Attachments) .update(Attachments)
.set({ .set({
noteId: this.status.id, noteId: this.data.id,
}) })
.where(inArray(Attachments.id, media_attachments)); .where(inArray(Attachments.id, media_attachments));
} }
@ -555,10 +571,10 @@ export class Note {
: [], : [],
attachments.map((a) => a.id), attachments.map((a) => a.id),
note.replies_to note.replies_to
? (await Note.resolve(note.replies_to))?.getStatus().id ? (await Note.resolve(note.replies_to))?.data.id
: undefined, : undefined,
note.quotes note.quotes
? (await Note.resolve(note.quotes))?.getStatus().id ? (await Note.resolve(note.quotes))?.data.id
: undefined, : undefined,
); );
} }
@ -582,10 +598,10 @@ export class Note {
), ),
attachments.map((a) => a.id), attachments.map((a) => a.id),
note.replies_to note.replies_to
? (await Note.resolve(note.replies_to))?.getStatus().id ? (await Note.resolve(note.replies_to))?.data.id
: undefined, : undefined,
note.quotes note.quotes
? (await Note.resolve(note.quotes))?.getStatus().id ? (await Note.resolve(note.quotes))?.data.id
: undefined, : undefined,
); );
@ -596,27 +612,28 @@ export class Note {
return createdNote; return createdNote;
} }
async delete() { async delete(ids: string[]): Promise<void>;
return ( async delete(): Promise<void>;
await db async delete(ids?: unknown): Promise<void> {
.delete(Notes) if (Array.isArray(ids)) {
.where(eq(Notes.id, this.status.id)) await db.delete(Notes).where(inArray(Notes.id, ids));
.returning() } else {
)[0]; await db.delete(Notes).where(eq(Notes.id, this.id));
}
} }
async update(newStatus: Partial<Status>) { async update(
return ( newStatus: Partial<StatusWithRelations>,
await db ): Promise<StatusWithRelations> {
.update(Notes) await db.update(Notes).set(newStatus).where(eq(Notes.id, this.data.id));
.set(newStatus)
.where(eq(Notes.id, this.status.id))
.returning()
)[0];
}
static async deleteMany(ids: string[]) { const updated = await Note.fromId(this.data.id);
return await db.delete(Notes).where(inArray(Notes.id, ids)).returning();
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. * @returns Whether this status is viewable by the user.
*/ */
async isViewableByUser(user: User | null) { async isViewableByUser(user: User | null) {
if (this.getAuthor().id === user?.id) return true; if (this.author.id === user?.id) return true;
if (this.getStatus().visibility === "public") return true; if (this.data.visibility === "public") return true;
if (this.getStatus().visibility === "unlisted") return true; if (this.data.visibility === "unlisted") return true;
if (this.getStatus().visibility === "private") { if (this.data.visibility === "private") {
return user return user
? await db.query.Relationships.findFirst({ ? await db.query.Relationships.findFirst({
where: (relationship, { and, eq }) => where: (relationship, { and, eq }) =>
@ -641,13 +658,12 @@ export class Note {
: false; : false;
} }
return ( return (
user && user && this.data.mentions.find((mention) => mention.id === user.id)
this.getStatus().mentions.find((mention) => mention.id === user.id)
); );
} }
async toAPI(userFetching?: User | null): Promise<APIStatus> { async toAPI(userFetching?: User | null): Promise<APIStatus> {
const data = this.getStatus(); const data = this.data;
// 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(
@ -684,7 +700,7 @@ export class Note {
id: data.id, id: data.id,
in_reply_to_id: data.replyId || null, in_reply_to_id: data.replyId || null,
in_reply_to_account_id: data.reply?.authorId || 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(), created_at: new Date(data.createdAt).toISOString(),
application: data.application application: data.application
? applicationToAPI(data.application) ? applicationToAPI(data.application)
@ -713,9 +729,9 @@ export class Note {
// TODO: Add polls // TODO: Add polls
poll: null, poll: null,
reblog: data.reblog reblog: data.reblog
? await Note.fromStatus( ? await new Note(data.reblog as StatusWithRelations).toAPI(
data.reblog as StatusWithRelations, userFetching,
).toAPI(userFetching) )
: null, : null,
reblogged: data.reblogged, reblogged: data.reblogged,
reblogs_count: data.reblogCount, reblogs_count: data.reblogCount,
@ -723,9 +739,9 @@ export class Note {
sensitive: data.sensitive, sensitive: data.sensitive,
spoiler_text: data.spoilerText, spoiler_text: data.spoilerText,
tags: [], tags: [],
uri: data.uri || this.getURI(), uri: data.uri || this.getUri(),
visibility: data.visibility as APIStatus["visibility"], visibility: data.visibility as APIStatus["visibility"],
url: data.uri || this.getMastoURI(), url: data.uri || this.getMastoUri(),
bookmarked: false, bookmarked: false,
// @ts-expect-error Glitch-SOC extension // @ts-expect-error Glitch-SOC extension
quote: data.quotingId quote: data.quotingId
@ -737,30 +753,30 @@ export class Note {
}; };
} }
getURI() { getUri() {
return localObjectURI(this.getStatus().id); return localObjectURI(this.data.id);
} }
static getURI(id?: string | null) { static getUri(id?: string | null) {
if (!id) return null; if (!id) return null;
return localObjectURI(id); return localObjectURI(id);
} }
getMastoURI() { getMastoUri() {
return new URL( return new URL(
`/@${this.getAuthor().getUser().username}/${this.id}`, `/@${this.author.data.username}/${this.id}`,
config.http.base_url, config.http.base_url,
).toString(); ).toString();
} }
toLysand(): typeof EntityValidator.$Note { toLysand(): typeof EntityValidator.$Note {
const status = this.getStatus(); const status = this.data;
return { return {
type: "Note", type: "Note",
created_at: new Date(status.createdAt).toISOString(), created_at: new Date(status.createdAt).toISOString(),
id: status.id, id: status.id,
author: this.getAuthor().getUri(), author: this.author.getUri(),
uri: this.getURI(), uri: this.getUri(),
content: { content: {
"text/html": { "text/html": {
content: status.content, content: status.content,
@ -774,8 +790,8 @@ export class Note {
), ),
is_sensitive: status.sensitive, is_sensitive: status.sensitive,
mentions: status.mentions.map((mention) => mention.uri || ""), mentions: status.mentions.map((mention) => mention.uri || ""),
quotes: Note.getURI(status.quotingId) ?? undefined, quotes: Note.getUri(status.quotingId) ?? undefined,
replies_to: Note.getURI(status.replyId) ?? undefined, replies_to: Note.getUri(status.replyId) ?? undefined,
subject: status.spoilerText, subject: status.spoilerText,
visibility: status.visibility as visibility: status.visibility as
| "public" | "public"
@ -799,9 +815,9 @@ export class Note {
let currentStatus: Note = this; let currentStatus: Note = this;
while (currentStatus.getStatus().replyId) { while (currentStatus.data.replyId) {
const parent = await Note.fromId( const parent = await Note.fromId(
currentStatus.getStatus().replyId, currentStatus.data.replyId,
fetcher?.id, fetcher?.id,
); );

View file

@ -11,9 +11,20 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { db } from "~/drizzle/db"; import { db } from "~/drizzle/db";
import { RoleToUsers, Roles } from "~/drizzle/schema"; import { RoleToUsers, Roles } from "~/drizzle/schema";
import { BaseInterface } from "./base";
export class Role { export type RoleType = InferSelectModel<typeof Roles>;
private constructor(private role: InferSelectModel<typeof Roles>) {}
export class Role extends BaseInterface<typeof Roles> {
async reload(): Promise<void> {
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<typeof Roles>) { public static fromRole(role: InferSelectModel<typeof Roles>) {
return new Role(role); return new Role(role);
@ -104,26 +115,44 @@ export class Role {
return found.map((s) => new Role(s)); return found.map((s) => new Role(s));
} }
public async save( async update(newRole: Partial<RoleType>): Promise<RoleType> {
role: Partial<InferSelectModel<typeof Roles>> = this.role, await db.update(Roles).set(newRole).where(eq(Roles.id, this.id));
) {
return new Role( const updated = await Role.fromId(this.data.id);
(
await db if (!updated) {
.update(Roles) throw new Error("Failed to update role");
.set(role) }
.where(eq(Roles.id, this.id))
.returning() return updated.data;
)[0],
);
} }
public async delete() { async save(): Promise<RoleType> {
await db.delete(Roles).where(eq(Roles.id, this.id)); return this.update(this.data);
} }
public static async new(role: InferInsertModel<typeof Roles>) { async delete(ids: string[]): Promise<void>;
return new Role((await db.insert(Roles).values(role).returning())[0]); async delete(): Promise<void>;
async delete(ids?: unknown): Promise<void> {
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<typeof Roles>,
): Promise<Role> {
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) { public async linkUser(userId: string) {
@ -145,22 +174,18 @@ export class Role {
} }
get id() { get id() {
return this.role.id; return this.data.id;
}
public getRole() {
return this.role;
} }
public toAPI() { public toAPI() {
return { return {
id: this.id, id: this.id,
name: this.role.name, name: this.data.name,
permissions: this.role.permissions, permissions: this.data.permissions,
priority: this.role.priority, priority: this.data.priority,
description: this.role.description, description: this.data.description,
visible: this.role.visible, visible: this.data.visible,
icon: proxyUrl(this.role.icon), icon: proxyUrl(this.data.icon),
}; };
} }
} }

View file

@ -74,23 +74,20 @@ export class Timeline {
switch (this.type) { switch (this.type) {
case TimelineType.NOTE: { case TimelineType.NOTE: {
const objectBefore = await Note.fromSql( const objectBefore = await Note.fromSql(
gt(Notes.id, notes[0].getStatus().id), gt(Notes.id, notes[0].data.id),
); );
if (objectBefore) { if (objectBefore) {
linkHeader.push( linkHeader.push(
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${ `<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${
notes[0].getStatus().id notes[0].data.id
}>; rel="prev"`, }>; rel="prev"`,
); );
} }
if (notes.length >= (limit ?? 20)) { if (notes.length >= (limit ?? 20)) {
const objectAfter = await Note.fromSql( const objectAfter = await Note.fromSql(
gt( gt(Notes.id, notes[notes.length - 1].data.id),
Notes.id,
notes[notes.length - 1].getStatus().id,
),
); );
if (objectAfter) { if (objectAfter) {
@ -98,7 +95,7 @@ export class Timeline {
`<${urlWithoutQuery}?limit=${ `<${urlWithoutQuery}?limit=${
limit ?? 20 limit ?? 20
}&max_id=${ }&max_id=${
notes[notes.length - 1].getStatus().id notes[notes.length - 1].data.id
}>; rel="next"`, }>; rel="next"`,
); );
} }

View file

@ -42,14 +42,23 @@ import {
import { type Config, config } from "~/packages/config-manager"; import { type Config, config } from "~/packages/config-manager";
import type { Account as APIAccount } from "~/types/mastodon/account"; import type { Account as APIAccount } from "~/types/mastodon/account";
import type { Mention as APIMention } from "~/types/mastodon/mention"; import type { Mention as APIMention } from "~/types/mastodon/mention";
import { BaseInterface } from "./base";
import type { Note } from "./note"; import type { Note } from "./note";
import { Role } from "./role"; import { Role } from "./role";
/** /**
* Gives helpers to fetch users from database in a nice format * Gives helpers to fetch users from database in a nice format
*/ */
export class User { export class User extends BaseInterface<typeof Users, UserWithRelations> {
constructor(private user: UserWithRelations) {} async reload(): Promise<void> {
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<User | null> { static async fromId(id: string | null): Promise<User | null> {
if (!id) return null; if (!id) return null;
@ -93,15 +102,11 @@ export class User {
} }
get id() { get id() {
return this.user.id; return this.data.id;
}
getUser() {
return this.user;
} }
isLocal() { isLocal() {
return this.user.instanceId === null; return this.data.instanceId === null;
} }
isRemote() { isRemote() {
@ -110,8 +115,8 @@ export class User {
getUri() { getUri() {
return ( return (
this.user.uri || this.data.uri ||
new URL(`/users/${this.user.id}`, config.http.base_url).toString() new URL(`/users/${this.data.id}`, config.http.base_url).toString()
); );
} }
@ -125,12 +130,12 @@ export class User {
public getAllPermissions() { public getAllPermissions() {
return ( return (
this.user.roles this.data.roles
.flatMap((role) => role.permissions) .flatMap((role) => role.permissions)
// Add default permissions // Add default permissions
.concat(config.permissions.default) .concat(config.permissions.default)
// If admin, add admin permissions // If admin, add admin permissions
.concat(this.user.isAdmin ? config.permissions.admin : []) .concat(this.data.isAdmin ? config.permissions.admin : [])
.reduce((acc, permission) => { .reduce((acc, permission) => {
if (!acc.includes(permission)) acc.push(permission); if (!acc.includes(permission)) acc.push(permission);
return acc; return acc;
@ -169,10 +174,14 @@ export class User {
)[0].count; )[0].count;
} }
async delete() { async delete(ids: string[]): Promise<void>;
return ( async delete(): Promise<void>;
await db.delete(Users).where(eq(Users.id, this.id)).returning() async delete(ids?: unknown): Promise<void> {
)[0]; 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() { async resetPassword() {
@ -211,17 +220,8 @@ export class User {
)[0]; )[0];
} }
async save() { async save(): Promise<UserWithRelations> {
return ( return this.update(this.data);
await db
.update(Users)
.set({
...this.user,
updatedAt: new Date().toISOString(),
})
.where(eq(Users.id, this.id))
.returning()
)[0];
} }
async updateFromRemote() { async updateFromRemote() {
@ -237,7 +237,7 @@ export class User {
throw new Error("User not found after update"); throw new Error("User not found after update");
} }
this.user = updated.getUser(); this.data = updated.data;
return this; return this;
} }
@ -405,12 +405,12 @@ export class User {
* @returns The raw URL for the user's avatar * @returns The raw URL for the user's avatar
*/ */
getAvatarUrl(config: Config) { getAvatarUrl(config: Config) {
if (!this.user.avatar) if (!this.data.avatar)
return ( return (
config.defaults.avatar || 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() { static async generateKeys() {
@ -494,57 +494,50 @@ export class User {
* @returns The raw URL for the user's header * @returns The raw URL for the user's header
*/ */
getHeaderUrl(config: Config) { getHeaderUrl(config: Config) {
if (!this.user.header) return config.defaults.header || ""; if (!this.data.header) return config.defaults.header || "";
return this.user.header; return this.data.header;
} }
getAcct() { getAcct() {
return this.isLocal() return this.isLocal()
? this.user.username ? this.data.username
: `${this.user.username}@${this.user.instance?.baseUrl}`; : `${this.data.username}@${this.data.instance?.baseUrl}`;
} }
static getAcct(isLocal: boolean, username: string, baseUrl?: string) { static getAcct(isLocal: boolean, username: string, baseUrl?: string) {
return isLocal ? username : `${username}@${baseUrl}`; return isLocal ? username : `${username}@${baseUrl}`;
} }
async update(data: Partial<typeof Users.$inferSelect>) { async update(
const updated = ( newUser: Partial<UserWithRelations>,
await db ): Promise<UserWithRelations> {
.update(Users) await db.update(Users).set(newUser).where(eq(Users.id, this.id));
.set({
...data,
updatedAt: new Date().toISOString(),
})
.where(eq(Users.id, this.id))
.returning()
)[0];
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"); if (!updated) {
throw new Error("Failed to update user");
this.user = newUser.getUser(); }
// If something important is updated, federate it // If something important is updated, federate it
if ( if (
data.username || newUser.username ||
data.displayName || newUser.displayName ||
data.note || newUser.note ||
data.avatar || newUser.avatar ||
data.header || newUser.header ||
data.fields || newUser.fields ||
data.publicKey || newUser.publicKey ||
data.isAdmin || newUser.isAdmin ||
data.isBot || newUser.isBot ||
data.isLocked || newUser.isLocked ||
data.endpoints || newUser.endpoints ||
data.isDiscoverable newUser.isDiscoverable
) { ) {
await this.federateToFollowers(this.toLysand()); await this.federateToFollowers(this.toLysand());
} }
return this; return updated.data;
} }
async federateToFollowers(object: typeof EntityValidator.$Entity) { async federateToFollowers(object: typeof EntityValidator.$Entity) {
@ -569,7 +562,7 @@ export class User {
} }
toAPI(isOwnAccount = false): APIAccount { toAPI(isOwnAccount = false): APIAccount {
const user = this.getUser(); const user = this.data;
return { return {
id: user.id, id: user.id,
username: user.username, username: user.username,
@ -642,7 +635,7 @@ export class User {
throw new Error("Cannot convert remote user to Lysand format"); throw new Error("Cannot convert remote user to Lysand format");
} }
const user = this.getUser(); const user = this.data;
return { return {
id: user.id, id: user.id,
@ -709,7 +702,7 @@ export class User {
toMention(): APIMention { toMention(): APIMention {
return { return {
url: this.getUri(), url: this.getUri(),
username: this.getUser().username, username: this.data.username,
acct: this.getAcct(), acct: this.getAcct(),
id: this.id, id: this.id,
}; };

View file

@ -33,7 +33,7 @@ describe(meta.route, () => {
test("should get a JWT with email", async () => { test("should get a JWT with email", async () => {
const formData = new FormData(); const formData = new FormData();
formData.append("identifier", users[0]?.getUser().email ?? ""); formData.append("identifier", users[0]?.data.email ?? "");
formData.append("password", passwords[0]); formData.append("password", passwords[0]);
const response = await sendTestRequest( const response = await sendTestRequest(
@ -72,7 +72,7 @@ describe(meta.route, () => {
test("should get a JWT with username", async () => { test("should get a JWT with username", async () => {
const formData = new FormData(); const formData = new FormData();
formData.append("identifier", users[0]?.getUser().username ?? ""); formData.append("identifier", users[0]?.data.username ?? "");
formData.append("password", passwords[0]); formData.append("password", passwords[0]);
const response = await sendTestRequest( const response = await sendTestRequest(
@ -187,7 +187,7 @@ describe(meta.route, () => {
test("invalid password", async () => { test("invalid password", async () => {
const formData = new FormData(); const formData = new FormData();
formData.append("identifier", users[0]?.getUser().email ?? ""); formData.append("identifier", users[0]?.data.email ?? "");
formData.append("password", "password"); formData.append("password", "password");
const response = await sendTestRequest( const response = await sendTestRequest(

View file

@ -108,10 +108,7 @@ export default (app: Hono) =>
if ( if (
!user || !user ||
!(await Bun.password.verify( !(await Bun.password.verify(password, user.data.password || ""))
password,
user.getUser().password || "",
))
) )
return returnError( return returnError(
context.req.query(), context.req.query(),
@ -119,13 +116,13 @@ export default (app: Hono) =>
"Invalid identifier or password", "Invalid identifier or password",
); );
if (user.getUser().passwordResetToken) { if (user.data.passwordResetToken) {
return response(null, 302, { return response(null, 302, {
Location: new URL( Location: new URL(
`${ `${
config.frontend.routes.password_reset config.frontend.routes.password_reset
}?${new URLSearchParams({ }?${new URLSearchParams({
token: user.getUser().passwordResetToken ?? "", token: user.data.passwordResetToken ?? "",
login_reset: "true", login_reset: "true",
}).toString()}`, }).toString()}`,
config.http.base_url, config.http.base_url,

View file

@ -57,20 +57,17 @@ export default (app: Hono) =>
if ( if (
!user || !user ||
!(await Bun.password.verify( !(await Bun.password.verify(password, user.data.password || ""))
password,
user.getUser().password || "",
))
) )
return redirectToLogin("Invalid email or password"); return redirectToLogin("Invalid email or password");
if (user.getUser().passwordResetToken) { if (user.data.passwordResetToken) {
return response(null, 302, { return response(null, 302, {
Location: new URL( Location: new URL(
`${ `${
config.frontend.routes.password_reset config.frontend.routes.password_reset
}?${new URLSearchParams({ }?${new URLSearchParams({
token: user.getUser().passwordResetToken ?? "", token: user.data.passwordResetToken ?? "",
login_reset: "true", login_reset: "true",
}).toString()}`, }).toString()}`,
config.http.base_url, config.http.base_url,

View file

@ -35,7 +35,7 @@ describe(meta.route, () => {
test("should login with normal password", async () => { test("should login with normal password", async () => {
const formData = new FormData(); const formData = new FormData();
formData.append("identifier", users[0]?.getUser().username ?? ""); formData.append("identifier", users[0]?.data.username ?? "");
formData.append("password", passwords[0]); formData.append("password", passwords[0]);
const response = await sendTestRequest( const response = await sendTestRequest(
@ -62,7 +62,7 @@ describe(meta.route, () => {
const formData = new FormData(); const formData = new FormData();
formData.append("identifier", users[0]?.getUser().username ?? ""); formData.append("identifier", users[0]?.data.username ?? "");
formData.append("password", passwords[0]); formData.append("password", passwords[0]);
const response = await sendTestRequest( const response = await sendTestRequest(
@ -108,7 +108,7 @@ describe(meta.route, () => {
const loginFormData = new FormData(); const loginFormData = new FormData();
loginFormData.append("identifier", users[0]?.getUser().username ?? ""); loginFormData.append("identifier", users[0]?.data.username ?? "");
loginFormData.append("password", newPassword); loginFormData.append("password", newPassword);
const loginResponse = await sendTestRequest( const loginResponse = await sendTestRequest(

View file

@ -61,17 +61,17 @@ describe(meta.route, () => {
const data = (await response.json()) as APIAccount; const data = (await response.json()) as APIAccount;
expect(data).toMatchObject({ expect(data).toMatchObject({
id: users[0].id, id: users[0].id,
username: users[0].getUser().username, username: users[0].data.username,
display_name: users[0].getUser().displayName, display_name: users[0].data.displayName,
avatar: expect.any(String), avatar: expect.any(String),
header: expect.any(String), header: expect.any(String),
locked: users[0].getUser().isLocked, locked: users[0].data.isLocked,
created_at: new Date(users[0].getUser().createdAt).toISOString(), created_at: new Date(users[0].data.createdAt).toISOString(),
followers_count: 0, followers_count: 0,
following_count: 0, following_count: 0,
statuses_count: 40, statuses_count: 40,
note: users[0].getUser().note, note: users[0].data.note,
acct: users[0].getUser().username, acct: users[0].data.username,
url: expect.any(String), url: expect.any(String),
avatar_static: expect.any(String), avatar_static: expect.any(String),
header_static: expect.any(String), header_static: expect.any(String),

View file

@ -16,7 +16,7 @@ describe(meta.route, () => {
const response = await sendTestRequest( const response = await sendTestRequest(
new Request( new Request(
new URL( new URL(
`${meta.route}?acct=${users[0].getUser().username}`, `${meta.route}?acct=${users[0].data.username}`,
config.http.base_url, config.http.base_url,
), ),
{ {
@ -33,8 +33,8 @@ describe(meta.route, () => {
expect(data).toEqual( expect(data).toEqual(
expect.objectContaining({ expect.objectContaining({
id: users[0].id, id: users[0].id,
username: users[0].getUser().username, username: users[0].data.username,
display_name: users[0].getUser().displayName, display_name: users[0].data.displayName,
avatar: expect.any(String), avatar: expect.any(String),
header: expect.any(String), header: expect.any(String),
}), }),

View file

@ -16,7 +16,7 @@ describe(meta.route, () => {
const response = await sendTestRequest( const response = await sendTestRequest(
new Request( new Request(
new URL( new URL(
`${meta.route}?q=${users[0].getUser().username}`, `${meta.route}?q=${users[0].data.username}`,
config.http.base_url, config.http.base_url,
), ),
{ {
@ -34,8 +34,8 @@ describe(meta.route, () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: users[0].id, id: users[0].id,
username: users[0].getUser().username, username: users[0].data.username,
display_name: users[0].getUser().displayName, display_name: users[0].data.displayName,
avatar: expect.any(String), avatar: expect.any(String),
header: expect.any(String), header: expect.any(String),
}), }),

View file

@ -118,7 +118,7 @@ export default (app: Hono) =>
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
const self = user.getUser(); const self = user.data;
const sanitizedDisplayName = await sanitizedHtmlStrip( const sanitizedDisplayName = await sanitizedHtmlStrip(
display_name ?? "", display_name ?? "",

View file

@ -98,7 +98,7 @@ export default (app: Hono) =>
// Check if user is admin // Check if user is admin
if ( if (
!user.hasPermission(RolePermissions.MANAGE_EMOJIS) && !user.hasPermission(RolePermissions.MANAGE_EMOJIS) &&
emoji.ownerId !== user.getUser().id emoji.ownerId !== user.data.id
) { ) {
return jsonResponse( return jsonResponse(
{ {

View file

@ -77,7 +77,7 @@ beforeAll(async () => {
Authorization: `Bearer ${tokens[1].accessToken}`, Authorization: `Bearer ${tokens[1].accessToken}`,
}, },
body: new URLSearchParams({ body: new URLSearchParams({
status: `@${users[0].getUser().username} test mention`, status: `@${users[0].data.username} test mention`,
visibility: "direct", visibility: "direct",
local_only: "true", local_only: "true",
}), }),

View file

@ -12,7 +12,7 @@ let higherPriorityRole: Role;
beforeAll(async () => { beforeAll(async () => {
// Create new role // Create new role
role = await Role.new({ role = await Role.insert({
name: "test", name: "test",
permissions: DEFAULT_ROLES, permissions: DEFAULT_ROLES,
priority: 2, priority: 2,
@ -27,7 +27,7 @@ beforeAll(async () => {
await role.linkUser(users[0].id); await role.linkUser(users[0].id);
// Create new role // Create new role
roleNotLinked = await Role.new({ roleNotLinked = await Role.insert({
name: "test2", name: "test2",
permissions: ADMIN_ROLES, permissions: ADMIN_ROLES,
priority: 0, priority: 0,
@ -39,7 +39,7 @@ beforeAll(async () => {
expect(roleNotLinked).toBeDefined(); expect(roleNotLinked).toBeDefined();
// Create a role with higher priority than the user's role // Create a role with higher priority than the user's role
higherPriorityRole = await Role.new({ higherPriorityRole = await Role.insert({
name: "higherPriorityRole", name: "higherPriorityRole",
permissions: DEFAULT_ROLES, permissions: DEFAULT_ROLES,
priority: 3, // Higher priority than the user's role priority: 3, // Higher priority than the user's role
@ -149,7 +149,7 @@ describe(meta.route, () => {
}); });
test("should assign new role", async () => { test("should assign new role", async () => {
await role.save({ await role.update({
permissions: [RolePermissions.MANAGE_ROLES], permissions: [RolePermissions.MANAGE_ROLES],
}); });
@ -194,7 +194,7 @@ describe(meta.route, () => {
icon: expect.any(String), icon: expect.any(String),
}); });
await role.save({ await role.update({
permissions: [], permissions: [],
}); });
}); });
@ -243,7 +243,7 @@ describe(meta.route, () => {
test("should return 403 if user tries to add role with higher priority", async () => { test("should return 403 if user tries to add role with higher priority", async () => {
// Add MANAGE_ROLES permission to user // Add MANAGE_ROLES permission to user
await role.save({ await role.update({
permissions: [RolePermissions.MANAGE_ROLES], 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", error: "Cannot assign role 'higherPriorityRole' with priority 3 to user with highest role priority 0",
}); });
await role.save({ await role.update({
permissions: [], permissions: [],
}); });
}); });

View file

@ -47,7 +47,7 @@ export default (app: Hono) =>
const userRoles = await Role.getUserRoles( const userRoles = await Role.getUserRoles(
user.id, user.id,
user.getUser().isAdmin, user.data.isAdmin,
); );
const role = await Role.fromId(id); const role = await Role.fromId(id);
@ -62,22 +62,19 @@ export default (app: Hono) =>
case "POST": { case "POST": {
const userHighestRole = userRoles.reduce((prev, current) => const userHighestRole = userRoles.reduce((prev, current) =>
prev.getRole().priority > current.getRole().priority prev.data.priority > current.data.priority
? prev ? prev
: current, : current,
); );
if ( if (role.data.priority > userHighestRole.data.priority) {
role.getRole().priority >
userHighestRole.getRole().priority
) {
return errorResponse( return errorResponse(
`Cannot assign role '${ `Cannot assign role '${
role.getRole().name role.data.name
}' with priority ${ }' with priority ${
role.getRole().priority role.data.priority
} to user with highest role priority ${ } to user with highest role priority ${
userHighestRole.getRole().priority userHighestRole.data.priority
}`, }`,
403, 403,
); );
@ -89,22 +86,19 @@ export default (app: Hono) =>
} }
case "DELETE": { case "DELETE": {
const userHighestRole = userRoles.reduce((prev, current) => const userHighestRole = userRoles.reduce((prev, current) =>
prev.getRole().priority > current.getRole().priority prev.data.priority > current.data.priority
? prev ? prev
: current, : current,
); );
if ( if (role.data.priority > userHighestRole.data.priority) {
role.getRole().priority >
userHighestRole.getRole().priority
) {
return errorResponse( return errorResponse(
`Cannot remove role '${ `Cannot remove role '${
role.getRole().name role.data.name
}' with priority ${ }' with priority ${
role.getRole().priority role.data.priority
} from user with highest role priority ${ } from user with highest role priority ${
userHighestRole.getRole().priority userHighestRole.data.priority
}`, }`,
403, 403,
); );

View file

@ -10,7 +10,7 @@ let role: Role;
beforeAll(async () => { beforeAll(async () => {
// Create new role // Create new role
role = await Role.new({ role = await Role.insert({
name: "test", name: "test",
permissions: ADMIN_ROLES, permissions: ADMIN_ROLES,
priority: 0, priority: 0,

View file

@ -29,7 +29,7 @@ export default (app: Hono) =>
const userRoles = await Role.getUserRoles( const userRoles = await Role.getUserRoles(
user.id, user.id,
user.getUser().isAdmin, user.data.isAdmin,
); );
return jsonResponse(userRoles.map((r) => r.toAPI())); return jsonResponse(userRoles.map((r) => r.toAPI()));

View file

@ -53,7 +53,7 @@ export default (app: Hono) =>
const existingLike = await db.query.Likes.findFirst({ const existingLike = await db.query.Likes.findFirst({
where: (like, { and, eq }) => where: (like, { and, eq }) =>
and( and(
eq(like.likedId, note.getStatus().id), eq(like.likedId, note.data.id),
eq(like.likerId, user.id), eq(like.likerId, user.id),
), ),
}); });

View file

@ -68,7 +68,7 @@ describe(meta.route, () => {
expect(objects.length).toBe(1); expect(objects.length).toBe(1);
for (const [, status] of objects.entries()) { for (const [, status] of objects.entries()) {
expect(status.id).toBe(users[1].id); expect(status.id).toBe(users[1].id);
expect(status.username).toBe(users[1].getUser().username); expect(status.username).toBe(users[1].data.username);
} }
}); });
}); });

View file

@ -103,7 +103,7 @@ export default (app: Hono) =>
return jsonResponse(await foundStatus.toAPI(user)); return jsonResponse(await foundStatus.toAPI(user));
} }
if (context.req.method === "DELETE") { if (context.req.method === "DELETE") {
if (foundStatus.getAuthor().id !== user?.id) { if (foundStatus.author.id !== user?.id) {
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
} }
@ -112,7 +112,7 @@ export default (app: Hono) =>
await foundStatus.delete(); await foundStatus.delete();
await user.federateToFollowers( await user.federateToFollowers(
undoFederationRequest(user, foundStatus.getURI()), undoFederationRequest(user, foundStatus.getUri()),
); );
return jsonResponse(await foundStatus.toAPI(user), 200); return jsonResponse(await foundStatus.toAPI(user), 200);

View file

@ -47,17 +47,14 @@ export default (app: Hono) =>
if (!foundStatus) return errorResponse("Record not found", 404); if (!foundStatus) return errorResponse("Record not found", 404);
if (foundStatus.getAuthor().id !== user.id) if (foundStatus.author.id !== user.id)
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
if ( if (
await db.query.UserToPinnedNotes.findFirst({ await db.query.UserToPinnedNotes.findFirst({
where: (userPinnedNote, { and, eq }) => where: (userPinnedNote, { and, eq }) =>
and( and(
eq( eq(userPinnedNote.noteId, foundStatus.data.id),
userPinnedNote.noteId,
foundStatus.getStatus().id,
),
eq(userPinnedNote.userId, user.id), eq(userPinnedNote.userId, user.id),
), ),
}) })

View file

@ -58,7 +58,7 @@ export default (app: Hono) =>
const existingReblog = await Note.fromSql( const existingReblog = await Note.fromSql(
and( and(
eq(Notes.authorId, user.id), 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({ const newReblog = await Note.insert({
authorId: user.id, authorId: user.id,
reblogId: foundStatus.getStatus().id, reblogId: foundStatus.data.id,
visibility, visibility,
sensitive: false, sensitive: false,
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
@ -85,12 +85,12 @@ export default (app: Hono) =>
return errorResponse("Failed to reblog", 500); return errorResponse("Failed to reblog", 500);
} }
if (foundStatus.getAuthor().isLocal() && user.isLocal()) { if (foundStatus.author.isLocal() && user.isLocal()) {
await db.insert(Notifications).values({ await db.insert(Notifications).values({
accountId: user.id, accountId: user.id,
notifiedId: foundStatus.getAuthor().id, notifiedId: foundStatus.author.id,
type: "reblog", type: "reblog",
noteId: newReblog.reblogId, noteId: newReblog.data.reblogId,
}); });
} }

View file

@ -68,7 +68,7 @@ describe(meta.route, () => {
expect(objects.length).toBe(1); expect(objects.length).toBe(1);
for (const [, status] of objects.entries()) { for (const [, status] of objects.entries()) {
expect(status.id).toBe(users[1].id); expect(status.id).toBe(users[1].id);
expect(status.username).toBe(users[1].getUser().username); expect(status.username).toBe(users[1].data.username);
} }
}); });
}); });

View file

@ -51,8 +51,8 @@ export default (app: Hono) =>
return jsonResponse({ return jsonResponse({
id: status.id, id: status.id,
// TODO: Give real source for spoilerText // TODO: Give real source for spoilerText
spoiler_text: status.getStatus().spoilerText, spoiler_text: status.data.spoilerText,
text: status.getStatus().contentSource, text: status.data.contentSource,
} as APIStatusSource); } as APIStatusSource);
}, },
); );

View file

@ -46,7 +46,7 @@ export default (app: Hono) =>
if (!status) return errorResponse("Record not found", 404); if (!status) return errorResponse("Record not found", 404);
if (status.getAuthor().id !== user.id) if (status.author.id !== user.id)
return errorResponse("Unauthorized", 401); return errorResponse("Unauthorized", 401);
await user.unpin(status); await user.unpin(status);

View file

@ -53,7 +53,7 @@ export default (app: Hono) =>
const existingReblog = await Note.fromSql( const existingReblog = await Note.fromSql(
and( and(
eq(Notes.authorId, user.id), eq(Notes.authorId, user.id),
eq(Notes.reblogId, foundStatus.getStatus().id), eq(Notes.reblogId, foundStatus.data.id),
), ),
undefined, undefined,
user?.id, user?.id,
@ -66,7 +66,7 @@ export default (app: Hono) =>
await existingReblog.delete(); await existingReblog.delete();
await user.federateToFollowers( await user.federateToFollowers(
undoFederationRequest(user, existingReblog.getURI()), undoFederationRequest(user, existingReblog.getUri()),
); );
const newNote = await Note.fromId(id, user.id); const newNote = await Note.fromId(id, user.id);

View file

@ -320,7 +320,7 @@ describe(meta.route, () => {
Authorization: `Bearer ${tokens[0].accessToken}`, Authorization: `Bearer ${tokens[0].accessToken}`,
}, },
body: new URLSearchParams({ body: new URLSearchParams({
status: `Hello, @${users[1].getUser().username}!`, status: `Hello, @${users[1].data.username}!`,
local_only: "true", local_only: "true",
}), }),
}), }),
@ -336,8 +336,8 @@ describe(meta.route, () => {
expect(object.mentions).toBeArrayOfSize(1); expect(object.mentions).toBeArrayOfSize(1);
expect(object.mentions[0]).toMatchObject({ expect(object.mentions[0]).toMatchObject({
id: users[1].id, id: users[1].id,
username: users[1].getUser().username, username: users[1].data.username,
acct: users[1].getUser().username, acct: users[1].data.username,
}); });
}); });
@ -349,7 +349,7 @@ describe(meta.route, () => {
Authorization: `Bearer ${tokens[0].accessToken}`, Authorization: `Bearer ${tokens[0].accessToken}`,
}, },
body: new URLSearchParams({ body: new URLSearchParams({
status: `Hello, @${users[1].getUser().username}@${ status: `Hello, @${users[1].data.username}@${
new URL(config.http.base_url).host new URL(config.http.base_url).host
}!`, }!`,
local_only: "true", local_only: "true",
@ -367,8 +367,8 @@ describe(meta.route, () => {
expect(object.mentions).toBeArrayOfSize(1); expect(object.mentions).toBeArrayOfSize(1);
expect(object.mentions[0]).toMatchObject({ expect(object.mentions[0]).toMatchObject({
id: users[1].id, id: users[1].id,
username: users[1].getUser().username, username: users[1].data.username,
acct: users[1].getUser().username, acct: users[1].data.username,
}); });
}); });
}); });

View file

@ -92,7 +92,7 @@ export default (app: Hono) =>
url: null, url: null,
}, },
contact: { contact: {
email: contactAccount?.getUser().email || null, email: contactAccount?.data.email || null,
account: contactAccount?.toAPI() || null, account: contactAccount?.toAPI() || null,
}, },
rules: config.signups.rules.map((rule, index) => ({ rules: config.signups.rules.map((rule, index) => ({

View file

@ -250,19 +250,17 @@ export default (app: Hono) =>
// Include the user's profile information // Include the user's profile information
idTokenPayload = { idTokenPayload = {
...idTokenPayload, ...idTokenPayload,
name: user.getUser().displayName, name: user.data.displayName,
preferred_username: user.getUser().username, preferred_username: user.data.username,
picture: user.getAvatarUrl(config), picture: user.getAvatarUrl(config),
updated_at: new Date( updated_at: new Date(user.data.updatedAt).toISOString(),
user.getUser().updatedAt,
).toISOString(),
}; };
} }
if (scopeIncludesEmail) { if (scopeIncludesEmail) {
// Include the user's email address // Include the user's email address
idTokenPayload = { idTokenPayload = {
...idTokenPayload, ...idTokenPayload,
email: user.getUser().email, email: user.data.email,
// TODO: Add verification system // TODO: Add verification system
email_verified: true, email_verified: true,
}; };

View file

@ -148,12 +148,12 @@ export default (app: Hono) =>
new LogManager(Bun.stdout).log( new LogManager(Bun.stdout).log(
LogLevel.DEBUG, LogLevel.DEBUG,
"Inbox.Signature", "Inbox.Signature",
`Sender public key: ${sender.getUser().publicKey}`, `Sender public key: ${sender.data.publicKey}`,
); );
} }
const validator = await SignatureValidator.fromStringKey( 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 // If base_url uses https and request uses http, rewrite request to use https
@ -238,8 +238,8 @@ export default (app: Hono) =>
await db await db
.update(Relationships) .update(Relationships)
.set({ .set({
following: !user.getUser().isLocked, following: !user.data.isLocked,
requested: user.getUser().isLocked, requested: user.data.isLocked,
showingReblogs: true, showingReblogs: true,
notifying: true, notifying: true,
languages: [], languages: [],
@ -250,7 +250,7 @@ export default (app: Hono) =>
await db await db
.update(Relationships) .update(Relationships)
.set({ .set({
requestedBy: user.getUser().isLocked, requestedBy: user.data.isLocked,
}) })
.where( .where(
and( and(
@ -261,13 +261,13 @@ export default (app: Hono) =>
await db.insert(Notifications).values({ await db.insert(Notifications).values({
accountId: account.id, accountId: account.id,
type: user.getUser().isLocked type: user.data.isLocked
? "follow_request" ? "follow_request"
: "follow", : "follow",
notifiedId: user.id, notifiedId: user.id,
}); });
if (!user.getUser().isLocked) { if (!user.data.isLocked) {
await sendFollowAccept(account, user); await sendFollowAccept(account, user);
} }

View file

@ -72,7 +72,7 @@ export default (app: Hono) =>
return jsonResponse({ return jsonResponse({
subject: `acct:${ subject: `acct:${
isUuid ? user.id : user.getUser().username isUuid ? user.id : user.data.username
}@${host}`, }@${host}`,
links: [ links: [

View file

@ -79,7 +79,7 @@ describe("API Tests", () => {
const account = (await response.json()) as APIAccount; 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.bot).toBe(false);
expect(account.locked).toBe(false); expect(account.locked).toBe(false);
expect(account.created_at).toBeDefined(); expect(account.created_at).toBeDefined();
@ -89,7 +89,7 @@ describe("API Tests", () => {
expect(account.note).toBe(""); expect(account.note).toBe("");
expect(account.url).toBe( expect(account.url).toBe(
new URL( new URL(
`/@${user.getUser().username}`, `/@${user.data.username}`,
config.http.base_url, config.http.base_url,
).toString(), ).toString(),
); );

View file

@ -68,7 +68,7 @@ describe("POST /api/auth/login/", () => {
test("should get a JWT", async () => { test("should get a JWT", async () => {
const formData = new FormData(); const formData = new FormData();
formData.append("identifier", users[0]?.getUser().email ?? ""); formData.append("identifier", users[0]?.data.email ?? "");
formData.append("password", passwords[0]); formData.append("password", passwords[0]);
const response = await sendTestRequest( const response = await sendTestRequest(

View file

@ -85,7 +85,7 @@ export const getTestStatuses = async (
user: User, user: User,
partial?: Partial<Status>, partial?: Partial<Status>,
) => { ) => {
const statuses: Status[] = []; const statuses: Note[] = [];
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const newStatus = await Note.insert({ const newStatus = await Note.insert({
@ -116,5 +116,5 @@ export const getTestStatuses = async (
undefined, undefined,
user.id, user.id,
) )
).map((n) => n.getStatus()); ).map((n) => n.data);
}; };

View file

@ -58,10 +58,10 @@ export const addUserToMeilisearch = async (user: User) => {
await meilisearch.index(MeiliIndexType.Accounts).addDocuments([ await meilisearch.index(MeiliIndexType.Accounts).addDocuments([
{ {
id: user.id, id: user.id,
username: user.getUser().username, username: user.data.username,
displayName: user.getUser().displayName, displayName: user.data.displayName,
note: user.getUser().note, note: user.data.note,
createdAt: user.getUser().createdAt, createdAt: user.data.createdAt,
}, },
]); ]);
}; };