refactor (backend): port getInstanceActor/getRelayActor to backend-rs

This commit is contained in:
naskya 2024-07-13 20:36:37 +09:00
parent 9f22a43229
commit 9bb357dc50
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
12 changed files with 155 additions and 63 deletions

View file

@ -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.

View file

@ -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

View file

@ -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);

View file

@ -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
}
}
}

View file

@ -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,
},
}
}
}

View file

@ -1,4 +1,5 @@
//! Services used to federate with other servers
pub mod acct;
pub mod internal_actor;
pub mod nodeinfo;

View file

@ -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")]

View file

@ -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`);
}

View file

@ -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

View file

@ -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(

View file

@ -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;
}

View file

@ -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);