2023-09-27 00:19:10 +02:00
|
|
|
/* eslint-disable @typescript-eslint/require-await */
|
2023-09-18 07:38:08 +02:00
|
|
|
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
2023-09-27 00:19:10 +02:00
|
|
|
import { APActor, APImage, APOrderedCollectionPage } from "activitypub-types";
|
2023-09-20 02:16:50 +02:00
|
|
|
import { getConfig, getHost } from "@config";
|
2023-09-18 07:38:08 +02:00
|
|
|
import { appendFile } from "fs/promises";
|
2023-09-18 22:29:56 +02:00
|
|
|
import { errorResponse } from "@response";
|
2023-09-20 02:16:50 +02:00
|
|
|
import { APIAccount } from "~types/entities/account";
|
2023-09-18 07:38:08 +02:00
|
|
|
|
|
|
|
|
/**
|
2023-09-27 00:19:10 +02:00
|
|
|
* Represents a raw actor entity in the database.
|
2023-09-18 07:38:08 +02:00
|
|
|
*/
|
2023-09-27 00:19:10 +02:00
|
|
|
@Entity({ name: "actors" })
|
2023-09-18 07:38:08 +02:00
|
|
|
export class RawActor extends BaseEntity {
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* The unique identifier of the actor.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
@PrimaryGeneratedColumn("uuid")
|
|
|
|
|
id!: string;
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* The ActivityPub actor data associated with the actor.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
@Column("jsonb")
|
|
|
|
|
data!: APActor;
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* The list of follower IDs associated with the actor.
|
|
|
|
|
*/
|
|
|
|
|
@Column("jsonb", { default: [] })
|
2023-09-20 02:16:50 +02:00
|
|
|
followers!: string[];
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* Retrieves a RawActor entity by actor ID.
|
|
|
|
|
* @param id The ID of the actor to retrieve.
|
|
|
|
|
* @returns The RawActor entity with the specified ID, or undefined if not found.
|
|
|
|
|
*/
|
2023-09-20 02:16:50 +02:00
|
|
|
static async getByActorId(id: string) {
|
2023-09-18 07:38:08 +02:00
|
|
|
return await RawActor.createQueryBuilder("actor")
|
2023-09-27 00:19:10 +02:00
|
|
|
.where("actor.data->>'id' = :id", { id })
|
2023-09-18 07:38:08 +02:00
|
|
|
.getOne();
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* Adds a new RawActor entity to the database if an actor with the same ID does not already exist.
|
|
|
|
|
* @param data The ActivityPub actor data to add.
|
|
|
|
|
* @returns The newly created RawActor entity, or an error response if the actor already exists or is filtered.
|
|
|
|
|
*/
|
2023-09-18 22:29:56 +02:00
|
|
|
static async addIfNotExists(data: APActor) {
|
2023-09-27 00:19:10 +02:00
|
|
|
if (await RawActor.exists(data.id ?? "")) {
|
|
|
|
|
return errorResponse("Actor already exists", 409);
|
|
|
|
|
}
|
2023-09-18 22:29:56 +02:00
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
const actor = new RawActor();
|
|
|
|
|
actor.data = data;
|
|
|
|
|
actor.followers = [];
|
2023-09-18 22:29:56 +02:00
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
const config = getConfig();
|
2023-09-18 22:29:56 +02:00
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
if (
|
|
|
|
|
config.activitypub.discard_avatars.some(instance =>
|
|
|
|
|
actor.id.includes(instance)
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
actor.data.icon = undefined;
|
|
|
|
|
}
|
2023-09-18 22:29:56 +02:00
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
if (
|
|
|
|
|
config.activitypub.discard_banners.some(instance =>
|
|
|
|
|
actor.id.includes(instance)
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
actor.data.image = undefined;
|
2023-09-18 22:29:56 +02:00
|
|
|
}
|
2023-09-27 00:19:10 +02:00
|
|
|
|
|
|
|
|
if (await actor.isObjectFiltered()) {
|
|
|
|
|
return errorResponse("Actor filtered", 409);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await actor.save();
|
|
|
|
|
|
|
|
|
|
return actor;
|
2023-09-18 22:29:56 +02:00
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* Retrieves the domain of the instance associated with the actor.
|
|
|
|
|
* @returns The domain of the instance associated with the actor.
|
|
|
|
|
*/
|
2023-09-20 02:16:50 +02:00
|
|
|
getInstanceDomain() {
|
|
|
|
|
return new URL(this.data.id ?? "").host;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* Fetches the list of followers associated with the actor and updates the `followers` property.
|
|
|
|
|
*/
|
2023-09-20 02:16:50 +02:00
|
|
|
async fetchFollowers() {
|
|
|
|
|
let followers: APOrderedCollectionPage = await fetch(
|
|
|
|
|
`${this.data.followers?.toString() ?? ""}?page=1`,
|
|
|
|
|
{
|
2023-09-27 00:19:10 +02:00
|
|
|
headers: { Accept: "application/activity+json" },
|
2023-09-20 02:16:50 +02:00
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let followersList = followers.orderedItems ?? [];
|
|
|
|
|
|
|
|
|
|
while (followers.type === "OrderedCollectionPage" && followers.next) {
|
2023-09-27 00:19:10 +02:00
|
|
|
followers = await fetch((followers.next as string).toString(), {
|
|
|
|
|
headers: { Accept: "application/activity+json" },
|
2023-09-20 02:16:50 +02:00
|
|
|
}).then(res => res.json());
|
|
|
|
|
|
|
|
|
|
followersList = {
|
|
|
|
|
...followersList,
|
|
|
|
|
...(followers.orderedItems ?? []),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.followers = followersList as string[];
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* Converts the RawActor entity to an API account object.
|
|
|
|
|
* @param isOwnAccount Whether the account is the user's own account.
|
|
|
|
|
* @returns The API account object representing the RawActor entity.
|
|
|
|
|
*/
|
2023-09-20 02:16:50 +02:00
|
|
|
async toAPIAccount(isOwnAccount = false): Promise<APIAccount> {
|
|
|
|
|
const config = getConfig();
|
2023-09-27 00:19:10 +02:00
|
|
|
const { preferredUsername, name, summary, published, icon, image } =
|
|
|
|
|
this.data;
|
|
|
|
|
|
2023-09-20 02:16:50 +02:00
|
|
|
return {
|
|
|
|
|
id: this.id,
|
2023-09-27 00:19:10 +02:00
|
|
|
username: preferredUsername ?? "",
|
|
|
|
|
display_name: name ?? preferredUsername ?? "",
|
|
|
|
|
note: summary ?? "",
|
|
|
|
|
url: `${
|
|
|
|
|
config.http.base_url
|
|
|
|
|
}/@${preferredUsername}@${this.getInstanceDomain()}`,
|
|
|
|
|
avatar:
|
|
|
|
|
((icon as APImage).url as string | undefined) ??
|
|
|
|
|
config.defaults.avatar,
|
|
|
|
|
header:
|
|
|
|
|
((image as APImage).url as string | undefined) ??
|
|
|
|
|
config.defaults.header,
|
2023-09-20 02:16:50 +02:00
|
|
|
locked: false,
|
2023-09-27 00:19:10 +02:00
|
|
|
created_at: new Date(published ?? 0).toISOString(),
|
2023-09-20 02:16:50 +02:00
|
|
|
followers_count: 0,
|
|
|
|
|
following_count: 0,
|
|
|
|
|
statuses_count: 0,
|
|
|
|
|
emojis: [],
|
|
|
|
|
fields: [],
|
|
|
|
|
bot: false,
|
|
|
|
|
source: isOwnAccount
|
|
|
|
|
? {
|
|
|
|
|
privacy: "public",
|
|
|
|
|
sensitive: false,
|
|
|
|
|
language: "en",
|
|
|
|
|
note: "",
|
|
|
|
|
fields: [],
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
|
|
|
|
avatar_static: "",
|
|
|
|
|
header_static: "",
|
|
|
|
|
acct:
|
|
|
|
|
this.getInstanceDomain() == getHost()
|
2023-09-27 00:19:10 +02:00
|
|
|
? `${preferredUsername}`
|
|
|
|
|
: `${preferredUsername}@${this.getInstanceDomain()}`,
|
2023-09-20 02:16:50 +02:00
|
|
|
limited: false,
|
|
|
|
|
moved: null,
|
|
|
|
|
noindex: false,
|
|
|
|
|
suspended: false,
|
|
|
|
|
discoverable: undefined,
|
|
|
|
|
mute_expires_at: undefined,
|
|
|
|
|
group: false,
|
|
|
|
|
role: undefined,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* Determines whether the actor is filtered based on the instance's filter rules.
|
|
|
|
|
* @returns Whether the actor is filtered.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
async isObjectFiltered() {
|
|
|
|
|
const config = getConfig();
|
2023-09-27 00:19:10 +02:00
|
|
|
const { type, preferredUsername, name, id } = this.data;
|
2023-09-18 07:38:08 +02:00
|
|
|
|
|
|
|
|
const usernameFilterResult = await Promise.all(
|
|
|
|
|
config.filters.username_filters.map(async filter => {
|
2023-09-27 00:19:10 +02:00
|
|
|
if (type === "Person" && preferredUsername?.match(filter)) {
|
|
|
|
|
if (config.logging.log_filters) {
|
2023-09-18 07:38:08 +02:00
|
|
|
await appendFile(
|
|
|
|
|
process.cwd() + "/logs/filters.log",
|
2023-09-27 00:19:10 +02:00
|
|
|
`${new Date().toISOString()} Filtered actor username: "${preferredUsername}" (ID: ${id}) based on rule: ${filter}\n`
|
2023-09-18 07:38:08 +02:00
|
|
|
);
|
2023-09-27 00:19:10 +02:00
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const displayNameFilterResult = await Promise.all(
|
|
|
|
|
config.filters.displayname_filters.map(async filter => {
|
2023-09-27 00:19:10 +02:00
|
|
|
if (type === "Person" && name?.match(filter)) {
|
|
|
|
|
if (config.logging.log_filters) {
|
2023-09-18 07:38:08 +02:00
|
|
|
await appendFile(
|
|
|
|
|
process.cwd() + "/logs/filters.log",
|
2023-09-27 00:19:10 +02:00
|
|
|
`${new Date().toISOString()} Filtered actor username: "${preferredUsername}" (ID: ${id}) based on rule: ${filter}\n`
|
2023-09-18 07:38:08 +02:00
|
|
|
);
|
2023-09-27 00:19:10 +02:00
|
|
|
}
|
2023-09-18 07:38:08 +02:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
usernameFilterResult.includes(true) ||
|
|
|
|
|
displayNameFilterResult.includes(true)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-27 00:19:10 +02:00
|
|
|
/**
|
|
|
|
|
* Determines whether an actor with the specified ID exists in the database.
|
|
|
|
|
* @param id The ID of the actor to check for.
|
|
|
|
|
* @returns Whether an actor with the specified ID exists in the database.
|
|
|
|
|
*/
|
2023-09-18 07:38:08 +02:00
|
|
|
static async exists(id: string) {
|
2023-09-20 02:16:50 +02:00
|
|
|
return !!(await RawActor.getByActorId(id));
|
2023-09-18 07:38:08 +02:00
|
|
|
}
|
|
|
|
|
}
|