server/database/entities/Status.ts

464 lines
10 KiB
TypeScript
Raw Normal View History

2023-09-12 22:48:10 +02:00
import { getConfig } from "@config";
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
2023-09-22 03:09:14 +02:00
JoinTable,
2023-09-12 22:48:10 +02:00
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
2023-09-27 00:19:10 +02:00
RemoveOptions,
2023-09-12 22:48:10 +02:00
UpdateDateColumn,
} from "typeorm";
import { APIStatus } from "~types/entities/status";
import { User } from "./User";
import { Application } from "./Application";
import { Emoji } from "./Emoji";
2023-09-13 07:30:45 +02:00
import { RawActivity } from "./RawActivity";
2023-09-27 00:19:10 +02:00
import { RawObject } from "./RawObject";
import { Instance } from "./Instance";
2023-09-12 22:48:10 +02:00
const config = getConfig();
2023-10-23 03:47:04 +02:00
export const statusRelations = [
"account",
"reblog",
"object",
"in_reply_to_post",
"instance",
"in_reply_to_account",
"in_reply_to_post.account",
"application",
"emojis",
"mentions",
"likes",
"announces",
];
2023-09-12 22:48:10 +02:00
/**
2023-09-28 20:19:21 +02:00
* Represents a status (i.e. a post)
2023-09-12 22:48:10 +02:00
*/
@Entity({
name: "statuses",
})
export class Status extends BaseEntity {
2023-09-28 20:19:21 +02:00
/**
* The unique identifier for this status.
*/
2023-09-12 22:48:10 +02:00
@PrimaryGeneratedColumn("uuid")
id!: string;
2023-09-28 20:19:21 +02:00
/**
* The user account that created this status.
*/
2023-09-13 02:29:13 +02:00
@ManyToOne(() => User, user => user.id)
2023-09-12 22:48:10 +02:00
account!: User;
2023-09-28 20:19:21 +02:00
/**
* The date and time when this status was created.
*/
2023-09-12 22:48:10 +02:00
@CreateDateColumn()
created_at!: Date;
2023-09-28 20:19:21 +02:00
/**
* The date and time when this status was last updated.
*/
2023-09-12 22:48:10 +02:00
@UpdateDateColumn()
updated_at!: Date;
2023-09-28 20:19:21 +02:00
/**
* The status that this status is a reblog of, if any.
*/
2023-09-13 02:29:13 +02:00
@ManyToOne(() => Status, status => status.id, {
2023-09-12 22:48:10 +02:00
nullable: true,
2023-10-23 03:47:04 +02:00
onDelete: "SET NULL",
2023-09-12 22:48:10 +02:00
})
2023-10-23 03:47:04 +02:00
reblog?: Status | null;
2023-09-12 22:48:10 +02:00
2023-09-28 20:19:21 +02:00
/**
* The raw object associated with this status.
*/
2023-09-27 01:08:05 +02:00
@ManyToOne(() => RawObject, {
nullable: true,
onDelete: "SET NULL",
})
2023-09-27 00:19:10 +02:00
object!: RawObject;
2023-09-28 20:19:21 +02:00
/**
* Whether this status is a reblog.
*/
2023-09-12 22:48:10 +02:00
@Column("boolean")
isReblog!: boolean;
2023-09-28 20:19:21 +02:00
/**
* The content of this status.
*/
2023-09-12 22:48:10 +02:00
@Column("varchar", {
default: "",
})
content!: string;
2023-09-28 20:19:21 +02:00
/**
* The visibility of this status.
*/
2023-09-12 22:48:10 +02:00
@Column("varchar")
visibility!: APIStatus["visibility"];
2023-09-28 20:19:21 +02:00
/**
* The raw object that this status is a reply to, if any.
*/
@ManyToOne(() => Status, {
nullable: true,
2023-10-23 03:47:04 +02:00
onDelete: "SET NULL",
})
in_reply_to_post!: Status | null;
/**
* The status' instance
*/
@ManyToOne(() => Instance, {
2023-09-27 00:19:10 +02:00
nullable: true,
})
instance!: Instance | null;
2023-09-27 00:19:10 +02:00
2023-09-28 20:19:21 +02:00
/**
* The raw actor that this status is a reply to, if any.
*/
@ManyToOne(() => User, {
2023-09-27 00:19:10 +02:00
nullable: true,
})
in_reply_to_account!: User | null;
2023-09-27 00:19:10 +02:00
2023-09-28 20:19:21 +02:00
/**
* Whether this status is sensitive.
*/
2023-09-12 22:48:10 +02:00
@Column("boolean")
sensitive!: boolean;
2023-09-28 20:19:21 +02:00
/**
* The spoiler text for this status.
*/
2023-09-12 22:48:10 +02:00
@Column("varchar", {
default: "",
})
spoiler_text!: string;
2023-09-28 20:19:21 +02:00
/**
* The application associated with this status, if any.
*/
2023-09-13 02:29:13 +02:00
@ManyToOne(() => Application, app => app.id, {
nullable: true,
2023-09-12 22:48:10 +02:00
})
application!: Application | null;
2023-09-28 20:19:21 +02:00
/**
* The emojis associated with this status.
*/
2023-09-13 02:29:13 +02:00
@ManyToMany(() => Emoji, emoji => emoji.id)
2023-09-22 03:09:14 +02:00
@JoinTable()
2023-09-12 22:48:10 +02:00
emojis!: Emoji[];
/**
* The users mentioned (excluding followers and such)
*/
@ManyToMany(() => User, user => user.id)
@JoinTable()
mentions!: User[];
2023-09-28 20:19:21 +02:00
/**
* The activities that have liked this status.
*/
2023-09-22 03:09:14 +02:00
@ManyToMany(() => RawActivity, activity => activity.id)
@JoinTable()
2023-09-14 04:25:45 +02:00
likes!: RawActivity[];
2023-09-13 07:30:45 +02:00
2023-09-28 20:19:21 +02:00
/**
* The activities that have announced this status.
*/
2023-09-22 03:09:14 +02:00
@ManyToMany(() => RawActivity, activity => activity.id)
@JoinTable()
2023-09-14 04:25:45 +02:00
announces!: RawActivity[];
2023-09-13 02:29:13 +02:00
2023-09-28 20:19:21 +02:00
/**
* Removes this status from the database.
* @param options The options for removing this status.
* @returns A promise that resolves when the status has been removed.
*/
2023-09-27 00:19:10 +02:00
async remove(options?: RemoveOptions | undefined) {
// Delete object
2023-09-27 20:45:07 +02:00
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (this.object) await this.object.remove(options);
2023-09-27 00:19:10 +02:00
2023-09-27 01:08:05 +02:00
return await super.remove(options);
2023-09-27 00:19:10 +02:00
}
async parseEmojis(string: string) {
const emojis = [...string.matchAll(/:([a-zA-Z0-9_]+):/g)].map(
match => match[1]
);
const emojiObjects = await Promise.all(
emojis.map(async emoji => {
const emojiObject = await Emoji.findOne({
where: {
shortcode: emoji,
},
});
return emojiObject;
})
);
return emojiObjects.filter(emoji => emoji !== null) as Emoji[];
}
/**
* Returns whether this status is viewable by a user.
* @param user The user to check.
* @returns Whether this status is viewable by the user.
*/
isViewableByUser(user: User | null) {
const relationship = user?.relationships.find(
rel => rel.id === this.account.id
);
if (this.visibility === "public") return true;
else if (this.visibility === "unlisted") return true;
else if (this.visibility === "private") {
return !!relationship?.following;
} else {
return user && this.mentions.includes(user);
}
}
/**
* Return all the ancestors of this post,
*/
async getAncestors(fetcher: User | null) {
const max = fetcher ? 4096 : 40;
const ancestors = [];
let id = this.in_reply_to_post?.id;
while (ancestors.length < max && id) {
const currentStatus = await Status.findOne({
where: {
id: id,
},
2023-10-23 03:47:04 +02:00
relations: statusRelations,
});
if (currentStatus) {
if (currentStatus.isViewableByUser(fetcher)) {
ancestors.push(currentStatus);
}
id = currentStatus.in_reply_to_post?.id;
} else {
break;
}
}
return ancestors;
}
/**
* Return all the descendants of this post,
*/
async getDescendants(fetcher: User | null) {
const max = fetcher ? 4096 : 60;
// Go through all descendants in a tree-like manner
const descendants: Status[] = [];
return await Status._getDescendants(this, fetcher, max, descendants);
}
/**
* Return all the descendants of a post,
* @param status The status to get the descendants of.
* @param isAuthenticated Whether the user is authenticated.
* @param max The maximum number of descendants to get.
* @param descendants The descendants to add to.
* @returns A promise that resolves with the descendants.
* @private
*/
private static async _getDescendants(
status: Status,
fetcher: User | null,
max: number,
descendants: Status[]
) {
const currentStatus = await Status.find({
where: {
in_reply_to_post: {
id: status.id,
},
},
2023-10-23 03:47:04 +02:00
relations: statusRelations,
});
for (const status of currentStatus) {
if (status.isViewableByUser(fetcher)) {
descendants.push(status);
}
if (descendants.length < max) {
await this._getDescendants(status, fetcher, max, descendants);
}
}
return descendants;
}
2023-09-28 20:19:21 +02:00
/**
* Creates a new status and saves it to the database.
* @param data The data for the new status.
* @returns A promise that resolves with the new status.
*/
2023-09-22 03:09:14 +02:00
static async createNew(data: {
account: User;
application: Application | null;
content: string;
visibility: APIStatus["visibility"];
sensitive: boolean;
spoiler_text: string;
emojis: Emoji[];
2023-09-27 00:19:10 +02:00
reply?: {
status: Status;
user: User;
2023-09-27 00:19:10 +02:00
};
2023-09-22 03:09:14 +02:00
}) {
const newStatus = new Status();
newStatus.account = data.account;
newStatus.application = data.application ?? null;
newStatus.content = data.content;
newStatus.visibility = data.visibility;
newStatus.sensitive = data.sensitive;
newStatus.spoiler_text = data.spoiler_text;
newStatus.emojis = data.emojis;
newStatus.likes = [];
newStatus.announces = [];
newStatus.isReblog = false;
newStatus.announces = [];
newStatus.mentions = [];
newStatus.instance = data.account.instance;
2023-09-22 03:09:14 +02:00
2023-09-27 00:19:10 +02:00
newStatus.object = new RawObject();
if (data.reply) {
newStatus.in_reply_to_post = data.reply.status;
newStatus.in_reply_to_account = data.reply.user;
2023-09-27 00:19:10 +02:00
}
newStatus.object.data = {
2023-10-16 08:04:03 +02:00
id: `${config.http.base_url}/users/${data.account.username}/statuses/${newStatus.id}`,
2023-09-27 00:19:10 +02:00
type: "Note",
summary: data.spoiler_text,
2023-10-23 02:23:15 +02:00
content: data.content,
inReplyTo: data.reply?.status
? data.reply.status.object.data.id
2023-09-27 00:19:10 +02:00
: undefined,
2023-09-27 20:45:07 +02:00
published: new Date().toISOString(),
tag: [],
2023-10-16 08:04:03 +02:00
attributedTo: `${config.http.base_url}/users/${data.account.username}`,
2023-09-27 00:19:10 +02:00
};
// Get people mentioned in the content
const mentionedPeople = [
...data.content.matchAll(/@([a-zA-Z0-9_]+)/g),
].map(match => {
2023-10-16 08:04:03 +02:00
return `${config.http.base_url}/users/${match[1]}`;
2023-09-27 00:19:10 +02:00
});
// Map this to Users
const mentionedUsers = (
2023-09-27 00:19:10 +02:00
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 user = await User.findOne({
where: {
username: person.split("@")[1],
// If contains instanceUrl
instance: {
base_url: instanceUrl,
},
},
relations: {
actor: true,
instance: true,
},
});
newStatus.mentions.push(user as User);
return user?.actor.data.id;
2023-09-27 00:19:10 +02:00
} else {
const user = await User.findOne({
2023-09-27 00:19:10 +02:00
where: {
username: person.split("@")[1],
},
relations: {
actor: true,
},
});
newStatus.mentions.push(user as User);
return user?.actor.data.id;
2023-09-27 00:19:10 +02:00
}
})
)
).map(user => user as string);
2023-09-27 00:19:10 +02:00
newStatus.object.data.to = mentionedUsers;
2023-09-27 00:19:10 +02:00
if (data.visibility === "private") {
newStatus.object.data.cc = [
2023-10-16 08:04:03 +02:00
`${config.http.base_url}/users/${data.account.username}/followers`,
2023-09-27 00:19:10 +02:00
];
} 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 = [
2023-10-16 08:04:03 +02:00
`${config.http.base_url}/users/${data.account.username}/followers`,
2023-09-27 00:19:10 +02:00
];
}
// 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",
];
}
2023-09-22 23:41:05 +02:00
// TODO: Add default language
2023-09-27 01:08:05 +02:00
await newStatus.object.save();
2023-09-22 03:09:14 +02:00
await newStatus.save();
return newStatus;
}
2023-09-28 20:19:21 +02:00
/**
* Converts this status to an API status.
* @returns A promise that resolves with the API status.
*/
2023-09-12 22:48:10 +02:00
async toAPI(): Promise<APIStatus> {
return {
...(await this.object.toAPI()),
id: this.id,
2023-10-23 03:47:04 +02:00
in_reply_to_id: this.in_reply_to_post?.id || null,
in_reply_to_account_id: this.in_reply_to_post?.account.id || null,
};
2023-09-12 22:48:10 +02:00
}
}