From fa7c0f816340b5ee16d9251f9da896445fc48cfb Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Fri, 6 Oct 2023 10:55:48 -0400 Subject: [PATCH 01/28] Bot messages work --- .vscode/settings.json | 3 +- service/Makefile | 3 +- service/src/bot/api/mod.rs | 5 + service/src/bot/api/model.rs | 0 service/src/bot/api/routes.rs | 118 ++++++++++++++++++ service/src/bot/commands/message.rs | 0 service/src/bot/commands/mod.rs | 1 + service/src/bot/mod.rs | 169 +------------------------ service/src/db/spells/routes.rs | 10 +- service/src/lib.rs | 6 + service/src/main.rs | 185 ++++++++++++++++++++++++++-- 11 files changed, 317 insertions(+), 183 deletions(-) create mode 100644 service/src/bot/api/mod.rs create mode 100644 service/src/bot/api/model.rs create mode 100644 service/src/bot/api/routes.rs create mode 100644 service/src/bot/commands/message.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 92e36aa..fbc8bad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "rust-analyzer.linkedProjects": [ "./service/Cargo.toml" - ] + ], + "rust-analyzer.showUnlinkedFileNotification": false } \ No newline at end of file diff --git a/service/Makefile b/service/Makefile index 895c6dc..b7fecae 100644 --- a/service/Makefile +++ b/service/Makefile @@ -13,8 +13,9 @@ help: ## Help command build: ## Build the docker image docker compose build -db: ## Start the docker database +utils: ## Start the utils docker compose up -d db + docker compose up -d rbmq up: ## Start the app docker compose up -d diff --git a/service/src/bot/api/mod.rs b/service/src/bot/api/mod.rs new file mode 100644 index 0000000..6666fdc --- /dev/null +++ b/service/src/bot/api/mod.rs @@ -0,0 +1,5 @@ +mod model; +mod routes; + +pub use model::*; +pub use routes::init_routes; \ No newline at end of file diff --git a/service/src/bot/api/model.rs b/service/src/bot/api/model.rs new file mode 100644 index 0000000..e69de29 diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs new file mode 100644 index 0000000..2f83df0 --- /dev/null +++ b/service/src/bot/api/routes.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; + +use actix_web::{get, post, put, delete, web, HttpResponse, HttpRequest, ResponseError}; +use log::warn; +use serde::{Serialize, Deserialize}; +use serenity::{http::Http, model::prelude::{GuildChannel, ChannelType}}; +use siren::ServiceError; + +#[get("/guilds")] +async fn get_guilds(data: web::Data>) -> HttpResponse { + let guild_results = &data.get_guilds(None, None).await; + let guilds = match guild_results { + Ok(guilds) => guilds, + Err(err) => return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + }; + HttpResponse::Ok().json(guilds) +} + +#[get("/{id}/text")] +async fn get_text_channels(id: web::Path, data: web::Data>) -> HttpResponse { + let channel_results = &data.get_channels(id.parse::().unwrap()).await; + let channels = match channel_results { + Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Text).collect::>(), + Err(err) => return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + }; + HttpResponse::Ok().json(channels) +} + +#[get("/{id}/voice")] +async fn get_voice_channels(id: web::Path, data: web::Data>) -> HttpResponse { + let channel_results = &data.get_channels(id.parse::().unwrap()).await; + let channels = match channel_results { + Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Voice).collect::>(), + Err(err) => return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + }; + HttpResponse::Ok().json(channels) +} + +#[derive(Serialize, Deserialize)] +struct ChannelMessage { + message: String +} + +#[post("/{guild_id}/text/{channel_id}/message")] +async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>) -> HttpResponse { + let (guild_id, channel_id) = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + let channel_id = match channel_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse channel id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + let channel_results = &data.get_channels(guild_id).await; + let channels = match channel_results { + Ok(channels) => channels, + Err(err) => { + warn!("Could not get channels: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + let channel = match channels.iter().find(|c| c.id.0 == channel_id) { + Some(channel) => channel, + None => { + warn!("Could not find channel with id {}", channel_id); + return ResponseError::error_response(&ServiceError { + status: 422, + message: format!("Could not find channel with id {}", channel_id) + }) + } + }; + + if let Err(err) = channel.say(&data.get_ref(), &text.message).await { + warn!("Could not send message: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + }; + + HttpResponse::Ok().finish() +} + +pub fn init_routes(config: &mut web::ServiceConfig) { + config + .service(get_guilds) + .service(web::scope("guilds") + .service(get_text_channels) + .service(get_voice_channels) + .service(send_message) + ); +} \ No newline at end of file diff --git a/service/src/bot/commands/message.rs b/service/src/bot/commands/message.rs new file mode 100644 index 0000000..e69de29 diff --git a/service/src/bot/commands/mod.rs b/service/src/bot/commands/mod.rs index 6f92333..a38dda8 100644 --- a/service/src/bot/commands/mod.rs +++ b/service/src/bot/commands/mod.rs @@ -1,5 +1,6 @@ pub mod audio; pub mod help; +pub mod message; pub mod oai; pub mod ping; pub mod schedule; diff --git a/service/src/bot/mod.rs b/service/src/bot/mod.rs index 43dcce0..eb1e203 100644 --- a/service/src/bot/mod.rs +++ b/service/src/bot/mod.rs @@ -1,169 +1,2 @@ -use std::collections::{HashSet, HashMap}; -use std::env; -use std::sync::Arc; - -use commands::audio::{create_response, AudioConfig, AudioConfigs}; - -use log::{error, warn, info}; -use serenity::async_trait; -use serenity::framework::StandardFramework; -use serenity::model::application::interaction::Interaction; -use serenity::model::gateway::Ready; -use serenity::model::channel::Message; -use serenity::http::Http; -use serenity::prelude::*; -use songbird::SerenityInit; - -use crate::bot::commands::oai::GPTModel; - +pub mod api; pub mod commands; - -struct Handler { - // Open AI Config - oai: Option -} - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - // Ignore messages from bots - if msg.author.bot { - return; - } - match &self.oai { - Some(oai) => { - match msg.mentions_me(&ctx.http).await { - Ok(mentioned) => { - let bot_in_thread = match msg.channel_id.get_thread_members(&ctx.http).await { - Ok(t) => { - match t.iter().find(|t| t.user_id.unwrap().0 == ctx.cache.current_user_id().0) { - Some(_) => true, - None => false - } - } - Err(_) => false - }; - if mentioned || bot_in_thread { - commands::oai::generate_response(&ctx, &msg, oai).await; - } - } - Err(why) => warn!("Could not check mentions: {:?}", why) - }; - } - None => {} - } - } - - async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::ApplicationCommand(command) = interaction { - match command.data.name.as_str() { - "play" => commands::audio::play::run(&ctx, &command).await, - "stop" => commands::audio::stop::run(&ctx, &command).await, - "pause" => commands::audio::pause::run(&ctx, &command).await, - "resume" => commands::audio::resume::run(&ctx, &command).await, - "skip" => commands::audio::skip::run(&ctx, &command).await, - "volume" => commands::audio::volume::run(&ctx, &command).await, - _ => { - let content: String = match command.data.name.as_str() { - "ping" => commands::ping::run(&command.data.options), - _ => "Unknown command".to_string() - }; - - if let Err(why) = create_response(&ctx, &command, content).await { - warn!("Cannot respond to slash command: {}", why); - } - } - } - } - } - - async fn ready(&self, ctx: Context, ready: Ready) { - if ready.guilds.is_empty() { - warn!("No ready guilds found"); - } - for guild in ready.guilds { - let audio_config_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected AudioConfigs in TypeMap.").clone() - }; - { - let mut audio_configs = audio_config_lock.write().await; - let _ = audio_configs.insert(guild.id, AudioConfig { volume: 1.0 }); - } - let commands = guild.id.set_application_commands(&ctx.http, |commands| { - commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::ping::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::play::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::stop::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::pause::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::resume::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::skip::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::volume::register(command) }) - }).await; - match commands { - Ok(c) => info!("Registered {} commands for guild {}", c.len(), guild.id.0), - Err(why) => error!("Could not register commands for guild {}: {:?}", guild.id.0, why) - }; - } - } -} - -pub async fn run() { - let token: String = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); - let intents: GatewayIntents = GatewayIntents::all(); - - let http: Http = Http::new(&token); - let (owners, _bot_id) = match http.get_current_application_info().await { - Ok(info) => { - let mut owners: HashSet = HashSet::new(); - if let Some(team) = info.team { - owners.insert(team.owner_user_id); - } else { - owners.insert(info.owner.id); - } - match http.get_current_user().await { - Ok(bot) => (owners, bot.id), - Err(why) => panic!("Could not access the bot id: {:?}", why) - } - }, - Err(why) => panic!("Could not access application info: {:?}", why) - }; - - let handler = match env::var("OPENAI_API_KEY") { - Ok(token) => { - info!("Loaded OpenAI token"); - Handler { - oai: Some(commands::oai::OAI { - client: reqwest::Client::new(), - base_url: "https://api.openai.com/v1".to_string(), - service_url: "http://localhost:5000".to_string(), - max_attempts: 5, - token, - max_context_questions: 30, - max_tokens: 2048, - default_model: GPTModel::GPT35Turbo, - }) - } - } - Err(err) => { - warn!("Could not load OpenAI token: {}", err); - Handler { oai: None } - } - }; - - let mut client = Client::builder(token, intents) - .event_handler(handler) - .framework(StandardFramework::new() - .configure(|c| c.owners(owners))) - .register_songbird() - .await - .expect("Error creating client"); - - { - let mut data = client.data.write().await; - data.insert::(Arc::new(RwLock::new(HashMap::default()))); - } - - if let Err(why) = client.start_autosharded().await { - error!("An error occurred while running the client: {:?}", why); - } -} \ No newline at end of file diff --git a/service/src/db/spells/routes.rs b/service/src/db/spells/routes.rs index cf61605..5069476 100644 --- a/service/src/db/spells/routes.rs +++ b/service/src/db/spells/routes.rs @@ -181,8 +181,10 @@ async fn delete(id: web::Path) -> HttpResponse { } pub fn init_routes(config: &mut web::ServiceConfig) { - config.service(get_all); - config.service(get_by_id); - config.service(create); - config.service(delete); + config.service(web::scope("dnd") + .service(get_all) + .service(get_by_id) + .service(create) + .service(update) + ); } \ No newline at end of file diff --git a/service/src/lib.rs b/service/src/lib.rs index f1d97fa..b8e64bd 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -81,6 +81,12 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(error: serenity::Error) -> ServiceError { + ServiceError::new(500, format!("Unknown serenity error: {}", error)) + } +} + impl ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { let status_code = match StatusCode::from_u16(self.status) { diff --git a/service/src/main.rs b/service/src/main.rs index bd2a483..1969cf3 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -3,16 +3,119 @@ extern crate diesel; extern crate diesel_migrations; use std::env; +use std::collections::{HashSet, HashMap}; +use std::sync::Arc; +use bot::commands::audio::{create_response, AudioConfig, AudioConfigs}; + +use log::{error, warn, info}; +use serenity::async_trait; +use serenity::framework::StandardFramework; +use serenity::model::application::interaction::Interaction; +use serenity::model::gateway::Ready; +use serenity::model::channel::Message; +use serenity::http::Http; +use serenity::prelude::*; +use songbird::SerenityInit; + +use crate::bot::commands::oai::GPTModel; use actix_cors::Cors; -use actix_web::{HttpServer, App}; +use actix_web::{HttpServer, App, web}; use dotenv::dotenv; -use log::{error, info, warn}; mod bot; mod db; +struct Handler { + // Open AI Config + oai: Option +} + +#[async_trait] +impl EventHandler for Handler { + async fn message(&self, ctx: Context, msg: Message) { + // Ignore messages from bots + if msg.author.bot { + return; + } + match &self.oai { + Some(oai) => { + match msg.mentions_me(&ctx.http).await { + Ok(mentioned) => { + let bot_in_thread = match msg.channel_id.get_thread_members(&ctx.http).await { + Ok(t) => { + match t.iter().find(|t| t.user_id.unwrap().0 == ctx.cache.current_user_id().0) { + Some(_) => true, + None => false + } + } + Err(_) => false + }; + if mentioned || bot_in_thread { + bot::commands::oai::generate_response(&ctx, &msg, oai).await; + } + } + Err(why) => warn!("Could not check mentions: {:?}", why) + }; + } + None => {} + } + } + + async fn interaction_create(&self, ctx: Context, interaction: Interaction) { + if let Interaction::ApplicationCommand(command) = interaction { + match command.data.name.as_str() { + "play" => bot::commands::audio::play::run(&ctx, &command).await, + "stop" => bot::commands::audio::stop::run(&ctx, &command).await, + "pause" => bot::commands::audio::pause::run(&ctx, &command).await, + "resume" => bot::commands::audio::resume::run(&ctx, &command).await, + "skip" => bot::commands::audio::skip::run(&ctx, &command).await, + "volume" => bot::commands::audio::volume::run(&ctx, &command).await, + _ => { + let content: String = match command.data.name.as_str() { + "ping" => bot::commands::ping::run(&command.data.options), + _ => "Unknown command".to_string() + }; + + if let Err(why) = create_response(&ctx, &command, content).await { + warn!("Cannot respond to slash command: {}", why); + } + } + } + } + } + + async fn ready(&self, ctx: Context, ready: Ready) { + if ready.guilds.is_empty() { + warn!("No ready guilds found"); + } + for guild in ready.guilds { + let audio_config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected AudioConfigs in TypeMap.").clone() + }; + { + let mut audio_configs = audio_config_lock.write().await; + let _ = audio_configs.insert(guild.id, AudioConfig { volume: 1.0 }); + } + let commands = guild.id.set_application_commands(&ctx.http, |commands| { + commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::ping::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::play::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::stop::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::pause::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::resume::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::skip::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::volume::register(command) }) + }).await; + match commands { + Ok(c) => info!("Registered {} commands for guild {}", c.len(), guild.id.0), + Err(why) => error!("Could not register commands for guild {}: {:?}", guild.id.0, why) + }; + } + } +} + #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); @@ -23,20 +126,83 @@ async fn main() -> std::io::Result<()> { Err(err) => warn!("Unable to load initial database data: {}", err) }; + let token: String = env::var("DISCORD_TOKEN").expect("Expected a token in the environment"); + let intents: GatewayIntents = GatewayIntents::all(); + + let http: Http = Http::new(&token); + let (owners, _bot_id) = match http.get_current_application_info().await { + Ok(info) => { + let mut owners: HashSet = HashSet::new(); + if let Some(team) = info.team { + owners.insert(team.owner_user_id); + } else { + owners.insert(info.owner.id); + } + match http.get_current_user().await { + Ok(bot) => (owners, bot.id), + Err(why) => panic!("Could not access the bot id: {:?}", why) + } + }, + Err(why) => panic!("Could not access application info: {:?}", why) + }; + + let handler = match env::var("OPENAI_API_KEY") { + Ok(token) => { + info!("Loaded OpenAI token"); + Handler { + oai: Some(bot::commands::oai::OAI { + client: reqwest::Client::new(), + base_url: "https://api.openai.com/v1".to_string(), + service_url: "http://localhost:5000".to_string(), + max_attempts: 5, + token, + max_context_questions: 30, + max_tokens: 2048, + default_model: GPTModel::GPT35Turbo, + }) + } + } + Err(err) => { + warn!("Could not load OpenAI token: {}", err); + Handler { oai: None } + } + }; + + let mut client = Client::builder(token, intents) + .event_handler(handler) + .framework(StandardFramework::new() + .configure(|c| c.owners(owners))) + .register_songbird() + .await + .expect("Error creating client"); + + { + let mut data = client.data.write().await; + data.insert::(Arc::new(RwLock::new(HashMap::default()))); + } + + let bot_http = Arc::clone(&client.cache_and_http.http); + + tokio::spawn(async move { + if let Err(why) = client.start_autosharded().await { + error!("An error occurred while running the client: {:?}", why); + } + }); + let host = env::var("SERVICE_HOST").unwrap_or("localhost".to_string()); let port = env::var("SERVICE_PORT").unwrap_or("5000".to_string()); - tokio::spawn(bot::run()); - - match HttpServer::new(|| { + let server = match HttpServer::new(move || { let cors = Cors::default() .allow_any_origin() .allow_any_method() .allow_any_header() .max_age(3600); App::new() - .configure(db::messages::init_routes) - .configure(db::spells::init_routes) + .app_data(web::Data::new(Arc::clone(&bot_http))) + .configure(crate::db::messages::init_routes) + .configure(crate::db::spells::init_routes) + .configure(crate::bot::api::init_routes) .wrap(cors) }) .bind(format!("{}:{}", host, port)) { @@ -48,7 +214,8 @@ async fn main() -> std::io::Result<()> { error!("Could not bind server: {}", err); return Err(err); } - } - .run() + }; + + server.run() .await } From 2f61aed2041f29db238be5fc201faf83fa08933b Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Fri, 6 Oct 2023 16:09:28 -0400 Subject: [PATCH 02/28] v0.2.6 Working on audio commands from api --- service/.version | 2 +- service/Cargo.toml | 2 +- service/Dockerfile | 17 ++++- service/Makefile | 1 - service/src/bot/api/routes.rs | 81 +++++++++++++++++--- service/src/bot/handler.rs | 98 ++++++++++++++++++++++++ service/src/bot/mod.rs | 1 + service/src/main.rs | 139 ++++++++++------------------------ 8 files changed, 222 insertions(+), 119 deletions(-) create mode 100644 service/src/bot/handler.rs diff --git a/service/.version b/service/.version index 686ee6f..3a22719 100644 --- a/service/.version +++ b/service/.version @@ -1 +1 @@ -SIREN_VERSION=0.2.5 \ No newline at end of file +SIREN_VERSION=0.2.6 \ No newline at end of file diff --git a/service/Cargo.toml b/service/Cargo.toml index 5f615ae..658c72e 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "service" -version = "0.2.5" +version = "0.2.6" edition = "2021" authors = ["Ben Sherriff "] repository = "https://github.com/bensherriff/siren" diff --git a/service/Dockerfile b/service/Dockerfile index 095ea6d..4d10586 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -14,23 +14,32 @@ RUN cargo build --release # ========== # Packages # ========== -FROM debian:bookworm-slim as packages +FROM debian:bullseye-slim as packages +# FROM debian:bookworm-slim as packages WORKDIR /packages RUN apt-get update && apt-get install -y curl tar xz-utils && \ - curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux > yt-dlp && \ + # curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux > yt-dlp && \ + curl -L https://github.com/yt-dlp/yt-dlp/releases/download/2023.07.06/yt-dlp_linux > yt-dlp && \ chmod +x yt-dlp && \ - curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ + # curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ + curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/autobuild-2023-06-30-14-08/ffmpeg-N-111310-g96d6990517-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz # ========= # Runtime # ========= -FROM rust:bookworm as runtime +FROM debian:bullseye-slim as runtime +# FROM debian:bookworm-slim as runtime +# FROM rust:bookworm as runtime WORKDIR /service USER root COPY --from=builder /builder/target/release/service /usr/local/bin/service COPY --from=packages /packages /usr/bin +RUN apt-get update && apt-get install -y libpq5 +# RUN apt-get update && apt-get install -y libpq5 && \ + # apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + CMD ["service"] diff --git a/service/Makefile b/service/Makefile index b7fecae..2c48928 100644 --- a/service/Makefile +++ b/service/Makefile @@ -15,7 +15,6 @@ build: ## Build the docker image utils: ## Start the utils docker compose up -d db - docker compose up -d rbmq up: ## Start the app docker compose up -d diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index 2f83df0..8e08438 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -1,14 +1,16 @@ -use std::sync::Arc; +use std::{sync::Arc, pin::Pin}; use actix_web::{get, post, put, delete, web, HttpResponse, HttpRequest, ResponseError}; use log::warn; use serde::{Serialize, Deserialize}; -use serenity::{http::Http, model::prelude::{GuildChannel, ChannelType}}; +use serenity::model::prelude::{GuildChannel, ChannelType}; use siren::ServiceError; +use crate::AppState; + #[get("/guilds")] -async fn get_guilds(data: web::Data>) -> HttpResponse { - let guild_results = &data.get_guilds(None, None).await; +async fn get_guilds(data: web::Data>) -> HttpResponse { + let guild_results = &data.http.get_guilds(None, None).await; let guilds = match guild_results { Ok(guilds) => guilds, Err(err) => return ResponseError::error_response(&ServiceError { @@ -20,8 +22,8 @@ async fn get_guilds(data: web::Data>) -> HttpResponse { } #[get("/{id}/text")] -async fn get_text_channels(id: web::Path, data: web::Data>) -> HttpResponse { - let channel_results = &data.get_channels(id.parse::().unwrap()).await; +async fn get_text_channels(id: web::Path, data: web::Data>) -> HttpResponse { + let channel_results = &data.http.get_channels(id.parse::().unwrap()).await; let channels = match channel_results { Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Text).collect::>(), Err(err) => return ResponseError::error_response(&ServiceError { @@ -33,8 +35,8 @@ async fn get_text_channels(id: web::Path, data: web::Data>) -> } #[get("/{id}/voice")] -async fn get_voice_channels(id: web::Path, data: web::Data>) -> HttpResponse { - let channel_results = &data.get_channels(id.parse::().unwrap()).await; +async fn get_voice_channels(id: web::Path, data: web::Data>) -> HttpResponse { + let channel_results = &data.http.get_channels(id.parse::().unwrap()).await; let channels = match channel_results { Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Voice).collect::>(), Err(err) => return ResponseError::error_response(&ServiceError { @@ -51,7 +53,7 @@ struct ChannelMessage { } #[post("/{guild_id}/text/{channel_id}/message")] -async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>) -> HttpResponse { +async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>) -> HttpResponse { let (guild_id, channel_id) = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, @@ -73,7 +75,7 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json channels, Err(err) => { @@ -96,7 +98,7 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json, text: web::Json, data: web::Data>) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + if let Some(handler_lock) = data.songbird.get(guild_id) { + let handler = handler_lock.lock().await; + if let Err(err) = handler.queue().pause() { + warn!("Could not pause track: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + } + + HttpResponse::Ok().finish() +} + +#[post("/{guild_id}/voice/pause")] +async fn pause(path: web::Path, data: web::Data>) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + if let Some(handler_lock) = data.songbird.get(guild_id) { + let handler = handler_lock.lock().await; + if let Err(err) = handler.queue().pause() { + warn!("Could not pause track: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + } + + HttpResponse::Ok().finish() +} + pub fn init_routes(config: &mut web::ServiceConfig) { config .service(get_guilds) @@ -114,5 +172,6 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(get_text_channels) .service(get_voice_channels) .service(send_message) + .service(pause) ); } \ No newline at end of file diff --git a/service/src/bot/handler.rs b/service/src/bot/handler.rs new file mode 100644 index 0000000..893eaf8 --- /dev/null +++ b/service/src/bot/handler.rs @@ -0,0 +1,98 @@ +use log::{warn, info, error}; +use serenity::async_trait; +use serenity::model::application::interaction::Interaction; +use serenity::model::gateway::Ready; +use serenity::model::channel::Message; +use serenity::prelude::*; + +use super::commands; +use super::commands::audio::{AudioConfigs, create_response, AudioConfig}; + +pub struct Handler { + // Open AI Config + pub oai: Option +} + +#[async_trait] +impl EventHandler for Handler { + async fn message(&self, ctx: Context, msg: Message) { + // Ignore messages from bots + if msg.author.bot { + return; + } + match &self.oai { + Some(oai) => { + match msg.mentions_me(&ctx.http).await { + Ok(mentioned) => { + let bot_in_thread = match msg.channel_id.get_thread_members(&ctx.http).await { + Ok(t) => { + match t.iter().find(|t| t.user_id.unwrap().0 == ctx.cache.current_user_id().0) { + Some(_) => true, + None => false + } + } + Err(_) => false + }; + if mentioned || bot_in_thread { + commands::oai::generate_response(&ctx, &msg, oai).await; + } + } + Err(why) => warn!("Could not check mentions: {:?}", why) + }; + } + None => {} + } + } + + async fn interaction_create(&self, ctx: Context, interaction: Interaction) { + if let Interaction::ApplicationCommand(command) = interaction { + match command.data.name.as_str() { + "play" => commands::audio::play::run(&ctx, &command).await, + "stop" => commands::audio::stop::run(&ctx, &command).await, + "pause" => commands::audio::pause::run(&ctx, &command).await, + "resume" => commands::audio::resume::run(&ctx, &command).await, + "skip" => commands::audio::skip::run(&ctx, &command).await, + "volume" => commands::audio::volume::run(&ctx, &command).await, + _ => { + let content: String = match command.data.name.as_str() { + "ping" => commands::ping::run(&command.data.options), + _ => "Unknown command".to_string() + }; + + if let Err(why) = create_response(&ctx, &command, content).await { + warn!("Cannot respond to slash command: {}", why); + } + } + } + } + } + + async fn ready(&self, ctx: Context, ready: Ready) { + if ready.guilds.is_empty() { + warn!("No ready guilds found"); + } + for guild in ready.guilds { + let audio_config_lock = { + let data_read = ctx.data.read().await; + data_read.get::().expect("Expected AudioConfigs in TypeMap.").clone() + }; + { + let mut audio_configs = audio_config_lock.write().await; + let _ = audio_configs.insert(guild.id, AudioConfig { volume: 1.0 }); + } + let commands = guild.id.set_application_commands(&ctx.http, |commands| { + commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::ping::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::play::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::stop::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::pause::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::resume::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::skip::register(command) }) + .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::volume::register(command) }) + }).await; + match commands { + Ok(c) => info!("Registered {} commands for guild {}", c.len(), guild.id.0), + Err(why) => error!("Could not register commands for guild {}: {:?}", guild.id.0, why) + }; + } + } +} \ No newline at end of file diff --git a/service/src/bot/mod.rs b/service/src/bot/mod.rs index eb1e203..5e4fa6a 100644 --- a/service/src/bot/mod.rs +++ b/service/src/bot/mod.rs @@ -1,2 +1,3 @@ pub mod api; pub mod commands; +pub mod handler; diff --git a/service/src/main.rs b/service/src/main.rs index 1969cf3..c0fe250 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -5,117 +5,23 @@ extern crate diesel_migrations; use std::env; use std::collections::{HashSet, HashMap}; use std::sync::Arc; - -use bot::commands::audio::{create_response, AudioConfig, AudioConfigs}; - +use bot::commands::audio::AudioConfig; use log::{error, warn, info}; -use serenity::async_trait; use serenity::framework::StandardFramework; -use serenity::model::application::interaction::Interaction; -use serenity::model::gateway::Ready; -use serenity::model::channel::Message; use serenity::http::Http; +use serenity::model::prelude::GuildId; use serenity::prelude::*; -use songbird::SerenityInit; +use songbird::{SerenityInit, Songbird}; -use crate::bot::commands::oai::GPTModel; use actix_cors::Cors; use actix_web::{HttpServer, App, web}; +use crate::bot::{commands::{oai::GPTModel, audio::AudioConfigs}, handler::Handler}; use dotenv::dotenv; mod bot; mod db; -struct Handler { - // Open AI Config - oai: Option -} - -#[async_trait] -impl EventHandler for Handler { - async fn message(&self, ctx: Context, msg: Message) { - // Ignore messages from bots - if msg.author.bot { - return; - } - match &self.oai { - Some(oai) => { - match msg.mentions_me(&ctx.http).await { - Ok(mentioned) => { - let bot_in_thread = match msg.channel_id.get_thread_members(&ctx.http).await { - Ok(t) => { - match t.iter().find(|t| t.user_id.unwrap().0 == ctx.cache.current_user_id().0) { - Some(_) => true, - None => false - } - } - Err(_) => false - }; - if mentioned || bot_in_thread { - bot::commands::oai::generate_response(&ctx, &msg, oai).await; - } - } - Err(why) => warn!("Could not check mentions: {:?}", why) - }; - } - None => {} - } - } - - async fn interaction_create(&self, ctx: Context, interaction: Interaction) { - if let Interaction::ApplicationCommand(command) = interaction { - match command.data.name.as_str() { - "play" => bot::commands::audio::play::run(&ctx, &command).await, - "stop" => bot::commands::audio::stop::run(&ctx, &command).await, - "pause" => bot::commands::audio::pause::run(&ctx, &command).await, - "resume" => bot::commands::audio::resume::run(&ctx, &command).await, - "skip" => bot::commands::audio::skip::run(&ctx, &command).await, - "volume" => bot::commands::audio::volume::run(&ctx, &command).await, - _ => { - let content: String = match command.data.name.as_str() { - "ping" => bot::commands::ping::run(&command.data.options), - _ => "Unknown command".to_string() - }; - - if let Err(why) = create_response(&ctx, &command, content).await { - warn!("Cannot respond to slash command: {}", why); - } - } - } - } - } - - async fn ready(&self, ctx: Context, ready: Ready) { - if ready.guilds.is_empty() { - warn!("No ready guilds found"); - } - for guild in ready.guilds { - let audio_config_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected AudioConfigs in TypeMap.").clone() - }; - { - let mut audio_configs = audio_config_lock.write().await; - let _ = audio_configs.insert(guild.id, AudioConfig { volume: 1.0 }); - } - let commands = guild.id.set_application_commands(&ctx.http, |commands| { - commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::ping::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::play::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::stop::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::pause::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::resume::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::skip::register(command) }) - .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { bot::commands::audio::volume::register(command) }) - }).await; - match commands { - Ok(c) => info!("Registered {} commands for guild {}", c.len(), guild.id.0), - Err(why) => error!("Could not register commands for guild {}: {:?}", guild.id.0, why) - }; - } - } -} - #[actix_web::main] async fn main() -> std::io::Result<()> { dotenv().ok(); @@ -168,20 +74,44 @@ async fn main() -> std::io::Result<()> { } }; + // let songbird = Songbird::serenity_from_config(songbird::Config::default().decode_mode(songbird::driver::DecodeMode::Decode)); + let songbird = Songbird::serenity(); + let mut client = Client::builder(token, intents) .event_handler(handler) .framework(StandardFramework::new() .configure(|c| c.owners(owners))) + // .register_songbird_with(Arc::clone(&songbird)) + // .register_songbird_from_config(songbird::Config::default().decode_mode(songbird::driver::DecodeMode::Decode)) .register_songbird() .await .expect("Error creating client"); + let audio_configs: Arc>> = Arc::new(RwLock::new(HashMap::default())); + { let mut data = client.data.write().await; - data.insert::(Arc::new(RwLock::new(HashMap::default()))); + data.insert::(Arc::clone(&audio_configs)); } - let bot_http = Arc::clone(&client.cache_and_http.http); + let http = Arc::clone(&client.cache_and_http.http); + // let cache_http = Arc::clone(&client.cache_and_http.clone()); + // let data = Arc::clone(&client.data.clone()); + // let t = songbird::Config::default().decode_mode(songbird::driver::DecodeMode::Decode); + + let app_data = Arc::new(AppState { + http, + songbird: Arc::clone(&songbird), + audio_configs + }); + + + let shard_manager = Arc::clone(&client.shard_manager); + + tokio::spawn(async move { + tokio::signal::ctrl_c().await.expect("Could not register ctrl+c handler"); + shard_manager.lock().await.shutdown_all().await; + }); tokio::spawn(async move { if let Err(why) = client.start_autosharded().await { @@ -199,7 +129,8 @@ async fn main() -> std::io::Result<()> { .allow_any_header() .max_age(3600); App::new() - .app_data(web::Data::new(Arc::clone(&bot_http))) + // .app_data(web::Data::new(Arc::clone(&http))) + .app_data(web::Data::new(Arc::clone(&app_data))) .configure(crate::db::messages::init_routes) .configure(crate::db::spells::init_routes) .configure(crate::bot::api::init_routes) @@ -219,3 +150,9 @@ async fn main() -> std::io::Result<()> { server.run() .await } + +pub struct AppState { + pub http: Arc, + pub songbird: Arc, + pub audio_configs: Arc>> +} From fd0226f420313597cd6b6b6cc1e8153581fd3a04 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Fri, 6 Oct 2023 16:56:10 -0400 Subject: [PATCH 03/28] Updated dockerfile to use pipx --- service/Dockerfile | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/service/Dockerfile b/service/Dockerfile index 4d10586..ff07a8b 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -11,35 +11,36 @@ COPY Cargo.toml ./ RUN apt-get update && apt-get install -y cmake RUN cargo build --release -# ========== -# Packages -# ========== -FROM debian:bullseye-slim as packages -# FROM debian:bookworm-slim as packages -WORKDIR /packages +# # ========== +# # Packages +# # ========== +# FROM debian:bullseye-slim as packages +# # FROM debian:bookworm-slim as packages +# WORKDIR /packages -RUN apt-get update && apt-get install -y curl tar xz-utils && \ - # curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux > yt-dlp && \ - curl -L https://github.com/yt-dlp/yt-dlp/releases/download/2023.07.06/yt-dlp_linux > yt-dlp && \ - chmod +x yt-dlp && \ - # curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ - curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/autobuild-2023-06-30-14-08/ffmpeg-N-111310-g96d6990517-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ - tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz +# RUN apt-get update && apt-get install -y curl tar xz-utils && \ +# curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux > yt-dlp && \ +# # curl -L https://github.com/yt-dlp/yt-dlp/releases/download/2023.07.06/yt-dlp_linux > yt-dlp && \ +# chmod +x yt-dlp && \ +# curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ +# # curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/autobuild-2023-06-30-14-08/ffmpeg-N-111310-g96d6990517-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ +# tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz # ========= # Runtime # ========= -FROM debian:bullseye-slim as runtime -# FROM debian:bookworm-slim as runtime -# FROM rust:bookworm as runtime +# FROM debian:bullseye-slim as runtime +FROM debian:bookworm-slim as runtime WORKDIR /service USER root COPY --from=builder /builder/target/release/service /usr/local/bin/service -COPY --from=packages /packages /usr/bin +# COPY --from=packages /packages /usr/bin -RUN apt-get update && apt-get install -y libpq5 -# RUN apt-get update && apt-get install -y libpq5 && \ - # apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt-get update && apt-get install -y \ + libc6 libc6-dev libopus-dev libpq5 libpq-dev python3-pip ffmpeg pipx + # apt-get auto-remove -y && \ + # pip install -U yt-dlp +RUN pipx install yt-dlp CMD ["service"] From cb1fd182f17915eaaa7642f3ffe96571a916bf02 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Fri, 6 Oct 2023 19:39:14 -0400 Subject: [PATCH 04/28] Fixed dockerfile --- service/Dockerfile | 51 ++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/service/Dockerfile b/service/Dockerfile index ff07a8b..1bad8a4 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -11,36 +11,51 @@ COPY Cargo.toml ./ RUN apt-get update && apt-get install -y cmake RUN cargo build --release -# # ========== -# # Packages -# # ========== -# FROM debian:bullseye-slim as packages -# # FROM debian:bookworm-slim as packages -# WORKDIR /packages +# ========== +# Packages +# ========== +FROM debian:bookworm-slim as packages +WORKDIR /packages -# RUN apt-get update && apt-get install -y curl tar xz-utils && \ -# curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux > yt-dlp && \ -# # curl -L https://github.com/yt-dlp/yt-dlp/releases/download/2023.07.06/yt-dlp_linux > yt-dlp && \ -# chmod +x yt-dlp && \ -# curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ -# # curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/autobuild-2023-06-30-14-08/ffmpeg-N-111310-g96d6990517-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ -# tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz +RUN apt-get update && apt-get install -y curl tar xz-utils +ARG TARGETPLATFORM +# Check if the target platform is linux/x86_64, otherwise log error and exit +RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ + echo "amd64" && false; \ + elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ + curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_armv7l > yt-dlp && \ + chmod +x yt-dlp; \ + elif [ "$TARGETPLATFORM" = "linux/aarch64" ]; then \ + curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_aarch64 > yt-dlp && \ + chmod +x yt-dlp; \ + elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_aarch64 > yt-dlp && \ + chmod +x yt-dlp && \ + curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz > ffmpeg.tar.xz && \ + tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz; \ + elif [ "$TARGETPLATFORM" = "linux/x86_64" ]; then \ + echo "Unsupported platform: $TARGETPLATFORM" && \ + curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux > yt-dlp && \ + chmod +x yt-dlp && \ + curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ + tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz; \ + else \ + echo "Unsupported platform: $TARGETPLATFORM" && false; \ +fi # ========= # Runtime # ========= -# FROM debian:bullseye-slim as runtime FROM debian:bookworm-slim as runtime WORKDIR /service USER root COPY --from=builder /builder/target/release/service /usr/local/bin/service -# COPY --from=packages /packages /usr/bin +COPY --from=packages /packages /usr/bin RUN apt-get update && apt-get install -y \ - libc6 libc6-dev libopus-dev libpq5 libpq-dev python3-pip ffmpeg pipx + libc6 libc6-dev libopus-dev libpq5 libpq-dev python3-pip ffmpeg # apt-get auto-remove -y && \ - # pip install -U yt-dlp -RUN pipx install yt-dlp +# RUN pipx install yt-dlp CMD ["service"] From 3ca91c776555b61ce6fb57b789f0763140532cc7 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Fri, 6 Oct 2023 23:34:25 -0400 Subject: [PATCH 05/28] Changing bot commands to be used by endpoints --- .../migrations/000010_create_guilds/down.sql | 1 + .../migrations/000010_create_guilds/up.sql | 5 ++ service/src/bot/commands/audio/mod.rs | 86 ++----------------- service/src/bot/commands/audio/play.rs | 56 ++++++++++-- service/src/bot/commands/audio/volume.rs | 13 +-- service/src/bot/handler.rs | 13 +-- service/src/db/guilds/mod.rs | 3 + service/src/db/guilds/model.rs | 43 ++++++++++ service/src/db/mod.rs | 1 + service/src/db/schema.rs | 8 ++ service/src/main.rs | 29 ++----- 11 files changed, 133 insertions(+), 125 deletions(-) create mode 100644 service/migrations/000010_create_guilds/down.sql create mode 100644 service/migrations/000010_create_guilds/up.sql create mode 100644 service/src/db/guilds/mod.rs create mode 100644 service/src/db/guilds/model.rs diff --git a/service/migrations/000010_create_guilds/down.sql b/service/migrations/000010_create_guilds/down.sql new file mode 100644 index 0000000..c2477fc --- /dev/null +++ b/service/migrations/000010_create_guilds/down.sql @@ -0,0 +1 @@ +DROP TABLE guilds; \ No newline at end of file diff --git a/service/migrations/000010_create_guilds/up.sql b/service/migrations/000010_create_guilds/up.sql new file mode 100644 index 0000000..54c825d --- /dev/null +++ b/service/migrations/000010_create_guilds/up.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS guilds ( + id BIGINT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + volume INTEGER NOT NULL +); \ No newline at end of file diff --git a/service/src/bot/commands/audio/mod.rs b/service/src/bot/commands/audio/mod.rs index cd60d28..4eaeb76 100644 --- a/service/src/bot/commands/audio/mod.rs +++ b/service/src/bot/commands/audio/mod.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use log::debug; +use serenity::client::Cache; use serenity::model::application::interaction::{InteractionResponseType, application_command::ApplicationCommandInteraction}; use serenity::model::prelude::{GuildId, ChannelId}; use serenity::model::user::User; @@ -29,16 +30,7 @@ pub struct AudioConfig { pub volume: f32 } -/// Joins a Discord voice channel. -/// -/// # Arguments -/// - ctx - The context of the command. -/// - guild_id_option - The guild ID of the guild to join. -/// - user - The user that is requesting to join the voice channel. -/// -/// # Returns -/// Result<(), String> - Ok if the bot successfully joined the voice channel, Err if there was an error. -pub async fn join(ctx: &Context, guild_id_option: &Option, user: &User) -> Result<(), String> { +pub async fn join(cache: &Arc, manager: Arc, guild_id_option: &Option, user: &User) -> Result<(), String> { let guild_id = match guild_id_option { Some(g) => g, None => { @@ -46,13 +38,12 @@ pub async fn join(ctx: &Context, guild_id_option: &Option, user: &User) } }; - let channel_id = match find_voice_channel(&ctx, &guild_id, &user) { + let channel_id = match find_voice_channel(cache, &guild_id, &user) { Ok(channel) => channel, Err(err) => return Err(format!("{}", err)) }; debug!("<{}> Joining channel {}", guild_id.0, channel_id); - let manager = get_songbird(ctx).await; let (_handle_lock, success) = manager.join(guild_id.to_owned(), channel_id.to_owned()).await; match success { Ok(s) => Ok(s), @@ -60,15 +51,7 @@ pub async fn join(ctx: &Context, guild_id_option: &Option, user: &User) } } -/// Leaves a Discord voice channel. -/// -/// # Arguments -/// - ctx - The context of the command. -/// - guild_id_option - The guild ID of the guild to leave. -/// -/// # Returns -/// Result<(), String> - Ok if the bot successfully left the voice channel, Err if there was an error. -pub async fn leave(ctx: &Context, guild_id_option: &Option) -> Result<(), String> { +pub async fn leave(manager: Arc, guild_id_option: &Option) -> Result<(), String> { let guild_id = match guild_id_option { Some(g) => g, None => { @@ -76,7 +59,6 @@ pub async fn leave(ctx: &Context, guild_id_option: &Option) -> Result<( } }; - let manager = get_songbird(ctx).await; if manager.get(*guild_id).is_some() { debug!("<{}> Disconnecting from channel", guild_id.0); if let Err(e) = manager.remove(*guild_id).await { @@ -86,17 +68,8 @@ pub async fn leave(ctx: &Context, guild_id_option: &Option) -> Result<( Ok(()) } -/// Finds the voice channel that the user is in. -/// -/// # Arguments -/// - ctx - The context of the command. -/// - guild_id - The guild ID of the guild to search. -/// - user - The user to search for. -/// -/// # Returns -/// Result - Ok if the user is in a voice channel, Err if the user is not in a voice channel. -fn find_voice_channel(ctx: &Context, guild_id: &GuildId, user: &User) -> Result { - let guild = match guild_id.to_guild_cached(ctx.cache.to_owned()) { +fn find_voice_channel(cache: &Arc, guild_id: &GuildId, user: &User) -> Result { + let guild = match guild_id.to_guild_cached(cache.to_owned()) { Some(g) => g, None => return Err(format!("Guild not found")) }; @@ -107,15 +80,6 @@ fn find_voice_channel(ctx: &Context, guild_id: &GuildId, user: &User) -> Result< } } -/// Creates a response to an interaction. -/// -/// # Arguments -/// - ctx - The context of the command. -/// - command - The command that was sent. -/// - content - The content of the response. -/// -/// # Returns -/// Result<(), SerenityError> - Ok if the response was created successfully, Err if there was an error. pub async fn create_response(ctx: &Context, command: &ApplicationCommandInteraction, content: String) -> Result<(), SerenityError> { command.create_interaction_response(&ctx.http, |response: &mut serenity::builder::CreateInteractionResponse<'_>| { response @@ -124,31 +88,13 @@ pub async fn create_response(ctx: &Context, command: &ApplicationCommandInteract }).await } -/// Edits a response to an interaction. -/// -/// # Arguments -/// - ctx - The context of the command. -/// - command - The command that was sent. -/// - content - The content of the response. -/// -/// # Returns -/// Result - Ok if the response was edited successfully, Err if there was an error. pub async fn edit_response(ctx: &Context, command: &ApplicationCommandInteraction, content: String) -> Result { command.edit_original_interaction_response(&ctx.http, |response: &mut serenity::builder::EditInteractionResponse| { response.content(content) }).await } -/// Adds a song to the queue. -/// -/// # Arguments -/// - call - The call to add the song to. -/// - url - The URL of the song to add. -/// - lazy - Whether or not to lazy load the song. -/// -/// # Returns -/// Result - Ok if the song was added successfully, Err if there was an error. -pub async fn add_song(call: Arc>, url: &str, lazy: bool, audio_config: Option<&AudioConfig>) -> Result { +pub async fn add_song(call: Arc>, url: &str, lazy: bool, volume: Option) -> Result { let source = if is_valid_url(url) { Restartable::ytdl(url.to_owned(), lazy).await? } else { @@ -158,19 +104,12 @@ pub async fn add_song(call: Arc>, url: &str, lazy: bool, audio_confi let track: Input = source.into(); let metadata = *track.metadata.clone(); let track_handle = handler.enqueue_source(track); - if let Some(ac) = audio_config { - let _ = track_handle.set_volume(ac.volume); + if let Some(volume) = volume { + let _ = track_handle.set_volume(volume); } Ok(metadata) } -/// Checks if a string is a valid URL. -/// -/// # Arguments -/// - url - The string to check. -/// -/// # Returns -/// bool - True if the string is a valid URL, false if it is not. fn is_valid_url(url: &str) -> bool { match url.parse::() { Ok(_) => return true, @@ -178,13 +117,6 @@ fn is_valid_url(url: &str) -> bool { } } -/// Gets the Songbird voice client. -/// -/// # Arguments -/// - ctx - The context of the command. -/// -/// # Returns -/// Arc - The Songbird voice client. pub async fn get_songbird(ctx: &Context) -> Arc { songbird::get(ctx).await.expect("Songbird Voice client placed in at initialization") } diff --git a/service/src/bot/commands/audio/play.rs b/service/src/bot/commands/audio/play.rs index cb0442b..164b40e 100644 --- a/service/src/bot/commands/audio/play.rs +++ b/service/src/bot/commands/audio/play.rs @@ -1,11 +1,18 @@ +use std::sync::Arc; + use log::{debug, warn, error}; +use serenity::model::prelude::GuildId; +use serenity::model::user::User; use serenity::{prelude::*, async_trait}; use serenity::builder::CreateApplicationCommand; use serenity::model::application::interaction::application_command::ApplicationCommandInteraction; +use siren::ServiceError; use songbird::EventHandler; -use crate::bot::commands::audio::{join, leave, add_song, get_songbird, AudioConfigs}; +use crate::AppState; +use crate::bot::commands::audio::{join, leave, add_song, get_songbird}; +use crate::db::guilds::QueryGuild; use super::{create_response, edit_response}; @@ -46,7 +53,8 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { return; } - match join(&ctx, &command.guild_id, &command.user).await { + let manager = get_songbird(ctx).await; + match join(&ctx.cache, manager,&command.guild_id, &command.user).await { Ok(_) => { let guild_id = match command.guild_id { Some(g) => g, @@ -65,12 +73,8 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { let call_handler = handler_lock.lock().await; call_handler.queue().is_empty() }; - let audio_config = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected AudioConfigs in TypeMap.").clone() - }; - let ac = audio_config.read().await; - match add_song(handler_lock.clone(), &track_url, is_queue_empty, ac.get(&guild_id)).await { + let guild = QueryGuild::get(guild_id.0 as i64).unwrap(); + match add_song(handler_lock.clone(), &track_url, is_queue_empty, Some(guild.volume)).await { Ok(added_song) => { let track_title = added_song.title.unwrap(); debug!("Added track: {}", track_title); @@ -86,7 +90,7 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { if let Err(why) = edit_response(&ctx, &command, format!("Failed to add song: {}", why)).await { error!("Failed to edit response message: {}", why); } - if let Err(why) = leave(&ctx, &command.guild_id).await { + if let Err(why) = leave(manager, &command.guild_id).await { error!("Failed to leave voice channel: {}", why); } return; @@ -103,6 +107,40 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { } } +pub async fn play(state: Arc, guild_id: Option, user: &User, track_url: String) -> Result<(), ServiceError> { + match join(&state.cache, Arc::clone(&state.songbird), &guild_id, user).await { + Ok(_) => { + let guild_id = match guild_id { + Some(g) => g, + None => { + return Err(ServiceError { + status: 422, + message: "No guild ID set".to_string() + }); + } + }; + if let Some(handler_lock) = state.songbird.get(guild_id) { + let is_queue_empty = { + let call_handler = handler_lock.lock().await; + call_handler.queue().is_empty() + }; + let guild = QueryGuild::get(guild_id.0 as i64)?; + match add_song(handler_lock.clone(), &track_url, is_queue_empty, Some(guild.volume)).await { + Ok(_) => {}, + Err(_) => {} + } + } + Ok(()) + }, + Err(err) => { + return Err(ServiceError { + status: 422, + message: err.to_string() + }); + } + } +} + pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { command.name("play").description("Plays the given track").create_option(|option| { option .name("track") diff --git a/service/src/bot/commands/audio/volume.rs b/service/src/bot/commands/audio/volume.rs index 4ef8501..a8cf258 100644 --- a/service/src/bot/commands/audio/volume.rs +++ b/service/src/bot/commands/audio/volume.rs @@ -4,7 +4,9 @@ use serenity::prelude::*; use serenity::builder::CreateApplicationCommand; use serenity::model::application::interaction::application_command::ApplicationCommandInteraction; -use super::{get_songbird, create_response, edit_response, AudioConfigs, AudioConfig}; +use crate::db::guilds::InsertGuild; + +use super::{get_songbird, create_response, edit_response}; pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { // Get the volume @@ -55,14 +57,7 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { return; } }; - let audio_config_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected AudioConfigs in TypeMap.").clone() - }; - { - let mut audio_configs = audio_config_lock.write().await; - *audio_configs.entry(guild_id).or_insert(AudioConfig { volume: 1.0 }) = AudioConfig { volume: bound_volume }; - } + let _ = InsertGuild::update_audio(guild_id.0 as i64, bound_volume); let manager = get_songbird(ctx).await; if let Some(handler_lock) = manager.get(guild_id) { let handler = handler_lock.lock().await; diff --git a/service/src/bot/handler.rs b/service/src/bot/handler.rs index 893eaf8..848ebdb 100644 --- a/service/src/bot/handler.rs +++ b/service/src/bot/handler.rs @@ -5,8 +5,10 @@ use serenity::model::gateway::Ready; use serenity::model::channel::Message; use serenity::prelude::*; +use crate::db::guilds::InsertGuild; + use super::commands; -use super::commands::audio::{AudioConfigs, create_response, AudioConfig}; +use super::commands::audio::create_response; pub struct Handler { // Open AI Config @@ -72,14 +74,7 @@ impl EventHandler for Handler { warn!("No ready guilds found"); } for guild in ready.guilds { - let audio_config_lock = { - let data_read = ctx.data.read().await; - data_read.get::().expect("Expected AudioConfigs in TypeMap.").clone() - }; - { - let mut audio_configs = audio_config_lock.write().await; - let _ = audio_configs.insert(guild.id, AudioConfig { volume: 1.0 }); - } + let _ = InsertGuild::insert(InsertGuild { id: (guild.id.0 as i64), name: "".to_string(), volume: 100.0 }); let commands = guild.id.set_application_commands(&ctx.http, |commands| { commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::ping::register(command) }) .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::play::register(command) }) diff --git a/service/src/db/guilds/mod.rs b/service/src/db/guilds/mod.rs new file mode 100644 index 0000000..24e3024 --- /dev/null +++ b/service/src/db/guilds/mod.rs @@ -0,0 +1,3 @@ +mod model; + +pub use model::*; \ No newline at end of file diff --git a/service/src/db/guilds/model.rs b/service/src/db/guilds/model.rs new file mode 100644 index 0000000..4017cab --- /dev/null +++ b/service/src/db/guilds/model.rs @@ -0,0 +1,43 @@ +use diesel::prelude::*; +use serde::{Serialize, Deserialize}; +use siren::ServiceError; + +use crate::db::{schema::guilds, connection}; + +#[derive(Queryable, QueryableByName, Serialize, Deserialize)] +#[diesel(table_name = guilds)] +pub struct QueryGuild { + pub id: i64, + pub name: String, + pub volume: f32 +} + +impl QueryGuild { + pub fn get(id: i64) -> Result { + let mut conn = connection()?; + let guild = guilds::table.filter(guilds::id.eq(id)).first(&mut conn)?; + Ok(guild) + } +} + +#[derive(Insertable, AsChangeset, Serialize, Deserialize)] +#[diesel(table_name = guilds)] +pub struct InsertGuild { + pub id: i64, + pub name: String, + pub volume: f32 +} + +impl InsertGuild { + pub fn insert(guild: Self) -> Result { + let mut conn = connection()?; + let guild = diesel::insert_into(guilds::table).values(guild).get_result(&mut conn)?; + Ok(guild) + } + + pub fn update_audio(id: i64, volume: f32) -> Result { + let mut conn = connection()?; + let guild = diesel::update(guilds::table.filter(guilds::id.eq(id))).set(guilds::volume.eq(volume)).get_result(&mut conn)?; + Ok(guild) + } +} diff --git a/service/src/db/mod.rs b/service/src/db/mod.rs index 4b69032..d444418 100644 --- a/service/src/db/mod.rs +++ b/service/src/db/mod.rs @@ -11,6 +11,7 @@ pub mod bestiary; pub mod classes; pub mod conditions; pub mod feats; +pub mod guilds; pub mod items; pub mod messages; pub mod options; diff --git a/service/src/db/schema.rs b/service/src/db/schema.rs index a148271..2564bdc 100644 --- a/service/src/db/schema.rs +++ b/service/src/db/schema.rs @@ -29,4 +29,12 @@ diesel::table! { attack_type -> Nullable, data -> Jsonb } +} + +diesel::table! { + guilds (id) { + id -> BigInt, + name -> Text, + volume -> Float, + } } \ No newline at end of file diff --git a/service/src/main.rs b/service/src/main.rs index c0fe250..200bf01 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -3,19 +3,18 @@ extern crate diesel; extern crate diesel_migrations; use std::env; -use std::collections::{HashSet, HashMap}; +use std::collections::HashSet; use std::sync::Arc; -use bot::commands::audio::AudioConfig; use log::{error, warn, info}; +use serenity::client::Cache; use serenity::framework::StandardFramework; use serenity::http::Http; -use serenity::model::prelude::GuildId; use serenity::prelude::*; use songbird::{SerenityInit, Songbird}; use actix_cors::Cors; use actix_web::{HttpServer, App, web}; -use crate::bot::{commands::{oai::GPTModel, audio::AudioConfigs}, handler::Handler}; +use crate::bot::{commands::oai::GPTModel, handler::Handler}; use dotenv::dotenv; @@ -74,35 +73,23 @@ async fn main() -> std::io::Result<()> { } }; - // let songbird = Songbird::serenity_from_config(songbird::Config::default().decode_mode(songbird::driver::DecodeMode::Decode)); let songbird = Songbird::serenity(); let mut client = Client::builder(token, intents) .event_handler(handler) .framework(StandardFramework::new() .configure(|c| c.owners(owners))) - // .register_songbird_with(Arc::clone(&songbird)) - // .register_songbird_from_config(songbird::Config::default().decode_mode(songbird::driver::DecodeMode::Decode)) .register_songbird() .await .expect("Error creating client"); - let audio_configs: Arc>> = Arc::new(RwLock::new(HashMap::default())); - - { - let mut data = client.data.write().await; - data.insert::(Arc::clone(&audio_configs)); - } - let http = Arc::clone(&client.cache_and_http.http); - // let cache_http = Arc::clone(&client.cache_and_http.clone()); - // let data = Arc::clone(&client.data.clone()); - // let t = songbird::Config::default().decode_mode(songbird::driver::DecodeMode::Decode); + let cache = Arc::clone(&client.cache_and_http.cache); let app_data = Arc::new(AppState { http, - songbird: Arc::clone(&songbird), - audio_configs + cache, + songbird: Arc::clone(&songbird) }); @@ -153,6 +140,6 @@ async fn main() -> std::io::Result<()> { pub struct AppState { pub http: Arc, - pub songbird: Arc, - pub audio_configs: Arc>> + pub cache: Arc, + pub songbird: Arc } From 690a327084e32283cf2182c9743d150a8cfdcde3 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sat, 7 Oct 2023 22:55:31 -0400 Subject: [PATCH 06/28] Cleanup, working on getting play to work --- service/Dockerfile | 11 +-- .../migrations/000010_create_guilds/up.sql | 3 +- service/src/bot/api/routes.rs | 66 +++++++++---- service/src/bot/commands/audio/mod.rs | 35 +++---- service/src/bot/commands/audio/play.rs | 98 +++++++------------ service/src/bot/commands/audio/volume.rs | 2 +- service/src/bot/handler.rs | 2 +- service/src/db/guilds/model.rs | 8 +- service/src/db/schema.rs | 3 +- 9 files changed, 109 insertions(+), 119 deletions(-) diff --git a/service/Dockerfile b/service/Dockerfile index 1bad8a4..c0bbaba 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -17,10 +17,10 @@ RUN cargo build --release FROM debian:bookworm-slim as packages WORKDIR /packages -RUN apt-get update && apt-get install -y curl tar xz-utils ARG TARGETPLATFORM -# Check if the target platform is linux/x86_64, otherwise log error and exit -RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ + +RUN apt-get update && apt-get install -y curl tar xz-utils && \ + if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ echo "amd64" && false; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_armv7l > yt-dlp && \ @@ -53,9 +53,6 @@ USER root COPY --from=builder /builder/target/release/service /usr/local/bin/service COPY --from=packages /packages /usr/bin -RUN apt-get update && apt-get install -y \ - libc6 libc6-dev libopus-dev libpq5 libpq-dev python3-pip ffmpeg - # apt-get auto-remove -y && \ -# RUN pipx install yt-dlp +RUN apt-get update && apt-get install -y libc6 libc6-dev libopus-dev libpq5 libpq-dev python3-pip ffmpeg CMD ["service"] diff --git a/service/migrations/000010_create_guilds/up.sql b/service/migrations/000010_create_guilds/up.sql index 54c825d..b66576f 100644 --- a/service/migrations/000010_create_guilds/up.sql +++ b/service/migrations/000010_create_guilds/up.sql @@ -1,5 +1,4 @@ CREATE TABLE IF NOT EXISTS guilds ( id BIGINT PRIMARY KEY NOT NULL, - name TEXT NOT NULL, - volume INTEGER NOT NULL + volume DOUBLE PRECISION NOT NULL ); \ No newline at end of file diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index 8e08438..ab5212e 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -6,7 +6,7 @@ use serde::{Serialize, Deserialize}; use serenity::model::prelude::{GuildChannel, ChannelType}; use siren::ServiceError; -use crate::AppState; +use crate::{AppState, bot::commands::audio::{play::play_track, join}}; #[get("/guilds")] async fn get_guilds(data: web::Data>) -> HttpResponse { @@ -109,32 +109,61 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json, data: web::Data>) -> HttpResponse { - let guild_id = path.into_inner(); +#[derive(Serialize, Deserialize)] +struct PlayRequest { + track_url: String +} + +#[post("/{guild_id}/voice/{channel_id}/play")] +async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>) -> HttpResponse { + let (guild_id, channel_id) = path.into_inner(); let guild_id = match guild_id.parse::() { Ok(id) => id, Err(err) => { warn!("Could not parse guild id: {:?}", err); - return ResponseError::error_response(&ServiceError { - status: 422, - message: err.to_string() - }) + return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) + } + }; + let channel_id = match channel_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse channel id: {:?}", err); + return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) + } + }; + let http = Pin::new(&data.http).get_ref(); + let guild = match http.get_guild(guild_id).await { + Ok(guild) => guild, + Err(err) => { + warn!("Could not get guild: {:?}", err); + return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) + } + }; + let channel = match http.get_channel(channel_id).await { + Ok(channel) => channel, + Err(err) => { + warn!("Could not get channel: {:?}", err); + return ResponseError::error_response(&ServiceError { status: 422, message: err.to_string() }) } }; - if let Some(handler_lock) = data.songbird.get(guild_id) { - let handler = handler_lock.lock().await; - if let Err(err) = handler.queue().pause() { - warn!("Could not pause track: {:?}", err); - return ResponseError::error_response(&ServiceError { - status: 422, - message: err.to_string() - }) + let manager = Arc::clone(&data.songbird); + + match join(Arc::clone(&manager), &guild.id, &channel.id()).await { + Ok(_) => { + match play_track(Arc::clone(&data.songbird), guild.id, play_request.track_url.to_string()).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(err) => { + warn!("Could not play track: {:?}", err); + return ResponseError::error_response(&err) + } + } + }, + Err(err) => { + warn!("Could not join channel: {:?}", err); + return ResponseError::error_response(&ServiceError { status: 500, message: err.to_string() }) } } - - HttpResponse::Ok().finish() } #[post("/{guild_id}/voice/pause")] @@ -173,5 +202,6 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(get_voice_channels) .service(send_message) .service(pause) + .service(play) ); } \ No newline at end of file diff --git a/service/src/bot/commands/audio/mod.rs b/service/src/bot/commands/audio/mod.rs index 4eaeb76..7d0a793 100644 --- a/service/src/bot/commands/audio/mod.rs +++ b/service/src/bot/commands/audio/mod.rs @@ -1,13 +1,13 @@ -use std::collections::HashMap; use std::sync::Arc; -use log::debug; +use log::{debug, warn}; use serenity::client::Cache; use serenity::model::application::interaction::{InteractionResponseType, application_command::ApplicationCommandInteraction}; use serenity::model::prelude::{GuildId, ChannelId}; use serenity::model::user::User; use serenity::prelude::*; +use siren::ServiceError; use songbird::{Call, Songbird}; use songbird::input::{Restartable, Input, Metadata, error::Error as SongbirdError}; @@ -18,36 +18,29 @@ pub mod skip; pub mod stop; pub mod volume; -#[derive(Clone, Debug)] -pub struct AudioConfigs; - -impl TypeMapKey for AudioConfigs { - type Value = Arc>>; -} - -#[derive(Clone, Debug)] -pub struct AudioConfig { - pub volume: f32 -} - -pub async fn join(cache: &Arc, manager: Arc, guild_id_option: &Option, user: &User) -> Result<(), String> { +pub async fn join_by_user(cache: &Arc, manager: Arc, guild_id_option: &Option, user: &User) -> Result<(), ServiceError> { let guild_id = match guild_id_option { Some(g) => g, - None => { - return Err(format!("{}", "No guild ID set")); - } + None => return Err(ServiceError { status: 422, message: format!("{}", "No guild ID set") }) }; let channel_id = match find_voice_channel(cache, &guild_id, &user) { Ok(channel) => channel, - Err(err) => return Err(format!("{}", err)) + Err(err) => return Err(ServiceError { status: 500, message: err.to_string() }) }; - debug!("<{}> Joining channel {}", guild_id.0, channel_id); + join(manager, guild_id, &channel_id).await +} + +pub async fn join(manager: Arc, guild_id: &GuildId, channel_id: &ChannelId) -> Result<(), ServiceError> { + debug!("<{}> Joining channel {}", guild_id.0, channel_id.0); let (_handle_lock, success) = manager.join(guild_id.to_owned(), channel_id.to_owned()).await; match success { Ok(s) => Ok(s), - Err(err) => Err(format!("{}", err)) + Err(err) => { + warn!("Failed to join channel: {:?}", err); + Err(ServiceError { status: 500, message: err.to_string() }) + } } } diff --git a/service/src/bot/commands/audio/play.rs b/service/src/bot/commands/audio/play.rs index 164b40e..3fb0c7c 100644 --- a/service/src/bot/commands/audio/play.rs +++ b/service/src/bot/commands/audio/play.rs @@ -3,18 +3,16 @@ use std::sync::Arc; use log::{debug, warn, error}; use serenity::model::prelude::GuildId; -use serenity::model::user::User; use serenity::{prelude::*, async_trait}; use serenity::builder::CreateApplicationCommand; use serenity::model::application::interaction::application_command::ApplicationCommandInteraction; use siren::ServiceError; -use songbird::EventHandler; +use songbird::{EventHandler, Songbird}; -use crate::AppState; -use crate::bot::commands::audio::{join, leave, add_song, get_songbird}; +use crate::bot::commands::audio::{leave, add_song, get_songbird}; use crate::db::guilds::QueryGuild; -use super::{create_response, edit_response}; +use super::{create_response, edit_response, join_by_user}; pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { // Get the track url @@ -54,7 +52,7 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { } let manager = get_songbird(ctx).await; - match join(&ctx.cache, manager,&command.guild_id, &command.user).await { + match join_by_user(&ctx.cache, manager,&command.guild_id, &command.user).await { Ok(_) => { let guild_id = match command.guild_id { Some(g) => g, @@ -66,37 +64,20 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { } }; debug!("Play command executed with track: {:?}", track_url); - let manager = get_songbird(ctx).await; - if let Some(handler_lock) = manager.get(guild_id) { - let is_queue_empty = { - let call_handler = handler_lock.lock().await; - call_handler.queue().is_empty() - }; - let guild = QueryGuild::get(guild_id.0 as i64).unwrap(); - match add_song(handler_lock.clone(), &track_url, is_queue_empty, Some(guild.volume)).await { - Ok(added_song) => { - let track_title = added_song.title.unwrap(); - debug!("Added track: {}", track_title); - if let Err(why) = edit_response(&ctx, &command, format!("Added track to queue: {}", track_title)).await { - error!("Failed to edit response message: {}", why); - } - let mut handler = handler_lock.lock().await; - handler.remove_all_global_events(); - handler.add_global_event(songbird::Event::Track(songbird::TrackEvent::End), TrackEndNotifier { guild_id, call: manager }) + match play_track(manager, guild_id, track_url).await { + Ok(_) => { + if let Err(why) = edit_response(&ctx, &command, "Playing track".to_string()).await { + error!("Failed to edit response message: {}", why); } - Err(why) => { - warn!("Failed to add song: {}", why); - if let Err(why) = edit_response(&ctx, &command, format!("Failed to add song: {}", why)).await { - error!("Failed to edit response message: {}", why); - } - if let Err(why) = leave(manager, &command.guild_id).await { - error!("Failed to leave voice channel: {}", why); - } - return; + }, + Err(err) => { + warn!("Failed to play track: {}", err); + if let Err(why) = edit_response(&ctx, &command, format!("Failed to play track: {}", err)).await { + error!("Failed to edit response message: {}", why); } - }; - } + } + }; }, Err(err) => { warn!("{}", err); @@ -107,38 +88,31 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { } } -pub async fn play(state: Arc, guild_id: Option, user: &User, track_url: String) -> Result<(), ServiceError> { - match join(&state.cache, Arc::clone(&state.songbird), &guild_id, user).await { - Ok(_) => { - let guild_id = match guild_id { - Some(g) => g, - None => { - return Err(ServiceError { - status: 422, - message: "No guild ID set".to_string() - }); - } - }; - if let Some(handler_lock) = state.songbird.get(guild_id) { - let is_queue_empty = { - let call_handler = handler_lock.lock().await; - call_handler.queue().is_empty() - }; - let guild = QueryGuild::get(guild_id.0 as i64)?; - match add_song(handler_lock.clone(), &track_url, is_queue_empty, Some(guild.volume)).await { - Ok(_) => {}, - Err(_) => {} +pub async fn play_track(manager: Arc, guild_id: GuildId, track_url: String) -> Result<(), ServiceError> { + if let Some(handler_lock) = manager.get(guild_id) { + let is_queue_empty = { + let call_handler = handler_lock.lock().await; + call_handler.queue().is_empty() + }; + let guild = QueryGuild::get(guild_id.0 as i64)?; + match add_song(handler_lock.clone(), &track_url, is_queue_empty, Some(guild.volume as f32)).await { + Ok(added_song) => { + let track_title = added_song.title.unwrap(); + debug!("Added track: {}", track_title); + let mut handler = handler_lock.lock().await; + handler.remove_all_global_events(); + handler.add_global_event(songbird::Event::Track(songbird::TrackEvent::End), TrackEndNotifier { guild_id, call: manager }) + }, + Err(err) => { + warn!("Failed to add song: {}", err); + if let Err(why) = leave(manager, &Some(guild_id)).await { + error!("Failed to leave voice channel: {}", why); } + return Err(ServiceError { status: 422, message: err.to_string() }) } - Ok(()) - }, - Err(err) => { - return Err(ServiceError { - status: 422, - message: err.to_string() - }); } } + Ok(()) } pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { diff --git a/service/src/bot/commands/audio/volume.rs b/service/src/bot/commands/audio/volume.rs index a8cf258..51fa174 100644 --- a/service/src/bot/commands/audio/volume.rs +++ b/service/src/bot/commands/audio/volume.rs @@ -57,7 +57,7 @@ pub async fn run(ctx: &Context, command: &ApplicationCommandInteraction) { return; } }; - let _ = InsertGuild::update_audio(guild_id.0 as i64, bound_volume); + let _ = InsertGuild::update_audio(guild_id.0 as i64, bound_volume as f64); let manager = get_songbird(ctx).await; if let Some(handler_lock) = manager.get(guild_id) { let handler = handler_lock.lock().await; diff --git a/service/src/bot/handler.rs b/service/src/bot/handler.rs index 848ebdb..c0bbd0d 100644 --- a/service/src/bot/handler.rs +++ b/service/src/bot/handler.rs @@ -74,7 +74,7 @@ impl EventHandler for Handler { warn!("No ready guilds found"); } for guild in ready.guilds { - let _ = InsertGuild::insert(InsertGuild { id: (guild.id.0 as i64), name: "".to_string(), volume: 100.0 }); + let _ = InsertGuild::insert(InsertGuild { id: (guild.id.0 as i64), volume: 100.0 }); let commands = guild.id.set_application_commands(&ctx.http, |commands| { commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::ping::register(command) }) .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::play::register(command) }) diff --git a/service/src/db/guilds/model.rs b/service/src/db/guilds/model.rs index 4017cab..ccc6904 100644 --- a/service/src/db/guilds/model.rs +++ b/service/src/db/guilds/model.rs @@ -8,8 +8,7 @@ use crate::db::{schema::guilds, connection}; #[diesel(table_name = guilds)] pub struct QueryGuild { pub id: i64, - pub name: String, - pub volume: f32 + pub volume: f64 } impl QueryGuild { @@ -24,8 +23,7 @@ impl QueryGuild { #[diesel(table_name = guilds)] pub struct InsertGuild { pub id: i64, - pub name: String, - pub volume: f32 + pub volume: f64 } impl InsertGuild { @@ -35,7 +33,7 @@ impl InsertGuild { Ok(guild) } - pub fn update_audio(id: i64, volume: f32) -> Result { + pub fn update_audio(id: i64, volume: f64) -> Result { let mut conn = connection()?; let guild = diesel::update(guilds::table.filter(guilds::id.eq(id))).set(guilds::volume.eq(volume)).get_result(&mut conn)?; Ok(guild) diff --git a/service/src/db/schema.rs b/service/src/db/schema.rs index 2564bdc..290464b 100644 --- a/service/src/db/schema.rs +++ b/service/src/db/schema.rs @@ -34,7 +34,6 @@ diesel::table! { diesel::table! { guilds (id) { id -> BigInt, - name -> Text, - volume -> Float, + volume -> Float8, } } \ No newline at end of file From 0ec6264bfaafc034d80c23209bd91d43c76566c0 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sat, 7 Oct 2023 23:26:12 -0400 Subject: [PATCH 07/28] Got play to work --- service/migrations/000010_create_guilds/up.sql | 1 + service/src/bot/handler.rs | 6 +++++- service/src/db/guilds/model.rs | 2 ++ service/src/db/schema.rs | 1 + service/src/main.rs | 3 +-- 5 files changed, 10 insertions(+), 3 deletions(-) diff --git a/service/migrations/000010_create_guilds/up.sql b/service/migrations/000010_create_guilds/up.sql index b66576f..d2dd580 100644 --- a/service/migrations/000010_create_guilds/up.sql +++ b/service/migrations/000010_create_guilds/up.sql @@ -1,4 +1,5 @@ CREATE TABLE IF NOT EXISTS guilds ( id BIGINT PRIMARY KEY NOT NULL, + bot_id BIGINT NOT NULL, volume DOUBLE PRECISION NOT NULL ); \ No newline at end of file diff --git a/service/src/bot/handler.rs b/service/src/bot/handler.rs index c0bbd0d..de09fee 100644 --- a/service/src/bot/handler.rs +++ b/service/src/bot/handler.rs @@ -74,7 +74,11 @@ impl EventHandler for Handler { warn!("No ready guilds found"); } for guild in ready.guilds { - let _ = InsertGuild::insert(InsertGuild { id: (guild.id.0 as i64), volume: 100.0 }); + let _ = InsertGuild::insert(InsertGuild { + id: (guild.id.0 as i64), + bot_id: ctx.cache.current_user().id.0 as i64, + volume: 100.0 + }); let commands = guild.id.set_application_commands(&ctx.http, |commands| { commands.create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::ping::register(command) }) .create_application_command(|command: &mut serenity::builder::CreateApplicationCommand| { commands::audio::play::register(command) }) diff --git a/service/src/db/guilds/model.rs b/service/src/db/guilds/model.rs index ccc6904..2c3039f 100644 --- a/service/src/db/guilds/model.rs +++ b/service/src/db/guilds/model.rs @@ -8,6 +8,7 @@ use crate::db::{schema::guilds, connection}; #[diesel(table_name = guilds)] pub struct QueryGuild { pub id: i64, + pub bot_id: i64, pub volume: f64 } @@ -23,6 +24,7 @@ impl QueryGuild { #[diesel(table_name = guilds)] pub struct InsertGuild { pub id: i64, + pub bot_id: i64, pub volume: f64 } diff --git a/service/src/db/schema.rs b/service/src/db/schema.rs index 290464b..64b84e5 100644 --- a/service/src/db/schema.rs +++ b/service/src/db/schema.rs @@ -34,6 +34,7 @@ diesel::table! { diesel::table! { guilds (id) { id -> BigInt, + bot_id -> BigInt, volume -> Float8, } } \ No newline at end of file diff --git a/service/src/main.rs b/service/src/main.rs index 200bf01..e76172e 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -79,7 +79,7 @@ async fn main() -> std::io::Result<()> { .event_handler(handler) .framework(StandardFramework::new() .configure(|c| c.owners(owners))) - .register_songbird() + .register_songbird_with(Arc::clone(&songbird)) .await .expect("Error creating client"); @@ -116,7 +116,6 @@ async fn main() -> std::io::Result<()> { .allow_any_header() .max_age(3600); App::new() - // .app_data(web::Data::new(Arc::clone(&http))) .app_data(web::Data::new(Arc::clone(&app_data))) .configure(crate::db::messages::init_routes) .configure(crate::db::spells::init_routes) From 1035d3bc21c80a7dc6a53c1d75552830816aaa08 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sun, 8 Oct 2023 00:18:39 -0400 Subject: [PATCH 08/28] First pass on bot management page --- service/src/bot/api/routes.rs | 54 ++++++++++++++- ui/package-lock.json | 24 ++++++- ui/package.json | 1 + ui/src/api/guilds.ts | 33 +++++++++ ui/src/api/guilds.types.ts | 13 ++++ ui/src/api/index.ts | 2 +- ui/src/app/management/page.tsx | 106 +++++++++++++++++++++++++++++ ui/src/components/Topbar/index.tsx | 4 ++ 8 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 ui/src/api/guilds.ts create mode 100644 ui/src/api/guilds.types.ts create mode 100644 ui/src/app/management/page.tsx diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index ab5212e..976b096 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -166,6 +166,56 @@ async fn play(path: web::Path<(String, String)>, play_request: web::Json, data: web::Data>) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + if let Some(handler_lock) = data.songbird.get(guild_id) { + let handler = handler_lock.lock().await; + handler.queue().stop(); + } + + HttpResponse::Ok().finish() +} + +#[post("/{guild_id}/voice/resume")] +async fn resume(path: web::Path, data: web::Data>) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + if let Some(handler_lock) = data.songbird.get(guild_id) { + let handler = handler_lock.lock().await; + if let Err(err) = handler.queue().resume() { + warn!("Could not resume track: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + } + + HttpResponse::Ok().finish() +} + #[post("/{guild_id}/voice/pause")] async fn pause(path: web::Path, data: web::Data>) -> HttpResponse { let guild_id = path.into_inner(); @@ -201,7 +251,9 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(get_text_channels) .service(get_voice_channels) .service(send_message) - .service(pause) .service(play) + .service(stop) + .service(resume) + .service(pause) ); } \ No newline at end of file diff --git a/ui/package-lock.json b/ui/package-lock.json index 2d20efe..0f90ac5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@mantine/core": "^7.1.2", + "@mantine/form": "^7.1.2", "@mantine/hooks": "^7.1.2", "@mantine/modals": "^7.1.2", "@mantine/notifications": "^7.1.2", @@ -225,6 +226,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@mantine/form": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.1.2.tgz", + "integrity": "sha512-FnUu5XNmRM265G0wy19qSRiItG/2eQ0GQCctnokw6ws9ZnCU1NqvsmpuDE/UiV4YCAOhAVHfqnjG/8tsrlw7ug==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "klona": "^2.0.5" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/@mantine/hooks": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.1.2.tgz", @@ -2305,8 +2318,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.3.0", @@ -3353,6 +3365,14 @@ "json-buffer": "3.0.1" } }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "engines": { + "node": ">= 8" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", diff --git a/ui/package.json b/ui/package.json index 7be067a..9df65eb 100644 --- a/ui/package.json +++ b/ui/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@mantine/core": "^7.1.2", + "@mantine/form": "^7.1.2", "@mantine/hooks": "^7.1.2", "@mantine/modals": "^7.1.2", "@mantine/notifications": "^7.1.2", diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts new file mode 100644 index 0000000..09de488 --- /dev/null +++ b/ui/src/api/guilds.ts @@ -0,0 +1,33 @@ +import { getRequest, postRequest } from '.'; +import { GuildChannel, GuildInfo } from './guilds.types'; + +export async function getGuilds(): Promise { + const response = await getRequest('guilds', {}); + return response?.data || { data: [] }; +} + +export async function getTextChannels(guildId: number): Promise { + const response = await getRequest(`guilds/${guildId}/text`, {}); + return response?.data || { data: [] }; +} + +export async function getVoiceChannels(guildId: number): Promise { + const response = await getRequest(`guilds/${guildId}/voice`, {}); + return response?.data || { data: [] }; +} + +export async function playTrack(guildId: number, channelId: number, track: string): Promise { + await postRequest(`guilds/${guildId}/voice/${channelId}/play`, { track_url: track }); +} + +export async function stopTrack(guildId: number, channelId: number): Promise { + await postRequest(`guilds/${guildId}/voice/${channelId}/stop`, {}); +} + +export async function pauseTrack(guildId: number, channelId: number): Promise { + await postRequest(`guilds/${guildId}/voice/${channelId}/pause`, {}); +} + +export async function resumeTrack(guildId: number, channelId: number): Promise { + await postRequest(`guilds/${guildId}/voice/${channelId}/resume`, {}); +} diff --git a/ui/src/api/guilds.types.ts b/ui/src/api/guilds.types.ts new file mode 100644 index 0000000..995153d --- /dev/null +++ b/ui/src/api/guilds.types.ts @@ -0,0 +1,13 @@ +export interface GuildInfo { + id: number; + icon?: string; + name: string; + owner: boolean; +} + +export interface GuildChannel { + id: number; + name: string; + type: string; + guild_id: number; +} diff --git a/ui/src/api/index.ts b/ui/src/api/index.ts index e0baecc..027cf23 100644 --- a/ui/src/api/index.ts +++ b/ui/src/api/index.ts @@ -12,7 +12,7 @@ export async function getRequest(endpoint: string, params: any): Promise | undefined> { const response = await axios - .post(`${serviceHost}:${servicePort}/${endpoint}`, { body }) + .post(`${serviceHost}:${servicePort}/${endpoint}`, body || {}) .catch((error) => console.error(error)); return response || undefined; } diff --git a/ui/src/app/management/page.tsx b/ui/src/app/management/page.tsx new file mode 100644 index 0000000..5eac9be --- /dev/null +++ b/ui/src/app/management/page.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { + getGuilds, + getTextChannels, + getVoiceChannels, + pauseTrack, + playTrack, + resumeTrack, + stopTrack +} from '@/api/guilds'; +import { GuildChannel, GuildInfo } from '@/api/guilds.types'; +import { Button, Slider, Tabs, TextInput } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import React, { useEffect, useState } from 'react'; + +export default function Page() { + const [guilds, setGuilds] = useState([]); + const [activeGuild, setActiveGuild] = useState(null); + const [textChannels, setTextChannels] = useState([]); + const [voiceChannels, setVoiceChannels] = useState([]); + + useEffect(() => { + getGuilds().then((g) => { + setGuilds(g); + if (g.length > 0) { + setActiveGuild(g[0]); + } + }); + }, []); + + useEffect(() => { + if (activeGuild) { + getTextChannels(activeGuild.id).then((c) => setTextChannels(c)); + getVoiceChannels(activeGuild.id).then((c) => setVoiceChannels(c)); + } + }, [activeGuild]); + + const playForm = useForm({ + initialValues: { + trackUrl: '' + } + }); + + return ( + + + {guilds.map((guild) => ( + setActiveGuild(guild)}> + {guild.name} + + ))} + + {guilds.map((guild) => ( + +

{guild.name}

+

Text Channels

+ + + {textChannels.map((channel) => ( + + {channel.name} + + ))} + + {textChannels.map((channel) => ( + + {channel.name} + + ))} + +

Voice Channels

+ + + {voiceChannels.map((channel) => ( + + {channel.name} + + ))} + + {voiceChannels.map((channel) => ( + + {channel.name} +
{ + playTrack(activeGuild!.id, channel.id, values.trackUrl); + })} + > + + + + + + +
+ {}} /> +
+
+ ))} +
+
+ ))} +
+ ); +} diff --git a/ui/src/components/Topbar/index.tsx b/ui/src/components/Topbar/index.tsx index 780e7a5..5b00233 100644 --- a/ui/src/components/Topbar/index.tsx +++ b/ui/src/components/Topbar/index.tsx @@ -32,6 +32,10 @@ const headerItems = [ { name: 'Spells', link: '/spells' + }, + { + name: 'Management', + link: '/management' } ]; From 6d30eb468b988bf8c41a67e255548b7499eb17b3 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sun, 8 Oct 2023 08:58:38 -0400 Subject: [PATCH 09/28] Updated to include volume --- service/src/bot/api/routes.rs | 42 +++++++++++++++++++++++++++++++++- ui/src/api/guilds.ts | 12 +++++----- ui/src/app/management/page.tsx | 21 +++++++++++++---- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index 976b096..1c06644 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -6,7 +6,7 @@ use serde::{Serialize, Deserialize}; use serenity::model::prelude::{GuildChannel, ChannelType}; use siren::ServiceError; -use crate::{AppState, bot::commands::audio::{play::play_track, join}}; +use crate::{AppState, bot::commands::audio::{play::play_track, join}, db::guilds::InsertGuild}; #[get("/guilds")] async fn get_guilds(data: web::Data>) -> HttpResponse { @@ -244,6 +244,46 @@ async fn pause(path: web::Path, data: web::Data>) -> HttpR HttpResponse::Ok().finish() } +#[derive(Serialize, Deserialize)] +struct SetVolume { + volume: String +} + +#[post("/{guild_id}/voice/volume")] +async fn set_volume(path: web::Path, volume: web::Json::, data: web::Data>) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + let bound_volume = volume.volume.parse::().unwrap_or(0.0); + + let _ = InsertGuild::update_audio(guild_id as i64, bound_volume as f64); + + if let Some(handler_lock) = data.songbird.get(guild_id) { + let handler = handler_lock.lock().await; + for (_, track_handle) in handler.queue().current_queue().iter().enumerate() { + let _ = track_handle.set_volume(bound_volume); + } + if let Err(err) = handler.queue().pause() { + warn!("Could not pause track: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + } + + HttpResponse::Ok().finish() +} + pub fn init_routes(config: &mut web::ServiceConfig) { config .service(get_guilds) diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts index 09de488..64bbe8b 100644 --- a/ui/src/api/guilds.ts +++ b/ui/src/api/guilds.ts @@ -20,14 +20,14 @@ export async function playTrack(guildId: number, channelId: number, track: strin await postRequest(`guilds/${guildId}/voice/${channelId}/play`, { track_url: track }); } -export async function stopTrack(guildId: number, channelId: number): Promise { - await postRequest(`guilds/${guildId}/voice/${channelId}/stop`, {}); +export async function stopTrack(guildId: number): Promise { + await postRequest(`guilds/${guildId}/voice/stop`, {}); } -export async function pauseTrack(guildId: number, channelId: number): Promise { - await postRequest(`guilds/${guildId}/voice/${channelId}/pause`, {}); +export async function pauseTrack(guildId: number): Promise { + await postRequest(`guilds/${guildId}/voice/pause`, {}); } -export async function resumeTrack(guildId: number, channelId: number): Promise { - await postRequest(`guilds/${guildId}/voice/${channelId}/resume`, {}); +export async function resumeTrack(guildId: number): Promise { + await postRequest(`guilds/${guildId}/voice/resume`, {}); } diff --git a/ui/src/app/management/page.tsx b/ui/src/app/management/page.tsx index 5eac9be..4d3a01c 100644 --- a/ui/src/app/management/page.tsx +++ b/ui/src/app/management/page.tsx @@ -82,7 +82,7 @@ export default function Page() { {channel.name}
{ playTrack(activeGuild!.id, channel.id, values.trackUrl); })} @@ -90,11 +90,22 @@ export default function Page() { - - - -
+
+ + + +
+
{}} /> +
))} From edbf47cf53a20901183c8db0dc3136d3afd65326 Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sun, 8 Oct 2023 09:20:53 -0400 Subject: [PATCH 10/28] Fixed volume --- service/Dockerfile | 3 +-- service/src/bot/api/routes.rs | 8 +------- ui/src/api/guilds.ts | 4 ++++ ui/src/app/management/page.tsx | 26 +++++++++++++++++++++----- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/service/Dockerfile b/service/Dockerfile index c0bbaba..7700c67 100644 --- a/service/Dockerfile +++ b/service/Dockerfile @@ -21,7 +21,7 @@ ARG TARGETPLATFORM RUN apt-get update && apt-get install -y curl tar xz-utils && \ if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - echo "amd64" && false; \ + echo "Unsupported platform: amd64" && false; \ elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \ curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux_armv7l > yt-dlp && \ chmod +x yt-dlp; \ @@ -34,7 +34,6 @@ RUN apt-get update && apt-get install -y curl tar xz-utils && \ curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linuxarm64-gpl.tar.xz > ffmpeg.tar.xz && \ tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz; \ elif [ "$TARGETPLATFORM" = "linux/x86_64" ]; then \ - echo "Unsupported platform: $TARGETPLATFORM" && \ curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux > yt-dlp && \ chmod +x yt-dlp && \ curl -L https://github.com/yt-dlp/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl.tar.xz > ffmpeg.tar.xz && \ diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index 1c06644..089e5aa 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -272,13 +272,6 @@ async fn set_volume(path: web::Path, volume: web::Json::, dat for (_, track_handle) in handler.queue().current_queue().iter().enumerate() { let _ = track_handle.set_volume(bound_volume); } - if let Err(err) = handler.queue().pause() { - warn!("Could not pause track: {:?}", err); - return ResponseError::error_response(&ServiceError { - status: 422, - message: err.to_string() - }) - } } HttpResponse::Ok().finish() @@ -295,5 +288,6 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(stop) .service(resume) .service(pause) + .service(set_volume) ); } \ No newline at end of file diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts index 64bbe8b..67969b1 100644 --- a/ui/src/api/guilds.ts +++ b/ui/src/api/guilds.ts @@ -31,3 +31,7 @@ export async function pauseTrack(guildId: number): Promise { export async function resumeTrack(guildId: number): Promise { await postRequest(`guilds/${guildId}/voice/resume`, {}); } + +export async function setVolume(guildId: number, volume: number): Promise { + await postRequest(`guilds/${guildId}/voice/volume`, { volume: `${volume}` }); +} diff --git a/ui/src/app/management/page.tsx b/ui/src/app/management/page.tsx index 4d3a01c..bf90fd9 100644 --- a/ui/src/app/management/page.tsx +++ b/ui/src/app/management/page.tsx @@ -7,6 +7,7 @@ import { pauseTrack, playTrack, resumeTrack, + setVolume, stopTrack } from '@/api/guilds'; import { GuildChannel, GuildInfo } from '@/api/guilds.types'; @@ -38,7 +39,8 @@ export default function Page() { const playForm = useForm({ initialValues: { - trackUrl: '' + trackUrl: '', + volume: 50.0 } }); @@ -101,12 +103,26 @@ export default function Page() { Resume
-
- {}} /> - -
+
))} From 6c3d29d705d1ac0590c93554fabc3eb7081a792a Mon Sep 17 00:00:00 2001 From: Benjamin Sherriff Date: Sun, 8 Oct 2023 17:11:34 -0400 Subject: [PATCH 11/28] Fixes --- service/src/bot/api/routes.rs | 60 +++++++++++++++++++++++++++++++++- service/src/dnd/mod.rs | 0 service/src/main.rs | 1 + ui/src/api/guilds.ts | 13 ++++++++ ui/src/api/spells.ts | 2 +- ui/src/app/management/page.tsx | 38 +++++++++++---------- 6 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 service/src/dnd/mod.rs diff --git a/service/src/bot/api/routes.rs b/service/src/bot/api/routes.rs index 089e5aa..1b87fb0 100644 --- a/service/src/bot/api/routes.rs +++ b/service/src/bot/api/routes.rs @@ -6,7 +6,7 @@ use serde::{Serialize, Deserialize}; use serenity::model::prelude::{GuildChannel, ChannelType}; use siren::ServiceError; -use crate::{AppState, bot::commands::audio::{play::play_track, join}, db::guilds::InsertGuild}; +use crate::{AppState, bot::commands::audio::{play::play_track, join}, db::guilds::{InsertGuild, QueryGuild}}; #[get("/guilds")] async fn get_guilds(data: web::Data>) -> HttpResponse { @@ -249,6 +249,34 @@ struct SetVolume { volume: String } +#[get("/{guild_id}/voice/volume")] +async fn get_volume(path: web::Path) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + let volume = match QueryGuild::get(guild_id as i64) { + Ok(guild) => guild.volume, + Err(err) => { + warn!("Could not get volume: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + HttpResponse::Ok().json(volume) +} + #[post("/{guild_id}/voice/volume")] async fn set_volume(path: web::Path, volume: web::Json::, data: web::Data>) -> HttpResponse { let guild_id = path.into_inner(); @@ -277,6 +305,34 @@ async fn set_volume(path: web::Path, volume: web::Json::, dat HttpResponse::Ok().finish() } +#[post("/{guild_id}/voice/skip")] +async fn skip(path: web::Path, data: web::Data>) -> HttpResponse { + let guild_id = path.into_inner(); + let guild_id = match guild_id.parse::() { + Ok(id) => id, + Err(err) => { + warn!("Could not parse guild id: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + }; + + if let Some(handler_lock) = data.songbird.get(guild_id) { + let handler = handler_lock.lock().await; + if let Err(err) = handler.queue().skip() { + warn!("Could not skip track: {:?}", err); + return ResponseError::error_response(&ServiceError { + status: 422, + message: err.to_string() + }) + } + } + + HttpResponse::Ok().finish() +} + pub fn init_routes(config: &mut web::ServiceConfig) { config .service(get_guilds) @@ -289,5 +345,7 @@ pub fn init_routes(config: &mut web::ServiceConfig) { .service(resume) .service(pause) .service(set_volume) + .service(get_volume) + .service(skip) ); } \ No newline at end of file diff --git a/service/src/dnd/mod.rs b/service/src/dnd/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/service/src/main.rs b/service/src/main.rs index e76172e..130b20f 100644 --- a/service/src/main.rs +++ b/service/src/main.rs @@ -18,6 +18,7 @@ use crate::bot::{commands::oai::GPTModel, handler::Handler}; use dotenv::dotenv; +mod dnd; mod bot; mod db; diff --git a/ui/src/api/guilds.ts b/ui/src/api/guilds.ts index 67969b1..3b21ea8 100644 --- a/ui/src/api/guilds.ts +++ b/ui/src/api/guilds.ts @@ -11,6 +11,10 @@ export async function getTextChannels(guildId: number): Promise return response?.data || { data: [] }; } +export async function sendMessage(guildId: number, channelId: number, message: string): Promise { + await postRequest(`guilds/${guildId}/text/${channelId}/message`, { message }); +} + export async function getVoiceChannels(guildId: number): Promise { const response = await getRequest(`guilds/${guildId}/voice`, {}); return response?.data || { data: [] }; @@ -35,3 +39,12 @@ export async function resumeTrack(guildId: number): Promise { export async function setVolume(guildId: number, volume: number): Promise { await postRequest(`guilds/${guildId}/voice/volume`, { volume: `${volume}` }); } + +export async function skipTrack(guildId: number): Promise { + await postRequest(`guilds/${guildId}/voice/skip`, {}); +} + +export async function getVolume(guildId: number): Promise { + const response = await getRequest(`guilds/${guildId}/voice/volume`, {}); + return response?.data?.volume || 0; +} diff --git a/ui/src/api/spells.ts b/ui/src/api/spells.ts index 1c2e0eb..de28c1a 100644 --- a/ui/src/api/spells.ts +++ b/ui/src/api/spells.ts @@ -19,7 +19,7 @@ interface GetSpellsParams { } export async function getSpells(params?: GetSpellsParams): Promise { - const response = await getRequest('spells', { + const response = await getRequest('dnd/spells', { name: params?.name, like_name: params?.like_name, schools: params?.schools?.join(','), diff --git a/ui/src/app/management/page.tsx b/ui/src/app/management/page.tsx index bf90fd9..7b90ebb 100644 --- a/ui/src/app/management/page.tsx +++ b/ui/src/app/management/page.tsx @@ -7,11 +7,13 @@ import { pauseTrack, playTrack, resumeTrack, + sendMessage, setVolume, + skipTrack, stopTrack } from '@/api/guilds'; import { GuildChannel, GuildInfo } from '@/api/guilds.types'; -import { Button, Slider, Tabs, TextInput } from '@mantine/core'; +import { Button, Slider, Tabs, TextInput, Textarea } from '@mantine/core'; import { useForm } from '@mantine/form'; import React, { useEffect, useState } from 'react'; @@ -39,6 +41,7 @@ export default function Page() { const playForm = useForm({ initialValues: { + message: '', trackUrl: '', volume: 50.0 } @@ -48,26 +51,32 @@ export default function Page() { {guilds.map((guild) => ( - setActiveGuild(guild)}> + setActiveGuild(guild)}> {guild.name} ))} {guilds.map((guild) => ( - +

{guild.name}

Text Channels

{textChannels.map((channel) => ( - + {channel.name} ))} {textChannels.map((channel) => ( - - {channel.name} + +
sendMessage(activeGuild!.id, channel.id, values.message))} + > +