Compare commits
3 Commits
04a0d499e1
...
2b3e681a38
Author | SHA1 | Date |
---|---|---|
|
2b3e681a38 | |
|
b449a900fe | |
|
4a9f826369 |
|
@ -139,6 +139,7 @@ fn spawn_api(settings: Arc<Mutex<Settings>>) {
|
||||||
.route("/index.html", get(page::home))
|
.route("/index.html", get(page::home))
|
||||||
.route("/login", get(page::login))
|
.route("/login", get(page::login))
|
||||||
.route("/guild/:guild_id", get(page::guild_dashboard))
|
.route("/guild/:guild_id", get(page::guild_dashboard))
|
||||||
|
.route("/v2/auth", get(routes::v2_auth))
|
||||||
.route(
|
.route(
|
||||||
"/v2/intros/add/:guild_id/:channel",
|
"/v2/intros/add/:guild_id/:channel",
|
||||||
post(routes::v2_add_intro_to_user),
|
post(routes::v2_add_intro_to_user),
|
||||||
|
@ -147,6 +148,11 @@ fn spawn_api(settings: Arc<Mutex<Settings>>) {
|
||||||
"/v2/intros/remove/:guild_id/:channel",
|
"/v2/intros/remove/:guild_id/:channel",
|
||||||
post(routes::v2_remove_intro_from_user),
|
post(routes::v2_remove_intro_from_user),
|
||||||
)
|
)
|
||||||
|
.route("/v2/intros/:guild/add", get(routes::v2_add_guild_intro))
|
||||||
|
.route(
|
||||||
|
"/v2/intros/:guild/upload",
|
||||||
|
post(routes::v2_upload_guild_intro),
|
||||||
|
)
|
||||||
.route("/health", get(routes::health))
|
.route("/health", get(routes::health))
|
||||||
.route("/me", get(routes::me))
|
.route("/me", get(routes::me))
|
||||||
.route("/intros/:guild", get(routes::intros))
|
.route("/intros/:guild", get(routes::intros))
|
||||||
|
@ -162,7 +168,6 @@ fn spawn_api(settings: Arc<Mutex<Settings>>) {
|
||||||
post(routes::remove_intro_to_user),
|
post(routes::remove_intro_to_user),
|
||||||
)
|
)
|
||||||
.route("/auth", get(routes::auth))
|
.route("/auth", get(routes::auth))
|
||||||
.route("/v2/auth", get(routes::v2_auth))
|
|
||||||
.layer(
|
.layer(
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
// TODO: move this to env variable
|
// TODO: move this to env variable
|
||||||
|
|
74
src/page.rs
74
src/page.rs
|
@ -54,7 +54,10 @@ fn guild_list<'a>(
|
||||||
) -> HtmxBuilder {
|
) -> HtmxBuilder {
|
||||||
HtmxBuilder::new(Tag::Empty).ul(|b| {
|
HtmxBuilder::new(Tag::Empty).ul(|b| {
|
||||||
let mut b = b;
|
let mut b = b;
|
||||||
|
let mut in_any_guilds = false;
|
||||||
for (guild_id, guild_settings) in guilds {
|
for (guild_id, guild_settings) in guilds {
|
||||||
|
in_any_guilds = true;
|
||||||
|
|
||||||
b = b.li(|b| {
|
b = b.li(|b| {
|
||||||
b.link(
|
b.link(
|
||||||
&guild_settings.name,
|
&guild_settings.name,
|
||||||
|
@ -63,6 +66,10 @@ fn guild_list<'a>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !in_any_guilds {
|
||||||
|
b = b.builder_text(Tag::Header4, "Looks like you aren't in any guilds");
|
||||||
|
}
|
||||||
|
|
||||||
b
|
b
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -111,6 +118,7 @@ pub(crate) async fn guild_dashboard(
|
||||||
return Err(Redirect::to(&format!("{}/", state.origin)));
|
return Err(Redirect::to(&format!("{}/", state.origin)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let can_upload = guild_user.permissions.can(auth::Permission::UploadSounds);
|
||||||
let is_moderator = guild_user.permissions.can(auth::Permission::DeleteSounds);
|
let is_moderator = guild_user.permissions.can(auth::Permission::DeleteSounds);
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
|
@ -124,7 +132,7 @@ pub(crate) async fn guild_dashboard(
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.builder(Tag::Empty, |b| {
|
.builder(Tag::Empty, |b| {
|
||||||
if is_moderator {
|
let mut b = if is_moderator {
|
||||||
b.builder(Tag::Div, |b| {
|
b.builder(Tag::Div, |b| {
|
||||||
b.attribute("class", "container")
|
b.attribute("class", "container")
|
||||||
.builder(Tag::Article, |b| {
|
.builder(Tag::Article, |b| {
|
||||||
|
@ -135,8 +143,27 @@ pub(crate) async fn guild_dashboard(
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
b
|
b
|
||||||
}
|
};
|
||||||
|
b = if can_upload {
|
||||||
|
b.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Article, |b| {
|
||||||
|
b.builder_text(Tag::Header, "Upload New Intro")
|
||||||
|
.push_builder(upload_form(&state.origin, guild_id))
|
||||||
|
})
|
||||||
|
})
|
||||||
.builder(Tag::Div, |b| {
|
.builder(Tag::Div, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.builder(Tag::Article, |b| {
|
||||||
|
b.builder_text(Tag::Header, "Upload New Intro from Url")
|
||||||
|
.push_builder(ytdl_form(&state.origin, guild_id))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
b
|
||||||
|
};
|
||||||
|
|
||||||
|
b.builder(Tag::Div, |b| {
|
||||||
b.attribute("class", "container")
|
b.attribute("class", "container")
|
||||||
.builder(Tag::Article, |b| {
|
.builder(Tag::Article, |b| {
|
||||||
let mut b = b.builder_text(Tag::Header, "Guild Intros");
|
let mut b = b.builder_text(Tag::Header, "Guild Intros");
|
||||||
|
@ -198,6 +225,49 @@ pub(crate) async fn guild_dashboard(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn upload_form(origin: &str, guild_id: u64) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty).form(|b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.hx_post(&format!("{}/v2/intros/{}/upload", origin, guild_id))
|
||||||
|
.attribute("hx-encoding", "multipart/form-data")
|
||||||
|
.builder(Tag::FieldSet, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.input(|b| {
|
||||||
|
b.attribute("name", "name")
|
||||||
|
.attribute("placeholder", "enter intro title")
|
||||||
|
})
|
||||||
|
.label(|b| {
|
||||||
|
b.text("Choose File")
|
||||||
|
.input(|b| b.attribute("type", "file").attribute("name", "file"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.button(|b| b.attribute("type", "submit").text("Upload"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ytdl_form(origin: &str, guild_id: u64) -> HtmxBuilder {
|
||||||
|
HtmxBuilder::new(Tag::Empty).form(|b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.hx_get(&format!("{}/v2/intros/{}/add", origin, guild_id))
|
||||||
|
.builder(Tag::FieldSet, |b| {
|
||||||
|
b.attribute("class", "container")
|
||||||
|
.label(|b| {
|
||||||
|
b.text("Video Url").input(|b| {
|
||||||
|
b.attribute("placeholder", "enter video url")
|
||||||
|
.attribute("name", "url")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.label(|b| {
|
||||||
|
b.text("Intro Title").input(|b| {
|
||||||
|
b.attribute("placeholder", "enter intro title")
|
||||||
|
.attribute("name", "name")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.button(|b| b.attribute("type", "submit").text("Upload"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn moderator_dashboard(state: &ApiState) -> HtmxBuilder {
|
fn moderator_dashboard(state: &ApiState) -> HtmxBuilder {
|
||||||
HtmxBuilder::new(Tag::Empty).link("Go back to old UI", &format!("{}/old", state.origin))
|
HtmxBuilder::new(Tag::Empty).link("Go back to old UI", &format!("{}/old", state.origin))
|
||||||
}
|
}
|
||||||
|
|
130
src/routes.rs
130
src/routes.rs
|
@ -611,6 +611,77 @@ pub(crate) async fn upload_guild_intro(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn v2_upload_guild_intro(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Path(guild): Path<u64>,
|
||||||
|
user: User,
|
||||||
|
mut form_data: Multipart,
|
||||||
|
) -> Result<HeaderMap, Error> {
|
||||||
|
let mut settings = state.settings.lock().await;
|
||||||
|
let mut friendly_name = None;
|
||||||
|
let mut file = None;
|
||||||
|
|
||||||
|
while let Ok(Some(field)) = form_data.next_field().await {
|
||||||
|
let Some(field_name) = field.name() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if field_name.eq_ignore_ascii_case("name") {
|
||||||
|
friendly_name = Some(field.text().await.map_err(|_| Error::InvalidRequest)?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if field_name.eq_ignore_ascii_case("file") {
|
||||||
|
file = Some(field.bytes().await.map_err(|_| Error::InvalidRequest)?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(friendly_name) = friendly_name else {
|
||||||
|
return Err(Error::InvalidRequest);
|
||||||
|
};
|
||||||
|
let Some(file) = file else {
|
||||||
|
return Err(Error::InvalidRequest);
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let Some(guild) = settings.guilds.get(&guild) else {
|
||||||
|
return Err(Error::NoGuildFound);
|
||||||
|
};
|
||||||
|
let Some(guild_user) = guild.users.get(&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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Refresh", HeaderValue::from_static("true"));
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn add_guild_intro(
|
pub(crate) async fn add_guild_intro(
|
||||||
State(state): State<ApiState>,
|
State(state): State<ApiState>,
|
||||||
Path(guild): Path<u64>,
|
Path(guild): Path<u64>,
|
||||||
|
@ -676,6 +747,65 @@ pub(crate) async fn add_guild_intro(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn v2_add_guild_intro(
|
||||||
|
State(state): State<ApiState>,
|
||||||
|
Path(guild): Path<u64>,
|
||||||
|
Query(mut params): Query<HashMap<String, String>>,
|
||||||
|
user: User,
|
||||||
|
) -> Result<HeaderMap, Error> {
|
||||||
|
let mut settings = state.settings.lock().await;
|
||||||
|
let Some(url) = params.remove("url") else {
|
||||||
|
return Err(Error::InvalidRequest);
|
||||||
|
};
|
||||||
|
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 Some(guild_user) = guild.users.get(&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 child = tokio::process::Command::new("yt-dlp")
|
||||||
|
.arg(&url)
|
||||||
|
.args(["-o", &format!("sounds/{uuid}")])
|
||||||
|
.args(["-x", "--audio-format", "mp3"])
|
||||||
|
.spawn()
|
||||||
|
.map_err(Error::Ytdl)?
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(Error::Ytdl)?;
|
||||||
|
|
||||||
|
if !child.success() {
|
||||||
|
return Err(Error::YtdlTerminated);
|
||||||
|
}
|
||||||
|
|
||||||
|
guild.intros.insert(
|
||||||
|
uuid.clone(),
|
||||||
|
Intro::File(FileIntro {
|
||||||
|
filename: format!("{uuid}.mp3"),
|
||||||
|
friendly_name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Refresh", HeaderValue::from_static("true"));
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn delete_guild_intro(
|
pub(crate) async fn delete_guild_intro(
|
||||||
State(state): State<ApiState>,
|
State(state): State<ApiState>,
|
||||||
Path(guild): Path<u64>,
|
Path(guild): Path<u64>,
|
||||||
|
|
Loading…
Reference in New Issue