diff --git a/flake.nix b/flake.nix index f57ec4a..19c96e4 100644 --- a/flake.nix +++ b/flake.nix @@ -46,7 +46,7 @@ packages = with pkgs; flake-utils.lib.flattenTree rec { default = rustPlatform.buildRustPackage rec { name = "memejoin-rs"; - version = "0.1.2-alpha"; + version = "0.1.4-alpha"; src = self; buildInputs = [ openssl.dev ]; nativeBuildInputs = [ local-rust pkg-config openssl openssl.dev cmake gcc libopus ]; @@ -58,7 +58,7 @@ docker = dockerTools.buildImage { name = "memejoin-rs"; - tag = "0.1.2-alpha"; + tag = "0.1.4-alpha"; copyToRoot = buildEnv { name = "image-root"; paths = [ default cacert openssl openssl.dev ffmpeg libopus youtube-dl yt-dlp ]; diff --git a/src/auth.rs b/src/auth.rs index 497f4be..5bfaa0d 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -39,7 +39,7 @@ impl Permissions { #[repr(u8)] pub enum Permission { None, - DownloadSounds, + UploadSounds, } impl Permission { diff --git a/src/main.rs b/src/main.rs index fe61dd0..e8818e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ #![feature(async_closure)] mod auth; +mod media; mod routes; pub mod settings; @@ -135,6 +136,7 @@ fn spawn_api(settings: Arc>) { .route("/health", get(routes::health)) .route("/me", get(routes::me)) .route("/intros/:guild/add", get(routes::add_guild_intro)) + .route("/intros/:guild/upload", post(routes::upload_guild_intro)) .route("/intros/:guild", get(routes::intros)) .route( "/intros/:guild/:channel/:intro", diff --git a/src/media.rs b/src/media.rs new file mode 100644 index 0000000..4c25888 --- /dev/null +++ b/src/media.rs @@ -0,0 +1,20 @@ +use crate::routes::Error; + +pub(crate) async fn normalize(src: &str, dest: &str) -> Result<(), Error> { + let child = tokio::process::Command::new("ffmpeg") + .args(["-i", src]) + .arg("-vn") + .args(["-map", "0:a"]) + .arg(dest) + .spawn() + .map_err(|err| Error::Ffmpeg(err.to_string()))? + .wait() + .await + .map_err(|err| Error::Ffmpeg(err.to_string()))?; + + if !child.success() { + return Err(Error::FfmpegTerminated); + } + + Ok(()) +} diff --git a/src/routes.rs b/src/routes.rs index 60cda9a..69fa37f 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -1,6 +1,7 @@ use std::{collections::HashMap, sync::Arc}; use axum::{ + body::Bytes, extract::{Path, Query, State}, http::HeaderMap, response::IntoResponse, @@ -13,8 +14,11 @@ use serde_json::{json, Value}; use tracing::{error, info}; use uuid::Uuid; -use crate::settings::{ApiState, GuildUser, Intro, IntroIndex, UserSettings}; use crate::{auth, settings::FileIntro}; +use crate::{ + media, + settings::{ApiState, GuildUser, Intro, IntroIndex, UserSettings}, +}; #[derive(Serialize)] pub(crate) enum IntroResponse<'a> { @@ -71,9 +75,13 @@ pub(crate) enum Error { InvalidPermission, #[error("{0}")] Ytdl(#[from] std::io::Error), + #[error("{0}")] + Ffmpeg(String), #[error("ytdl terminated unsuccessfully")] YtdlTerminated, + #[error("ffmpeg terminated unsuccessfully")] + FfmpegTerminated, } impl IntoResponse for Error { @@ -92,7 +100,8 @@ impl IntoResponse for Error { Self::Ytdl(error) => { (StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response() } - Self::YtdlTerminated => { + Self::Ffmpeg(error) => (StatusCode::INTERNAL_SERVER_ERROR, error).into_response(), + Self::YtdlTerminated | Self::FfmpegTerminated => { (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() } } @@ -224,9 +233,9 @@ pub(crate) async fn add_intro_to_user( }); // TODO: don't save on every change - //if let Err(err) = settings.save() { - // error!("Failed to save config: {err:?}"); - //} + if let Err(err) = settings.save() { + error!("Failed to save config: {err:?}"); + } } } @@ -255,9 +264,9 @@ pub(crate) async fn remove_intro_to_user( } // TODO: don't save on every change - //if let Err(err) = settings.save() { - // error!("Failed to save config: {err:?}"); - //} + if let Err(err) = settings.save() { + error!("Failed to save config: {err:?}"); + } } pub(crate) async fn intros( @@ -294,6 +303,7 @@ pub(crate) async fn me( g.1.users // TODO: why must clone .entry(username.clone()) + // TODO: check if owner for permissions .or_insert(Default::default()); let mut guild = MeGuild { @@ -326,6 +336,52 @@ pub(crate) async fn me( } } +pub(crate) async fn upload_guild_intro( + State(state): State>, + Path(guild): Path, + Query(mut params): Query>, + headers: HeaderMap, + file: Bytes, +) -> Result<(), Error> { + let mut settings = state.settings.lock().await; + + let Some(token) = headers.get("token").and_then(|v| v.to_str().ok()) else { return Err(Error::NoUserFound); }; + let Some(friendly_name) = params.remove("name") else { return Err(Error::InvalidRequest); }; + + { + let Some(guild) = settings.guilds.get(&guild) else { return Err(Error::NoGuildFound); }; + let auth_user = match settings.auth_users.get(token) { + Some(user) => user, + None => return Err(Error::NoUserFound), + }; + let Some(guild_user) = guild.users.get(&auth_user.name) else { return Err(Error::NoUserFound) }; + + if !guild_user.permissions.can(auth::Permission::UploadSounds) { + return Err(Error::InvalidPermission); + } + } + + let Some(guild) = settings.guilds.get_mut(&guild) else { return Err(Error::NoGuildFound); }; + let uuid = Uuid::new_v4().to_string(); + let temp_path = format!("./sounds/temp/{uuid}"); + let dest_path = format!("./sounds/{uuid}.mp3"); + + // Write original file so its ready for codec conversion + std::fs::write(&temp_path, file)?; + media::normalize(&temp_path, &dest_path).await?; + std::fs::remove_file(&temp_path)?; + + guild.intros.insert( + uuid.clone(), + Intro::File(FileIntro { + filename: format!("{uuid}.mp3"), + friendly_name, + }), + ); + + Ok(()) +} + pub(crate) async fn add_guild_intro( State(state): State>, Path(guild): Path, @@ -346,7 +402,7 @@ pub(crate) async fn add_guild_intro( }; let Some(guild_user) = guild.users.get(&auth_user.name) else { return Err(Error::NoUserFound) }; - if !guild_user.permissions.can(auth::Permission::DownloadSounds) { + if !guild_user.permissions.can(auth::Permission::UploadSounds) { return Err(Error::InvalidPermission); } }