diff options
| -rw-r--r-- | Cargo.lock | 58 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | common/src/api.rs | 3 | ||||
| -rw-r--r-- | server/Cargo.toml | 2 | ||||
| -rw-r--r-- | server/src/api.rs | 8 | ||||
| -rw-r--r-- | server/src/compat/youtube.rs | 74 | ||||
| -rw-r--r-- | server/src/logic/playersync.rs | 6 | ||||
| -rw-r--r-- | server/src/logic/stream.rs | 133 | ||||
| -rw-r--r-- | server/src/logic/userdata.rs | 101 | ||||
| -rw-r--r-- | server/src/main.rs | 2 | ||||
| -rw-r--r-- | server/src/request_info.rs | 9 | ||||
| -rw-r--r-- | server/src/routes.rs | 92 | ||||
| -rw-r--r-- | server/src/ui/account/mod.rs | 22 | ||||
| -rw-r--r-- | server/src/ui/assets.rs | 77 | ||||
| -rw-r--r-- | server/src/ui/error.rs | 36 | ||||
| -rw-r--r-- | server/src/ui/mod.rs | 79 | ||||
| -rw-r--r-- | server/src/ui/node.rs | 44 | ||||
| -rw-r--r-- | server/src/ui/search.rs | 41 | ||||
| -rw-r--r-- | server/src/ui/stats.rs | 20 | ||||
| -rw-r--r-- | ui/Cargo.toml | 2 | ||||
| -rw-r--r-- | ui/src/components/login.rs | 43 | ||||
| -rw-r--r-- | ui/src/components/mod.rs | 16 | ||||
| -rw-r--r-- | ui/src/old/account/mod.rs | 103 |
23 files changed, 392 insertions, 580 deletions
@@ -46,20 +46,6 @@ dependencies = [ ] [[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] name = "aes-gcm-siv" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -666,13 +652,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ - "aes-gcm", - "base64", - "hkdf", "percent-encoding", - "rand 0.8.5", - "sha2", - "subtle", "time", "version_check", ] @@ -1257,16 +1237,6 @@ dependencies = [ ] [[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] name = "gif" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1337,24 +1307,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] name = "http" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2292,16 +2244,18 @@ dependencies = [ [[package]] name = "markup" -version = "0.15.0" -source = "git+https://github.com/metamuffin/markup.rs?rev=2ee9aee#2ee9aeeb7654ede4dbdd9c9bc7f57cab888ef12f" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be10fa8085360758d551c6577b0692c9e7614d2d743036cf2fa452476a3e0569" dependencies = [ "markup-proc-macro", ] [[package]] name = "markup-proc-macro" -version = "0.15.0" -source = "git+https://github.com/metamuffin/markup.rs?rev=2ee9aee#2ee9aeeb7654ede4dbdd9c9bc7f57cab888ef12f" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee486981a2fe832d4deb4d23a35508c111ef7070758177ce3580eb2a384fab2f" dependencies = [ "proc-macro2", "quote", @@ -21,6 +21,7 @@ members = [ "cache/tools", ] resolver = "3" +default-members = ["server"] [workspace.dependencies] log = "0.4.28" diff --git a/common/src/api.rs b/common/src/api.rs index 9040852..415ef76 100644 --- a/common/src/api.rs +++ b/common/src/api.rs @@ -20,6 +20,9 @@ fields! { VIEW_PLAYER: u64 = 2028 "player"; VIEW_STATGROUP: Object = 2041 "statgroup"; VIEW_STATTEXT: Object = 2042 "stattext"; + VIEW_ACCOUNT_LOGIN: () = 2043 "account_login"; + VIEW_ACCOUNT_LOGOUT: () = 2043 "account_logout"; + VIEW_ACCOUNT_SET_PASSWORD: &str = 2044 "account_set_password"; NKU_NODE: Object = 2025 "node"; NKU_UDATA: Object = 2026 "udata"; diff --git a/server/Cargo.toml b/server/Cargo.toml index 534dce5..f12b8fe 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -25,7 +25,7 @@ env_logger = "0.11.8" futures = "0.3.31" log = { workspace = true } rand = "0.9.2" -rocket = { version = "0.5", features = ["secrets", "json"] } +rocket = { version = "0.5", features = ["json"] } rocket_ws = "0.1" serde = { version = "1.0.228", features = ["derive", "rc"] } serde_json = "1.0.145" diff --git a/server/src/api.rs b/server/src/api.rs index 2b3d016..9d16433 100644 --- a/server/src/api.rs +++ b/server/src/api.rs @@ -4,6 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use super::ui::error::MyResult; +use crate::request_info::RequestInfo; use rocket::{get, response::Redirect, serde::json::Json}; #[get("/api")] @@ -38,7 +39,8 @@ pub fn r_version() -> &'static str { // } #[get("/nodes_modified?<since>")] -pub fn r_nodes_modified_since(session: A<Session>, since: u64) -> MyResult<Json<Vec<NodeID>>> { - let nodes = get_nodes_modified_since(&session.0, since)?; - Ok(Json(nodes)) +pub fn r_nodes_modified_since(ri: RequestInfo<'_>, since: u64) -> MyResult<Json<Vec<String>>> { + // let nodes = get_nodes_modified_since(&session.0, since)?; + // Ok(Json(nodes)) + todo!() } diff --git a/server/src/compat/youtube.rs b/server/src/compat/youtube.rs index 5e86014..e511d9b 100644 --- a/server/src/compat/youtube.rs +++ b/server/src/compat/youtube.rs @@ -3,53 +3,47 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{request_info::A, ui::error::MyResult}; -use anyhow::anyhow; -use jellycommon::{ - routes::{u_node_slug, u_node_slug_player}, - IdentifierType, -}; -use jellylogic::{ - node::{get_node_by_eid, node_id_to_slug}, - session::Session, -}; +use crate::{request_info::RequestInfo, ui::error::MyResult}; use rocket::{get, response::Redirect}; #[get("/watch?<v>")] -pub fn r_youtube_watch(session: A<Session>, v: &str) -> MyResult<Redirect> { - if v.len() != 11 { - Err(anyhow!("video id length incorrect"))? - } - let Some(id) = get_node_by_eid(&session.0, IdentifierType::YoutubeVideo, v)? else { - Err(anyhow!("element not found"))? - }; - let slug = node_id_to_slug(&session.0, id)?; - Ok(Redirect::to(u_node_slug_player(&slug))) +pub fn r_youtube_watch(ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> { + // if v.len() != 11 { + // Err(anyhow!("video id length incorrect"))? + // } + // let Some(id) = get_node_by_eid(&session.0, IdentifierType::YoutubeVideo, v)? else { + // Err(anyhow!("element not found"))? + // }; + // let slug = node_id_to_slug(&session.0, id)?; + // Ok(Redirect::to(u_node_slug_player(&slug))) + todo!() } #[get("/channel/<id>")] -pub fn r_youtube_channel(session: A<Session>, id: &str) -> MyResult<Redirect> { - let Some(id) = (if id.starts_with("UC") { - get_node_by_eid(&session.0, IdentifierType::YoutubeChannel, id)? - } else if id.starts_with("@") { - get_node_by_eid(&session.0, IdentifierType::YoutubeChannelHandle, id)? - } else { - Err(anyhow!("unknown channel id format"))? - }) else { - Err(anyhow!("channel not found"))? - }; - let slug = node_id_to_slug(&session.0, id)?; - Ok(Redirect::to(u_node_slug(&slug))) +pub fn r_youtube_channel(ri: RequestInfo<'_>, id: &str) -> MyResult<Redirect> { + // let Some(id) = (if id.starts_with("UC") { + // get_node_by_eid(&session.0, IdentifierType::YoutubeChannel, id)? + // } else if id.starts_with("@") { + // get_node_by_eid(&session.0, IdentifierType::YoutubeChannelHandle, id)? + // } else { + // Err(anyhow!("unknown channel id format"))? + // }) else { + // Err(anyhow!("channel not found"))? + // }; + // let slug = node_id_to_slug(&session.0, id)?; + // Ok(Redirect::to(u_node_slug(&slug))) + todo!() } #[get("/embed/<v>")] -pub fn r_youtube_embed(session: A<Session>, v: &str) -> MyResult<Redirect> { - if v.len() != 11 { - Err(anyhow!("video id length incorrect"))? - } - let Some(id) = get_node_by_eid(&session.0, IdentifierType::YoutubeVideo, v)? else { - Err(anyhow!("element not found"))? - }; - let slug = node_id_to_slug(&session.0, id)?; - Ok(Redirect::to(u_node_slug_player(&slug))) +pub fn r_youtube_embed(ri: RequestInfo<'_>, v: &str) -> MyResult<Redirect> { + // if v.len() != 11 { + // Err(anyhow!("video id length incorrect"))? + // } + // let Some(id) = get_node_by_eid(&session.0, IdentifierType::YoutubeVideo, v)? else { + // Err(anyhow!("element not found"))? + // }; + // let slug = node_id_to_slug(&session.0, id)?; + // Ok(Redirect::to(u_node_slug_player(&slug))) + todo!() } diff --git a/server/src/logic/playersync.rs b/server/src/logic/playersync.rs index 6c1f9f4..71e2809 100644 --- a/server/src/logic/playersync.rs +++ b/server/src/logic/playersync.rs @@ -2,12 +2,12 @@ use anyhow::bail; use chashmap::CHashMap; use futures::{SinkExt, StreamExt}; use log::warn; -use rocket::{get, State}; -use rocket_ws::{stream::DuplexStream, Channel, Message, WebSocket}; +use rocket::{State, get}; +use rocket_ws::{Channel, Message, WebSocket, stream::DuplexStream}; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast::{self, Sender}; -use crate::request_info::cors::Cors; +use crate::responders::cors::Cors; #[derive(Default)] pub struct PlayersyncChannels { diff --git a/server/src/logic/stream.rs b/server/src/logic/stream.rs index 55d6850..430c10c 100644 --- a/server/src/logic/stream.rs +++ b/server/src/logic/stream.rs @@ -3,18 +3,16 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{request_info::A, ui::error::MyError}; -use anyhow::{anyhow, Result}; -use jellycommon::{api::NodeFilterSort, stream::StreamSpec, NodeID, TrackSource}; -use jellylogic::{node::get_node, session::Session}; +use crate::{request_info::RequestInfo, ui::error::MyError}; +use anyhow::{Result, anyhow}; +use jellycommon::stream::StreamSpec; use jellystream::SMediaInfo; use log::{info, warn}; use rocket::{ - get, head, + Either, Request, Response, get, head, http::{Header, Status}, request::{self, FromRequest}, response::{self, Redirect, Responder}, - Either, Request, Response, }; use std::{ collections::{BTreeMap, BTreeSet}, @@ -22,14 +20,14 @@ use std::{ sync::Arc, }; use tokio::{ - io::{duplex, DuplexStream}, + io::{DuplexStream, duplex}, task::spawn_blocking, }; use tokio_util::io::SyncIoBridge; #[head("/n/<_id>/stream?<spec..>")] pub async fn r_stream_head( - _sess: A<Session>, + _sess: RequestInfo<'_>, _id: &str, spec: BTreeMap<String, String>, ) -> Result<Either<StreamResponse, Redirect>, MyError> { @@ -45,27 +43,27 @@ pub async fn r_stream_head( #[get("/n/<id>/stream?<spec..>")] pub async fn r_stream( - session: A<Session>, + session: RequestInfo<'_>, id: &str, range: Option<RequestRange>, spec: BTreeMap<String, String>, ) -> Result<Either<StreamResponse, RedirectResponse>, MyError> { let spec = StreamSpec::from_query_kv(&spec).map_err(|x| anyhow!("spec invalid: {x}"))?; - // TODO perm - let node = get_node( - &session.0, - NodeID::from_slug(id), - false, - false, - NodeFilterSort::default(), - )? - .node; + // // TODO perm + // let node = get_node( + // &session.0, + // NodeID::from_slug(id), + // false, + // false, + // NodeFilterSort::default(), + // )? + // .node; - let media = Arc::new( - node.media - .clone() - .ok_or(anyhow!("item does not contain media"))?, - ); + // let media = Arc::new( + // node.media + // .clone() + // .ok_or(anyhow!("item does not contain media"))?, + // ); // TODO its unclear how requests with multiple tracks should be handled. // if spec.track.len() == 1 { @@ -115,55 +113,56 @@ pub async fn r_stream( // } // } - info!( - "stream request (range={})", - range - .as_ref() - .map(|r| r.to_cr_hv()) - .unwrap_or("none".to_string()) - ); + // info!( + // "stream request (range={})", + // range + // .as_ref() + // .map(|r| r.to_cr_hv()) + // .unwrap_or("none".to_string()) + // ); - let urange = match &range { - Some(r) => { - let r = r.0.first().unwrap_or(&(None..None)); - r.start.unwrap_or(0)..r.end.unwrap_or(u64::MAX) - } - None => 0..u64::MAX, - }; + // let urange = match &range { + // Some(r) => { + // let r = r.0.first().unwrap_or(&(None..None)); + // r.start.unwrap_or(0)..r.end.unwrap_or(u64::MAX) + // } + // None => 0..u64::MAX, + // }; - let head = jellystream::stream_head(&spec); + // let head = jellystream::stream_head(&spec); - let mut sources = BTreeSet::new(); - for t in &media.tracks { - if let TrackSource::Local(path, _) = &t.source { - sources.insert(path.to_owned()); - } - } - let media = Arc::new(SMediaInfo { - files: sources, - title: node.title.clone(), - }); + // let mut sources = BTreeSet::new(); + // for t in &media.tracks { + // if let TrackSource::Local(path, _) = &t.source { + // sources.insert(path.to_owned()); + // } + // } + // let media = Arc::new(SMediaInfo { + // files: sources, + // title: node.title.clone(), + // }); - // TODO cleaner solution needed - let mut reader = match spawn_blocking(move || jellystream::stream(media, spec, urange)) - .await - .unwrap() - { - Ok(o) => o, - Err(e) => { - warn!("stream error: {e:?}"); - Err(e)? - } - }; - let (stream_write, stream_read) = duplex(4096); - spawn_blocking(move || std::io::copy(&mut reader, &mut SyncIoBridge::new(stream_write))); + // // TODO cleaner solution needed + // let mut reader = match spawn_blocking(move || jellystream::stream(media, spec, urange)) + // .await + // .unwrap() + // { + // Ok(o) => o, + // Err(e) => { + // warn!("stream error: {e:?}"); + // Err(e)? + // } + // }; + // let (stream_write, stream_read) = duplex(4096); + // spawn_blocking(move || std::io::copy(&mut reader, &mut SyncIoBridge::new(stream_write))); - Ok(Either::Left(StreamResponse { - stream: stream_read, - range, - advertise_range: head.range_supported, - content_type: head.content_type, - })) + // Ok(Either::Left(StreamResponse { + // stream: stream_read, + // range, + // advertise_range: head.range_supported, + // content_type: head.content_type, + // })) + todo!() } pub struct RedirectResponse(String); diff --git a/server/src/logic/userdata.rs b/server/src/logic/userdata.rs index 104de4a..ea96507 100644 --- a/server/src/logic/userdata.rs +++ b/server/src/logic/userdata.rs @@ -3,23 +3,10 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use crate::{request_info::A, ui::error::MyResult}; -use jellycommon::{ - api::NodeFilterSort, - routes::u_node_id, - user::{NodeUserData, WatchedState}, - NodeID, -}; -use jellylogic::{ - node::{ - get_node, update_node_userdata_rating, update_node_userdata_watched, - update_node_userdata_watched_progress, - }, - session::Session, -}; +use crate::ui::error::MyResult; use rocket::{ - form::Form, get, post, response::Redirect, serde::json::Json, FromForm, FromFormField, - UriDisplayQuery, + FromForm, FromFormField, UriDisplayQuery, form::Form, get, post, response::Redirect, + serde::json::Json, }; #[derive(Debug, FromFormField, UriDisplayQuery)] @@ -29,48 +16,48 @@ pub enum UrlWatchedState { Pending, } -#[get("/n/<id>/userdata")] -pub fn r_node_userdata(session: A<Session>, id: A<NodeID>) -> MyResult<Json<NodeUserData>> { - let u = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?.userdata; - Ok(Json(u)) -} +// #[get("/n/<id>/userdata")] +// pub fn r_node_userdata(session: A<Session>, id: A<NodeID>) -> MyResult<Json<NodeUserData>> { +// let u = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?.userdata; +// Ok(Json(u)) +// } -#[post("/n/<id>/watched?<state>")] -pub async fn r_node_userdata_watched( - session: A<Session>, - id: A<NodeID>, - state: UrlWatchedState, -) -> MyResult<Redirect> { - update_node_userdata_watched( - &session.0, - id.0, - match state { - UrlWatchedState::None => WatchedState::None, - UrlWatchedState::Watched => WatchedState::Watched, - UrlWatchedState::Pending => WatchedState::Pending, - }, - )?; - Ok(Redirect::found(u_node_id(id.0))) -} +// #[post("/n/<id>/watched?<state>")] +// pub async fn r_node_userdata_watched( +// session: A<Session>, +// id: A<NodeID>, +// state: UrlWatchedState, +// ) -> MyResult<Redirect> { +// update_node_userdata_watched( +// &session.0, +// id.0, +// match state { +// UrlWatchedState::None => WatchedState::None, +// UrlWatchedState::Watched => WatchedState::Watched, +// UrlWatchedState::Pending => WatchedState::Pending, +// }, +// )?; +// Ok(Redirect::found(u_node_id(id.0))) +// } -#[derive(FromForm)] -pub struct UpdateRating { - #[field(validate = range(-10..=10))] - rating: i32, -} +// #[derive(FromForm)] +// pub struct UpdateRating { +// #[field(validate = range(-10..=10))] +// rating: i32, +// } -#[post("/n/<id>/update_rating", data = "<form>")] -pub async fn r_node_userdata_rating( - session: A<Session>, - id: A<NodeID>, - form: Form<UpdateRating>, -) -> MyResult<Redirect> { - update_node_userdata_rating(&session.0, id.0, form.rating)?; - Ok(Redirect::found(u_node_id(id.0))) -} +// #[post("/n/<id>/update_rating", data = "<form>")] +// pub async fn r_node_userdata_rating( +// session: A<Session>, +// id: A<NodeID>, +// form: Form<UpdateRating>, +// ) -> MyResult<Redirect> { +// update_node_userdata_rating(&session.0, id.0, form.rating)?; +// Ok(Redirect::found(u_node_id(id.0))) +// } -#[post("/n/<id>/progress?<t>")] -pub async fn r_node_userdata_progress(session: A<Session>, id: A<NodeID>, t: f64) -> MyResult<()> { - update_node_userdata_watched_progress(&session.0, id.0, t)?; - Ok(()) -} +// #[post("/n/<id>/progress?<t>")] +// pub async fn r_node_userdata_progress(session: A<Session>, id: A<NodeID>, t: f64) -> MyResult<()> { +// update_node_userdata_watched_progress(&session.0, id.0, t)?; +// Ok(()) +// } diff --git a/server/src/main.rs b/server/src/main.rs index bd9901a..aab335a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -48,7 +48,7 @@ async fn main() { } } -pub(crate) struct State { +pub struct State { pub config: Config, pub cache: Cache, diff --git a/server/src/request_info.rs b/server/src/request_info.rs index 3468c58..0f2fd3a 100644 --- a/server/src/request_info.rs +++ b/server/src/request_info.rs @@ -9,7 +9,8 @@ use crate::{ auth::token_to_user, ui::error::{MyError, MyResult}, }; -use jellycommon::jellyobject::ObjectBuffer; +use anyhow::anyhow; +use jellycommon::jellyobject::{Object, ObjectBuffer}; use jellyui::RenderInfo; use rocket::{ Request, async_trait, @@ -46,6 +47,12 @@ impl<'a> RequestInfo<'a> { state: state.clone(), }) } + pub fn require_user(&'a self) -> MyResult<Object<'a>> { + self.user + .as_ref() + .map(|u| u.as_object()) + .ok_or(MyError(anyhow!("user required"))) + } pub fn render_info(&'a self) -> RenderInfo<'a> { RenderInfo { lang: self.lang, diff --git a/server/src/routes.rs b/server/src/routes.rs index 19c76b2..01d0081 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -10,32 +10,12 @@ use crate::{ logic::{ playersync::{PlayersyncChannels, r_playersync}, stream::r_stream, - userdata::{ - r_node_userdata, r_node_userdata_progress, r_node_userdata_rating, - r_node_userdata_watched, - }, }, ui::{ - account::{ - r_account_login, r_account_login_post, r_account_logout, r_account_logout_post, - r_account_register, r_account_register_post, - settings::{r_account_settings, r_account_settings_post}, - }, - admin::{ - import::{r_admin_import, r_admin_import_post, r_admin_import_stream}, - log::{r_admin_log, r_admin_log_stream}, - r_admin_dashboard, r_admin_invite, r_admin_remove_invite, r_admin_update_search, - user::{r_admin_remove_user, r_admin_user, r_admin_user_permission, r_admin_users}, - }, - assets::{r_image, r_item_poster, r_node_thumbnail}, + assets::r_image, error::{r_api_catch, r_catch}, - home::r_home, - items::r_items, node::r_node, - player::r_player, - r_favicon, r_index, - search::r_search, - stats::r_stats, + r_favicon, style::{r_assets_font, r_assets_js, r_assets_js_map, r_assets_style}, }, }; @@ -52,7 +32,7 @@ macro_rules! uri { }; } -pub fn build_rocket(state: Arc<State>) -> Rocket<Build> { +pub(super) fn build_rocket(state: Arc<State>) -> Rocket<Build> { rocket::build() .configure(Config { address: std::env::var("BIND_ADDR") @@ -84,47 +64,47 @@ pub fn build_rocket(state: Arc<State>) -> Rocket<Build> { "/", routes![ // Frontend - r_account_login_post, - r_account_login, - r_account_logout_post, - r_account_logout, - r_account_register_post, - r_account_register, - r_account_settings_post, - r_account_settings, - r_admin_dashboard, - r_admin_import, - r_admin_import_post, - r_admin_import_stream, - r_admin_invite, - r_admin_log_stream, - r_admin_log, - r_admin_remove_invite, - r_admin_remove_user, - r_admin_update_search, - r_admin_user_permission, - r_admin_user, - r_admin_users, - r_items, + // r_account_login_post, + // r_account_login, + // r_account_logout_post, + // r_account_logout, + // r_account_register_post, + // r_account_register, + // r_account_settings_post, + // r_account_settings, + // r_admin_dashboard, + // r_admin_import, + // r_admin_import_post, + // r_admin_import_stream, + // r_admin_invite, + // r_admin_log_stream, + // r_admin_log, + // r_admin_remove_invite, + // r_admin_remove_user, + // r_admin_update_search, + // r_admin_user_permission, + // r_admin_user, + // r_admin_users, + // r_items, r_image, r_assets_font, r_assets_js_map, r_assets_js, r_assets_style, r_favicon, - r_home, - r_index, - r_item_poster, + // r_home, + // r_index, + // r_item_poster, r_node, - r_node_thumbnail, - r_node_userdata_progress, - r_node_userdata_rating, - r_node_userdata_watched, - r_node_userdata, - r_player, + // r_node_thumbnail, + // r_node_userdata_progress, + // r_node_userdata_rating, + // r_node_userdata_watched, + // r_node_userdata, + // r_player, r_playersync, - r_search, - r_stats, + // r_search, + // r_stats, r_stream, // API r_nodes_modified_since, diff --git a/server/src/ui/account/mod.rs b/server/src/ui/account/mod.rs index 429b70a..ec8bd49 100644 --- a/server/src/ui/account/mod.rs +++ b/server/src/ui/account/mod.rs @@ -3,12 +3,17 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -pub mod settings; +// pub mod settings; use super::error::MyError; -use crate::ui::{error::MyResult, home::rocket_uri_macro_r_home}; +use crate::{ + request_info::RequestInfo, + ui::{error::MyResult, home::rocket_uri_macro_r_home}, +}; use anyhow::anyhow; +use jellycommon::{VIEW_ACCOUNT_LOGIN, jellyobject::Object}; use jellyimport::is_importing; +use jellyui::render_view; use rocket::{ FromForm, form::{Contextual, Form}, @@ -30,16 +35,9 @@ pub struct RegisterForm { } #[get("/account/register")] -pub async fn r_account_register(lang: AcceptLanguage) -> RawHtml<String> { - let AcceptLanguage(lang) = lang; - RawHtml(render_page( - &AccountRegister { lang: &lang }, - RenderInfo { - importing: false, - session: None, - lang, - }, - )) +pub async fn r_account_register(ri: RequestInfo<'_>) -> RawHtml<String> { + let ob = Object::EMPTY.insert(VIEW_ACCOUNT_LOGIN, ()); + RawHtml(render_view(ri.render_info(), ob.as_object())) } #[derive(FromForm, Serialize, Deserialize)] diff --git a/server/src/ui/assets.rs b/server/src/ui/assets.rs index 8f3fb4a..c956672 100644 --- a/server/src/ui/assets.rs +++ b/server/src/ui/assets.rs @@ -4,58 +4,57 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use super::error::MyResult; -use anyhow::{Context, anyhow}; -use rocket::{get, http::ContentType, response::Redirect}; -use std::str::FromStr; +use crate::{request_info::RequestInfo, responders::cache::CacheControlImage}; +use anyhow::Context; +use rocket::{get, http::ContentType}; +use std::path::PathBuf; pub const AVIF_QUALITY: u32 = 70; pub const AVIF_SPEED: u8 = 5; -#[get("/image/<key..>?<size>")] +#[get("/image/<path..>?<size>")] pub async fn r_image( - _session: A<Session>, - key: A<Asset>, + ri: RequestInfo<'_>, + path: PathBuf, size: Option<usize>, ) -> MyResult<(ContentType, CacheControlImage)> { let size = size.unwrap_or(2048); - - if !key.0.0.ends_with(".image") { - Err(anyhow!("request to non-image"))? - } + let path = path.to_string_lossy().to_string(); // fit the resolution into a finite set so the maximum cache is finite too. let width = 2usize.pow(size.clamp(128, 2048).ilog2()); - let encoded = jellytranscoder::image::transcode(&key.0.0, AVIF_QUALITY, AVIF_SPEED, width) - .context("transcoding asset")?; + let encoded = + jellytranscoder::image::transcode(&ri.state.cache, &path, AVIF_QUALITY, AVIF_SPEED, width) + .context("transcoding asset")?; Ok((ContentType::AVIF, CacheControlImage(encoded))) } -#[get("/n/<id>/image/<slot>?<size>")] -pub async fn r_item_poster( - session: A<Session>, - id: A<NodeID>, - slot: &str, - size: Option<usize>, -) -> MyResult<Redirect> { - let slot = PictureSlot::from_str(slot).map_err(|_| anyhow!("slot invalid"))?; - let node = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?; - let picture = node - .node - .pictures - .get(&slot) - .cloned() - .ok_or(anyhow!("no pic todo"))?; - Ok(Redirect::permanent(rocket::uri!(r_image(picture, size)))) -} +// #[get("/n/<id>/image/<slot>?<size>")] +// pub async fn r_item_poster( +// session: A<Session>, +// id: A<NodeID>, +// slot: &str, +// size: Option<usize>, +// ) -> MyResult<Redirect> { +// let slot = PictureSlot::from_str(slot).map_err(|_| anyhow!("slot invalid"))?; +// let node = get_node(&session.0, id.0, false, false, NodeFilterSort::default())?; +// let picture = node +// .node +// .pictures +// .get(&slot) +// .cloned() +// .ok_or(anyhow!("no pic todo"))?; +// Ok(Redirect::permanent(rocket::uri!(r_image(picture, size)))) +// } -#[get("/n/<id>/thumbnail?<t>&<size>")] -pub async fn r_node_thumbnail( - session: A<Session>, - id: A<NodeID>, - t: f64, - size: Option<usize>, -) -> MyResult<Redirect> { - let picture = get_node_thumbnail(&session.0, id.0, t).await?; - Ok(Redirect::permanent(rocket::uri!(r_image(picture, size)))) -} +// #[get("/n/<id>/thumbnail?<t>&<size>")] +// pub async fn r_node_thumbnail( +// session: A<Session>, +// id: A<NodeID>, +// t: f64, +// size: Option<usize>, +// ) -> MyResult<Redirect> { +// let picture = get_node_thumbnail(&session.0, id.0, t).await?; +// Ok(Redirect::permanent(rocket::uri!(r_image(picture, size)))) +// } diff --git a/server/src/ui/error.rs b/server/src/ui/error.rs index 0f279fc..578d841 100644 --- a/server/src/ui/error.rs +++ b/server/src/ui/error.rs @@ -3,38 +3,20 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use log::info; use rocket::{ - catch, - http::{ContentType, Status}, - response::{self, content::RawHtml, Responder}, - Request, + Request, catch, + http::Status, + response::{self, Responder, content::RawHtml}, }; -use serde_json::{json, Value}; -use std::{fmt::Display, fs::File, io::Read, sync::LazyLock}; - -static ERROR_IMAGE: LazyLock<Vec<u8>> = LazyLock::new(|| { - info!("loading error image"); - let mut f = File::open(CONF.asset_path.join("error.avif")) - .expect("please create error.avif in the asset dir"); - let mut o = Vec::new(); - f.read_to_end(&mut o).unwrap(); - o -}); +use serde_json::{Value, json}; +use std::fmt::Display; #[catch(default)] pub fn r_catch(status: Status, _request: &Request) -> RawHtml<String> { catch_with_message(format!("{status}")) } fn catch_with_message(message: String) -> RawHtml<String> { - RawHtml(render_page( - &ErrorPage { status: message }, - RenderInfo { - importing: false, - session: None, - lang: Language::English, - }, - )) + RawHtml(message) // TODO } #[catch(default)] @@ -52,9 +34,9 @@ impl<'r> Responder<'r, 'static> for MyError { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { match req.accept().map(|a| a.preferred()) { Some(x) if x.is_json() => json!({ "error": format!("{}", self.0) }).respond_to(req), - Some(x) if x.is_avif() || x.is_png() || x.is_jpeg() => { - (ContentType::AVIF, ERROR_IMAGE.as_slice()).respond_to(req) - } + // Some(x) if x.is_avif() || x.is_png() || x.is_jpeg() => { + // (ContentType::AVIF, ERROR_IMAGE.as_slice()).respond_to(req) + // } _ => catch_with_message(format!("{:#}", self.0)).respond_to(req), } } diff --git a/server/src/ui/mod.rs b/server/src/ui/mod.rs index 92b93fe..55fad6a 100644 --- a/server/src/ui/mod.rs +++ b/server/src/ui/mod.rs @@ -4,58 +4,51 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ use error::MyResult; -use home::rocket_uri_macro_r_home; -use rocket::{ - Either, - futures::FutureExt, - get, - response::{Redirect, content::RawHtml}, -}; -use std::{future::Future, pin::Pin}; -use tokio::{ - fs::{File, read_to_string}, - io::AsyncRead, -}; +use rocket::{futures::FutureExt, get}; +use std::{future::Future, pin::Pin, sync::Arc}; +use tokio::{fs::File, io::AsyncRead}; + +use crate::State; pub mod account; -pub mod admin; +// pub mod admin; pub mod assets; pub mod error; -pub mod home; -pub mod items; +// pub mod home; +// pub mod items; pub mod node; -pub mod player; -pub mod search; -pub mod stats; +// pub mod player; +// pub mod search; +// pub mod stats; pub mod style; -#[get("/")] -pub async fn r_index( - lang: AcceptLanguage, - sess: Option<A<Session>>, -) -> MyResult<Either<Redirect, RawHtml<String>>> { - let AcceptLanguage(lang) = lang; - if sess.is_some() { - Ok(Either::Left(Redirect::temporary(rocket::uri!(r_home())))) - } else { - let front = read_to_string(CONF.asset_path.join("front.htm")).await?; - Ok(Either::Right(RawHtml(render_page( - &CustomPage { - title: "Jellything".to_string(), - body: front, - }, - RenderInfo { - importing: false, - session: None, - lang, - }, - )))) - } -} +// #[get("/")] +// pub async fn r_index( +// lang: AcceptLanguage, +// sess: Option<A<Session>>, +// ) -> MyResult<Either<Redirect, RawHtml<String>>> { +// let AcceptLanguage(lang) = lang; +// if sess.is_some() { +// Ok(Either::Left(Redirect::temporary(rocket::uri!(r_home())))) +// } else { +// let front = read_to_string(CONF.asset_path.join("front.htm")).await?; +// Ok(Either::Right(RawHtml(render_page( +// &CustomPage { +// title: "Jellything".to_string(), +// body: front, +// }, +// RenderInfo { +// importing: false, +// session: None, +// lang, +// }, +// )))) +// } +// } #[get("/favicon.ico")] -pub async fn r_favicon() -> MyResult<File> { - Ok(File::open(CONF.asset_path.join("favicon.ico")).await?) +pub async fn r_favicon(s: &rocket::State<Arc<State>>) -> MyResult<File> { + Ok(File::open(s.config.asset_path.join("favicon.ico")).await?) } pub struct Defer(Pin<Box<dyn Future<Output = String> + Send>>); diff --git a/server/src/ui/node.rs b/server/src/ui/node.rs index 85beac6..cf5c793 100644 --- a/server/src/ui/node.rs +++ b/server/src/ui/node.rs @@ -5,42 +5,12 @@ */ use super::error::MyResult; use crate::request_info::RequestInfo; -use rocket::{Either, get, response::content::RawHtml, serde::json::Json}; +use jellycommon::jellyobject::Object; +use jellyui::render_view; +use rocket::{get, response::content::RawHtml}; -#[get("/n/<id>?<parents>&<children>&<filter..>")] -pub async fn r_node( - ri: RequestInfo<'_>, - id: A<NodeID>, - filter: Option<ANodeFilterSort>, - parents: bool, - children: bool, -) -> MyResult<Either<RawHtml<String>, Json<ApiNodeResponse>>> { - let filter: Option<NodeFilterSort> = filter.map(Into::into); - let filter = filter.unwrap_or_default(); - - let r = get_node( - &ri.session, - id.0, - !ri.accept.is_json() || children, - !ri.accept.is_json() || parents, - filter.clone(), - )?; - - Ok(if ri.accept.is_json() { - Either::Right(Json(r)) - } else { - Either::Left(RawHtml(render_page( - &NodePage { - node: &r.node, - udata: &r.userdata, - children: &r.children, - parents: &r.parents, - similar: &[], - filter: &filter, - lang: &ri.lang, - player: false, - }, - ri.render_info(), - ))) - }) +#[get("/n/<slug>")] +pub fn r_node(ri: RequestInfo<'_>, slug: &str) -> MyResult<RawHtml<String>> { + ri.require_user()?; + Ok(RawHtml(render_view(ri.render_info(), Object::EMPTY))) } diff --git a/server/src/ui/search.rs b/server/src/ui/search.rs index fce79e3..8ec2697 100644 --- a/server/src/ui/search.rs +++ b/server/src/ui/search.rs @@ -10,27 +10,28 @@ use rocket::{Either, get, response::content::RawHtml, serde::json::Json}; #[get("/search?<query>&<page>")] pub async fn r_search( - ri: RequestInfo, + ri: RequestInfo<'_>, query: Option<&str>, page: Option<usize>, -) -> MyResult<Either<RawHtml<String>, Json<ApiSearchResponse>>> { - let r = query - .map(|query| search(&ri.session, query, page)) - .transpose()?; +) -> MyResult<RawHtml<String>> { + // let r = query + // .map(|query| search(&ri.session, query, page)) + // .transpose()?; - Ok(if ri.accept.is_json() { - let Some(r) = r else { - Err(anyhow!("no query"))? - }; - Either::Right(Json(r)) - } else { - Either::Left(RawHtml(render_page( - &SearchPage { - lang: &ri.lang, - query: &query.map(|s| s.to_string()), - r, - }, - ri.render_info(), - ))) - }) + // Ok(if ri.accept.is_json() { + // let Some(r) = r else { + // Err(anyhow!("no query"))? + // }; + // Either::Right(Json(r)) + // } else { + // Either::Left(RawHtml(render_page( + // &SearchPage { + // lang: &ri.lang, + // query: &query.map(|s| s.to_string()), + // r, + // }, + // ri.render_info(), + // ))) + // }) + todo!() } diff --git a/server/src/ui/stats.rs b/server/src/ui/stats.rs index fc4ae64..387ca63 100644 --- a/server/src/ui/stats.rs +++ b/server/src/ui/stats.rs @@ -3,22 +3,10 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -use super::error::MyError; -use crate::request_info::RequestInfo; -use rocket::{Either, get, response::content::RawHtml, serde::json::Json}; +use crate::{request_info::RequestInfo, ui::error::MyResult}; +use rocket::{get, response::content::RawHtml}; #[get("/stats")] -pub fn r_stats( - ri: RequestInfo, -) -> Result<Either<RawHtml<String>, Json<ApiStatsResponse>>, MyError> { - let r = stats(&ri.session)?; - - Ok(if ri.accept.is_json() { - Either::Right(Json(r)) - } else { - Either::Left(RawHtml(render_page( - &StatsPage { lang: &ri.lang, r }, - ri.render_info(), - ))) - }) +pub fn r_stats(ri: RequestInfo) -> MyResult<RawHtml<String>> { + todo!() } diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 9bf5082..55ff42f 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -markup = { git = "https://github.com/metamuffin/markup.rs", rev = "2ee9aee" } +markup = "0.16.0" jellycommon = { path = "../common" } humansize = "2.1.3" serde = { version = "1.0.228", features = ["derive", "rc"] } diff --git a/ui/src/components/login.rs b/ui/src/components/login.rs new file mode 100644 index 0000000..c54a541 --- /dev/null +++ b/ui/src/components/login.rs @@ -0,0 +1,43 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +use jellyui_locale::tr; + +use crate::RenderInfo; + +markup::define! { + AccountSetPassword<'a>(ri: &'a RenderInfo<'a>, session: &'a str) { + form.account[method="POST", action=""] { + h1 { @tr(ri.lang, "account.set_password") } + input[type="text", name="session", hidden, value=session]; br; + + label[for="inp-password"] { @tr(ri.lang, "account.password") } + input[type="password", id="inp-password", name="password"]; br; + + input[type="submit", value=tr(ri.lang, "account.register.submit")]; + + + } + } + AccountLogin<'a>(ri: &'a RenderInfo<'a>) { + form.account[method="POST", action=""] { + h1 { @tr(ri.lang, "account.login") } + + label[for="inp-username"] { @tr(ri.lang, "account.username") } + input[type="text", id="inp-username", name="username"]; br; + label[for="inp-password"] { @tr(ri.lang, "account.password") } + input[type="password", id="inp-password", name="password"]; br; + + input[type="submit", value=tr(ri.lang, if ri.user.is_some() { "account.login.submit.switch" } else { "account.login.submit" })]; + } + } + AccountLogout<'a>(ri: &'a RenderInfo<'a>) { + form.account[method="POST", action=""] { + h1 { @tr(ri.lang, "account.logout") } + input[type="submit", value=tr(ri.lang, "account.logout.submit")]; + } + } +} diff --git a/ui/src/components/mod.rs b/ui/src/components/mod.rs index 07b050b..792894e 100644 --- a/ui/src/components/mod.rs +++ b/ui/src/components/mod.rs @@ -4,6 +4,7 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ +pub mod login; pub mod message; pub mod node_page; pub mod props; @@ -11,7 +12,11 @@ pub mod stats; use crate::{ RenderInfo, - components::{message::Message, node_page::NodePage}, + components::{ + login::{AccountLogin, AccountLogout, AccountSetPassword}, + message::Message, + node_page::NodePage, + }, }; use jellycommon::{jellyobject::Object, *}; use markup::define; @@ -24,5 +29,14 @@ define! { @if let Some(nku) = view.get(VIEW_NODE_PAGE) { @NodePage { ri, nku } } + @if let Some(()) = view.get(VIEW_ACCOUNT_LOGIN) { + @AccountLogin { ri } + } + @if let Some(()) = view.get(VIEW_ACCOUNT_LOGOUT) { + @AccountLogout{ ri } + } + @if let Some(session) = view.get(VIEW_ACCOUNT_SET_PASSWORD) { + @AccountSetPassword { ri, session } + } } } diff --git a/ui/src/old/account/mod.rs b/ui/src/old/account/mod.rs deleted file mode 100644 index e7da26f..0000000 --- a/ui/src/old/account/mod.rs +++ /dev/null @@ -1,103 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ -pub mod settings; - -use crate::{Page, locale::tr, scaffold::RenderInfo}; -use jellycommon::routes::{u_account_login, u_account_register}; - -impl Page for AccountLogin<'_> { - fn title(&self) -> String { - tr( - self.ri.lang, - if self.logged_in { - "account.login.switch" - } else { - "account.login" - }, - ) - .to_string() - } - - fn to_render(&self) -> markup::DynRender<'_> { - markup::new!(@self) - } -} -impl Page for AccountRegister<'_> { - fn title(&self) -> String { - tr(self.ri.lang, "account.register").to_string() - } - fn to_render(&self) -> markup::DynRender<'_> { - markup::new!(@self) - } -} -impl Page for AccountRegisterSuccess<'_> { - fn title(&self) -> String { - tr(self.ri.lang, "account.register").to_string() - } - fn to_render(&self) -> markup::DynRender<'_> { - markup::new!(@self) - } -} -impl Page for AccountLogout<'_> { - fn title(&self) -> String { - tr(self.ri.lang, "account.logout").to_string() - } - fn to_render(&self) -> markup::DynRender<'_> { - markup::new!(@self) - } -} - -markup::define! { - AccountRegister<'a>(ri: &'a RenderInfo<'a>) { - form.account[method="POST", action=""] { - h1 { @tr(ri.lang, "account.register") } - - label[for="inp-invitation"] { @tr(ri.lang, "account.register.invitation") } - input[type="text", id="inp-invitation", name="invitation"]; br; - - label[for="inp-username"] { @tr(ri.lang, "account.username") } - input[type="text", id="inp-username", name="username"]; br; - label[for="inp-password"] { @tr(ri.lang, "account.password") } - input[type="password", id="inp-password", name="password"]; br; - - input[type="submit", value=tr(ri.lang, "account.register.submit")]; - - p { @tr(ri.lang, "account.register.login") " " a[href=u_account_login()] { @tr(ri.lang, "account.register.login_here") } } - } - } - AccountRegisterSuccess<'a>(ri: &'a RenderInfo<'a>, logged_in: bool) { - h1 { @tr(ri.lang, if *logged_in { - "account.register.success.switch" - } else { - "account.register.success" - })} - } - AccountLogin<'a>(ri: &'a RenderInfo<'a>, logged_in: bool) { - form.account[method="POST", action=""] { - h1 { @self.title() } - - label[for="inp-username"] { @tr(ri.lang, "account.username") } - input[type="text", id="inp-username", name="username"]; br; - label[for="inp-password"] { @tr(ri.lang, "account.password") } - input[type="password", id="inp-password", name="password"]; br; - - input[type="submit", value=tr(ri.lang, if *logged_in { "account.login.submit.switch" } else { "account.login.submit" })]; - - @if *logged_in { - p { @tr(ri.lang, "account.login.register.switch") " " a[href=u_account_register()] { @tr(ri.lang, "account.login.register_here") } } - } else { - p { @tr(ri.lang, "account.login.cookie_note") } - p { @tr(ri.lang, "account.login.register") " " a[href=u_account_register()] { @tr(ri.lang, "account.login.register_here") } } - } - } - } - AccountLogout<'a>(ri: &'a RenderInfo<'a>) { - form.account[method="POST", action=""] { - h1 { @tr(ri.lang, "account.logout") } - input[type="submit", value=tr(ri.lang, "account.logout.submit")]; - } - } -} |