mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
refactor(database): 🎨 Improve database handlers to have more consistent naming and methods
This commit is contained in:
parent
a6159b9d55
commit
5565bf00de
|
|
@ -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`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}`,
|
}`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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()}`,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
21
packages/database-interface/base.ts
Normal file
21
packages/database-interface/base.ts
Normal 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>;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "",
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()));
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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) => ({
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue