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>> +}