Clean up more ActivityPub code, refactoring

This commit is contained in:
Jesse Wierzbinski 2023-10-22 19:39:42 -10:00
parent d05b077df1
commit 80a3e4c92d
No known key found for this signature in database
GPG key ID: F9A1E418934E40B0
26 changed files with 317 additions and 170 deletions

View file

@ -6,13 +6,14 @@ import {
PrimaryGeneratedColumn,
} from "typeorm";
import { APImage, APObject, DateTime } from "activitypub-types";
import { getConfig } from "@config";
import { ConfigType, getConfig } from "@config";
import { appendFile } from "fs/promises";
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";
import { Status } from "./Status";
/**
* Represents a raw ActivityPub object in the database.
@ -171,4 +172,57 @@ export class RawObject extends BaseEntity {
static async exists(id: string) {
return !!(await RawObject.getById(id));
}
/**
* Creates a RawObject instance from a Status object.
* DOES NOT SAVE THE OBJECT TO THE DATABASE.
* @param status The Status object to create the RawObject from.
* @returns A Promise that resolves to the RawObject instance.
*/
static createFromStatus(status: Status, config: ConfigType) {
const object = new RawObject();
object.data = {
id: `${config.http.base_url}/users/${status.account.username}/statuses/${status.id}`,
type: "Note",
summary: status.spoiler_text,
content: status.content,
inReplyTo: status.in_reply_to_post?.object.data.id,
published: new Date().toISOString(),
tag: [],
attributedTo: `${config.http.base_url}/users/${status.account.username}`,
};
// Map status mentions to ActivityPub Actor IDs
const mentionedUsers = status.mentions.map(
user => user.actor.data.id as string
);
object.data.to = mentionedUsers;
if (status.visibility === "private") {
object.data.cc = [
`${config.http.base_url}/users/${status.account.username}/followers`,
];
} else if (status.visibility === "direct") {
// Add nothing else
} else if (status.visibility === "public") {
object.data.to = [
...object.data.to,
"https://www.w3.org/ns/activitystreams#Public",
];
object.data.cc = [
`${config.http.base_url}/users/${status.account.username}/followers`,
];
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
else if (status.visibility === "unlisted") {
object.data.to = [
...object.data.to,
"https://www.w3.org/ns/activitystreams#Public",
];
}
return object;
}
}

View file

@ -12,7 +12,7 @@ import {
UpdateDateColumn,
} from "typeorm";
import { APIStatus } from "~types/entities/status";
import { User } from "./User";
import { User, userRelations } from "./User";
import { Application } from "./Application";
import { Emoji } from "./Emoji";
import { RawActivity } from "./RawActivity";
@ -27,7 +27,6 @@ export const statusRelations = [
"object",
"in_reply_to_post",
"instance",
"in_reply_to_account",
"in_reply_to_post.account",
"application",
"emojis",
@ -36,6 +35,16 @@ export const statusRelations = [
"announces",
];
export const statusAndUserRelations = [
...statusRelations,
...[
"account.actor",
"account.relationships",
"account.pinned_notes",
"account.instance",
],
];
/**
* Represents a status (i.e. a post)
*/
@ -122,14 +131,6 @@ export class Status extends BaseEntity {
})
instance!: Instance | null;
/**
* The raw actor that this status is a reply to, if any.
*/
@ManyToOne(() => User, {
nullable: true,
})
in_reply_to_account!: User | null;
/**
* Whether this status is sensitive.
*/
@ -343,26 +344,9 @@ export class Status extends BaseEntity {
newStatus.mentions = [];
newStatus.instance = data.account.instance;
newStatus.object = new RawObject();
if (data.reply) {
newStatus.in_reply_to_post = data.reply.status;
newStatus.in_reply_to_account = data.reply.user;
}
newStatus.object.data = {
id: `${config.http.base_url}/users/${data.account.username}/statuses/${newStatus.id}`,
type: "Note",
summary: data.spoiler_text,
content: data.content,
inReplyTo: data.reply?.status
? data.reply.status.object.data.id
: undefined,
published: new Date().toISOString(),
tag: [],
attributedTo: `${config.http.base_url}/users/${data.account.username}`,
};
// Get people mentioned in the content
const mentionedPeople = [
...data.content.matchAll(/@([a-zA-Z0-9_]+)/g),
@ -370,79 +354,45 @@ export class Status extends BaseEntity {
return `${config.http.base_url}/users/${match[1]}`;
});
// Map this to Users
const mentionedUsers = (
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;
// Get list of mentioned users
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,
},
if (instanceUrl) {
const user = await User.findOne({
where: {
username: person.split("@")[1],
// If contains instanceUrl
instance: {
base_url: instanceUrl,
},
relations: {
actor: true,
instance: true,
},
});
},
relations: userRelations,
});
newStatus.mentions.push(user as User);
newStatus.mentions.push(user as User);
} else {
const user = await User.findOne({
where: {
username: person.split("@")[1],
},
relations: userRelations,
});
return user?.actor.data.id;
} else {
const user = await User.findOne({
where: {
username: person.split("@")[1],
},
relations: {
actor: true,
},
});
newStatus.mentions.push(user as User);
}
})
);
newStatus.mentions.push(user as User);
const object = RawObject.createFromStatus(newStatus, config);
return user?.actor.data.id;
}
})
)
).map(user => user as string);
newStatus.object.data.to = mentionedUsers;
if (data.visibility === "private") {
newStatus.object.data.cc = [
`${config.http.base_url}/users/${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}/users/${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
newStatus.object = object;
await newStatus.object.save();
await newStatus.save();
return newStatus;
@ -453,11 +403,57 @@ export class Status extends BaseEntity {
* @returns A promise that resolves with the API status.
*/
async toAPI(): Promise<APIStatus> {
const reblogCount = await Status.count({
where: {
reblog: {
id: this.id,
},
},
relations: ["reblog"],
});
const repliesCount = await Status.count({
where: {
in_reply_to_post: {
id: this.id,
},
},
relations: ["in_reply_to_post"],
});
return {
...(await this.object.toAPI()),
id: this.id,
in_reply_to_id: this.in_reply_to_post?.id || null,
in_reply_to_account_id: this.in_reply_to_post?.account.id || null,
account: await this.account.toAPI(),
created_at: new Date(this.created_at).toISOString(),
application: (await this.application?.toAPI()) || null,
card: null,
content: this.content,
emojis: await Promise.all(this.emojis.map(emoji => emoji.toAPI())),
favourited: false,
favourites_count: 0,
media_attachments: [],
mentions: await Promise.all(
this.mentions.map(async m => await m.toAPI())
),
language: null,
muted: false,
pinned: this.account.pinned_notes.some(note => note.id === this.id),
poll: null,
reblog: this.reblog ? await this.reblog.toAPI() : null,
reblogged: !!this.reblog,
reblogs_count: reblogCount,
replies_count: repliesCount,
sensitive: false,
spoiler_text: "",
tags: [],
uri: `${config.http.base_url}/users/${this.account.username}/statuses/${this.id}`,
visibility: "public",
url: `${config.http.base_url}/users/${this.account.username}/statuses/${this.id}`,
bookmarked: false,
quote: null,
quote_id: undefined,
};
}
}

View file

@ -19,13 +19,19 @@ import {
APCollectionPage,
APOrderedCollectionPage,
} from "activitypub-types";
import { RawObject } from "./RawObject";
import { Token } from "./Token";
import { Status, statusRelations } from "./Status";
import { APISource } from "~types/entities/source";
import { Relationship } from "./Relationship";
import { Instance } from "./Instance";
export const userRelations = [
"actor",
"relationships",
"pinned_notes",
"instance",
];
/**
* Represents a user in the database.
* Stores local and remote users
@ -156,9 +162,9 @@ export class User extends BaseEntity {
/**
* The pinned notes for the user.
*/
@ManyToMany(() => RawObject, object => object.id)
@ManyToMany(() => Status, status => status.id)
@JoinTable()
pinned_notes!: RawObject[];
pinned_notes!: Status[];
/**
* Get the user's avatar in raw URL format
@ -296,6 +302,8 @@ export class User extends BaseEntity {
fields: [],
};
user.pinned_notes = [];
await user.generateKeys();
await user.save();
await user.updateActor();
@ -315,12 +323,7 @@ export class User extends BaseEntity {
where: {
access_token,
},
relations: {
user: {
relationships: true,
actor: true,
},
},
relations: userRelations.map(r => `user.${r}`),
});
if (!token) return null;
@ -524,11 +527,48 @@ export class User extends BaseEntity {
relations: ["owner"],
});
const statusCount = await Status.count({
where: {
account: {
id: this.id,
},
},
relations: ["account"],
});
const config = getConfig();
return {
...(await this.actor.toAPIAccount(isOwnAccount)),
id: this.id,
username: this.username,
display_name: this.display_name,
note: this.note,
url: `${config.http.base_url}/users/${this.username}`,
avatar: this.getAvatarUrl(config) || config.defaults.avatar,
header: this.getHeaderUrl(config) || config.defaults.header,
locked: false,
created_at: new Date(this.created_at).toISOString(),
followers_count: follower_count,
following_count: following_count,
statuses_count: statusCount,
emojis: [],
fields: [],
bot: false,
source: isOwnAccount ? this.source : undefined,
avatar_static: "",
header_static: "",
acct:
this.instance === null
? `${this.username}`
: `${this.username}@${this.instance.base_url}`,
limited: false,
moved: null,
noindex: false,
suspended: false,
discoverable: undefined,
mute_expires_at: undefined,
group: false,
role: undefined,
};
}
}