mirror of
https://github.com/versia-pub/server.git
synced 2026-01-26 04:06: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, {
|
@ManyToOne(() => Instance, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
instance!: Instance;
|
instance!: Instance | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL for the emoji.
|
* The URL for the emoji.
|
||||||
|
|
@ -56,9 +56,9 @@ export class Emoji extends BaseEntity {
|
||||||
async toAPI(): Promise<APIEmoji> {
|
async toAPI(): Promise<APIEmoji> {
|
||||||
return {
|
return {
|
||||||
shortcode: this.shortcode,
|
shortcode: this.shortcode,
|
||||||
static_url: "",
|
static_url: this.url, // TODO: Add static version
|
||||||
url: "",
|
url: this.url,
|
||||||
visible_in_picker: false,
|
visible_in_picker: this.visible_in_picker,
|
||||||
category: undefined,
|
category: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {
|
||||||
BaseEntity,
|
BaseEntity,
|
||||||
Column,
|
Column,
|
||||||
Entity,
|
Entity,
|
||||||
|
Index,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
|
|
@ -23,6 +24,8 @@ export class RawActivity extends BaseEntity {
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
@Column("jsonb")
|
@Column("jsonb")
|
||||||
|
// Index ID for faster lookups
|
||||||
|
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
|
||||||
data!: APActivity;
|
data!: APActivity;
|
||||||
|
|
||||||
@ManyToMany(() => RawObject)
|
@ManyToMany(() => RawObject)
|
||||||
|
|
@ -239,7 +242,6 @@ export class RawActivity extends BaseEntity {
|
||||||
|
|
||||||
const rawActor = new RawActor();
|
const rawActor = new RawActor();
|
||||||
rawActor.data = actor;
|
rawActor.data = actor;
|
||||||
rawActor.followers = [];
|
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,15 @@ import {
|
||||||
BaseEntity,
|
BaseEntity,
|
||||||
Column,
|
Column,
|
||||||
Entity,
|
Entity,
|
||||||
JoinTable,
|
Index,
|
||||||
ManyToMany,
|
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { APActor, APImage, APOrderedCollectionPage } from "activitypub-types";
|
import { APActor, APImage } from "activitypub-types";
|
||||||
import { getConfig, getHost } from "@config";
|
import { getConfig, getHost } from "@config";
|
||||||
import { appendFile } from "fs/promises";
|
import { appendFile } from "fs/promises";
|
||||||
import { errorResponse } from "@response";
|
import { errorResponse } from "@response";
|
||||||
import { APIAccount } from "~types/entities/account";
|
import { APIAccount } from "~types/entities/account";
|
||||||
import { RawActivity } from "./RawActivity";
|
import { RawActivity } from "./RawActivity";
|
||||||
import { RawObject } from "./RawObject";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a raw actor entity in the database.
|
* 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.
|
* The ActivityPub actor data associated with the actor.
|
||||||
*/
|
*/
|
||||||
@Column("jsonb")
|
@Column("jsonb")
|
||||||
|
@Index({ unique: true, where: "(data->>'id') IS NOT NULL" })
|
||||||
data!: APActor;
|
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.
|
* Retrieves a RawActor entity by actor ID.
|
||||||
* @param id The ID of the actor to retrieve.
|
* @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.
|
* @returns The newly created RawActor entity, or an error response if the actor already exists or is filtered.
|
||||||
*/
|
*/
|
||||||
static async addIfNotExists(data: APActor) {
|
static async addIfNotExists(data: APActor) {
|
||||||
|
// TODO: Also add corresponding user
|
||||||
if (await RawActor.exists(data.id ?? "")) {
|
if (await RawActor.exists(data.id ?? "")) {
|
||||||
return errorResponse("Actor already exists", 409);
|
return errorResponse("Actor already exists", 409);
|
||||||
}
|
}
|
||||||
|
|
||||||
const actor = new RawActor();
|
const actor = new RawActor();
|
||||||
actor.data = data;
|
actor.data = data;
|
||||||
actor.followers = [];
|
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -105,33 +90,6 @@ export class RawActor extends BaseEntity {
|
||||||
return new URL(this.data.id ?? "").host;
|
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.
|
* Converts the RawActor entity to an API account object.
|
||||||
* @param isOwnAccount Whether the account is the user's own account.
|
* @param isOwnAccount Whether the account is the user's own account.
|
||||||
|
|
@ -169,7 +127,7 @@ export class RawActor extends BaseEntity {
|
||||||
config.defaults.header,
|
config.defaults.header,
|
||||||
locked: false,
|
locked: false,
|
||||||
created_at: new Date(published ?? 0).toISOString(),
|
created_at: new Date(published ?? 0).toISOString(),
|
||||||
followers_count: this.followers.length,
|
followers_count: 0,
|
||||||
following_count: 0,
|
following_count: 0,
|
||||||
statuses_count: statusCount,
|
statuses_count: statusCount,
|
||||||
emojis: [],
|
emojis: [],
|
||||||
|
|
@ -249,7 +207,6 @@ export class RawActor extends BaseEntity {
|
||||||
* @returns Whether an actor with the specified ID exists in the database.
|
* @returns Whether an actor with the specified ID exists in the database.
|
||||||
*/
|
*/
|
||||||
static async exists(id: string) {
|
static async exists(id: string) {
|
||||||
console.log(!!(await RawActor.getByActorId(id)));
|
|
||||||
return !!(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 { APImage, APObject, DateTime } from "activitypub-types";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
import { appendFile } from "fs/promises";
|
import { appendFile } from "fs/promises";
|
||||||
|
|
@ -6,6 +12,7 @@ import { APIStatus } from "~types/entities/status";
|
||||||
import { RawActor } from "./RawActor";
|
import { RawActor } from "./RawActor";
|
||||||
import { APIAccount } from "~types/entities/account";
|
import { APIAccount } from "~types/entities/account";
|
||||||
import { APIEmoji } from "~types/entities/emoji";
|
import { APIEmoji } from "~types/entities/emoji";
|
||||||
|
import { User } from "./User";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a raw ActivityPub object in the database.
|
* Represents a raw ActivityPub object in the database.
|
||||||
|
|
@ -24,6 +31,11 @@ export class RawObject extends BaseEntity {
|
||||||
* The data associated with the object.
|
* The data associated with the object.
|
||||||
*/
|
*/
|
||||||
@Column("jsonb")
|
@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;
|
data!: APObject;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -82,10 +94,8 @@ export class RawObject extends BaseEntity {
|
||||||
return {
|
return {
|
||||||
account:
|
account:
|
||||||
(await (
|
(await (
|
||||||
await RawActor.getByActorId(
|
await User.getByActorId(this.data.attributedTo as string)
|
||||||
this.data.attributedTo as string
|
)?.toAPI()) ?? (null as unknown as APIAccount),
|
||||||
)
|
|
||||||
)?.toAPIAccount()) ?? (null as unknown as APIAccount),
|
|
||||||
created_at: new Date(this.data.published as DateTime).toISOString(),
|
created_at: new Date(this.data.published as DateTime).toISOString(),
|
||||||
id: this.id,
|
id: this.id,
|
||||||
in_reply_to_id: null,
|
in_reply_to_id: null,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import { Application } from "./Application";
|
||||||
import { Emoji } from "./Emoji";
|
import { Emoji } from "./Emoji";
|
||||||
import { RawActivity } from "./RawActivity";
|
import { RawActivity } from "./RawActivity";
|
||||||
import { RawObject } from "./RawObject";
|
import { RawObject } from "./RawObject";
|
||||||
import { RawActor } from "./RawActor";
|
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -100,10 +99,10 @@ export class Status extends BaseEntity {
|
||||||
/**
|
/**
|
||||||
* The raw actor that this status is a reply to, if any.
|
* The raw actor that this status is a reply to, if any.
|
||||||
*/
|
*/
|
||||||
@ManyToOne(() => RawActor, {
|
@ManyToOne(() => User, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
in_reply_to_account!: RawActor | null;
|
in_reply_to_account!: User | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this status is sensitive.
|
* Whether this status is sensitive.
|
||||||
|
|
@ -196,7 +195,7 @@ export class Status extends BaseEntity {
|
||||||
emojis: Emoji[];
|
emojis: Emoji[];
|
||||||
reply?: {
|
reply?: {
|
||||||
object: RawObject;
|
object: RawObject;
|
||||||
actor: RawActor;
|
user: User;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
const newStatus = new Status();
|
const newStatus = new Status();
|
||||||
|
|
@ -217,7 +216,7 @@ export class Status extends BaseEntity {
|
||||||
|
|
||||||
if (data.reply) {
|
if (data.reply) {
|
||||||
newStatus.in_reply_to_post = data.reply.object;
|
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 = {
|
newStatus.object.data = {
|
||||||
|
|
@ -240,8 +239,8 @@ export class Status extends BaseEntity {
|
||||||
return `${config.http.base_url}/@${match[1]}`;
|
return `${config.http.base_url}/@${match[1]}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Map this to Actors
|
// Map this to Users
|
||||||
const mentionedActors = (
|
const mentionedUsers = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
mentionedPeople.map(async person => {
|
mentionedPeople.map(async person => {
|
||||||
// Check if post is in format @username or @username@instance.com
|
// Check if post is in format @username or @username@instance.com
|
||||||
|
|
@ -252,23 +251,23 @@ export class Status extends BaseEntity {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (instanceUrl) {
|
if (instanceUrl) {
|
||||||
const actor = await RawActor.createQueryBuilder("actor")
|
const user = await User.findOne({
|
||||||
.where("actor.data->>'id' = :id", {
|
where: {
|
||||||
// Where ID contains the instance URL
|
|
||||||
id: `%${instanceUrl}%`,
|
|
||||||
})
|
|
||||||
// Where actor preferredUsername is the username
|
|
||||||
.andWhere(
|
|
||||||
"actor.data->>'preferredUsername' = :username",
|
|
||||||
{
|
|
||||||
username: person.split("@")[1],
|
username: person.split("@")[1],
|
||||||
}
|
// If contains instanceUrl
|
||||||
)
|
instance: {
|
||||||
.getOne();
|
base_url: instanceUrl,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
actor: true,
|
||||||
|
instance: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return actor?.data.id;
|
return user?.actor.data.id;
|
||||||
} else {
|
} else {
|
||||||
const actor = await User.findOne({
|
const user = await User.findOne({
|
||||||
where: {
|
where: {
|
||||||
username: person.split("@")[1],
|
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") {
|
if (data.visibility === "private") {
|
||||||
newStatus.object.data.cc = [
|
newStatus.object.data.cc = [
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,13 @@ import {
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { APIAccount } from "~types/entities/account";
|
import { APIAccount } from "~types/entities/account";
|
||||||
import { RawActor } from "./RawActor";
|
import { RawActor } from "./RawActor";
|
||||||
import { APActor } from "activitypub-types";
|
import { APActor, APOrderedCollectionPage } from "activitypub-types";
|
||||||
import { RawObject } from "./RawObject";
|
import { RawObject } from "./RawObject";
|
||||||
import { Token } from "./Token";
|
import { Token } from "./Token";
|
||||||
import { Status } from "./Status";
|
import { Status } from "./Status";
|
||||||
import { APISource } from "~types/entities/source";
|
import { APISource } from "~types/entities/source";
|
||||||
import { Relationship } from "./Relationship";
|
import { Relationship } from "./Relationship";
|
||||||
|
import { Instance } from "./Instance";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a user in the database.
|
* Represents a user in the database.
|
||||||
|
|
@ -52,16 +53,19 @@ export class User extends BaseEntity {
|
||||||
/**
|
/**
|
||||||
* The password for the user.
|
* The password for the user.
|
||||||
*/
|
*/
|
||||||
@Column("varchar")
|
@Column("varchar", {
|
||||||
password!: string;
|
nullable: true,
|
||||||
|
})
|
||||||
|
password!: string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The email address for the user.
|
* The email address for the user.
|
||||||
*/
|
*/
|
||||||
@Column("varchar", {
|
@Column("varchar", {
|
||||||
unique: true,
|
unique: true,
|
||||||
|
nullable: true,
|
||||||
})
|
})
|
||||||
email!: string;
|
email!: string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The note for the user.
|
* The note for the user.
|
||||||
|
|
@ -118,8 +122,10 @@ export class User extends BaseEntity {
|
||||||
/**
|
/**
|
||||||
* The private key for the user.
|
* The private key for the user.
|
||||||
*/
|
*/
|
||||||
@Column("varchar")
|
@Column("varchar", {
|
||||||
private_key!: string;
|
nullable: true,
|
||||||
|
})
|
||||||
|
private_key!: string | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The relationships for the user.
|
* The relationships for the user.
|
||||||
|
|
@ -127,6 +133,16 @@ export class User extends BaseEntity {
|
||||||
@OneToMany(() => Relationship, relationship => relationship.owner)
|
@OneToMany(() => Relationship, relationship => relationship.owner)
|
||||||
relationships!: Relationship[];
|
relationships!: Relationship[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User's instance, null if local user
|
||||||
|
*/
|
||||||
|
@ManyToOne(() => Instance, {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
instance!: Instance | null;
|
||||||
|
|
||||||
|
/** */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The actor for the user.
|
* The actor for the user.
|
||||||
*/
|
*/
|
||||||
|
|
@ -140,6 +156,50 @@ export class User extends BaseEntity {
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
pinned_notes!: RawObject[];
|
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.
|
* Gets a user by actor ID.
|
||||||
* @param id The actor ID to search for.
|
* @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.
|
* @param data The data for the new user.
|
||||||
* @returns The newly created user.
|
* @returns The newly created user.
|
||||||
*/
|
*/
|
||||||
static async createNew(data: {
|
static async createNewLocal(data: {
|
||||||
username: string;
|
username: string;
|
||||||
display_name?: string;
|
display_name?: string;
|
||||||
password: string;
|
password: string;
|
||||||
|
|
@ -184,6 +244,7 @@ export class User extends BaseEntity {
|
||||||
user.header = data.header ?? config.defaults.avatar;
|
user.header = data.header ?? config.defaults.avatar;
|
||||||
|
|
||||||
user.relationships = [];
|
user.relationships = [];
|
||||||
|
user.instance = null;
|
||||||
|
|
||||||
user.source = {
|
user.source = {
|
||||||
language: null,
|
language: null,
|
||||||
|
|
@ -401,7 +462,32 @@ export class User extends BaseEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
async toAPI(): Promise<APIAccount> {
|
async toAPI(isOwnAccount = false): Promise<APIAccount> {
|
||||||
return await this.actor.toAPIAccount();
|
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 { errorResponse, jsonResponse } from "@response";
|
||||||
import { MatchedRoute } from "bun";
|
import { MatchedRoute } from "bun";
|
||||||
import { RawActor } from "~database/entities/RawActor";
|
|
||||||
import { User } from "~database/entities/User";
|
import { User } from "~database/entities/User";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,9 +19,9 @@ export default async (
|
||||||
|
|
||||||
const user = await User.retrieveFromToken(token);
|
const user = await User.retrieveFromToken(token);
|
||||||
|
|
||||||
let foundUser: RawActor | null;
|
let foundUser: User | null;
|
||||||
try {
|
try {
|
||||||
foundUser = await RawActor.findOneBy({
|
foundUser = await User.findOneBy({
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -31,7 +30,5 @@ export default async (
|
||||||
|
|
||||||
if (!foundUser) return errorResponse("User not found", 404);
|
if (!foundUser) return errorResponse("User not found", 404);
|
||||||
|
|
||||||
return jsonResponse(
|
return jsonResponse(await foundUser.toAPI(user?.id === foundUser.id));
|
||||||
await foundUser.toAPIAccount(user?.id === foundUser.id)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
|
|
||||||
// TODO: Check if locale is valid
|
// TODO: Check if locale is valid
|
||||||
|
|
||||||
await User.createNew({
|
await User.createNewLocal({
|
||||||
username: body.username ?? "",
|
username: body.username ?? "",
|
||||||
password: body.password ?? "",
|
password: body.password ?? "",
|
||||||
email: body.email ?? "",
|
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") {
|
if (req.method === "GET") {
|
||||||
return jsonResponse(await foundStatus.toAPI());
|
return jsonResponse(await foundStatus.toAPI());
|
||||||
} else if (req.method === "DELETE") {
|
} 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);
|
return errorResponse("Unauthorized", 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { parseRequest } from "@request";
|
||||||
import { errorResponse, jsonResponse } from "@response";
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
import { APActor } from "activitypub-types";
|
import { APActor } from "activitypub-types";
|
||||||
import { Application } from "~database/entities/Application";
|
import { Application } from "~database/entities/Application";
|
||||||
import { RawActor } from "~database/entities/RawActor";
|
|
||||||
import { RawObject } from "~database/entities/RawObject";
|
import { RawObject } from "~database/entities/RawObject";
|
||||||
import { Status } from "~database/entities/Status";
|
import { Status } from "~database/entities/Status";
|
||||||
import { User } from "~database/entities/User";
|
import { User } from "~database/entities/User";
|
||||||
|
|
@ -108,7 +107,7 @@ export default async (req: Request): Promise<Response> => {
|
||||||
|
|
||||||
// Get reply account and status if exists
|
// Get reply account and status if exists
|
||||||
let replyObject: RawObject | null = null;
|
let replyObject: RawObject | null = null;
|
||||||
let replyActor: RawActor | null = null;
|
let replyUser: User | null = null;
|
||||||
|
|
||||||
if (in_reply_to_id) {
|
if (in_reply_to_id) {
|
||||||
replyObject = await RawObject.findOne({
|
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 ?? ""
|
(replyObject?.data.attributedTo as APActor).id ?? ""
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -138,9 +137,9 @@ export default async (req: Request): Promise<Response> => {
|
||||||
spoiler_text: spoiler_text || "",
|
spoiler_text: spoiler_text || "",
|
||||||
emojis: [],
|
emojis: [],
|
||||||
reply:
|
reply:
|
||||||
replyObject && replyActor
|
replyObject && replyUser
|
||||||
? {
|
? {
|
||||||
actor: replyActor,
|
user: replyUser,
|
||||||
object: replyObject,
|
object: replyObject,
|
||||||
}
|
}
|
||||||
: undefined,
|
: 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,
|
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);
|
return errorResponse("Invalid username or password", 401);
|
||||||
|
|
||||||
// Get application
|
// Get application
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ beforeAll(async () => {
|
||||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||||
|
|
||||||
// Initialize test user
|
// Initialize test user
|
||||||
await User.createNew({
|
await User.createNewLocal({
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "test",
|
password: "test",
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@ 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 { Application } from "~database/entities/Application";
|
||||||
|
import { Emoji } from "~database/entities/Emoji";
|
||||||
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 { User } from "~database/entities/User";
|
||||||
import { APIAccount } from "~types/entities/account";
|
import { APIAccount } from "~types/entities/account";
|
||||||
|
import { APIEmoji } from "~types/entities/emoji";
|
||||||
import { APIInstance } from "~types/entities/instance";
|
import { APIInstance } from "~types/entities/instance";
|
||||||
import { APIRelationship } from "~types/entities/relationship";
|
import { APIRelationship } from "~types/entities/relationship";
|
||||||
import { APIStatus } from "~types/entities/status";
|
import { APIStatus } from "~types/entities/status";
|
||||||
|
|
@ -23,7 +25,7 @@ beforeAll(async () => {
|
||||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||||
|
|
||||||
// Initialize test user
|
// Initialize test user
|
||||||
user = await User.createNew({
|
user = await User.createNewLocal({
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "test",
|
password: "test",
|
||||||
|
|
@ -31,7 +33,7 @@ beforeAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize second test user
|
// Initialize second test user
|
||||||
user2 = await User.createNew({
|
user2 = await User.createNewLocal({
|
||||||
email: "test2@test.com",
|
email: "test2@test.com",
|
||||||
username: "test2",
|
username: "test2",
|
||||||
password: "test2",
|
password: "test2",
|
||||||
|
|
@ -105,7 +107,7 @@ describe("POST /api/v1/statuses", () => {
|
||||||
status = (await response.json()) as APIStatus;
|
status = (await response.json()) as APIStatus;
|
||||||
expect(status.content).toBe("Hello, world!");
|
expect(status.content).toBe("Hello, world!");
|
||||||
expect(status.visibility).toBe("public");
|
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.replies_count).toBe(0);
|
||||||
expect(status.favourites_count).toBe(0);
|
expect(status.favourites_count).toBe(0);
|
||||||
expect(status.reblogged).toBe(false);
|
expect(status.reblogged).toBe(false);
|
||||||
|
|
@ -238,7 +240,7 @@ describe("GET /api/v1/accounts/:id/statuses", () => {
|
||||||
// Basic validation
|
// Basic validation
|
||||||
expect(status1.content).toBe("Hello, world!");
|
expect(status1.content).toBe("Hello, world!");
|
||||||
expect(status1.visibility).toBe("public");
|
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 () => {
|
afterAll(async () => {
|
||||||
const activities = await RawActivity.createQueryBuilder("activity")
|
const activities = await RawActivity.createQueryBuilder("activity")
|
||||||
.where("activity.data->>'actor' = :actor", {
|
.where("activity.data->>'actor' = :actor", {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ beforeAll(async () => {
|
||||||
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
if (!AppDataSource.isInitialized) await AppDataSource.initialize();
|
||||||
|
|
||||||
// Initialize test user
|
// Initialize test user
|
||||||
await User.createNew({
|
await User.createNewLocal({
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "test",
|
password: "test",
|
||||||
|
|
|
||||||
|
|
@ -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.createNew({
|
await User.createNewLocal({
|
||||||
email: "test@test.com",
|
email: "test@test.com",
|
||||||
username: "test",
|
username: "test",
|
||||||
password: "test",
|
password: "test",
|
||||||
|
|
@ -32,6 +32,7 @@ describe("POST /api/v1/apps/", () => {
|
||||||
formData.append("website", "https://example.com");
|
formData.append("website", "https://example.com");
|
||||||
formData.append("redirect_uris", "https://example.com");
|
formData.append("redirect_uris", "https://example.com");
|
||||||
formData.append("scopes", "read write");
|
formData.append("scopes", "read write");
|
||||||
|
|
||||||
const response = await fetch(`${config.http.base_url}/api/v1/apps/`, {
|
const response = await fetch(`${config.http.base_url}/api/v1/apps/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
@ -64,10 +65,10 @@ describe("POST /auth/login/", () => {
|
||||||
test("should get a code", async () => {
|
test("should get a code", async () => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
formData.append("username", "test");
|
formData.append("email", "test@test.com");
|
||||||
formData.append("password", "test");
|
formData.append("password", "test");
|
||||||
const response = await fetch(
|
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",
|
method: "POST",
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
@ -93,7 +94,7 @@ describe("POST /oauth/token/", () => {
|
||||||
formData.append("redirect_uri", "https://example.com");
|
formData.append("redirect_uri", "https://example.com");
|
||||||
formData.append("client_id", client_id);
|
formData.append("client_id", client_id);
|
||||||
formData.append("client_secret", client_secret);
|
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/`, {
|
const response = await fetch(`${config.http.base_url}/oauth/token/`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export interface ConfigType {
|
||||||
discard_avatars: string[];
|
discard_avatars: string[];
|
||||||
force_sensitive: string[];
|
force_sensitive: string[];
|
||||||
remove_media: string[];
|
remove_media: string[];
|
||||||
|
fetch_all_colletion_members: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
filters: {
|
filters: {
|
||||||
|
|
@ -163,6 +164,7 @@ export const configDefaults: ConfigType = {
|
||||||
discard_avatars: [],
|
discard_avatars: [],
|
||||||
force_sensitive: [],
|
force_sensitive: [],
|
||||||
remove_media: [],
|
remove_media: [],
|
||||||
|
fetch_all_colletion_members: false,
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
note_filters: [],
|
note_filters: [],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue