Compare commits
2 Commits
c4d12562a1
...
752ce3f16c
| Author | SHA1 | Date |
|---|---|---|
|
|
752ce3f16c | |
|
|
79a2f2839f |
|
|
@ -15,12 +15,12 @@
|
||||||
};
|
};
|
||||||
yt-dlp = pkgs.yt-dlp.overrideAttrs (oldAttr: rec {
|
yt-dlp = pkgs.yt-dlp.overrideAttrs (oldAttr: rec {
|
||||||
inherit (oldAttr) name;
|
inherit (oldAttr) name;
|
||||||
version = "2024.05.27";
|
version = "2025.09.26";
|
||||||
src = pkgs.fetchFromGitHub {
|
src = pkgs.fetchFromGitHub {
|
||||||
owner = "yt-dlp";
|
owner = "yt-dlp";
|
||||||
repo = "yt-dlp";
|
repo = "yt-dlp";
|
||||||
rev = "${version}";
|
rev = "${version}";
|
||||||
sha256 = "55zDAMwCJPn5zKrAFw4ogTxxmvjrv4PvhYO7PsHbRo4=";
|
sha256 = "/uzs87Vw+aDNfIJVLOx3C8RyZvWLqjggmnjrOvUX1Eg=";
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
local-rust = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain).override {
|
local-rust = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain).override {
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
{
|
{
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
|
git
|
||||||
local-rust
|
local-rust
|
||||||
rust-analyzer
|
rust-analyzer
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
|
|
||||||
use crate::lib::domain::intro_tool::{
|
use crate::lib::domain::intro_tool::{
|
||||||
models,
|
models::{self, guild::IntroId},
|
||||||
ports::{IntroToolRepository, IntroToolService},
|
ports::{IntroToolRepository, IntroToolService},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -112,7 +112,7 @@ where
|
||||||
async fn add_intro_to_guild(
|
async fn add_intro_to_guild(
|
||||||
&self,
|
&self,
|
||||||
req: models::guild::AddIntroToGuildRequest,
|
req: models::guild::AddIntroToGuildRequest,
|
||||||
) -> Result<(), models::guild::AddIntroToGuildError> {
|
) -> Result<IntroId, models::guild::AddIntroToGuildError> {
|
||||||
self.wrapped_service.add_intro_to_guild(req).await
|
self.wrapped_service.add_intro_to_guild(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,10 +258,16 @@ pub struct CreateChannelRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AddIntroToGuildRequest {
|
pub struct AddIntroToGuildRequest {
|
||||||
guild_id: GuildId,
|
pub guild_id: GuildId,
|
||||||
name: String,
|
pub name: String,
|
||||||
volume: i32,
|
pub volume: i32,
|
||||||
filename: String,
|
|
||||||
|
pub data: IntroRequestData,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum IntroRequestData {
|
||||||
|
Data(Vec<u8>),
|
||||||
|
Url(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AddIntroToUserRequest {
|
pub struct AddIntroToUserRequest {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{collections::HashMap, future::Future};
|
use std::{collections::HashMap, future::Future};
|
||||||
|
|
||||||
use crate::lib::domain::intro_tool::models::guild::ChannelName;
|
use crate::lib::domain::intro_tool::models::guild::{ChannelName, IntroId};
|
||||||
|
|
||||||
use super::models::guild::{
|
use super::models::guild::{
|
||||||
AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserError, AddIntroToUserRequest,
|
AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserError, AddIntroToUserRequest,
|
||||||
|
|
@ -44,10 +44,10 @@ pub trait IntroToolService: Send + Sync + Clone + 'static {
|
||||||
req: CreateChannelRequest,
|
req: CreateChannelRequest,
|
||||||
) -> Result<Channel, CreateChannelError>;
|
) -> Result<Channel, CreateChannelError>;
|
||||||
|
|
||||||
async fn add_intro_to_guild(
|
fn add_intro_to_guild(
|
||||||
&self,
|
&self,
|
||||||
req: AddIntroToGuildRequest,
|
req: AddIntroToGuildRequest,
|
||||||
) -> Result<(), AddIntroToGuildError>;
|
) -> impl Future<Output = Result<IntroId, AddIntroToGuildError>> + Send;
|
||||||
|
|
||||||
async fn add_intro_to_user(
|
async fn add_intro_to_user(
|
||||||
&self,
|
&self,
|
||||||
|
|
@ -104,10 +104,12 @@ pub trait IntroToolRepository: Send + Sync + Clone + 'static {
|
||||||
req: CreateChannelRequest,
|
req: CreateChannelRequest,
|
||||||
) -> Result<Channel, CreateChannelError>;
|
) -> Result<Channel, CreateChannelError>;
|
||||||
|
|
||||||
async fn add_intro_to_guild(
|
fn add_intro_to_guild(
|
||||||
&self,
|
&self,
|
||||||
req: AddIntroToGuildRequest,
|
name: &str,
|
||||||
) -> Result<(), AddIntroToGuildError>;
|
guild_id: GuildId,
|
||||||
|
filename: String,
|
||||||
|
) -> impl Future<Output = Result<IntroId, AddIntroToGuildError>> + Send;
|
||||||
|
|
||||||
async fn add_intro_to_user(
|
async fn add_intro_to_user(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
use crate::lib::domain::intro_tool::{
|
use anyhow::{anyhow, Context};
|
||||||
models::guild::{GetUserError, GuildId, User},
|
use uuid::Uuid;
|
||||||
ports::{IntroToolRepository, IntroToolService},
|
|
||||||
|
use crate::{
|
||||||
|
lib::domain::intro_tool::{
|
||||||
|
models::guild::{self, GetUserError, GuildId, IntroId, User},
|
||||||
|
ports::{IntroToolRepository, IntroToolService},
|
||||||
|
},
|
||||||
|
media,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::models;
|
use super::models;
|
||||||
|
|
@ -37,7 +43,7 @@ where
|
||||||
async fn get_guild(
|
async fn get_guild(
|
||||||
&self,
|
&self,
|
||||||
guild_id: impl Into<GuildId>,
|
guild_id: impl Into<GuildId>,
|
||||||
) -> Result<models::guild::Guild, models::guild::GetGuildError> {
|
) -> Result<guild::Guild, guild::GetGuildError> {
|
||||||
self.repo.get_guild(guild_id.into()).await
|
self.repo.get_guild(guild_id.into()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,21 +53,21 @@ where
|
||||||
async fn get_guild_intros(
|
async fn get_guild_intros(
|
||||||
&self,
|
&self,
|
||||||
guild_id: GuildId,
|
guild_id: GuildId,
|
||||||
) -> Result<Vec<models::guild::Intro>, models::guild::GetIntroError> {
|
) -> Result<Vec<guild::Intro>, guild::GetIntroError> {
|
||||||
self.repo.get_guild_intros(guild_id).await
|
self.repo.get_guild_intros(guild_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(
|
async fn get_user(
|
||||||
&self,
|
&self,
|
||||||
username: impl AsRef<str> + Send,
|
username: impl AsRef<str> + Send,
|
||||||
) -> Result<models::guild::User, models::guild::GetUserError> {
|
) -> Result<guild::User, guild::GetUserError> {
|
||||||
self.repo.get_user(username).await
|
self.repo.get_user(username).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user_guilds(
|
async fn get_user_guilds(
|
||||||
&self,
|
&self,
|
||||||
username: impl AsRef<str> + Send,
|
username: impl AsRef<str> + Send,
|
||||||
) -> Result<Vec<models::guild::GuildRef>, models::guild::GetGuildError> {
|
) -> Result<Vec<guild::GuildRef>, guild::GetGuildError> {
|
||||||
self.repo.get_user_guilds(username).await
|
self.repo.get_user_guilds(username).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,36 +77,79 @@ where
|
||||||
|
|
||||||
async fn create_guild(
|
async fn create_guild(
|
||||||
&self,
|
&self,
|
||||||
req: models::guild::CreateGuildRequest,
|
req: guild::CreateGuildRequest,
|
||||||
) -> Result<models::guild::Guild, models::guild::CreateGuildError> {
|
) -> Result<guild::Guild, guild::CreateGuildError> {
|
||||||
self.repo.create_guild(req).await
|
self.repo.create_guild(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
&self,
|
&self,
|
||||||
req: models::guild::CreateUserRequest,
|
req: guild::CreateUserRequest,
|
||||||
) -> Result<models::guild::User, models::guild::CreateUserError> {
|
) -> Result<guild::User, guild::CreateUserError> {
|
||||||
self.repo.create_user(req).await
|
self.repo.create_user(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_channel(
|
async fn create_channel(
|
||||||
&self,
|
&self,
|
||||||
req: models::guild::CreateChannelRequest,
|
req: guild::CreateChannelRequest,
|
||||||
) -> Result<models::guild::Channel, models::guild::CreateChannelError> {
|
) -> Result<guild::Channel, guild::CreateChannelError> {
|
||||||
self.repo.create_channel(req).await
|
self.repo.create_channel(req).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_intro_to_guild(
|
async fn add_intro_to_guild(
|
||||||
&self,
|
&self,
|
||||||
req: models::guild::AddIntroToGuildRequest,
|
req: guild::AddIntroToGuildRequest,
|
||||||
) -> Result<(), models::guild::AddIntroToGuildError> {
|
) -> Result<IntroId, guild::AddIntroToGuildError> {
|
||||||
self.repo.add_intro_to_guild(req).await
|
let file_name = match &req.data {
|
||||||
|
guild::IntroRequestData::Data(bytes) => {
|
||||||
|
// TODO: put this behind an interface
|
||||||
|
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, bytes).context("failed to write temp file")?;
|
||||||
|
media::normalize(&temp_path, &dest_path)
|
||||||
|
.await
|
||||||
|
.context("failed to normalize file")?;
|
||||||
|
std::fs::remove_file(&temp_path).context("failed to remove temp file")?;
|
||||||
|
|
||||||
|
dest_path
|
||||||
|
}
|
||||||
|
guild::IntroRequestData::Url(url) => {
|
||||||
|
let uuid = Uuid::new_v4().to_string();
|
||||||
|
let file_name = format!("sounds/{uuid}");
|
||||||
|
|
||||||
|
// TODO: put this behind an interface
|
||||||
|
let child = tokio::process::Command::new("yt-dlp")
|
||||||
|
.arg(url)
|
||||||
|
.args(["-o", &file_name])
|
||||||
|
.args(["-x", "--audio-format", "mp3"])
|
||||||
|
.spawn()
|
||||||
|
.context("failed to spawn yt-dlp process")?
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.context("yt-dlp process failed")?;
|
||||||
|
|
||||||
|
if !child.success() {
|
||||||
|
return Err(guild::AddIntroToGuildError::Unknown(anyhow!(
|
||||||
|
"yt-dlp terminated unsuccessfully"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
file_name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.repo
|
||||||
|
.add_intro_to_guild(&req.name, req.guild_id, file_name)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_intro_to_user(
|
async fn add_intro_to_user(
|
||||||
&self,
|
&self,
|
||||||
req: models::guild::AddIntroToUserRequest,
|
req: guild::AddIntroToUserRequest,
|
||||||
) -> Result<(), models::guild::AddIntroToUserError> {
|
) -> Result<(), guild::AddIntroToUserError> {
|
||||||
self.repo.add_intro_to_user(req).await
|
self.repo.add_intro_to_user(req).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod handlers;
|
||||||
mod page;
|
mod page;
|
||||||
|
|
||||||
use std::{net::SocketAddr, sync::Arc};
|
use std::{net::SocketAddr, sync::Arc};
|
||||||
|
|
@ -115,10 +116,12 @@ where
|
||||||
.route("/", get(page::home))
|
.route("/", 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("/", get(page::home))
|
.route("/v2/intros/:guild/add", get(handlers::add_guild_intro))
|
||||||
// .route("/index.html", get(page::home))
|
.route(
|
||||||
// .route("/login", get(page::login))
|
"/v2/intros/:guild/upload",
|
||||||
// .route("/guild/:guild_id", get(page::guild_dashboard))
|
post(handlers::upload_guild_intro),
|
||||||
|
)
|
||||||
|
|
||||||
// .route("/guild/:guild_id/setup", get(routes::guild_setup))
|
// .route("/guild/:guild_id/setup", get(routes::guild_setup))
|
||||||
// .route(
|
// .route(
|
||||||
// "/guild/:guild_id/add_channel",
|
// "/guild/:guild_id/add_channel",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{Multipart, Path, Query, State},
|
||||||
|
http::{HeaderMap, HeaderValue},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::lib::{
|
||||||
|
domain::intro_tool::{
|
||||||
|
models::guild::{AddIntroToGuildRequest, GuildId, IntroRequestData, User},
|
||||||
|
ports::IntroToolService,
|
||||||
|
},
|
||||||
|
inbound::{
|
||||||
|
http::ApiState,
|
||||||
|
response::{ApiError, ErrorAsRedirect},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
trait FromApi<T, P>: Sized {
|
||||||
|
async fn from_api(value: T, params: P) -> Result<Self, ApiError>;
|
||||||
|
}
|
||||||
|
trait IntoDomain<T, P> {
|
||||||
|
async fn into_domain(self, params: P) -> Result<T, ApiError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I, O: FromApi<I, P>, P> IntoDomain<O, P> for I {
|
||||||
|
async fn into_domain(self, params: P) -> Result<O, ApiError> {
|
||||||
|
O::from_api(self, params).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi<HashMap<String, String>, GuildId> for AddIntroToGuildRequest {
|
||||||
|
async fn from_api(value: HashMap<String, String>, params: GuildId) -> Result<Self, ApiError> {
|
||||||
|
let Some(url) = value.get("url") else {
|
||||||
|
return Err(ApiError::bad_request("url is required"));
|
||||||
|
};
|
||||||
|
if url.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("url cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(name) = value.get("name") else {
|
||||||
|
return Err(ApiError::bad_request("name is required"));
|
||||||
|
};
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("name cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
guild_id: params,
|
||||||
|
name: name.to_string(),
|
||||||
|
volume: 0,
|
||||||
|
data: IntroRequestData::Url(url.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromApi<Multipart, GuildId> for AddIntroToGuildRequest {
|
||||||
|
async fn from_api(mut form_data: Multipart, params: GuildId) -> Result<Self, ApiError> {
|
||||||
|
let mut 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") {
|
||||||
|
name = Some(field.text().await.map_err(|err| {
|
||||||
|
ApiError::bad_request(format!("expected text for name: {err:?}"))
|
||||||
|
})?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if field_name.eq_ignore_ascii_case("file") {
|
||||||
|
file = Some(field.bytes().await.map_err(|err| {
|
||||||
|
ApiError::bad_request(format!("expected bytes for file: {err:?}"))
|
||||||
|
})?);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(name) = name else {
|
||||||
|
return Err(ApiError::bad_request("name is required"));
|
||||||
|
};
|
||||||
|
if name.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("name cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(file) = file else {
|
||||||
|
return Err(ApiError::bad_request("file is required"));
|
||||||
|
};
|
||||||
|
if file.is_empty() {
|
||||||
|
return Err(ApiError::bad_request("file cannot be empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
guild_id: params,
|
||||||
|
name: name.to_string(),
|
||||||
|
volume: 0,
|
||||||
|
data: IntroRequestData::Data(file.to_vec()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn add_guild_intro<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
Path(guild_id): Path<u64>,
|
||||||
|
Query(params): Query<HashMap<String, String>>,
|
||||||
|
user: User,
|
||||||
|
) -> Result<HeaderMap, ApiError> {
|
||||||
|
let req = params.into_domain(guild_id.into()).await?;
|
||||||
|
|
||||||
|
let guild = state.intro_tool_service.get_guild(guild_id).await?;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// does user have access to this guild
|
||||||
|
if !user_guilds
|
||||||
|
.iter()
|
||||||
|
.any(|guild_ref| guild_ref.id() == guild.id())
|
||||||
|
{
|
||||||
|
return Err(ApiError::forbidden(
|
||||||
|
"You do not have access to this guild".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.intro_tool_service.add_intro_to_guild(req).await?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Refresh", HeaderValue::from_static("true"));
|
||||||
|
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn upload_guild_intro<S: IntroToolService>(
|
||||||
|
State(state): State<ApiState<S>>,
|
||||||
|
Path(guild_id): Path<u64>,
|
||||||
|
user: User,
|
||||||
|
form_data: Multipart,
|
||||||
|
) -> Result<HeaderMap, ApiError> {
|
||||||
|
let req = form_data.into_domain(guild_id.into()).await?;
|
||||||
|
|
||||||
|
let guild = state.intro_tool_service.get_guild(guild_id).await?;
|
||||||
|
let user_guilds = state
|
||||||
|
.intro_tool_service
|
||||||
|
.get_user_guilds(user.name())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// does user have access to this guild
|
||||||
|
if !user_guilds
|
||||||
|
.iter()
|
||||||
|
.any(|guild_ref| guild_ref.id() == guild.id())
|
||||||
|
{
|
||||||
|
return Err(ApiError::forbidden(
|
||||||
|
"You do not have access to this guild".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.intro_tool_service.add_intro_to_guild(req).await?;
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("HX-Refresh", HeaderValue::from_static("true"));
|
||||||
|
|
||||||
|
Ok(headers)
|
||||||
|
}
|
||||||
|
|
@ -133,6 +133,8 @@ pub async fn guild_dashboard<S: IntroToolService>(
|
||||||
return Err(Redirect::to(&format!("{}/error", state.origin)));
|
return Err(Redirect::to(&format!("{}/error", state.origin)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let can_upload = true;
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
HtmxBuilder::new(Tag::Html)
|
HtmxBuilder::new(Tag::Html)
|
||||||
.push_builder(page_header("MemeJoin - Dashboard"))
|
.push_builder(page_header("MemeJoin - Dashboard"))
|
||||||
|
|
@ -156,24 +158,24 @@ pub async fn guild_dashboard<S: IntroToolService>(
|
||||||
// } else {
|
// } else {
|
||||||
// b
|
// b
|
||||||
// };
|
// };
|
||||||
// b = if can_upload {
|
let b = if can_upload {
|
||||||
// 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| {
|
||||||
// b.builder_text(Tag::Header, "Upload New Intro")
|
b.builder_text(Tag::Header, "Upload New Intro")
|
||||||
// .push_builder(upload_form(&state.origin, guild_id))
|
.push_builder(upload_form(&state.origin, guild_id))
|
||||||
// })
|
})
|
||||||
// })
|
})
|
||||||
// .builder(Tag::Div, |b| {
|
.builder(Tag::Div, |b| {
|
||||||
// b.attribute("class", "container")
|
b.attribute("class", "container")
|
||||||
// .builder(Tag::Article, |b| {
|
.builder(Tag::Article, |b| {
|
||||||
// b.builder_text(Tag::Header, "Upload New Intro from Url")
|
b.builder_text(Tag::Header, "Upload New Intro from Url")
|
||||||
// .push_builder(ytdl_form(&state.origin, guild_id))
|
.push_builder(ytdl_form(&state.origin, guild_id))
|
||||||
// })
|
})
|
||||||
// })
|
})
|
||||||
// } else {
|
} else {
|
||||||
// b
|
b
|
||||||
// };
|
};
|
||||||
|
|
||||||
b.builder(Tag::Div, |b| {
|
b.builder(Tag::Div, |b| {
|
||||||
b.attribute("class", "container")
|
b.attribute("class", "container")
|
||||||
|
|
@ -290,3 +292,41 @@ fn intro_list<'a>(intros: impl Iterator<Item = &'a Intro>, label: &str, post: &s
|
||||||
.button(|b| b.attribute("type", "submit").text(label))
|
.button(|b| b.attribute("type", "submit").text(label))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
.attribute("role", "group")
|
||||||
|
.input(|b| b.attribute("type", "file").attribute("name", "file"))
|
||||||
|
.input(|b| {
|
||||||
|
b.attribute("name", "name")
|
||||||
|
.attribute("placeholder", "enter intro title")
|
||||||
|
})
|
||||||
|
.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")
|
||||||
|
.attribute("role", "group")
|
||||||
|
.input(|b| {
|
||||||
|
b.attribute("placeholder", "enter video url")
|
||||||
|
.attribute("name", "url")
|
||||||
|
})
|
||||||
|
.input(|b| {
|
||||||
|
b.attribute("placeholder", "enter intro title")
|
||||||
|
.attribute("name", "name")
|
||||||
|
})
|
||||||
|
.button(|b| b.attribute("type", "submit").text("Upload"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use axum::response::Redirect;
|
use axum::{
|
||||||
|
response::{IntoResponse, Redirect},
|
||||||
|
Json,
|
||||||
|
};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::lib::domain::intro_tool::models::guild::{
|
use crate::lib::domain::intro_tool::models::guild::{
|
||||||
GetChannelError, GetGuildError, GetIntroError,
|
AddIntroToGuildError, GetChannelError, GetGuildError, GetIntroError,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(super) trait ErrorAsRedirect<T>: Sized {
|
pub(super) trait ErrorAsRedirect<T>: Sized {
|
||||||
|
|
@ -63,3 +68,99 @@ impl<T: Debug> ErrorAsRedirect<T> for Result<T, GetIntroError> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) struct ApiResponse<T: Serialize>(StatusCode, Json<T>);
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[serde(tag = "status")]
|
||||||
|
pub(super) enum ApiError {
|
||||||
|
NotFound {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
BadRequest {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
Forbidden {
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
InternalServerError {
|
||||||
|
#[serde(skip)]
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiError {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match self {
|
||||||
|
ApiError::NotFound { .. } => StatusCode::NOT_FOUND,
|
||||||
|
ApiError::BadRequest { .. } => StatusCode::BAD_REQUEST,
|
||||||
|
ApiError::Forbidden { .. } => StatusCode::FORBIDDEN,
|
||||||
|
ApiError::InternalServerError { .. } => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn not_found(message: impl ToString) -> Self {
|
||||||
|
Self::NotFound {
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn bad_request(message: impl ToString) -> Self {
|
||||||
|
Self::BadRequest {
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn forbidden(message: impl ToString) -> Self {
|
||||||
|
Self::Forbidden {
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn internal(message: impl ToString) -> Self {
|
||||||
|
Self::InternalServerError {
|
||||||
|
message: message.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ApiError {
|
||||||
|
fn into_response(self) -> axum::response::Response {
|
||||||
|
(self.status_code(), Json(self)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetGuildError> for ApiError {
|
||||||
|
fn from(value: GetGuildError) -> Self {
|
||||||
|
match value {
|
||||||
|
GetGuildError::NotFound => Self::not_found("Guild not found"),
|
||||||
|
GetGuildError::CouldNotFetchUsers(get_user_error) => {
|
||||||
|
tracing::error!(err = ?get_user_error, "could not fetch users from guild");
|
||||||
|
|
||||||
|
Self::internal("Could not fetch users from guild".to_string())
|
||||||
|
}
|
||||||
|
GetGuildError::CouldNotFetchChannels(get_channel_error) => {
|
||||||
|
tracing::error!(err = ?get_channel_error, "could not fetch channels from guild");
|
||||||
|
|
||||||
|
Self::internal("Could not fetch channels from guild".to_string())
|
||||||
|
}
|
||||||
|
GetGuildError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AddIntroToGuildError> for ApiError {
|
||||||
|
fn from(value: AddIntroToGuildError) -> Self {
|
||||||
|
match value {
|
||||||
|
AddIntroToGuildError::Unknown(error) => {
|
||||||
|
tracing::error!(err = ?error, "unknown error");
|
||||||
|
|
||||||
|
Self::internal(error.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use crate::lib::domain::intro_tool::{
|
||||||
self, AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserRequest, Channel,
|
self, AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserRequest, Channel,
|
||||||
ChannelName, CreateChannelError, CreateChannelRequest, CreateGuildError,
|
ChannelName, CreateChannelError, CreateChannelRequest, CreateGuildError,
|
||||||
CreateGuildRequest, CreateUserError, CreateUserRequest, GetChannelError, GetGuildError,
|
CreateGuildRequest, CreateUserError, CreateUserRequest, GetChannelError, GetGuildError,
|
||||||
GetIntroError, GetUserError, Guild, GuildId, GuildRef, Intro, User, UserName,
|
GetIntroError, GetUserError, Guild, GuildId, GuildRef, Intro, IntroId, User, UserName,
|
||||||
},
|
},
|
||||||
ports::IntroToolRepository,
|
ports::IntroToolRepository,
|
||||||
};
|
};
|
||||||
|
|
@ -347,9 +347,47 @@ impl IntroToolRepository for Sqlite {
|
||||||
|
|
||||||
async fn add_intro_to_guild(
|
async fn add_intro_to_guild(
|
||||||
&self,
|
&self,
|
||||||
req: AddIntroToGuildRequest,
|
name: &str,
|
||||||
) -> Result<(), AddIntroToGuildError> {
|
guild_id: GuildId,
|
||||||
todo!()
|
filename: String,
|
||||||
|
) -> Result<IntroId, AddIntroToGuildError> {
|
||||||
|
let conn = self.conn.lock().await;
|
||||||
|
|
||||||
|
let mut query = conn
|
||||||
|
.prepare(
|
||||||
|
"
|
||||||
|
INSERT INTO Intro
|
||||||
|
(
|
||||||
|
name,
|
||||||
|
volume,
|
||||||
|
guild_id,
|
||||||
|
filename
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
:name,
|
||||||
|
:volume,
|
||||||
|
:guild_id,
|
||||||
|
:filename
|
||||||
|
)
|
||||||
|
RETURNING id
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.context("failed to prepare query")?;
|
||||||
|
|
||||||
|
let intro_id = query
|
||||||
|
.query_row(
|
||||||
|
&[
|
||||||
|
(":name", name),
|
||||||
|
(":volume", &0.to_string()),
|
||||||
|
(":guild_id", &guild_id.to_string()),
|
||||||
|
(":filename", &filename),
|
||||||
|
],
|
||||||
|
|row| Ok(row.get::<_, i32>(0)?.into()),
|
||||||
|
)
|
||||||
|
.context("failed to query row")?;
|
||||||
|
|
||||||
|
Ok(intro_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn add_intro_to_user(
|
async fn add_intro_to_user(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue