diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 90bc2cfcc1..7f21436eb9 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -461,6 +461,8 @@ export declare function getFullApAccount(username: string, host?: string | undef export declare function getImageSizeFromUrl(url: string): Promise<ImageSize> +export declare function getInternalActor(actor: InternalActor): Promise<User> + export declare function getNoteSummary(fileIds: Array<string>, text: string | undefined | null, cw: string | undefined | null, hasPoll: boolean): string export declare function getTimestamp(id: string): number @@ -542,6 +544,9 @@ export interface Instance { faviconUrl: string | null } +export type InternalActor = 'instance'| +'relay'; + /** * Checks if a server is allowlisted. * Returns `Ok(true)` if private mode is disabled. diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index e45060a513..139739ac00 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -386,6 +386,7 @@ module.exports.genId = nativeBinding.genId module.exports.genIdAt = nativeBinding.genIdAt module.exports.getFullApAccount = nativeBinding.getFullApAccount module.exports.getImageSizeFromUrl = nativeBinding.getImageSizeFromUrl +module.exports.getInternalActor = nativeBinding.getInternalActor module.exports.getNoteSummary = nativeBinding.getNoteSummary module.exports.getTimestamp = nativeBinding.getTimestamp module.exports.greet = nativeBinding.greet @@ -393,6 +394,7 @@ module.exports.hashPassword = nativeBinding.hashPassword module.exports.HOUR = nativeBinding.HOUR module.exports.Inbound = nativeBinding.Inbound module.exports.initializeRustLogger = nativeBinding.initializeRustLogger +module.exports.InternalActor = nativeBinding.InternalActor module.exports.isAllowedServer = nativeBinding.isAllowedServer module.exports.isBlockedServer = nativeBinding.isBlockedServer module.exports.isOldPasswordAlgorithm = nativeBinding.isOldPasswordAlgorithm diff --git a/packages/backend-rs/src/federation/acct.rs b/packages/backend-rs/src/federation/acct.rs index 816e75baf2..60c9e6f25c 100644 --- a/packages/backend-rs/src/federation/acct.rs +++ b/packages/backend-rs/src/federation/acct.rs @@ -8,7 +8,7 @@ pub struct Acct { } #[derive(thiserror::Error, Debug)] -#[doc = "Error type to indicate a string-to-[`Acct`] conversion failure"] +#[doc = "Error type to indicate a [`String`]-to-[`Acct`] conversion failure"] #[error("failed to convert string '{0}' into acct")] pub struct InvalidAcctString(String); diff --git a/packages/backend-rs/src/federation/internal_actor/cache.rs b/packages/backend-rs/src/federation/internal_actor/cache.rs new file mode 100644 index 0000000000..0aeacee660 --- /dev/null +++ b/packages/backend-rs/src/federation/internal_actor/cache.rs @@ -0,0 +1,85 @@ +//! In-memory internal actor cache handler + +// TODO: refactoring + +use super::*; +use crate::{database::db_conn, model::entity::user}; +use sea_orm::prelude::*; +use std::sync::{Arc, Mutex}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + #[doc = "database error"] + Db(#[from] DbErr), + #[error("{} does not exist", Acct::from(.0.to_owned()))] + #[doc = "internal actor does not exist"] + InternalActorNotFound(InternalActor), +} + +static INSTANCE_ACTOR: Mutex<Option<Arc<user::Model>>> = Mutex::new(None); +static RELAY_ACTOR: Mutex<Option<Arc<user::Model>>> = Mutex::new(None); + +fn set_instance_actor(value: Arc<user::Model>) { + let _ = INSTANCE_ACTOR.lock().map(|mut cache| *cache = Some(value)); +} +fn set_relay_actor(value: Arc<user::Model>) { + let _ = RELAY_ACTOR.lock().map(|mut cache| *cache = Some(value)); +} + +async fn cache_instance_actor() -> Result<Arc<user::Model>, Error> { + let actor = user::Entity::find() + .filter(user::Column::Username.eq(INSTANCE_ACTOR_USERNAME)) + .filter(user::Column::Host.is_null()) + .one(db_conn().await?) + .await?; + + if let Some(actor) = actor { + let arc = Arc::new(actor); + set_instance_actor(arc.clone()); + Ok(arc) + } else { + Err(Error::InternalActorNotFound(InternalActor::Instance)) + } +} +async fn cache_relay_actor() -> Result<Arc<user::Model>, Error> { + let actor = user::Entity::find() + .filter(user::Column::Username.eq(RELAY_ACTOR_USERNAME)) + .filter(user::Column::Host.is_null()) + .one(db_conn().await?) + .await?; + + if let Some(actor) = actor { + let arc = Arc::new(actor); + set_relay_actor(arc.clone()); + Ok(arc) + } else { + Err(Error::InternalActorNotFound(InternalActor::Relay)) + } +} + +// for napi export +// https://github.com/napi-rs/napi-rs/issues/2060 +type User = user::Model; + +#[macros::export(js_name = "getInternalActor")] +pub async fn get(actor: InternalActor) -> Result<Arc<User>, Error> { + match actor { + InternalActor::Instance => { + if let Some(cache) = INSTANCE_ACTOR.lock().ok().and_then(|cache| cache.clone()) { + tracing::debug!("Using cached instance.actor"); + return Ok(cache); + } + tracing::debug!("Caching instance.actor"); + cache_instance_actor().await + } + InternalActor::Relay => { + if let Some(cache) = RELAY_ACTOR.lock().ok().and_then(|cache| cache.clone()) { + tracing::debug!("Using cached relay.actor"); + return Ok(cache); + } + tracing::debug!("Caching relay.actor"); + cache_relay_actor().await + } + } +} diff --git a/packages/backend-rs/src/federation/internal_actor/mod.rs b/packages/backend-rs/src/federation/internal_actor/mod.rs new file mode 100644 index 0000000000..6996062b16 --- /dev/null +++ b/packages/backend-rs/src/federation/internal_actor/mod.rs @@ -0,0 +1,34 @@ +mod cache; + +pub use cache::get; + +use super::acct::Acct; + +#[derive(Debug)] +#[macros::derive_clone_and_export(string_enum = "lowercase")] +pub enum InternalActor { + Instance, + Relay, +} + +const INSTANCE_ACTOR_USERNAME: &str = "instance.actor"; +const RELAY_ACTOR_USERNAME: &str = "relay.actor"; + +// TODO: When `std::mem::variant_count` is stabilized, use +// it to count system actors instead of hard coding the magic number +pub const INTERNAL_ACTORS: u64 = 2; + +impl From<InternalActor> for Acct { + fn from(actor: InternalActor) -> Self { + match actor { + InternalActor::Instance => Acct { + username: INSTANCE_ACTOR_USERNAME.to_owned(), + host: None, + }, + InternalActor::Relay => Acct { + username: RELAY_ACTOR_USERNAME.to_owned(), + host: None, + }, + } + } +} diff --git a/packages/backend-rs/src/federation/mod.rs b/packages/backend-rs/src/federation/mod.rs index 722a91abdf..1db28b0858 100644 --- a/packages/backend-rs/src/federation/mod.rs +++ b/packages/backend-rs/src/federation/mod.rs @@ -1,4 +1,5 @@ //! Services used to federate with other servers pub mod acct; +pub mod internal_actor; pub mod nodeinfo; diff --git a/packages/backend-rs/src/misc/user/count.rs b/packages/backend-rs/src/misc/user/count.rs index 8f17a71bc5..70353ef018 100644 --- a/packages/backend-rs/src/misc/user/count.rs +++ b/packages/backend-rs/src/misc/user/count.rs @@ -1,18 +1,12 @@ -use crate::model::entity::user; +use crate::{federation::internal_actor::INTERNAL_ACTORS, model::entity::user}; use sea_orm::prelude::*; -// TODO: When `std::mem::variant_count` is stabilized, use -// it to count system actors instead of hard coding the magic number - -// @instance.actor and @relay.actor are not real users -const NUMBER_OF_SYSTEM_ACTORS: u64 = 2; - pub async fn local_total(db: &DbConn) -> Result<u64, DbErr> { user::Entity::find() .filter(user::Column::Host.is_null()) .count(db) .await - .map(|count| count - NUMBER_OF_SYSTEM_ACTORS) + .map(|count| count - INTERNAL_ACTORS) } #[macros::ts_export(js_name = "countLocalUsers")] diff --git a/packages/backend/src/remote/activitypub/resolver.ts b/packages/backend/src/remote/activitypub/resolver.ts index abb9c2b2a4..fab913f869 100644 --- a/packages/backend/src/remote/activitypub/resolver.ts +++ b/packages/backend/src/remote/activitypub/resolver.ts @@ -1,8 +1,8 @@ import { config } from "@/config.js"; import type { ILocalUser } from "@/models/entities/user.js"; -import { getInstanceActor } from "@/services/instance-actor.js"; import { extractHost, + getInternalActor, isAllowedServer, isBlockedServer, isSelfHost, @@ -112,7 +112,7 @@ export default class Resolver { } if (!this.user) { - this.user = await getInstanceActor(); + this.user = await getInternalActor("instance"); } apLogger.info( @@ -158,7 +158,7 @@ export default class Resolver { if (!parsed.local) throw new Error("resolveLocal: not local"); switch (parsed.type) { - case "notes": + case "notes": { const note = await Notes.findOneByOrFail({ id: parsed.id }); if (parsed.rest === "activity") { // this refers to the create activity and not the note itself @@ -166,20 +166,24 @@ export default class Resolver { } else { return renderNote(note); } - case "users": + } + case "users": { const user = await Users.findOneByOrFail({ id: parsed.id }); return await renderPerson(user as ILocalUser); - case "questions": + } + case "questions": { // Polls are indexed by the note they are attached to. const [pollNote, poll] = await Promise.all([ Notes.findOneByOrFail({ id: parsed.id }), Polls.findOneByOrFail({ noteId: parsed.id }), ]); return await renderQuestion({ id: pollNote.userId }, pollNote, poll); - case "likes": + } + case "likes": { const reaction = await NoteReactions.findOneByOrFail({ id: parsed.id }); return renderActivity(renderLike(reaction, { uri: null })); - case "follows": + } + case "follows": { // if rest is a <followee id> if (parsed.rest != null && /^\w+$/.test(parsed.rest)) { const [follower, followee] = await Promise.all( @@ -207,6 +211,7 @@ export default class Resolver { throw new Error("resolveLocal: invalid follow URI"); } return renderActivity(renderFollow(follower, followee, url)); + } default: throw new Error(`resolveLocal: type ${parsed.type} unhandled`); } diff --git a/packages/backend/src/server/activitypub.ts b/packages/backend/src/server/activitypub.ts index 0d04ded58b..7b05ebffd0 100644 --- a/packages/backend/src/server/activitypub.ts +++ b/packages/backend/src/server/activitypub.ts @@ -9,7 +9,7 @@ import renderKey from "@/remote/activitypub/renderer/key.js"; import { renderPerson } from "@/remote/activitypub/renderer/person.js"; import renderEmoji from "@/remote/activitypub/renderer/emoji.js"; import { inbox as processInbox } from "@/queue/index.js"; -import { fetchMeta, isSelfHost } from "backend-rs"; +import { fetchMeta, getInternalActor, isSelfHost } from "backend-rs"; import { Notes, Users, @@ -24,7 +24,6 @@ import { checkFetch, getSignatureUser, } from "@/remote/activitypub/check-fetch.js"; -import { getInstanceActor } from "@/services/instance-actor.js"; import renderFollow from "@/remote/activitypub/renderer/follow.js"; import Featured from "./activitypub/featured.js"; import Following from "./activitypub/following.js"; @@ -296,7 +295,7 @@ router.get("/users/:user/collections/featured", Featured); // publickey router.get("/users/:user/publickey", async (ctx) => { - const instanceActor = await getInstanceActor(); + const instanceActor = await getInternalActor("instance"); if (ctx.params.user === instanceActor.id) { ctx.body = renderActivity( renderKey(instanceActor, await getUserKeypair(instanceActor.id)), @@ -360,7 +359,7 @@ async function userInfo(ctx: Router.RouterContext, user: User | null) { router.get("/users/:user", async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); - const instanceActor = await getInstanceActor(); + const instanceActor = await getInternalActor("instance"); if (ctx.params.user === instanceActor.id) { await userInfo(ctx, instanceActor); return; @@ -387,7 +386,7 @@ router.get("/@:user", async (ctx, next) => { if (!isActivityPubReq(ctx)) return await next(); if (ctx.params.user === "instance.actor") { - const instanceActor = await getInstanceActor(); + const instanceActor = await getInternalActor("instance"); await userInfo(ctx, instanceActor); return; } @@ -407,8 +406,8 @@ router.get("/@:user", async (ctx, next) => { await userInfo(ctx, user); }); -router.get("/actor", async (ctx, next) => { - const instanceActor = await getInstanceActor(); +router.get("/actor", async (ctx, _next) => { + const instanceActor = await getInternalActor("instance"); await userInfo(ctx, instanceActor); }); //#endregion diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts index a94687cf40..1104a591c8 100644 --- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts +++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts @@ -1,6 +1,6 @@ import define from "@/server/api/define.js"; import { AbuseUserReports, Users } from "@/models/index.js"; -import { getInstanceActor } from "@/services/instance-actor.js"; +import { getInternalActor } from "backend-rs"; import { deliver } from "@/queue/index.js"; import { renderActivity } from "@/remote/activitypub/renderer/index.js"; import { renderFlag } from "@/remote/activitypub/renderer/flag.js"; @@ -29,7 +29,7 @@ export default define(meta, paramDef, async (ps, me) => { } if (ps.forward && report.targetUserHost != null) { - const actor = await getInstanceActor(); + const actor = await getInternalActor("instance"); const targetUser = await Users.findOneByOrFail({ id: report.targetUserId }); deliver( diff --git a/packages/backend/src/services/instance-actor.ts b/packages/backend/src/services/instance-actor.ts deleted file mode 100644 index 8bdcc1d36d..0000000000 --- a/packages/backend/src/services/instance-actor.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { ILocalUser } from "@/models/entities/user.js"; -import { Users } from "@/models/index.js"; -import { Cache } from "@/misc/cache.js"; -import { IsNull } from "typeorm"; - -const ACTOR_USERNAME = "instance.actor" as const; - -const cache = new Cache<ILocalUser>("instanceActor", 60 * 30); - -export async function getInstanceActor(): Promise<ILocalUser> { - const cached = await cache.get(null, true); - if (cached) return cached; - - const user = (await Users.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, - })) as ILocalUser; - - await cache.set(null, user); - return user; -} diff --git a/packages/backend/src/services/relay.ts b/packages/backend/src/services/relay.ts index cbc90e25ac..90e217c2e9 100644 --- a/packages/backend/src/services/relay.ts +++ b/packages/backend/src/services/relay.ts @@ -1,4 +1,3 @@ -import { IsNull } from "typeorm"; import { renderFollowRelay } from "@/remote/activitypub/renderer/follow-relay.js"; import { renderActivity, @@ -6,25 +5,14 @@ import { } from "@/remote/activitypub/renderer/index.js"; import renderUndo from "@/remote/activitypub/renderer/undo.js"; import { deliver } from "@/queue/index.js"; -import type { ILocalUser, User } from "@/models/entities/user.js"; -import { Users, Relays } from "@/models/index.js"; -import { genId } from "backend-rs"; +import type { User } from "@/models/entities/user.js"; +import { Relays } from "@/models/index.js"; +import { getInternalActor, genId } from "backend-rs"; import { Cache } from "@/misc/cache.js"; import type { Relay } from "@/models/entities/relay.js"; -const ACTOR_USERNAME = "relay.actor" as const; - const relaysCache = new Cache<Relay[]>("relay", 60 * 60); -export async function getRelayActor(): Promise<ILocalUser> { - const user = await Users.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, - }); - - return user as ILocalUser; -} - export async function addRelay(inbox: string) { const relay = await Relays.insert({ id: genId(), @@ -32,7 +20,7 @@ export async function addRelay(inbox: string) { status: "requesting", }).then((x) => Relays.findOneByOrFail(x.identifiers[0])); - const relayActor = await getRelayActor(); + const relayActor = await getInternalActor("relay"); const follow = renderFollowRelay(relay, relayActor); const activity = renderActivity(follow); deliver(relayActor, activity, relay.inbox); @@ -49,7 +37,7 @@ export async function removeRelay(inbox: string) { throw new Error("relay not found"); } - const relayActor = await getRelayActor(); + const relayActor = await getInternalActor("relay"); const follow = renderFollowRelay(relay, relayActor); const undo = renderUndo(follow, relayActor); const activity = renderActivity(undo);