mirror of
https://github.com/versia-pub/server.git
synced 2026-01-26 20:26:01 +01:00
Fix existing bugs in tests, refactor users
This commit is contained in:
parent
b7587f8d3f
commit
65ff53e90c
92
benchmarks/posting.ts
Normal file
92
benchmarks/posting.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { getConfig } from "@config";
|
||||
import { AppDataSource } from "~database/datasource";
|
||||
import { Application } from "~database/entities/Application";
|
||||
import { RawActivity } from "~database/entities/RawActivity";
|
||||
import { Token, TokenType } from "~database/entities/Token";
|
||||
import { User } from "~database/entities/User";
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
let token: Token;
|
||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||
|
||||
// Initialize test user
|
||||
const user = await User.createNewLocal({
|
||||
email: "test@test.com",
|
||||
username: "test",
|
||||
password: "test",
|
||||
display_name: "",
|
||||
});
|
||||
|
||||
const app = new Application();
|
||||
|
||||
app.name = "Test Application";
|
||||
app.website = "https://example.com";
|
||||
app.client_id = "test";
|
||||
app.redirect_uris = "https://example.com";
|
||||
app.scopes = "read write";
|
||||
app.secret = "test";
|
||||
app.vapid_key = null;
|
||||
|
||||
await app.save();
|
||||
|
||||
// Initialize test token
|
||||
token = new Token();
|
||||
|
||||
token.access_token = "test";
|
||||
token.application = app;
|
||||
token.code = "test";
|
||||
token.scope = "read write";
|
||||
token.token_type = TokenType.BEARER;
|
||||
token.user = user;
|
||||
|
||||
token = await token.save();
|
||||
|
||||
await fetch(`${config.http.base_url}/api/v1/statuses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
status: "Hello, world!",
|
||||
visibility: "public",
|
||||
}),
|
||||
});
|
||||
|
||||
const timeBefore = performance.now();
|
||||
|
||||
// Repeat 100 times
|
||||
for (let i = 0; i < 100; i++) {
|
||||
await fetch(`${config.http.base_url}/api/v1/timelines/public`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const timeAfter = performance.now();
|
||||
|
||||
const activities = await RawActivity.createQueryBuilder("activity")
|
||||
.where("activity.data->>'actor' = :actor", {
|
||||
actor: `${config.http.base_url}/@test`,
|
||||
})
|
||||
.leftJoinAndSelect("activity.objects", "objects")
|
||||
.getMany();
|
||||
|
||||
// Delete all created objects and activities as part of testing
|
||||
await Promise.all(
|
||||
activities.map(async activity => {
|
||||
await Promise.all(
|
||||
activity.objects.map(async object => await object.remove())
|
||||
);
|
||||
await activity.remove();
|
||||
})
|
||||
);
|
||||
|
||||
await user.remove();
|
||||
|
||||
console.log(`Time taken: ${timeAfter - timeBefore}ms`);
|
||||
|
|
@ -34,7 +34,7 @@ export class Emoji extends BaseEntity {
|
|||
@ManyToOne(() => Instance, {
|
||||
nullable: true,
|
||||
})
|
||||
instance!: Instance;
|
||||
instance!: Instance | null;
|
||||
|
||||
/**
|
||||
* The URL for the emoji.
|
||||
|
|
@ -56,9 +56,9 @@ export class Emoji extends BaseEntity {
|
|||
async toAPI(): Promise<APIEmoji> {
|
||||
return {
|
||||
shortcode: this.shortcode,
|
||||
static_url: "",
|
||||
url: "",
|
||||
visible_in_picker: false,
|
||||
static_url: this.url, // TODO: Add static version
|
||||
url: this.url,
|
||||
visible_in_picker: this.visible_in_picker,
|
||||
category: undefined,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {
|
|||
BaseEntity,
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
|
|
@ -23,6 +24,8 @@ export class RawActivity extends BaseEntity {
|
|||
id!: string;
|
||||
|
||||
@Column("jsonb")
|
||||
// Index ID for faster lookups
|
||||
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
|
||||
data!: APActivity;
|
||||
|
||||
@ManyToMany(() => RawObject)
|
||||
|
|
@ -239,7 +242,6 @@ export class RawActivity extends BaseEntity {
|
|||
|
||||
const rawActor = new RawActor();
|
||||
rawActor.data = actor;
|
||||
rawActor.followers = [];
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
|
|
|
|||
|
|
@ -3,18 +3,15 @@ import {
|
|||
BaseEntity,
|
||||
Column,
|
||||
Entity,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
} from "typeorm";
|
||||
import { APActor, APImage, APOrderedCollectionPage } from "activitypub-types";
|
||||
import { APActor, APImage } from "activitypub-types";
|
||||
import { getConfig, getHost } from "@config";
|
||||
import { appendFile } from "fs/promises";
|
||||
import { errorResponse } from "@response";
|
||||
import { APIAccount } from "~types/entities/account";
|
||||
import { RawActivity } from "./RawActivity";
|
||||
import { RawObject } from "./RawObject";
|
||||
|
||||
/**
|
||||
* Represents a raw actor entity in the database.
|
||||
*/
|
||||
|
|
@ -30,21 +27,9 @@ export class RawActor extends BaseEntity {
|
|||
* The ActivityPub actor data associated with the actor.
|
||||
*/
|
||||
@Column("jsonb")
|
||||
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
|
||||
data!: APActor;
|
||||
|
||||
/**
|
||||
* The list of follower IDs associated with the actor.
|
||||
*/
|
||||
@Column("jsonb", { default: [] })
|
||||
followers!: string[];
|
||||
|
||||
/**
|
||||
* The list of featured objects associated with the actor.
|
||||
*/
|
||||
@ManyToMany(() => RawObject, { nullable: true })
|
||||
@JoinTable()
|
||||
featured!: RawObject[];
|
||||
|
||||
/**
|
||||
* Retrieves a RawActor entity by actor ID.
|
||||
* @param id The ID of the actor to retrieve.
|
||||
|
|
@ -62,13 +47,13 @@ export class RawActor extends BaseEntity {
|
|||
* @returns The newly created RawActor entity, or an error response if the actor already exists or is filtered.
|
||||
*/
|
||||
static async addIfNotExists(data: APActor) {
|
||||
// TODO: Also add corresponding user
|
||||
if (await RawActor.exists(data.id ?? "")) {
|
||||
return errorResponse("Actor already exists", 409);
|
||||
}
|
||||
|
||||
const actor = new RawActor();
|
||||
actor.data = data;
|
||||
actor.followers = [];
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
|
|
@ -105,33 +90,6 @@ export class RawActor extends BaseEntity {
|
|||
return new URL(this.data.id ?? "").host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of followers associated with the actor and updates the `followers` property.
|
||||
*/
|
||||
async fetchFollowers() {
|
||||
let followers: APOrderedCollectionPage = await fetch(
|
||||
`${this.data.followers?.toString() ?? ""}?page=1`,
|
||||
{
|
||||
headers: { Accept: "application/activity+json" },
|
||||
}
|
||||
);
|
||||
|
||||
let followersList = followers.orderedItems ?? [];
|
||||
|
||||
while (followers.type === "OrderedCollectionPage" && followers.next) {
|
||||
followers = await fetch((followers.next as string).toString(), {
|
||||
headers: { Accept: "application/activity+json" },
|
||||
}).then(res => res.json());
|
||||
|
||||
followersList = {
|
||||
...followersList,
|
||||
...(followers.orderedItems ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
this.followers = followersList as string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the RawActor entity to an API account object.
|
||||
* @param isOwnAccount Whether the account is the user's own account.
|
||||
|
|
@ -169,7 +127,7 @@ export class RawActor extends BaseEntity {
|
|||
config.defaults.header,
|
||||
locked: false,
|
||||
created_at: new Date(published ?? 0).toISOString(),
|
||||
followers_count: this.followers.length,
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
statuses_count: statusCount,
|
||||
emojis: [],
|
||||
|
|
@ -249,7 +207,6 @@ export class RawActor extends BaseEntity {
|
|||
* @returns Whether an actor with the specified ID exists in the database.
|
||||
*/
|
||||
static async exists(id: string) {
|
||||
console.log(!!(await RawActor.getByActorId(id)));
|
||||
return !!(await RawActor.getByActorId(id));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
import {
|
||||
BaseEntity,
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
PrimaryGeneratedColumn,
|
||||
} from "typeorm";
|
||||
import { APImage, APObject, DateTime } from "activitypub-types";
|
||||
import { getConfig } from "@config";
|
||||
import { appendFile } from "fs/promises";
|
||||
|
|
@ -6,6 +12,7 @@ import { APIStatus } from "~types/entities/status";
|
|||
import { RawActor } from "./RawActor";
|
||||
import { APIAccount } from "~types/entities/account";
|
||||
import { APIEmoji } from "~types/entities/emoji";
|
||||
import { User } from "./User";
|
||||
|
||||
/**
|
||||
* Represents a raw ActivityPub object in the database.
|
||||
|
|
@ -24,6 +31,11 @@ export class RawObject extends BaseEntity {
|
|||
* The data associated with the object.
|
||||
*/
|
||||
@Column("jsonb")
|
||||
// Index ID, attributedTo, to and published for faster lookups
|
||||
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
|
||||
@Index({ where: "(data->>'attributedTo') IS NOT NULL" })
|
||||
@Index({ where: "(data->>'to') IS NOT NULL" })
|
||||
@Index({ where: "(data->>'published') IS NOT NULL" })
|
||||
data!: APObject;
|
||||
|
||||
/**
|
||||
|
|
@ -82,10 +94,8 @@ export class RawObject extends BaseEntity {
|
|||
return {
|
||||
account:
|
||||
(await (
|
||||
await RawActor.getByActorId(
|
||||
this.data.attributedTo as string
|
||||
)
|
||||
)?.toAPIAccount()) ?? (null as unknown as APIAccount),
|
||||
await User.getByActorId(this.data.attributedTo as string)
|
||||
)?.toAPI()) ?? (null as unknown as APIAccount),
|
||||
created_at: new Date(this.data.published as DateTime).toISOString(),
|
||||
id: this.id,
|
||||
in_reply_to_id: null,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import { Application } from "./Application";
|
|||
import { Emoji } from "./Emoji";
|
||||
import { RawActivity } from "./RawActivity";
|
||||
import { RawObject } from "./RawObject";
|
||||
import { RawActor } from "./RawActor";
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
|
|
@ -100,10 +99,10 @@ export class Status extends BaseEntity {
|
|||
/**
|
||||
* The raw actor that this status is a reply to, if any.
|
||||
*/
|
||||
@ManyToOne(() => RawActor, {
|
||||
@ManyToOne(() => User, {
|
||||
nullable: true,
|
||||
})
|
||||
in_reply_to_account!: RawActor | null;
|
||||
in_reply_to_account!: User | null;
|
||||
|
||||
/**
|
||||
* Whether this status is sensitive.
|
||||
|
|
@ -196,7 +195,7 @@ export class Status extends BaseEntity {
|
|||
emojis: Emoji[];
|
||||
reply?: {
|
||||
object: RawObject;
|
||||
actor: RawActor;
|
||||
user: User;
|
||||
};
|
||||
}) {
|
||||
const newStatus = new Status();
|
||||
|
|
@ -217,7 +216,7 @@ export class Status extends BaseEntity {
|
|||
|
||||
if (data.reply) {
|
||||
newStatus.in_reply_to_post = data.reply.object;
|
||||
newStatus.in_reply_to_account = data.reply.actor;
|
||||
newStatus.in_reply_to_account = data.reply.user;
|
||||
}
|
||||
|
||||
newStatus.object.data = {
|
||||
|
|
@ -240,8 +239,8 @@ export class Status extends BaseEntity {
|
|||
return `${config.http.base_url}/@${match[1]}`;
|
||||
});
|
||||
|
||||
// Map this to Actors
|
||||
const mentionedActors = (
|
||||
// Map this to Users
|
||||
const mentionedUsers = (
|
||||
await Promise.all(
|
||||
mentionedPeople.map(async person => {
|
||||
// Check if post is in format @username or @username@instance.com
|
||||
|
|
@ -252,23 +251,23 @@ export class Status extends BaseEntity {
|
|||
: null;
|
||||
|
||||
if (instanceUrl) {
|
||||
const actor = await RawActor.createQueryBuilder("actor")
|
||||
.where("actor.data->>'id' = :id", {
|
||||
// Where ID contains the instance URL
|
||||
id: `%${instanceUrl}%`,
|
||||
})
|
||||
// Where actor preferredUsername is the username
|
||||
.andWhere(
|
||||
"actor.data->>'preferredUsername' = :username",
|
||||
{
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
username: person.split("@")[1],
|
||||
}
|
||||
)
|
||||
.getOne();
|
||||
// If contains instanceUrl
|
||||
instance: {
|
||||
base_url: instanceUrl,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
actor: true,
|
||||
instance: true,
|
||||
},
|
||||
});
|
||||
|
||||
return actor?.data.id;
|
||||
return user?.actor.data.id;
|
||||
} else {
|
||||
const actor = await User.findOne({
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
username: person.split("@")[1],
|
||||
},
|
||||
|
|
@ -277,13 +276,13 @@ export class Status extends BaseEntity {
|
|||
},
|
||||
});
|
||||
|
||||
return actor?.actor.data.id;
|
||||
return user?.actor.data.id;
|
||||
}
|
||||
})
|
||||
)
|
||||
).map(actor => actor as string);
|
||||
).map(user => user as string);
|
||||
|
||||
newStatus.object.data.to = mentionedActors;
|
||||
newStatus.object.data.to = mentionedUsers;
|
||||
|
||||
if (data.visibility === "private") {
|
||||
newStatus.object.data.cc = [
|
||||
|
|
|
|||
|
|
@ -14,12 +14,13 @@ import {
|
|||
} from "typeorm";
|
||||
import { APIAccount } from "~types/entities/account";
|
||||
import { RawActor } from "./RawActor";
|
||||
import { APActor } from "activitypub-types";
|
||||
import { APActor, APOrderedCollectionPage } from "activitypub-types";
|
||||
import { RawObject } from "./RawObject";
|
||||
import { Token } from "./Token";
|
||||
import { Status } from "./Status";
|
||||
import { APISource } from "~types/entities/source";
|
||||
import { Relationship } from "./Relationship";
|
||||
import { Instance } from "./Instance";
|
||||
|
||||
/**
|
||||
* Represents a user in the database.
|
||||
|
|
@ -52,16 +53,19 @@ export class User extends BaseEntity {
|
|||
/**
|
||||
* The password for the user.
|
||||
*/
|
||||
@Column("varchar")
|
||||
password!: string;
|
||||
@Column("varchar", {
|
||||
nullable: true,
|
||||
})
|
||||
password!: string | null;
|
||||
|
||||
/**
|
||||
* The email address for the user.
|
||||
*/
|
||||
@Column("varchar", {
|
||||
unique: true,
|
||||
nullable: true,
|
||||
})
|
||||
email!: string;
|
||||
email!: string | null;
|
||||
|
||||
/**
|
||||
* The note for the user.
|
||||
|
|
@ -118,8 +122,10 @@ export class User extends BaseEntity {
|
|||
/**
|
||||
* The private key for the user.
|
||||
*/
|
||||
@Column("varchar")
|
||||
private_key!: string;
|
||||
@Column("varchar", {
|
||||
nullable: true,
|
||||
})
|
||||
private_key!: string | null;
|
||||
|
||||
/**
|
||||
* The relationships for the user.
|
||||
|
|
@ -127,6 +133,16 @@ export class User extends BaseEntity {
|
|||
@OneToMany(() => Relationship, relationship => relationship.owner)
|
||||
relationships!: Relationship[];
|
||||
|
||||
/**
|
||||
* User's instance, null if local user
|
||||
*/
|
||||
@ManyToOne(() => Instance, {
|
||||
nullable: true,
|
||||
})
|
||||
instance!: Instance | null;
|
||||
|
||||
/** */
|
||||
|
||||
/**
|
||||
* The actor for the user.
|
||||
*/
|
||||
|
|
@ -140,6 +156,50 @@ export class User extends BaseEntity {
|
|||
@JoinTable()
|
||||
pinned_notes!: RawObject[];
|
||||
|
||||
/**
|
||||
* Update this user data from its actor
|
||||
* @returns The updated user.
|
||||
*/
|
||||
async updateFromActor() {
|
||||
const actor = await this.actor.toAPIAccount();
|
||||
|
||||
this.username = actor.username;
|
||||
this.display_name = actor.display_name;
|
||||
this.note = actor.note;
|
||||
this.avatar = actor.avatar;
|
||||
this.header = actor.header;
|
||||
this.avatar = actor.avatar;
|
||||
|
||||
return await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the list of followers associated with the actor and updates the user's followers
|
||||
*/
|
||||
async fetchFollowers() {
|
||||
let followers: APOrderedCollectionPage = await fetch(
|
||||
`${this.actor.data.followers?.toString() ?? ""}?page=1`,
|
||||
{
|
||||
headers: { Accept: "application/activity+json" },
|
||||
}
|
||||
);
|
||||
|
||||
let followersList = followers.orderedItems ?? [];
|
||||
|
||||
while (followers.type === "OrderedCollectionPage" && followers.next) {
|
||||
followers = await fetch((followers.next as string).toString(), {
|
||||
headers: { Accept: "application/activity+json" },
|
||||
}).then(res => res.json());
|
||||
|
||||
followersList = {
|
||||
...followersList,
|
||||
...(followers.orderedItems ?? []),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: integrate followers
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a user by actor ID.
|
||||
* @param id The actor ID to search for.
|
||||
|
|
@ -159,11 +219,11 @@ export class User extends BaseEntity {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
* Creates a new LOCAL user.
|
||||
* @param data The data for the new user.
|
||||
* @returns The newly created user.
|
||||
*/
|
||||
static async createNew(data: {
|
||||
static async createNewLocal(data: {
|
||||
username: string;
|
||||
display_name?: string;
|
||||
password: string;
|
||||
|
|
@ -184,6 +244,7 @@ export class User extends BaseEntity {
|
|||
user.header = data.header ?? config.defaults.avatar;
|
||||
|
||||
user.relationships = [];
|
||||
user.instance = null;
|
||||
|
||||
user.source = {
|
||||
language: null,
|
||||
|
|
@ -401,7 +462,32 @@ export class User extends BaseEntity {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async toAPI(): Promise<APIAccount> {
|
||||
return await this.actor.toAPIAccount();
|
||||
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"],
|
||||
});
|
||||
|
||||
return {
|
||||
...(await this.actor.toAPIAccount(isOwnAccount)),
|
||||
id: this.id,
|
||||
followers_count: follower_count,
|
||||
following_count: following_count,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { MatchedRoute } from "bun";
|
||||
import { RawActor } from "~database/entities/RawActor";
|
||||
import { User } from "~database/entities/User";
|
||||
|
||||
/**
|
||||
|
|
@ -20,9 +19,9 @@ export default async (
|
|||
|
||||
const user = await User.retrieveFromToken(token);
|
||||
|
||||
let foundUser: RawActor | null;
|
||||
let foundUser: User | null;
|
||||
try {
|
||||
foundUser = await RawActor.findOneBy({
|
||||
foundUser = await User.findOneBy({
|
||||
id,
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
@ -31,7 +30,5 @@ export default async (
|
|||
|
||||
if (!foundUser) return errorResponse("User not found", 404);
|
||||
|
||||
return jsonResponse(
|
||||
await foundUser.toAPIAccount(user?.id === foundUser.id)
|
||||
);
|
||||
return jsonResponse(await foundUser.toAPI(user?.id === foundUser.id));
|
||||
};
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export default async (req: Request): Promise<Response> => {
|
|||
|
||||
// TODO: Check if locale is valid
|
||||
|
||||
await User.createNew({
|
||||
await User.createNewLocal({
|
||||
username: body.username ?? "",
|
||||
password: body.password ?? "",
|
||||
email: body.email ?? "",
|
||||
|
|
|
|||
17
server/api/api/v1/custom_emojis/index.ts
Normal file
17
server/api/api/v1/custom_emojis/index.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { jsonResponse } from "@response";
|
||||
import { IsNull } from "typeorm";
|
||||
import { Emoji } from "~database/entities/Emoji";
|
||||
|
||||
/**
|
||||
* Creates a new user
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
export default async (): Promise<Response> => {
|
||||
const emojis = await Emoji.findBy({
|
||||
instance: IsNull(),
|
||||
});
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(emojis.map(async emoji => await emoji.toAPI()))
|
||||
);
|
||||
};
|
||||
|
|
@ -37,7 +37,7 @@ export default async (
|
|||
if (req.method === "GET") {
|
||||
return jsonResponse(await foundStatus.toAPI());
|
||||
} else if (req.method === "DELETE") {
|
||||
if ((await foundStatus.toAPI()).account.id !== user?.actor.id) {
|
||||
if ((await foundStatus.toAPI()).account.id !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import { parseRequest } from "@request";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { APActor } from "activitypub-types";
|
||||
import { Application } from "~database/entities/Application";
|
||||
import { RawActor } from "~database/entities/RawActor";
|
||||
import { RawObject } from "~database/entities/RawObject";
|
||||
import { Status } from "~database/entities/Status";
|
||||
import { User } from "~database/entities/User";
|
||||
|
|
@ -108,7 +107,7 @@ export default async (req: Request): Promise<Response> => {
|
|||
|
||||
// Get reply account and status if exists
|
||||
let replyObject: RawObject | null = null;
|
||||
let replyActor: RawActor | null = null;
|
||||
let replyUser: User | null = null;
|
||||
|
||||
if (in_reply_to_id) {
|
||||
replyObject = await RawObject.findOne({
|
||||
|
|
@ -117,7 +116,7 @@ export default async (req: Request): Promise<Response> => {
|
|||
},
|
||||
});
|
||||
|
||||
replyActor = await RawActor.getByActorId(
|
||||
replyUser = await User.getByActorId(
|
||||
(replyObject?.data.attributedTo as APActor).id ?? ""
|
||||
);
|
||||
}
|
||||
|
|
@ -138,9 +137,9 @@ export default async (req: Request): Promise<Response> => {
|
|||
spoiler_text: spoiler_text || "",
|
||||
emojis: [],
|
||||
reply:
|
||||
replyObject && replyActor
|
||||
replyObject && replyUser
|
||||
? {
|
||||
actor: replyActor,
|
||||
user: replyUser,
|
||||
object: replyObject,
|
||||
}
|
||||
: undefined,
|
||||
|
|
|
|||
68
server/api/api/v1/timelines/home.ts
Normal file
68
server/api/api/v1/timelines/home.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { parseRequest } from "@request";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { RawObject } from "~database/entities/RawObject";
|
||||
|
||||
/**
|
||||
* Fetch home timeline statuses
|
||||
*/
|
||||
export default async (req: Request): Promise<Response> => {
|
||||
const {
|
||||
limit = 20,
|
||||
max_id,
|
||||
min_id,
|
||||
since_id,
|
||||
} = await parseRequest<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
}>(req);
|
||||
|
||||
if (limit < 1 || limit > 40) {
|
||||
return errorResponse("Limit must be between 1 and 40", 400);
|
||||
}
|
||||
|
||||
let query = RawObject.createQueryBuilder("object")
|
||||
.where("object.data->>'type' = 'Note'")
|
||||
// From a user followed by the current user
|
||||
.andWhere("CAST(object.data->>'to' AS jsonb) @> CAST(:to AS jsonb)", {
|
||||
to: JSON.stringify([
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
]),
|
||||
})
|
||||
.orderBy("object.data->>'published'", "DESC")
|
||||
.take(limit);
|
||||
|
||||
if (max_id) {
|
||||
const maxPost = await RawObject.findOneBy({ id: max_id });
|
||||
if (maxPost) {
|
||||
query = query.andWhere("object.data->>'published' < :max_date", {
|
||||
max_date: maxPost.data.published,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (min_id) {
|
||||
const minPost = await RawObject.findOneBy({ id: min_id });
|
||||
if (minPost) {
|
||||
query = query.andWhere("object.data->>'published' > :min_date", {
|
||||
min_date: minPost.data.published,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (since_id) {
|
||||
const sincePost = await RawObject.findOneBy({ id: since_id });
|
||||
if (sincePost) {
|
||||
query = query.andWhere("object.data->>'published' >= :since_date", {
|
||||
since_date: sincePost.data.published,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const objects = await query.getMany();
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(objects.map(async object => await object.toAPI()))
|
||||
);
|
||||
};
|
||||
|
|
@ -35,7 +35,7 @@ export default async (
|
|||
email,
|
||||
});
|
||||
|
||||
if (!user || !(await Bun.password.verify(password, user.password)))
|
||||
if (!user || !(await Bun.password.verify(password, user.password || "")))
|
||||
return errorResponse("Invalid username or password", 401);
|
||||
|
||||
// Get application
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ beforeAll(async () => {
|
|||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||
|
||||
// Initialize test user
|
||||
await User.createNew({
|
||||
await User.createNewLocal({
|
||||
email: "test@test.com",
|
||||
username: "test",
|
||||
password: "test",
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ import { getConfig } from "@config";
|
|||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { AppDataSource } from "~database/datasource";
|
||||
import { Application } from "~database/entities/Application";
|
||||
import { Emoji } from "~database/entities/Emoji";
|
||||
import { RawActivity } from "~database/entities/RawActivity";
|
||||
import { Token, TokenType } from "~database/entities/Token";
|
||||
import { User } from "~database/entities/User";
|
||||
import { APIAccount } from "~types/entities/account";
|
||||
import { APIEmoji } from "~types/entities/emoji";
|
||||
import { APIInstance } from "~types/entities/instance";
|
||||
import { APIRelationship } from "~types/entities/relationship";
|
||||
import { APIStatus } from "~types/entities/status";
|
||||
|
|
@ -23,7 +25,7 @@ beforeAll(async () => {
|
|||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||
|
||||
// Initialize test user
|
||||
user = await User.createNew({
|
||||
user = await User.createNewLocal({
|
||||
email: "test@test.com",
|
||||
username: "test",
|
||||
password: "test",
|
||||
|
|
@ -31,7 +33,7 @@ beforeAll(async () => {
|
|||
});
|
||||
|
||||
// Initialize second test user
|
||||
user2 = await User.createNew({
|
||||
user2 = await User.createNewLocal({
|
||||
email: "test2@test.com",
|
||||
username: "test2",
|
||||
password: "test2",
|
||||
|
|
@ -105,7 +107,7 @@ describe("POST /api/v1/statuses", () => {
|
|||
status = (await response.json()) as APIStatus;
|
||||
expect(status.content).toBe("Hello, world!");
|
||||
expect(status.visibility).toBe("public");
|
||||
expect(status.account.id).toBe(user.actor.id);
|
||||
expect(status.account.id).toBe(user.id);
|
||||
expect(status.replies_count).toBe(0);
|
||||
expect(status.favourites_count).toBe(0);
|
||||
expect(status.reblogged).toBe(false);
|
||||
|
|
@ -238,7 +240,7 @@ describe("GET /api/v1/accounts/:id/statuses", () => {
|
|||
// Basic validation
|
||||
expect(status1.content).toBe("Hello, world!");
|
||||
expect(status1.visibility).toBe("public");
|
||||
expect(status1.account.id).toBe(user.actor.id);
|
||||
expect(status1.account.id).toBe(user.id);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -678,6 +680,42 @@ describe("GET /api/v1/instance", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("GET /api/v1/custom_emojis", () => {
|
||||
beforeAll(async () => {
|
||||
const emoji = new Emoji();
|
||||
|
||||
emoji.instance = null;
|
||||
emoji.url = "https://example.com";
|
||||
emoji.shortcode = "test";
|
||||
emoji.visible_in_picker = true;
|
||||
|
||||
await emoji.save();
|
||||
});
|
||||
test("should return an array of at least one custom emoji", async () => {
|
||||
const response = await fetch(
|
||||
`${config.http.base_url}/api/v1/custom_emojis`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe("application/json");
|
||||
|
||||
const emojis: APIEmoji[] = await response.json();
|
||||
|
||||
expect(emojis.length).toBeGreaterThan(0);
|
||||
expect(emojis[0].shortcode).toBe("test");
|
||||
expect(emojis[0].url).toBe("https://example.com");
|
||||
});
|
||||
afterAll(async () => {
|
||||
await Emoji.delete({ shortcode: "test" });
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const activities = await RawActivity.createQueryBuilder("activity")
|
||||
.where("activity.data->>'actor' = :actor", {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ beforeAll(async () => {
|
|||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||
|
||||
// Initialize test user
|
||||
await User.createNew({
|
||||
await User.createNewLocal({
|
||||
email: "test@test.com",
|
||||
username: "test",
|
||||
password: "test",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ beforeAll(async () => {
|
|||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||
|
||||
// Initialize test user
|
||||
await User.createNew({
|
||||
await User.createNewLocal({
|
||||
email: "test@test.com",
|
||||
username: "test",
|
||||
password: "test",
|
||||
|
|
@ -32,6 +32,7 @@ describe("POST /api/v1/apps/", () => {
|
|||
formData.append("website", "https://example.com");
|
||||
formData.append("redirect_uris", "https://example.com");
|
||||
formData.append("scopes", "read write");
|
||||
|
||||
const response = await fetch(`${config.http.base_url}/api/v1/apps/`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
|
|
@ -64,10 +65,10 @@ describe("POST /auth/login/", () => {
|
|||
test("should get a code", async () => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append("username", "test");
|
||||
formData.append("email", "test@test.com");
|
||||
formData.append("password", "test");
|
||||
const response = await fetch(
|
||||
`${config.http.base_url}/auth/login/?client_id=${client_id}&redirect_uri=https://example.com&response_type=code&scopes=read+write`,
|
||||
`${config.http.base_url}/auth/login/?client_id=${client_id}&redirect_uri=https://example.com&response_type=code&scope=read+write`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
|
|
@ -93,7 +94,7 @@ describe("POST /oauth/token/", () => {
|
|||
formData.append("redirect_uri", "https://example.com");
|
||||
formData.append("client_id", client_id);
|
||||
formData.append("client_secret", client_secret);
|
||||
formData.append("scope", "read write");
|
||||
formData.append("scope", "read+write");
|
||||
|
||||
const response = await fetch(`${config.http.base_url}/oauth/token/`, {
|
||||
method: "POST",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export interface ConfigType {
|
|||
discard_avatars: string[];
|
||||
force_sensitive: string[];
|
||||
remove_media: string[];
|
||||
fetch_all_colletion_members: boolean;
|
||||
};
|
||||
|
||||
filters: {
|
||||
|
|
@ -163,6 +164,7 @@ export const configDefaults: ConfigType = {
|
|||
discard_avatars: [],
|
||||
force_sensitive: [],
|
||||
remove_media: [],
|
||||
fetch_all_colletion_members: false,
|
||||
},
|
||||
filters: {
|
||||
note_filters: [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue