Proxied images and user instance meta resolving

This commit is contained in:
Natty 2023-11-05 15:23:48 +01:00
parent a5ab2acca0
commit 6908a2f350
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
23 changed files with 443 additions and 22 deletions

7
Cargo.lock generated
View File

@ -1482,6 +1482,7 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"unicode-segmentation", "unicode-segmentation",
"url",
] ]
[[package]] [[package]]
@ -1537,12 +1538,14 @@ dependencies = [
name = "magnetar_common" name = "magnetar_common"
version = "0.2.1-alpha" version = "0.2.1-alpha"
dependencies = [ dependencies = [
"idna",
"magnetar_core", "magnetar_core",
"magnetar_sdk", "magnetar_sdk",
"percent-encoding", "percent-encoding",
"serde", "serde",
"thiserror", "thiserror",
"toml 0.8.1", "toml 0.8.1",
"url",
] ]
[[package]] [[package]]
@ -3670,9 +3673,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.4.0" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",

View File

@ -88,6 +88,7 @@ hyper = { workspace = true, features = ["full"] }
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
tower = { workspace = true } tower = { workspace = true }
tower-http = { workspace = true, features = ["cors", "trace", "fs"] } tower-http = { workspace = true, features = ["cors", "trace", "fs"] }
url = { workspace = true }
idna = { workspace = true } idna = { workspace = true }

View File

@ -47,6 +47,19 @@
# Environment variable: MAG_C_BIND_ADDR # Environment variable: MAG_C_BIND_ADDR
# networking.bind_addr = "::" # networking.bind_addr = "::"
# [Optional]
# The URL of a media proxy
# Default: null
# Environment variable: MAG_C_MEDIA_PROXY
# networking.media_proxy = ""
# [Optional]
# Whether to proxy remote files through this instance
# Default: false
# Environment variable: MAG_C_PROXY_REMOTE_FILES
# networking.proxy_remote_files = false
# -----------------------------[ CALCKEY FRONTEND ]---------------------------- # -----------------------------[ CALCKEY FRONTEND ]----------------------------
# [Optional] # [Optional]

View File

@ -61,6 +61,7 @@ impl CalckeyModel {
.sqlx_logging_level(LevelFilter::Debug) .sqlx_logging_level(LevelFilter::Debug)
.to_owned(); .to_owned();
info!("Attempting database connection...");
Ok(CalckeyModel(sea_orm::Database::connect(opt).await?)) Ok(CalckeyModel(sea_orm::Database::connect(opt).await?))
} }
@ -224,6 +225,18 @@ impl CalckeyModel {
.await?) .await?)
} }
pub async fn get_instance(
&self,
host: &str,
) -> Result<Option<instance::Model>, CalckeyDbError> {
let instance = instance::Entity::find()
.filter(instance::Column::Host.eq(host))
.one(&self.0)
.await?;
Ok(instance)
}
pub async fn get_instance_meta(&self) -> Result<meta::Model, CalckeyDbError> { pub async fn get_instance_meta(&self) -> Result<meta::Model, CalckeyDbError> {
let txn = self.0.begin().await?; let txn = self.0.begin().await?;

View File

@ -5,7 +5,6 @@ use sea_orm::{
QueryFilter, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select, QueryFilter, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::info;
use ck::{drive_file, note, note_reaction, user}; use ck::{drive_file, note, note_reaction, user};
use magnetar_sdk::types::RangeFilter; use magnetar_sdk::types::RangeFilter;

View File

@ -10,7 +10,9 @@ crate-type = ["rlib"]
magnetar_core = { path = "../core" } magnetar_core = { path = "../core" }
magnetar_sdk = { path = "../magnetar_sdk" } magnetar_sdk = { path = "../magnetar_sdk" }
idna = { workspace = true }
percent-encoding = { workspace = true } percent-encoding = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
toml = { workspace = true } toml = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
url = { workspace = true }

View File

@ -10,6 +10,8 @@ pub struct MagnetarNetworking {
pub port: u16, pub port: u16,
pub bind_addr: IpAddr, pub bind_addr: IpAddr,
pub protocol: MagnetarNetworkingProtocol, pub protocol: MagnetarNetworkingProtocol,
pub media_proxy: Option<String>,
pub proxy_remote_files: bool,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -66,6 +68,19 @@ fn env_protocol() -> MagnetarNetworkingProtocol {
} }
} }
fn env_media_proxy() -> Option<String> {
std::env::var("MAG_C_MEDIA_PROXY")
.ok()
.filter(String::is_empty)
}
fn env_proxy_remote_files() -> bool {
std::env::var("MAG_C_PROXY_REMOTE_FILES")
.unwrap_or_else(|_| "false".to_string())
.parse()
.expect("MAG_C_PROXY_REMOTE_FILES must be a boolean")
}
impl Default for MagnetarNetworking { impl Default for MagnetarNetworking {
fn default() -> Self { fn default() -> Self {
MagnetarNetworking { MagnetarNetworking {
@ -73,6 +88,8 @@ impl Default for MagnetarNetworking {
bind_addr: env_bind_addr(), bind_addr: env_bind_addr(),
port: env_port(), port: env_port(),
protocol: env_protocol(), protocol: env_protocol(),
media_proxy: env_media_proxy(),
proxy_remote_files: env_proxy_remote_files(),
} }
} }
} }
@ -192,6 +209,8 @@ pub enum MagnetarConfigError {
IoError(#[from] std::io::Error), IoError(#[from] std::io::Error),
#[error("Failed to parse configuration: {0}")] #[error("Failed to parse configuration: {0}")]
DeserializeError(#[from] toml::de::Error), DeserializeError(#[from] toml::de::Error),
#[error("Configuration error: Not a valid hostname")]
ConfigHostnameError(#[from] idna::Errors),
} }
pub fn load_config() -> Result<MagnetarConfig, MagnetarConfigError> { pub fn load_config() -> Result<MagnetarConfig, MagnetarConfigError> {
@ -200,7 +219,19 @@ pub fn load_config() -> Result<MagnetarConfig, MagnetarConfigError> {
let str_cfg = std::fs::read_to_string(path)?; let str_cfg = std::fs::read_to_string(path)?;
let config = toml::from_str(&str_cfg)?; let mut config: MagnetarConfig = toml::from_str(&str_cfg)?;
// Validate the host
idna::domain_to_unicode(&config.networking.host).1?;
if config
.networking
.media_proxy
.as_deref()
.is_some_and(str::is_empty)
{
config.networking.media_proxy = None;
}
Ok(config) Ok(config)
} }

View File

@ -1,4 +1,3 @@
use chrono::format;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use ts_rs::TS; use ts_rs::TS;

View File

@ -53,7 +53,9 @@ pub struct DriveFileBase {
pub mime_type: String, pub mime_type: String,
pub media_metadata: ImageMeta, pub media_metadata: ImageMeta,
pub url: Option<String>, pub url: Option<String>,
pub source_url: String,
pub thumbnail_url: Option<String>, pub thumbnail_url: Option<String>,
pub blurhash: Option<String>,
pub sensitive: bool, pub sensitive: bool,
pub comment: Option<String>, pub comment: Option<String>,
pub folder_id: Option<String>, pub folder_id: Option<String>,

View File

@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};
use ts_rs::TS;
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[ts(export)]
pub struct InstanceTicker {
pub name: Option<String>,
pub software_name: Option<String>,
pub software_version: Option<String>,
pub icon_url: Option<String>,
pub favicon_url: Option<String>,
pub theme_color: Option<String>,
}

View File

@ -1,5 +1,6 @@
pub mod drive; pub mod drive;
pub mod emoji; pub mod emoji;
pub mod instance;
pub mod note; pub mod note;
pub mod timeline; pub mod timeline;
pub mod user; pub mod user;

View File

@ -51,6 +51,7 @@ pack!(PackPollBase, Required<Id> as id & Required<PollBase> as poll);
#[ts(export)] #[ts(export)]
pub struct NoteBase { pub struct NoteBase {
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
pub cw: Option<String>, pub cw: Option<String>,
pub cw_mm: Option<MmXml>, pub cw_mm: Option<MmXml>,
pub uri: Option<String>, pub uri: Option<String>,
@ -64,6 +65,7 @@ pub struct NoteBase {
pub reply_count: u64, pub reply_count: u64,
pub renote_count: u64, pub renote_count: u64,
pub mentions: Vec<String>, pub mentions: Vec<String>,
pub visible_user_ids: Option<Vec<String>>,
pub hashtags: Vec<String>, pub hashtags: Vec<String>,
pub reactions: Vec<ReactionPair>, pub reactions: Vec<ReactionPair>,
pub local_only: bool, pub local_only: bool,

View File

@ -5,6 +5,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use ts_rs::TS; use ts_rs::TS;
use crate::types::instance::InstanceTicker;
use crate::types::note::PackNoteMaybeFull; use crate::types::note::PackNoteMaybeFull;
use magnetar_sdk_macros::pack; use magnetar_sdk_macros::pack;
@ -43,14 +44,14 @@ pub struct UserBase {
pub host: Option<String>, pub host: Option<String>,
pub speech_transform: SpeechTransform, pub speech_transform: SpeechTransform,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub avatar_url: Option<String>, pub avatar_url: String,
pub avatar_blurhash: Option<String>, pub avatar_blurhash: Option<String>,
pub avatar_color: Option<String>,
pub avatar_decoration: AvatarDecoration, pub avatar_decoration: AvatarDecoration,
pub is_admin: bool, pub is_admin: bool,
pub is_moderator: bool, pub is_moderator: bool,
pub is_bot: bool, pub is_bot: bool,
pub emojis: EmojiContext, pub emojis: EmojiContext,
pub instance: Option<InstanceTicker>,
} }
pack!(PackUserBase, Required<Id> as id & Required<UserBase> as user); pack!(PackUserBase, Required<Id> as id & Required<UserBase> as user);
@ -78,7 +79,6 @@ pub struct UserProfileExt {
pub also_known_as: Option<String>, pub also_known_as: Option<String>,
pub banner_url: Option<String>, pub banner_url: Option<String>,
pub banner_color: Option<String>,
pub banner_blurhash: Option<String>, pub banner_blurhash: Option<String>,
pub has_public_reactions: bool, pub has_public_reactions: bool,
@ -96,6 +96,8 @@ pub struct UserProfilePinsEx {
pub struct UserRelationExt { pub struct UserRelationExt {
pub follows_you: bool, pub follows_you: bool,
pub you_follow: bool, pub you_follow: bool,
pub you_request_follow: bool,
pub they_request_follow: bool,
pub blocks_you: bool, pub blocks_you: bool,
pub you_block: bool, pub you_block: bool,
pub mute: bool, pub mute: bool,

View File

@ -1,11 +1,26 @@
use magnetar_calckey_model::ck; use magnetar_calckey_model::ck;
use magnetar_sdk::types::drive::{DriveFileBase, ImageMeta}; use magnetar_sdk::types::drive::{DriveFileBase, ImageMeta};
use serde::Deserialize; use serde::Deserialize;
use url::Url;
use crate::model::{PackType, PackingContext}; use crate::model::{PackType, PackingContext};
impl PackType<&ck::drive_file::Model> for DriveFileBase { #[derive(Debug)]
fn extract(_context: &PackingContext, file: &ck::drive_file::Model) -> Self { pub struct PackFileBaseInput<'a> {
pub file: &'a ck::drive_file::Model,
pub effective_url: Option<&'a Url>,
pub effective_thumbnail_url: Option<&'a Url>,
}
impl PackType<PackFileBaseInput<'_>> for DriveFileBase {
fn extract(
_context: &PackingContext,
PackFileBaseInput {
file,
effective_url,
effective_thumbnail_url,
}: PackFileBaseInput<'_>,
) -> Self {
let media_metadata = ImageMeta::deserialize(file.properties.clone()).unwrap_or_default(); let media_metadata = ImageMeta::deserialize(file.properties.clone()).unwrap_or_default();
DriveFileBase { DriveFileBase {
@ -15,8 +30,10 @@ impl PackType<&ck::drive_file::Model> for DriveFileBase {
hash: None, // TODO: blake3 hash: None, // TODO: blake3
mime_type: file.r#type.clone(), mime_type: file.r#type.clone(),
media_metadata, media_metadata,
url: Some(file.url.clone()), url: effective_url.map(Url::to_string),
thumbnail_url: file.thumbnail_url.clone(), source_url: file.url.clone(),
thumbnail_url: effective_thumbnail_url.map(Url::to_string),
blurhash: file.blurhash.clone(),
sensitive: file.is_sensitive, sensitive: file.is_sensitive,
comment: file.comment.clone(), comment: file.comment.clone(),
folder_id: file.folder_id.clone(), folder_id: file.folder_id.clone(),

View File

@ -0,0 +1,16 @@
use crate::model::{PackType, PackingContext};
use magnetar_calckey_model::ck;
use magnetar_sdk::types::instance::InstanceTicker;
impl<'a> PackType<&'a ck::instance::Model> for InstanceTicker {
fn extract(_context: &PackingContext, data: &'a ck::instance::Model) -> Self {
InstanceTicker {
name: data.name.clone(),
software_name: data.software_name.clone(),
software_version: data.software_version.clone(),
icon_url: data.icon_url.clone(),
favicon_url: data.favicon_url.clone(),
theme_color: data.theme_color.clone(),
}
}
}

View File

@ -1,6 +1,7 @@
pub mod drive; pub mod drive;
pub mod emoji; pub mod emoji;
pub mod id; pub mod id;
pub mod instance;
pub mod note; pub mod note;
pub mod poll; pub mod poll;
pub mod user; pub mod user;

View File

@ -39,6 +39,7 @@ impl PackType<NoteBaseSource<'_>> for NoteBase {
use ck::sea_orm_active_enums::NoteVisibilityEnum as NVE; use ck::sea_orm_active_enums::NoteVisibilityEnum as NVE;
NoteBase { NoteBase {
created_at: note.created_at.into(), created_at: note.created_at.into(),
updated_at: note.updated_at.map(|d| d.into()),
cw: note.cw.clone(), cw: note.cw.clone(),
cw_mm: cw_mm.cloned(), cw_mm: cw_mm.cloned(),
uri: note.uri.clone(), uri: note.uri.clone(),
@ -62,6 +63,8 @@ impl PackType<NoteBaseSource<'_>> for NoteBase {
renote_count: note.renote_count as u64, renote_count: note.renote_count as u64,
mentions: note.mentions.clone(), mentions: note.mentions.clone(),
hashtags: note.tags.clone(), hashtags: note.tags.clone(),
visible_user_ids: matches!(note.visibility, NVE::Specified)
.then(|| note.visible_user_ids.clone()),
reactions: reactions.clone(), reactions: reactions.clone(),
local_only: note.local_only, local_only: note.local_only,
has_poll: note.has_poll, has_poll: note.has_poll,

View File

@ -1,12 +1,15 @@
use magnetar_calckey_model::ck; use magnetar_calckey_model::ck;
use magnetar_calckey_model::ck::sea_orm_active_enums::UserProfileFfvisibilityEnum; use magnetar_calckey_model::ck::sea_orm_active_enums::UserProfileFfvisibilityEnum;
use magnetar_sdk::types::drive::PackDriveFileBase;
use magnetar_sdk::types::emoji::{EmojiContext, PackEmojiBase}; use magnetar_sdk::types::emoji::{EmojiContext, PackEmojiBase};
use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::note::PackNoteMaybeFull; use magnetar_sdk::types::note::PackNoteMaybeFull;
use magnetar_sdk::types::user::{ use magnetar_sdk::types::user::{
AvatarDecoration, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform, AvatarDecoration, PackSecurityKeyBase, ProfileField, SecurityKeyBase, SpeechTransform,
UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx, UserRelationExt, UserSecretsExt, UserBase, UserDetailExt, UserProfileExt, UserProfilePinsEx, UserRelationExt, UserSecretsExt,
}; };
use magnetar_sdk::types::MmXml; use magnetar_sdk::types::MmXml;
use url::Url;
use crate::model::{PackType, PackingContext}; use crate::model::{PackType, PackingContext};
@ -19,8 +22,10 @@ impl PackType<&[PackEmojiBase]> for EmojiContext {
pub struct UserBaseSource<'a> { pub struct UserBaseSource<'a> {
pub user: &'a ck::user::Model, pub user: &'a ck::user::Model,
pub username_mm: Option<&'a MmXml>, pub username_mm: Option<&'a MmXml>,
pub avatar: Option<&'a ck::drive_file::Model>, pub avatar_url: &'a Url,
pub avatar: Option<&'a PackDriveFileBase>,
pub emoji_context: &'a EmojiContext, pub emoji_context: &'a EmojiContext,
pub instance: Option<&'a InstanceTicker>,
} }
impl PackType<UserBaseSource<'_>> for UserBase { impl PackType<UserBaseSource<'_>> for UserBase {
@ -29,8 +34,10 @@ impl PackType<UserBaseSource<'_>> for UserBase {
UserBaseSource { UserBaseSource {
user, user,
username_mm, username_mm,
avatar_url,
avatar, avatar,
emoji_context, emoji_context,
instance,
}: UserBaseSource, }: UserBaseSource,
) -> Self { ) -> Self {
UserBase { UserBase {
@ -49,9 +56,8 @@ impl PackType<UserBaseSource<'_>> for UserBase {
SpeechTransform::None SpeechTransform::None
}, },
created_at: user.created_at.into(), created_at: user.created_at.into(),
avatar_url: avatar.map(|v| v.url.clone()), avatar_url: avatar_url.to_string(),
avatar_blurhash: avatar.and_then(|v| v.blurhash.clone()), avatar_blurhash: avatar.and_then(|v| v.file.0.blurhash.clone()),
avatar_color: None,
avatar_decoration: if user.is_cat { avatar_decoration: if user.is_cat {
AvatarDecoration::CatEars AvatarDecoration::CatEars
} else { } else {
@ -61,6 +67,7 @@ impl PackType<UserBaseSource<'_>> for UserBase {
is_moderator: user.is_moderator, is_moderator: user.is_moderator,
is_bot: user.is_bot, is_bot: user.is_bot,
emojis: emoji_context.clone(), emojis: emoji_context.clone(),
instance: instance.map(|i| i.clone()),
} }
} }
} }
@ -69,6 +76,8 @@ pub struct UserProfileExtSource<'a> {
pub user: &'a ck::user::Model, pub user: &'a ck::user::Model,
pub profile: &'a ck::user_profile::Model, pub profile: &'a ck::user_profile::Model,
pub profile_fields: &'a Vec<ProfileField>, pub profile_fields: &'a Vec<ProfileField>,
pub banner_url: Option<&'a Url>,
pub banner: Option<&'a ck::drive_file::Model>,
pub description_mm: Option<&'a MmXml>, pub description_mm: Option<&'a MmXml>,
pub relation: Option<&'a UserRelationExt>, pub relation: Option<&'a UserRelationExt>,
} }
@ -80,6 +89,8 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
user, user,
profile, profile,
profile_fields, profile_fields,
banner_url,
banner,
description_mm, description_mm,
relation, relation,
}: UserProfileExtSource, }: UserProfileExtSource,
@ -108,9 +119,8 @@ impl PackType<UserProfileExtSource<'_>> for UserProfileExt {
url: profile.url.clone(), url: profile.url.clone(),
moved_to_uri: user.moved_to_uri.clone(), moved_to_uri: user.moved_to_uri.clone(),
also_known_as: user.also_known_as.clone(), also_known_as: user.also_known_as.clone(),
banner_url: None, banner_url: banner_url.map(Url::to_string),
banner_color: None, banner_blurhash: banner.and_then(|b| b.blurhash.clone()),
banner_blurhash: None,
has_public_reactions: profile.public_reactions, has_public_reactions: profile.public_reactions,
} }
} }
@ -133,6 +143,8 @@ struct UserRelationExtSource<'a> {
pub block_in: Option<&'a ck::blocking::Model>, pub block_in: Option<&'a ck::blocking::Model>,
pub mute: Option<&'a ck::muting::Model>, pub mute: Option<&'a ck::muting::Model>,
pub renote_mute: Option<&'a ck::renote_muting::Model>, pub renote_mute: Option<&'a ck::renote_muting::Model>,
pub follow_request_out: Option<&'a ck::follow_request::Model>,
pub follow_request_in: Option<&'a ck::follow_request::Model>,
} }
impl PackType<UserRelationExtSource<'_>> for UserRelationExt { impl PackType<UserRelationExtSource<'_>> for UserRelationExt {
@ -145,6 +157,8 @@ impl PackType<UserRelationExtSource<'_>> for UserRelationExt {
block_in, block_in,
mute, mute,
renote_mute, renote_mute,
follow_request_in,
follow_request_out,
}: UserRelationExtSource, }: UserRelationExtSource,
) -> Self { ) -> Self {
let self_user = context.self_user(); let self_user = context.self_user();
@ -180,6 +194,18 @@ impl PackType<UserRelationExtSource<'_>> for UserRelationExt {
self_user.id == renote_mute.muter_id && self_user.id != renote_mute.mutee_id self_user.id == renote_mute.muter_id && self_user.id != renote_mute.mutee_id
}) })
}), }),
you_request_follow: self_user.is_some_and(|self_user| {
follow_request_in.is_some_and(|follow_req_in| {
self_user.id == follow_req_in.followee_id
&& self_user.id != follow_req_in.follower_id
})
}),
they_request_follow: self_user.is_some_and(|self_user| {
follow_request_out.is_some_and(|follow_req_out| {
self_user.id == follow_req_out.follower_id
&& self_user.id != follow_req_out.followee_id
})
}),
} }
} }
} }

View File

@ -1,21 +1,144 @@
use crate::model::data::drive::PackFileBaseInput;
use crate::model::processing::PackResult; use crate::model::processing::PackResult;
use crate::model::{PackType, PackingContext}; use crate::model::{PackType, PackingContext};
use magnetar_calckey_model::ck; use magnetar_calckey_model::ck;
use magnetar_sdk::types::drive::{DriveFileBase, PackDriveFileBase}; use magnetar_sdk::types::drive::{DriveFileBase, PackDriveFileBase};
use magnetar_sdk::types::Id; use magnetar_sdk::types::Id;
use magnetar_sdk::{Packed, Required}; use magnetar_sdk::{Packed, Required};
use tracing::warn;
use url::Url;
pub struct DriveModel; pub struct DriveModel;
impl DriveModel { impl DriveModel {
pub fn media_proxy_url(
&self,
ctx: &PackingContext,
url: &str,
is_thumbnail: bool,
) -> Option<Url> {
if let Some(proxy) = &ctx.service.config.networking.media_proxy {
let params = if is_thumbnail {
vec![("url", url), ("thumbnail", "1")]
} else {
vec![("url", url)]
};
let url = Url::parse_with_params(proxy, &params);
if let Err(e) = url {
warn!("Url parse error: {e}");
}
return url.ok();
}
None
}
pub fn builtin_proxy_file(
&self,
ctx: &PackingContext,
file: &ck::drive_file::Model,
is_thumbnail: bool,
) -> Option<Url> {
let key = if is_thumbnail {
file.thumbnail_access_key.as_deref()
} else {
file.webpublic_access_key.as_deref()
};
if let Some(k) = key {
if k != "/" {
let url_raw = format!(
"{}://{}/files/{}",
ctx.service.config.networking.protocol.as_ref(),
&ctx.service.config.networking.host,
k
);
let url = Url::parse(&url_raw);
if let Err(e) = url {
warn!("Url parse error: {e}");
}
return url.ok();
}
}
None
}
pub fn get_public_url(
&self,
ctx: &PackingContext,
file: &ck::drive_file::Model,
is_thumbnail: bool,
) -> Option<Url> {
if let Some(uri) = &file.uri {
if file.user_host.is_none() {
if let Some(media_proxy_url) = self.media_proxy_url(ctx, uri, is_thumbnail) {
return Some(media_proxy_url);
}
if file.is_link && ctx.service.config.networking.proxy_remote_files {
if let Some(proxy_url) = self.builtin_proxy_file(ctx, file, is_thumbnail) {
return Some(proxy_url);
}
}
}
}
let is_image = matches!(
file.r#type.as_str(),
"image/png"
| "image/apng"
| "image/gif"
| "image/jpeg"
| "image/webp"
| "image/svg+xml"
| "image/avif"
);
let url_raw = if is_thumbnail {
file.thumbnail_url
.as_deref()
.or(is_image.then_some(file.webpublic_url.as_deref().unwrap_or(file.url.as_str())))
} else {
file.webpublic_url.as_deref().or(Some(file.url.as_str()))
};
if let Some(u) = url_raw {
let url = Url::parse(u);
if let Err(e) = url {
warn!("Url parse error: {e}");
}
return url.ok();
}
None
}
pub fn pack_existing( pub fn pack_existing(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
file: &ck::drive_file::Model, file: &ck::drive_file::Model,
) -> PackDriveFileBase { ) -> PackDriveFileBase {
let url = self.get_public_url(ctx, file, false);
let thumbnail_url = self.get_public_url(ctx, file, false);
PackDriveFileBase::pack_from(( PackDriveFileBase::pack_from((
Required(Id::from(&file.id)), Required(Id::from(&file.id)),
Required(DriveFileBase::extract(ctx, &file)), Required(DriveFileBase::extract(
ctx,
PackFileBaseInput {
file: &file,
effective_url: url.as_ref(),
effective_thumbnail_url: thumbnail_url.as_ref(),
},
)),
)) ))
} }

View File

@ -1,5 +1,6 @@
use crate::service::emoji_cache::EmojiCacheError; use crate::service::emoji_cache::EmojiCacheError;
use crate::service::generic_id_cache::GenericIdCacheError; use crate::service::generic_id_cache::GenericIdCacheError;
use crate::service::instance_cache::RemoteInstanceCacheError;
use crate::service::instance_meta_cache::InstanceMetaCacheError; use crate::service::instance_meta_cache::InstanceMetaCacheError;
use magnetar_calckey_model::sea_orm::DbErr; use magnetar_calckey_model::sea_orm::DbErr;
use magnetar_calckey_model::CalckeyDbError; use magnetar_calckey_model::CalckeyDbError;
@ -23,8 +24,12 @@ pub enum PackError {
InstanceMetaCacheError(#[from] InstanceMetaCacheError), InstanceMetaCacheError(#[from] InstanceMetaCacheError),
#[error("Generic cache error: {0}")] #[error("Generic cache error: {0}")]
GenericCacheError(#[from] GenericIdCacheError), GenericCacheError(#[from] GenericIdCacheError),
#[error("Remote instance cache error: {0}")]
RemoteInstanceCacheError(#[from] RemoteInstanceCacheError),
#[error("Deserializer error: {0}")] #[error("Deserializer error: {0}")]
DeserializerError(#[from] serde_json::Error), DeserializerError(#[from] serde_json::Error),
#[error("URL parse error: {0}")]
UrlParseError(#[from] url::ParseError),
} }
pub type PackResult<T> = Result<T, PackError>; pub type PackResult<T> = Result<T, PackError>;

View File

@ -1,13 +1,18 @@
use crate::model::data::user::UserBaseSource; use crate::model::data::user::UserBaseSource;
use crate::model::processing::drive::DriveModel;
use crate::model::processing::emoji::EmojiModel; use crate::model::processing::emoji::EmojiModel;
use crate::model::processing::{get_mm_token_emoji, PackResult}; use crate::model::processing::{get_mm_token_emoji, PackResult};
use crate::model::{PackType, PackingContext}; use crate::model::{PackType, PackingContext};
use magnetar_calckey_model::ck; use magnetar_calckey_model::ck;
use magnetar_sdk::mmm::Token; use magnetar_sdk::mmm::Token;
use magnetar_sdk::types::drive::PackDriveFileBase;
use magnetar_sdk::types::emoji::EmojiContext; use magnetar_sdk::types::emoji::EmojiContext;
use magnetar_sdk::types::instance::InstanceTicker;
use magnetar_sdk::types::user::{PackUserBase, UserBase}; use magnetar_sdk::types::user::{PackUserBase, UserBase};
use magnetar_sdk::types::{Id, MmXml}; use magnetar_sdk::types::{Id, MmXml};
use magnetar_sdk::{mmm, Packed, Required}; use magnetar_sdk::{mmm, Packed, Required};
use tracing::warn;
use url::Url;
pub struct UserModel; pub struct UserModel;
@ -16,15 +21,48 @@ impl UserModel {
mmm::Context::default().parse_ui(user.name.as_deref().unwrap_or(&user.username)) mmm::Context::default().parse_ui(user.name.as_deref().unwrap_or(&user.username))
} }
pub fn get_effective_avatar_url(
&self,
ctx: &PackingContext,
user: &ck::user::Model,
avatar: Option<&PackDriveFileBase>,
) -> PackResult<Url> {
Ok(avatar
.and_then(
|PackDriveFileBase {
file: Required(base),
..
}| base.thumbnail_url.as_deref(),
)
.map(Url::parse)
.and_then(|r| {
if let Err(e) = r {
warn!("Failed to parse avatar URL: {e}");
}
r.ok().map(Ok)
})
.unwrap_or_else(|| {
Url::parse(&format!(
"{}://{}/identicon/{}",
ctx.service.config.networking.protocol,
ctx.service.config.networking.host,
user.id
))
})?)
}
pub async fn base_from_existing( pub async fn base_from_existing(
&self, &self,
ctx: &PackingContext, ctx: &PackingContext,
user: &ck::user::Model, user: &ck::user::Model,
) -> PackResult<PackUserBase> { ) -> PackResult<PackUserBase> {
let drive_file_pack = DriveModel;
let avatar = match &user.avatar_id { let avatar = match &user.avatar_id {
Some(av_id) => ctx.service.drive_file_cache.get(av_id).await?, Some(av_id) => drive_file_pack.get_cached_base(ctx, av_id).await?,
None => None, None => None,
}; };
let avatar_url = &self.get_effective_avatar_url(ctx, user, avatar.as_ref())?;
let username_mm = self.tokenize_username(&user); let username_mm = self.tokenize_username(&user);
@ -33,6 +71,16 @@ impl UserModel {
let emojis = emoji_model let emojis = emoji_model
.fetch_many_emojis(ctx, &shortcodes, user.host.as_deref()) .fetch_many_emojis(ctx, &shortcodes, user.host.as_deref())
.await?; .await?;
let instance = ctx
.service
.remote_instance_cache
.get(
user.host
.as_deref()
.unwrap_or(&ctx.service.config.networking.host),
)
.await?
.map(|i| InstanceTicker::extract(ctx, i.as_ref()));
let emoji_context = EmojiContext(emojis); let emoji_context = EmojiContext(emojis);
let base = UserBase::extract( let base = UserBase::extract(
@ -40,8 +88,10 @@ impl UserModel {
UserBaseSource { UserBaseSource {
user, user,
username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(), username_mm: mmm::to_xml_string(&username_mm).map(MmXml).as_ref().ok(),
avatar: avatar.as_deref(), avatar_url,
avatar: avatar.as_ref(),
emoji_context: &emoji_context, emoji_context: &emoji_context,
instance: instance.as_ref(),
}, },
); );

View File

@ -0,0 +1,89 @@
use crate::web::ApiError;
use lru::LruCache;
use magnetar_calckey_model::{ck, CalckeyDbError, CalckeyModel};
use std::sync::Arc;
use std::time::{Duration, Instant};
use strum::EnumVariantNames;
use thiserror::Error;
use tokio::sync::Mutex;
#[derive(Debug, Error, EnumVariantNames)]
pub enum RemoteInstanceCacheError {
#[error("Database error: {0}")]
DbError(#[from] CalckeyDbError),
}
impl From<RemoteInstanceCacheError> for ApiError {
fn from(err: RemoteInstanceCacheError) -> Self {
let mut api_error: ApiError = match err {
RemoteInstanceCacheError::DbError(err) => err.into(),
};
api_error.message = format!("Remote instance cache error: {}", api_error.message);
api_error
}
}
#[derive(Debug)]
struct CacheEntry {
created: Instant,
data: Arc<ck::instance::Model>,
}
impl CacheEntry {
fn new(data: Arc<ck::instance::Model>) -> Self {
Self {
created: Instant::now(),
data,
}
}
}
pub struct RemoteInstanceCacheService {
cache: Mutex<LruCache<String, CacheEntry>>,
lifetime_max: Duration,
db: CalckeyModel,
}
impl RemoteInstanceCacheService {
pub(super) fn new(db: CalckeyModel, cache_size: usize, entry_lifetime: Duration) -> Self {
const CACHE_SIZE: usize = 256;
Self {
cache: Mutex::new(LruCache::new(
cache_size
.try_into()
.unwrap_or(CACHE_SIZE.try_into().unwrap()),
)),
lifetime_max: entry_lifetime,
db,
}
}
pub async fn get(
&self,
host: &str,
) -> Result<Option<Arc<ck::instance::Model>>, RemoteInstanceCacheError> {
let mut read = self.cache.lock().await;
if let Some(item) = read.peek(host) {
if item.created + self.lifetime_max >= Instant::now() {
let data = item.data.clone();
read.promote(host);
return Ok(Some(data));
}
}
drop(read);
let val = self.db.get_instance(host).await?;
if val.is_none() {
return Ok(None);
}
let mut write = self.cache.lock().await;
let data = Arc::new(val.unwrap());
write.put(host.to_string(), CacheEntry::new(data.clone()));
Ok(Some(data))
}
}

View File

@ -6,15 +6,19 @@ use thiserror::Error;
pub mod emoji_cache; pub mod emoji_cache;
pub mod generic_id_cache; pub mod generic_id_cache;
pub mod instance_cache;
pub mod instance_meta_cache; pub mod instance_meta_cache;
pub mod local_user_cache; pub mod local_user_cache;
#[non_exhaustive]
pub struct MagnetarService { pub struct MagnetarService {
pub db: CalckeyModel, pub db: CalckeyModel,
pub cache: CalckeyCache, pub cache: CalckeyCache,
pub config: &'static MagnetarConfig, pub config: &'static MagnetarConfig,
pub local_user_cache: local_user_cache::LocalUserCacheService, pub local_user_cache: local_user_cache::LocalUserCacheService,
pub instance_meta_cache: instance_meta_cache::InstanceMetaCacheService, pub instance_meta_cache: instance_meta_cache::InstanceMetaCacheService,
pub remote_instance_cache: instance_cache::RemoteInstanceCacheService,
pub emoji_cache: emoji_cache::EmojiCacheService, pub emoji_cache: emoji_cache::EmojiCacheService,
pub drive_file_cache: generic_id_cache::GenericIdCacheService<ck::drive_file::Entity>, pub drive_file_cache: generic_id_cache::GenericIdCacheService<ck::drive_file::Entity>,
} }
@ -45,6 +49,11 @@ impl MagnetarService {
local_user_cache::LocalUserCacheService::new(config, db.clone(), cache.clone()).await?; local_user_cache::LocalUserCacheService::new(config, db.clone(), cache.clone()).await?;
let instance_meta_cache = instance_meta_cache::InstanceMetaCacheService::new(db.clone()); let instance_meta_cache = instance_meta_cache::InstanceMetaCacheService::new(db.clone());
let emoji_cache = emoji_cache::EmojiCacheService::new(db.clone()); let emoji_cache = emoji_cache::EmojiCacheService::new(db.clone());
let remote_instance_cache = instance_cache::RemoteInstanceCacheService::new(
db.clone(),
256,
Duration::from_secs(100),
);
let drive_file_cache = let drive_file_cache =
generic_id_cache::GenericIdCacheService::new(db.clone(), 128, Duration::from_secs(10)); generic_id_cache::GenericIdCacheService::new(db.clone(), 128, Duration::from_secs(10));
@ -54,6 +63,7 @@ impl MagnetarService {
config, config,
local_user_cache, local_user_cache,
instance_meta_cache, instance_meta_cache,
remote_instance_cache,
emoji_cache, emoji_cache,
drive_file_cache, drive_file_cache,
}) })