From 83ba4fe37a1184b998be09b9cbe53a22c7ba9e3b Mon Sep 17 00:00:00 2001 From: alyx Date: Mon, 1 Apr 2024 20:27:44 -0400 Subject: Move caching to src/cache/; Finalize API parsing fixes Font and user cache code has been moved to special files, independant from src/config.rs API parsing changes have been properly tested, and last.fm API JSON is now trace-logged for debugging convenience. --- src/cache/font.rs | 58 +++++++++++++++++++++++++++++++ src/cache/user.rs | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 src/cache/font.rs create mode 100644 src/cache/user.rs (limited to 'src/cache') diff --git a/src/cache/font.rs b/src/cache/font.rs new file mode 100644 index 0000000..c17247d --- /dev/null +++ b/src/cache/font.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; +use std::time::Duration; +use std::sync::LazyLock; + +use tokio::sync::RwLock; +use reqwest::{StatusCode, Client}; + +use super::{CacheFuture, CacheGetter, Cache, AsyncCache}; +use crate::CONFIG; + +#[derive(serde::Deserialize, Debug, Default)] +#[serde(default)] +#[serde(rename = "kebab-case")] +pub struct FontQuery { + pub font: Option>, + pub include_font: Option>, + pub google_font: Option>, +// pub small_font: Option<()> +} + +pub type FontFuture = CacheFuture>; +pub type FontGetter = CacheGetter>; +pub type FontCache = Cache>; + +fn font_getter(fontname: &String) -> FontFuture { + let fontname = urlencoding::encode(fontname.as_ref()).to_string(); + Box::pin(async move { + let Some(google_api_key) = CONFIG.google_api_key.clone() + else { + unreachable!(); + }; + + let fontreq = HTTP.get(format!("https://www.googleapis.com/webfonts/v1/webfonts?key={}&family={fontname}", google_api_key)) + .send().await + .map_err(|e| {log::error!("Failed to get info for font `{fontname}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to Google Fonts!")})?; + if fontreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "Font does not exist!")); } + if fontreq.status() == StatusCode::FORBIDDEN { + log::error!("Invalid Google API key in config!"); + return Err((StatusCode::SERVICE_UNAVAILABLE, "This instance is not configured to support Google Fonts properly, please use a different font.")); + } + + let cssreq = HTTP.get(format!("https://fonts.googleapis.com/css2?family={fontname}")) + .send().await + .map_err(|e| {log::error!("Failed to get CSS for font `{fontname}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't download font CSS!")})?; + + Ok(cssreq.text().await.unwrap().into()) + }) +} + +static HTTP: LazyLock = crate::http::lazy(); + +static FONT_CACHE: LazyLock = LazyLock::new(|| { + Arc::new(RwLock::new(AsyncCache::new(Duration::from_secs(86400), font_getter as FontGetter))) +}); + +pub async fn get_fontinfo(font: &String) -> Result, (StatusCode, &'static str)> { + FONT_CACHE.write().await.get_owned(font).await +} diff --git a/src/cache/user.rs b/src/cache/user.rs new file mode 100644 index 0000000..c3b6f04 --- /dev/null +++ b/src/cache/user.rs @@ -0,0 +1,101 @@ +use std::sync::Arc; +use std::time::Duration; +use std::sync::LazyLock; + +use tokio::sync::RwLock; +use reqwest::{StatusCode, Client}; + +use super::{CacheFuture, CacheGetter, Cache, AsyncCache}; +use crate::deserialize::{User, Track, TrackStub}; +use crate::CONFIG; + +type UserFuture = CacheFuture>; +type UserGetter = CacheGetter>; +type UserCache = Cache>; + +#[derive(Debug)] +enum Whitelist { + Exclusive{cache: UserCache}, + Open{default_cache: UserCache, whitelist_cache: UserCache} +} + +fn user_getter(username: &String) -> UserFuture { + use crate::deserialize::{GetUserInfo, GetRecentTracks, GetTrackInfo}; + + let username = urlencoding::encode(username.as_ref()).to_string(); + Box::pin(async move { + let userreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=user.getInfo&format=json&user={username}&api_key={}", CONFIG.lastfm_api_key)) + .send().await + .map_err(|e| {log::error!("Failed to get info for user `{username}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to last.fm!")})?; + if userreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "User does not exist!")); } + + let userstr = userreq.text().await.unwrap(); + log::trace!("Got user.getUserInfo JSON for `{username}`: {userstr}"); + let userinfo = serde_json::from_str::(&userstr) + .map_err(|e| {log::error!("Couldn't parse user.getInfo for `{username}`: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse user.getInfo!")})?.user; + + let tracksreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=user.getRecentTracks&format=json&extended=1&limit=1&user={username}&api_key={}", CONFIG.lastfm_api_key)) + .send().await + .map_err(|e| {log::error!("Failed to get tracks for user `{username}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to last.fm!")})?; + if tracksreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "User does not exist!")); } + if tracksreq.status() == StatusCode::FORBIDDEN { return Err((StatusCode::FORBIDDEN, "You need to unprivate your song history!")); } + + let tracksstr = tracksreq.text().await.unwrap(); + log::trace!("Got user.getRecentTracks JSON for `{username}`: {tracksstr}"); + let trackstub = serde_json::from_str::(&tracksstr) + .map_err(|e| {log::error!("Couldn't parse user.getRecentTracks for `{username}`: {e}"); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse user.getRecentTracks!")})? + .recenttracks.track.into_iter().next().ok_or((StatusCode::UNPROCESSABLE_ENTITY, "You need to listen to some songs first!"))?; + + let trackreq = HTTP.get(format!("https://ws.audioscrobbler.com/2.0/?method=track.getInfo&format=json&username={username}&api_key={}&track={}&artist={}", CONFIG.lastfm_api_key, trackstub.name, trackstub.artist.name)) + .send().await + .map_err(|e| {log::error!("Failed to get tracks for user `{username}`: {e}"); (StatusCode::SERVICE_UNAVAILABLE, "Couldn't connect to last.fm!")})?; + if trackreq.status() == StatusCode::NOT_FOUND { return Err((StatusCode::NOT_FOUND, "Track does not exist!")); } + + let trackstr = trackreq.text().await.unwrap(); + log::trace!("Got user.getTrackInfo JSON for `{username}`: {trackstr}"); + let trackinfo = serde_json::from_str::(&trackstr) + .map_err(|e| {log::error!("Couldn't parse track.getInfo for `{}` by `{}` on behalf of {username}: {e}", trackstub.name, trackstub.artist.name); (StatusCode::INTERNAL_SERVER_ERROR, "Couldn't parse track.getInfo!")})?.track; + + Ok(Arc::new((userinfo, trackinfo, trackstub))) + }) +} + +static HTTP: LazyLock = crate::http::lazy(); + +static WHITELIST: LazyLock = LazyLock::new(|| { + let default_cache = Arc::new(RwLock::new(AsyncCache::new(CONFIG.default_refresh, user_getter as UserGetter))); + let whitelist_cache = Arc::new(RwLock::new(AsyncCache::new(CONFIG.whitelist_refresh, user_getter as UserGetter))); + match CONFIG.whitelist_mode.as_str() { + "open" => { + Whitelist::Open{default_cache, whitelist_cache} + }, + "exclusive" => { + if CONFIG.whitelist.is_empty() { + panic!("Exclusive mode set with empty whitelist, cannot serve any requests!"); + } + Whitelist::Exclusive{cache: whitelist_cache} + }, + m => { + panic!("Bad whitelist mode: `{m}`"); + } + } +}); + +pub async fn get_userinfo(user: &String) -> (Result, (StatusCode, &'static str)>, Duration) { + match LazyLock::force(&WHITELIST) { + Whitelist::Open{default_cache, whitelist_cache} => { + if CONFIG.whitelist.contains(user) { + (whitelist_cache.write().await.get_owned(user).await, CONFIG.whitelist_refresh) + } + else { + (default_cache.write().await.get_owned(user).await, CONFIG.default_refresh) + } + }, + Whitelist::Exclusive{cache} => { + if CONFIG.whitelist.contains(user) { + (cache.write().await.get_owned(user).await, CONFIG.whitelist_refresh) + } + else { (Err((StatusCode::FORBIDDEN, "User not in whitelist!")), CONFIG.default_refresh) } + } + } +} -- cgit v1.2.3-70-g09d2