diff --git a/Cargo.toml b/Cargo.toml index ce4f0d8..296ce1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,14 @@ name = "memejoin-rs" version = "0.2.2-alpha" edition = "2021" +[[bin]] +name = "memejoin-rs" +path = "src/main.rs" + +[lib] +name = "memejoin_rs" +path = "src/lib/mod.rs" + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] diff --git a/src/db/mod.rs b/src/db/mod.rs index f18a856..1d51bc0 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -249,66 +249,66 @@ impl Database { intros } - pub(crate) fn get_all_user_permissions( - &self, - guild_id: u64, - ) -> Result> { - let mut query = self.conn.prepare( - " - SELECT - username, - permissions - FROM UserPermission - WHERE - guild_id = :guild_id - ", - )?; + // pub(crate) fn get_all_user_permissions( + // &self, + // guild_id: u64, + // ) -> Result> { + // let mut query = self.conn.prepare( + // " + // SELECT + // username, + // permissions + // FROM UserPermission + // WHERE + // guild_id = :guild_id + // ", + // )?; + // + // let permissions = query + // .query_map( + // &[ + // // :vomit: + // (":guild_id", &guild_id.to_string()), + // ], + // |row| Ok((row.get(0)?, auth::Permissions(row.get(1)?))), + // )? + // .collect::>>()?; + // + // Ok(permissions) + // } - let permissions = query - .query_map( - &[ - // :vomit: - (":guild_id", &guild_id.to_string()), - ], - |row| Ok((row.get(0)?, auth::Permissions(row.get(1)?))), - )? - .collect::>>()?; + // pub(crate) fn get_user_permissions( + // &self, + // username: &str, + // guild_id: u64, + // ) -> Result { + // self.conn.query_row( + // " + // SELECT + // permissions + // FROM UserPermission + // WHERE + // username = ?1 + // AND guild_id = ?2 + // ", + // [username, &guild_id.to_string()], + // |row| Ok(auth::Permissions(row.get(0)?)), + // ) + // } - Ok(permissions) - } - - pub(crate) fn get_user_permissions( - &self, - username: &str, - guild_id: u64, - ) -> Result { - self.conn.query_row( - " - SELECT - permissions - FROM UserPermission - WHERE - username = ?1 - AND guild_id = ?2 - ", - [username, &guild_id.to_string()], - |row| Ok(auth::Permissions(row.get(0)?)), - ) - } - - pub(crate) fn get_user_app_permissions(&self, username: &str) -> Result { - self.conn.query_row( - " - SELECT - permissions - FROM UserAppPermission - WHERE - username = ?1 - ", - [username], - |row| Ok(auth::AppPermissions(row.get(0)?)), - ) - } + // pub(crate) fn get_user_app_permissions(&self, username: &str) -> Result { + // self.conn.query_row( + // " + // SELECT + // permissions + // FROM UserAppPermission + // WHERE + // username = ?1 + // ", + // [username], + // |row| Ok(auth::AppPermissions(row.get(0)?)), + // ) + // } pub(crate) fn get_guild_channels(&self, guild_id: u64) -> Result> { let mut query = self.conn.prepare( @@ -476,28 +476,29 @@ impl Database { Ok(()) } - pub(crate) fn insert_user_permission( - &self, - username: &str, - guild_id: u64, - permissions: auth::Permissions, - ) -> Result<()> { - let affected = self.conn.execute( - " - INSERT INTO - UserPermission (username, guild_id, permissions) - VALUES (?1, ?2, ?3) - ON CONFLICT(username, guild_id) DO UPDATE SET permissions = ?3", - [username, &guild_id.to_string(), &permissions.0.to_string()], - )?; - - if affected < 1 { - warn!("no rows affected when attempting to insert user permissions"); - } - - Ok(()) - } + // pub(crate) fn insert_user_permission( + // &self, + // username: &str, + // guild_id: u64, + // permissions: auth::Permissions, + // ) -> Result<()> { + // let affected = self.conn.execute( + // " + // INSERT INTO + // UserPermission (username, guild_id, permissions) + // VALUES (?1, ?2, ?3) + // ON CONFLICT(username, guild_id) DO UPDATE SET permissions = ?3", + // [username, &guild_id.to_string(), &permissions.0.to_string()], + // )?; + // + // if affected < 1 { + // warn!("no rows affected when attempting to insert user permissions"); + // } + // + // Ok(()) + // } + /* pub(crate) fn insert_user_app_permission( &self, username: &str, @@ -518,6 +519,7 @@ impl Database { Ok(()) } + */ pub fn delete_user_intro( &self, diff --git a/src/auth.rs b/src/lib/auth.rs similarity index 95% rename from src/auth.rs rename to src/lib/auth.rs index b10ae50..52051e6 100644 --- a/src/auth.rs +++ b/src/lib/auth.rs @@ -1,10 +1,16 @@ +#[derive(Clone)] +pub struct DiscordSecret { + pub client_id: String, + pub client_secret: String, + pub bot_token: String, +} + +/* use std::str::FromStr; use enum_iterator::Sequence; use serde::{Deserialize, Serialize}; -use crate::routes::Error; - #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct Discord { pub(crate) access_token: String, @@ -14,13 +20,6 @@ pub(crate) struct Discord { pub(crate) scope: String, } -#[derive(Clone)] -pub(crate) struct DiscordSecret { - pub(crate) client_id: String, - pub(crate) client_secret: String, - pub(crate) bot_token: String, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct User { pub(crate) auth: Discord, @@ -143,3 +142,4 @@ impl FromStr for Permission { } } } +*/ diff --git a/src/lib/domain/intro_tool/debug_service.rs b/src/lib/domain/intro_tool/debug_service.rs index 0608d49..26f9197 100644 --- a/src/lib/domain/intro_tool/debug_service.rs +++ b/src/lib/domain/intro_tool/debug_service.rs @@ -1,6 +1,6 @@ use chrono::{Duration, Utc}; -use crate::lib::domain::intro_tool::{ +use crate::domain::intro_tool::{ models::{self, guild::IntroId}, ports::{IntroToolRepository, IntroToolService}, }; diff --git a/src/lib/domain/intro_tool/ports.rs b/src/lib/domain/intro_tool/ports.rs index ca269a2..98c29b7 100644 --- a/src/lib/domain/intro_tool/ports.rs +++ b/src/lib/domain/intro_tool/ports.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, future::Future}; -use crate::lib::domain::intro_tool::models::guild::{ChannelName, IntroId}; +use crate::domain::intro_tool::models::guild::{ChannelName, IntroId}; use super::models::guild::{ AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserError, AddIntroToUserRequest, @@ -116,3 +116,19 @@ pub trait IntroToolRepository: Send + Sync + Clone + 'static { req: AddIntroToUserRequest, ) -> Result<(), AddIntroToUserError>; } + +pub trait RemoteAudioFetcher: Send + Sync + Clone + 'static { + fn fetch_remote_audio( + &self, + url: &str, + name: &str, + ) -> impl Future> + Send; +} + +pub trait LocalAudioFetcher: Send + Sync + Clone + 'static { + fn save_local_audio( + &self, + bytes: &[u8], + name: &str, + ) -> impl Future> + Send; +} diff --git a/src/lib/domain/intro_tool/service.rs b/src/lib/domain/intro_tool/service.rs index 2e799f0..ee36e63 100644 --- a/src/lib/domain/intro_tool/service.rs +++ b/src/lib/domain/intro_tool/service.rs @@ -1,36 +1,42 @@ -use anyhow::{anyhow, Context}; use uuid::Uuid; -use crate::{ - lib::domain::intro_tool::{ - models::guild::{self, GetUserError, GuildId, IntroId, User}, - ports::{IntroToolRepository, IntroToolService}, - }, - media, +use crate::domain::intro_tool::{ + models::guild::{self, GetUserError, GuildId, IntroId, User}, + ports::{IntroToolRepository, IntroToolService, LocalAudioFetcher, RemoteAudioFetcher}, }; -use super::models; - #[derive(Clone)] -pub struct Service +pub struct Service where R: IntroToolRepository, + RA: RemoteAudioFetcher, + LA: LocalAudioFetcher, { repo: R, + remote_audio_fetcher: RA, + local_audio_fetcher: LA, } -impl Service +impl Service where R: IntroToolRepository, + RA: RemoteAudioFetcher, + LA: LocalAudioFetcher, { - pub fn new(repo: R) -> Self { - Self { repo } + pub fn new(repo: R, remote_audio_fetcher: RA, local_audio_fetcher: LA) -> Self { + Self { + repo, + remote_audio_fetcher, + local_audio_fetcher, + } } } -impl IntroToolService for Service +impl IntroToolService for Service where R: IntroToolRepository, + RA: RemoteAudioFetcher, + LA: LocalAudioFetcher, { async fn needs_setup(&self) -> bool { let Ok(guild_count) = self.repo.get_guild_count().await else { @@ -102,42 +108,14 @@ where ) -> Result { 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 + self.local_audio_fetcher + .save_local_audio(bytes, Uuid::new_v4().to_string().as_str()) + .await? } 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.remote_audio_fetcher + .fetch_remote_audio(url, Uuid::new_v4().to_string().as_str()) + .await? } }; diff --git a/src/htmx.rs b/src/lib/htmx.rs similarity index 100% rename from src/htmx.rs rename to src/lib/htmx.rs diff --git a/src/lib/inbound/http.rs b/src/lib/inbound/http.rs index 4ee47ca..972d6c6 100644 --- a/src/lib/inbound/http.rs +++ b/src/lib/inbound/http.rs @@ -17,7 +17,7 @@ use tracing::info; use crate::{ auth, - lib::domain::intro_tool::{models::guild::User, ports::IntroToolService}, + domain::intro_tool::{models::guild::User, ports::IntroToolService}, }; #[derive(Clone)] diff --git a/src/lib/inbound/http/handlers.rs b/src/lib/inbound/http/handlers.rs index 424e501..7ea0e17 100644 --- a/src/lib/inbound/http/handlers.rs +++ b/src/lib/inbound/http/handlers.rs @@ -5,7 +5,7 @@ use axum::{ http::{HeaderMap, HeaderValue}, }; -use crate::lib::{ +use crate::{ domain::intro_tool::{ models::guild::{AddIntroToGuildRequest, GuildId, IntroRequestData, User}, ports::IntroToolService, diff --git a/src/lib/inbound/http/page.rs b/src/lib/inbound/http/page.rs index fba14b5..f74c6dc 100644 --- a/src/lib/inbound/http/page.rs +++ b/src/lib/inbound/http/page.rs @@ -4,14 +4,12 @@ use axum::{ }; use crate::{ - htmx::{Build, HtmxBuilder, Tag}, - lib::{ - domain::intro_tool::{ - models::guild::{ChannelName, GuildRef, Intro, User}, - ports::IntroToolService, - }, - inbound::{http::ApiState, response::ErrorAsRedirect}, + domain::intro_tool::{ + models::guild::{ChannelName, GuildRef, Intro, User}, + ports::IntroToolService, }, + htmx::{Build, HtmxBuilder, Tag}, + inbound::{http::ApiState, response::ErrorAsRedirect}, }; pub async fn home( diff --git a/src/lib/inbound/response.rs b/src/lib/inbound/response.rs index 0ddbc20..d035c1e 100644 --- a/src/lib/inbound/response.rs +++ b/src/lib/inbound/response.rs @@ -7,7 +7,7 @@ use axum::{ use reqwest::StatusCode; use serde::Serialize; -use crate::lib::domain::intro_tool::models::guild::{ +use crate::domain::intro_tool::models::guild::{ AddIntroToGuildError, GetChannelError, GetGuildError, GetIntroError, }; diff --git a/src/lib/mod.rs b/src/lib/mod.rs index a65f027..291a9c3 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -1,3 +1,5 @@ +pub mod auth; pub mod domain; +pub mod htmx; pub mod inbound; pub mod outbound; diff --git a/src/lib/outbound/ffmpeg.rs b/src/lib/outbound/ffmpeg.rs new file mode 100644 index 0000000..03d6af9 --- /dev/null +++ b/src/lib/outbound/ffmpeg.rs @@ -0,0 +1,33 @@ +use anyhow::{anyhow, Context}; + +use crate::domain::intro_tool::ports::LocalAudioFetcher; + +#[derive(Clone)] +pub struct Ffmpeg; + +impl LocalAudioFetcher for Ffmpeg { + async fn save_local_audio(&self, bytes: &[u8], name: &str) -> Result { + let temp_path = format!("./sounds/temp/{name}"); + let dest_path = format!("./sounds/{name}.mp3"); + + // Write original file so its ready for codec conversion + std::fs::write(&temp_path, bytes).context("failed to write temp file")?; + let child = tokio::process::Command::new("ffmpeg") + .args(["-i", &temp_path]) + .arg("-vn") + .args(["-map", "0:a"]) + .arg(&dest_path) + .spawn() + .map_err(|err| anyhow!(err.to_string()))? + .wait() + .await + .map_err(|err| anyhow!(err.to_string()))?; + + if !child.success() { + return Err(anyhow!("ffmpeg terminated unsuccessfully")); + } + std::fs::remove_file(&temp_path).context("failed to remove temp file")?; + + Ok(dest_path) + } +} diff --git a/src/lib/outbound/mod.rs b/src/lib/outbound/mod.rs index 6b1c108..659980b 100644 --- a/src/lib/outbound/mod.rs +++ b/src/lib/outbound/mod.rs @@ -1 +1,3 @@ +pub mod ffmpeg; pub mod sqlite; +pub mod ytdlp; diff --git a/src/lib/outbound/sqlite.rs b/src/lib/outbound/sqlite.rs index ac26a08..35c79ee 100644 --- a/src/lib/outbound/sqlite.rs +++ b/src/lib/outbound/sqlite.rs @@ -5,7 +5,7 @@ use tokio::sync::Mutex; use anyhow::Context; use rusqlite::Connection; -use crate::lib::domain::intro_tool::{ +use crate::domain::intro_tool::{ models::guild::{ self, AddIntroToGuildError, AddIntroToGuildRequest, AddIntroToUserRequest, Channel, ChannelName, CreateChannelError, CreateChannelRequest, CreateGuildError, diff --git a/src/lib/outbound/ytdlp.rs b/src/lib/outbound/ytdlp.rs new file mode 100644 index 0000000..3cad694 --- /dev/null +++ b/src/lib/outbound/ytdlp.rs @@ -0,0 +1,28 @@ +use anyhow::{anyhow, Context}; + +use crate::domain::intro_tool::ports::RemoteAudioFetcher; + +#[derive(Clone)] +pub struct Ytdlp; + +impl RemoteAudioFetcher for Ytdlp { + async fn fetch_remote_audio(&self, url: &str, name: &str) -> Result { + let file_name = format!("sounds/{name}"); + + 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(anyhow!("yt-dlp terminated unsuccessfully")); + } + + Ok(format!("{file_name}.mp3")) + } +} diff --git a/src/main.rs b/src/main.rs index 895a210..97c13d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,9 @@ -// #![feature(stmt_expr_attributes)] -// #![feature(proc_macro_hygiene)] -// #![feature(async_closure)] - -mod lib; - -mod auth; mod db; -mod htmx; -mod media; -mod page; -mod routes; pub mod settings; -use axum::http::Method; -use axum::routing::{get, post}; -use axum::Router; -use settings::ApiState; use std::env; -use std::net::SocketAddr; use std::sync::Arc; use tokio::sync::mpsc; -use tower_http::cors::{Any, CorsLayer}; use serenity::async_trait; use serenity::model::prelude::{Channel, ChannelId, GuildId, Member, Ready}; @@ -30,9 +13,7 @@ use serenity::prelude::*; use songbird::SerenityInit; use tracing::*; -use crate::lib::domain::intro_tool; -use crate::lib::{inbound, outbound}; -use crate::settings::Settings; +use memejoin_rs::{auth, domain::intro_tool, inbound, outbound}; enum HandlerMessage { Ready(Context), @@ -120,67 +101,6 @@ impl EventHandler for Handler { } } -fn spawn_api(db: Arc>) { - let secrets = auth::DiscordSecret { - client_id: env::var("DISCORD_CLIENT_ID").expect("expected DISCORD_CLIENT_ID env var"), - client_secret: env::var("DISCORD_CLIENT_SECRET") - .expect("expected DISCORD_CLIENT_SECRET env var"), - bot_token: env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var"), - }; - let origin = env::var("APP_ORIGIN").expect("expected APP_ORIGIN"); - - let state = ApiState { - db, - secrets, - origin: origin.clone(), - }; - - tokio::spawn(async move { - let api = Router::new() - .route("/", get(page::home)) - .route("/index.html", get(page::home)) - .route("/login", get(page::login)) - .route("/guild/:guild_id", get(page::guild_dashboard)) - .route("/guild/:guild_id/setup", get(routes::guild_setup)) - .route( - "/guild/:guild_id/add_channel", - post(routes::guild_add_channel), - ) - .route( - "/guild/:guild_id/permissions/update", - post(routes::update_guild_permissions), - ) - .route("/v2/auth", get(routes::v2_auth)) - .route( - "/v2/intros/add/:guild_id/:channel", - post(routes::v2_add_intro_to_user), - ) - .route( - "/v2/intros/remove/:guild_id/:channel", - 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)) - .layer( - CorsLayer::new() - .allow_origin([origin.parse().unwrap()]) - .allow_headers(Any) - .allow_methods([Method::GET, Method::POST, Method::DELETE]), - ) - .with_state(state); - let addr = SocketAddr::from(([0, 0, 0, 0], 8100)); - info!("socket listening on {addr}"); - axum::Server::bind(&addr) - .serve(api.into_make_service()) - .await - .unwrap(); - }); -} - async fn spawn_bot(db: Arc>) { let token = env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var"); let songbird = songbird::Songbird::serenity(); @@ -318,10 +238,6 @@ async fn main() -> std::io::Result<()> { dotenv::dotenv().ok(); tracing_subscriber::fmt::init(); - let settings = serde_json::from_str::( - &std::fs::read_to_string("config/settings.json").expect("no config/settings.json"), - ) - .expect("error parsing settings file"); let secrets = auth::DiscordSecret { client_id: env::var("DISCORD_CLIENT_ID").expect("expected DISCORD_CLIENT_ID env var"), client_secret: env::var("DISCORD_CLIENT_SECRET") @@ -331,9 +247,12 @@ async fn main() -> std::io::Result<()> { let origin = env::var("APP_ORIGIN").expect("expected APP_ORIGIN"); let db = outbound::sqlite::Sqlite::new("./config/db.sqlite").expect("couldn't open sqlite db"); + let local_audio_fetcher = outbound::ffmpeg::Ffmpeg; + let remote_audio_fetcher = outbound::ytdlp::Ytdlp; if let Ok(impersonated_username) = env::var("IMPERSONATED_USERNAME") { - let service = intro_tool::service::Service::new(db); + let service = + intro_tool::service::Service::new(db, remote_audio_fetcher, local_audio_fetcher); let service = intro_tool::debug_service::DebugService::new(service, impersonated_username); let http_server = inbound::http::HttpServer::new(service, secrets, origin) @@ -341,7 +260,8 @@ async fn main() -> std::io::Result<()> { http_server.run().await; } else { - let service = intro_tool::service::Service::new(db); + let service = + intro_tool::service::Service::new(db, remote_audio_fetcher, local_audio_fetcher); let http_server = inbound::http::HttpServer::new(service, secrets, origin) .expect("couldn't start http server");