From 0839fe27b2dd20ad364e98df0ab0dbf257e8dd58 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Fri, 7 Jun 2024 22:21:53 +0900
Subject: [PATCH] refactor (backend-rs): save nodeinfo cache directly

---
 packages/backend-rs/index.d.ts                | 93 ++++++++++---------
 packages/backend-rs/index.js                  |  3 +-
 .../src/federation/nodeinfo/generate.rs       | 52 +++++++----
 .../src/federation/nodeinfo/schema.rs         | 20 ++--
 packages/backend-rs/src/lib.rs                |  2 +-
 packages/backend/src/boot/master.ts           |  3 +
 packages/macro-rs/src/lib.rs                  | 29 ++++++
 7 files changed, 128 insertions(+), 74 deletions(-)

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