diff --git a/Cargo.lock b/Cargo.lock index 6c13b3f..7ab3fce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,19 +370,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atom_syndication" -version = "0.12.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f68d23e2cb4fd958c705b91a6b4c80ceeaf27a9e11651272a8389d5ce1a4a3" -dependencies = [ - "chrono", - "derive_builder", - "diligent-date-parser", - "never", - "quick-xml", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -966,7 +953,6 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", "syn 2.0.100", ] @@ -1002,37 +988,6 @@ dependencies = [ "serde", ] -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.100", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn 2.0.100", -] - [[package]] name = "digest" version = "0.10.7" @@ -1045,15 +1000,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "diligent-date-parser" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ede7d79366f419921e2e2f67889c12125726692a313bffb474bd5f37a581e9" -dependencies = [ - "chrono", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -1410,25 +1356,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "h2" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "half" version = "2.5.0" @@ -1580,7 +1507,6 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", "http", "http-body", "httparse", @@ -2048,6 +1974,14 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "migration" +version = "0.1.0" +dependencies = [ + "async-std", + "sea-orm-migration", +] + [[package]] name = "mime" version = "0.3.17" @@ -2108,12 +2042,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "never" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" - [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2570,16 +2498,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.37.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4ce8c88de324ff838700f36fb6ab86c96df0e3c4ab6ef3a9b2044465cce1369" -dependencies = [ - "encoding_rs", - "memchr", -] - [[package]] name = "quote" version = "1.0.40" @@ -2862,18 +2780,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rss" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2107738f003660f0a91f56fd3e3bd3ab5d918b2ddaf1e1ec2136fb1c46f71bf" -dependencies = [ - "atom_syndication", - "derive_builder", - "never", - "quick-xml", -] - [[package]] name = "rust-embed" version = "8.7.0" @@ -3038,17 +2944,15 @@ dependencies = [ "futures-util", "image", "itertools 0.14.0", + "migration", "mime_guess", "password-auth", "pulldown-cmark", "rand 0.9.0", - "rss", "rust-embed", - "samey-migration", "sea-orm", "serde", "serde_json", - "strum 0.27.1", "thiserror 2.0.12", "time", "tokio", @@ -3056,14 +2960,6 @@ dependencies = [ "tower-sessions", ] -[[package]] -name = "samey-migration" -version = "0.1.0" -dependencies = [ - "async-std", - "sea-orm-migration", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3104,7 +3000,7 @@ dependencies = [ "serde", "serde_json", "sqlx", - "strum 0.26.3", + "strum", "thiserror 2.0.12", "time", "tracing", @@ -3679,28 +3575,6 @@ version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -[[package]] -name = "strum" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.100", -] - [[package]] name = "subtle" version = "2.6.1" diff --git a/Cargo.toml b/Cargo.toml index 07d02ec..3694c23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,18 +6,14 @@ rust-version = "1.85" license = "MIT" authors = ["Bad Manners "] readme = "README.md" -keywords = ["booru", "image-board", "gallery"] -categories = ["web-programming"] -description = "Sam's small image board" -repository = "https://github.com/BadMannersXYZ/samey" [workspace] -members = ["migration"] +members = [".", "migration"] [dependencies] askama = { version = "0.13.0", features = ["serde_json"] } async-trait = "0.1.88" -axum = { version = "0.8.3", features = ["http2", "multipart", "macros"] } +axum = { version = "0.8.3", features = ["multipart", "macros"] } axum-extra = { version = "0.10.1", features = ["form"] } axum-login = "0.17.0" chrono = "0.4.40" @@ -25,12 +21,11 @@ clap = "4.5.35" futures-util = "0.3.31" image = "0.25.6" itertools = "0.14.0" -samey-migration = { path = "./migration", version = "0.1.0" } +migration = { path = "migration" } mime_guess = "2.0.5" password-auth = "1.0.0" pulldown-cmark = "0.13.0" rand = "0.9.0" -rss = "2.0.12" rust-embed = { version = "8.7.0", features = ["axum", "debug-embed"] } sea-orm = { version = "1.1.8", features = [ "sqlx-sqlite", @@ -45,7 +40,6 @@ time = "0.3.41" tokio = { version = "1.44.1", features = ["full"] } tower-http = { version = "0.6.2", features = ["fs"] } tower-sessions = "0.14.0" -strum = { version = "0.27.1", features = ["derive"] } [profile.release] strip = true diff --git a/README.md b/README.md index 879aef7..2499066 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,29 @@ Sam's small image board. -[Check out a sample instance here!](https://samey.badmanners.xyz/) - ## Status Still very much an early WIP. -### Features +### Roadmap -- Image and video posts. -- Tagging with autocompletion. -- Post pools. -- RSS feeds. - -### Possible roadmap - -- [ ] Display thumbnails on post selection -- [ ] Text media -- [ ] Testing -- [ ] Improve CSS -- [ ] User management +- [ ] Logging +- [ ] Improved error handling +- [ ] Caching - [ ] Lossless compression +- [ ] Bulk edit tags/Fix tag capitalization +- [ ] User management +- [ ] Cleanup/fixup background tasks +- [ ] Text media +- [ ] Improve CSS - [ ] Migrate to Cot...? +## Prerequisites + +- `ffmpeg` and `ffprobe` + ## Running -### Dependencies - -- `ffmpeg` (with `ffprobe`) - ### Development ```bash diff --git a/docker-compose.yml b/docker-compose.yml index eb37c5a..9fb11e0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,6 @@ services: samey: image: badmanners/samey:latest - build: - context: . container_name: samey restart: unless-stopped ports: diff --git a/migration/Cargo.toml b/migration/Cargo.toml index 621dbd2..cc2fe11 100644 --- a/migration/Cargo.toml +++ b/migration/Cargo.toml @@ -1,19 +1,16 @@ [package] -name = "samey-migration" +name = "migration" version = "0.1.0" -edition = "2024" -rust-version = "1.85" -license = "MIT" -authors = ["Bad Manners "] -readme = "README.md" -keywords = ["booru", "image-board", "gallery"] -categories = ["web-programming"] -description = "Migrations for Samey, Sam's small image board" -repository = "https://github.com/BadMannersXYZ/samey" +edition = "2021" +publish = false + +[lib] +name = "migration" +path = "src/lib.rs" [dependencies] -async-std = { version = "1.13.1", features = ["attributes", "tokio1"] } -sea-orm-migration = { version = "1.1.8", features = [ - "sqlx-sqlite", - "runtime-tokio-rustls", -] } +async-std = { version = "1", features = ["attributes", "tokio1"] } + +[dependencies.sea-orm-migration] +version = "1.1.8" +features = ["sqlx-sqlite", "runtime-tokio-rustls"] diff --git a/migration/src/main.rs b/migration/src/main.rs index 7c1e85c..c6b6e48 100644 --- a/migration/src/main.rs +++ b/migration/src/main.rs @@ -2,5 +2,5 @@ use sea_orm_migration::prelude::*; #[async_std::main] async fn main() { - cli::run_cli(samey_migration::Migrator).await; + cli::run_cli(migration::Migrator).await; } diff --git a/src/auth.rs b/src/auth.rs index 0d1adf1..975b075 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,8 +1,8 @@ use std::fmt::Debug; use axum_login::{AuthUser, AuthnBackend, UserId}; +use migration::Expr; use password_auth::verify_password; -use samey_migration::Expr; use sea_orm::{ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; use serde::Deserialize; use time::OffsetDateTime; @@ -221,9 +221,3 @@ impl ExpiredDeletion for SessionStorage { Ok(()) } } - -impl From> for SameyError { - fn from(value: axum_login::Error) -> Self { - value.into() - } -} diff --git a/src/config.rs b/src/config.rs index 8838d08..17ecefa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,13 +6,11 @@ use crate::{ }; pub(crate) const APPLICATION_NAME_KEY: &str = "APPLICATION_NAME"; -pub(crate) const BASE_URL_KEY: &str = "BASE_URL"; pub(crate) const AGE_CONFIRMATION_KEY: &str = "AGE_CONFIRMATION"; #[derive(Clone)] pub(crate) struct AppConfig { pub(crate) application_name: String, - pub(crate) base_url: String, pub(crate) age_confirmation: bool, } @@ -26,14 +24,6 @@ impl AppConfig { Some(row) => row.data.as_str().unwrap_or("Samey").to_owned(), None => "Samey".to_owned(), }; - let base_url = match SameyConfig::find() - .filter(samey_config::Column::Key.eq(BASE_URL_KEY)) - .one(db) - .await? - { - Some(row) => row.data.as_str().unwrap_or("").to_owned(), - None => "".to_owned(), - }; let age_confirmation = match SameyConfig::find() .filter(samey_config::Column::Key.eq(AGE_CONFIRMATION_KEY)) .one(db) @@ -44,7 +34,6 @@ impl AppConfig { }; Ok(Self { application_name, - base_url, age_confirmation, }) } diff --git a/src/error.rs b/src/error.rs index 018456c..d3bdda0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,114 +4,54 @@ use axum::{ response::{Html, IntoResponse, Response}, }; -#[derive(askama::Template)] -#[template(path = "pages/bad_request.html")] -struct BadRequestTemplate<'a> { - error: &'a str, -} - -#[derive(askama::Template)] -#[template(path = "pages/unauthorized.html")] -struct UnauthorizedTemplate; - -#[derive(askama::Template)] -#[template(path = "pages/forbidden.html")] -struct ForbiddenTemplate; - #[derive(askama::Template)] #[template(path = "pages/not_found.html")] struct NotFoundTemplate; -#[derive(askama::Template)] -#[template(path = "pages/internal_server_error.html")] -struct InternalServerErrorTemplate; - -/// Errors from Samey. #[derive(Debug, thiserror::Error)] pub enum SameyError { - /// Integer conversion error. #[error("Integer conversion error: {0}")] IntConversion(#[from] std::num::TryFromIntError), - /// Integer parsing error. - #[error("Integer parsing error: {0}")] - IntParse(#[from] std::num::ParseIntError), - /// IO error. #[error("IO error: {0}")] IO(#[from] std::io::Error), - /// Task error. #[error("Task error: {0}")] Join(#[from] tokio::task::JoinError), - /// Template render error. #[error("Template render error: {0}")] Render(#[from] askama::Error), - /// Database error. #[error("Database error: {0}")] Database(#[from] sea_orm::error::DbErr), - /// File streaming error. #[error("File streaming error: {0}")] Multipart(#[from] axum::extract::multipart::MultipartError), - /// Image error. #[error("Image error: {0}")] Image(#[from] image::ImageError), - /// Authentication error. - #[error("Authentication error: {0}")] - Authentication(String), - /// Not found. #[error("Not found")] NotFound, - /// Not allowed. + #[error("Authentication error: {0}")] + Authentication(String), #[error("Not allowed")] Forbidden, - /// Bad request. #[error("Bad request: {0}")] BadRequest(String), - /// Custom internal error. #[error("Internal error: {0}")] Other(String), } impl IntoResponse for SameyError { fn into_response(self) -> Response { + println!("Server error - {}", &self); match &self { SameyError::IntConversion(_) - | SameyError::IntParse(_) | SameyError::IO(_) | SameyError::Join(_) | SameyError::Render(_) | SameyError::Database(_) | SameyError::Image(_) | SameyError::Other(_) => { - println!("Internal server error - {:?}", &self); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Html( - InternalServerErrorTemplate {} - .render() - .expect("shouldn't fail to render InternalServerErrorTemplate"), - ), - ) - .into_response() + (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong!").into_response() + } + SameyError::Multipart(_) | SameyError::BadRequest(_) => { + (StatusCode::BAD_REQUEST, "Invalid request").into_response() } - SameyError::Multipart(error) => ( - StatusCode::BAD_REQUEST, - Html( - BadRequestTemplate { - error: &error.body_text(), - } - .render() - .expect("shouldn't fail to render BadRequestTemplate"), - ), - ) - .into_response(), - SameyError::BadRequest(error) => ( - StatusCode::BAD_REQUEST, - Html( - BadRequestTemplate { error } - .render() - .expect("shouldn't fail to render BadRequestTemplate"), - ), - ) - .into_response(), SameyError::NotFound => ( StatusCode::NOT_FOUND, Html( @@ -121,24 +61,10 @@ impl IntoResponse for SameyError { ), ) .into_response(), - SameyError::Authentication(_) => ( - StatusCode::UNAUTHORIZED, - Html( - UnauthorizedTemplate {} - .render() - .expect("shouldn't fail to render UnauthorizedTemplate"), - ), - ) - .into_response(), - SameyError::Forbidden => ( - StatusCode::FORBIDDEN, - Html( - ForbiddenTemplate {} - .render() - .expect("shouldn't fail to render ForbiddenTemplate"), - ), - ) - .into_response(), + SameyError::Authentication(_) => { + (StatusCode::UNAUTHORIZED, "Not authorized").into_response() + } + SameyError::Forbidden => (StatusCode::FORBIDDEN, "Forbidden").into_response(), } } } diff --git a/src/lib.rs b/src/lib.rs index 85e2c75..8bc2b5f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,9 @@ -//! Sam's small image board. - pub(crate) mod auth; pub(crate) mod config; pub(crate) mod entities; pub(crate) mod error; pub(crate) mod query; -pub(crate) mod tags; +pub(crate) mod rating; pub(crate) mod video; pub(crate) mod views; @@ -35,6 +33,9 @@ use crate::entities::{prelude::SameyUser, samey_user}; pub use crate::error::SameyError; use crate::views::*; +pub(crate) const NEGATIVE_PREFIX: &str = "-"; +pub(crate) const RATING_PREFIX: &str = "rating:"; + #[derive(rust_embed::Embed)] #[folder = "static/"] struct Asset; @@ -62,26 +63,14 @@ pub(crate) struct AppState { app_config: Arc>, } -/// Helper function to create a single user. -/// -/// You can specify if they must be an admin user via the `is_admin` flag. -/// -/// ``` -/// use samey::create_user; -/// -/// # async fn _main() { -/// let db = sea_orm::Database::connect("sqlite:db.sqlite3?mode=rwc").await.unwrap(); -/// create_user(db, "admin", "secretPassword", true).await.expect("Unable to add admin user"); -/// # } -/// ``` pub async fn create_user( db: DatabaseConnection, - username: &str, - password: &str, + username: String, + password: String, is_admin: bool, ) -> Result<(), SameyError> { SameyUser::insert(samey_user::ActiveModel { - username: Set(username.into()), + username: Set(username), password: Set(generate_hash(password)), is_admin: Set(is_admin), ..Default::default() @@ -91,18 +80,6 @@ pub async fn create_user( Ok(()) } -/// Creates an Axum router for a Samey application. -/// -/// ``` -/// use samey::get_router; -/// -/// # async fn _main() { -/// let db = sea_orm::Database::connect("sqlite:db.sqlite3?mode=rwc").await.unwrap(); -/// let app = get_router(db, "files").await.unwrap(); -/// let listener = tokio::net::TcpListener::bind(("0.0.0.0", 3000)).await.unwrap(); -/// axum::serve(listener, app).await.unwrap(); -/// # } -/// ``` pub async fn get_router( db: DatabaseConnection, files_dir: impl AsRef, @@ -115,9 +92,7 @@ pub async fn get_router( fs::create_dir_all(files_dir.as_ref()).await?; let session_store = SessionStorage::new(db.clone()); - let session_layer = SessionManagerLayer::new(session_store).with_expiry( - tower_sessions::Expiry::OnInactivity(time::Duration::weeks(1)), - ); + let session_layer = SessionManagerLayer::new(session_store); let auth_layer = AuthManagerLayerBuilder::new(Backend::new(db), session_layer).build(); Ok(Router::new() @@ -146,14 +121,12 @@ pub async fn get_router( .route_with_tsr("/pools", get(get_pools)) .route_with_tsr("/pools/{page}", get(get_pools_page)) .route_with_tsr("/pool", post(create_pool)) - .route_with_tsr("/pool/{pool_id}", get(view_pool).delete(delete_pool)) + .route_with_tsr("/pool/{pool_id}", get(view_pool)) .route_with_tsr("/pool/{pool_id}/name", put(change_pool_name)) .route_with_tsr("/pool/{pool_id}/public", put(change_pool_visibility)) .route_with_tsr("/pool/{pool_id}/post", post(add_post_to_pool)) .route_with_tsr("/pool/{pool_id}/sort", put(sort_pool)) .route_with_tsr("/pool_post/{pool_post_id}", delete(remove_pool_post)) - // Bulk edit tag routes - .route_with_tsr("/bulk_edit_tag", get(bulk_edit_tag).post(edit_tag)) // Settings routes .route_with_tsr("/settings", get(settings).post(update_settings)) // Search routes @@ -161,7 +134,6 @@ pub async fn get_router( .route_with_tsr("/posts/{page}", get(posts_page)) // Other routes .route_with_tsr("/remove", delete(remove_field)) - .route("/posts.xml", get(rss_page)) .route("/", get(index)) .with_state(state) .nest_service("/files", ServeDir::new(files_dir)) diff --git a/src/main.rs b/src/main.rs index 11ee87b..8c11604 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,89 +1,54 @@ -use std::{ - net::{IpAddr, Ipv6Addr}, - path::PathBuf, -}; - use clap::{Parser, Subcommand}; +use migration::{Migrator, MigratorTrait}; use samey::{create_user, get_router}; -use samey_migration::{Migrator, MigratorTrait}; use sea_orm::Database; #[derive(Parser)] struct Config { - #[arg(short, long, default_value = "sqlite:db.sqlite3?mode=rwc")] - database: String, - - #[arg(short, long, default_value = "files")] - files_directory: PathBuf, - #[command(subcommand)] command: Option, } #[derive(Subcommand)] enum Commands { - Run { - #[arg(short, long, default_value_t = IpAddr::V6(Ipv6Addr::UNSPECIFIED))] - address: IpAddr, - - #[arg(short, long, default_value_t = 3000)] - port: u16, - }, - + Run, Migrate, - AddAdminUser { #[arg(short, long)] username: String, - #[arg(short, long)] password: String, }, } -impl Default for Commands { - fn default() -> Self { - Commands::Run { - address: IpAddr::V6(Ipv6Addr::UNSPECIFIED), - port: 3000, - } - } -} - #[tokio::main] async fn main() { - let config = Config::parse(); - let db = Database::connect(config.database) + let db = Database::connect("sqlite:db.sqlite3?mode=rwc") .await .expect("Unable to connect to database"); - match config.command.unwrap_or_default() { - Commands::Migrate => { + let config = Config::parse(); + match config.command { + Some(Commands::Migrate) => { Migrator::up(&db, None) .await .expect("Unable to apply migrations"); } - - Commands::AddAdminUser { username, password } => { - create_user(db, &username, &password, true) + Some(Commands::AddAdminUser { username, password }) => { + create_user(db, username, password, true) .await .expect("Unable to add admin user"); } - - Commands::Run { address, port } => { + Some(Commands::Run) | None => { Migrator::up(&db, None) .await .expect("Unable to apply migrations"); - let app = get_router(db, config.files_directory) + let app = get_router(db, "files") .await .expect("Unable to start router"); - let listener = tokio::net::TcpListener::bind((address, port)) + let listener = tokio::net::TcpListener::bind("0.0.0.0:3000") .await - .expect("Unable to bind TCP listener"); - if address.is_ipv6() { - println!("Listening on http://[{}]:{}", address, port); - } else { - println!("Listening on http://{}:{}", address, port); - } + .expect("Unable to listen to port"); + println!("Listening on http://localhost:3000"); axum::serve(listener, app).await.unwrap(); } } diff --git a/src/query.rs b/src/query.rs index 3b75647..a4d51e0 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,31 +1,25 @@ use std::collections::HashSet; -use chrono::NaiveDateTime; -use samey_migration::{Expr, Query}; +use migration::{Expr, Query}; use sea_orm::{ - ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoIdentity, - IntoSimpleExpr, QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns, - SelectModel, Selector, + ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoSimpleExpr, + QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns, SelectModel, + Selector, }; use crate::{ - SameyError, + NEGATIVE_PREFIX, RATING_PREFIX, SameyError, auth::User, entities::{ prelude::{SameyPool, SameyPoolPost, SameyPost, SameyTag, SameyTagPost}, samey_pool, samey_pool_post, samey_post, samey_tag, samey_tag_post, }, - tags::{MEDIA_TYPE_PREFIX, NEGATIVE_PREFIX, RATING_PREFIX}, }; #[derive(Debug, FromQueryResult)] pub(crate) struct PostOverview { pub(crate) id: i32, pub(crate) thumbnail: String, - pub(crate) media: String, - pub(crate) title: Option, - pub(crate) description: Option, - pub(crate) uploaded_at: NaiveDateTime, pub(crate) tags: Option, pub(crate) media_type: String, pub(crate) rating: String, @@ -39,23 +33,17 @@ pub(crate) fn search_posts( let mut exclude_tags = HashSet::::new(); let mut include_ratings = HashSet::::new(); let mut exclude_ratings = HashSet::::new(); - let mut include_types = HashSet::::new(); - let mut exclude_types = HashSet::::new(); if let Some(tags) = tags { - for tag in tags.iter().map(|tag| tag.to_lowercase()) { - if let Some(negative_tag) = tag.strip_prefix(NEGATIVE_PREFIX) { - if let Some(negative_rating_tag) = negative_tag.strip_prefix(RATING_PREFIX) { - exclude_ratings.insert(negative_rating_tag.into()); - } else if let Some(negative_type_tag) = negative_tag.strip_prefix(MEDIA_TYPE_PREFIX) - { - exclude_types.insert(negative_type_tag.into()); + for mut tag in tags.iter().map(|tag| tag.to_lowercase()) { + if tag.starts_with(NEGATIVE_PREFIX) { + if tag.as_str()[NEGATIVE_PREFIX.len()..].starts_with(RATING_PREFIX) { + exclude_ratings + .insert(tag.split_off(NEGATIVE_PREFIX.len() + RATING_PREFIX.len())); } else { - exclude_tags.insert(negative_tag.into()); + exclude_tags.insert(tag.split_off(NEGATIVE_PREFIX.len())); } - } else if let Some(rating_tag) = tag.strip_prefix(RATING_PREFIX) { - include_ratings.insert(rating_tag.into()); - } else if let Some(type_tag) = tag.strip_prefix(MEDIA_TYPE_PREFIX) { - include_types.insert(type_tag.into()); + } else if tag.starts_with(RATING_PREFIX) { + include_ratings.insert(tag.split_off(RATING_PREFIX.len())); } else { include_tags.insert(tag); } @@ -66,10 +54,6 @@ pub(crate) fn search_posts( let mut query = SameyPost::find() .select_only() .column(samey_post::Column::Id) - .column(samey_post::Column::Media) - .column(samey_post::Column::Title) - .column(samey_post::Column::Description) - .column(samey_post::Column::UploadedAt) .column(samey_post::Column::Thumbnail) .column(samey_post::Column::Rating) .column(samey_post::Column::MediaType) @@ -88,21 +72,11 @@ pub(crate) fn search_posts( if !exclude_ratings.is_empty() { query = query.filter(samey_post::Column::Rating.is_not_in(exclude_ratings)) } - if !include_types.is_empty() { - query = query.filter(samey_post::Column::MediaType.is_in(include_types)) - } - if !exclude_types.is_empty() { - query = query.filter(samey_post::Column::MediaType.is_not_in(exclude_types)) - } query } else { let mut query = SameyPost::find() .select_only() .column(samey_post::Column::Id) - .column(samey_post::Column::Media) - .column(samey_post::Column::Title) - .column(samey_post::Column::Description) - .column(samey_post::Column::UploadedAt) .column(samey_post::Column::Thumbnail) .column(samey_post::Column::Rating) .column(samey_post::Column::MediaType) @@ -160,12 +134,6 @@ pub(crate) fn search_posts( if !exclude_ratings.is_empty() { query = query.filter(samey_post::Column::Rating.is_not_in(exclude_ratings)) } - if !include_types.is_empty() { - query = query.filter(samey_post::Column::MediaType.is_in(include_types)) - } - if !exclude_types.is_empty() { - query = query.filter(samey_post::Column::MediaType.is_not_in(exclude_types)) - } query }; @@ -295,18 +263,3 @@ pub(crate) fn filter_posts_by_user( ), } } - -pub(crate) async fn clean_dangling_tags(db: &DatabaseConnection) -> Result<(), SameyError> { - let dangling_tags = SameyTag::find() - .select_column_as(samey_tag_post::Column::Id.count(), "count") - .left_join(SameyTagPost) - .group_by(samey_tag::Column::Id) - .having(Expr::column("count".into_identity()).eq(0)) - .all(db) - .await?; - SameyTag::delete_many() - .filter(samey_tag::Column::Id.is_in(dangling_tags.into_iter().map(|tag| tag.id))) - .exec(db) - .await?; - Ok(()) -} diff --git a/src/rating.rs b/src/rating.rs new file mode 100644 index 0000000..b95b44c --- /dev/null +++ b/src/rating.rs @@ -0,0 +1,31 @@ +use std::fmt::Display; + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum Rating { + Unrated, + Safe, + Questionable, + Explicit, +} + +impl From for Rating { + fn from(value: String) -> Self { + match value.as_ref() { + "s" => Self::Safe, + "q" => Self::Questionable, + "e" => Self::Explicit, + _ => Self::Unrated, + } + } +} + +impl Display for Rating { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Rating::Unrated => f.write_str("Unrated"), + Rating::Safe => f.write_str("Safe"), + Rating::Questionable => f.write_str("Questionable"), + Rating::Explicit => f.write_str("Explicit"), + } + } +} diff --git a/src/tags.rs b/src/tags.rs deleted file mode 100644 index 3954e8f..0000000 --- a/src/tags.rs +++ /dev/null @@ -1,23 +0,0 @@ -pub(crate) const NEGATIVE_PREFIX: &str = "-"; -pub(crate) const RATING_PREFIX: &str = "rating:"; -pub(crate) const MEDIA_TYPE_PREFIX: &str = "type:"; - -#[derive(strum::EnumIter, strum::Display, Debug)] -pub(crate) enum Rating { - #[strum(serialize = "u")] - Unrated, - #[strum(serialize = "s")] - Safe, - #[strum(serialize = "q")] - Questionable, - #[strum(serialize = "e")] - Explicit, -} - -#[derive(strum::EnumIter, strum::Display, Debug)] -pub(crate) enum MediaType { - #[strum(serialize = "image")] - Image, - #[strum(serialize = "video")] - Video, -} diff --git a/src/video.rs b/src/video.rs index 62e9cae..554ea3c 100644 --- a/src/video.rs +++ b/src/video.rs @@ -8,7 +8,7 @@ pub(crate) fn generate_thumbnail( max_thumbnail_dimension: u32, ) -> Result<(), SameyError> { let status = Command::new("ffmpeg") - .args([ + .args(&[ "-i", input_path, "-vf", @@ -39,7 +39,7 @@ pub(crate) fn generate_thumbnail( pub(crate) fn get_dimensions_for_video(input_path: &str) -> Result<(u32, u32), SameyError> { let output = Command::new("ffprobe") - .args([ + .args(&[ "-v", "error", "-select_streams", diff --git a/src/views.rs b/src/views.rs index e41430a..9c99371 100644 --- a/src/views.rs +++ b/src/views.rs @@ -17,20 +17,19 @@ use axum_extra::extract::Form; use chrono::Utc; use image::{GenericImageView, ImageFormat, ImageReader}; use itertools::Itertools; +use migration::{Expr, OnConflict}; use rand::Rng; -use samey_migration::{OnConflict, Query as MigrationQuery}; use sea_orm::{ ActiveValue::Set, ColumnTrait, Condition, EntityTrait, FromQueryResult, IntoSimpleExpr, ModelTrait, PaginatorTrait, QueryFilter, QuerySelect, }; use serde::Deserialize; -use strum::IntoEnumIterator; use tokio::{task::spawn_blocking, try_join}; use crate::{ - AppState, + AppState, NEGATIVE_PREFIX, RATING_PREFIX, auth::{AuthSession, Credentials, User}, - config::{AGE_CONFIRMATION_KEY, APPLICATION_NAME_KEY, BASE_URL_KEY}, + config::{AGE_CONFIRMATION_KEY, APPLICATION_NAME_KEY}, entities::{ prelude::{ SameyConfig, SameyPool, SameyPoolPost, SameyPost, SameyPostSource, SameyTag, @@ -41,10 +40,9 @@ use crate::{ }, error::SameyError, query::{ - PoolPost, PostOverview, PostPoolData, clean_dangling_tags, filter_posts_by_user, - get_pool_data_for_post, get_posts_in_pool, get_tags_for_post, search_posts, + PoolPost, PostOverview, PostPoolData, filter_posts_by_user, get_pool_data_for_post, + get_posts_in_pool, get_tags_for_post, search_posts, }, - tags::{MEDIA_TYPE_PREFIX, MediaType, NEGATIVE_PREFIX, RATING_PREFIX, Rating}, video::{generate_thumbnail, get_dimensions_for_video}, }; @@ -92,63 +90,6 @@ pub(crate) async fn index( )) } -// RSS view - -#[derive(Template)] -#[template(path = "fragments/rss_entry.html")] -struct RssEntryTemplate<'a> { - post: PostOverview, - base_url: &'a str, -} - -#[axum::debug_handler] -pub(crate) async fn rss_page( - State(AppState { app_config, db, .. }): State, - Query(query): Query, -) -> Result { - let app_config = app_config.read().await; - let application_name = app_config.application_name.clone(); - let base_url = app_config.base_url.clone(); - drop(app_config); - - let tags = query - .tags - .as_ref() - .map(|tags| tags.split_whitespace().collect::>()); - - let posts = search_posts(tags.as_ref(), None) - .paginate(&db, 20) - .fetch_page(0) - .await?; - - let channel = rss::ChannelBuilder::default() - .title(&application_name) - .link(&base_url) - .items( - posts - .into_iter() - .map(|post| { - rss::ItemBuilder::default() - .title(post.tags.clone()) - .pub_date(post.uploaded_at.and_utc().to_rfc2822()) - .link(format!("{}/post/{}", &base_url, post.id)) - .content( - RssEntryTemplate { - post, - base_url: &base_url, - } - .render() - .ok(), - ) - .build() - }) - .collect_vec(), - ) - .build(); - - Ok(channel.to_string()) -} - // Auth views #[derive(Template)] @@ -185,17 +126,24 @@ pub(crate) async fn login( mut auth_session: AuthSession, Form(credentials): Form, ) -> Result { - let user = match auth_session.authenticate(credentials).await? { - Some(user) => user, - None => return Err(SameyError::Authentication("Invalid credentials".into())), + let user = match auth_session.authenticate(credentials).await { + Ok(Some(user)) => user, + Ok(None) => return Err(SameyError::Authentication("Invalid credentials".into())), + Err(_) => return Err(SameyError::Other("Auth session error".into())), }; - auth_session.login(&user).await?; + auth_session + .login(&user) + .await + .map_err(|_| SameyError::Other("Login failed".into()))?; Ok(Redirect::to("/")) } pub(crate) async fn logout(mut auth_session: AuthSession) -> Result { - auth_session.logout().await?; + auth_session + .logout() + .await + .map_err(|_| SameyError::Other("Logout error".into()))?; Ok(Redirect::to("/")) } @@ -255,9 +203,10 @@ impl FromStr for Format { "application/x-matroska" | "video/mastroska" => Ok(Self::Video(".mkv")), "video/quicktime" => Ok(Self::Video(".mov")), _ => Ok(Self::Image( - ImageFormat::from_mime_type(content_type).ok_or(SameyError::BadRequest( - format!("Unknown content type: {}", content_type), - ))?, + ImageFormat::from_mime_type(content_type).ok_or(SameyError::Other(format!( + "Unknown content type: {}", + content_type + )))?, )), } } @@ -328,7 +277,7 @@ pub(crate) async fn upload( "media-file" => { let content_type = field .content_type() - .ok_or(SameyError::BadRequest("Missing content type".into()))?; + .ok_or(SameyError::Other("Missing content type".into()))?; match Format::from_str(content_type)? { format @ Format::Video(video_format) => { media_type = Some(format.media_type()); @@ -351,7 +300,6 @@ pub(crate) async fn upload( .read(true) .write(true) .create(true) - .truncate(true) .open(&file_path)?; while let Some(chunk) = field.chunk().await? { file.write_all(&chunk)?; @@ -402,7 +350,6 @@ pub(crate) async fn upload( .read(true) .write(true) .create(true) - .truncate(true) .open(&file_path)?; while let Some(chunk) = field.chunk().await? { file.write_all(&chunk)?; @@ -496,9 +443,7 @@ pub(crate) async fn upload( Ok(Redirect::to(&format!("/post/{}", uploaded_post))) } else { - Err(SameyError::BadRequest( - "Missing parameters for upload".into(), - )) + Err(SameyError::Other("Missing parameters for upload".into())) } } @@ -526,43 +471,32 @@ pub(crate) async fn search_tags( State(AppState { db, .. }): State, Form(body): Form, ) -> Result { - let tags = match body.tags[..body.selection_end].split(' ').next_back() { + let tags = match body.tags[..body.selection_end].split(' ').last() { Some(mut tag) => { tag = tag.trim(); if tag.is_empty() { vec![] - } else if let Some(stripped_tag) = tag.strip_prefix(NEGATIVE_PREFIX) { - if stripped_tag.starts_with(RATING_PREFIX) { - Rating::iter() - .map(|rating| format!("{}{}", RATING_PREFIX, rating)) - .filter(|t| t.starts_with(stripped_tag)) - .map(|tag| SearchTag { - value: format!("-{}", &tag), - name: tag, - }) - .collect() - } else if stripped_tag.starts_with(MEDIA_TYPE_PREFIX) { - MediaType::iter() - .map(|rating| format!("{}{}", MEDIA_TYPE_PREFIX, rating)) - .filter(|t| t.starts_with(stripped_tag)) - .map(|tag| SearchTag { - value: format!("-{}", &tag), - name: tag, - }) - .collect() + } else if tag.starts_with(NEGATIVE_PREFIX) { + if tag[NEGATIVE_PREFIX.len()..].starts_with(RATING_PREFIX) { + [ + format!("{}u", RATING_PREFIX), + format!("{}s", RATING_PREFIX), + format!("{}q", RATING_PREFIX), + format!("{}e", RATING_PREFIX), + ] + .into_iter() + .filter(|t| t.starts_with(&tag[NEGATIVE_PREFIX.len()..])) + .map(|tag| SearchTag { + value: format!("-{}", &tag), + name: tag, + }) + .collect() } else { SameyTag::find() - .filter( - Condition::any() - .add( - samey_tag::Column::NormalizedName - .starts_with(stripped_tag.to_lowercase()), - ) - .add( - samey_tag::Column::NormalizedName - .contains(format!(":{}", stripped_tag.to_lowercase())), - ), - ) + .filter(Expr::cust_with_expr( + "LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')", + tag[NEGATIVE_PREFIX.len()..].to_lowercase(), + )) .limit(10) .all(&db) .await? @@ -574,33 +508,25 @@ pub(crate) async fn search_tags( .collect() } } else if tag.starts_with(RATING_PREFIX) { - Rating::iter() - .map(|rating| format!("{}{}", RATING_PREFIX, rating)) - .filter(|t| t.starts_with(tag)) - .map(|tag| SearchTag { - value: tag.clone(), - name: tag, - }) - .collect() - } else if tag.starts_with(MEDIA_TYPE_PREFIX) { - MediaType::iter() - .map(|rating| format!("{}{}", MEDIA_TYPE_PREFIX, rating)) - .filter(|t| t.starts_with(tag)) - .map(|tag| SearchTag { - value: tag.clone(), - name: tag, - }) - .collect() + [ + format!("{}u", RATING_PREFIX), + format!("{}s", RATING_PREFIX), + format!("{}q", RATING_PREFIX), + format!("{}e", RATING_PREFIX), + ] + .into_iter() + .filter(|t| t.starts_with(tag)) + .map(|tag| SearchTag { + value: tag.clone(), + name: tag, + }) + .collect() } else { SameyTag::find() - .filter( - Condition::any() - .add(samey_tag::Column::NormalizedName.starts_with(tag.to_lowercase())) - .add( - samey_tag::Column::NormalizedName - .contains(format!(":{}", tag.to_lowercase())), - ), - ) + .filter(Expr::cust_with_expr( + "LOWER(\"samey_tag\".\"name\") LIKE CONCAT(?, '%')", + tag.to_lowercase(), + )) .limit(10) .all(&db) .await? @@ -1132,10 +1058,12 @@ pub(crate) async fn sort_pool( }; let max_index = if body.new_index == posts.len().saturating_sub(1) { None - } else if body.new_index < body.old_index { - Some(body.new_index) } else { - Some(body.new_index + 1) + if body.new_index < body.old_index { + Some(body.new_index) + } else { + Some(body.new_index + 1) + } }; let min = min_index.map(|index| posts[index].position).unwrap_or(0.0); let max = max_index @@ -1163,170 +1091,12 @@ pub(crate) async fn sort_pool( )) } -pub(crate) async fn delete_pool( - State(AppState { db, .. }): State, - auth_session: AuthSession, - Path(pool_id): Path, -) -> Result { - let pool = SameyPool::find_by_id(pool_id) - .one(&db) - .await? - .ok_or(SameyError::NotFound)?; - - let can_edit = match auth_session.user.as_ref() { - None => false, - Some(user) => user.is_admin || pool.uploader_id == user.id, - }; - - if !can_edit { - return Err(SameyError::Forbidden); - } - - SameyPool::delete_by_id(pool_id).exec(&db).await?; - - Ok(Redirect::to("/")) -} - -// Bulk edit tag views - -enum BulkEditTagMessage { - None, - Success, - Failure(String), -} - -#[derive(Template)] -#[template(path = "pages/bulk_edit_tag.html")] -struct BulkEditTagTemplate { - application_name: String, - age_confirmation: bool, - message: BulkEditTagMessage, -} - -pub(crate) async fn bulk_edit_tag( - State(AppState { app_config, .. }): State, - auth_session: AuthSession, -) -> Result { - if auth_session.user.is_none_or(|user| !user.is_admin) { - return Err(SameyError::Forbidden); - } - - let app_config = app_config.read().await; - let application_name = app_config.application_name.clone(); - let age_confirmation = app_config.age_confirmation; - drop(app_config); - - Ok(Html( - BulkEditTagTemplate { - application_name, - age_confirmation, - message: BulkEditTagMessage::None, - } - .render()?, - )) -} - -#[derive(Debug, Deserialize)] -pub(crate) struct EditTagForm { - tags: String, - new_tag: String, -} - -pub(crate) async fn edit_tag( - State(AppState { db, app_config, .. }): State, - auth_session: AuthSession, - Form(body): Form, -) -> Result { - if auth_session.user.is_none_or(|user| !user.is_admin) { - return Err(SameyError::Forbidden); - } - - let app_config = app_config.read().await; - let application_name = app_config.application_name.clone(); - let age_confirmation = app_config.age_confirmation; - drop(app_config); - - let old_tag: Vec<_> = body.tags.split_whitespace().collect(); - if old_tag.len() != 1 { - return Ok(Html( - BulkEditTagTemplate { - application_name, - age_confirmation, - message: BulkEditTagMessage::Failure("expected single tag to edit".into()), - } - .render()?, - )); - } - let old_tag = old_tag.first().unwrap(); - let normalized_old_tag = old_tag.to_lowercase(); - - let new_tag: Vec<_> = body.new_tag.split_whitespace().collect(); - if new_tag.len() != 1 { - return Ok(Html( - BulkEditTagTemplate { - application_name, - age_confirmation, - message: BulkEditTagMessage::Failure("expected single new tag".into()), - } - .render()?, - )); - } - let new_tag = new_tag.first().unwrap(); - let normalized_new_tag = new_tag.to_lowercase(); - - let old_tag_db = SameyTag::find() - .filter(samey_tag::Column::NormalizedName.eq(&normalized_old_tag)) - .one(&db) - .await? - .ok_or(SameyError::NotFound)?; - - if let Some(new_tag_db) = SameyTag::find() - .filter(samey_tag::Column::NormalizedName.eq(&normalized_new_tag)) - .one(&db) - .await? - { - let subquery = MigrationQuery::select() - .column((SameyTagPost, samey_tag_post::Column::PostId)) - .from(SameyTagPost) - .and_where(samey_tag_post::Column::TagId.eq(new_tag_db.id)) - .to_owned(); - SameyTagPost::update_many() - .filter(samey_tag_post::Column::TagId.eq(old_tag_db.id)) - .filter(samey_tag_post::Column::PostId.not_in_subquery(subquery)) - .set(samey_tag_post::ActiveModel { - tag_id: Set(new_tag_db.id), - ..Default::default() - }) - .exec(&db) - .await?; - SameyTag::delete_by_id(old_tag_db.id).exec(&db).await?; - } else { - SameyTag::update(samey_tag::ActiveModel { - id: Set(old_tag_db.id), - name: Set(new_tag.to_string()), - normalized_name: Set(normalized_new_tag), - }) - .exec(&db) - .await?; - } - - Ok(Html( - BulkEditTagTemplate { - application_name, - age_confirmation, - message: BulkEditTagMessage::Success, - } - .render()?, - )) -} - // Settings views #[derive(Template)] #[template(path = "pages/settings.html")] struct SettingsTemplate { application_name: String, - base_url: String, age_confirmation: bool, } @@ -1340,7 +1110,6 @@ pub(crate) async fn settings( let app_config = app_config.read().await; let application_name = app_config.application_name.clone(); - let base_url = app_config.base_url.clone(); let age_confirmation = app_config.age_confirmation; drop(app_config); @@ -1360,7 +1129,6 @@ pub(crate) async fn settings( Ok(Html( SettingsTemplate { application_name, - base_url, age_confirmation, } .render_with_values(&values)?, @@ -1370,18 +1138,11 @@ pub(crate) async fn settings( #[derive(Debug, Deserialize)] pub(crate) struct UpdateSettingsForm { application_name: String, - base_url: String, - favicon_post_id: String, age_confirmation: Option, } pub(crate) async fn update_settings( - State(AppState { - db, - app_config, - files_dir, - .. - }): State, + State(AppState { db, app_config, .. }): State, auth_session: AuthSession, Form(body): Form, ) -> Result { @@ -1391,27 +1152,18 @@ pub(crate) async fn update_settings( let mut configs = vec![]; - let application_name = body.application_name.trim(); - if !application_name.is_empty() { + if !body.application_name.is_empty() { let _ = mem::replace( &mut app_config.write().await.application_name, - application_name.into(), + body.application_name.clone(), ); configs.push(samey_config::ActiveModel { key: Set(APPLICATION_NAME_KEY.into()), - data: Set(application_name.into()), + data: Set(body.application_name.into()), ..Default::default() }); } - let base_url = body.base_url.trim_end_matches('/'); - let _ = mem::replace(&mut app_config.write().await.base_url, base_url.into()); - configs.push(samey_config::ActiveModel { - key: Set(BASE_URL_KEY.into()), - data: Set(base_url.into()), - ..Default::default() - }); - let age_confirmation = body.age_confirmation.is_some(); let _ = mem::replace( &mut app_config.write().await.age_confirmation, @@ -1434,21 +1186,6 @@ pub(crate) async fn update_settings( .await?; } - if let Some(favicon_post_id) = body.favicon_post_id.split_whitespace().next() { - match favicon_post_id.parse::() { - Ok(favicon_post_id) => { - let post = SameyPost::find_by_id(favicon_post_id) - .one(&db) - .await? - .ok_or(SameyError::NotFound)?; - ImageReader::open(files_dir.join(post.thumbnail))? - .decode()? - .save_with_format(files_dir.join("favicon.png"), ImageFormat::Png)?; - } - Err(err) => return Err(SameyError::IntParse(err)), - } - } - Ok(Redirect::to("/")) } @@ -1511,10 +1248,6 @@ pub(crate) async fn view_post_page( Some(parent_post) => Some(PostOverview { id: parent_id, thumbnail: parent_post.thumbnail, - title: parent_post.title, - description: parent_post.description, - uploaded_at: parent_post.uploaded_at, - media: parent_post.media, tags: Some( get_tags_for_post(post_id) .all(&db) @@ -1544,10 +1277,6 @@ pub(crate) async fn view_post_page( children_posts.push(PostOverview { id: child_post.id, thumbnail: child_post.thumbnail, - title: child_post.title, - description: child_post.description, - uploaded_at: child_post.uploaded_at, - media: child_post.media, tags: Some( get_tags_for_post(child_post.id) .all(&db) @@ -1642,7 +1371,6 @@ struct SubmitPostDetailsTemplate { parent_post: Option, sources: Vec, tags: Vec, - tags_text: String, can_edit: bool, } @@ -1667,14 +1395,14 @@ pub(crate) async fn submit_post_details( } let title = match body.title.trim() { - "" => None, + title if title.is_empty() => None, title => Some(title.to_owned()), }; let description = match body.description.trim() { - "" => None, + description if description.is_empty() => None, description => Some(description.to_owned()), }; - let parent_post = if let Ok(parent_id) = body.parent_post.trim().parse() { + let parent_post = if let Some(parent_id) = body.parent_post.trim().parse().ok() { match filter_posts_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref()) .one(&db) .await? @@ -1682,10 +1410,6 @@ pub(crate) async fn submit_post_details( Some(parent_post) => Some(PostOverview { id: parent_id, thumbnail: parent_post.thumbnail, - title: parent_post.title, - description: parent_post.description, - uploaded_at: parent_post.uploaded_at, - media: parent_post.media, tags: Some( get_tags_for_post(post_id) .all(&db) @@ -1773,31 +1497,17 @@ pub(crate) async fn submit_post_details( upload_tags.sort_by(|a, b| a.name.cmp(&b.name)); upload_tags }; - let mut tags_text = String::new(); - for tag in &tags { - if !tags_text.is_empty() { - tags_text.push(' '); - } - tags_text.push_str(&tag.name); - } let sources = SameyPostSource::find() .filter(samey_post_source::Column::PostId.eq(post_id)) .all(&db) .await?; - tokio::spawn(async move { - if let Err(err) = clean_dangling_tags(&db).await { - println!("Error when cleaning dangling tags - {}", err); - } - }); - Ok(Html( SubmitPostDetailsTemplate { post, sources, tags, - tags_text, parent_post, can_edit: true, } @@ -1905,11 +1615,9 @@ pub(crate) async fn delete_post( SameyPost::delete_by_id(post.id).exec(&db).await?; tokio::spawn(async move { - let _ = std::fs::remove_file(files_dir.join(post.media)); - let _ = std::fs::remove_file(files_dir.join(post.thumbnail)); - if let Err(err) = clean_dangling_tags(&db).await { - println!("Error when cleaning dangling tags - {}", err); - } + let base_path = files_dir.as_ref(); + let _ = std::fs::remove_file(base_path.join(post.media)); + let _ = std::fs::remove_file(base_path.join(post.thumbnail)); }); Ok(Redirect::to("/")) diff --git a/templates/fragments/common_headers.html b/templates/fragments/common_headers.html index f02d8a9..47eacf4 100644 --- a/templates/fragments/common_headers.html +++ b/templates/fragments/common_headers.html @@ -1,6 +1,5 @@ - diff --git a/templates/fragments/edit_post_details.html b/templates/fragments/edit_post_details.html index 64f6616..6fdf1ed 100644 --- a/templates/fragments/edit_post_details.html +++ b/templates/fragments/edit_post_details.html @@ -1,55 +1,46 @@ -
-
-
- - {% let tags_value = tags %} {% include "fragments/tags_input.html" %} - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
      - {% for source in sources %} - {% include "fragments/post_source.html" %} - {% endfor %} -
    - -
    -
    - - -
    -
    - - - -
    -
    -
    \ No newline at end of file +
    +
    + + {% let tags_value = tags %} {% include "fragments/tags_input.html" %} +
      +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + + +
      +
      + +
        + {% for source in sources %} + {% include "fragments/post_source.html" %} + {% endfor %} +
      + +
      +
      + + +
      +
      + + + +
      + diff --git a/templates/fragments/get_video_media.html b/templates/fragments/get_video_media.html index feff536..7e130c0 100644 --- a/templates/fragments/get_video_media.html +++ b/templates/fragments/get_video_media.html @@ -1,7 +1,6 @@ diff --git a/templates/fragments/post_details.html b/templates/fragments/post_details.html index e399887..1283837 100644 --- a/templates/fragments/post_details.html +++ b/templates/fragments/post_details.html @@ -1,23 +1,24 @@ -
      +

      - {% if let Some(title) = post.title %}{{ title }}{% else %}Details{% endif %} + {% if let Some(title) = post.title %}{{ title }}{% else %}Details{% + endif %}

      {% if let Some(description) = post.description %}
      {{ description | markdown }}
      {% endif %} - {% if can_edit %} - + - {% endif %} @@ -53,12 +54,6 @@
      Is public post?{% if post.is_public %}Yes{% else %}No{% endif %} + {% if post.is_public %}Yes{% else %}No{% endif %} +
      Rating - {% match post.rating.as_ref() %} {% when "u" %} Unrated {% when "s" %} - Safe {% when "q" %} Questionable {% when "e" %} Explicit {% else %} - Unknown {% endmatch %} + {% match post.rating.as_ref() %} {% when "u" %} Unrated {% when "s" %} Safe + {% when "q" %} Questionable {% when "e" %} Explicit {% else %} Unknown {% + endmatch %}
      {% if can_edit %} - + {% endif %} -
      + diff --git a/templates/fragments/rss_entry.html b/templates/fragments/rss_entry.html deleted file mode 100644 index 246e3b3..0000000 --- a/templates/fragments/rss_entry.html +++ /dev/null @@ -1,11 +0,0 @@ -

      - {% if let Some(title) = post.title %}{{ title }}{% else %}Details{% endif %} -

      -{% match post.media_type.as_ref() %}{% when "image" %} - -{% when "video" %} - -{% else %}{% endmatch %}{% if let Some(description) = post.description %} -

      Description

      -
      {{ description | markdown }}
      -{% endif %} diff --git a/templates/fragments/search_tags.html b/templates/fragments/search_tags.html index dfa176b..c01593f 100644 --- a/templates/fragments/search_tags.html +++ b/templates/fragments/search_tags.html @@ -1,10 +1,4 @@ -{% if !tags.is_empty() %} -
    • - -
    • -{% endif %}{% for tag in tags %} +{% for tag in tags %}
    • {% endif %} -
      -

      Tags

      - {% if tags.is_empty() %} -

      No tags in post. Consider adding some!

      - {% else %} - - {% endif %} -
      + diff --git a/templates/fragments/tags_input.html b/templates/fragments/tags_input.html index 4c6ad96..4fd09d9 100644 --- a/templates/fragments/tags_input.html +++ b/templates/fragments/tags_input.html @@ -5,11 +5,10 @@ name="tags" placeholder="Tags" hx-post="/search_tags" - hx-trigger="input changed delay:500ms" + hx-trigger="input changed" hx-target="next .tags-autocomplete" - hx-swap="innerHTML" - hx-vals="js:{selection_end: document.querySelector('.tags').selectionEnd}" - hx-on::after-settle="document.querySelector('.tags').focus(); document.querySelector('.tags').setSelectionRange(-1, -1);" + hx-vals="js:{selection_end: event.target.selectionEnd}" + hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);" value="{{ tags_value }}" aria-autocomplete="list" aria-controls="search-autocomplete" diff --git a/templates/pages/bad_request.html b/templates/pages/bad_request.html deleted file mode 100644 index 6ae29a3..0000000 --- a/templates/pages/bad_request.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Bad request - {% include "fragments/common_headers.html" %} - - - -
      -

      Bad request

      -

      The provided parameters are invalid: {{ error }}

      -
      - - diff --git a/templates/pages/bulk_edit_tag.html b/templates/pages/bulk_edit_tag.html deleted file mode 100644 index 0d06b0f..0000000 --- a/templates/pages/bulk_edit_tag.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - Bulk edit tag - {{ application_name }} - - {% include "fragments/common_headers.html" %} - - - {% if age_confirmation %}{% include "fragments/age_restricted_check.html" - %}{% endif %} - -
      -

      Bulk edit tag

      -
      -

      Select tag to edit

      - - {% let tags_value = "" %} {% include "fragments/tags_input.html" %} - -
        - - - {% match message %}{% when BulkEditTagMessage::Success %} -
        Success!
        - {% when BulkEditTagMessage::Failure with (msg) %} -
        Error: {{ msg }}
        - {% when BulkEditTagMessage::None %}{% endmatch %} - -
        -
        - - diff --git a/templates/pages/forbidden.html b/templates/pages/forbidden.html deleted file mode 100644 index 994b195..0000000 --- a/templates/pages/forbidden.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Forbidden - {% include "fragments/common_headers.html" %} - - - -
        -

        Forbidden

        -

        The requested action is not allowed.

        -
        - - diff --git a/templates/pages/index.html b/templates/pages/index.html index 8425e5e..407bebc 100644 --- a/templates/pages/index.html +++ b/templates/pages/index.html @@ -14,13 +14,6 @@

        Search

        {% let tags_value = "" %} {% include "fragments/tags_input.html" %} -
          @@ -41,9 +34,6 @@ Create pool {% if user.is_admin %} -
        • - Bulk edit tag -
        • Settings
        • diff --git a/templates/pages/internal_server_error.html b/templates/pages/internal_server_error.html deleted file mode 100644 index 3b7721e..0000000 --- a/templates/pages/internal_server_error.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - Internal server error - {% include "fragments/common_headers.html" %} - - - -
          -

          Internal server error

          -

          - Something went wrong! Ask your administrator to check the application - logs. -

          -
          - - diff --git a/templates/pages/pool.html b/templates/pages/pool.html index 3135733..74f5ba5 100644 --- a/templates/pages/pool.html +++ b/templates/pages/pool.html @@ -106,16 +106,6 @@ value="true" /> -
          - -
          {% endif %} diff --git a/templates/pages/posts.html b/templates/pages/posts.html index 2356019..e89c4b8 100644 --- a/templates/pages/posts.html +++ b/templates/pages/posts.html @@ -9,18 +9,10 @@ {% if age_confirmation %}{% include "fragments/age_restricted_check.html" %}{% endif %} -

          Search

          {% let tags_value = tags_text.clone().unwrap_or("".into()) %} {% include "fragments/tags_input.html" %} -
            diff --git a/templates/pages/settings.html b/templates/pages/settings.html index 23a764d..ff79c13 100644 --- a/templates/pages/settings.html +++ b/templates/pages/settings.html @@ -20,14 +20,6 @@ value="{{ application_name }}" /> -
            - - -
            -
            - - -
            - - - Unauthorized - {% include "fragments/common_headers.html" %} - - - -
            -

            Unauthorized

            -

            The provided credentials are invalid.

            -
            - - diff --git a/templates/pages/upload.html b/templates/pages/upload.html index e3853d2..f6533a6 100644 --- a/templates/pages/upload.html +++ b/templates/pages/upload.html @@ -13,13 +13,6 @@

            Upload media

            {% let tags_value = "" %} {% include "fragments/tags_input.html" %} -
              < To posts
              +
              +

              Search

              + + +
                + + +
                {% for item in pool_data %} @@ -92,19 +112,15 @@ {% endif %} -
                +

                Tags

                {% if tags.is_empty() %}

                No tags in post. Consider adding some!

                {% else %} -