mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
guh
This commit is contained in:
parent
d0c07d804b
commit
2cadb68a56
|
|
@ -12,4 +12,7 @@ module.exports = {
|
||||||
ignorePatterns: ["node_modules/", "dist/", ".eslintrc.cjs"],
|
ignorePatterns: ["node_modules/", "dist/", ".eslintrc.cjs"],
|
||||||
plugins: ["@typescript-eslint"],
|
plugins: ["@typescript-eslint"],
|
||||||
root: true,
|
root: true,
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,55 @@
|
||||||
|
/* eslint-disable @typescript-eslint/require-await */
|
||||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
import { APActor, APOrderedCollectionPage, IconField } from "activitypub-types";
|
import { APActor, APImage, APOrderedCollectionPage } 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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores an ActivityPub actor as raw JSON-LD data
|
* Represents a raw actor entity in the database.
|
||||||
*/
|
*/
|
||||||
@Entity({
|
@Entity({ name: "actors" })
|
||||||
name: "actors",
|
|
||||||
})
|
|
||||||
export class RawActor extends BaseEntity {
|
export class RawActor extends BaseEntity {
|
||||||
|
/**
|
||||||
|
* The unique identifier of the actor.
|
||||||
|
*/
|
||||||
@PrimaryGeneratedColumn("uuid")
|
@PrimaryGeneratedColumn("uuid")
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ActivityPub actor data associated with the actor.
|
||||||
|
*/
|
||||||
@Column("jsonb")
|
@Column("jsonb")
|
||||||
data!: APActor;
|
data!: APActor;
|
||||||
|
|
||||||
@Column("jsonb", {
|
/**
|
||||||
default: [],
|
* The list of follower IDs associated with the actor.
|
||||||
})
|
*/
|
||||||
|
@Column("jsonb", { default: [] })
|
||||||
followers!: string[];
|
followers!: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
static async getByActorId(id: string) {
|
static async getByActorId(id: string) {
|
||||||
return await RawActor.createQueryBuilder("actor")
|
return await RawActor.createQueryBuilder("actor")
|
||||||
.where("actor.data->>'id' = :id", {
|
.where("actor.data->>'id' = :id", { id })
|
||||||
id,
|
|
||||||
})
|
|
||||||
.getOne();
|
.getOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
static async addIfNotExists(data: APActor) {
|
static async addIfNotExists(data: APActor) {
|
||||||
if (!(await RawActor.exists(data.id ?? ""))) {
|
if (await RawActor.exists(data.id ?? "")) {
|
||||||
|
return errorResponse("Actor already exists", 409);
|
||||||
|
}
|
||||||
|
|
||||||
const actor = new RawActor();
|
const actor = new RawActor();
|
||||||
actor.data = data;
|
actor.data = data;
|
||||||
actor.followers = [];
|
actor.followers = [];
|
||||||
|
|
@ -40,7 +57,7 @@ export class RawActor extends BaseEntity {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config.activitypub.discard_avatars.find(instance =>
|
config.activitypub.discard_avatars.some(instance =>
|
||||||
actor.id.includes(instance)
|
actor.id.includes(instance)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
@ -48,7 +65,7 @@ export class RawActor extends BaseEntity {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
config.activitypub.discard_banners.find(instance =>
|
config.activitypub.discard_banners.some(instance =>
|
||||||
actor.id.includes(instance)
|
actor.id.includes(instance)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
@ -63,38 +80,33 @@ export class RawActor extends BaseEntity {
|
||||||
|
|
||||||
return actor;
|
return actor;
|
||||||
}
|
}
|
||||||
return errorResponse("Actor already exists", 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the domain of the instance associated with the actor.
|
||||||
|
* @returns The domain of the instance associated with the actor.
|
||||||
|
*/
|
||||||
getInstanceDomain() {
|
getInstanceDomain() {
|
||||||
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() {
|
async fetchFollowers() {
|
||||||
// Fetch follower list using ActivityPub
|
|
||||||
|
|
||||||
// Loop to fetch all followers until there are no more pages
|
|
||||||
let followers: APOrderedCollectionPage = await fetch(
|
let followers: APOrderedCollectionPage = await fetch(
|
||||||
`${this.data.followers?.toString() ?? ""}?page=1`,
|
`${this.data.followers?.toString() ?? ""}?page=1`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: { Accept: "application/activity+json" },
|
||||||
Accept: "application/activity+json",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let followersList = followers.orderedItems ?? [];
|
let followersList = followers.orderedItems ?? [];
|
||||||
|
|
||||||
while (followers.type === "OrderedCollectionPage" && followers.next) {
|
while (followers.type === "OrderedCollectionPage" && followers.next) {
|
||||||
// Fetch next page
|
followers = await fetch((followers.next as string).toString(), {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
headers: { Accept: "application/activity+json" },
|
||||||
followers = await fetch(followers.next.toString(), {
|
|
||||||
headers: {
|
|
||||||
Accept: "application/activity+json",
|
|
||||||
},
|
|
||||||
}).then(res => res.json());
|
}).then(res => res.json());
|
||||||
|
|
||||||
// Add new followers to list
|
|
||||||
followersList = {
|
followersList = {
|
||||||
...followersList,
|
...followersList,
|
||||||
...(followers.orderedItems ?? []),
|
...(followers.orderedItems ?? []),
|
||||||
|
|
@ -104,26 +116,32 @@ export class RawActor extends BaseEntity {
|
||||||
this.followers = followersList as string[];
|
this.followers = followersList as string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
async toAPIAccount(isOwnAccount = false): Promise<APIAccount> {
|
async toAPIAccount(isOwnAccount = false): Promise<APIAccount> {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
const { preferredUsername, name, summary, published, icon, image } =
|
||||||
|
this.data;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
username: this.data.preferredUsername ?? "",
|
username: preferredUsername ?? "",
|
||||||
display_name: this.data.name ?? this.data.preferredUsername ?? "",
|
display_name: name ?? preferredUsername ?? "",
|
||||||
note: this.data.summary ?? "",
|
note: summary ?? "",
|
||||||
url: `${config.http.base_url}/@${
|
url: `${
|
||||||
this.data.preferredUsername
|
config.http.base_url
|
||||||
}@${this.getInstanceDomain()}`,
|
}/@${preferredUsername}@${this.getInstanceDomain()}`,
|
||||||
// @ts-expect-error It actually works
|
avatar:
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
((icon as APImage).url as string | undefined) ??
|
||||||
avatar: (this.data.icon as IconField).url ?? config.defaults.avatar,
|
config.defaults.avatar,
|
||||||
// @ts-expect-error It actually works
|
header:
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
((image as APImage).url as string | undefined) ??
|
||||||
header: this.data.image?.url ?? config.defaults.header,
|
config.defaults.header,
|
||||||
locked: false,
|
locked: false,
|
||||||
created_at: new Date(this.data.published ?? 0).toISOString(),
|
created_at: new Date(published ?? 0).toISOString(),
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
||||||
followers_count: 0,
|
followers_count: 0,
|
||||||
following_count: 0,
|
following_count: 0,
|
||||||
statuses_count: 0,
|
statuses_count: 0,
|
||||||
|
|
@ -143,10 +161,8 @@ export class RawActor extends BaseEntity {
|
||||||
header_static: "",
|
header_static: "",
|
||||||
acct:
|
acct:
|
||||||
this.getInstanceDomain() == getHost()
|
this.getInstanceDomain() == getHost()
|
||||||
? `${this.data.preferredUsername}`
|
? `${preferredUsername}`
|
||||||
: `${
|
: `${preferredUsername}@${this.getInstanceDomain()}`,
|
||||||
this.data.preferredUsername
|
|
||||||
}@${this.getInstanceDomain()}`,
|
|
||||||
limited: false,
|
limited: false,
|
||||||
moved: null,
|
moved: null,
|
||||||
noindex: false,
|
noindex: false,
|
||||||
|
|
@ -158,24 +174,23 @@ export class RawActor extends BaseEntity {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the actor is filtered based on the instance's filter rules.
|
||||||
|
* @returns Whether the actor is filtered.
|
||||||
|
*/
|
||||||
async isObjectFiltered() {
|
async isObjectFiltered() {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
const { type, preferredUsername, name, id } = this.data;
|
||||||
|
|
||||||
const usernameFilterResult = await Promise.all(
|
const usernameFilterResult = await Promise.all(
|
||||||
config.filters.username_filters.map(async filter => {
|
config.filters.username_filters.map(async filter => {
|
||||||
if (
|
if (type === "Person" && preferredUsername?.match(filter)) {
|
||||||
this.data.type === "Person" &&
|
if (config.logging.log_filters) {
|
||||||
this.data.preferredUsername?.match(filter)
|
|
||||||
) {
|
|
||||||
// Log filter
|
|
||||||
|
|
||||||
if (config.logging.log_filters)
|
|
||||||
await appendFile(
|
await appendFile(
|
||||||
process.cwd() + "/logs/filters.log",
|
process.cwd() + "/logs/filters.log",
|
||||||
`${new Date().toISOString()} Filtered actor username: "${
|
`${new Date().toISOString()} Filtered actor username: "${preferredUsername}" (ID: ${id}) based on rule: ${filter}\n`
|
||||||
this.data.preferredUsername
|
|
||||||
}" (ID: ${this.data.id}) based on rule: ${filter}\n`
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -183,19 +198,13 @@ export class RawActor extends BaseEntity {
|
||||||
|
|
||||||
const displayNameFilterResult = await Promise.all(
|
const displayNameFilterResult = await Promise.all(
|
||||||
config.filters.displayname_filters.map(async filter => {
|
config.filters.displayname_filters.map(async filter => {
|
||||||
if (
|
if (type === "Person" && name?.match(filter)) {
|
||||||
this.data.type === "Person" &&
|
if (config.logging.log_filters) {
|
||||||
this.data.name?.match(filter)
|
|
||||||
) {
|
|
||||||
// Log filter
|
|
||||||
|
|
||||||
if (config.logging.log_filters)
|
|
||||||
await appendFile(
|
await appendFile(
|
||||||
process.cwd() + "/logs/filters.log",
|
process.cwd() + "/logs/filters.log",
|
||||||
`${new Date().toISOString()} Filtered actor username: "${
|
`${new Date().toISOString()} Filtered actor username: "${preferredUsername}" (ID: ${id}) based on rule: ${filter}\n`
|
||||||
this.data.preferredUsername
|
|
||||||
}" (ID: ${this.data.id}) based on rule: ${filter}\n`
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -207,6 +216,11 @@ export class RawActor extends BaseEntity {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
static async exists(id: string) {
|
static async exists(id: string) {
|
||||||
return !!(await RawActor.getByActorId(id));
|
return !!(await RawActor.getByActorId(id));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
import { APActor, APObject, DateTime } from "activitypub-types";
|
import { APActor, APImage, APObject, DateTime } from "activitypub-types";
|
||||||
import { getConfig } from "@config";
|
import { getConfig } from "@config";
|
||||||
import { appendFile } from "fs/promises";
|
import { appendFile } from "fs/promises";
|
||||||
import { APIStatus } from "~types/entities/status";
|
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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores an ActivityPub object as raw JSON-LD data
|
* Stores an ActivityPub object as raw JSON-LD data
|
||||||
|
|
@ -27,7 +28,38 @@ export class RawObject extends BaseEntity {
|
||||||
.getOne();
|
.getOne();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
async parseEmojis() {
|
||||||
|
const emojis = this.data.tag as {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
updated: string;
|
||||||
|
icon: {
|
||||||
|
type: "Image";
|
||||||
|
mediaType: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
|
||||||
|
return emojis.map(emoji => ({
|
||||||
|
shortcode: emoji.name,
|
||||||
|
static_url: (emoji.icon as APImage).url,
|
||||||
|
url: (emoji.icon as APImage).url,
|
||||||
|
visible_in_picker: true,
|
||||||
|
category: "custom",
|
||||||
|
})) as APIEmoji[];
|
||||||
|
}
|
||||||
|
|
||||||
async toAPI(): Promise<APIStatus> {
|
async toAPI(): Promise<APIStatus> {
|
||||||
|
const mentions = (
|
||||||
|
await Promise.all(
|
||||||
|
(this.data.to as string[]).map(
|
||||||
|
async person => await RawActor.getByActorId(person)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).filter(m => m) as RawActor[];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
account:
|
account:
|
||||||
(await (
|
(await (
|
||||||
|
|
@ -41,11 +73,13 @@ export class RawObject extends BaseEntity {
|
||||||
application: null,
|
application: null,
|
||||||
card: null,
|
card: null,
|
||||||
content: this.data.content as string,
|
content: this.data.content as string,
|
||||||
emojis: [],
|
emojis: await this.parseEmojis(),
|
||||||
favourited: false,
|
favourited: false,
|
||||||
favourites_count: 0,
|
favourites_count: 0,
|
||||||
media_attachments: [],
|
media_attachments: [],
|
||||||
mentions: [],
|
mentions: await Promise.all(
|
||||||
|
mentions.map(async m => await m.toAPIAccount())
|
||||||
|
),
|
||||||
in_reply_to_account_id: null,
|
in_reply_to_account_id: null,
|
||||||
language: null,
|
language: null,
|
||||||
muted: false,
|
muted: false,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,9 @@ import {
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
|
RemoveOptions,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from "typeorm";
|
} from "typeorm";
|
||||||
import { APIStatus } from "~types/entities/status";
|
import { APIStatus } from "~types/entities/status";
|
||||||
|
|
@ -15,6 +17,8 @@ import { User } from "./User";
|
||||||
import { Application } from "./Application";
|
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 { RawActor } from "./RawActor";
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
|
|
@ -42,6 +46,9 @@ export class Status extends BaseEntity {
|
||||||
})
|
})
|
||||||
reblog?: Status;
|
reblog?: Status;
|
||||||
|
|
||||||
|
@OneToOne(() => Status)
|
||||||
|
object!: RawObject;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
isReblog!: boolean;
|
isReblog!: boolean;
|
||||||
|
|
||||||
|
|
@ -53,6 +60,16 @@ export class Status extends BaseEntity {
|
||||||
@Column("varchar")
|
@Column("varchar")
|
||||||
visibility!: APIStatus["visibility"];
|
visibility!: APIStatus["visibility"];
|
||||||
|
|
||||||
|
@ManyToOne(() => RawObject, {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
in_reply_to_post!: RawObject;
|
||||||
|
|
||||||
|
@ManyToOne(() => RawActor, {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
in_reply_to_account!: RawActor;
|
||||||
|
|
||||||
@Column("boolean")
|
@Column("boolean")
|
||||||
sensitive!: boolean;
|
sensitive!: boolean;
|
||||||
|
|
||||||
|
|
@ -78,6 +95,15 @@ export class Status extends BaseEntity {
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
announces!: RawActivity[];
|
announces!: RawActivity[];
|
||||||
|
|
||||||
|
async remove(options?: RemoveOptions | undefined) {
|
||||||
|
// Delete object
|
||||||
|
await this.object.remove(options);
|
||||||
|
|
||||||
|
await super.remove(options);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
static async createNew(data: {
|
static async createNew(data: {
|
||||||
account: User;
|
account: User;
|
||||||
application: Application | null;
|
application: Application | null;
|
||||||
|
|
@ -86,6 +112,10 @@ export class Status extends BaseEntity {
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
spoiler_text: string;
|
spoiler_text: string;
|
||||||
emojis: Emoji[];
|
emojis: Emoji[];
|
||||||
|
reply?: {
|
||||||
|
object: RawObject;
|
||||||
|
actor: RawActor;
|
||||||
|
};
|
||||||
}) {
|
}) {
|
||||||
const newStatus = new Status();
|
const newStatus = new Status();
|
||||||
|
|
||||||
|
|
@ -101,6 +131,99 @@ export class Status extends BaseEntity {
|
||||||
newStatus.isReblog = false;
|
newStatus.isReblog = false;
|
||||||
newStatus.announces = [];
|
newStatus.announces = [];
|
||||||
|
|
||||||
|
newStatus.object = new RawObject();
|
||||||
|
|
||||||
|
if (data.reply) {
|
||||||
|
newStatus.in_reply_to_post = data.reply.object;
|
||||||
|
newStatus.in_reply_to_account = data.reply.actor;
|
||||||
|
}
|
||||||
|
|
||||||
|
newStatus.object.data = {
|
||||||
|
id: `${config.http.base_url}/@${data.account.username}/statuses/${newStatus.id}`,
|
||||||
|
type: "Note",
|
||||||
|
summary: data.spoiler_text,
|
||||||
|
content: data.content, // TODO: Format as HTML
|
||||||
|
inReplyTo: data.reply?.object
|
||||||
|
? data.reply.object.data.id
|
||||||
|
: undefined,
|
||||||
|
attributedTo: `${config.http.base_url}/@${data.account.username}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get people mentioned in the content
|
||||||
|
const mentionedPeople = [
|
||||||
|
...data.content.matchAll(/@([a-zA-Z0-9_]+)/g),
|
||||||
|
].map(match => {
|
||||||
|
return `${config.http.base_url}/@${match[1]}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map this to Actors
|
||||||
|
const mentionedActors = (
|
||||||
|
await Promise.all(
|
||||||
|
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 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",
|
||||||
|
{
|
||||||
|
username: person.split("@")[1],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
return actor?.data.id;
|
||||||
|
} else {
|
||||||
|
const actor = await User.findOne({
|
||||||
|
where: {
|
||||||
|
username: person.split("@")[1],
|
||||||
|
},
|
||||||
|
relations: {
|
||||||
|
actor: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return actor?.actor.data.id;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).map(actor => actor as string);
|
||||||
|
|
||||||
|
newStatus.object.data.to = mentionedActors;
|
||||||
|
|
||||||
|
if (data.visibility === "private") {
|
||||||
|
newStatus.object.data.cc = [
|
||||||
|
`${config.http.base_url}/@${data.account.username}/followers`,
|
||||||
|
];
|
||||||
|
} else if (data.visibility === "direct") {
|
||||||
|
// Add nothing else
|
||||||
|
} else if (data.visibility === "public") {
|
||||||
|
newStatus.object.data.to = [
|
||||||
|
...newStatus.object.data.to,
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public",
|
||||||
|
];
|
||||||
|
newStatus.object.data.cc = [
|
||||||
|
`${config.http.base_url}/@${data.account.username}/followers`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||||
|
else if (data.visibility === "unlisted") {
|
||||||
|
newStatus.object.data.to = [
|
||||||
|
...newStatus.object.data.to,
|
||||||
|
"https://www.w3.org/ns/activitystreams#Public",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Add default language
|
// TODO: Add default language
|
||||||
|
|
||||||
await newStatus.save();
|
await newStatus.save();
|
||||||
|
|
|
||||||
57
server/api/api/v1/statuses/[id]/index.ts
Normal file
57
server/api/api/v1/statuses/[id]/index.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { getUserByToken } from "@auth";
|
||||||
|
import { errorResponse, jsonResponse } from "@response";
|
||||||
|
import { MatchedRoute } from "bun";
|
||||||
|
import { RawObject } from "~database/entities/RawObject";
|
||||||
|
import { Status } from "~database/entities/Status";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a user
|
||||||
|
*/
|
||||||
|
export default async (
|
||||||
|
req: Request,
|
||||||
|
matchedRoute: MatchedRoute
|
||||||
|
): Promise<Response> => {
|
||||||
|
const id = matchedRoute.params.id;
|
||||||
|
|
||||||
|
// Check auth token
|
||||||
|
const token = req.headers.get("Authorization")?.split(" ")[1] || null;
|
||||||
|
const user = await getUserByToken(token);
|
||||||
|
|
||||||
|
// TODO: Add checks for user's permissions to view this status
|
||||||
|
|
||||||
|
let foundStatus: RawObject | null;
|
||||||
|
try {
|
||||||
|
foundStatus = await RawObject.findOneBy({
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return errorResponse("Invalid ID", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundStatus) return errorResponse("Record not found", 404);
|
||||||
|
|
||||||
|
if (req.method === "GET") {
|
||||||
|
return jsonResponse(await foundStatus.toAPI());
|
||||||
|
} else if (req.method === "DELETE") {
|
||||||
|
if ((await foundStatus.toAPI()).account.id !== user?.id) {
|
||||||
|
return errorResponse("Unauthorized", 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get associated Status object
|
||||||
|
const status = await Status.createQueryBuilder("status")
|
||||||
|
.leftJoinAndSelect("status.object", "object")
|
||||||
|
.where("object.id = :id", { id: foundStatus.id })
|
||||||
|
.getOne();
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return errorResponse("Status not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete status and all associated objects
|
||||||
|
await status.object.remove();
|
||||||
|
|
||||||
|
return jsonResponse({});
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse({});
|
||||||
|
};
|
||||||
|
|
@ -53,8 +53,12 @@ export default async (req: Request): Promise<Response> => {
|
||||||
visibility?: "public" | "unlisted" | "private" | "direct";
|
visibility?: "public" | "unlisted" | "private" | "direct";
|
||||||
language?: string;
|
language?: string;
|
||||||
scheduled_at?: string;
|
scheduled_at?: string;
|
||||||
|
local_only?: boolean;
|
||||||
|
content_type?: string;
|
||||||
}>(req);
|
}>(req);
|
||||||
|
|
||||||
|
// TODO: Parse Markdown statuses
|
||||||
|
|
||||||
// Validate status
|
// Validate status
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return errorResponse("Status is required", 422);
|
return errorResponse("Status is required", 422);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue