Add sticky option to menus

Menus can now be created as sticky menus. When new messages appear
in the channel, the sticky message will be resent to be the latest
one in the channel. It only get's recreated every ten seconds max
to avoid getting ratelimited.
To work with recreatable menus, the message handle returned by
the menu is now wrapped into an Arc<RwLock<>>.

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/9/head
trivernis 3 years ago
parent bf29b51092
commit c6b80f8abd
Signed by: Trivernis
GPG Key ID: DFFFCC2C7A02DB45

1
Cargo.lock generated

@ -209,6 +209,7 @@ version = "0.1.0"
dependencies = [
"futures",
"log 0.4.14",
"serde_json",
"serenity",
"thiserror",
"tokio",

@ -72,6 +72,7 @@ version = "0.1.0"
dependencies = [
"futures",
"log",
"serde_json",
"serenity",
"thiserror",
"tokio",

@ -11,4 +11,5 @@ serenity = "0.10.5"
tokio = "1.4.0"
thiserror = "1.0.24"
log = "0.4.14"
futures = "0.3.14"
futures = "0.3.14"
serde_json = "1.0.64"

@ -38,7 +38,8 @@ pub async fn close_menu(
menu.close(ctx.http()).await?;
let listeners = get_listeners_from_context(&ctx).await?;
let mut listeners_lock = listeners.lock().await;
listeners_lock.remove(&menu.message);
let message = menu.message.read().await;
listeners_lock.remove(&*message);
Ok(())
}
@ -49,7 +50,7 @@ async fn display_page(ctx: &Context, menu: &mut Menu<'_>) -> SerenityUtilsResult
.pages
.get(menu.current_page)
.ok_or(SerenityUtilsError::PageNotFound(menu.current_page))?;
let mut msg = menu.get_message(ctx).await?;
let mut msg = menu.get_message(ctx.http()).await?;
msg.edit(ctx, |e| {
e.0.clone_from(&mut page.0.clone());

@ -3,6 +3,7 @@ use crate::error::{SerenityUtilsError, SerenityUtilsResult};
use crate::menu::container::get_listeners_from_context;
use crate::menu::controls::{close_menu, next_page, previous_page};
use crate::menu::traits::EventDrivenMessage;
use crate::menu::EventDrivenMessagesRef;
use futures::FutureExt;
use serenity::async_trait;
use serenity::builder::CreateMessage;
@ -15,7 +16,7 @@ use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
use tokio::sync::{Mutex, RwLock};
pub static NEXT_PAGE_EMOJI: &str = "➡️";
pub static PREVIOUS_PAGE_EMOJI: &str = "⬅️";
@ -63,34 +64,102 @@ impl ActionContainer {
}
/// A menu message
#[derive(Clone)]
pub struct Menu<'a> {
pub message: MessageHandle,
pub message: Arc<RwLock<MessageHandle>>,
pub pages: Vec<CreateMessage<'a>>,
pub current_page: usize,
pub controls: HashMap<String, ActionContainer>,
pub timeout: Instant,
pub sticky: bool,
closed: bool,
listeners: EventDrivenMessagesRef,
}
impl<'a> Menu<'a> {
impl Menu<'_> {
/// Removes all reactions from the menu
pub(crate) async fn close(&mut self, http: &Http) -> SerenityUtilsResult<()> {
log::debug!("Closing menu...");
http.delete_message_reactions(self.message.channel_id, self.message.message_id)
let handle = self.message.read().await;
http.delete_message_reactions(handle.channel_id, handle.message_id)
.await?;
self.closed = true;
Ok(())
}
/// Returns the message of the menu
pub async fn get_message(&self, ctx: &Context) -> SerenityUtilsResult<Message> {
let msg = ctx
.http
.get_message(self.message.channel_id, self.message.message_id)
pub async fn get_message(&self, http: &Http) -> SerenityUtilsResult<Message> {
let handle = self.message.read().await;
let msg = http
.get_message(handle.channel_id, handle.message_id)
.await?;
Ok(msg)
}
/// Deep clones the menu to avoid lifetime conflicts with the stored CreateMessage's
fn deep_clone<'b>(&self) -> Menu<'b> {
let pages = self
.pages
.iter()
.map(|p| {
let mut page = p.clone();
let mut new_page: CreateMessage<'b> = CreateMessage::default();
new_page.0.clone_from(&mut page.0);
new_page
})
.collect::<Vec<CreateMessage<'b>>>();
Menu {
message: self.message.clone(),
sticky: self.sticky,
current_page: self.current_page,
listeners: self.listeners.clone(),
controls: self.controls.clone(),
timeout: self.timeout.clone(),
pages,
closed: self.closed,
}
}
/// Recreates the message completely
pub async fn recreate(&self, http: &Http) -> SerenityUtilsResult<()> {
log::debug!("Recreating message");
let mut handle = self.message.write().await;
log::debug!("Deleting original message");
http.delete_message(handle.channel_id, handle.message_id)
.await?;
log::debug!("Getting current page");
let current_page = self
.pages
.get(self.current_page)
.cloned()
.ok_or(SerenityUtilsError::PageNotFound(self.current_page))?;
log::debug!("Creating new message");
let message = http
.send_message(
handle.channel_id,
&serde_json::to_value(current_page.0).unwrap(),
)
.await?;
log::trace!("New message is {:?}", message);
handle.message_id = message.id.0;
let handle = (*handle).clone();
log::debug!("Deep cloning menu");
let menu: Menu<'static> = self.deep_clone();
let menu: Arc<Mutex<Box<dyn EventDrivenMessage>>> = Arc::new(Mutex::new(Box::new(menu)));
{
log::debug!("Adding new message to listeners");
let mut listeners_lock = self.listeners.lock().await;
listeners_lock.insert(handle, menu);
}
log::debug!("Message recreated");
Ok(())
}
}
#[async_trait]
@ -104,6 +173,22 @@ impl<'a> EventDrivenMessage for Menu<'a> {
if Instant::now() >= self.timeout {
log::debug!("Menu timout reached. Closing menu.");
self.close(http).await?;
} else if self.sticky {
log::debug!("Message is sticky. Checking for new messages in channel...");
let handle = {
let handle = self.message.read().await;
(*handle).clone()
};
let channel_id = ChannelId(handle.channel_id);
let messages = channel_id
.messages(http, |p| p.after(handle.message_id).limit(1))
.await?;
log::trace!("Messages are {:?}", messages);
if messages.len() > 0 {
log::debug!("New messages in channel. Recreating...");
self.recreate(http).await?;
}
}
Ok(())
@ -148,6 +233,7 @@ pub struct MenuBuilder {
current_page: usize,
controls: HashMap<String, ActionContainer>,
timeout: Duration,
sticky: bool,
}
impl Default for MenuBuilder {
@ -157,6 +243,7 @@ impl Default for MenuBuilder {
current_page: 0,
controls: HashMap::new(),
timeout: Duration::from_secs(60),
sticky: false,
}
}
}
@ -250,12 +337,20 @@ impl MenuBuilder {
self
}
/// If the message should be sticky and always be
/// the last one in the channel
pub fn sticky(mut self, value: bool) -> Self {
self.sticky = value;
self
}
/// builds the menu
pub async fn build(
self,
ctx: &Context,
channel_id: ChannelId,
) -> SerenityUtilsResult<MessageHandle> {
) -> SerenityUtilsResult<Arc<RwLock<MessageHandle>>> {
log::debug!("Building menu...");
let mut current_page = self
.pages
@ -276,14 +371,17 @@ impl MenuBuilder {
log::debug!("Creating menu...");
let message_handle = MessageHandle::new(message.channel_id, message.id);
let handle_lock = Arc::new(RwLock::new(message_handle));
let menu = Menu {
message: message_handle,
message: Arc::clone(&handle_lock),
pages: self.pages,
current_page: self.current_page,
controls: self.controls,
timeout: Instant::now() + self.timeout,
closed: false,
listeners: Arc::clone(&listeners),
sticky: self.sticky,
};
log::debug!("Storing menu to listeners...");
@ -301,6 +399,6 @@ impl MenuBuilder {
}
log::debug!("Menu successfully created.");
Ok(message_handle)
Ok(handle_lock)
}
}

@ -28,6 +28,7 @@ async fn current(ctx: &Context, msg: &Message) -> CommandResult {
let np_msg = create_now_playing_msg(ctx, msg.channel_id, &metadata).await?;
if let Some(old_np) = mem::replace(&mut queue_lock.now_playing_msg, Some(np_msg)) {
let old_np = old_np.read().await;
if let Ok(message) = old_np.get_message(&ctx.http).await {
let _ = message.delete(ctx).await;
}

@ -251,7 +251,7 @@ async fn play_next_in_queue(
let track = handler_lock.play_only_source(source);
log::trace!("Track is {:?}", track);
if let Some(np) = queue_lock.now_playing_msg {
if let Some(np) = &queue_lock.now_playing_msg {
if let Err(e) = update_now_playing_msg(http, np, track.metadata()).await {
log::error!("Failed to update now playing message: {:?}", e);
}
@ -259,6 +259,7 @@ async fn play_next_in_queue(
queue_lock.set_current(track);
} else {
if let Some(np) = mem::take(&mut queue_lock.now_playing_msg) {
let np = np.read().await;
if let Ok(message) = np.get_message(http).await {
let _ = message.delete(http).await;
}

@ -10,18 +10,23 @@ use bot_serenityutils::core::MessageHandle;
use bot_serenityutils::menu::MenuBuilder;
use serenity::builder::CreateMessage;
use serenity::client::Context;
use std::time::Duration;
use tokio::sync::RwLock;
/// Creates a new now playing message and returns the embed for that message
pub async fn create_now_playing_msg(
ctx: &Context,
channel_id: ChannelId,
meta: &Metadata,
) -> BotResult<MessageHandle> {
) -> BotResult<Arc<RwLock<MessageHandle>>> {
log::debug!("Creating now playing message");
let mut page = CreateMessage::default();
page.embed(|e| create_now_playing_embed(meta, e));
let handle = MenuBuilder::default()
.add_page(page)
.sticky(true)
.timeout(Duration::from_secs(60 * 60 * 24))
.build(ctx, channel_id)
.await?;
@ -31,9 +36,11 @@ pub async fn create_now_playing_msg(
/// Updates the now playing message with new content
pub async fn update_now_playing_msg(
http: &Arc<Http>,
handle: MessageHandle,
handle: &Arc<RwLock<MessageHandle>>,
meta: &Metadata,
) -> BotResult<()> {
log::debug!("Updating now playing message");
let handle = handle.read().await;
let mut message = handle.get_message(http).await?;
message
.edit(http, |m| m.embed(|e| create_now_playing_embed(meta, e)))

@ -8,13 +8,15 @@ use bot_coreutils::shuffle::Shuffle;
use crate::providers::music::responses::{PlaylistEntry, VideoInformation};
use crate::providers::music::youtube_dl;
use bot_serenityutils::core::MessageHandle;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct MusicQueue {
inner: VecDeque<Song>,
current: Option<TrackHandle>,
paused: bool,
pub now_playing_msg: Option<MessageHandle>,
pub now_playing_msg: Option<Arc<RwLock<MessageHandle>>>,
pub leave_flag: bool,
}

Loading…
Cancel
Save