HUGE rewrite to use Prisma instead of TypeORM (not finished yet)

This commit is contained in:
Jesse Wierzbinski 2023-11-10 16:36:06 -10:00
parent a1c0164e9d
commit 5eed8374cd
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
53 changed files with 1452 additions and 1967 deletions

View file

@ -2,10 +2,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { getConfig } from "@config"; import { getConfig } from "@config";
import { AppDataSource } from "~database/datasource"; import { AppDataSource } from "~database/datasource";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
import { RawActivity } from "~database/entities/RawActivity"; import { RawActivity } from "~database/entities/RawActivity";
import { Token, TokenType } from "~database/entities/Token"; import { Token, TokenType } from "~database/entities/Token";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
const config = getConfig(); const config = getConfig();
@ -13,14 +13,14 @@ let token: Token;
if (!AppDataSource.isInitialized) await AppDataSource.initialize(); if (!AppDataSource.isInitialized) await AppDataSource.initialize();
// Initialize test user // Initialize test user
const user = await User.createNewLocal({ const user = await UserAction.createNewLocal({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
display_name: "", display_name: "",
}); });
const app = new Application(); const app = new ApplicationAction();
app.name = "Test Application"; app.name = "Test Application";
app.website = "https://example.com"; app.website = "https://example.com";

BIN
bun.lockb

Binary file not shown.

View file

@ -1,5 +1,6 @@
import { DataSource } from "typeorm"; import { DataSource } from "typeorm";
import { getConfig } from "../utils/config"; import { getConfig } from "../utils/config";
import { PrismaClient } from "@prisma/client";
const config = getConfig(); const config = getConfig();
@ -14,4 +15,8 @@ const AppDataSource = new DataSource({
entities: [process.cwd() + "/database/entities/*.ts"], entities: [process.cwd() + "/database/entities/*.ts"],
}); });
export { AppDataSource }; const client = new PrismaClient({
datasourceUrl: `postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}`,
});
export { AppDataSource, client };

View file

@ -1,76 +1,42 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
import { APIApplication } from "~types/entities/application"; import { APIApplication } from "~types/entities/application";
import { Token } from "./Token"; import { Application } from "@prisma/client";
import { client } from "~database/datasource";
/** /**
* Represents an application that can authenticate with the API. * Represents an application that can authenticate with the API.
*/ */
@Entity({
name: "applications",
})
export class Application extends BaseEntity {
/** The unique identifier for this application. */
@PrimaryGeneratedColumn("uuid")
id!: string;
/** The name of this application. */ /**
@Column("varchar")
name!: string;
/** The website associated with this application, if any. */
@Column("varchar", {
nullable: true,
})
website!: string | null;
/** The VAPID key associated with this application, if any. */
@Column("varchar", {
nullable: true,
})
vapid_key!: string | null;
/** The client ID associated with this application. */
@Column("varchar")
client_id!: string;
/** The secret associated with this application. */
@Column("varchar")
secret!: string;
/** The scopes associated with this application. */
@Column("varchar")
scopes = "read";
/** The redirect URIs associated with this application. */
@Column("varchar")
redirect_uris = "urn:ietf:wg:oauth:2.0:oob";
/**
* Retrieves the application associated with the given access token. * Retrieves the application associated with the given access token.
* @param token The access token to retrieve the application for. * @param token The access token to retrieve the application for.
* @returns The application associated with the given access token, or null if no such application exists. * @returns The application associated with the given access token, or null if no such application exists.
*/ */
static async getFromToken(token: string): Promise<Application | null> { export const getFromToken = async (
const dbToken = await Token.findOne({ token: string
): Promise<Application | null> => {
const dbToken = await client.token.findFirst({
where: { where: {
access_token: token, access_token: token,
}, },
relations: ["application"], include: {
application: true,
},
}); });
return dbToken?.application || null; return dbToken?.application || null;
} };
/** /**
* Converts this application to an API application. * Converts this application to an API application.
* @returns The API application representation of this application. * @returns The API application representation of this application.
*/ */
export const applicationToAPI = async (
app: Application
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
async toAPI(): Promise<APIApplication> { ): Promise<APIApplication> => {
return { return {
name: this.name, name: app.name,
website: this.website, website: app.website,
vapid_key: this.vapid_key, vapid_key: app.vapid_key,
}; };
} };
}

View file

@ -1,137 +1,78 @@
import {
BaseEntity,
Column,
Entity,
IsNull,
ManyToOne,
PrimaryGeneratedColumn,
} from "typeorm";
import { APIEmoji } from "~types/entities/emoji"; import { APIEmoji } from "~types/entities/emoji";
import { Instance } from "./Instance";
import { Emoji as LysandEmoji } from "~types/lysand/extensions/org.lysand/custom_emojis"; import { Emoji as LysandEmoji } from "~types/lysand/extensions/org.lysand/custom_emojis";
import { client } from "~database/datasource";
import { Emoji } from "@prisma/client";
/** /**
* Represents an emoji entity in the database. * Represents an emoji entity in the database.
*/ */
@Entity({
name: "emojis",
})
export class Emoji extends BaseEntity {
/**
* The unique identifier for the emoji.
*/
@PrimaryGeneratedColumn("uuid")
id!: string;
/** /**
* The shortcode for the emoji.
*/
@Column("varchar")
shortcode!: string;
/**
* The instance that the emoji is from.
* If is null, the emoji is from the server's instance
*/
@ManyToOne(() => Instance, {
nullable: true,
})
instance!: Instance | null;
/**
* The URL for the emoji.
*/
@Column("varchar")
url!: string;
/**
* The alt text for the emoji.
*/
@Column("varchar", {
nullable: true,
})
alt!: string | null;
/**
* The content type of the emoji.
*/
@Column("varchar")
content_type!: string;
/**
* Whether the emoji is visible in the picker.
*/
@Column("boolean")
visible_in_picker!: boolean;
/**
* Used for parsing emojis from local text * Used for parsing emojis from local text
* @param text The text to parse * @param text The text to parse
* @returns An array of emojis * @returns An array of emojis
*/ */
static async parseEmojis(text: string): Promise<Emoji[]> { export const parseEmojis = async (text: string): Promise<Emoji[]> => {
const regex = /:[a-zA-Z0-9_]+:/g; const regex = /:[a-zA-Z0-9_]+:/g;
const matches = text.match(regex); const matches = text.match(regex);
if (!matches) return []; if (!matches) return [];
return ( return await client.emoji.findMany({
await Promise.all(
matches.map(match =>
Emoji.findOne({
where: { where: {
shortcode: match.slice(1, -1), shortcode: {
instance: IsNull(), in: matches.map(match => match.replace(/:/g, "")),
}, },
relations: ["instance"], },
}) include: {
) instance: true,
)
).filter(emoji => emoji !== null) as Emoji[];
}
static async addIfNotExists(emoji: LysandEmoji) {
const existingEmoji = await Emoji.findOne({
where: {
shortcode: emoji.name,
instance: IsNull(),
}, },
}); });
if (existingEmoji) return existingEmoji; };
const newEmoji = new Emoji();
newEmoji.shortcode = emoji.name;
// TODO: Content types
newEmoji.url = emoji.url[0].content;
newEmoji.alt = emoji.alt || null;
newEmoji.content_type = emoji.url[0].content_type;
newEmoji.visible_in_picker = true;
await newEmoji.save();
return newEmoji;
}
/** export const addEmojiIfNotExists = async (emoji: LysandEmoji) => {
const existingEmoji = await client.emoji.findFirst({
where: {
shortcode: emoji.name,
instance: null,
},
});
if (existingEmoji) return existingEmoji;
return await client.emoji.create({
data: {
shortcode: emoji.name,
url: emoji.url[0].content,
alt: emoji.alt || null,
content_type: emoji.url[0].content_type,
visible_in_picker: true,
},
});
};
/**
* Converts the emoji to an APIEmoji object. * Converts the emoji to an APIEmoji object.
* @returns The APIEmoji object. * @returns The APIEmoji object.
*/ */
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
async toAPI(): Promise<APIEmoji> { export const emojiToAPI = async (emoji: Emoji): Promise<APIEmoji> => {
return { return {
shortcode: this.shortcode, shortcode: emoji.shortcode,
static_url: this.url, // TODO: Add static version static_url: emoji.url, // TODO: Add static version
url: this.url, url: emoji.url,
visible_in_picker: this.visible_in_picker, visible_in_picker: emoji.visible_in_picker,
category: undefined, category: undefined,
}; };
} };
toLysand(): LysandEmoji { export const emojiToLysand = (emoji: Emoji): LysandEmoji => {
return { return {
name: this.shortcode, name: emoji.shortcode,
url: [ url: [
{ {
content: this.url, content: emoji.url,
content_type: this.content_type, content_type: emoji.content_type,
}, },
], ],
alt: this.alt || undefined, alt: emoji.alt || undefined,
}; };
} };
}

View file

@ -1,59 +1,23 @@
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; import { Instance } from "@prisma/client";
import { ContentFormat, ServerMetadata } from "~types/lysand/Object"; import { client } from "~database/datasource";
import { ServerMetadata } from "~types/lysand/Object";
/** /**
* Represents an instance in the database. * Represents an instance in the database.
*/ */
@Entity({
name: "instances",
})
export class Instance extends BaseEntity {
/**
* The unique identifier of the instance.
*/
@PrimaryGeneratedColumn("uuid")
id!: string;
/** /**
* The base URL of the instance.
* Must not have the https:// or http:// prefix.
*/
@Column("varchar")
base_url!: string;
/**
* The name of the instance.
*/
@Column("varchar")
name!: string;
/**
* The description of the instance.
*/
@Column("varchar")
version!: string;
/**
* The logo of the instance.
*/
@Column("jsonb")
logo?: ContentFormat[];
/**
* The banner of the instance.
*/
banner?: ContentFormat[];
/**
* Adds an instance to the database if it doesn't already exist. * Adds an instance to the database if it doesn't already exist.
* @param url * @param url
* @returns Either the database instance if it already exists, or a newly created instance. * @returns Either the database instance if it already exists, or a newly created instance.
*/ */
static async addIfNotExists(url: string): Promise<Instance> { export const addInstanceIfNotExists = async (
url: string
): Promise<Instance> => {
const origin = new URL(url).origin; const origin = new URL(url).origin;
const hostname = new URL(url).hostname; const hostname = new URL(url).hostname;
const found = await Instance.findOne({ const found = await client.instance.findFirst({
where: { where: {
base_url: hostname, base_url: hostname,
}, },
@ -61,13 +25,9 @@ export class Instance extends BaseEntity {
if (found) return found; if (found) return found;
const instance = new Instance();
instance.base_url = hostname;
// Fetch the instance configuration // Fetch the instance configuration
const metadata = (await fetch(`${origin}/.well-known/lysand`).then( const metadata = (await fetch(`${origin}/.well-known/lysand`).then(res =>
res => res.json() res.json()
)) as Partial<ServerMetadata>; )) as Partial<ServerMetadata>;
if (metadata.type !== "ServerMetadata") { if (metadata.type !== "ServerMetadata") {
@ -78,13 +38,12 @@ export class Instance extends BaseEntity {
throw new Error("Invalid instance metadata"); throw new Error("Invalid instance metadata");
} }
instance.name = metadata.name; return await client.instance.create({
instance.version = metadata.version; data: {
instance.logo = metadata.logo; base_url: hostname,
instance.banner = metadata.banner; name: metadata.name,
version: metadata.version,
await instance.save(); logo: metadata.logo as any,
},
return instance; });
} };
}

View file

@ -1,45 +1,19 @@
import { /* eslint-disable @typescript-eslint/no-unsafe-member-access */
BaseEntity,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from "typeorm";
import { User } from "./User";
import { Status } from "./Status";
import { Like as LysandLike } from "~types/lysand/Object"; import { Like as LysandLike } from "~types/lysand/Object";
import { getConfig } from "@config"; import { getConfig } from "@config";
import { Like } from "@prisma/client";
/** /**
* Represents a Like entity in the database. * Represents a Like entity in the database.
*/ */
@Entity({
name: "likes",
})
export class Like extends BaseEntity {
/** The unique identifier of the Like. */
@PrimaryGeneratedColumn("uuid")
id!: string;
/** The User who liked the Status. */ export const toLysand = (like: Like): LysandLike => {
@ManyToOne(() => User)
liker!: User;
/** The Status that was liked. */
@ManyToOne(() => Status)
liked!: Status;
@CreateDateColumn()
created_at!: Date;
toLysand(): LysandLike {
return { return {
id: this.id, id: like.id,
author: this.liker.uri, author: (like as any).liker?.uri,
type: "Like", type: "Like",
created_at: new Date(this.created_at).toISOString(), created_at: new Date(like.createdAt).toISOString(),
object: this.liked.toLysand().uri, object: (like as any).liked?.uri,
uri: `${getConfig().http.base_url}/actions/${this.id}`, uri: `${getConfig().http.base_url}/actions/${like.id}`,
}; };
} };
}

View file

@ -1,98 +1,39 @@
import { /* eslint-disable @typescript-eslint/no-unsafe-return */
BaseEntity, /* eslint-disable @typescript-eslint/no-unsafe-member-access */
Column, import { LysandObject } from "@prisma/client";
Entity, import { client } from "~database/datasource";
ManyToOne,
PrimaryGeneratedColumn,
} from "typeorm";
import { LysandObjectType } from "~types/lysand/Object"; import { LysandObjectType } from "~types/lysand/Object";
/** /**
* Represents a Lysand object in the database. * Represents a Lysand object in the database.
*/ */
@Entity({
name: "objects",
})
export class LysandObject extends BaseEntity {
/**
* The unique identifier for the object. If local, same as `remote_id`
*/
@PrimaryGeneratedColumn("uuid")
id!: string;
/** export const createFromObject = async (object: LysandObjectType) => {
* UUID of the object across the network. If the object is local, same as `id` const foundObject = await client.lysandObject.findFirst({
*/
remote_id!: string;
/**
* Any valid Lysand type, such as `Note`, `Like`, `Follow`, etc.
*/
@Column("varchar")
type!: string;
/**
* Remote URI for the object
* Example: `https://example.com/publications/ef235cc6-d68c-4756-b0df-4e6623c4d51c`
*/
@Column("varchar")
uri!: string;
@Column("timestamp")
created_at!: Date;
/**
* References an Actor object
*/
@ManyToOne(() => LysandObject, object => object.uri, {
nullable: true,
})
author!: LysandObject | null;
@Column("jsonb")
extra_data!: Omit<
Omit<Omit<Omit<LysandObjectType, "created_at">, "id">, "uri">,
"type"
>;
@Column("jsonb")
extensions!: Record<string, any>;
static new(type: string, uri: string): LysandObject {
const object = new LysandObject();
object.type = type;
object.uri = uri;
object.created_at = new Date();
return object;
}
static async createFromObject(object: LysandObjectType) {
let newObject: LysandObject;
const foundObject = await LysandObject.findOne({
where: { remote_id: object.id }, where: { remote_id: object.id },
relations: ["author"], include: {
author: true,
},
}); });
if (foundObject) { if (foundObject) {
newObject = foundObject; return foundObject;
} else {
newObject = new LysandObject();
} }
const author = await LysandObject.findOne({ const author = await client.lysandObject.findFirst({
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
where: { uri: (object as any).author }, where: { uri: (object as any).author },
}); });
newObject.author = author; return await client.lysandObject.create({
newObject.created_at = new Date(object.created_at); data: {
newObject.extensions = object.extensions || {}; authorId: author?.id,
newObject.remote_id = object.id; created_at: new Date(object.created_at),
newObject.type = object.type; extensions: object.extensions || {},
newObject.uri = object.uri; remote_id: object.id,
type: object.type,
uri: object.uri,
// Rest of data (remove id, author, created_at, extensions, type, uri) // Rest of data (remove id, author, created_at, extensions, type, uri)
newObject.extra_data = Object.fromEntries( extra_data: Object.fromEntries(
Object.entries(object).filter( Object.entries(object).filter(
([key]) => ([key]) =>
![ ![
@ -104,28 +45,28 @@ export class LysandObject extends BaseEntity {
"uri", "uri",
].includes(key) ].includes(key)
) )
); ),
},
});
};
await newObject.save(); export const toLysand = (lyObject: LysandObject): LysandObjectType => {
return newObject;
}
toLysand(): LysandObjectType {
return { return {
id: this.remote_id || this.id, id: lyObject.remote_id || lyObject.id,
created_at: new Date(this.created_at).toISOString(), created_at: new Date(lyObject.created_at).toISOString(),
type: this.type, type: lyObject.type,
uri: this.uri, uri: lyObject.uri,
...this.extra_data, // @ts-expect-error This works, I promise
extensions: this.extensions, ...lyObject.extra_data,
extensions: lyObject.extensions,
}; };
} };
isPublication(): boolean { export const isPublication = (lyObject: LysandObject): boolean => {
return this.type === "Note" || this.type === "Patch"; return lyObject.type === "Note" || lyObject.type === "Patch";
} };
isAction(): boolean { export const isAction = (lyObject: LysandObject): boolean => {
return [ return [
"Like", "Like",
"Follow", "Follow",
@ -134,14 +75,13 @@ export class LysandObject extends BaseEntity {
"FollowReject", "FollowReject",
"Undo", "Undo",
"Announce", "Announce",
].includes(this.type); ].includes(lyObject.type);
} };
isActor(): boolean { export const isActor = (lyObject: LysandObject): boolean => {
return this.type === "User"; return lyObject.type === "User";
} };
isExtension(): boolean { export const isExtension = (lyObject: LysandObject): boolean => {
return this.type === "Extension"; return lyObject.type === "Extension";
} };
}

View file

@ -1,144 +1,63 @@
import { import { Relationship, User } from "@prisma/client";
BaseEntity,
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
import { User } from "./User";
import { APIRelationship } from "~types/entities/relationship"; import { APIRelationship } from "~types/entities/relationship";
import { client } from "~database/datasource";
/** /**
* Stores Mastodon API relationships * Stores Mastodon API relationships
*/ */
@Entity({
name: "relationships",
})
export class Relationship extends BaseEntity {
/** The unique identifier for the relationship. */
@PrimaryGeneratedColumn("uuid")
id!: string;
/** The user who owns the relationship. */ /**
@ManyToOne(() => User, user => user.relationships)
owner!: User;
/** The user who is the subject of the relationship. */
@ManyToOne(() => User)
subject!: User;
/** Whether the owner is following the subject. */
@Column("boolean")
following!: boolean;
/** Whether the owner is showing reblogs from the subject. */
@Column("boolean")
showing_reblogs!: boolean;
/** Whether the owner is receiving notifications from the subject. */
@Column("boolean")
notifying!: boolean;
/** Whether the owner is followed by the subject. */
@Column("boolean")
followed_by!: boolean;
/** Whether the owner is blocking the subject. */
@Column("boolean")
blocking!: boolean;
/** Whether the owner is blocked by the subject. */
@Column("boolean")
blocked_by!: boolean;
/** Whether the owner is muting the subject. */
@Column("boolean")
muting!: boolean;
/** Whether the owner is muting notifications from the subject. */
@Column("boolean")
muting_notifications!: boolean;
/** Whether the owner has requested to follow the subject. */
@Column("boolean")
requested!: boolean;
/** Whether the owner is blocking the subject's domain. */
@Column("boolean")
domain_blocking!: boolean;
/** Whether the owner has endorsed the subject. */
@Column("boolean")
endorsed!: boolean;
/** The languages the owner has specified for the subject. */
@Column("jsonb")
languages!: string[];
/** A note the owner has added for the subject. */
@Column("varchar")
note!: string;
/** The date the relationship was created. */
@CreateDateColumn()
created_at!: Date;
/** The date the relationship was last updated. */
@UpdateDateColumn()
updated_at!: Date;
/**
* Creates a new relationship between two users. * Creates a new relationship between two users.
* @param owner The user who owns the relationship. * @param owner The user who owns the relationship.
* @param other The user who is the subject of the relationship. * @param other The user who is the subject of the relationship.
* @returns The newly created relationship. * @returns The newly created relationship.
*/ */
static async createNew(owner: User, other: User): Promise<Relationship> { export const createNew = async (
const newRela = new Relationship(); owner: User,
newRela.owner = owner; other: User
newRela.subject = other; ): Promise<Relationship> => {
newRela.languages = []; return await client.relationship.create({
newRela.following = false; data: {
newRela.showing_reblogs = false; ownerId: owner.id,
newRela.notifying = false; subjectId: other.id,
newRela.followed_by = false; languages: [],
newRela.blocking = false; following: false,
newRela.blocked_by = false; showingReblogs: false,
newRela.muting = false; notifying: false,
newRela.muting_notifications = false; followedBy: false,
newRela.requested = false; blocking: false,
newRela.domain_blocking = false; blockedBy: false,
newRela.endorsed = false; muting: false,
newRela.note = ""; mutingNotifications: false,
requested: false,
domainBlocking: false,
endorsed: false,
note: "",
},
});
};
await newRela.save(); /**
return newRela;
}
/**
* Converts the relationship to an API-friendly format. * Converts the relationship to an API-friendly format.
* @returns The API-friendly relationship. * @returns The API-friendly relationship.
*/ */
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
async toAPI(): Promise<APIRelationship> { export const toAPI = async (rel: Relationship): Promise<APIRelationship> => {
return { return {
blocked_by: this.blocked_by, blocked_by: rel.blockedBy,
blocking: this.blocking, blocking: rel.blocking,
domain_blocking: this.domain_blocking, domain_blocking: rel.domainBlocking,
endorsed: this.endorsed, endorsed: rel.endorsed,
followed_by: this.followed_by, followed_by: rel.followedBy,
following: this.following, following: rel.following,
id: this.subject.id, // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
muting: this.muting, id: (rel as any).subject.id,
muting_notifications: this.muting_notifications, muting: rel.muting,
notifying: this.notifying, muting_notifications: rel.mutingNotifications,
requested: this.requested, notifying: rel.notifying,
showing_reblogs: this.showing_reblogs, requested: rel.requested,
languages: this.languages, showing_reblogs: rel.showingReblogs,
note: this.note, languages: rel.languages,
note: rel.note,
}; };
} };
}

View file

@ -1,256 +1,206 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { getConfig } from "@config"; import { getConfig } from "@config";
import { import {
BaseEntity, UserWithRelations,
Column, fetchRemoteUser,
CreateDateColumn, parseMentionsUris,
Entity, userRelations,
JoinTable, userToAPI,
ManyToMany, } from "./User";
ManyToOne, import { client } from "~database/datasource";
PrimaryGeneratedColumn,
RemoveOptions,
Tree,
TreeChildren,
TreeParent,
UpdateDateColumn,
} from "typeorm";
import { APIStatus } from "~types/entities/status";
import { User, userRelations } from "./User";
import { Application } from "./Application";
import { Emoji } from "./Emoji";
import { Instance } from "./Instance";
import { Like } from "./Like";
import { AppDataSource } from "~database/datasource";
import { LysandPublication, Note } from "~types/lysand/Object"; import { LysandPublication, Note } from "~types/lysand/Object";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import { getBestContentType } from "@content_types"; import { getBestContentType } from "@content_types";
import {
Application,
Emoji,
Instance,
Like,
Relationship,
Status,
User,
} from "@prisma/client";
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
import { APIStatus } from "~types/entities/status";
import { applicationToAPI } from "./Application";
const config = getConfig(); const config = getConfig();
export const statusRelations = [ export const statusAndUserRelations = {
"account", author: {
"reblog", include: userRelations,
"in_reply_to_post", },
"instance", application: true,
"in_reply_to_post.account", emojis: true,
"application", inReplyToPost: {
"emojis", include: {
"mentions", author: {
]; include: userRelations,
},
application: true,
emojis: true,
inReplyToPost: {
include: {
author: true,
},
},
instance: true,
mentions: true,
pinnedBy: true,
replies: {
include: {
_count: true,
},
},
},
},
instance: true,
mentions: true,
pinnedBy: true,
replies: {
include: {
_count: true,
},
},
reblog: {
include: {
author: {
include: userRelations,
},
application: true,
emojis: true,
inReplyToPost: {
include: {
author: true,
},
},
instance: true,
mentions: true,
pinnedBy: true,
replies: {
include: {
_count: true,
},
},
},
},
quotingPost: {
include: {
author: {
include: userRelations,
},
application: true,
emojis: true,
inReplyToPost: {
include: {
author: true,
},
},
instance: true,
mentions: true,
pinnedBy: true,
replies: {
include: {
_count: true,
},
},
},
},
likes: {
include: {
liker: true,
},
},
};
export const statusAndUserRelations = [ type StatusWithRelations = Status & {
...statusRelations, author: UserWithRelations;
// Can't directly map to userRelations as variable isnt yet initialized application: Application | null;
...[ emojis: Emoji[];
"account.relationships", inReplyToPost:
"account.pinned_notes", | (Status & {
"account.instance", author: UserWithRelations;
"account.emojis", application: Application | null;
], emojis: Emoji[];
]; inReplyToPost: Status | null;
instance: Instance | null;
mentions: User[];
pinnedBy: User[];
replies: Status[] & {
_count: number;
};
})
| null;
instance: Instance | null;
mentions: User[];
pinnedBy: User[];
replies: Status[] & {
_count: number;
};
reblog:
| (Status & {
author: UserWithRelations;
application: Application | null;
emojis: Emoji[];
inReplyToPost: Status | null;
instance: Instance | null;
mentions: User[];
pinnedBy: User[];
replies: Status[] & {
_count: number;
};
})
| null;
quotingPost:
| (Status & {
author: UserWithRelations;
application: Application | null;
emojis: Emoji[];
inReplyToPost: Status | null;
instance: Instance | null;
mentions: User[];
pinnedBy: User[];
replies: Status[] & {
_count: number;
};
})
| null;
likes: (Like & {
liker: User;
})[];
};
/** /**
* Represents a status (i.e. a post) * Represents a status (i.e. a post)
*/ */
@Entity({
name: "statuses",
})
@Tree("closure-table")
export class Status extends BaseEntity {
/**
* The unique identifier for this status.
*/
@PrimaryGeneratedColumn("uuid")
id!: string;
/** /**
* The URI for this status.
*/
@Column("varchar")
uri!: string;
/**
* The user account that created this status.
*/
@ManyToOne(() => User, user => user.id)
account!: User;
/**
* The date and time when this status was created.
*/
@CreateDateColumn()
created_at!: Date;
/**
* The date and time when this status was last updated.
*/
@UpdateDateColumn()
updated_at!: Date;
/**
* The status that this status is a reblog of, if any.
*/
@ManyToOne(() => Status, status => status.id, {
nullable: true,
onDelete: "SET NULL",
})
reblog?: Status | null;
/**
* Whether this status is a reblog.
*/
@Column("boolean")
isReblog!: boolean;
/**
* The content of this status.
*/
@Column("varchar", {
default: "",
})
content!: string;
/**
* The content type of this status.
*/
@Column("varchar", {
default: "text/plain",
})
content_type!: string;
/**
* The visibility of this status.
*/
@Column("varchar")
visibility!: APIStatus["visibility"];
/**
* The raw object that this status is a reply to, if any.
*/
@TreeParent({
onDelete: "SET NULL",
})
in_reply_to_post!: Status | null;
/**
* The status that this status is quoting, if any
*/
@ManyToOne(() => Status, status => status.id, {
nullable: true,
})
quoting_post!: Status | null;
@TreeChildren()
replies!: Status[];
/**
* The status' instance
*/
@ManyToOne(() => Instance, {
nullable: true,
})
instance!: Instance | null;
/**
* Whether this status is sensitive.
*/
@Column("boolean")
sensitive!: boolean;
/**
* The spoiler text for this status.
*/
@Column("varchar", {
default: "",
})
spoiler_text!: string;
/**
* The application associated with this status, if any.
*/
@ManyToOne(() => Application, app => app.id, {
nullable: true,
})
application!: Application | null;
/**
* The emojis associated with this status.
*/
@ManyToMany(() => Emoji, emoji => emoji.id)
@JoinTable()
emojis!: Emoji[];
/**
* The users mentioned (excluding followers and such)
*/
@ManyToMany(() => User, user => user.id)
@JoinTable()
mentions!: User[];
/**
* Removes this status from the database.
* @param options The options for removing this status.
* @returns A promise that resolves when the status has been removed.
*/
async remove(options?: RemoveOptions | undefined) {
// Delete object
// Get all associated Likes and remove them as well
await Like.delete({
liked: {
id: this.id,
},
});
return await super.remove(options);
}
async parseEmojis(string: string) {
const emojis = [...string.matchAll(/:([a-zA-Z0-9_]+):/g)].map(
match => match[1]
);
const emojiObjects = await Promise.all(
emojis.map(async emoji => {
const emojiObject = await Emoji.findOne({
where: {
shortcode: emoji,
},
});
return emojiObject;
})
);
return emojiObjects.filter(emoji => emoji !== null) as Emoji[];
}
/**
* Returns whether this status is viewable by a user. * Returns whether this status is viewable by a user.
* @param user The user to check. * @param user The user to check.
* @returns Whether this status is viewable by the user. * @returns Whether this status is viewable by the user.
*/ */
isViewableByUser(user: User | null) { export const isViewableByUser = (status: Status, user: User | null) => {
const relationship = user?.relationships.find( if (status.visibility === "public") return true;
rel => rel.id === this.account.id else if (status.visibility === "unlisted") return true;
else if (status.visibility === "private") {
// @ts-expect-error Prisma TypeScript types dont include relations
return !!(user?.relationships as Relationship[]).find(
rel => rel.id === status.authorId
); );
if (this.visibility === "public") return true;
else if (this.visibility === "unlisted") return true;
else if (this.visibility === "private") {
return !!relationship?.following;
} else { } else {
return user && this.mentions.includes(user); // @ts-expect-error Prisma TypeScript types dont include relations
} return user && (status.mentions as User[]).includes(user);
} }
};
static async fetchFromRemote(uri: string) { export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
// Check if already in database // Check if already in database
const existingStatus = await Status.findOne({ const existingStatus = await client.status.findFirst({
where: { where: {
uri: uri, uri: uri,
}, },
include: statusAndUserRelations,
}); });
if (existingStatus) return existingStatus; if (existingStatus) return existingStatus;
@ -263,22 +213,22 @@ export class Status extends BaseEntity {
const content = getBestContentType(body.contents); const content = getBestContentType(body.contents);
const emojis = await Emoji.parseEmojis(content?.content || ""); const emojis = await parseEmojis(content?.content || "");
const author = await User.fetchRemoteUser(body.author); const author = await fetchRemoteUser(body.author);
let replyStatus: Status | null = null; let replyStatus: Status | null = null;
let quotingStatus: Status | null = null; let quotingStatus: Status | null = null;
if (body.replies_to.length > 0) { if (body.replies_to.length > 0) {
replyStatus = await Status.fetchFromRemote(body.replies_to[0]); replyStatus = await fetchFromRemote(body.replies_to[0]);
} }
if (body.quotes.length > 0) { if (body.quotes.length > 0) {
quotingStatus = await Status.fetchFromRemote(body.quotes[0]); quotingStatus = await fetchFromRemote(body.quotes[0]);
} }
const newStatus = await Status.createNew({ return await createNew({
account: author, account: author,
content: content?.content || "", content: content?.content || "",
content_type: content?.content_type, content_type: content?.content_type,
@ -289,88 +239,41 @@ export class Status extends BaseEntity {
uri: body.uri, uri: body.uri,
sensitive: body.is_sensitive, sensitive: body.is_sensitive,
emojis: emojis, emojis: emojis,
mentions: await User.parseMentions(body.mentions), mentions: await parseMentionsUris(body.mentions),
reply: replyStatus reply: replyStatus
? { ? {
status: replyStatus, status: replyStatus,
user: replyStatus.account, user: (replyStatus as any).author,
} }
: undefined, : undefined,
quote: quotingStatus || undefined, quote: quotingStatus || undefined,
}); });
};
return await newStatus.save(); /**
}
/**
* Return all the ancestors of this post, * Return all the ancestors of this post,
*/ */
async getAncestors(fetcher: User | null) { // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
const max = fetcher ? 4096 : 40; export const getAncestors = async (fetcher: UserWithRelations | null) => {
const ancestors = []; // TODO: Implement
return [];
};
let id = this.in_reply_to_post?.id; /**
while (ancestors.length < max && id) {
const currentStatus = await Status.findOne({
where: {
id: id,
},
relations: statusAndUserRelations,
});
if (currentStatus) {
if (currentStatus.isViewableByUser(fetcher)) {
ancestors.push(currentStatus);
}
id = currentStatus.in_reply_to_post?.id;
} else {
break;
}
}
return ancestors;
}
/**
* Return all the descendants of this post, * Return all the descendants of this post,
*/ */
async getDescendants(fetcher: User | null) { // eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
const max = fetcher ? 4096 : 60; export const getDescendants = async (fetcher: UserWithRelations | null) => {
// TODO: Implement
return [];
};
const descendants = await AppDataSource.getTreeRepository( /**
Status
).findDescendantsTree(this, {
depth: fetcher ? 20 : undefined,
relations: statusAndUserRelations,
});
// Go through .replies of each descendant recursively and add them to the list
const flatten = (descendants: Status): Status[] => {
const flattened = [];
for (const descendant of descendants.replies) {
if (descendant.isViewableByUser(fetcher)) {
flattened.push(descendant);
}
flattened.push(...flatten(descendant));
}
return flattened;
};
const flattened = flatten(descendants);
return flattened.slice(0, max);
}
/**
* Creates a new status and saves it to the database. * Creates a new status and saves it to the database.
* @param data The data for the new status. * @param data The data for the new status.
* @returns A promise that resolves with the new status. * @returns A promise that resolves with the new status.
*/ */
static async createNew(data: { const createNew = async (data: {
account: User; account: User;
application: Application | null; application: Application | null;
content: string; content: string;
@ -386,198 +289,168 @@ export class Status extends BaseEntity {
user: User; user: User;
}; };
quote?: Status; quote?: Status;
}) { }) => {
const newStatus = new Status();
newStatus.account = data.account;
newStatus.application = data.application ?? null;
newStatus.content = data.content;
newStatus.content_type = data.content_type ?? "text/plain";
newStatus.visibility = data.visibility;
newStatus.sensitive = data.sensitive;
newStatus.spoiler_text = data.spoiler_text;
newStatus.emojis = data.emojis;
newStatus.isReblog = false;
newStatus.mentions = [];
newStatus.instance = data.account.instance;
newStatus.uri =
data.uri || `${config.http.base_url}/statuses/${newStatus.id}`;
newStatus.quoting_post = data.quote || null;
if (data.reply) {
newStatus.in_reply_to_post = data.reply.status;
}
// Get people mentioned in the content // Get people mentioned in the content
const mentionedPeople = [ const mentionedPeople = [...data.content.matchAll(/@([a-zA-Z0-9_]+)/g)].map(
...data.content.matchAll(/@([a-zA-Z0-9_]+)/g), match => {
].map(match => {
return `${config.http.base_url}/users/${match[1]}`; return `${config.http.base_url}/users/${match[1]}`;
}); }
);
let mentions = data.mentions || [];
// Get list of mentioned users // Get list of mentioned users
if (!data.mentions) { if (mentions.length === 0) {
await Promise.all( mentions = await client.user.findMany({
mentionedPeople.map(async person => {
// Check if post is in format @username or @username@instance.com
// If is @username, the user is a local user
const instanceUrl =
person.split("@").length === 3
? person.split("@")[2]
: null;
if (instanceUrl) {
const user = await User.findOne({
where: { where: {
OR: mentionedPeople.map(person => ({
username: person.split("@")[1], username: person.split("@")[1],
// If contains instanceUrl
instance: { instance: {
base_url: instanceUrl, base_url: person.split("@")[2],
},
})),
},
include: userRelations,
});
}
const status = await client.status.create({
data: {
authorId: data.account.id,
applicationId: data.application?.id,
content: data.content,
contentType: data.content_type,
visibility: data.visibility,
sensitive: data.sensitive,
spoilerText: data.spoiler_text,
emojis: {
connect: data.emojis.map(emoji => {
return {
id: emoji.id,
};
}),
},
inReplyToPostId: data.reply?.status.id,
quotingPostId: data.quote?.id,
instanceId: data.account.instanceId || undefined,
isReblog: false,
uri: data.uri || `${config.http.base_url}/statuses/xxx`,
mentions: {
connect: mentions.map(mention => {
return {
id: mention.id,
};
}),
}, },
}, },
relations: userRelations, include: statusAndUserRelations,
}); });
newStatus.mentions.push(user as User); if (!data.uri) status.uri = `${config.http.base_url}/statuses/${status.id}`;
} else {
const user = await User.findOne({ return status;
};
export const isFavouritedBy = async (status: Status, user: User) => {
return !!(await client.like.findFirst({
where: { where: {
username: person.split("@")[1], likerId: user.id,
likedId: status.id,
}, },
relations: userRelations, }));
}); };
newStatus.mentions.push(user as User); /**
}
})
);
} else {
newStatus.mentions = data.mentions;
}
await newStatus.save();
return newStatus;
}
async isFavouritedBy(user: User) {
const like = await Like.findOne({
where: {
liker: {
id: user.id,
},
liked: {
id: this.id,
},
},
relations: ["liker"],
});
return !!like;
}
/**
* Converts this status to an API status. * Converts this status to an API status.
* @returns A promise that resolves with the API status. * @returns A promise that resolves with the API status.
*/ */
async toAPI(user?: User): Promise<APIStatus> { export const statusToAPI = async (
const reblogCount = await Status.count({ status: StatusWithRelations,
where: { user?: UserWithRelations
reblog: { ): Promise<APIStatus> => {
id: this.id,
},
},
relations: ["reblog"],
});
const repliesCount = await Status.count({
where: {
in_reply_to_post: {
id: this.id,
},
},
relations: ["in_reply_to_post"],
});
const favourited = user ? await this.isFavouritedBy(user) : false;
const favourites_count = await Like.count({
where: {
liked: {
id: this.id,
},
},
relations: ["liked"],
});
return { return {
id: this.id, id: status.id,
in_reply_to_id: this.in_reply_to_post?.id || null, in_reply_to_id: status.inReplyToPostId || null,
in_reply_to_account_id: this.in_reply_to_post?.account.id || null, in_reply_to_account_id: status.inReplyToPost?.authorId || null,
account: await this.account.toAPI(), account: await userToAPI(status.author),
created_at: new Date(this.created_at).toISOString(), created_at: new Date(status.createdAt).toISOString(),
application: (await this.application?.toAPI()) || null, application: status.application
? await applicationToAPI(status.application)
: null,
card: null, card: null,
content: this.content, content: status.content,
emojis: await Promise.all(this.emojis.map(emoji => emoji.toAPI())), emojis: await Promise.all(
favourited, status.emojis.map(emoji => emojiToAPI(emoji))
favourites_count: favourites_count,
media_attachments: [],
mentions: await Promise.all(
this.mentions.map(async m => await m.toAPI())
), ),
favourited: !!status.likes.find(like => like.likerId === user?.id),
favourites_count: status.likes.length,
media_attachments: [],
mentions: [],
language: null, language: null,
muted: false, muted: user
pinned: this.account.pinned_notes.some(note => note.id === this.id), ? user.relationships.find(r => r.subjectId == status.authorId)
?.muting || false
: false,
pinned: status.author.pinnedNotes.some(note => note.id === status.id),
// TODO: Add pols
poll: null, poll: null,
reblog: this.reblog ? await this.reblog.toAPI() : null, reblog: status.reblog
reblogged: !!this.reblog, ? await statusToAPI(status.reblog as unknown as StatusWithRelations)
reblogs_count: reblogCount, : null,
replies_count: repliesCount, reblogged: !!(await client.status.findFirst({
sensitive: this.sensitive, where: {
spoiler_text: this.spoiler_text, authorId: user?.id,
reblogId: status.id,
},
})),
reblogs_count: await client.status.count({
where: {
reblogId: status.id,
},
}),
replies_count: status.replies._count,
sensitive: status.sensitive,
spoiler_text: status.spoilerText,
tags: [], tags: [],
uri: `${config.http.base_url}/users/${this.account.username}/statuses/${this.id}`, uri: `${config.http.base_url}/statuses/${status.id}`,
visibility: "public", visibility: "public",
url: `${config.http.base_url}/users/${this.account.username}/statuses/${this.id}`, url: `${config.http.base_url}/statuses/${status.id}`,
bookmarked: false, bookmarked: false,
quote: null, quote: null,
quote_id: undefined, quote_id: undefined,
}; };
} };
toLysand(): Note { export const statusToLysand = (status: StatusWithRelations): Note => {
return { return {
type: "Note", type: "Note",
created_at: new Date(this.created_at).toISOString(), created_at: new Date(status.createdAt).toISOString(),
id: this.id, id: status.id,
author: this.account.uri, author: status.authorId,
uri: `${config.http.base_url}/users/${this.account.id}/statuses/${this.id}`, uri: `${config.http.base_url}/users/${status.authorId}/statuses/${status.id}`,
contents: [ contents: [
{ {
content: this.content, content: status.content,
content_type: "text/html", content_type: "text/html",
}, },
{ {
// Content converted to plaintext // Content converted to plaintext
content: htmlToText(this.content), content: htmlToText(status.content),
content_type: "text/plain", content_type: "text/plain",
}, },
], ],
// TODO: Add attachments // TODO: Add attachments
attachments: [], attachments: [],
is_sensitive: this.sensitive, is_sensitive: status.sensitive,
mentions: this.mentions.map(mention => mention.id), mentions: status.mentions.map(mention => mention.uri),
// TODO: Add quotes quotes: status.quotingPost ? [status.quotingPost.uri] : [],
quotes: [], replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [],
replies_to: this.in_reply_to_post?.id subject: status.spoilerText,
? [this.in_reply_to_post.id]
: [],
subject: this.spoiler_text,
extensions: { extensions: {
"org.lysand:custom_emojis": { "org.lysand:custom_emojis": {
emojis: this.emojis.map(emoji => emoji.toLysand()), emojis: status.emojis.map(emoji => emojiToLysand(emoji)),
}, },
// TODO: Add polls and reactions // TODO: Add polls and reactions
}, },
}; };
} };
}

View file

@ -1,54 +1,3 @@
import {
Entity,
BaseEntity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
} from "typeorm";
import { User } from "./User";
import { Application } from "./Application";
/**
* Represents an access token for a user or application.
*/
@Entity({
name: "tokens",
})
export class Token extends BaseEntity {
/** The unique identifier for the token. */
@PrimaryGeneratedColumn("uuid")
id!: string;
/** The type of token. */
@Column("varchar")
token_type: TokenType = TokenType.BEARER;
/** The scope of the token. */
@Column("varchar")
scope!: string;
/** The access token string. */
@Column("varchar")
access_token!: string;
/** The authorization code used to obtain the token. */
@Column("varchar")
code!: string;
/** The date and time the token was created. */
@CreateDateColumn()
created_at!: Date;
/** The user associated with the token. */
@ManyToOne(() => User, user => user.id)
user!: User;
/** The application associated with the token. */
@ManyToOne(() => Application, application => application.id)
application!: Application;
}
/** /**
* The type of token. * The type of token.
*/ */

View file

@ -1,225 +1,100 @@
import { ConfigType, getConfig } from "@config"; import { ConfigType, getConfig } from "@config";
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinTable,
ManyToMany,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
RemoveOptions,
UpdateDateColumn,
} from "typeorm";
import { APIAccount } from "~types/entities/account"; import { APIAccount } from "~types/entities/account";
import { Token } from "./Token";
import { Status, statusRelations } from "./Status";
import { APISource } from "~types/entities/source";
import { Relationship } from "./Relationship";
import { Instance } from "./Instance";
import { User as LysandUser } from "~types/lysand/Object"; import { User as LysandUser } from "~types/lysand/Object";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import { Emoji } from "./Emoji"; import {
Emoji,
Instance,
Like,
Relationship,
Status,
User,
} from "@prisma/client";
import { client } from "~database/datasource";
import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji";
import { addInstanceIfNotExists } from "./Instance";
import { APISource } from "~types/entities/source";
export const userRelations = [ export interface AuthData {
"relationships", user: UserWithRelations | null;
"pinned_notes", token: string;
"instance", }
"emojis",
];
/** /**
* Represents a user in the database. * Represents a user in the database.
* Stores local and remote users * Stores local and remote users
*/ */
@Entity({
name: "users",
})
export class User extends BaseEntity {
/**
* The unique identifier for the user.
*/
@PrimaryGeneratedColumn("uuid")
id!: string;
/** export const userRelations = {
* The user URI on the global network emojis: true,
*/ instance: true,
@Column("varchar") likes: true,
uri!: string; relationships: true,
relationshipSubjects: true,
pinnedNotes: true,
statuses: {
select: {
_count: true,
},
},
};
/** export type UserWithRelations = User & {
* The username for the user. emojis: Emoji[];
*/ instance: Instance | null;
@Column("varchar", { likes: Like[];
unique: true, relationships: Relationship[];
}) relationshipSubjects: Relationship[];
username!: string; pinnedNotes: Status[];
statuses: {
length: number;
};
};
/** /**
* The display name for the user.
*/
@Column("varchar")
display_name!: string;
/**
* The password for the user.
*/
@Column("varchar", {
nullable: true,
})
password!: string | null;
/**
* The email address for the user.
*/
@Column("varchar", {
unique: true,
nullable: true,
})
email!: string | null;
/**
* The note for the user.
*/
@Column("varchar", {
default: "",
})
note!: string;
/**
* Whether the user is an admin or not.
*/
@Column("boolean", {
default: false,
})
is_admin!: boolean;
@Column("jsonb", {
nullable: true,
})
endpoints!: {
liked: string;
disliked: string;
featured: string;
followers: string;
following: string;
inbox: string;
outbox: string;
} | null;
/**
* The source for the user.
*/
@Column("jsonb")
source!: APISource;
/**
* The avatar for the user (filename, as UUID)
*/
@Column("varchar")
avatar!: string;
/**
* The header for the user (filename, as UUID)
*/
@Column("varchar")
header!: string;
/**
* The date the user was created.
*/
@CreateDateColumn()
created_at!: Date;
/**
* The date the user was last updated.
*/
@UpdateDateColumn()
updated_at!: Date;
/**
* The public key for the user.
*/
@Column("varchar")
public_key!: string;
/**
* The private key for the user.
*/
@Column("varchar", {
nullable: true,
})
private_key!: string | null;
/**
* The relationships for the user.
*/
@OneToMany(() => Relationship, relationship => relationship.owner)
relationships!: Relationship[];
/**
* User's instance, null if local user
*/
@ManyToOne(() => Instance, {
nullable: true,
})
instance!: Instance | null;
/**
* The pinned notes for the user.
*/
@ManyToMany(() => Status, status => status.id)
@JoinTable()
pinned_notes!: Status[];
/**
* The emojis for the user.
*/
@ManyToMany(() => Emoji, emoji => emoji.id)
@JoinTable()
emojis!: Emoji[];
/**
* Get the user's avatar in raw URL format * Get the user's avatar in raw URL format
* @param config The config to use * @param config The config to use
* @returns The raw URL for the user's avatar * @returns The raw URL for the user's avatar
*/ */
getAvatarUrl(config: ConfigType) { export const getAvatarUrl = (user: User, config: ConfigType) => {
if (!user.avatar) return config.defaults.avatar;
if (config.media.backend === "local") { if (config.media.backend === "local") {
return `${config.http.base_url}/media/${this.avatar}`; return `${config.http.base_url}/media/${user.avatar}`;
} else if (config.media.backend === "s3") { } else if (config.media.backend === "s3") {
return `${config.s3.public_url}/${this.avatar}`; return `${config.s3.public_url}/${user.avatar}`;
}
} }
return "";
};
/** /**
* Get the user's header in raw URL format * Get the user's header in raw URL format
* @param config The config to use * @param config The config to use
* @returns The raw URL for the user's header * @returns The raw URL for the user's header
*/ */
getHeaderUrl(config: ConfigType) { export const getHeaderUrl = (user: User, config: ConfigType) => {
if (!user.header) return config.defaults.header;
if (config.media.backend === "local") { if (config.media.backend === "local") {
return `${config.http.base_url}/media/${this.header}`; return `${config.http.base_url}/media/${user.header}`;
} else if (config.media.backend === "s3") { } else if (config.media.backend === "s3") {
return `${config.s3.public_url}/${this.header}`; return `${config.s3.public_url}/${user.header}`;
}
} }
return "";
};
static async getFromRequest(req: Request) { export const getFromRequest = async (req: Request): Promise<AuthData> => {
// Check auth token // Check auth token
const token = req.headers.get("Authorization")?.split(" ")[1] || ""; const token = req.headers.get("Authorization")?.split(" ")[1] || "";
return { user: await User.retrieveFromToken(token), token }; return { user: await retrieveUserFromToken(token), token };
} };
static async fetchRemoteUser(uri: string) { export const fetchRemoteUser = async (uri: string) => {
// Check if user not already in database // Check if user not already in database
const foundUser = await User.findOne({ const foundUser = await client.user.findUnique({
where: { where: {
uri, uri,
}, },
include: userRelations,
}); });
if (foundUser) return foundUser; if (foundUser) return foundUser;
@ -234,8 +109,6 @@ export class User extends BaseEntity {
const data = (await response.json()) as Partial<LysandUser>; const data = (await response.json()) as Partial<LysandUser>;
const user = new User();
if ( if (
!( !(
data.id && data.id &&
@ -255,11 +128,16 @@ export class User extends BaseEntity {
throw new Error("Invalid user data"); throw new Error("Invalid user data");
} }
user.id = data.id; // Parse emojis and add them to database
user.username = data.username; const userEmojis =
user.uri = data.uri; data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
user.created_at = new Date(data.created_at);
user.endpoints = { const user = await client.user.create({
data: {
username: data.username,
uri: data.uri,
createdAt: new Date(data.created_at),
endpoints: {
disliked: data.disliked, disliked: data.disliked,
featured: data.featured, featured: data.featured,
liked: data.liked, liked: data.liked,
@ -267,63 +145,59 @@ export class User extends BaseEntity {
following: data.following, following: data.following,
inbox: data.inbox, inbox: data.inbox,
outbox: data.outbox, outbox: data.outbox,
}; },
avatar: (data.avatar && data.avatar[0].content) || "",
header: (data.header && data.header[0].content) || "",
displayName: data.display_name ?? "",
note: data.bio?.[0].content ?? "",
publicKey: data.public_key.public_key,
source: {
language: null,
note: "",
privacy: "public",
sensitive: false,
fields: [],
},
},
});
user.avatar = (data.avatar && data.avatar[0].content) || ""; const emojis = [];
user.header = (data.header && data.header[0].content) || "";
user.display_name = data.display_name ?? "";
// TODO: Add bio content types
user.note = data.bio?.[0].content ?? "";
// Parse emojis and add them to database for (const emoji of userEmojis) {
const emojis = emojis.push(await addEmojiIfNotExists(emoji));
data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
for (const emoji of emojis) {
user.emojis.push(await Emoji.addIfNotExists(emoji));
} }
user.public_key = data.public_key.public_key;
const uriData = new URL(data.uri); const uriData = new URL(data.uri);
user.instance = await Instance.addIfNotExists(uriData.origin); return await client.user.update({
where: {
id: user.id,
},
data: {
emojis: {
connect: emojis.map(emoji => ({
id: emoji.id,
})),
},
instanceId: (await addInstanceIfNotExists(uriData.origin)).id,
},
include: userRelations,
});
};
await user.save(); /**
return user;
}
/**
* Fetches the list of followers associated with the actor and updates the user's followers * Fetches the list of followers associated with the actor and updates the user's followers
*/ */
async fetchFollowers() { export const fetchFollowers = () => {
// //
} };
/** /**
* Gets a user by actor ID.
* @param id The actor ID to search for.
* @returns The user with the given actor ID.
*/
static async getByActorId(id: string) {
return await User.createQueryBuilder("user")
// Objects is a many-to-many relationship
.leftJoinAndSelect("user.actor", "actor")
.leftJoinAndSelect("user.relationships", "relationships")
.where("actor.data @> :data", {
data: JSON.stringify({
id,
}),
})
.getOne();
}
/**
* Creates a new LOCAL user. * Creates a new LOCAL user.
* @param data The data for the new user. * @param data The data for the new user.
* @returns The newly created user. * @returns The newly created user.
*/ */
static async createNewLocal(data: { export const createNewLocalUser = async (data: {
username: string; username: string;
display_name?: string; display_name?: string;
password: string; password: string;
@ -331,154 +205,103 @@ export class User extends BaseEntity {
bio?: string; bio?: string;
avatar?: string; avatar?: string;
header?: string; header?: string;
}) { }) => {
const config = getConfig(); const config = getConfig();
const user = new User();
user.username = data.username; const keys = await generateUserKeys();
user.display_name = data.display_name ?? data.username;
user.password = await Bun.password.hash(data.password);
user.email = data.email;
user.note = data.bio ?? "";
user.avatar = data.avatar ?? config.defaults.avatar;
user.header = data.header ?? config.defaults.avatar;
user.uri = `${config.http.base_url}/users/${user.id}`;
user.emojis = [];
user.relationships = []; const user = await client.user.create({
user.instance = null; data: {
username: data.username,
user.source = { displayName: data.display_name ?? data.username,
password: await Bun.password.hash(data.password),
email: data.email,
note: data.bio ?? "",
avatar: data.avatar ?? config.defaults.avatar,
header: data.header ?? config.defaults.avatar,
uri: "",
publicKey: keys.public_key,
privateKey: keys.private_key,
source: {
language: null, language: null,
note: "", note: "",
privacy: "public", privacy: "public",
sensitive: false, sensitive: false,
fields: [], fields: [],
};
user.pinned_notes = [];
await user.generateKeys();
await user.save();
return user;
}
static async parseMentions(mentions: string[]) {
return await Promise.all(
mentions.map(async mention => {
const user = await User.findOne({
where: {
uri: mention,
}, },
relations: userRelations, },
}); });
if (user) return user; return await client.user.update({
else return await User.fetchRemoteUser(mention); where: {
}) id: user.id,
); },
} data: {
uri: `${config.http.base_url}/users/${user.id}`,
},
include: userRelations,
});
};
/** /**
* Parses mentions from a list of URIs
*/
export const parseMentionsUris = async (mentions: string[]) => {
return await client.user.findMany({
where: {
uri: {
in: mentions,
},
},
include: userRelations,
});
};
/**
* Retrieves a user from a token. * Retrieves a user from a token.
* @param access_token The access token to retrieve the user from. * @param access_token The access token to retrieve the user from.
* @returns The user associated with the given access token. * @returns The user associated with the given access token.
*/ */
static async retrieveFromToken(access_token: string) { export const retrieveUserFromToken = async (access_token: string) => {
if (!access_token) return null; if (!access_token) return null;
const token = await Token.findOne({ const token = await client.token.findFirst({
where: { where: {
access_token, access_token,
}, },
relations: userRelations.map(r => `user.${r}`), include: {
user: {
include: userRelations,
},
},
}); });
if (!token) return null; if (!token) return null;
return token.user; return token.user;
} };
/** /**
* Gets the relationship to another user. * Gets the relationship to another user.
* @param other The other user to get the relationship to. * @param other The other user to get the relationship to.
* @returns The relationship to the other user. * @returns The relationship to the other user.
*/ */
async getRelationshipToOtherUser(other: User) { export const getRelationshipToOtherUser = async (
const relationship = await Relationship.findOne({ user: UserWithRelations,
other: User
) => {
return await client.relationship.findFirst({
where: { where: {
owner: { ownerId: user.id,
id: this.id, subjectId: other.id,
},
subject: {
id: other.id,
},
},
relations: ["owner", "subject"],
});
return relationship;
}
/**
* Removes the user.
* @param options The options for removing the user.
* @returns The removed user.
*/
async remove(options?: RemoveOptions | undefined) {
// Clean up tokens
const tokens = await Token.findBy({
user: {
id: this.id,
}, },
}); });
};
const statuses = await Status.find({ /**
where: {
account: {
id: this.id,
},
},
relations: statusRelations,
});
// Delete both
await Promise.all(tokens.map(async token => await token.remove()));
await Promise.all(statuses.map(async status => await status.remove()));
// Get relationships
const relationships = await this.getRelationships();
// Delete them all
await Promise.all(
relationships.map(async relationship => await relationship.remove())
);
return await super.remove(options);
}
/**
* Gets the relationships for the user.
* @returns The relationships for the user.
*/
async getRelationships() {
const relationships = await Relationship.find({
where: {
owner: {
id: this.id,
},
},
relations: ["subject"],
});
return relationships;
}
/**
* Generates keys for the user. * Generates keys for the user.
*/ */
async generateKeys(): Promise<void> { export const generateUserKeys = async () => {
const keys = (await crypto.subtle.generateKey("Ed25519", true, [ const keys = (await crypto.subtle.generateKey("Ed25519", true, [
"sign", "sign",
"verify", "verify",
@ -503,66 +326,51 @@ export class User extends BaseEntity {
// Add header, footer and newlines later on // Add header, footer and newlines later on
// These keys are base64 encrypted // These keys are base64 encrypted
this.private_key = privateKey; return {
this.public_key = publicKey; private_key: privateKey,
} public_key: publicKey,
};
// eslint-disable-next-line @typescript-eslint/require-await };
async toAPI(isOwnAccount = false): Promise<APIAccount> {
const follower_count = await Relationship.count({
where: {
subject: {
id: this.id,
},
following: true,
},
relations: ["subject"],
});
const following_count = await Relationship.count({
where: {
owner: {
id: this.id,
},
following: true,
},
relations: ["owner"],
});
const statusCount = await Status.count({
where: {
account: {
id: this.id,
},
},
relations: ["account"],
});
// eslint-disable-next-line @typescript-eslint/require-await
export const userToAPI = async (
user: UserWithRelations,
isOwnAccount = false
): Promise<APIAccount> => {
const config = getConfig(); const config = getConfig();
return { return {
id: this.id, id: user.id,
username: this.username, username: user.username,
display_name: this.display_name, display_name: user.displayName,
note: this.note, note: user.note,
url: `${config.http.base_url}/users/${this.username}`, url: user.uri,
avatar: this.getAvatarUrl(config) || config.defaults.avatar, avatar: getAvatarUrl(user, config),
header: this.getHeaderUrl(config) || config.defaults.header, header: getHeaderUrl(user, config),
// TODO: Add locked
locked: false, locked: false,
created_at: new Date(this.created_at).toISOString(), created_at: new Date(user.createdAt).toISOString(),
followers_count: follower_count, followers_count: user.relationshipSubjects.filter(r => r.following)
following_count: following_count, .length,
statuses_count: statusCount, following_count: user.relationships.filter(r => r.following).length,
emojis: await Promise.all(this.emojis.map(emoji => emoji.toAPI())), statuses_count: user.statuses.length,
emojis: await Promise.all(user.emojis.map(emoji => emojiToAPI(emoji))),
// TODO: Add fields
fields: [], fields: [],
// TODO: Add bot
bot: false, bot: false,
source: isOwnAccount ? this.source : undefined, source:
isOwnAccount && user.source
? (user.source as any as APISource)
: undefined,
// TODO: Add static avatar and header
avatar_static: "", avatar_static: "",
header_static: "", header_static: "",
acct: acct:
this.instance === null user.instance === null
? `${this.username}` ? `${user.username}`
: `${this.username}@${this.instance.base_url}`, : `${user.username}@${user.instance.base_url}`,
// TODO: Add these fields
limited: false, limited: false,
moved: null, moved: null,
noindex: false, noindex: false,
@ -572,54 +380,54 @@ export class User extends BaseEntity {
group: false, group: false,
role: undefined, role: undefined,
}; };
} };
/** /**
* Should only return local users * Should only return local users
*/ */
toLysand(): LysandUser { export const userToLysand = (user: UserWithRelations): LysandUser => {
if (this.instance !== null) { if (user.instanceId !== null) {
throw new Error("Cannot convert remote user to Lysand format"); throw new Error("Cannot convert remote user to Lysand format");
} }
return { return {
id: this.id, id: user.id,
type: "User", type: "User",
uri: this.uri, uri: user.uri,
bio: [ bio: [
{ {
content: this.note, content: user.note,
content_type: "text/html", content_type: "text/html",
}, },
{ {
content: htmlToText(this.note), content: htmlToText(user.note),
content_type: "text/plain", content_type: "text/plain",
}, },
], ],
created_at: new Date(this.created_at).toISOString(), created_at: new Date(user.createdAt).toISOString(),
disliked: `${this.uri}/disliked`, disliked: `${user.uri}/disliked`,
featured: `${this.uri}/featured`, featured: `${user.uri}/featured`,
liked: `${this.uri}/liked`, liked: `${user.uri}/liked`,
followers: `${this.uri}/followers`, followers: `${user.uri}/followers`,
following: `${this.uri}/following`, following: `${user.uri}/following`,
inbox: `${this.uri}/inbox`, inbox: `${user.uri}/inbox`,
outbox: `${this.uri}/outbox`, outbox: `${user.uri}/outbox`,
indexable: false, indexable: false,
username: this.username, username: user.username,
avatar: [ avatar: [
{ {
content: this.getAvatarUrl(getConfig()) || "", content: getAvatarUrl(user, getConfig()) || "",
content_type: `image/${this.avatar.split(".")[1]}`, content_type: `image/${user.avatar.split(".")[1]}`,
}, },
], ],
header: [ header: [
{ {
content: this.getHeaderUrl(getConfig()) || "", content: getHeaderUrl(user, getConfig()) || "",
content_type: `image/${this.header.split(".")[1]}`, content_type: `image/${user.header.split(".")[1]}`,
}, },
], ],
display_name: this.display_name, display_name: user.displayName,
fields: this.source.fields.map(field => ({ fields: (user.source as any as APISource).fields.map(field => ({
key: [ key: [
{ {
content: field.name, content: field.name,
@ -642,14 +450,13 @@ export class User extends BaseEntity {
], ],
})), })),
public_key: { public_key: {
actor: `${getConfig().http.base_url}/users/${this.id}`, actor: `${getConfig().http.base_url}/users/${user.id}`,
public_key: this.public_key, public_key: user.publicKey,
}, },
extensions: { extensions: {
"org.lysand:custom_emojis": { "org.lysand:custom_emojis": {
emojis: this.emojis.map(emoji => emoji.toLysand()), emojis: user.emojis.map(emoji => emojiToLysand(emoji)),
}, },
}, },
}; };
} };
}

View file

@ -6,7 +6,7 @@ import { appendFile } from "fs/promises";
import { matches } from "ip-matching"; import { matches } from "ip-matching";
import "reflect-metadata"; import "reflect-metadata";
import { AppDataSource } from "~database/datasource"; import { AppDataSource } from "~database/datasource";
import { User } from "~database/entities/User"; import { AuthData, UserAction } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
const router = new Bun.FileSystemRouter({ const router = new Bun.FileSystemRouter({
@ -60,7 +60,8 @@ Bun.serve({
meta: APIRouteMeta; meta: APIRouteMeta;
default: ( default: (
req: Request, req: Request,
matchedRoute: MatchedRoute matchedRoute: MatchedRoute,
auth: AuthData
) => Response | Promise<Response>; ) => Response | Promise<Response>;
} = await import(matchedRoute.filePath); } = await import(matchedRoute.filePath);
@ -78,11 +79,11 @@ Bun.serve({
// TODO: Check for ratelimits // TODO: Check for ratelimits
const auth = await UserAction.getFromRequest(req);
// Check for authentication if required // Check for authentication if required
if (meta.auth.required) { if (meta.auth.required) {
const { user } = await User.getFromRequest(req); if (!auth.user) {
if (!user) {
return new Response(undefined, { return new Response(undefined, {
status: 401, status: 401,
statusText: "Unauthorized", statusText: "Unauthorized",
@ -91,9 +92,7 @@ Bun.serve({
} else if ( } else if (
(meta.auth.requiredOnMethods ?? []).includes(req.method as any) (meta.auth.requiredOnMethods ?? []).includes(req.method as any)
) { ) {
const { user } = await User.getFromRequest(req); if (!auth.user) {
if (!user) {
return new Response(undefined, { return new Response(undefined, {
status: 401, status: 401,
statusText: "Unauthorized", statusText: "Unauthorized",
@ -101,7 +100,7 @@ Bun.serve({
} }
} }
return file.default(req, matchedRoute); return file.default(req, matchedRoute, auth);
} else { } else {
return new Response(undefined, { return new Response(undefined, {
status: 404, status: 404,

View file

@ -36,7 +36,8 @@
"start": "bun run index.ts" "start": "bun run index.ts"
}, },
"trustedDependencies": [ "trustedDependencies": [
"sharp" "sharp",
"@prisma/client"
], ],
"devDependencies": { "devDependencies": {
"@julr/unocss-preset-forms": "^0.0.5", "@julr/unocss-preset-forms": "^0.0.5",
@ -61,6 +62,7 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.429.0", "@aws-sdk/client-s3": "^3.429.0",
"@prisma/client": "^5.5.2",
"chalk": "^5.3.0", "chalk": "^5.3.0",
"html-to-text": "^9.0.5", "html-to-text": "^9.0.5",
"ip-matching": "^2.1.2", "ip-matching": "^2.1.2",
@ -68,6 +70,7 @@
"jsonld": "^8.3.1", "jsonld": "^8.3.1",
"marked": "^9.1.2", "marked": "^9.1.2",
"pg": "^8.11.3", "pg": "^8.11.3",
"prisma": "^5.5.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"typeorm": "^0.3.17" "typeorm": "^0.3.17"
} }

166
prisma/schema.prisma Normal file
View file

@ -0,0 +1,166 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [pg_uuidv7]
}
model Application {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
name String
website String?
vapid_key String?
client_id String
secret String
scopes String
redirect_uris String
statuses Status[] // One to many relation with Status
tokens Token[] // One to many relation with Token
}
model Emoji {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
shortcode String
url String
visible_in_picker Boolean
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String? @db.Uuid
alt String?
content_type String
users User[] // Many to many relation with User
statuses Status[] // Many to many relation with Status
}
model Instance {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
base_url String
name String
version String
logo Json
emojis Emoji[] // One to many relation with Emoji
statuses Status[] // One to many relation with Status
users User[] // One to many relation with User
}
model Like {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
liker User @relation("UserLiked", fields: [likerId], references: [id], onDelete: Cascade)
likerId String @db.Uuid
liked Status @relation("LikedToStatus", fields: [likedId], references: [id], onDelete: Cascade)
likedId String @db.Uuid
createdAt DateTime @default(now())
}
model LysandObject {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
remote_id String @unique
type String
uri String @unique
created_at DateTime @default(now())
author LysandObject? @relation("LysandObjectToAuthor", fields: [authorId], references: [id], onDelete: Cascade)
authorId String? @db.Uuid
extra_data Json
extensions Json
children LysandObject[] @relation("LysandObjectToAuthor")
}
model Relationship {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
owner User @relation("OwnerToRelationship", fields: [ownerId], references: [id], onDelete: Cascade)
ownerId String @db.Uuid
subject User @relation("SubjectToRelationship", fields: [subjectId], references: [id], onDelete: Cascade)
subjectId String @db.Uuid
following Boolean
showingReblogs Boolean
notifying Boolean
followedBy Boolean
blocking Boolean
blockedBy Boolean
muting Boolean
mutingNotifications Boolean
requested Boolean
domainBlocking Boolean
endorsed Boolean
languages String[]
note String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Status {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique
author User @relation("UserStatuses", fields: [authorId], references: [id], onDelete: Cascade)
authorId String @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
reblog Status? @relation("StatusToStatus", fields: [reblogId], references: [id], onDelete: Cascade)
reblogId String? @db.Uuid
isReblog Boolean
content String @default("")
contentType String @default("text/plain")
visibility String
inReplyToPost Status? @relation("StatusToStatusReply", fields: [inReplyToPostId], references: [id], onDelete: SetNull)
inReplyToPostId String? @db.Uuid
quotingPost Status? @relation("StatusToStatusQuote", fields: [quotingPostId], references: [id], onDelete: SetNull)
quotingPostId String? @db.Uuid
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade)
instanceId String? @db.Uuid
sensitive Boolean
spoilerText String @default("")
application Application? @relation(fields: [applicationId], references: [id], onDelete: SetNull)
applicationId String? @db.Uuid
emojis Emoji[] @relation
mentions User[]
likes Like[] @relation("LikedToStatus")
reblogs Status[] @relation("StatusToStatus")
replies Status[] @relation("StatusToStatusReply")
quotes Status[] @relation("StatusToStatusQuote")
pinnedBy User[] @relation("UserPinnedNotes")
}
model Token {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
token_type String
scope String
access_token String
code String
created_at DateTime @default(now())
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String? @db.Uuid
application Application? @relation(fields: [applicationId], references: [id], onDelete: Cascade)
applicationId String? @db.Uuid
}
model User {
id String @id @default(dbgenerated("uuid_generate_v7()")) @db.Uuid
uri String @unique
username String @unique
displayName String
password String? // Nullable
email String? @unique // Nullable
note String @default("")
isAdmin Boolean @default(false)
endpoints Json? // Nullable
source Json
avatar String
header String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
publicKey String
privateKey String? // Nullable
relationships Relationship[] @relation("OwnerToRelationship") // One to many relation with Relationship
relationshipSubjects Relationship[] @relation("SubjectToRelationship") // One to many relation with Relationship
instance Instance? @relation(fields: [instanceId], references: [id], onDelete: Cascade) // Many to one relation with Instance
instanceId String? @db.Uuid
pinnedNotes Status[] @relation("UserPinnedNotes") // Many to many relation with Status
emojis Emoji[] // Many to many relation with Emoji
statuses Status[] @relation("UserStatuses") // One to many relation with Status
tokens Token[] // One to many relation with Token
likes Like[] @relation("UserLiked") // One to many relation with Like
statusesMentioned Status[] // Many to many relation with Status
}

View file

@ -1,8 +1,8 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { User, userRelations } from "~database/entities/User";
import { getConfig, getHost } from "@config"; import { getConfig, getHost } from "@config";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { client } from "~database/datasource";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -34,9 +34,8 @@ export default async (
return errorResponse("User is a remote user", 404); return errorResponse("User is a remote user", 404);
} }
const user = await User.findOne({ const user = await client.user.findUnique({
where: { username: requestedUser.split("@")[0] }, where: { username: requestedUser.split("@")[0] },
relations: userRelations
}); });
if (!user) { if (!user) {

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -25,11 +25,11 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -2,7 +2,7 @@ import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -26,7 +26,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -36,7 +36,7 @@ export default async (
languages?: string[]; languages?: string[];
}>(req); }>(req);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -1,6 +1,6 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -24,11 +24,11 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
let foundUser: User | null; let foundUser: UserAction | null;
try { try {
foundUser = await User.findOne({ foundUser = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -2,7 +2,7 @@ import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -26,7 +26,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -36,7 +36,7 @@ export default async (
duration: number; duration: number;
}>(req); }>(req);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -2,7 +2,7 @@ import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -26,7 +26,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -34,7 +34,7 @@ export default async (
comment: string; comment: string;
}>(req); }>(req);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -25,11 +25,11 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -25,11 +25,11 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -2,7 +2,7 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { FindManyOptions } from "typeorm"; import { FindManyOptions } from "typeorm";
@ -47,7 +47,7 @@ export default async (
tagged?: string; tagged?: string;
} = matchedRoute.query; } = matchedRoute.query;
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -25,11 +25,11 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -25,11 +25,11 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -25,11 +25,11 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -1,7 +1,7 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -25,11 +25,11 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id, id,
}, },

View file

@ -1,6 +1,6 @@
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { APIAccount } from "~types/entities/account"; import { APIAccount } from "~types/entities/account";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
@ -20,7 +20,7 @@ export const meta = applyConfig({
* Find familiar followers (followers of a user that you also follow) * Find familiar followers (followers of a user that you also follow)
*/ */
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -39,7 +39,7 @@ export default async (req: Request): Promise<Response> => {
// Find followers of user that you also follow // Find followers of user that you also follow
// Get user // Get user
const user = await User.findOne({ const user = await UserAction.findOne({
where: { id }, where: { id },
relations: { relations: {
relationships: { relationships: {

View file

@ -2,7 +2,7 @@ import { getConfig } from "@config";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { tempmailDomains } from "@tempmail"; import { tempmailDomains } from "@tempmail";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -115,7 +115,7 @@ export default async (req: Request): Promise<Response> => {
}); });
// Check if username is taken // Check if username is taken
if (await User.findOne({ where: { username: body.username } })) if (await UserAction.findOne({ where: { username: body.username } }))
errors.details.username.push({ errors.details.username.push({
error: "ERR_TAKEN", error: "ERR_TAKEN",
description: `is already taken`, description: `is already taken`,
@ -170,7 +170,7 @@ export default async (req: Request): Promise<Response> => {
// TODO: Check if locale is valid // TODO: Check if locale is valid
await User.createNewLocal({ await UserAction.createNewLocal({
username: body.username ?? "", username: body.username ?? "",
password: body.password ?? "", password: body.password ?? "",
email: body.email ?? "", email: body.email ?? "",

View file

@ -1,7 +1,7 @@
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { Relationship } from "~database/entities/Relationship"; import { Relationship } from "~database/entities/Relationship";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -20,7 +20,7 @@ export const meta = applyConfig({
* Find relationships * Find relationships
*/ */
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { user: self } = await User.getFromRequest(req); const { user: self } = await UserAction.getFromRequest(req);
if (!self) return errorResponse("Unauthorized", 401); if (!self) return errorResponse("Unauthorized", 401);
@ -38,7 +38,7 @@ export default async (req: Request): Promise<Response> => {
const relationships = ( const relationships = (
await Promise.all( await Promise.all(
ids.map(async id => { ids.map(async id => {
const user = await User.findOneBy({ id }); const user = await UserAction.findOneBy({ id });
if (!user) return null; if (!user) return null;
let relationship = await self.getRelationshipToOtherUser(user); let relationship = await self.getRelationshipToOtherUser(user);

View file

@ -1,12 +1,12 @@
import { getConfig } from "@config"; import { getConfig } from "@config";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { sanitize } from "isomorphic-dompurify"; import { sanitize } from "isomorphic-dompurify";
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import { uploadFile } from "~classes/media"; import { uploadFile } from "~classes/media";
import { Emoji } from "~database/entities/Emoji"; import { EmojiAction } from "~database/entities/Emoji";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["PATCH"], allowedMethods: ["PATCH"],
@ -24,7 +24,7 @@ export const meta = applyConfig({
* Patches a user * Patches a user
*/ */
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
@ -202,8 +202,8 @@ export default async (req: Request): Promise<Response> => {
// Parse emojis // Parse emojis
const displaynameEmojis = await Emoji.parseEmojis(sanitizedDisplayName); const displaynameEmojis = await EmojiAction.parseEmojis(sanitizedDisplayName);
const noteEmojis = await Emoji.parseEmojis(sanitizedNote); const noteEmojis = await EmojiAction.parseEmojis(sanitizedNote);
user.emojis = [...displaynameEmojis, ...noteEmojis]; user.emojis = [...displaynameEmojis, ...noteEmojis];

View file

@ -1,5 +1,5 @@
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { applyConfig } from "@api"; import { applyConfig } from "@api";
export const meta = applyConfig({ export const meta = applyConfig({
@ -17,7 +17,7 @@ export const meta = applyConfig({
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
// TODO: Add checks for disabled or not email verified accounts // TODO: Add checks for disabled or not email verified accounts
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);

View file

@ -2,7 +2,7 @@ import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -27,7 +27,7 @@ export default async (req: Request): Promise<Response> => {
website: string; website: string;
}>(req); }>(req);
const application = new Application(); const application = new ApplicationAction();
application.name = client_name || ""; application.name = client_name || "";

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -19,8 +19,8 @@ export const meta = applyConfig({
* Returns OAuth2 credentials * Returns OAuth2 credentials
*/ */
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { user, token } = await User.getFromRequest(req); const { user, token } = await UserAction.getFromRequest(req);
const application = await Application.getFromToken(token); const application = await ApplicationAction.getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
if (!application) return errorResponse("Unauthorized", 401); if (!application) return errorResponse("Unauthorized", 401);

View file

@ -1,7 +1,7 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { IsNull } from "typeorm"; import { IsNull } from "typeorm";
import { Emoji } from "~database/entities/Emoji"; import { EmojiAction } from "~database/entities/Emoji";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -20,7 +20,7 @@ export const meta = applyConfig({
*/ */
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
export default async (): Promise<Response> => { export default async (): Promise<Response> => {
const emojis = await Emoji.findBy({ const emojis = await EmojiAction.findBy({
instance: IsNull(), instance: IsNull(),
}); });

View file

@ -2,7 +2,7 @@ import { applyConfig } from "@api";
import { getConfig } from "@config"; import { getConfig } from "@config";
import { jsonResponse } from "@response"; import { jsonResponse } from "@response";
import { Status } from "~database/entities/Status"; import { Status } from "~database/entities/Status";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["GET"], allowedMethods: ["GET"],
@ -24,7 +24,7 @@ export default async (): Promise<Response> => {
const config = getConfig(); const config = getConfig();
const statusCount = await Status.count(); const statusCount = await Status.count();
const userCount = await User.count(); const userCount = await UserAction.count();
// TODO: fill in more values // TODO: fill in more values
return jsonResponse({ return jsonResponse({

View file

@ -2,7 +2,7 @@ import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -28,7 +28,7 @@ export default async (
// User token + read:statuses for up to 4,096 ancestors, 4,096 descendants, unlimited depth, and private statuses. // User token + read:statuses for up to 4,096 ancestors, 4,096 descendants, unlimited depth, and private statuses.
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
let foundStatus: Status | null; let foundStatus: Status | null;
try { try {

View file

@ -4,7 +4,7 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Like } from "~database/entities/Like"; import { Like } from "~database/entities/Like";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -28,7 +28,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);

View file

@ -6,7 +6,7 @@ import { MatchedRoute } from "bun";
import { FindManyOptions } from "typeorm"; import { FindManyOptions } from "typeorm";
import { Like } from "~database/entities/Like"; import { Like } from "~database/entities/Like";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -30,7 +30,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
let foundStatus: Status | null; let foundStatus: Status | null;
try { try {

View file

@ -2,7 +2,7 @@ import { applyConfig } from "@api";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -27,7 +27,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
let foundStatus: Status | null; let foundStatus: Status | null;
try { try {

View file

@ -5,7 +5,7 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { FindManyOptions } from "typeorm"; import { FindManyOptions } from "typeorm";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -29,7 +29,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
let foundStatus: Status | null; let foundStatus: Status | null;
try { try {

View file

@ -4,7 +4,7 @@ import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Like } from "~database/entities/Like"; import { Like } from "~database/entities/Like";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -28,7 +28,7 @@ export default async (
): Promise<Response> => { ): Promise<Response> => {
const id = matchedRoute.params.id; const id = matchedRoute.params.id;
const { user } = await User.getFromRequest(req); const { user } = await UserAction.getFromRequest(req);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);

View file

@ -6,10 +6,11 @@ import { getConfig } from "@config";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { sanitizeHtml } from "@sanitization"; import { sanitizeHtml } from "@sanitization";
import { MatchedRoute } from "bun";
import { parse } from "marked"; import { parse } from "marked";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
import { Status, statusRelations } from "~database/entities/Status"; import { Status, statusRelations } from "~database/entities/Status";
import { User } from "~database/entities/User"; import { AuthData, UserAction } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -27,9 +28,13 @@ export const meta: APIRouteMeta = applyConfig({
/** /**
* Post new status * Post new status
*/ */
export default async (req: Request): Promise<Response> => { export default async (
const { user, token } = await User.getFromRequest(req); req: Request,
const application = await Application.getFromToken(token); matchedRoute: MatchedRoute,
authData: AuthData
): Promise<Response> => {
const { user, token } = authData;
const application = await ApplicationAction.getFromToken(token);
if (!user) return errorResponse("Unauthorized", 401); if (!user) return errorResponse("Unauthorized", 401);
@ -122,7 +127,7 @@ export default async (req: Request): Promise<Response> => {
// Get reply account and status if exists // Get reply account and status if exists
let replyStatus: Status | null = null; let replyStatus: Status | null = null;
let replyUser: User | null = null; let replyUser: UserAction | null = null;
if (in_reply_to_id) { if (in_reply_to_id) {
replyStatus = await Status.findOne({ replyStatus = await Status.findOne({

View file

@ -2,9 +2,10 @@
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun";
import { FindManyOptions } from "typeorm"; import { FindManyOptions } from "typeorm";
import { Status, statusAndUserRelations } from "~database/entities/Status"; import { Status, statusAndUserRelations } from "~database/entities/Status";
import { User } from "~database/entities/User"; import { AuthData } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -22,7 +23,11 @@ export const meta: APIRouteMeta = applyConfig({
/** /**
* Fetch home timeline statuses * Fetch home timeline statuses
*/ */
export default async (req: Request): Promise<Response> => { export default async (
req: Request,
matchedRoute: MatchedRoute,
authData: AuthData
): Promise<Response> => {
const { const {
limit = 20, limit = 20,
max_id, max_id,
@ -35,7 +40,7 @@ export default async (req: Request): Promise<Response> => {
limit?: number; limit?: number;
}>(req); }>(req);
const { user } = await User.getFromRequest(req); const { user } = authData;
if (limit < 1 || limit > 40) { if (limit < 1 || limit > 40) {
return errorResponse("Limit must be between 1 and 40", 400); return errorResponse("Limit must be between 1 and 40", 400);

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { applyConfig } from "@api"; import { applyConfig } from "@api";
import { parseRequest } from "@request"; import { parseRequest } from "@request";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
@ -18,9 +17,29 @@ export const meta: APIRouteMeta = applyConfig({
}, },
}); });
/** const updateQuery = async (
* Fetch public timeline statuses id: string | undefined,
*/ operator: string,
query: FindManyOptions<Status>
) => {
if (!id) return query;
const post = await Status.findOneBy({ id });
if (post) {
query = {
...query,
where: {
...query.where,
created_at: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
...(query.where as any)?.created_at,
[operator]: post.created_at,
},
},
};
}
return query;
};
export default async (req: Request): Promise<Response> => { export default async (req: Request): Promise<Response> => {
const { const {
local, local,
@ -59,53 +78,9 @@ export default async (req: Request): Promise<Response> => {
relations: statusAndUserRelations, relations: statusAndUserRelations,
}; };
if (max_id) { query = await updateQuery(max_id, "$lt", query);
const maxPost = await Status.findOneBy({ id: max_id }); query = await updateQuery(min_id, "$gt", query);
if (maxPost) { query = await updateQuery(since_id, "$gte", query);
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$lt: maxPost.created_at,
},
},
};
}
}
if (min_id) {
const minPost = await Status.findOneBy({ id: min_id });
if (minPost) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$gt: minPost.created_at,
},
},
};
}
}
if (since_id) {
const sincePost = await Status.findOneBy({ id: since_id });
if (sincePost) {
query = {
...query,
where: {
...query.where,
created_at: {
...(query.where as any)?.created_at,
$gte: sincePost.created_at,
},
},
};
}
}
if (only_media) { if (only_media) {
// TODO: add // TODO: add

View file

@ -2,9 +2,9 @@ import { applyConfig } from "@api";
import { errorResponse } from "@response"; import { errorResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
import { Token } from "~database/entities/Token"; import { Token } from "~database/entities/Token";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { APIRouteMeta } from "~types/api"; import { APIRouteMeta } from "~types/api";
export const meta: APIRouteMeta = applyConfig({ export const meta: APIRouteMeta = applyConfig({
@ -45,7 +45,7 @@ export default async (
return errorResponse("Missing username or password", 400); return errorResponse("Missing username or password", 400);
// Get user // Get user
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
email, email,
}, },
@ -56,7 +56,7 @@ export default async (
return errorResponse("Invalid username or password", 401); return errorResponse("Invalid username or password", 401);
// Get application // Get application
const application = await Application.findOneBy({ const application = await ApplicationAction.findOneBy({
client_id, client_id,
}); });

View file

@ -5,10 +5,10 @@ import { getConfig } from "@config";
import { getBestContentType } from "@content_types"; import { getBestContentType } from "@content_types";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { Emoji } from "~database/entities/Emoji"; import { EmojiAction } from "~database/entities/Emoji";
import { LysandObject } from "~database/entities/Object"; import { LysandObject } from "~database/entities/Object";
import { Status } from "~database/entities/Status"; import { Status } from "~database/entities/Status";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
import { import {
ContentFormat, ContentFormat,
LysandAction, LysandAction,
@ -61,7 +61,7 @@ export default async (
// Process request body // Process request body
const body = (await req.json()) as LysandPublication | LysandAction; const body = (await req.json()) as LysandPublication | LysandAction;
const author = await User.findOne({ const author = await UserAction.findOne({
where: { where: {
uri: body.author, uri: body.author,
}, },
@ -145,7 +145,7 @@ export default async (
const content = getBestContentType(body.contents); const content = getBestContentType(body.contents);
const emojis = await Emoji.parseEmojis(content?.content || ""); const emojis = await EmojiAction.parseEmojis(content?.content || "");
const newStatus = await Status.createNew({ const newStatus = await Status.createNew({
account: author, account: author,
@ -158,7 +158,7 @@ export default async (
sensitive: body.is_sensitive, sensitive: body.is_sensitive,
uri: body.uri, uri: body.uri,
emojis: emojis, emojis: emojis,
mentions: await User.parseMentions(body.mentions), mentions: await UserAction.parseMentions(body.mentions),
}); });
// If there is a reply, fetch all the reply parents and add them to the database // If there is a reply, fetch all the reply parents and add them to the database
@ -187,7 +187,7 @@ export default async (
const content = getBestContentType(patch.contents); const content = getBestContentType(patch.contents);
const emojis = await Emoji.parseEmojis(content?.content || ""); const emojis = await EmojiAction.parseEmojis(content?.content || "");
const status = await Status.findOneBy({ const status = await Status.findOneBy({
id: patch.patched_id, id: patch.patched_id,

View file

@ -4,7 +4,7 @@ import { applyConfig } from "@api";
import { getConfig } from "@config"; import { getConfig } from "@config";
import { errorResponse, jsonResponse } from "@response"; import { errorResponse, jsonResponse } from "@response";
import { MatchedRoute } from "bun"; import { MatchedRoute } from "bun";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
export const meta = applyConfig({ export const meta = applyConfig({
allowedMethods: ["POST"], allowedMethods: ["POST"],
@ -29,7 +29,7 @@ export default async (
const config = getConfig(); const config = getConfig();
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
id: uuid, id: uuid,
}, },

View file

@ -3,25 +3,25 @@
import { getConfig } from "@config"; import { getConfig } from "@config";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource"; import { AppDataSource } from "~database/datasource";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
import { Emoji } from "~database/entities/Emoji"; import { EmojiAction } from "~database/entities/Emoji";
import { Token, TokenType } from "~database/entities/Token"; import { Token, TokenType } from "~database/entities/Token";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { APIEmoji } from "~types/entities/emoji"; import { APIEmoji } from "~types/entities/emoji";
import { APIInstance } from "~types/entities/instance"; import { APIInstance } from "~types/entities/instance";
const config = getConfig(); const config = getConfig();
let token: Token; let token: Token;
let user: User; let user: UserAction;
let user2: User; let user2: UserAction;
describe("API Tests", () => { describe("API Tests", () => {
beforeAll(async () => { beforeAll(async () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize(); if (!AppDataSource.isInitialized) await AppDataSource.initialize();
// Initialize test user // Initialize test user
user = await User.createNewLocal({ user = await UserAction.createNewLocal({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
@ -29,14 +29,14 @@ describe("API Tests", () => {
}); });
// Initialize second test user // Initialize second test user
user2 = await User.createNewLocal({ user2 = await UserAction.createNewLocal({
email: "test2@test.com", email: "test2@test.com",
username: "test2", username: "test2",
password: "test2", password: "test2",
display_name: "", display_name: "",
}); });
const app = new Application(); const app = new ApplicationAction();
app.name = "Test Application"; app.name = "Test Application";
app.website = "https://example.com"; app.website = "https://example.com";
@ -106,7 +106,7 @@ describe("API Tests", () => {
describe("GET /api/v1/custom_emojis", () => { describe("GET /api/v1/custom_emojis", () => {
beforeAll(async () => { beforeAll(async () => {
const emoji = new Emoji(); const emoji = new EmojiAction();
emoji.instance = null; emoji.instance = null;
emoji.url = "https://example.com/test.png"; emoji.url = "https://example.com/test.png";
@ -139,7 +139,7 @@ describe("API Tests", () => {
expect(emojis[0].url).toBe("https://example.com/test.png"); expect(emojis[0].url).toBe("https://example.com/test.png");
}); });
afterAll(async () => { afterAll(async () => {
await Emoji.delete({ shortcode: "test" }); await EmojiAction.delete({ shortcode: "test" });
}); });
}); });
}); });

View file

@ -3,9 +3,9 @@
import { getConfig } from "@config"; import { getConfig } from "@config";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource"; import { AppDataSource } from "~database/datasource";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
import { Token, TokenType } from "~database/entities/Token"; import { Token, TokenType } from "~database/entities/Token";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { APIAccount } from "~types/entities/account"; import { APIAccount } from "~types/entities/account";
import { APIRelationship } from "~types/entities/relationship"; import { APIRelationship } from "~types/entities/relationship";
import { APIStatus } from "~types/entities/status"; import { APIStatus } from "~types/entities/status";
@ -13,15 +13,15 @@ import { APIStatus } from "~types/entities/status";
const config = getConfig(); const config = getConfig();
let token: Token; let token: Token;
let user: User; let user: UserAction;
let user2: User; let user2: UserAction;
describe("API Tests", () => { describe("API Tests", () => {
beforeAll(async () => { beforeAll(async () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize(); if (!AppDataSource.isInitialized) await AppDataSource.initialize();
// Initialize test user // Initialize test user
user = await User.createNewLocal({ user = await UserAction.createNewLocal({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
@ -29,14 +29,14 @@ describe("API Tests", () => {
}); });
// Initialize second test user // Initialize second test user
user2 = await User.createNewLocal({ user2 = await UserAction.createNewLocal({
email: "test2@test.com", email: "test2@test.com",
username: "test2", username: "test2",
password: "test2", password: "test2",
display_name: "", display_name: "",
}); });
const app = new Application(); const app = new ApplicationAction();
app.name = "Test Application"; app.name = "Test Application";
app.website = "https://example.com"; app.website = "https://example.com";

View file

@ -3,17 +3,17 @@
import { getConfig } from "@config"; import { getConfig } from "@config";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource"; import { AppDataSource } from "~database/datasource";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
import { Token, TokenType } from "~database/entities/Token"; import { Token, TokenType } from "~database/entities/Token";
import { User } from "~database/entities/User"; import { UserAction } from "~database/entities/User";
import { APIContext } from "~types/entities/context"; import { APIContext } from "~types/entities/context";
import { APIStatus } from "~types/entities/status"; import { APIStatus } from "~types/entities/status";
const config = getConfig(); const config = getConfig();
let token: Token; let token: Token;
let user: User; let user: UserAction;
let user2: User; let user2: UserAction;
let status: APIStatus | null = null; let status: APIStatus | null = null;
let status2: APIStatus | null = null; let status2: APIStatus | null = null;
@ -22,7 +22,7 @@ describe("API Tests", () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize(); if (!AppDataSource.isInitialized) await AppDataSource.initialize();
// Initialize test user // Initialize test user
user = await User.createNewLocal({ user = await UserAction.createNewLocal({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
@ -30,14 +30,14 @@ describe("API Tests", () => {
}); });
// Initialize second test user // Initialize second test user
user2 = await User.createNewLocal({ user2 = await UserAction.createNewLocal({
email: "test2@test.com", email: "test2@test.com",
username: "test2", username: "test2",
password: "test2", password: "test2",
display_name: "", display_name: "",
}); });
const app = new Application(); const app = new ApplicationAction();
app.name = "Test Application"; app.name = "Test Application";
app.website = "https://example.com"; app.website = "https://example.com";
@ -157,7 +157,7 @@ describe("API Tests", () => {
expect(status2.card).toBeNull(); expect(status2.card).toBeNull();
expect(status2.poll).toBeNull(); expect(status2.poll).toBeNull();
expect(status2.emojis).toEqual([]); expect(status2.emojis).toEqual([]);
expect(status2.in_reply_to_id).toEqual(status?.id); expect(status2.in_reply_to_id).toEqual(status?.id || null);
expect(status2.in_reply_to_account_id).toEqual(user.id); expect(status2.in_reply_to_account_id).toEqual(user.id);
}); });
}); });
@ -181,7 +181,7 @@ describe("API Tests", () => {
const statusJson = (await response.json()) as APIStatus; const statusJson = (await response.json()) as APIStatus;
expect(statusJson.id).toBe(status?.id); expect(statusJson.id).toBe(status?.id || "");
expect(statusJson.content).toBeDefined(); expect(statusJson.content).toBeDefined();
expect(statusJson.created_at).toBeDefined(); expect(statusJson.created_at).toBeDefined();
expect(statusJson.account).toBeDefined(); expect(statusJson.account).toBeDefined();
@ -231,7 +231,7 @@ describe("API Tests", () => {
expect(context.descendants.length).toBe(1); expect(context.descendants.length).toBe(1);
// First descendant should be status2 // First descendant should be status2
expect(context.descendants[0].id).toBe(status2?.id); expect(context.descendants[0].id).toBe(status2?.id || "");
}); });
}); });
@ -322,7 +322,7 @@ describe("API Tests", () => {
"application/json" "application/json"
); );
const users = (await response.json()) as User[]; const users = (await response.json()) as UserAction[];
expect(users.length).toBe(1); expect(users.length).toBe(1);
expect(users[0].id).toBe(user.id); expect(users[0].id).toBe(user.id);

View file

@ -1,9 +1,9 @@
import { getConfig } from "@config"; import { getConfig } from "@config";
import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { AppDataSource } from "~database/datasource"; import { AppDataSource } from "~database/datasource";
import { Application } from "~database/entities/Application"; import { ApplicationAction } from "~database/entities/Application";
import { Token } from "~database/entities/Token"; import { Token } from "~database/entities/Token";
import { User, userRelations } from "~database/entities/User"; import { UserAction, userRelations } from "~database/entities/User";
const config = getConfig(); const config = getConfig();
@ -16,7 +16,7 @@ beforeAll(async () => {
if (!AppDataSource.isInitialized) await AppDataSource.initialize(); if (!AppDataSource.isInitialized) await AppDataSource.initialize();
// Initialize test user // Initialize test user
await User.createNewLocal({ await UserAction.createNewLocal({
email: "test@test.com", email: "test@test.com",
username: "test", username: "test",
password: "test", password: "test",
@ -139,7 +139,7 @@ describe("GET /api/v1/apps/verify_credentials", () => {
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json"); expect(response.headers.get("content-type")).toBe("application/json");
const credentials = (await response.json()) as Partial<Application>; const credentials = (await response.json()) as Partial<ApplicationAction>;
expect(credentials.name).toBe("Test Application"); expect(credentials.name).toBe("Test Application");
expect(credentials.website).toBe("https://example.com"); expect(credentials.website).toBe("https://example.com");
@ -150,7 +150,7 @@ describe("GET /api/v1/apps/verify_credentials", () => {
afterAll(async () => { afterAll(async () => {
// Clean up user // Clean up user
const user = await User.findOne({ const user = await UserAction.findOne({
where: { where: {
username: "test", username: "test",
}, },
@ -164,7 +164,7 @@ afterAll(async () => {
}, },
}); });
const applications = await Application.findBy({ const applications = await ApplicationAction.findBy({
client_id, client_id,
secret: client_secret, secret: client_secret,
}); });