refactor (backend-rs): save nodeinfo cache directly

This commit is contained in:
naskya 2024-06-07 22:21:53 +09:00
parent 101c001379
commit 0839fe27b2
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
7 changed files with 128 additions and 74 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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