allow intro uploads

pull/5/head v0.1.4-alpha
Patrick Cleavelin 2023-03-08 21:29:49 -06:00
parent 665e83a6fe
commit 9bedffa616
5 changed files with 90 additions and 12 deletions

View File

@ -46,7 +46,7 @@
packages = with pkgs; flake-utils.lib.flattenTree rec { packages = with pkgs; flake-utils.lib.flattenTree rec {
default = rustPlatform.buildRustPackage rec { default = rustPlatform.buildRustPackage rec {
name = "memejoin-rs"; name = "memejoin-rs";
version = "0.1.2-alpha"; version = "0.1.4-alpha";
src = self; src = self;
buildInputs = [ openssl.dev ]; buildInputs = [ openssl.dev ];
nativeBuildInputs = [ local-rust pkg-config openssl openssl.dev cmake gcc libopus ]; nativeBuildInputs = [ local-rust pkg-config openssl openssl.dev cmake gcc libopus ];
@ -58,7 +58,7 @@
docker = dockerTools.buildImage { docker = dockerTools.buildImage {
name = "memejoin-rs"; name = "memejoin-rs";
tag = "0.1.2-alpha"; tag = "0.1.4-alpha";
copyToRoot = buildEnv { copyToRoot = buildEnv {
name = "image-root"; name = "image-root";
paths = [ default cacert openssl openssl.dev ffmpeg libopus youtube-dl yt-dlp ]; paths = [ default cacert openssl openssl.dev ffmpeg libopus youtube-dl yt-dlp ];

View File

@ -39,7 +39,7 @@ impl Permissions {
#[repr(u8)] #[repr(u8)]
pub enum Permission { pub enum Permission {
None, None,
DownloadSounds, UploadSounds,
} }
impl Permission { impl Permission {

View File

@ -3,6 +3,7 @@
#![feature(async_closure)] #![feature(async_closure)]
mod auth; mod auth;
mod media;
mod routes; mod routes;
pub mod settings; pub mod settings;
@ -135,6 +136,7 @@ fn spawn_api(settings: Arc<Mutex<Settings>>) {
.route("/health", get(routes::health)) .route("/health", get(routes::health))
.route("/me", get(routes::me)) .route("/me", get(routes::me))
.route("/intros/:guild/add", get(routes::add_guild_intro)) .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", get(routes::intros))
.route( .route(
"/intros/:guild/:channel/:intro", "/intros/:guild/:channel/:intro",

20
src/media.rs Normal file
View File

@ -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(())
}

View File

@ -1,6 +1,7 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use axum::{ use axum::{
body::Bytes,
extract::{Path, Query, State}, extract::{Path, Query, State},
http::HeaderMap, http::HeaderMap,
response::IntoResponse, response::IntoResponse,
@ -13,8 +14,11 @@ use serde_json::{json, Value};
use tracing::{error, info}; use tracing::{error, info};
use uuid::Uuid; use uuid::Uuid;
use crate::settings::{ApiState, GuildUser, Intro, IntroIndex, UserSettings};
use crate::{auth, settings::FileIntro}; use crate::{auth, settings::FileIntro};
use crate::{
media,
settings::{ApiState, GuildUser, Intro, IntroIndex, UserSettings},
};
#[derive(Serialize)] #[derive(Serialize)]
pub(crate) enum IntroResponse<'a> { pub(crate) enum IntroResponse<'a> {
@ -71,9 +75,13 @@ pub(crate) enum Error {
InvalidPermission, InvalidPermission,
#[error("{0}")] #[error("{0}")]
Ytdl(#[from] std::io::Error), Ytdl(#[from] std::io::Error),
#[error("{0}")]
Ffmpeg(String),
#[error("ytdl terminated unsuccessfully")] #[error("ytdl terminated unsuccessfully")]
YtdlTerminated, YtdlTerminated,
#[error("ffmpeg terminated unsuccessfully")]
FfmpegTerminated,
} }
impl IntoResponse for Error { impl IntoResponse for Error {
@ -92,7 +100,8 @@ impl IntoResponse for Error {
Self::Ytdl(error) => { Self::Ytdl(error) => {
(StatusCode::INTERNAL_SERVER_ERROR, error.to_string()).into_response() (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() (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 // TODO: don't save on every change
//if let Err(err) = settings.save() { if let Err(err) = settings.save() {
// error!("Failed to save config: {err:?}"); 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 // TODO: don't save on every change
//if let Err(err) = settings.save() { if let Err(err) = settings.save() {
// error!("Failed to save config: {err:?}"); error!("Failed to save config: {err:?}");
//} }
} }
pub(crate) async fn intros( pub(crate) async fn intros(
@ -294,6 +303,7 @@ pub(crate) async fn me(
g.1.users g.1.users
// TODO: why must clone // TODO: why must clone
.entry(username.clone()) .entry(username.clone())
// TODO: check if owner for permissions
.or_insert(Default::default()); .or_insert(Default::default());
let mut guild = MeGuild { let mut guild = MeGuild {
@ -326,6 +336,52 @@ pub(crate) async fn me(
} }
} }
pub(crate) async fn upload_guild_intro(
State(state): State<Arc<ApiState>>,
Path(guild): Path<u64>,
Query(mut params): Query<HashMap<String, String>>,
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( pub(crate) async fn add_guild_intro(
State(state): State<Arc<ApiState>>, State(state): State<Arc<ApiState>>,
Path(guild): Path<u64>, Path(guild): Path<u64>,
@ -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) }; 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); return Err(Error::InvalidPermission);
} }
} }