diff --git a/Cargo.lock b/Cargo.lock index f102c7b..889762b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.18" +version = "1.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" +checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" dependencies = [ "jobserver", "libc", diff --git a/flake.lock b/flake.lock index fddcf98..95839db 100644 --- a/flake.lock +++ b/flake.lock @@ -77,11 +77,11 @@ "pre-commit-hooks": "pre-commit-hooks_2" }, "locked": { - "lastModified": 1726232533, - "narHash": "sha256-rhho/HLlDkJ/d3k6oQivgCSdVz4C1LLklPtO/aBhC2I=", + "lastModified": 1726417371, + "narHash": "sha256-tBq8w81ZV48tyFhLz5WQjqfoEShIXkOb6Rlzidcz8yQ=", "owner": "cachix", "repo": "devenv", - "rev": "199a23e3bcfbfacaec3836d1c884918e13239b50", + "rev": "1f55f89ca32d617b7a7c18422e3c364cb003df3d", "type": "github" }, "original": { @@ -167,11 +167,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1726357397, - "narHash": "sha256-W68/drb51fBhOl/BMOoRlI+7qxeoNWGmCZwAyuXVlQY=", + "lastModified": 1726381916, + "narHash": "sha256-/ybEGP0EyalM6UNe8hnx/SqcGtey+UMtKKOy1sCpX7I=", "owner": "nix-community", "repo": "fenix", - "rev": "8f14b37d4ad9eafd33315ba67faded5c1e1a1044", + "rev": "3ade5be29c0ed4f5aeb93d9293a2b5bad62f1d1c", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index de8066b..c55b0a9 100644 --- a/flake.nix +++ b/flake.nix @@ -52,7 +52,7 @@ sqlx-cli pgcli cargo-edit - openssl + openssl ]; # https://devenv.sh/scripts/ diff --git a/lyra/src/command/model.rs b/lyra/src/command/model.rs index 86195af..fe4e64b 100644 --- a/lyra/src/command/model.rs +++ b/lyra/src/command/model.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use twilight_model::{ application::interaction::{ application_command::{CommandData, CommandDataOption}, + message_component::MessageComponentInteractionData, Interaction, InteractionDataResolved, }, channel::Channel, @@ -19,9 +20,9 @@ use twilight_model::{ use crate::error::{command::AutocompleteResult, CommandResult}; pub use self::ctx::{ - Autocomplete as AutocompleteCtx, CommandDataAware, Ctx, Guild as GuildCtx, - GuildModal as GuildModalCtx, GuildRef as GuildCtxRef, Kind as CtxKind, Message as MessageCtx, - RespondViaMessage, RespondViaModal, Slash as SlashCtx, User, + Autocomplete as AutocompleteCtx, CommandDataAware, Component as ComponentCtx, Ctx, + Guild as GuildCtx, GuildModal as GuildModalCtx, GuildRef as GuildCtxRef, Kind as CtxKind, + Message as MessageCtx, RespondViaMessage, RespondViaModal, Slash as SlashCtx, User, }; pub trait NonPingInteraction { @@ -90,7 +91,7 @@ impl PartialCommandData { #[non_exhaustive] pub enum PartialInteractionData { Command(PartialCommandData), - _Other, + Component(Box), } pub trait CommandInfoAware { diff --git a/lyra/src/command/model/ctx.rs b/lyra/src/command/model/ctx.rs index 60eedfe..82503d1 100644 --- a/lyra/src/command/model/ctx.rs +++ b/lyra/src/command/model/ctx.rs @@ -1,5 +1,6 @@ mod autocomplete; mod command_data; +mod component; mod menu; mod message; mod modal; @@ -28,7 +29,7 @@ use crate::{ HttpAware, InteractionInterface, OwnedBotState, OwnedBotStateAware, }, error::{ - command::RespondError, core::DeserializeBodyFromHttpError, Cache, CacheResult, NotInGuild, + command::RespondError, core::DeserialiseBodyFromHttpError, Cache, CacheResult, NotInGuild, }, gateway::{GuildIdAware, OptionallyGuildIdAware, SenderAware}, lavalink::Lavalink, @@ -162,7 +163,7 @@ impl Ctx { &self.inner.token } - pub async fn interface(&self) -> Result { + pub async fn interface(&self) -> Result { Ok(self.bot.interaction().await?.interfaces(&self.inner)) } diff --git a/lyra/src/command/model/ctx/command_data.rs b/lyra/src/command/model/ctx/command_data.rs index 9d8f50a..fcf5e71 100644 --- a/lyra/src/command/model/ctx/command_data.rs +++ b/lyra/src/command/model/ctx/command_data.rs @@ -46,22 +46,17 @@ impl Ctx { } impl Ctx { - pub fn command_data(&self) -> &PartialCommandData { - // SAFETY: `self` is `Ctx`, - // so `self.data` is present - let data = unsafe { self.data.as_ref().unwrap_unchecked() }; - let PartialInteractionData::Command(data) = data else { - // SAFETY: + pub const fn command_data(&self) -> &PartialCommandData { + let Some(PartialInteractionData::Command(data)) = self.data.as_ref() else { + // SAFETY: `self` is `Ctx`, + // so `data` will always be `PartialInteractionData::Command(_)` unsafe { unreachable_unchecked() } }; data } pub fn into_command_data(self) -> PartialCommandData { - // SAFETY: `self` is `Ctx`, - // so `self.data` is present - let data = unsafe { self.data.unwrap_unchecked() }; - let PartialInteractionData::Command(command_data) = data else { + let Some(PartialInteractionData::Command(command_data)) = self.data else { // SAFETY: `self` is `Ctx`, // so `data` will always be `PartialInteractionData::Command(_)` unsafe { unreachable_unchecked() } diff --git a/lyra/src/command/model/ctx/component.rs b/lyra/src/command/model/ctx/component.rs new file mode 100644 index 0000000..19a5efc --- /dev/null +++ b/lyra/src/command/model/ctx/component.rs @@ -0,0 +1,49 @@ +use twilight_gateway::{Latency, MessageSender}; +use twilight_model::{ + application::interaction::message_component::MessageComponentInteractionData, channel::Message, + gateway::payload::incoming::InteractionCreate, +}; + +use crate::{command::model::PartialInteractionData, core::model::OwnedBotState}; + +use super::{ComponentMarker, Ctx, Location}; + +impl Ctx { + pub const fn from_data( + inner: Box, + data: Box, + bot: OwnedBotState, + latency: Latency, + sender: MessageSender, + ) -> Self { + Self { + inner, + bot, + latency, + sender, + data: Some(PartialInteractionData::Component(data)), + acknowledged: false, + acknowledgement: None, + kind: std::marker::PhantomData, + location: std::marker::PhantomData, + } + } + + pub fn component_data_mut(&mut self) -> &mut MessageComponentInteractionData { + let Some(PartialInteractionData::Component(data)) = self.data.as_mut() else { + // SAFETY: `self` is `Ctx`, + // so `data` will always be `PartialInteractionData::Component(_)` + unsafe { std::hint::unreachable_unchecked() } + }; + data + } + + pub fn message(&self) -> &Message { + // SAFETY: `self` is `Ctx`, so `self.inner.message` exists + unsafe { self.inner.message.as_ref().unwrap_unchecked() } + } + + pub fn take_custom_id(&mut self) -> String { + std::mem::take(&mut self.component_data_mut().custom_id) + } +} diff --git a/lyra/src/command/util.rs b/lyra/src/command/util.rs index 7113865..e7599ba 100644 --- a/lyra/src/command/util.rs +++ b/lyra/src/command/util.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use lavalink_rs::{error::LavalinkResult, player_context::PlayerContext}; use lyra_ext::unix_time; use rand::{distributions::Alphanumeric, Rng}; @@ -218,6 +220,17 @@ impl DefaultAvatarUrlAware for User { } } +pub fn controller_fmt<'a>( + ctx: &impl AuthorIdAware, + via_controller: bool, + string: &'a str, +) -> Cow<'a, str> { + if via_controller { + return format!("{} {}", ctx.author_id().mention(), string).into(); + } + string.into() +} + pub async fn auto_join_or_check_in_voice_with_user_and_check_not_suppressed( ctx: &mut GuildCtx, ) -> Result<(), AutoJoinOrCheckInVoiceWithUserError> { diff --git a/lyra/src/component.rs b/lyra/src/component.rs index c1aead9..72ee184 100644 --- a/lyra/src/component.rs +++ b/lyra/src/component.rs @@ -1,6 +1,5 @@ pub mod config; pub mod connection; -pub mod controller; pub mod debug; pub mod info; pub mod misc; diff --git a/lyra/src/component/connection/leave.rs b/lyra/src/component/connection/leave.rs index dfd9f6d..e26bcf4 100644 --- a/lyra/src/component/connection/leave.rs +++ b/lyra/src/component/connection/leave.rs @@ -50,7 +50,7 @@ pub(super) async fn disconnect_cleanup( connection.dispatch(Event::QueueClear); }; if let Some(player_ctx) = lavalink.get_player_context(guild_id) { - delete_now_playing_message(cx.http(), &player_ctx.data_unwrapped()).await; + delete_now_playing_message(cx, &player_ctx.data_unwrapped()).await; } lavalink.drop_connection(guild_id); lavalink.delete_player(guild_id).await?; diff --git a/lyra/src/component/controller.rs b/lyra/src/component/controller.rs deleted file mode 100644 index 8b13789..0000000 --- a/lyra/src/component/controller.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lyra/src/component/playback.rs b/lyra/src/component/playback.rs index dff8d5e..3ea6a02 100644 --- a/lyra/src/component/playback.rs +++ b/lyra/src/component/playback.rs @@ -5,12 +5,12 @@ mod restart; mod seek; mod skip; -pub use back::Back; +pub use back::{back, Back}; pub use jump::{Autocomplete as JumpAutocomplete, Jump}; -pub use play_pause::PlayPause; +pub use play_pause::{play_pause, PlayPause}; pub use restart::Restart; pub use seek::Seek; -pub use skip::Skip; +pub use skip::{skip, Skip}; use crate::{ command::require, diff --git a/lyra/src/component/playback/back.rs b/lyra/src/component/playback/back.rs index 758a200..f51073f 100644 --- a/lyra/src/component/playback/back.rs +++ b/lyra/src/component/playback/back.rs @@ -1,6 +1,16 @@ use twilight_interactions::command::{CommandModel, CreateCommand}; -use crate::command::{check, macros::out, model::BotSlashCommand, require}; +use crate::{ + command::{ + check, + macros::out, + model::{BotSlashCommand, GuildCtx, RespondViaMessage}, + require, + util::controller_fmt, + }, + error::component::playback::PlayPauseError, + lavalink::OwnedPlayerData, +}; /// Jumps to the track before the current one in the queue. Will wrap around if queue repeat is enabled. #[derive(CreateCommand, CommandModel)] @@ -14,30 +24,43 @@ impl BotSlashCommand for Back { let player = require::player(&ctx)?; let data = player.data(); - let mut data_w = data.write().await; - let queue = require::queue_not_empty_mut(&mut data_w)?; - let mut txt; + let data_r = data.read().await; + let queue = require::queue_not_empty(&data_r)?; + let txt; if let Ok(current_track) = require::current_track(queue) { check::current_track_is_users(¤t_track, in_voice_with_user)?; - txt = format!("⏮️ ~~`{}`~~", current_track.track.data().info.title); + txt = Some(current_track.track.data().info.title.clone()); } else { - txt = String::new(); + txt = None; } + drop(data_r); - queue.downgrade_repeat_mode(); - queue.acquire_advance_lock(); - queue.recede(); + Ok(back(txt, player, data, &mut ctx, false).await?) + } +} - // SAFETY: since the queue is not empty, receding must always yield a new current track - let item = unsafe { queue.current().unwrap_unchecked() }; - player.context.play_now(item.data()).await?; +pub async fn back( + current_track_title: Option, + player: require::PlayerInterface, + data: OwnedPlayerData, + ctx: &mut GuildCtx, + via_controller: bool, +) -> Result<(), PlayPauseError> { + let mut data_w = data.write().await; + let queue = data_w.queue_mut(); + queue.downgrade_repeat_mode(); + queue.acquire_advance_lock(); + queue.recede(); + // SAFETY: since the queue is not empty, receding must always yield a new current track + let item = unsafe { queue.current().unwrap_unchecked() }; + player.context.play_now(item.data()).await?; + let message = current_track_title.map_or_else( + || format!("⏮️ `{}`", item.data().info.title), + |title| format!("⏮️ ~~`{title}`~~",), + ); + drop(data_w); - if txt.is_empty() { - txt = format!("⏮️ `{}`", item.data().info.title); - } - drop(data_w); - - out!(txt, ctx); - } + let content = controller_fmt(ctx, via_controller, &message); + out!(content, ctx); } diff --git a/lyra/src/component/playback/play_pause.rs b/lyra/src/component/playback/play_pause.rs index c920f37..7d56174 100644 --- a/lyra/src/component/playback/play_pause.rs +++ b/lyra/src/component/playback/play_pause.rs @@ -1,8 +1,16 @@ use twilight_interactions::command::{CommandModel, CreateCommand}; use crate::{ - command::{check, macros::out, model::BotSlashCommand, require, SlashCtx}, - error::CommandResult, + command::{ + check, + macros::out, + model::{BotSlashCommand, GuildCtx, RespondViaMessage}, + require, + util::controller_fmt, + SlashCtx, + }, + error::{component::playback::PlayPauseError, CommandResult}, + lavalink::OwnedPlayerData, }; /// Toggles the playback of the current track. @@ -20,18 +28,28 @@ impl BotSlashCommand for PlayPause { let data_r = data.read().await; let queue = require::queue_not_empty(&data_r)?; check::current_track_is_users(&require::current_track(queue)?, in_voice_with_user)?; - let pause = !data_r.paused(); - let message = if pause { - "▶️ Paused" - } else { - "⏸️ Resumed" - }; drop(data_r); + Ok(play_pause(player, data, &mut ctx, false).await?) + } +} - player - .set_pause_with(pause, &mut data.write().await) - .await?; +pub async fn play_pause( + player: require::PlayerInterface, + data: OwnedPlayerData, + ctx: &mut GuildCtx, + via_controller: bool, +) -> Result<(), PlayPauseError> { + let mut data_w = data.write().await; + let pause = !data_w.paused(); - out!(message, ctx); - } + player.set_pause_with(pause, &mut data_w).await?; + drop(data_w); + + let message = if pause { + "▶️ Paused" + } else { + "⏸️ Resumed" + }; + let content = controller_fmt(ctx, via_controller, message); + out!(content, ctx); } diff --git a/lyra/src/component/playback/skip.rs b/lyra/src/component/playback/skip.rs index 0bc66fe..01cf1be 100644 --- a/lyra/src/component/playback/skip.rs +++ b/lyra/src/component/playback/skip.rs @@ -1,6 +1,16 @@ use twilight_interactions::command::{CommandModel, CreateCommand}; -use crate::command::{check, macros::out, model::BotSlashCommand, require}; +use crate::{ + command::{ + check, + macros::out, + model::{BotSlashCommand, GuildCtx, RespondViaMessage}, + require, + util::controller_fmt, + }, + error::component::playback::PlayPauseError, + lavalink::OwnedPlayerData, +}; /// Skip playing the current track. #[derive(CreateCommand, CommandModel)] @@ -14,22 +24,35 @@ impl BotSlashCommand for Skip { let player = require::player(&ctx)?; let data = player.data(); - let mut data_w = data.write().await; - let queue = require::queue_not_empty_mut(&mut data_w)?; + let data_r = data.read().await; + let queue = require::queue_not_empty(&data_r)?; let current_track = require::current_track(queue)?; check::current_track_is_users(¤t_track, in_voice_with_user)?; - let txt = format!("⏭️ ~~`{}`~~", current_track.track.data().info.title); - - queue.downgrade_repeat_mode(); - queue.acquire_advance_lock(); - queue.advance(); - if let Some(item) = queue.current() { - player.context.play_now(item.data()).await?; - } else { - player.context.stop_now().await?; - } - drop(data_w); + let current_track_title = current_track.track.data().info.title.clone(); + drop(data_r); + Ok(skip(¤t_track_title, player, data, &mut ctx, false).await?) + } +} - out!(txt, ctx); +pub async fn skip( + current_track_title: &str, + player: require::PlayerInterface, + data: OwnedPlayerData, + ctx: &mut GuildCtx, + via_controller: bool, +) -> Result<(), PlayPauseError> { + let mut data_w = data.write().await; + let queue = data_w.queue_mut(); + queue.downgrade_repeat_mode(); + queue.acquire_advance_lock(); + queue.advance(); + if let Some(item) = queue.current() { + player.context.play_now(item.data()).await?; + } else { + player.context.stop_now().await?; } + drop(data_w); + let message = format!("⏭️ ~~`{current_track_title}`~~"); + let content = controller_fmt(ctx, via_controller, &message); + out!(content, ctx); } diff --git a/lyra/src/component/queue.rs b/lyra/src/component/queue.rs index 6082c57..f1d1c7e 100644 --- a/lyra/src/component/queue.rs +++ b/lyra/src/component/queue.rs @@ -21,8 +21,8 @@ pub use play::{Autocomplete as PlayAutocomplete, File as PlayFile, Play}; pub use r#move::{Autocomplete as MoveAutocomplete, Move}; pub use remove::{Autocomplete as RemoveAutocomplete, Remove}; pub use remove_range::{Autocomplete as RemoveRangeAutocomplete, RemoveRange}; -pub use repeat::Repeat; -pub use shuffle::Shuffle; +pub use repeat::{get_next_repeat_mode, repeat, Repeat}; +pub use shuffle::{shuffle, Shuffle}; use std::{collections::HashSet, num::NonZeroUsize, time::Duration}; diff --git a/lyra/src/component/queue/clear.rs b/lyra/src/component/queue/clear.rs index 66c5378..b8f67c6 100644 --- a/lyra/src/component/queue/clear.rs +++ b/lyra/src/component/queue/clear.rs @@ -10,9 +10,8 @@ use crate::{ require, }, error::CommandResult, - gateway::GuildIdAware, lavalink::Event, - LavalinkAware, + LavalinkAndGuildIdAware, }; /// Clears the queue @@ -24,7 +23,7 @@ impl BotSlashCommand for Clear { async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; let in_voice = require::in_voice(&ctx)?.and_unsuppressed()?; - let connection = ctx.lavalink().try_get_connection(ctx.guild_id())?; + let connection = ctx.try_get_connection()?; let in_voice_with_user = check::user_in(in_voice)?; let player = require::player(&ctx)?; diff --git a/lyra/src/component/queue/repeat.rs b/lyra/src/component/queue/repeat.rs index e55bac6..f19fda8 100644 --- a/lyra/src/component/queue/repeat.rs +++ b/lyra/src/component/queue/repeat.rs @@ -4,14 +4,15 @@ use crate::{ command::{ check::{self, ResolveWithPoll, StartPoll}, macros::out_or_upd, - model::BotSlashCommand, + model::{BotSlashCommand, CtxKind, GuildCtx, RespondViaMessage}, poll::Topic, - require, SlashCtx, + require, + util::controller_fmt, + SlashCtx, }, - error::CommandResult, - gateway::GuildIdAware, - lavalink::{DelegateMethods, Event, RepeatMode as LavalinkRepeatMode}, - LavalinkAware, + error::{component::queue::RepeatError, CommandResult}, + lavalink::{Event, OwnedPlayerData, RepeatMode as LavalinkRepeatMode}, + LavalinkAndGuildIdAware, }; #[derive(CommandOption, CreateOption)] @@ -45,16 +46,11 @@ pub struct Repeat { impl BotSlashCommand for Repeat { async fn run(self, ctx: SlashCtx) -> CommandResult { let mut ctx = require::guild(ctx)?; - let guild_id = ctx.guild_id(); let mode = { if let Some(mode) = self.mode { mode.into() } else { - let mode = match ctx.lavalink().get_player_data(guild_id) { - Some(data) => data.read().await.queue().repeat_mode(), - None => LavalinkRepeatMode::Off, - }; - mode.next() + get_next_repeat_mode(&ctx).await } }; @@ -72,12 +68,27 @@ impl BotSlashCommand for Repeat { .and_then_start(&mut ctx) .await?; - ctx.lavalink() - .try_get_connection(guild_id)? - .dispatch(Event::QueueRepeat); - data.write().await.queue_mut().set_repeat_mode(mode); - - let txt = &format!("{} {}", mode.emoji(), mode); - out_or_upd!(txt, ctx); + Ok(repeat(&mut ctx, data, mode, false).await?) } } + +pub async fn get_next_repeat_mode(ctx: &GuildCtx) -> LavalinkRepeatMode { + let mode = match ctx.get_player_data() { + Some(data) => data.read().await.queue().repeat_mode(), + None => LavalinkRepeatMode::Off, + }; + mode.next() +} + +pub async fn repeat( + ctx: &mut GuildCtx, + data: OwnedPlayerData, + mode: LavalinkRepeatMode, + via_controller: bool, +) -> Result<(), RepeatError> { + ctx.try_get_connection()?.dispatch(Event::QueueRepeat); + data.write().await.queue_mut().set_repeat_mode(mode); + let message = format!("{} {}", mode.emoji(), mode); + let content = controller_fmt(ctx, via_controller, &message); + out_or_upd!(content, ctx); +} diff --git a/lyra/src/component/queue/shuffle.rs b/lyra/src/component/queue/shuffle.rs index 67b1d8a..762dcc7 100644 --- a/lyra/src/component/queue/shuffle.rs +++ b/lyra/src/component/queue/shuffle.rs @@ -4,11 +4,13 @@ use crate::{ command::{ check, macros::{bad, out}, - model::BotSlashCommand, - require, SlashCtx, + model::{BotSlashCommand, GuildCtx, RespondViaMessage}, + require, + util::controller_fmt, + SlashCtx, }, error::CommandResult, - lavalink::IndexerType, + lavalink::{IndexerType, OwnedPlayerData}, }; /// Toggles queue shuffling @@ -24,31 +26,40 @@ impl BotSlashCommand for Shuffle { let data = player.data(); let data_r = data.read().await; - let queue = require::queue_not_empty(&data_r)?; - let indexer_type = queue.indexer_type(); + require::queue_not_empty(&data_r)?; drop(data_r); - match indexer_type { - IndexerType::Shuffled => { - data.write() - .await - .queue_mut() - .set_indexer_type(IndexerType::Standard); - out!("**` ⮆ `** Disabled shuffle", ctx); - } - IndexerType::Fair => { - bad!( - "Cannot enable shuffle as fair queue is currently enabled", - ctx - ); - } - IndexerType::Standard => { - data.write() - .await - .queue_mut() - .set_indexer_type(IndexerType::Shuffled); - out!("🔀 Enabled shuffle", ctx); - } + Ok(shuffle(data, &mut ctx, false).await?) + } +} + +pub async fn shuffle( + data: OwnedPlayerData, + ctx: &mut GuildCtx, + via_controller: bool, +) -> Result<(), crate::error::command::RespondError> { + let mut data_w = data.write().await; + let indexer_type = data_w.queue().indexer_type(); + match indexer_type { + IndexerType::Shuffled => { + data_w.queue_mut().set_indexer_type(IndexerType::Standard); + drop(data_w); + let content = controller_fmt(ctx, via_controller, "**` ⮆ `** Disabled shuffle"); + out!(content, ctx); + } + IndexerType::Fair => { + drop(data_w); + bad!( + // The shuffle button on the playback controller will be disabled, so no need to use `controller_fmt` here + "Cannot enable shuffle as fair queue is currently enabled", + ctx + ); + } + IndexerType::Standard => { + data_w.queue_mut().set_indexer_type(IndexerType::Shuffled); + drop(data_w); + let content = controller_fmt(ctx, via_controller, "🔀 Enabled shuffle"); + out!(content, ctx); } } } diff --git a/lyra/src/core.rs b/lyra/src/core.rs index 392fe0c..67153e4 100644 --- a/lyra/src/core.rs +++ b/lyra/src/core.rs @@ -1,3 +1,5 @@ pub mod r#const; +pub mod emoji; pub mod model; +pub mod r#static; pub mod traced; diff --git a/lyra/src/core/model/emoji.rs b/lyra/src/core/emoji.rs similarity index 74% rename from lyra/src/core/model/emoji.rs rename to lyra/src/core/emoji.rs index e1919c9..1d0eba5 100644 --- a/lyra/src/core/model/emoji.rs +++ b/lyra/src/core/emoji.rs @@ -2,15 +2,11 @@ use std::sync::OnceLock; use twilight_model::channel::message::EmojiReactionType; -use crate::error::core::DeserializeBodyFromHttpError; - -use super::BotState; +use crate::{core::model::HttpAware, error::core::DeserialiseBodyFromHttpError}; macro_rules! generate_emojis { ($ (($name: ident, $default: expr)) ,* $(,)? ) => {$( - pub async fn $name( - bot: &BotState, - ) -> Result<&'static EmojiReactionType, DeserializeBodyFromHttpError> { + pub async fn $name(_cx: &(impl HttpAware + Sync)) -> Result<&'static EmojiReactionType, DeserialiseBodyFromHttpError> { ::paste::paste! { static [<$name:upper>]: OnceLock = OnceLock::new(); if let Some(emoji) = [<$name:upper>].get() { @@ -18,7 +14,10 @@ macro_rules! generate_emojis { } } - let emojis = bot.application_emojis().await?; + let emojis: &'static [twilight_model::guild::Emoji] = &[]; + + // FIXME: https://github.com/twilight-rs/twilight/issues/2373 + // let emojis = crate::core::r#static::application::emojis(cx).await?; let emoji = emojis.iter().find(|e| e.name == stringify!($name)); let reaction = emoji.map_or( { diff --git a/lyra/src/core/model.rs b/lyra/src/core/model.rs index b69f0ab..5900e4c 100644 --- a/lyra/src/core/model.rs +++ b/lyra/src/core/model.rs @@ -1,4 +1,3 @@ -mod emoji; mod interaction; use std::{ @@ -11,7 +10,6 @@ use std::{ use dashmap::DashMap; use sqlx::{Pool, Postgres}; -use tokio::sync::OnceCell; use twilight_cache_inmemory::InMemoryCache; use twilight_gateway::ShardId; use twilight_http::Client; @@ -25,13 +23,15 @@ use twilight_model::{ }; use twilight_standby::Standby; -use crate::{error::core::DeserializeBodyFromHttpError, lavalink::Lavalink, LavalinkAware}; +use crate::{error::core::DeserialiseBodyFromHttpError, lavalink::Lavalink, LavalinkAware}; pub use self::interaction::{ AcknowledgementAware, Client as InteractionClient, Interface as InteractionInterface, MessageResponse, UnitFollowupResult, UnitRespondResult, }; +use super::r#static::application; + pub struct Config { pub token: &'static str, pub lavalink_host: &'static str, @@ -140,8 +140,6 @@ pub struct BotState { standby: Standby, lavalink: Lavalink, info: BotInfo, - application_id: OnceCell>, - application_emojis: OnceCell<&'static [Emoji]>, } impl BotState { @@ -163,8 +161,6 @@ impl BotState { lavalink, db, info, - application_id: OnceCell::new(), - application_emojis: OnceCell::new(), } } @@ -176,32 +172,21 @@ impl BotState { &self.info } + #[inline] pub async fn application_id( &self, - ) -> Result, DeserializeBodyFromHttpError> { - self.application_id - .get_or_try_init(|| async { - let application = self.http.current_user_application().await?.model().await?; - Ok(application.id) - }) - .await - .copied() + ) -> Result, DeserialiseBodyFromHttpError> { + application::id(self).await } + #[inline] pub async fn application_emojis( &self, - ) -> Result<&'static [Emoji], DeserializeBodyFromHttpError> { - self.application_emojis - .get_or_try_init(|| async { - let application_id = self.application_id().await?; - let req = self.http.get_application_emojis(application_id); - Ok(&*req.await?.models().await?.leak()) - }) - .await - .copied() - } - - pub async fn interaction(&self) -> Result { + ) -> Result<&'static [Emoji], DeserialiseBodyFromHttpError> { + application::emojis(self).await + } + + pub async fn interaction(&self) -> Result { let client = self.http.interaction(self.application_id().await?); Ok(InteractionClient::new(client)) } diff --git a/lyra/src/core/static.rs b/lyra/src/core/static.rs new file mode 100644 index 0000000..ab66dfb --- /dev/null +++ b/lyra/src/core/static.rs @@ -0,0 +1,78 @@ +pub mod application { + use tokio::sync::OnceCell; + use twilight_model::{ + guild::Emoji, + id::{marker::ApplicationMarker, Id}, + }; + + use crate::{core::model::HttpAware, error::core::DeserialiseBodyFromHttpError}; + + static ID: OnceCell> = OnceCell::const_new(); + static EMOJIS: OnceCell<&'static [Emoji]> = OnceCell::const_new(); + + pub async fn id( + cx: &(impl HttpAware + Sync), + ) -> Result, DeserialiseBodyFromHttpError> { + ID.get_or_try_init(|| async { + let application = cx.http().current_user_application().await?.model().await?; + Ok(application.id) + }) + .await + .copied() + } + + pub async fn emojis( + cx: &(impl HttpAware + Sync), + ) -> Result<&'static [Emoji], DeserialiseBodyFromHttpError> { + EMOJIS + .get_or_try_init(|| async { + let application_id = id(cx).await?; + let req = cx.http().get_application_emojis(application_id); + Ok(&*req.await?.models().await?.leak()) + }) + .await + .copied() + } +} + +pub mod component { + use std::sync::LazyLock; + + use rand::{distributions::Alphanumeric, Rng}; + + pub struct NowPlayingButtonIds { + pub shuffle: &'static str, + pub previous: &'static str, + pub play_pause: &'static str, + pub next: &'static str, + pub repeat: &'static str, + } + + impl NowPlayingButtonIds { + const BUTTON_ID_LEN: usize = 100; + fn new() -> Self { + let mut button_id_iter = rand::thread_rng() + .sample_iter(&Alphanumeric) + .map(char::from); + + let mut button_id_gen = || { + button_id_iter + .by_ref() + .take(Self::BUTTON_ID_LEN) + .collect::() + .leak() + }; + + Self { + shuffle: button_id_gen(), + previous: button_id_gen(), + play_pause: button_id_gen(), + next: button_id_gen(), + repeat: button_id_gen(), + } + } + } + + pub static NOW_PLAYING_BUTTON_IDS: LazyLock = + LazyLock::new(NowPlayingButtonIds::new); +} diff --git a/lyra/src/error/command.rs b/lyra/src/error/command.rs index 197c814..4a62cf7 100644 --- a/lyra/src/error/command.rs +++ b/lyra/src/error/command.rs @@ -8,13 +8,13 @@ pub mod util; #[error("creating a response failed: {}", .0)] pub enum RespondError { TwilightHttp(#[from] twilight_http::Error), - DeserializeBodyFromHttp(#[from] super::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] super::core::DeserialiseBodyFromHttpError), } #[derive(thiserror::Error, Debug)] #[error("creating a followup failed: {}", .0)] pub enum FollowupError { - DeserializeBodyFromHttp(#[from] super::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] super::core::DeserialiseBodyFromHttpError), Followup(#[from] super::core::FollowupError), } @@ -47,7 +47,7 @@ pub enum Error { Join(#[from] super::component::connection::join::ResidualError), Leave(#[from] super::component::connection::leave::ResidualError), Play(#[from] super::component::queue::play::Error), - DeserializeBodyFromHttp(#[from] super::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] super::core::DeserialiseBodyFromHttpError), RemoveTracks(#[from] super::component::queue::RemoveTracksError), TwilightHttp(#[from] twilight_http::Error), Lavalink(#[from] lavalink_rs::error::LavalinkError), @@ -59,6 +59,8 @@ pub enum Error { NotPlaying(#[from] super::NotPlaying), Paused(#[from] super::Paused), UnrecognisedConnection(#[from] super::UnrecognisedConnection), + PlayPause(#[from] super::component::playback::PlayPauseError), + Repeat(#[from] super::component::queue::RepeatError), } pub enum FlattenedError<'a> { @@ -118,11 +120,11 @@ impl<'a> Fe<'a> { } const fn from_deserialize_body_from_http_error( - error: &'a super::core::DeserializeBodyFromHttpError, + error: &'a super::core::DeserialiseBodyFromHttpError, ) -> Self { match error { - super::core::DeserializeBodyFromHttpError::TwilightHttp(_) => Self::TwilightHttp, - super::core::DeserializeBodyFromHttpError::DeserializeBody(_) => Self::DeserializeBody, + super::core::DeserialiseBodyFromHttpError::TwilightHttp(_) => Self::TwilightHttp, + super::core::DeserialiseBodyFromHttpError::DeserializeBody(_) => Self::DeserializeBody, } } @@ -190,7 +192,7 @@ impl<'a> Fe<'a> { match error { poll::WaitForVotesError::TwilightHttp(_) => Self::TwilightHttp, poll::WaitForVotesError::EventRecv(_) => Self::EventRecv, - poll::WaitForVotesError::DeserializeBodyFromHttp(e) => { + poll::WaitForVotesError::DeserialiseBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) } poll::WaitForVotesError::UpdateEmbed(e) => Self::from_update_embed(e), @@ -215,7 +217,7 @@ impl<'a> Fe<'a> { check::HandlePollError::PollLoss(e) => Self::PollLoss(e), check::HandlePollError::PollVoided(e) => Self::PollVoided(e), check::HandlePollError::StartPoll(e) => Self::from_start_poll(e), - check::HandlePollError::DeserializeBodyFromHttp(e) => { + check::HandlePollError::DeserialiseBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) } } @@ -235,7 +237,7 @@ impl<'a> Fe<'a> { const fn from_respond(error: &'a RespondError) -> Self { match error { RespondError::TwilightHttp(_) => Self::TwilightHttp, - RespondError::DeserializeBodyFromHttp(e) => { + RespondError::DeserialiseBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) } } @@ -243,7 +245,7 @@ impl<'a> Fe<'a> { const fn from_followup(error: &'a FollowupError) -> Self { match error { - FollowupError::DeserializeBodyFromHttp(e) => { + FollowupError::DeserialiseBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) } FollowupError::Followup(e) => Self::from_core_followup_error(e), @@ -518,11 +520,27 @@ impl<'a> Fe<'a> { super::component::queue::RemoveTracksError::Lavalink(_) => Self::Lavalink, super::component::queue::RemoveTracksError::Respond(e) => Self::from_respond(e), super::component::queue::RemoveTracksError::Followup(e) => Self::from_followup(e), - super::component::queue::RemoveTracksError::DeserializeBodyFromHttp(e) => { + super::component::queue::RemoveTracksError::DeserialiseBodyFromHttp(e) => { Self::from_deserialize_body_from_http_error(e) } } } + + const fn from_play_pause(error: &'a super::component::playback::PlayPauseError) -> Self { + match error { + super::component::playback::PlayPauseError::Lavalink(_) => Self::Lavalink, + super::component::playback::PlayPauseError::Respond(e) => Self::from_respond(e), + } + } + + const fn from_repeat(error: &'a super::component::queue::RepeatError) -> Self { + match error { + super::component::queue::RepeatError::UnrecognisedConnection(_) => { + Self::UnrecognisedConnection + } + super::component::queue::RepeatError::Respond(e) => Self::from_respond(e), + } + } } impl Error { @@ -557,10 +575,12 @@ impl Error { Self::Join(e) => Fe::from_join_residual(e), Self::Leave(e) => Fe::from_leave_residual(e), Self::Play(e) => Fe::from_play(e), - Self::DeserializeBodyFromHttp(e) => Fe::from_deserialize_body_from_http_error(e), + Self::DeserialiseBodyFromHttp(e) => Fe::from_deserialize_body_from_http_error(e), Self::RemoveTracks(e) => Fe::from_remove_tracks(e), Self::CheckUserOnlyIn(e) => Fe::from_check_user_only_in(e), Self::HandlePoll(e) => Fe::from_handle_poll(e), + Self::PlayPause(e) => Fe::from_play_pause(e), + Self::Repeat(e) => Fe::from_repeat(e), } } } diff --git a/lyra/src/error/command/check.rs b/lyra/src/error/command/check.rs index e0e973d..eaf8592 100644 --- a/lyra/src/error/command/check.rs +++ b/lyra/src/error/command/check.rs @@ -174,7 +174,7 @@ pub enum HandlePollError { AnotherPollOngoing(#[from] AnotherPollOngoingError), StartPoll(#[from] super::poll::StartPollError), EventSend(#[from] tokio::sync::broadcast::error::SendError), - DeserializeBodyFromHttp(#[from] crate::error::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] crate::error::core::DeserialiseBodyFromHttpError), PollLoss(#[from] PollLossError), PollVoided(#[from] PollVoidedError), EventRecv(#[from] tokio::sync::broadcast::error::RecvError), @@ -183,7 +183,7 @@ pub enum HandlePollError { #[derive(Error, Debug)] #[error(transparent)] pub enum SendSupersededWinNoticeError { - DeserializeBodyFromHttp(#[from] crate::error::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] crate::error::core::DeserialiseBodyFromHttpError), Http(#[from] twilight_http::Error), MessageValidation(#[from] twilight_validate::message::MessageValidationError), } diff --git a/lyra/src/error/command/poll.rs b/lyra/src/error/command/poll.rs index 2278421..90c441a 100644 --- a/lyra/src/error/command/poll.rs +++ b/lyra/src/error/command/poll.rs @@ -29,7 +29,7 @@ pub enum StartPollError { #[derive(Error, Debug)] #[error(transparent)] pub enum WaitForVotesError { - DeserializeBodyFromHttp(#[from] crate::error::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] crate::error::core::DeserialiseBodyFromHttpError), TwilightHttp(#[from] twilight_http::Error), UpdateEmbed(#[from] UpdateEmbedError), EventRecv(#[from] tokio::sync::broadcast::error::RecvError), diff --git a/lyra/src/error/component/connection.rs b/lyra/src/error/component/connection.rs index 69d25b0..56e869f 100644 --- a/lyra/src/error/component/connection.rs +++ b/lyra/src/error/component/connection.rs @@ -7,7 +7,7 @@ pub mod join { pub enum DeleteEmptyVoiceNoticeError { Http(#[from] twilight_http::Error), StandbyDropped(#[from] twilight_standby::future::Canceled), - DeserializeBodyFromHttp(#[from] crate::error::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] crate::error::core::DeserialiseBodyFromHttpError), } #[derive(thiserror::Error, Debug)] diff --git a/lyra/src/error/component/playback.rs b/lyra/src/error/component/playback.rs index a0a3ea1..2d25948 100644 --- a/lyra/src/error/component/playback.rs +++ b/lyra/src/error/component/playback.rs @@ -1,8 +1,21 @@ use thiserror::Error; +pub use play_pause::Error as PlayPauseError; + #[derive(Error, Debug)] #[error("handling `VoiceStateUpdate` failed: {:?}", .0)] pub enum HandleVoiceStateUpdateError { Lavalink(#[from] lavalink_rs::error::LavalinkError), TwilightHttp(#[from] twilight_http::Error), } + +pub mod play_pause { + use thiserror::Error; + + #[derive(Error, Debug)] + #[error(transparent)] + pub enum Error { + Lavalink(#[from] lavalink_rs::error::LavalinkError), + Respond(#[from] crate::error::command::RespondError), + } +} diff --git a/lyra/src/error/component/queue.rs b/lyra/src/error/component/queue.rs index c74b226..e9ef5d6 100644 --- a/lyra/src/error/component/queue.rs +++ b/lyra/src/error/component/queue.rs @@ -29,6 +29,17 @@ pub mod play { } } +pub mod repeat { + #[derive(thiserror::Error, Debug)] + #[error(transparent)] + pub enum Error { + UnrecognisedConnection(#[from] crate::error::UnrecognisedConnection), + Respond(#[from] crate::error::command::RespondError), + } +} + +pub use repeat::Error as RepeatError; + use thiserror::Error; #[derive(Error, Debug)] @@ -40,5 +51,5 @@ pub enum RemoveTracksError { #[error(transparent)] Followup(#[from] crate::error::command::FollowupError), #[error(transparent)] - DeserializeBodyFromHttp(#[from] crate::error::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] crate::error::core::DeserialiseBodyFromHttpError), } diff --git a/lyra/src/error/core.rs b/lyra/src/error/core.rs index 0d70235..ee13b2e 100644 --- a/lyra/src/error/core.rs +++ b/lyra/src/error/core.rs @@ -12,7 +12,7 @@ pub type FollowupResult = Result; #[derive(Error, Debug)] #[error(transparent)] -pub enum DeserializeBodyFromHttpError { +pub enum DeserialiseBodyFromHttpError { TwilightHttp(#[from] twilight_http::Error), DeserializeBody(#[from] twilight_http::response::DeserializeBodyError), } diff --git a/lyra/src/error/gateway.rs b/lyra/src/error/gateway.rs index 33f43f2..7f3992e 100644 --- a/lyra/src/error/gateway.rs +++ b/lyra/src/error/gateway.rs @@ -5,7 +5,7 @@ pub enum ProcessError { #[error(transparent)] EventSend(#[from] tokio::sync::broadcast::error::SendError), #[error(transparent)] - DeserializeBodyFromHttp(#[from] super::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] super::core::DeserialiseBodyFromHttpError), #[error(transparent)] Http(#[from] twilight_http::Error), #[error(transparent)] @@ -24,6 +24,12 @@ pub enum ProcessError { PlaybackHandleVoiceStateUpdate(#[from] super::component::playback::HandleVoiceStateUpdateError), #[error(transparent)] Respond(#[from] super::command::RespondError), + #[error(transparent)] + Lavalink(#[from] lavalink_rs::error::LavalinkError), + #[error(transparent)] + PlayPause(#[from] super::component::playback::PlayPauseError), + #[error(transparent)] + Repeat(#[from] super::component::queue::RepeatError), #[error("error executing command `/{}`: {:?}", .name, .source)] CommandExecute { name: Box, diff --git a/lyra/src/error/lavalink.rs b/lyra/src/error/lavalink.rs index 0a600e5..b7791b9 100644 --- a/lyra/src/error/lavalink.rs +++ b/lyra/src/error/lavalink.rs @@ -11,6 +11,7 @@ pub enum ProcessError { TwilightHttp(#[from] twilight_http::Error), Sqlx(#[from] sqlx::Error), DeserialiseBody(#[from] twilight_http::response::DeserializeBodyError), + DeserialiseBodyFromHttp(#[from] super::core::DeserialiseBodyFromHttpError), GenerateNowPlayingEmbed(#[from] GenerateNowPlayingEmbedError), } diff --git a/lyra/src/error/runner.rs b/lyra/src/error/runner.rs index 5763b1d..fb9c6f0 100644 --- a/lyra/src/error/runner.rs +++ b/lyra/src/error/runner.rs @@ -8,7 +8,7 @@ pub enum StartError { DeserializeBody(#[from] twilight_http::response::DeserializeBodyError), Http(#[from] twilight_http::Error), WaitUntilShutdown(#[from] WaitUntilShutdownError), - DeserializeBodyFromHttp(#[from] super::core::DeserializeBodyFromHttpError), + DeserialiseBodyFromHttp(#[from] super::core::DeserialiseBodyFromHttpError), RegisterGlobalCommands(#[from] super::core::RegisterGlobalCommandsError), } diff --git a/lyra/src/gateway/interaction.rs b/lyra/src/gateway/interaction.rs index d26f273..365e1cd 100644 --- a/lyra/src/gateway/interaction.rs +++ b/lyra/src/gateway/interaction.rs @@ -18,18 +18,22 @@ use crate::{ bad, bad_or_fol, cant_or_fol, caut, crit, crit_or_fol, err, hid, nope, nope_or_fol, note, out_upd, sus, sus_fol, }, - model::NonPingInteraction, + model::{ComponentCtx, NonPingInteraction}, require, util::MessageLinkAware, AutocompleteCtx, MessageCtx, SlashCtx, }, - component::{connection::Join, queue::Play}, + component::{ + connection::Join, + queue::{get_next_repeat_mode, repeat, Play}, + }, core::{ model::{ BotState, InteractionClient, InteractionInterface, OwnedBotState, UnitFollowupResult, UnitRespondResult, }, r#const::exit_code::{DUBIOUS, PROHIBITED, WARNING}, + r#static::component::NOW_PLAYING_BUTTON_IDS, }, error::{ command::{ @@ -181,7 +185,7 @@ impl Context { let name = data.name.clone().into(); let (tx, _) = oneshot::channel::<()>(); - let Err(source) = ::from_partial_data( + let Err(source) = AutocompleteCtx::from_partial_data( self.inner, &data, self.bot, @@ -198,7 +202,6 @@ impl Context { Err(ProcessError::AutocompleteExecute { name, source }) } - #[allow(clippy::unused_async)] async unsafe fn process_as_component(mut self) -> ProcessResult { let Some(InteractionData::MessageComponent(data)) = self.inner.data.take() else { // SAFETY: interaction is of type `MessageComponent`, @@ -206,8 +209,63 @@ impl Context { unsafe { unreachable_unchecked() } }; tracing::trace!(?data); - // TODO: implement controller + let ctx = ComponentCtx::from_data(self.inner, data, self.bot, self.latency, self.sender); + let Ok(mut ctx) = require::guild(ctx) else { + return Ok(()); + }; + let Ok(player) = require::player(&ctx) else { + return Ok(()); + }; + + let player_data = player.data(); + let player_data_r = player_data.read().await; + let now_playing_message_id = player_data_r.now_playing_message_id(); + if now_playing_message_id != Some(ctx.message().id) { + return Ok(()); + } + let Some(current_track_title) = player_data_r + .queue() + .current() + .map(|item| item.data().info.title.clone()) + else { + return Ok(()); + }; + drop(player_data_r); + + match ctx.take_custom_id() { + id if id == NOW_PLAYING_BUTTON_IDS.shuffle => { + crate::component::queue::shuffle(player_data, &mut ctx, true).await?; + } + id if id == NOW_PLAYING_BUTTON_IDS.previous => { + crate::component::playback::back( + Some(current_track_title), + player, + player_data, + &mut ctx, + true, + ) + .await?; + } + id if id == NOW_PLAYING_BUTTON_IDS.play_pause => { + crate::component::playback::play_pause(player, player_data, &mut ctx, true).await?; + } + id if id == NOW_PLAYING_BUTTON_IDS.next => { + crate::component::playback::skip( + ¤t_track_title, + player, + player_data, + &mut ctx, + true, + ) + .await?; + } + id if id == NOW_PLAYING_BUTTON_IDS.repeat => { + let mode = get_next_repeat_mode(&ctx).await; + repeat(&mut ctx, player_data, mode, true).await?; + } + _ => {} + } Ok(()) } diff --git a/lyra/src/lavalink/model.rs b/lyra/src/lavalink/model.rs index e11f681..9f7456a 100644 --- a/lyra/src/lavalink/model.rs +++ b/lyra/src/lavalink/model.rs @@ -58,6 +58,10 @@ pub trait ClientAndGuildIdAware: ClientAware + GuildIdAware { self.lavalink().get_player_context(self.guild_id()) } + fn get_player_data(&self) -> Option { + self.get_player().map(|player| player.data_unwrapped()) + } + fn get_connection(&self) -> Option { self.lavalink().get_connection(self.guild_id()) } @@ -65,6 +69,12 @@ pub trait ClientAndGuildIdAware: ClientAware + GuildIdAware { fn get_connection_mut(&self) -> Option { self.lavalink().get_connection_mut(self.guild_id()) } + + /// # Errors + /// when an unrecognised connection was found + fn try_get_connection(&self) -> Result { + self.lavalink().try_get_connection(self.guild_id()) + } } type ClientRefAndGuildId<'a> = (&'a Lavalink, Id); diff --git a/lyra/src/lavalink/track/end.rs b/lyra/src/lavalink/track/end.rs index 14794c3..c828ef2 100644 --- a/lyra/src/lavalink/track/end.rs +++ b/lyra/src/lavalink/track/end.rs @@ -1,5 +1,4 @@ use lavalink_rs::{client::LavalinkClient, model::events::TrackEnd}; -use twilight_http::Client; use crate::{ core::model::HttpAware, @@ -27,7 +26,7 @@ pub(super) async fn impl_end( }; let data = player.data_unwrapped(); - delete_now_playing_message(lavalink.data_unwrapped().http(), &data).await; + delete_now_playing_message(lavalink.data_unwrapped().as_ref(), &data).await; let data_r = data.read().await; if data_r.queue().not_advance_locked().await { @@ -49,11 +48,11 @@ pub(super) async fn impl_end( Ok(()) } -pub async fn delete_now_playing_message(http: &Client, data: &PlayerData) { +pub async fn delete_now_playing_message(cx: &(impl HttpAware + Sync), data: &PlayerData) { let mut data_w = data.write().await; if let Some(message_id) = data_w.take_now_playing_message_id() { let channel_id = data_w.now_playing_message_channel_id(); - let _ = http.delete_message(channel_id, message_id).await; + let _ = cx.http().delete_message(channel_id, message_id).await; data_w.sync_now_playing_message_channel_id(); }; drop(data_w); diff --git a/lyra/src/lavalink/track/start.rs b/lyra/src/lavalink/track/start.rs index a42ca9a..03ae01e 100644 --- a/lyra/src/lavalink/track/start.rs +++ b/lyra/src/lavalink/track/start.rs @@ -10,7 +10,10 @@ use lyra_ext::{ use twilight_cache_inmemory::{InMemoryCache, Reference}; use twilight_mention::{timestamp::TimestampStyle, Mention}; use twilight_model::{ - channel::message::Embed, + channel::message::{ + component::{ActionRow, Button, ButtonStyle}, + Component, Embed, + }, id::{marker::UserMarker, Id}, user::User, }; @@ -20,14 +23,19 @@ use twilight_util::builder::embed::{ use crate::{ command::util::{AvatarUrlAware, DefaultAvatarUrlAware, GuildAvatarUrlAware}, - core::model::{CacheAware, DatabaseAware, HttpAware}, + core::{ + emoji, + model::{CacheAware, DatabaseAware, HttpAware}, + r#static::component::NOW_PLAYING_BUTTON_IDS, + }, error::{ + core::DeserialiseBodyFromHttpError, lavalink::{GenerateNowPlayingEmbedError, GetDominantPaletteFromUrlError, ProcessResult}, Cache, }, lavalink::{ - model::ArtworkCache, CorrectTrackInfo, PluginInfo, QueueItem, UnwrappedData, - UnwrappedPlayerInfoUri, + model::ArtworkCache, ClientData, CorrectTrackInfo, IndexerType, PluginInfo, QueueItem, + RepeatMode, UnwrappedData, UnwrappedPlayerInfoUri, }, }; @@ -84,11 +92,15 @@ pub(super) async fn impl_start( data_r.speed(), ) .await?; + let components = + generate_now_playing_components(&lavalink_data, queue.repeat_mode(), queue.indexer_type()) + .await?; let req = lavalink_data .http() .create_message(data_r.now_playing_message_channel_id()) .content("🎵 **Now Playing**") .embeds(&[embed]) + .components(&[components]) .await?; let message_id = req.model().await?.id; @@ -97,6 +109,89 @@ pub(super) async fn impl_start( Ok(()) } +async fn generate_now_playing_components( + lavalink_data: &ClientData, + repeat_mode: RepeatMode, + indexer: IndexerType, +) -> Result { + let (shuffle_emoji, shuffle_disabled) = { + let (shuffle_emoji, shuffle_disabled) = match indexer { + IndexerType::Standard => (emoji::shuffle_off(lavalink_data).await, false), + IndexerType::Fair => (emoji::shuffle_off(lavalink_data).await, true), + IndexerType::Shuffled => (emoji::shuffle_on(lavalink_data).await, false), + }; + (Some(shuffle_emoji?.clone()), shuffle_disabled) + }; + let shuffle_button = Component::Button(Button { + custom_id: Some(NOW_PLAYING_BUTTON_IDS.shuffle.to_owned()), + disabled: shuffle_disabled, + emoji: shuffle_emoji, + style: ButtonStyle::Danger, + label: None, + url: None, + sku_id: None, + }); + + let previous_button = Component::Button(Button { + custom_id: Some(NOW_PLAYING_BUTTON_IDS.previous.to_owned()), + disabled: false, + emoji: Some(emoji::previous(lavalink_data).await?.clone()), + style: ButtonStyle::Secondary, + label: None, + url: None, + sku_id: None, + }); + + let play_pause_button = Component::Button(Button { + custom_id: Some(NOW_PLAYING_BUTTON_IDS.play_pause.to_owned()), + disabled: false, + emoji: Some(emoji::pause(lavalink_data).await?.clone()), + style: ButtonStyle::Primary, + label: None, + url: None, + sku_id: None, + }); + + let next_button = Component::Button(Button { + custom_id: Some(NOW_PLAYING_BUTTON_IDS.next.to_owned()), + disabled: false, + emoji: Some(emoji::next(lavalink_data).await?.clone()), + style: ButtonStyle::Secondary, + label: None, + url: None, + sku_id: None, + }); + + let repeat_emoji = Some( + match repeat_mode { + RepeatMode::Off => emoji::repeat_off(lavalink_data).await, + RepeatMode::All => emoji::repeat_all(lavalink_data).await, + RepeatMode::Track => emoji::repeat_track(lavalink_data).await, + }? + .clone(), + ); + let repeat_button = Component::Button(Button { + custom_id: Some(NOW_PLAYING_BUTTON_IDS.repeat.to_owned()), + disabled: false, + emoji: repeat_emoji, + style: ButtonStyle::Success, + label: None, + url: None, + sku_id: None, + }); + + let row = Component::ActionRow(ActionRow { + components: vec![ + shuffle_button, + previous_button, + play_pause_button, + next_button, + repeat_button, + ], + }); + Ok(row) +} + async fn generate_now_playing_embed( cache: &InMemoryCache, artwork_cache: &ArtworkCache, @@ -153,14 +248,8 @@ async fn generate_now_playing_embed( }; match (requester.nick(), requester.avatar_url(twilight_guild_id)) { (Some(nick), Some(url)) => (nick.to_owned(), url), - (Some(nick), None) => { - let user = get_user()?; - (nick.to_owned(), get_display_avatar(&user)) - } - (None, Some(url)) => { - let user = get_user()?; - (get_display_name(&user), url) - } + (Some(nick), None) => (nick.to_owned(), get_display_avatar(&get_user()?)), + (None, Some(url)) => (get_display_name(&get_user()?), url), (None, None) => { let user = get_user()?; (get_display_name(&user), get_display_avatar(&user)) diff --git a/lyra/src/runner.rs b/lyra/src/runner.rs index c64e00d..06b4899 100644 --- a/lyra/src/runner.rs +++ b/lyra/src/runner.rs @@ -30,7 +30,7 @@ use twilight_model::{ }; use crate::{ - core::{model::HttpAware, r#const::metadata::BANNER}, + core::r#const::metadata::BANNER, lavalink::{handlers, ClientData}, LavalinkAware, }; @@ -216,7 +216,7 @@ async fn wait_until_shutdown( tracing::debug!("deleting all now playing messages..."); for data in bot.lavalink().iter_player_data() { - crate::lavalink::delete_now_playing_message(bot.http(), &data).await; + crate::lavalink::delete_now_playing_message(bot, &data).await; } tracing::debug!("sending close frames to all shards...");