mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
HUGE rewrite to use Prisma instead of TypeORM (not finished yet)
This commit is contained in:
parent
a1c0164e9d
commit
5eed8374cd
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
});
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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}`,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
|
||||||
17
index.ts
17
index.ts
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
166
prisma/schema.prisma
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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 ?? "",
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 || "";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue