From d19be1d7f3387636d59d0dd49c63d88a9886cbfe Mon Sep 17 00:00:00 2001 From: Ladislav Hano Date: Fri, 24 Jan 2025 21:52:13 +0100 Subject: [PATCH] chore: restructurize the whole source --- .gitignore | 4 +- Cargo.toml | 9 +- src/{commands/mod.rs => commands.rs} | 6 +- src/commands/{moover/mod.rs => moover.rs} | 4 +- src/commands/moover/gif.rs | 20 +++ src/commands/{notice/mod.rs => notice.rs} | 2 +- src/commands/other/mod.rs | 0 .../mod.rs => user_interactions.rs} | 2 +- src/commands/user_interactions/interaction.rs | 2 +- src/commands/voice.rs | 11 ++ src/commands/voice/general/mod.rs | 3 - .../voice/{general => }/general_player.rs | 16 ++- src/commands/voice/mod.rs | 6 - src/commands/voice/player_common.rs | 8 +- src/commands/voice/{radio/mod.rs => radio.rs} | 3 + src/commands/voice/radio/radio_player.rs | 115 ++++++++++++++++-- src/commands/voice/radio/radio_utils.rs | 83 +++++++++++++ .../voice/{spotify/mod.rs => spotify.rs} | 0 .../voice/{util.rs => voice_utils.rs} | 9 +- src/commands/voice/{yt/mod.rs => yt.rs} | 2 +- src/main.rs | 13 +- src/message_handler.rs | 4 +- src/other/notice.rs | 4 +- src/types.rs | 9 +- src/{util/mod.rs => utils.rs} | 0 src/{util => utils}/debug.rs | 0 src/{util => utils}/embeds.rs | 0 src/{util => utils}/gifs.rs | 0 .../poise_context_extension.rs | 0 src/{util => utils}/utilities.rs | 0 30 files changed, 267 insertions(+), 68 deletions(-) rename src/{commands/mod.rs => commands.rs} (64%) rename src/commands/{moover/mod.rs => moover.rs} (64%) create mode 100644 src/commands/moover/gif.rs rename src/commands/{notice/mod.rs => notice.rs} (78%) delete mode 100644 src/commands/other/mod.rs rename src/commands/{user_interactions/mod.rs => user_interactions.rs} (85%) create mode 100644 src/commands/voice.rs delete mode 100644 src/commands/voice/general/mod.rs rename src/commands/voice/{general => }/general_player.rs (79%) delete mode 100644 src/commands/voice/mod.rs rename src/commands/voice/{radio/mod.rs => radio.rs} (55%) create mode 100644 src/commands/voice/radio/radio_utils.rs rename src/commands/voice/{spotify/mod.rs => spotify.rs} (100%) rename src/commands/voice/{util.rs => voice_utils.rs} (86%) rename src/commands/voice/{yt/mod.rs => yt.rs} (54%) rename src/{util/mod.rs => utils.rs} (100%) rename src/{util => utils}/debug.rs (100%) rename src/{util => utils}/embeds.rs (100%) rename src/{util => utils}/gifs.rs (100%) rename src/{util => utils}/poise_context_extension.rs (100%) rename src/{util => utils}/utilities.rs (100%) diff --git a/.gitignore b/.gitignore index 2c549b2..f50483c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ # IDE config .vscode -mooverdb.db* \ No newline at end of file +mooverdb.db* + +*.txt \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index c9e3f30..79850ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,21 +7,22 @@ edition = "2021" [dependencies] rand = "0.8.5" -anyhow = "1.0.89" +anyhow = "1.0.95" tokio-cron-scheduler = "0.13.0" dotenv = "0.15.0" poise = "0.6.1" serenity = { version = "0.12.4", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "http", "cache"] } # serenity_utils = "0.7.0" -tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] } regex = "1.11.1" -chrono = "0.4.38" -sqlx = {version="0.8.2", features=["runtime-tokio", "sqlite"]} +chrono = "0.4.39" +sqlx = {version="0.8.3", features=["runtime-tokio", "sqlite"]} form_urlencoded = "1.2.1" tenorv2 = { path = "./tenor-v2/tenorv2" } # librespot = { version = "0.6.0", default-features = false, features = ["rodio-backend"] } songbird = { version = "0.4.6", features = ["driver", "builtin-queue"] } reqwest = "0.11.27" # songbird depends on ^0.11 +radiobrowser = "0.6.1" [dependencies.symphonia] version = "0.5.2" diff --git a/src/commands/mod.rs b/src/commands.rs similarity index 64% rename from src/commands/mod.rs rename to src/commands.rs index df0d152..6a1c0f7 100644 --- a/src/commands/mod.rs +++ b/src/commands.rs @@ -1,11 +1,13 @@ pub use moover::*; pub use notice::*; pub use user_interactions::*; -pub use other::*; +// pub use other::*; pub use voice::*; +// pub use command_utils::*; pub mod moover; pub mod notice; pub mod user_interactions; -pub mod other; +// pub mod other; pub mod voice; +// mod command_utils; diff --git a/src/commands/moover/mod.rs b/src/commands/moover.rs similarity index 64% rename from src/commands/moover/mod.rs rename to src/commands/moover.rs index 51a3f5b..f2c99f7 100644 --- a/src/commands/moover/mod.rs +++ b/src/commands/moover.rs @@ -1,7 +1,7 @@ pub use moove::*; +pub use gif::*; pub use say::*; -// pub use gif::*; pub mod moove; +pub mod gif; pub mod say; -// pub mod gif; diff --git a/src/commands/moover/gif.rs b/src/commands/moover/gif.rs new file mode 100644 index 0000000..4861a37 --- /dev/null +++ b/src/commands/moover/gif.rs @@ -0,0 +1,20 @@ +use poise; +use serenity::all::{Embed, User}; + +use crate::types::{Error, Context}; + +#[poise::command( + slash_command, + description_localized("en-US", "Send a gif from Tenor") +)] +pub async fn gif(ctx: Context<'_>, + #[description = "What should I search?"] + what: String, + #[description = "Should the gif be R-rated?"] + contentfilter: String +) -> Result<(), Error> { + // let embed; + // send_with_embed(ctx, "hug", &title, &desc).await?; + ctx.reply("Done!").await?; + Ok(()) +} \ No newline at end of file diff --git a/src/commands/notice/mod.rs b/src/commands/notice.rs similarity index 78% rename from src/commands/notice/mod.rs rename to src/commands/notice.rs index d63767d..33bed17 100644 --- a/src/commands/notice/mod.rs +++ b/src/commands/notice.rs @@ -2,4 +2,4 @@ pub use birthday::*; pub use events::*; pub mod birthday; -pub mod events; \ No newline at end of file +pub mod events; diff --git a/src/commands/other/mod.rs b/src/commands/other/mod.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/commands/user_interactions/mod.rs b/src/commands/user_interactions.rs similarity index 85% rename from src/commands/user_interactions/mod.rs rename to src/commands/user_interactions.rs index 0e8654b..f9a7882 100644 --- a/src/commands/user_interactions/mod.rs +++ b/src/commands/user_interactions.rs @@ -3,4 +3,4 @@ pub use hug::*; pub mod interaction; pub mod headpat; -pub mod hug; \ No newline at end of file +pub mod hug; diff --git a/src/commands/user_interactions/interaction.rs b/src/commands/user_interactions/interaction.rs index 6db121a..f495f3e 100644 --- a/src/commands/user_interactions/interaction.rs +++ b/src/commands/user_interactions/interaction.rs @@ -2,7 +2,7 @@ use anyhow::anyhow; use serenity::all::{Colour, CreateEmbed, CreateMessage}; use tenorv2::tenor_builder::Tenor; -use crate::{types::Context, util::{gifs::get_random_tenor_gif, utilities}}; +use crate::{types::Context, utils::{gifs::get_random_tenor_gif, utilities}}; /// Sends embed with random tenor gif from searched query /// title and desc are used in the embed diff --git a/src/commands/voice.rs b/src/commands/voice.rs new file mode 100644 index 0000000..98fd8f5 --- /dev/null +++ b/src/commands/voice.rs @@ -0,0 +1,11 @@ +use voice_utils::*; + +mod voice_utils; +pub mod player_common; +pub mod radio; +pub mod general_player; + +// ! not working +// pub mod yt; +// TODO implement +// pub mod spotify; diff --git a/src/commands/voice/general/mod.rs b/src/commands/voice/general/mod.rs deleted file mode 100644 index 8b6dfd4..0000000 --- a/src/commands/voice/general/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use general_player::*; - -pub mod general_player; \ No newline at end of file diff --git a/src/commands/voice/general/general_player.rs b/src/commands/voice/general_player.rs similarity index 79% rename from src/commands/voice/general/general_player.rs rename to src/commands/voice/general_player.rs index c3cd0d6..860d22f 100644 --- a/src/commands/voice/general/general_player.rs +++ b/src/commands/voice/general_player.rs @@ -2,10 +2,11 @@ use std::vec; use songbird::input::YoutubeDl; -use crate::commands::util::connect; -use crate::util::poise_context_extension::ContextExt; +use crate::utils::poise_context_extension::ContextExt; use crate::types::{Context, Error}; -use crate::commands::voice::util::autocomplete_channel; +use crate::commands::voice::voice_utils::autocomplete_channel; + +use super::connect; // TODO: search, queue #[poise::command( @@ -38,12 +39,9 @@ pub async fn play(ctx: Context<'_>, let http_client = &ctx.data().http_client; if manager.get(guild_id).is_none() { - match connect(&ctx, guild_id, channel).await { - Ok(_) => (), - Err(e) => { - ctx.reply_ephemeral(&e.to_string()).await?; - return Ok(()) - } + if let Err(e) = connect(&ctx, guild_id, channel).await { + ctx.reply_ephemeral(&e.to_string()).await?; + return Ok(()) } } diff --git a/src/commands/voice/mod.rs b/src/commands/voice/mod.rs deleted file mode 100644 index f3d7e73..0000000 --- a/src/commands/voice/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod util; -pub mod player_common; -pub mod radio; -// pub mod spotify; -pub mod general; -pub mod yt; \ No newline at end of file diff --git a/src/commands/voice/player_common.rs b/src/commands/voice/player_common.rs index 38ac50a..f87031c 100644 --- a/src/commands/voice/player_common.rs +++ b/src/commands/voice/player_common.rs @@ -2,12 +2,12 @@ use std::vec; use songbird::input::{File, Input}; -use crate::util::debug::send_error; -use crate::util::poise_context_extension::ContextExt; -use crate::util::utilities::get_local_songs; +use crate::utils::debug::send_error; +use crate::utils::poise_context_extension::ContextExt; +use crate::utils::utilities::get_local_songs; use crate::types::{Context, Error}; -use super::util::{connect, autocomplete_channel}; +use super::voice_utils::{connect, autocomplete_channel}; /** * Common commands that are the same for every implementation diff --git a/src/commands/voice/radio/mod.rs b/src/commands/voice/radio.rs similarity index 55% rename from src/commands/voice/radio/mod.rs rename to src/commands/voice/radio.rs index 27ead6c..0ea2063 100644 --- a/src/commands/voice/radio/mod.rs +++ b/src/commands/voice/radio.rs @@ -1,3 +1,6 @@ pub use radio_player::*; +use radio_utils::*; pub mod radio_player; + +mod radio_utils; diff --git a/src/commands/voice/radio/radio_player.rs b/src/commands/voice/radio/radio_player.rs index b9ed39d..599c610 100644 --- a/src/commands/voice/radio/radio_player.rs +++ b/src/commands/voice/radio/radio_player.rs @@ -1,26 +1,41 @@ use std::vec; +use radiobrowser::RadioBrowserAPI; use reqwest::Client; use songbird::input::Input; use songbird::input::HttpRequest; -use crate::commands::util::connect; -use crate::util::poise_context_extension::ContextExt; +use super::super::connect; +use crate::utils::poise_context_extension::ContextExt; use crate::types::{Context, Error}; -use crate::commands::voice::util::autocomplete_channel; +use crate::commands::voice::voice_utils::autocomplete_channel; + +use super::radio_utils::paginate_search; + +use super::super::voice_utils::MAX_ENTRIES; + +#[poise::command( + slash_command, + description_localized("en-US", "Plays music from URL source"), + subcommands("search", "play") +)] +pub async fn radio(_ctx: Context<'_>) -> Result<(), Error> { + Ok(()) +} // TODO: autocomplete radio stream URLs #[poise::command( slash_command, description_localized("en-US", "Plays music from URL source") )] -pub async fn radio(ctx: Context<'_>, +pub async fn play(ctx: Context<'_>, #[autocomplete = "autocomplete_channel"] #[description = "Voice channel name: "] channel: Option, - #[description = "Source URL: "] - url: String, + #[autocomplete = "autocomplete_radio"] + #[description = "Radio station: "] + name: String, ) -> Result<(), Error> { if ctx.guild().is_none() { @@ -39,12 +54,9 @@ pub async fn radio(ctx: Context<'_>, }; if manager.get(guild_id).is_none() { - match connect(&ctx, guild_id, channel).await { - Ok(_) => (), - Err(e) => { - ctx.reply_ephemeral(&e.to_string()).await?; - return Ok(()) - } + if let Err(e) = connect(&ctx, guild_id, channel).await { + ctx.reply_ephemeral(&e.to_string()).await?; + return Ok(()) } } @@ -52,7 +64,7 @@ pub async fn radio(ctx: Context<'_>, let mut handler = handler_lock.lock().await; let client = Client::new(); - let request = HttpRequest::new(client, url); + let request = HttpRequest::new(client, name); let input = Input::from(request); handler.play_only_input(input); } @@ -64,3 +76,80 @@ pub async fn radio(ctx: Context<'_>, ctx.reply_ephemeral("Done!").await?; Ok(()) } + +async fn autocomplete_radio( + ctx: Context<'_>, + partial: &str +) -> Vec { + + let api = { + match &ctx.data().radio_browser { + Some(v) => v.clone(), + None => { + let Ok(radio_browser) = RadioBrowserAPI::new().await else { + return vec!["There was an error with radio API!".to_owned()] + }; + radio_browser + } + } + }; + + let stations: Vec = match api.get_stations() + .limit(MAX_ENTRIES) + .name(partial) + .hidebroken(true) + .send().await { + Ok(stations) => { + stations.iter().map(|station| + format!("Name: {} Language: {} Bitrate: {}", station.name, station.countrycode, station.bitrate) + ).collect() + }, + Err(_) => return vec!["".to_owned()] + }; + + dbg!(&stations); + + return stations + // return vec![]; +} + +// TODO +// autocomplete +// search with buttons and list in embed with direct links to stream +// embed showing currently playing song + +// TODO: autocomplete radio stream URLs +#[poise::command( + slash_command, + description_localized("en-US", "Search for a radio") +)] +pub async fn search(ctx: Context<'_>, + #[description = "Radio station: "] + name: String, + #[description = "Country: "] + country: Option, + #[description = "Tag: "] + tag: Option +) -> Result<(), Error> { + + let api = match &ctx.data().radio_browser { + Some(v) => v.clone(), + None => { + let Ok(radio_browser) = RadioBrowserAPI::new().await else { + let _ = ctx.reply_ephemeral("There was an error with radio API!").await; + return Ok(()) + }; + radio_browser + } + }; + + let station_search_builder = api.get_stations() + .name(name) + .tag(tag.unwrap_or("".to_owned())) + .country(country.unwrap_or("".to_owned())) + .hidebroken(true); + + paginate_search(ctx, station_search_builder); + + Ok(()) +} diff --git a/src/commands/voice/radio/radio_utils.rs b/src/commands/voice/radio/radio_utils.rs new file mode 100644 index 0000000..de1832c --- /dev/null +++ b/src/commands/voice/radio/radio_utils.rs @@ -0,0 +1,83 @@ +use poise::{ChoiceParameter, CreateReply}; +use radiobrowser::StationSearchBuilder; +use serenity::{all::{CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse, CreateInteractionResponseMessage}, futures::SinkExt}; + +use crate::types::Context; + +#[derive(ChoiceParameter)] +pub enum WelcomeChoice { + #[name = "5"] + A, + #[name = "10"] + B, + #[name = "15"] + C +} + +pub async fn paginate_search( + ctx: Context<'_>, + search_builder: StationSearchBuilder, +) -> Result<(), serenity::Error> { + // Define some unique identifiers for the navigation buttons + let ctx_id = ctx.id(); + let prev_button_id = format!("{}prev", ctx_id); + let next_button_id = format!("{}next", ctx_id); + + let Ok(stations) = search_builder.send().await else { + let _ = ctx.reply("Something went wrong, try searching again").await; + return Ok(()) + }; + + // Send the embed with the first page as content + let reply = { + let components = CreateActionRow::Buttons(vec![ + CreateButton::new(&prev_button_id).emoji('◀'), + CreateButton::new(&next_button_id).emoji('▶'), + ]); + + CreateReply::default() + .embed(CreateEmbed::new()) + .components(vec![components]) + }; + + ctx.send(reply).await?; + + // Loop through incoming interactions with the navigation buttons + let mut current_page = 0; + while let Some(press) = serenity::collector::ComponentInteractionCollector::new(ctx) + // We defined our button IDs to start with `ctx_id`. If they don't, some other command's + // button was pressed + .filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string())) + // Timeout when no navigation button has been pressed for 24 hours + .timeout(std::time::Duration::from_secs(3600 * 24)) + .await + { + // Depending on which button was pressed, go to next or previous page + if press.data.custom_id == next_button_id { + current_page += 1; + // TODO find a way to check end + // if current_page >= pages.len() { + // current_page = 0; + // } + } else if press.data.custom_id == prev_button_id { + // TODO find a way to get pages len + // current_page = current_page.checked_sub(1).unwrap_or(pages.len() - 1); + } else { + // This is an unrelated button interaction + continue; + } + + // Update the message with the new page contents + press + .create_response( + ctx.serenity_context(), + CreateInteractionResponse::UpdateMessage( + CreateInteractionResponseMessage::new() + .embed(CreateEmbed::new()), + ), + ) + .await?; + } + + Ok(()) +} diff --git a/src/commands/voice/spotify/mod.rs b/src/commands/voice/spotify.rs similarity index 100% rename from src/commands/voice/spotify/mod.rs rename to src/commands/voice/spotify.rs diff --git a/src/commands/voice/util.rs b/src/commands/voice/voice_utils.rs similarity index 86% rename from src/commands/voice/util.rs rename to src/commands/voice/voice_utils.rs index 4208b51..d9e4a8c 100644 --- a/src/commands/voice/util.rs +++ b/src/commands/voice/voice_utils.rs @@ -1,10 +1,13 @@ -use serenity::all::{ChannelId, GuildId}; +use poise::{CreateReply}; +use serenity::all::{ChannelId, CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse, CreateInteractionResponseMessage, Embed, GuildId}; use serenity::async_trait; use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler}; use songbird::TrackEvent; -use crate::{types::{Context, Error}, util::utilities::get_channel_by_name}; +use crate::{types::{Context, Error}, utils::utilities::get_channel_by_name}; + +pub const MAX_ENTRIES: &str = "15"; /// Returns either voice channel to which the user is currently connected to or the one passed via name async fn get_voice_channel(ctx: &Context<'_>, name: Option) -> Result { @@ -68,7 +71,7 @@ pub async fn autocomplete_channel( _partial: &str, ) -> Vec { - use crate::util::utilities::get_vc_names; + use crate::utils::utilities::get_vc_names; match ctx.guild() { Some(guild) => get_vc_names(guild), None => [].to_vec() diff --git a/src/commands/voice/yt/mod.rs b/src/commands/voice/yt.rs similarity index 54% rename from src/commands/voice/yt/mod.rs rename to src/commands/voice/yt.rs index 989288e..af9e888 100644 --- a/src/commands/voice/yt/mod.rs +++ b/src/commands/voice/yt.rs @@ -1,3 +1,3 @@ pub use yt_player::*; -pub mod yt_player; \ No newline at end of file +pub mod yt_player; diff --git a/src/main.rs b/src/main.rs index 66380e6..a36e45a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ mod message_handler; use message_handler::handle; mod commands; -mod util; +mod utils; mod other; use other::notice; @@ -57,7 +57,7 @@ impl EventHandler for Handler { println!("{} v3.3.1 is connected!", ready.user.name); #[cfg(feature="RELEASE")] { - use util::debug::hello; + use utils::debug::hello; let _ = hello(ctx.http.clone()).await; } @@ -87,10 +87,9 @@ async fn main() -> anyhow::Result<()> { commands::say(), commands::hug(), // commands::player::play_local(), - commands::player_common::disconnect(), commands::radio::radio(), - commands::general::play(), - commands::yt::play_yt() + commands::general_player::play(), + commands::player_common::disconnect(), ], prefix_options: poise::PrefixFrameworkOptions { prefix: Some("/".into()), @@ -129,8 +128,10 @@ async fn main() -> anyhow::Result<()> { register_globally(ctx, &framework.options().commands).await?; } + Ok(Data { - http_client: HttpClient::new() + http_client: HttpClient::new(), + radio_browser: RadioBrowserAPI::new().await.ok() }) }) }) diff --git a/src/message_handler.rs b/src/message_handler.rs index 4aa263e..8033036 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -5,8 +5,8 @@ use serenity::model::channel::Message; use std::collections::HashMap; use std::sync::Arc; -use crate::util::debug::send_error; -use crate::util::utilities; +use crate::utils::debug::send_error; +use crate::utils::utilities; use crate::commands::moover::moove::{moove, moove_check}; diff --git a/src/other/notice.rs b/src/other/notice.rs index 69b39ed..9d75231 100644 --- a/src/other/notice.rs +++ b/src/other/notice.rs @@ -6,8 +6,8 @@ use serenity::{all::{GuildId, UserId}, builder::{CreateEmbed, CreateMessage}, cl use sqlx::{Connection, FromRow, SqliteConnection}; -use crate::util::debug::send_error; -use crate::util::utilities; +use crate::utils::debug::send_error; +use crate::utils::utilities; use std::{env, sync::Arc}; diff --git a/src/types.rs b/src/types.rs index 1103f1c..068d8e4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -2,20 +2,15 @@ use std::{ffi::OsString, path::PathBuf}; -use serenity::prelude::TypeMapKey; - +use radiobrowser::RadioBrowserAPI; use reqwest::Client as HttpClient; // TODO: this should be probably expanded also to be used by multiple servers and commands // radio and spotify commands always create new HttpClient -pub struct HttpKey; - -impl TypeMapKey for HttpKey { - type Value = HttpClient; -} pub struct Data { pub http_client: HttpClient, + pub radio_browser: Option, } pub type Error = Box; diff --git a/src/util/mod.rs b/src/utils.rs similarity index 100% rename from src/util/mod.rs rename to src/utils.rs diff --git a/src/util/debug.rs b/src/utils/debug.rs similarity index 100% rename from src/util/debug.rs rename to src/utils/debug.rs diff --git a/src/util/embeds.rs b/src/utils/embeds.rs similarity index 100% rename from src/util/embeds.rs rename to src/utils/embeds.rs diff --git a/src/util/gifs.rs b/src/utils/gifs.rs similarity index 100% rename from src/util/gifs.rs rename to src/utils/gifs.rs diff --git a/src/util/poise_context_extension.rs b/src/utils/poise_context_extension.rs similarity index 100% rename from src/util/poise_context_extension.rs rename to src/utils/poise_context_extension.rs diff --git a/src/util/utilities.rs b/src/utils/utilities.rs similarity index 100% rename from src/util/utilities.rs rename to src/utils/utilities.rs