Implement custom embed menu in subcrate
Signed-off-by: trivernis <trivernis@protonmail.com>pull/8/head
parent
4d290a5091
commit
dc8a84c51e
@ -0,0 +1 @@
|
|||||||
|
target
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "bot-serenityutils"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["trivernis <trivernis@protonmail.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serenity = "0.10.5"
|
||||||
|
tokio = "1.4.0"
|
||||||
|
thiserror = "1.0.24"
|
||||||
|
log = "0.4.14"
|
||||||
|
futures = "0.3.14"
|
@ -0,0 +1,4 @@
|
|||||||
|
use crate::menu::traits::EventDrivenMessage;
|
||||||
|
|
||||||
|
pub type MessageHandle = (u64, u64);
|
||||||
|
pub type BoxedEventDrivenMessage = Box<dyn EventDrivenMessage>;
|
@ -0,0 +1,15 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub type SerenityUtilsResult<T> = Result<T, SerenityUtilsError>;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SerenityUtilsError {
|
||||||
|
#[error("Serenity Error: {0}")]
|
||||||
|
SerenityError(#[from] serenity::Error),
|
||||||
|
|
||||||
|
#[error("Page {0} not found")]
|
||||||
|
PageNotFound(usize),
|
||||||
|
|
||||||
|
#[error("Serenity Utils not fully initialized")]
|
||||||
|
Uninitialized,
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
pub mod core;
|
||||||
|
pub mod error;
|
||||||
|
pub mod menu;
|
@ -0,0 +1,179 @@
|
|||||||
|
use crate::core::{BoxedEventDrivenMessage, MessageHandle};
|
||||||
|
use crate::error::{SerenityUtilsError, SerenityUtilsResult};
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::model::prelude::*;
|
||||||
|
use serenity::prelude::TypeMapKey;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
/// Container to store event driven messages in the serenity context data
|
||||||
|
pub struct EventDrivenMessageContainer;
|
||||||
|
pub type MessageRef = Arc<Mutex<BoxedEventDrivenMessage>>;
|
||||||
|
pub type EventDrivenMessagesRef = Arc<Mutex<HashMap<MessageHandle, MessageRef>>>;
|
||||||
|
|
||||||
|
impl TypeMapKey for EventDrivenMessageContainer {
|
||||||
|
type Value = EventDrivenMessagesRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts the loop to handle message updates
|
||||||
|
pub async fn start_update_loop(ctx: &Context) {
|
||||||
|
let event_messages = get_listeners_from_context(ctx)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get event message container");
|
||||||
|
let http = Arc::clone(&ctx.http);
|
||||||
|
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
log::trace!("Locking listener from update loop.");
|
||||||
|
let messages = {
|
||||||
|
let msgs_lock = event_messages.lock().await;
|
||||||
|
|
||||||
|
msgs_lock
|
||||||
|
.iter()
|
||||||
|
.map(|(k, v)| (*k, v.clone()))
|
||||||
|
.collect::<Vec<(MessageHandle, MessageRef)>>()
|
||||||
|
};
|
||||||
|
log::trace!("Listener locked.");
|
||||||
|
let mut frozen_messages = Vec::new();
|
||||||
|
|
||||||
|
for (key, msg) in messages {
|
||||||
|
let mut msg = msg.lock().await;
|
||||||
|
if let Err(e) = msg.update(&http).await {
|
||||||
|
log::error!("Failed to update message: {:?}", e);
|
||||||
|
}
|
||||||
|
if msg.is_frozen() {
|
||||||
|
frozen_messages.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let mut msgs_lock = event_messages.lock().await;
|
||||||
|
for key in frozen_messages {
|
||||||
|
msgs_lock.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::trace!("Listener unlocked");
|
||||||
|
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To be fired from the serenity handler when a message was deleted
|
||||||
|
pub async fn handle_message_delete(
|
||||||
|
ctx: &Context,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
message_id: MessageId,
|
||||||
|
) -> SerenityUtilsResult<()> {
|
||||||
|
let mut affected_messages = Vec::new();
|
||||||
|
{
|
||||||
|
let listeners = get_listeners_from_context(ctx).await?;
|
||||||
|
log::trace!("Locking listener from handle_message_delete.");
|
||||||
|
let mut listeners_lock = listeners.lock().await;
|
||||||
|
log::trace!("Listener locked.");
|
||||||
|
|
||||||
|
if let Some(msg) = listeners_lock.get(&(channel_id.0, message_id.0)) {
|
||||||
|
affected_messages.push(Arc::clone(msg));
|
||||||
|
listeners_lock.remove(&(channel_id.0, message_id.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::trace!("Listener unlocked");
|
||||||
|
for msg in affected_messages {
|
||||||
|
let mut msg = msg.lock().await;
|
||||||
|
msg.on_deleted(ctx).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// To be fired from the serenity handler when multiple messages were deleted
|
||||||
|
pub async fn handle_message_delete_bulk(
|
||||||
|
ctx: &Context,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
message_ids: &Vec<MessageId>,
|
||||||
|
) -> SerenityUtilsResult<()> {
|
||||||
|
let mut affected_messages = Vec::new();
|
||||||
|
{
|
||||||
|
let listeners = get_listeners_from_context(ctx).await?;
|
||||||
|
log::trace!("Locking listener from handle_message_delete_bulk.");
|
||||||
|
let mut listeners_lock = listeners.lock().await;
|
||||||
|
log::trace!("Listener locked.");
|
||||||
|
|
||||||
|
for message_id in message_ids {
|
||||||
|
if let Some(msg) = listeners_lock.get_mut(&(channel_id.0, message_id.0)) {
|
||||||
|
affected_messages.push(Arc::clone(msg));
|
||||||
|
listeners_lock.remove(&(channel_id.0, message_id.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::trace!("Listener unlocked");
|
||||||
|
for msg in affected_messages {
|
||||||
|
let mut msg = msg.lock().await;
|
||||||
|
msg.on_deleted(ctx).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fired when a reaction was added to a message
|
||||||
|
pub async fn handle_reaction_add(ctx: &Context, reaction: &Reaction) -> SerenityUtilsResult<()> {
|
||||||
|
let mut affected_messages = Vec::new();
|
||||||
|
{
|
||||||
|
let listeners = get_listeners_from_context(ctx).await?;
|
||||||
|
log::trace!("Locking listener from handle_reaction_add.");
|
||||||
|
let mut listeners_lock = listeners.lock().await;
|
||||||
|
log::trace!("Listener locked.");
|
||||||
|
|
||||||
|
let message_id = reaction.message_id;
|
||||||
|
let channel_id = reaction.channel_id;
|
||||||
|
|
||||||
|
if let Some(msg) = listeners_lock.get_mut(&(channel_id.0, message_id.0)) {
|
||||||
|
affected_messages.push(Arc::clone(&msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::trace!("Listener unlocked");
|
||||||
|
for msg in affected_messages {
|
||||||
|
let mut msg = msg.lock().await;
|
||||||
|
msg.on_reaction_add(ctx, reaction.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fired when a reaction was added to a message
|
||||||
|
pub async fn handle_reaction_remove(ctx: &Context, reaction: &Reaction) -> SerenityUtilsResult<()> {
|
||||||
|
let mut affected_messages = Vec::new();
|
||||||
|
{
|
||||||
|
let listeners = get_listeners_from_context(ctx).await?;
|
||||||
|
log::trace!("Locking listener from handle_reaction_remove.");
|
||||||
|
let mut listeners_lock = listeners.lock().await;
|
||||||
|
log::trace!("Listener locked.");
|
||||||
|
|
||||||
|
let message_id = reaction.message_id;
|
||||||
|
let channel_id = reaction.channel_id;
|
||||||
|
|
||||||
|
if let Some(msg) = listeners_lock.get_mut(&(channel_id.0, message_id.0)) {
|
||||||
|
affected_messages.push(Arc::clone(&msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::trace!("Listener unlocked");
|
||||||
|
for msg in affected_messages {
|
||||||
|
let mut msg = msg.lock().await;
|
||||||
|
msg.on_reaction_remove(ctx, reaction.clone()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_listeners_from_context(
|
||||||
|
ctx: &Context,
|
||||||
|
) -> SerenityUtilsResult<EventDrivenMessagesRef> {
|
||||||
|
let data = ctx.data.read().await;
|
||||||
|
let listeners = data
|
||||||
|
.get::<EventDrivenMessageContainer>()
|
||||||
|
.ok_or(SerenityUtilsError::Uninitialized)?;
|
||||||
|
log::trace!("Returning listener");
|
||||||
|
Ok(listeners.clone())
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
use crate::error::{SerenityUtilsError, SerenityUtilsResult};
|
||||||
|
use crate::menu::container::get_listeners_from_context;
|
||||||
|
use crate::menu::menu::Menu;
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::http::CacheHttp;
|
||||||
|
use serenity::model::channel::Reaction;
|
||||||
|
|
||||||
|
/// Shows the next page in the menu
|
||||||
|
pub async fn next_page(ctx: &Context, menu: &mut Menu<'_>, _: Reaction) -> SerenityUtilsResult<()> {
|
||||||
|
menu.current_page = (menu.current_page + 1) % menu.pages.len();
|
||||||
|
display_page(ctx, menu).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the previous page in the menu
|
||||||
|
pub async fn previous_page(
|
||||||
|
ctx: &Context,
|
||||||
|
menu: &mut Menu<'_>,
|
||||||
|
_: Reaction,
|
||||||
|
) -> SerenityUtilsResult<()> {
|
||||||
|
if menu.current_page == 0 {
|
||||||
|
menu.current_page = menu.pages.len() - 1;
|
||||||
|
} else {
|
||||||
|
menu.current_page = menu.current_page - 1;
|
||||||
|
}
|
||||||
|
display_page(ctx, menu).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows the previous page in the menu
|
||||||
|
pub async fn close_menu(
|
||||||
|
ctx: &Context,
|
||||||
|
menu: &mut Menu<'_>,
|
||||||
|
_: Reaction,
|
||||||
|
) -> SerenityUtilsResult<()> {
|
||||||
|
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);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays the menu page
|
||||||
|
async fn display_page(ctx: &Context, menu: &mut Menu<'_>) -> SerenityUtilsResult<()> {
|
||||||
|
let page = menu
|
||||||
|
.pages
|
||||||
|
.get(menu.current_page)
|
||||||
|
.ok_or(SerenityUtilsError::PageNotFound(menu.current_page))?;
|
||||||
|
let mut msg = menu.get_message(ctx).await?;
|
||||||
|
|
||||||
|
msg.edit(ctx, |e| {
|
||||||
|
e.0.clone_from(&mut page.0.clone());
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -0,0 +1,287 @@
|
|||||||
|
use crate::core::MessageHandle;
|
||||||
|
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 futures::FutureExt;
|
||||||
|
use serenity::async_trait;
|
||||||
|
use serenity::builder::CreateMessage;
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::http::Http;
|
||||||
|
use serenity::model::channel::{Message, Reaction, ReactionType};
|
||||||
|
use serenity::model::id::ChannelId;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
pub static NEXT_PAGE_EMOJI: &str = "➡️";
|
||||||
|
pub static PREVIOUS_PAGE_EMOJI: &str = "⬅️";
|
||||||
|
pub static CLOSE_MENU_EMOJI: &str = "❌";
|
||||||
|
|
||||||
|
pub type ControlAction = Arc<
|
||||||
|
dyn for<'b> Fn(
|
||||||
|
&'b Context,
|
||||||
|
&'b mut Menu<'_>,
|
||||||
|
Reaction,
|
||||||
|
) -> Pin<Box<dyn Future<Output = SerenityUtilsResult<()>> + Send + 'b>>
|
||||||
|
+ Send
|
||||||
|
+ Sync,
|
||||||
|
>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ActionContainer {
|
||||||
|
inner: ControlAction,
|
||||||
|
position: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionContainer {
|
||||||
|
/// Creates a new control action
|
||||||
|
pub fn new(position: usize, inner: ControlAction) -> Self {
|
||||||
|
Self { inner, position }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the action
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
ctx: &Context,
|
||||||
|
menu: &mut Menu<'_>,
|
||||||
|
reaction: Reaction,
|
||||||
|
) -> SerenityUtilsResult<()> {
|
||||||
|
self.inner.clone()(ctx, menu, reaction).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A menu message
|
||||||
|
pub struct Menu<'a> {
|
||||||
|
pub message: MessageHandle,
|
||||||
|
pub pages: Vec<CreateMessage<'a>>,
|
||||||
|
pub current_page: usize,
|
||||||
|
pub controls: HashMap<String, ActionContainer>,
|
||||||
|
pub timeout: Instant,
|
||||||
|
closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Menu<'a> {
|
||||||
|
/// 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.0, self.message.1)
|
||||||
|
.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.0, self.message.1).await?;
|
||||||
|
|
||||||
|
Ok(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<'a> EventDrivenMessage for Menu<'a> {
|
||||||
|
fn is_frozen(&self) -> bool {
|
||||||
|
self.closed
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update(&mut self, http: &Http) -> SerenityUtilsResult<()> {
|
||||||
|
log::trace!("Checking for menu timeout");
|
||||||
|
if Instant::now() >= self.timeout {
|
||||||
|
log::debug!("Menu timout reached. Closing menu.");
|
||||||
|
self.close(http).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_deleted(&mut self, _: &Context) -> SerenityUtilsResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_reaction_add(
|
||||||
|
&mut self,
|
||||||
|
ctx: &Context,
|
||||||
|
reaction: Reaction,
|
||||||
|
) -> SerenityUtilsResult<()> {
|
||||||
|
log::debug!("Reaction to menu added");
|
||||||
|
let current_user = ctx.http.get_current_user().await?;
|
||||||
|
|
||||||
|
if reaction.user_id.unwrap().0 == current_user.id.0 {
|
||||||
|
log::debug!("Reaction is from current user.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let emoji_string = reaction.emoji.as_data();
|
||||||
|
|
||||||
|
log::debug!("Deleting user reaction.");
|
||||||
|
reaction.delete(ctx).await?;
|
||||||
|
if let Some(control) = self.controls.get(&emoji_string).cloned() {
|
||||||
|
log::debug!("Running control");
|
||||||
|
control.run(ctx, self, reaction).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_reaction_remove(&mut self, _: &Context, _: Reaction) -> SerenityUtilsResult<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A builder for messages
|
||||||
|
pub struct MenuBuilder {
|
||||||
|
pages: Vec<CreateMessage<'static>>,
|
||||||
|
current_page: usize,
|
||||||
|
controls: HashMap<String, ActionContainer>,
|
||||||
|
timeout: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MenuBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pages: vec![],
|
||||||
|
current_page: 0,
|
||||||
|
controls: HashMap::new(),
|
||||||
|
timeout: Duration::from_secs(60),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MenuBuilder {
|
||||||
|
/// Creates a new paginaton menu
|
||||||
|
pub fn new_paginator() -> Self {
|
||||||
|
log::debug!("Creating new paginator");
|
||||||
|
let mut controls = HashMap::new();
|
||||||
|
controls.insert(
|
||||||
|
PREVIOUS_PAGE_EMOJI.to_string(),
|
||||||
|
ActionContainer::new(0, Arc::new(|c, m, r| previous_page(c, m, r).boxed())),
|
||||||
|
);
|
||||||
|
controls.insert(
|
||||||
|
CLOSE_MENU_EMOJI.to_string(),
|
||||||
|
ActionContainer::new(1, Arc::new(|c, m, r| close_menu(c, m, r).boxed())),
|
||||||
|
);
|
||||||
|
controls.insert(
|
||||||
|
NEXT_PAGE_EMOJI.to_string(),
|
||||||
|
ActionContainer::new(2, Arc::new(|c, m, r| next_page(c, m, r).boxed())),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
controls,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a page to the message builder
|
||||||
|
pub fn add_page(mut self, page: CreateMessage<'static>) -> Self {
|
||||||
|
self.pages.push(page);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds multiple pages to the message
|
||||||
|
pub fn add_pages<I>(mut self, pages: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = CreateMessage<'static>>,
|
||||||
|
{
|
||||||
|
let mut pages = pages.into_iter().collect();
|
||||||
|
self.pages.append(&mut pages);
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a single control to the message
|
||||||
|
pub fn add_control<S: ToString>(
|
||||||
|
mut self,
|
||||||
|
position: usize,
|
||||||
|
emoji: S,
|
||||||
|
action: ControlAction,
|
||||||
|
) -> Self {
|
||||||
|
self.controls
|
||||||
|
.insert(emoji.to_string(), ActionContainer::new(position, action));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a single control to the message
|
||||||
|
pub fn add_controls<S, I>(mut self, controls: I) -> Self
|
||||||
|
where
|
||||||
|
S: ToString,
|
||||||
|
I: IntoIterator<Item = (usize, S, ControlAction)>,
|
||||||
|
{
|
||||||
|
for (position, emoji, action) in controls {
|
||||||
|
self.controls
|
||||||
|
.insert(emoji.to_string(), ActionContainer::new(position, action));
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the timeout for the message
|
||||||
|
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.timeout = timeout;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the start page of the message
|
||||||
|
pub fn start_page(mut self, page: usize) -> Self {
|
||||||
|
self.current_page = page;
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// builds the menu
|
||||||
|
pub async fn build(self, ctx: &Context, channel_id: ChannelId) -> SerenityUtilsResult<()> {
|
||||||
|
log::debug!("Building menu...");
|
||||||
|
let mut current_page = self
|
||||||
|
.pages
|
||||||
|
.get(self.current_page)
|
||||||
|
.ok_or(SerenityUtilsError::PageNotFound(self.current_page))?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let message = channel_id.send_message(ctx, |_| &mut current_page).await?;
|
||||||
|
log::trace!("Message is {:?}", message);
|
||||||
|
let listeners = get_listeners_from_context(ctx).await?;
|
||||||
|
log::debug!("Sorting controls...");
|
||||||
|
let mut controls = self
|
||||||
|
.controls
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.collect::<Vec<(String, ActionContainer)>>();
|
||||||
|
controls.sort_by_key(|(_, a)| a.position);
|
||||||
|
|
||||||
|
log::debug!("Creating menu...");
|
||||||
|
let menu = Menu {
|
||||||
|
message: (message.channel_id.0, message.id.0),
|
||||||
|
pages: self.pages,
|
||||||
|
current_page: self.current_page,
|
||||||
|
controls: self.controls,
|
||||||
|
timeout: Instant::now() + self.timeout,
|
||||||
|
closed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
log::debug!("Storing menu to listeners...");
|
||||||
|
{
|
||||||
|
let mut listeners_lock = listeners.lock().await;
|
||||||
|
log::trace!("Listeners locked.");
|
||||||
|
listeners_lock.insert(
|
||||||
|
(message.channel_id.0, message.id.0),
|
||||||
|
Arc::new(Mutex::new(Box::new(menu))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("Adding controls...");
|
||||||
|
for (emoji, _) in controls {
|
||||||
|
message
|
||||||
|
.react(ctx, ReactionType::Unicode(emoji.clone()))
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
log::debug!("Menu successfully created.");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
pub(crate) mod container;
|
||||||
|
pub(crate) mod controls;
|
||||||
|
pub(crate) mod menu;
|
||||||
|
pub(crate) mod traits;
|
||||||
|
|
||||||
|
pub use container::*;
|
||||||
|
pub use controls::*;
|
||||||
|
pub use menu::{
|
||||||
|
ActionContainer, ControlAction, Menu, MenuBuilder, CLOSE_MENU_EMOJI, NEXT_PAGE_EMOJI,
|
||||||
|
PREVIOUS_PAGE_EMOJI,
|
||||||
|
};
|
||||||
|
pub use traits::EventDrivenMessage;
|
@ -0,0 +1,30 @@
|
|||||||
|
use crate::error::SerenityUtilsResult;
|
||||||
|
use serenity::client::Context;
|
||||||
|
use serenity::http::Http;
|
||||||
|
use serenity::{async_trait, model::prelude::*};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait EventDrivenMessage: Send + Sync {
|
||||||
|
/// Returns if a message has been frozen and won't handle any further events
|
||||||
|
fn is_frozen(&self) -> bool;
|
||||||
|
|
||||||
|
/// Fired periodically
|
||||||
|
async fn update(&mut self, http: &Http) -> SerenityUtilsResult<()>;
|
||||||
|
|
||||||
|
/// Fired when the message was deleted
|
||||||
|
async fn on_deleted(&mut self, ctx: &Context) -> SerenityUtilsResult<()>;
|
||||||
|
|
||||||
|
/// Fired when a reaction was added to the message
|
||||||
|
async fn on_reaction_add(
|
||||||
|
&mut self,
|
||||||
|
ctx: &Context,
|
||||||
|
reaction: Reaction,
|
||||||
|
) -> SerenityUtilsResult<()>;
|
||||||
|
|
||||||
|
/// Fired when a reaction was removed from the message
|
||||||
|
async fn on_reaction_remove(
|
||||||
|
&mut self,
|
||||||
|
ctx: &Context,
|
||||||
|
reaction: Reaction,
|
||||||
|
) -> SerenityUtilsResult<()>;
|
||||||
|
}
|
Loading…
Reference in New Issue