diff --git a/flake.nix b/flake.nix index f85032a..f57ec4a 100644 --- a/flake.nix +++ b/flake.nix @@ -34,6 +34,7 @@ pkg-config gcc openssl + pkg-config python3 ffmpeg cmake @@ -47,7 +48,8 @@ name = "memejoin-rs"; version = "0.1.2-alpha"; src = self; - nativeBuildInputs = [ local-rust cmake gcc libopus ]; + buildInputs = [ openssl.dev ]; + nativeBuildInputs = [ local-rust pkg-config openssl openssl.dev cmake gcc libopus ]; cargoLock = { lockFile = ./Cargo.lock; @@ -59,7 +61,7 @@ tag = "0.1.2-alpha"; copyToRoot = buildEnv { name = "image-root"; - paths = [ default ffmpeg libopus youtube-dl ]; + paths = [ default cacert openssl openssl.dev ffmpeg libopus youtube-dl yt-dlp ]; }; runAsRoot = '' #!${runtimeShell} diff --git a/src/auth.rs b/src/auth.rs index e54cf91..3c240e2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -27,8 +27,8 @@ pub(crate) struct User { pub(crate) name: String, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct Permissions(u8); +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub(crate) struct Permissions(pub(crate) u8); impl Default for Permissions { fn default() -> Permissions { Permissions(0) diff --git a/src/main.rs b/src/main.rs index 981ed13..5178621 100644 --- a/src/main.rs +++ b/src/main.rs @@ -129,7 +129,7 @@ fn spawn_api(settings: Arc>) { let api = Router::new() .route("/health", get(routes::health)) .route("/me", get(routes::me)) - .route("/intros/:guild/add/:url", get(routes::add_guild_intro)) + .route("/intros/:guild/add", get(routes::add_guild_intro)) .route("/intros/:guild", get(routes::intros)) .route( "/intros/:guild/:channel/:intro", @@ -142,12 +142,13 @@ fn spawn_api(settings: Arc>) { .route("/auth", get(routes::auth)) .layer( CorsLayer::new() - .allow_origin(Any) + // TODO: move this to env variable + .allow_origin(["https://spacegirl.nl".parse().unwrap()]) .allow_headers(Any) .allow_methods([Method::GET, Method::POST]), ) .with_state(Arc::new(state)); - let addr = SocketAddr::from(([0, 0, 0, 0], 7756)); + let addr = SocketAddr::from(([0, 0, 0, 0], 8080)); info!("socket listening on {addr}"); axum::Server::bind(&addr) .serve(api.into_make_service()) diff --git a/src/routes.rs b/src/routes.rs index e3a9baf..3ab1edf 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -13,7 +13,7 @@ use serde_json::{json, Value}; use tracing::{error, info}; use uuid::Uuid; -use crate::settings::{ApiState, Intro, IntroIndex}; +use crate::settings::{ApiState, Intro, IntroIndex, UserSettings}; use crate::{auth, settings::FileIntro}; #[derive(Serialize)] @@ -38,6 +38,7 @@ pub(crate) struct Me<'a> { pub(crate) struct MeGuild<'a> { pub(crate) name: String, pub(crate) channels: Vec>, + pub(crate) permissions: auth::Permissions, } #[derive(Serialize)] @@ -99,6 +100,13 @@ struct DiscordUser { pub username: String, } +#[derive(Deserialize)] +struct DiscordUserGuild { + pub id: String, + pub name: String, + pub owner: bool, +} + pub(crate) async fn auth( State(state): State>, Query(params): Query>, @@ -114,7 +122,7 @@ pub(crate) async fn auth( data.insert("client_secret", state.secrets.client_secret.as_str()); data.insert("grant_type", "authorization_code"); data.insert("code", code); - data.insert("redirect_uri", "http://localhost:5173/auth"); + data.insert("redirect_uri", "https://spacegirl.nl/memes/auth"); let client = reqwest::Client::new(); @@ -219,34 +227,66 @@ pub(crate) async fn intros( Json(json!(IntroResponse::Intros(&guild.intros))) } -pub(crate) async fn me(State(state): State>, headers: HeaderMap) -> Json { - let settings = state.settings.lock().await; - let Some(token) = headers.get("token").and_then(|v| v.to_str().ok()) else { return Json(json!(MeResponse::NoUserFound)); }; +pub(crate) async fn me( + State(state): State>, + headers: HeaderMap, +) -> 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 user = match settings.auth_users.get(token) { - Some(user) => user.name.clone(), - None => return Json(json!(MeResponse::NoUserFound)), + let (username, permissions, access_token) = match settings.auth_users.get(token) { + Some(user) => ( + user.name.clone(), + user.permissions, + user.auth.access_token.clone(), + ), + None => return Err(Error::NoUserFound), }; + // TODO: get bot's guilds so we only save users who are able to use the bot + let discord_guilds: Vec = reqwest::Client::new() + .get("https://discord.com/api/v10/users/@me/guilds") + .bearer_auth(access_token) + .send() + .await? + .json() + .await + .map_err(|err| { + settings.auth_users.remove(token); + + Error::Auth(err.to_string()) + })?; + let mut me = Me { - username: user.clone(), + username: username.clone(), guilds: Vec::new(), }; - for g in &settings.guilds { + for g in settings.guilds.iter_mut() { + // TODO: don't do this n^2 lookup + let Some(discord_guild) = discord_guilds.iter().find(|discord_guild| discord_guild.id == g.0.to_string()) else { continue; }; + let mut guild = MeGuild { name: g.0.to_string(), channels: Vec::new(), + // TODO: change `auth::User` to have guild specific permissions instead of global + permissions, }; - for channel in &g.1.channels { - let user_settings = channel.1.users.iter().find(|u| *u.0 == user); + for channel in g.1.channels.iter_mut() { + let user_settings = channel + .1 + .users + .entry(username.clone()) + .or_insert(UserSettings { intros: Vec::new() }); - let Some(user) = user_settings else { continue; }; + if discord_guild.owner { + guild.permissions.0 |= auth::Permission::DownloadSounds as u8; + } guild.channels.push(MeChannel { name: channel.0.to_owned(), - intros: &user.1.intros, + intros: &user_settings.intros, }); } @@ -254,22 +294,24 @@ pub(crate) async fn me(State(state): State>, headers: HeaderMap) - } if me.guilds.is_empty() { - Json(json!(MeResponse::NoUserFound)) + Ok(Json(json!(MeResponse::NoUserFound))) } else { - Json(json!(MeResponse::Me(me))) + Ok(Json(json!(MeResponse::Me(me)))) } } pub(crate) async fn add_guild_intro( State(state): State>, - Path((guild, url)): Path<(u64, String)>, + Path(guild): Path, Query(mut params): Query>, headers: HeaderMap, ) -> Result<(), Error> { let mut settings = state.settings.lock().await; // TODO: make this an impl on HeaderMap let Some(token) = headers.get("token").and_then(|v| v.to_str().ok()) else { return Err(Error::NoUserFound); }; + let Some(url) = params.remove("url") else { return Err(Error::InvalidRequest); }; let Some(friendly_name) = params.remove("name") else { return Err(Error::InvalidRequest); }; + let user = match settings.auth_users.get(token) { Some(user) => user, None => return Err(Error::NoUserFound), @@ -284,7 +326,7 @@ pub(crate) async fn add_guild_intro( let uuid = Uuid::new_v4().to_string(); let child = tokio::process::Command::new("yt-dlp") .arg(&url) - .args(["-o", &format!("./sounds/{uuid}")]) + .args(["-o", &format!("sounds/{uuid}")]) .args(["-x", "--audio-format", "mp3"]) .spawn() .map_err(Error::Ytdl)?