diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 06bb7e1430..487f87bdda 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -216,6 +216,7 @@ export function acctToString(acct: Acct): string export function fetchNodeinfo(host: string): Promise<Nodeinfo> export function nodeinfo_2_1(): Promise<any> export function nodeinfo_2_0(): Promise<any> +export function updateNodeinfoCache(): Promise<void> /** NodeInfo schema version 2.0. <https://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.0> */ export interface Nodeinfo { /** The schema version, must be 2.0. */ @@ -241,16 +242,16 @@ export interface Software20 { version: string } export enum Protocol { - Activitypub = 'activitypub', - Buddycloud = 'buddycloud', - Dfrn = 'dfrn', - Diaspora = 'diaspora', - Libertree = 'libertree', - Ostatus = 'ostatus', - Pumpio = 'pumpio', - Tent = 'tent', - Xmpp = 'xmpp', - Zot = 'zot' + Activitypub = 0, + Buddycloud = 1, + Dfrn = 2, + Diaspora = 3, + Libertree = 4, + Ostatus = 5, + Pumpio = 6, + Tent = 7, + Xmpp = 8, + Zot = 9 } /** The third party sites this server can connect to via their application API. */ export interface Services { @@ -261,45 +262,45 @@ export interface Services { } /** The third party sites this server can retrieve messages from for combined display with regular traffic. */ export enum Inbound { - Atom1 = 'atom1', - Gnusocial = 'gnusocial', - Imap = 'imap', - Pnut = 'pnut', - Pop3 = 'pop3', - Pumpio = 'pumpio', - Rss2 = 'rss2', - Twitter = 'twitter' + Atom1 = 0, + Gnusocial = 1, + Imap = 2, + Pnut = 3, + Pop3 = 4, + Pumpio = 5, + Rss2 = 6, + Twitter = 7 } /** The third party sites this server can publish messages to on the behalf of a user. */ export enum Outbound { - Atom1 = 'atom1', - Blogger = 'blogger', - Buddycloud = 'buddycloud', - Diaspora = 'diaspora', - Dreamwidth = 'dreamwidth', - Drupal = 'drupal', - Facebook = 'facebook', - Friendica = 'friendica', - Gnusocial = 'gnusocial', - Google = 'google', - Insanejournal = 'insanejournal', - Libertree = 'libertree', - Linkedin = 'linkedin', - Livejournal = 'livejournal', - Mediagoblin = 'mediagoblin', - Myspace = 'myspace', - Pinterest = 'pinterest', - Pnut = 'pnut', - Posterous = 'posterous', - Pumpio = 'pumpio', - Redmatrix = 'redmatrix', - Rss2 = 'rss2', - Smtp = 'smtp', - Tent = 'tent', - Tumblr = 'tumblr', - Twitter = 'twitter', - Wordpress = 'wordpress', - Xmpp = 'xmpp' + Atom1 = 0, + Blogger = 1, + Buddycloud = 2, + Diaspora = 3, + Dreamwidth = 4, + Drupal = 5, + Facebook = 6, + Friendica = 7, + Gnusocial = 8, + Google = 9, + Insanejournal = 10, + Libertree = 11, + Linkedin = 12, + Livejournal = 13, + Mediagoblin = 14, + Myspace = 15, + Pinterest = 16, + Pnut = 17, + Posterous = 18, + Pumpio = 19, + Redmatrix = 20, + Rss2 = 21, + Smtp = 22, + Tent = 23, + Tumblr = 24, + Twitter = 25, + Wordpress = 26, + Xmpp = 27 } /** Usage statistics for this server. */ export interface Usage { diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 1801db3f63..b4a6bea090 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -310,7 +310,7 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, fetchMeta, updateMetaCache, metaToPugArgs, loadConfig, stringToAcct, acctToString, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrc, DriveFileUsageHint, MutedNoteReason, NoteVisibility, NotificationType, PageVisibility, PollNoteVisibility, RelayStatus, UserEmojiModPerm, UserProfileFfvisibility, UserProfileMutingNotificationTypes, updateAntennasOnNewNote, updateAntennaCache, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, ChatEvent, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding +const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, fetchMeta, updateMetaCache, metaToPugArgs, loadConfig, stringToAcct, acctToString, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, updateNodeinfoCache, Protocol, Inbound, Outbound, greet, initializeRustLogger, showServerInfo, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isQuote, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, cpuInfo, cpuUsage, memoryUsage, storageUsage, AntennaSrc, DriveFileUsageHint, MutedNoteReason, NoteVisibility, NotificationType, PageVisibility, PollNoteVisibility, RelayStatus, UserEmojiModPerm, UserProfileFfvisibility, UserProfileMutingNotificationTypes, updateAntennasOnNewNote, updateAntennaCache, watchNote, unwatchNote, PushNotificationKind, sendPushNotification, publishToChannelStream, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, ChatEvent, getTimestamp, genId, genIdAt, generateSecureRandomString, generateUserToken } = nativeBinding module.exports.SECOND = SECOND module.exports.MINUTE = MINUTE @@ -328,6 +328,7 @@ module.exports.acctToString = acctToString module.exports.fetchNodeinfo = fetchNodeinfo module.exports.nodeinfo_2_1 = nodeinfo_2_1 module.exports.nodeinfo_2_0 = nodeinfo_2_0 +module.exports.updateNodeinfoCache = updateNodeinfoCache module.exports.Protocol = Protocol module.exports.Inbound = Inbound module.exports.Outbound = Outbound diff --git a/packages/backend-rs/src/federation/nodeinfo/generate.rs b/packages/backend-rs/src/federation/nodeinfo/generate.rs index 63621e9e92..c6a7bcdab7 100644 --- a/packages/backend-rs/src/federation/nodeinfo/generate.rs +++ b/packages/backend-rs/src/federation/nodeinfo/generate.rs @@ -2,21 +2,27 @@ use crate::{ config::{local_server_info, CONFIG}, - database::{cache, db_conn}, + database::db_conn, federation::nodeinfo::schema::*, model::entity::{note, user}, }; use sea_orm::prelude::*; use serde_json::json; -use std::collections::HashMap; +use std::{collections::HashMap, sync::Mutex}; + +static CACHE: Mutex<Option<Nodeinfo21>> = Mutex::new(None); + +fn set_cache(nodeinfo: &Nodeinfo21) { + let _ = CACHE + .lock() + .map(|mut cache| *cache = Some(nodeinfo.to_owned())); +} /// Errors that can occur while generating NodeInfo of the local server #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Database error: {0}")] Db(#[from] DbErr), - #[error("Cache error: {0}")] - Cache(#[from] cache::Error), #[error("Failed to serialize nodeinfo to JSON: {0}")] Json(#[from] serde_json::Error), } @@ -62,10 +68,12 @@ async fn statistics() -> Result<(u64, u64, u64, u64), DbErr> { /// Generates NodeInfo (version 2.1) of the local server. /// This function doesn't use caches and returns the latest information. async fn generate_nodeinfo_2_1() -> Result<Nodeinfo21, Error> { + tracing::info!("generating NodeInfo"); + let (local_users, local_active_halfyear, local_active_month, local_posts) = statistics().await?; let meta = local_server_info().await?; - let metadata = HashMap::from([ + let mut metadata = HashMap::from([ ( "nodeName".to_string(), json!(meta.name.unwrap_or_else(|| CONFIG.host.clone())), @@ -98,6 +106,7 @@ async fn generate_nodeinfo_2_1() -> Result<Nodeinfo21, Error> { json!(meta.theme_color.unwrap_or_else(|| "#31748f".to_string())), ), ]); + metadata.shrink_to_fit(); Ok(Nodeinfo21 { version: "2.1".to_string(), @@ -126,19 +135,24 @@ async fn generate_nodeinfo_2_1() -> Result<Nodeinfo21, Error> { }) } +async fn nodeinfo_2_1_impl(use_cache: bool) -> Result<Nodeinfo21, Error> { + if use_cache { + if let Some(nodeinfo) = CACHE.lock().ok().and_then(|cache| cache.to_owned()) { + return Ok(nodeinfo); + } + } + + let nodeinfo = generate_nodeinfo_2_1().await?; + + tracing::info!("updating cache"); + set_cache(&nodeinfo); + + Ok(nodeinfo) +} + /// Returns NodeInfo (version 2.1) of the local server. pub async fn nodeinfo_2_1() -> Result<Nodeinfo21, Error> { - const NODEINFO_2_1_CACHE_KEY: &str = "nodeinfo_2_1"; - - let cached = cache::get::<Nodeinfo21>(NODEINFO_2_1_CACHE_KEY).await?; - - if let Some(nodeinfo) = cached { - Ok(nodeinfo) - } else { - let nodeinfo = generate_nodeinfo_2_1().await?; - cache::set(NODEINFO_2_1_CACHE_KEY, &nodeinfo, 60 * 60).await?; - Ok(nodeinfo) - } + nodeinfo_2_1_impl(true).await } /// Returns NodeInfo (version 2.0) of the local server. @@ -155,3 +169,9 @@ pub async fn nodeinfo_2_1_as_json() -> Result<serde_json::Value, Error> { pub async fn nodeinfo_2_0_as_json() -> Result<serde_json::Value, Error> { Ok(serde_json::to_value(nodeinfo_2_0().await?)?) } + +#[crate::ts_export(js_name = "updateNodeinfoCache")] +pub async fn update_cache() -> Result<(), Error> { + nodeinfo_2_1_impl(false).await?; + Ok(()) +} diff --git a/packages/backend-rs/src/federation/nodeinfo/schema.rs b/packages/backend-rs/src/federation/nodeinfo/schema.rs index 2876afbbdb..82f1dfe728 100644 --- a/packages/backend-rs/src/federation/nodeinfo/schema.rs +++ b/packages/backend-rs/src/federation/nodeinfo/schema.rs @@ -10,8 +10,8 @@ use std::collections::HashMap; // * #[serde(tag = "version", rename = "2.1")] (https://github.com/3Hren/msgpack-rust/issues/318) /// NodeInfo schema version 2.1. <https://nodeinfo.diaspora.software/docson/index.html#/ns/schema/2.1> -#[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Deserialize, Serialize)] +#[cfg_attr(test, derive(Debug, PartialEq, Deserialize))] +#[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct Nodeinfo21 { /// The schema version, must be 2.1. @@ -53,8 +53,8 @@ pub struct Nodeinfo20 { } /// Metadata about server software in use (version 2.1). -#[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Deserialize, Serialize)] +#[cfg_attr(test, derive(Debug, PartialEq, Deserialize))] +#[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct Software21 { /// The canonical name of this server software. @@ -82,7 +82,7 @@ pub struct Software20 { #[cfg_attr(test, derive(Debug, PartialEq))] #[derive(Deserialize, Serialize)] #[serde(rename_all = "lowercase")] -#[crate::export(string_enum = "lowercase")] +#[crate::derive_clone_and_export] pub enum Protocol { Activitypub, Buddycloud, @@ -98,7 +98,7 @@ pub enum Protocol { /// The third party sites this server can connect to via their application API. #[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[crate::export(object)] pub struct Services { @@ -112,7 +112,7 @@ pub struct Services { #[cfg_attr(test, derive(Debug, PartialEq))] #[derive(Deserialize, Serialize)] #[serde(rename_all = "lowercase")] -#[crate::export(string_enum = "lowercase")] +#[crate::derive_clone_and_export] pub enum Inbound { #[serde(rename = "atom1.0")] Atom1, @@ -131,7 +131,7 @@ pub enum Inbound { #[cfg_attr(test, derive(Debug, PartialEq))] #[derive(Deserialize, Serialize)] #[serde(rename_all = "lowercase")] -#[crate::export(string_enum = "lowercase")] +#[crate::derive_clone_and_export] pub enum Outbound { #[serde(rename = "atom1.0")] Atom1, @@ -167,7 +167,7 @@ pub enum Outbound { /// Usage statistics for this server. #[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[crate::export(object)] pub struct Usage { @@ -178,7 +178,7 @@ pub struct Usage { /// statistics about the users of this server. #[cfg_attr(test, derive(Debug, PartialEq))] -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] #[crate::export(object)] pub struct Users { diff --git a/packages/backend-rs/src/lib.rs b/packages/backend-rs/src/lib.rs index d62ab792d6..6a124de340 100644 --- a/packages/backend-rs/src/lib.rs +++ b/packages/backend-rs/src/lib.rs @@ -1,4 +1,4 @@ -use macro_rs::{export, ts_export}; +use macro_rs::{derive_clone_and_export, export, ts_export}; pub mod config; pub mod database; diff --git a/packages/backend/src/boot/master.ts b/packages/backend/src/boot/master.ts index b70b9a2f11..dbf51c08c5 100644 --- a/packages/backend/src/boot/master.ts +++ b/packages/backend/src/boot/master.ts @@ -8,6 +8,7 @@ import { removeOldAttestationChallenges, showServerInfo, updateMetaCache, + updateNodeinfoCache, type Config, } from "backend-rs"; import { config } from "@/config.js"; @@ -51,6 +52,8 @@ export async function masterMain() { import("../daemons/queue-stats.js").then((x) => x.default()); // Update meta cache every 5 minitues setInterval(() => updateMetaCache(), 1000 * 60 * 5); + // Update nodeinfo cache every hour + setInterval(() => updateNodeinfoCache(), 1000 * 60 * 60); // Remove old attestation challenges setInterval(() => removeOldAttestationChallenges(), 1000 * 60 * 30); } diff --git a/packages/macro-rs/src/lib.rs b/packages/macro-rs/src/lib.rs index 943dd56cd1..0860298048 100644 --- a/packages/macro-rs/src/lib.rs +++ b/packages/macro-rs/src/lib.rs @@ -25,6 +25,35 @@ pub fn read_version_from_package_json(_item: proc_macro::TokenStream) -> proc_ma quote! { #version }.into() } +/// Export an enum to TypeScript, and derive [Clone]. +/// +/// You need this macro because [`napi_derive::napi`](https://docs.rs/napi-derive/latest/napi_derive/attr.napi.html) +/// automatically derives the [Clone] trait for enums and causes conflicts. +/// +/// This is a wrapper of [`napi_derive::napi`](https://docs.rs/napi-derive/latest/napi_derive/attr.napi.html) +/// that expands to +/// ```no_run +/// #[cfg_attr(not(feature = "napi"), derive(Clone))] +/// #[cfg_attr(feature = "napi", napi_derive::napi(attr))] +/// # fn f() {} // to work around doc test compilation error +/// ``` +/// where `attr` is given attribute(s). +#[proc_macro_attribute] +pub fn derive_clone_and_export( + attr: proc_macro::TokenStream, + item: proc_macro::TokenStream, +) -> proc_macro::TokenStream { + let attr: TokenStream = attr.into(); + let item: TokenStream = item.into(); + + quote! { + #[cfg_attr(not(feature = "napi"), derive(Clone))] + #[cfg_attr(feature = "napi", napi_derive::napi(#attr))] + #item + } + .into() +} + /// Export a function, struct, enum, const, etc. to TypeScript. /// /// This is a wrapper of [macro@napi] that expands to