v0.2.6 Working on audio commands from api

This commit is contained in:
Benjamin Sherriff
2023-10-06 16:09:28 -04:00
parent fa7c0f8163
commit 2f61aed204
8 changed files with 222 additions and 119 deletions

View File

@@ -1 +1 @@
SIREN_VERSION=0.2.5 SIREN_VERSION=0.2.6

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "service" name = "service"
version = "0.2.5" version = "0.2.6"
edition = "2021" edition = "2021"
authors = ["Ben Sherriff <hello@bensherriff.com>"] authors = ["Ben Sherriff <hello@bensherriff.com>"]
repository = "https://github.com/bensherriff/siren" repository = "https://github.com/bensherriff/siren"

View File

@@ -14,23 +14,32 @@ RUN cargo build --release
# ========== # ==========
# Packages # Packages
# ========== # ==========
FROM debian:bookworm-slim as packages FROM debian:bullseye-slim as packages
# FROM debian:bookworm-slim as packages
WORKDIR /packages WORKDIR /packages
RUN apt-get update && apt-get install -y curl tar xz-utils && \ 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 && \ 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 tar -xJf ffmpeg.tar.xz --wildcards */bin/ffmpeg --transform='s/^.*\///' && rm ffmpeg.tar.xz
# ========= # =========
# Runtime # 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 WORKDIR /service
USER root USER root
COPY --from=builder /builder/target/release/service /usr/local/bin/service 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/*
CMD ["service"] CMD ["service"]

View File

@@ -15,7 +15,6 @@ build: ## Build the docker image
utils: ## Start the utils utils: ## Start the utils
docker compose up -d db docker compose up -d db
docker compose up -d rbmq
up: ## Start the app up: ## Start the app
docker compose up -d docker compose up -d

View File

@@ -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 actix_web::{get, post, put, delete, web, HttpResponse, HttpRequest, ResponseError};
use log::warn; use log::warn;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use serenity::{http::Http, model::prelude::{GuildChannel, ChannelType}}; use serenity::model::prelude::{GuildChannel, ChannelType};
use siren::ServiceError; use siren::ServiceError;
use crate::AppState;
#[get("/guilds")] #[get("/guilds")]
async fn get_guilds(data: web::Data<Arc<Http>>) -> HttpResponse { async fn get_guilds(data: web::Data<Arc<AppState>>) -> HttpResponse {
let guild_results = &data.get_guilds(None, None).await; let guild_results = &data.http.get_guilds(None, None).await;
let guilds = match guild_results { let guilds = match guild_results {
Ok(guilds) => guilds, Ok(guilds) => guilds,
Err(err) => return ResponseError::error_response(&ServiceError { Err(err) => return ResponseError::error_response(&ServiceError {
@@ -20,8 +22,8 @@ async fn get_guilds(data: web::Data<Arc<Http>>) -> HttpResponse {
} }
#[get("/{id}/text")] #[get("/{id}/text")]
async fn get_text_channels(id: web::Path<String>, data: web::Data<Arc<Http>>) -> HttpResponse { async fn get_text_channels(id: web::Path<String>, data: web::Data<Arc<AppState>>) -> HttpResponse {
let channel_results = &data.get_channels(id.parse::<u64>().unwrap()).await; let channel_results = &data.http.get_channels(id.parse::<u64>().unwrap()).await;
let channels = match channel_results { let channels = match channel_results {
Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Text).collect::<Vec<&GuildChannel>>(), Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Text).collect::<Vec<&GuildChannel>>(),
Err(err) => return ResponseError::error_response(&ServiceError { Err(err) => return ResponseError::error_response(&ServiceError {
@@ -33,8 +35,8 @@ async fn get_text_channels(id: web::Path<String>, data: web::Data<Arc<Http>>) ->
} }
#[get("/{id}/voice")] #[get("/{id}/voice")]
async fn get_voice_channels(id: web::Path<String>, data: web::Data<Arc<Http>>) -> HttpResponse { async fn get_voice_channels(id: web::Path<String>, data: web::Data<Arc<AppState>>) -> HttpResponse {
let channel_results = &data.get_channels(id.parse::<u64>().unwrap()).await; let channel_results = &data.http.get_channels(id.parse::<u64>().unwrap()).await;
let channels = match channel_results { let channels = match channel_results {
Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Voice).collect::<Vec<&GuildChannel>>(), Ok(channels) => channels.iter().filter(|c| c.kind == ChannelType::Voice).collect::<Vec<&GuildChannel>>(),
Err(err) => return ResponseError::error_response(&ServiceError { Err(err) => return ResponseError::error_response(&ServiceError {
@@ -51,7 +53,7 @@ struct ChannelMessage {
} }
#[post("/{guild_id}/text/{channel_id}/message")] #[post("/{guild_id}/text/{channel_id}/message")]
async fn send_message(path: web::Path<(String, String)>, text: web::Json<ChannelMessage>, data: web::Data<Arc<Http>>) -> HttpResponse { async fn send_message(path: web::Path<(String, String)>, text: web::Json<ChannelMessage>, data: web::Data<Arc<AppState>>) -> HttpResponse {
let (guild_id, channel_id) = path.into_inner(); let (guild_id, channel_id) = path.into_inner();
let guild_id = match guild_id.parse::<u64>() { let guild_id = match guild_id.parse::<u64>() {
Ok(id) => id, Ok(id) => id,
@@ -73,7 +75,7 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json<Channel
}) })
} }
}; };
let channel_results = &data.get_channels(guild_id).await; let channel_results = &data.http.get_channels(guild_id).await;
let channels = match channel_results { let channels = match channel_results {
Ok(channels) => channels, Ok(channels) => channels,
Err(err) => { Err(err) => {
@@ -96,7 +98,7 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json<Channel
} }
}; };
if let Err(err) = channel.say(&data.get_ref(), &text.message).await { if let Err(err) = channel.say(&Pin::new(&data.http).get_ref(), &text.message).await {
warn!("Could not send message: {:?}", err); warn!("Could not send message: {:?}", err);
return ResponseError::error_response(&ServiceError { return ResponseError::error_response(&ServiceError {
status: 422, status: 422,
@@ -107,6 +109,62 @@ async fn send_message(path: web::Path<(String, String)>, text: web::Json<Channel
HttpResponse::Ok().finish() HttpResponse::Ok().finish()
} }
#[post("/{guild_id}/voice/play")]
async fn play(path: web::Path<String>, data: web::Data<Arc<AppState>>) -> HttpResponse {
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
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<String>, data: web::Data<Arc<AppState>>) -> HttpResponse {
let guild_id = path.into_inner();
let guild_id = match guild_id.parse::<u64>() {
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) { pub fn init_routes(config: &mut web::ServiceConfig) {
config config
.service(get_guilds) .service(get_guilds)
@@ -114,5 +172,6 @@ pub fn init_routes(config: &mut web::ServiceConfig) {
.service(get_text_channels) .service(get_text_channels)
.service(get_voice_channels) .service(get_voice_channels)
.service(send_message) .service(send_message)
.service(pause)
); );
} }

View File

@@ -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<commands::oai::OAI>
}
#[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::<AudioConfigs>().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)
};
}
}
}

View File

@@ -1,2 +1,3 @@
pub mod api; pub mod api;
pub mod commands; pub mod commands;
pub mod handler;

View File

@@ -5,117 +5,23 @@ extern crate diesel_migrations;
use std::env; use std::env;
use std::collections::{HashSet, HashMap}; use std::collections::{HashSet, HashMap};
use std::sync::Arc; use std::sync::Arc;
use bot::commands::audio::AudioConfig;
use bot::commands::audio::{create_response, AudioConfig, AudioConfigs};
use log::{error, warn, info}; use log::{error, warn, info};
use serenity::async_trait;
use serenity::framework::StandardFramework; 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::http::Http;
use serenity::model::prelude::GuildId;
use serenity::prelude::*; use serenity::prelude::*;
use songbird::SerenityInit; use songbird::{SerenityInit, Songbird};
use crate::bot::commands::oai::GPTModel;
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::{HttpServer, App, web}; use actix_web::{HttpServer, App, web};
use crate::bot::{commands::{oai::GPTModel, audio::AudioConfigs}, handler::Handler};
use dotenv::dotenv; use dotenv::dotenv;
mod bot; mod bot;
mod db; mod db;
struct Handler {
// Open AI Config
oai: Option<bot::commands::oai::OAI>
}
#[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::<AudioConfigs>().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] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
dotenv().ok(); 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) let mut client = Client::builder(token, intents)
.event_handler(handler) .event_handler(handler)
.framework(StandardFramework::new() .framework(StandardFramework::new()
.configure(|c| c.owners(owners))) .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() .register_songbird()
.await .await
.expect("Error creating client"); .expect("Error creating client");
let audio_configs: Arc<RwLock<HashMap<GuildId, AudioConfig>>> = Arc::new(RwLock::new(HashMap::default()));
{ {
let mut data = client.data.write().await; let mut data = client.data.write().await;
data.insert::<AudioConfigs>(Arc::new(RwLock::new(HashMap::default()))); data.insert::<AudioConfigs>(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 { tokio::spawn(async move {
if let Err(why) = client.start_autosharded().await { if let Err(why) = client.start_autosharded().await {
@@ -199,7 +129,8 @@ async fn main() -> std::io::Result<()> {
.allow_any_header() .allow_any_header()
.max_age(3600); .max_age(3600);
App::new() 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::messages::init_routes)
.configure(crate::db::spells::init_routes) .configure(crate::db::spells::init_routes)
.configure(crate::bot::api::init_routes) .configure(crate::bot::api::init_routes)
@@ -219,3 +150,9 @@ async fn main() -> std::io::Result<()> {
server.run() server.run()
.await .await
} }
pub struct AppState {
pub http: Arc<Http>,
pub songbird: Arc<Songbird>,
pub audio_configs: Arc<RwLock<HashMap<GuildId, AudioConfig>>>
}