commit
5b14c17878
@ -1 +0,0 @@
|
|||||||
target
|
|
File diff suppressed because it is too large
Load Diff
@ -1,15 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bot-serenityutils"
|
|
||||||
version = "0.2.4"
|
|
||||||
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"
|
|
||||||
serde_json = "1.0.64"
|
|
@ -1,44 +0,0 @@
|
|||||||
use crate::error::SerenityUtilsResult;
|
|
||||||
use crate::menu::traits::EventDrivenMessage;
|
|
||||||
use serenity::http::Http;
|
|
||||||
use serenity::model::channel::Message;
|
|
||||||
use serenity::model::id::{ChannelId, MessageId};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub static SHORT_TIMEOUT: Duration = Duration::from_secs(5);
|
|
||||||
pub static MEDIUM_TIMEOUT: Duration = Duration::from_secs(20);
|
|
||||||
pub static LONG_TIMEOUT: Duration = Duration::from_secs(60);
|
|
||||||
pub static EXTRA_LONG_TIMEOUT: Duration = Duration::from_secs(600);
|
|
||||||
|
|
||||||
pub type BoxedEventDrivenMessage = Box<dyn EventDrivenMessage>;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq, Hash)]
|
|
||||||
pub struct MessageHandle {
|
|
||||||
pub channel_id: u64,
|
|
||||||
pub message_id: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageHandle {
|
|
||||||
/// Creates a new message handle
|
|
||||||
pub fn new(channel_id: ChannelId, message_id: MessageId) -> Self {
|
|
||||||
Self {
|
|
||||||
message_id: message_id.0,
|
|
||||||
channel_id: channel_id.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new message handle from raw ids
|
|
||||||
pub fn from_raw_ids(channel_id: u64, message_id: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
message_id,
|
|
||||||
channel_id,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the message object of the handle
|
|
||||||
pub async fn get_message(&self, http: &Arc<Http>) -> SerenityUtilsResult<Message> {
|
|
||||||
let msg = http.get_message(self.channel_id, self.message_id).await?;
|
|
||||||
Ok(msg)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
use crate::core::MessageHandle;
|
|
||||||
use crate::error::SerenityUtilsResult;
|
|
||||||
use serenity::builder::CreateMessage;
|
|
||||||
use serenity::http::Http;
|
|
||||||
use serenity::model::channel::Message;
|
|
||||||
use serenity::model::id::ChannelId;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub struct EphemeralMessage;
|
|
||||||
|
|
||||||
impl EphemeralMessage {
|
|
||||||
/// Ensures that an already existing message is
|
|
||||||
/// deleted after a certain amount of time
|
|
||||||
pub async fn create_from_message(
|
|
||||||
http: &Arc<Http>,
|
|
||||||
message: &Message,
|
|
||||||
timeout: Duration,
|
|
||||||
) -> SerenityUtilsResult<()> {
|
|
||||||
log::debug!("Creating ephemeral message from existing message");
|
|
||||||
let handle = MessageHandle::new(message.channel_id, message.id);
|
|
||||||
let http = Arc::clone(&http);
|
|
||||||
|
|
||||||
log::debug!("Starting delete task");
|
|
||||||
tokio::spawn(async move {
|
|
||||||
log::debug!("Waiting for timeout to pass");
|
|
||||||
tokio::time::sleep(timeout).await;
|
|
||||||
log::debug!("Deleting ephemeral message");
|
|
||||||
if let Err(e) = http
|
|
||||||
.delete_message(handle.channel_id, handle.message_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
log::error!("Failed to delete ephemeral message {:?}: {}", handle, e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new message that is deleted after a certain amount of time
|
|
||||||
pub async fn create<'a, F>(
|
|
||||||
http: &Arc<Http>,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
timeout: Duration,
|
|
||||||
f: F,
|
|
||||||
) -> SerenityUtilsResult<Message>
|
|
||||||
where
|
|
||||||
F: for<'b> FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>,
|
|
||||||
{
|
|
||||||
log::debug!("Creating new ephemeral message");
|
|
||||||
let msg = channel_id.send_message(http, f).await?;
|
|
||||||
Self::create_from_message(http, &msg, timeout).await?;
|
|
||||||
|
|
||||||
Ok(msg)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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,
|
|
||||||
|
|
||||||
#[error("{0}")]
|
|
||||||
Msg(String),
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
pub mod core;
|
|
||||||
pub mod ephemeral_message;
|
|
||||||
pub mod error;
|
|
||||||
pub mod macros;
|
|
||||||
pub mod menu;
|
|
||||||
|
|
||||||
pub static VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
@ -1,16 +0,0 @@
|
|||||||
/// Forwards the error directly to the user
|
|
||||||
/// without having to accept it in any handler.
|
|
||||||
/// Can only be used in async functions that return a Result.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! forward_error {
|
|
||||||
($ctx:expr,$channel_id:expr,$result:expr) => {
|
|
||||||
match $result {
|
|
||||||
Err(e) => {
|
|
||||||
use bot_serenityutils::{core::SHORT_TIMEOUT, ephemeral_message::EphemeralMessage};
|
|
||||||
$channel_id.say($ctx, format!("‼️ {}", e)).await?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
Ok(v) => v,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,181 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static UPDATE_INTERVAL_SECS: u64 = 5;
|
|
||||||
|
|
||||||
/// 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(UPDATE_INTERVAL_SECS)).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.");
|
|
||||||
|
|
||||||
let handle = MessageHandle::new(channel_id, message_id);
|
|
||||||
if let Some(msg) = listeners_lock.get(&handle) {
|
|
||||||
affected_messages.push(Arc::clone(msg));
|
|
||||||
listeners_lock.remove(&handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
let handle = MessageHandle::new(channel_id, *message_id);
|
|
||||||
if let Some(msg) = listeners_lock.get_mut(&handle) {
|
|
||||||
affected_messages.push(Arc::clone(msg));
|
|
||||||
listeners_lock.remove(&handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 handle = MessageHandle::new(reaction.channel_id, reaction.message_id);
|
|
||||||
|
|
||||||
if let Some(msg) = listeners_lock.get_mut(&handle) {
|
|
||||||
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 handle = MessageHandle::new(reaction.channel_id, reaction.message_id);
|
|
||||||
|
|
||||||
if let Some(msg) = listeners_lock.get_mut(&handle) {
|
|
||||||
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())
|
|
||||||
}
|
|
@ -1,145 +0,0 @@
|
|||||||
use crate::error::{SerenityUtilsError, SerenityUtilsResult};
|
|
||||||
use crate::menu::container::get_listeners_from_context;
|
|
||||||
use crate::menu::menu::Menu;
|
|
||||||
use crate::menu::typedata::HelpActiveContainer;
|
|
||||||
use crate::menu::ActionContainer;
|
|
||||||
use serde_json::json;
|
|
||||||
use serde_json::Value;
|
|
||||||
use serenity::client::Context;
|
|
||||||
use serenity::http::CacheHttp;
|
|
||||||
use serenity::model::channel::Reaction;
|
|
||||||
use std::sync::atomic::Ordering;
|
|
||||||
|
|
||||||
/// Shows the next page in the menu
|
|
||||||
pub async fn next_page(ctx: &Context, menu: &mut Menu<'_>, _: Reaction) -> SerenityUtilsResult<()> {
|
|
||||||
log::debug!("Showing next page");
|
|
||||||
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<()> {
|
|
||||||
log::debug!("Showing previous page");
|
|
||||||
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<()> {
|
|
||||||
log::debug!("Closing menu");
|
|
||||||
menu.close(ctx.http()).await?;
|
|
||||||
let listeners = get_listeners_from_context(&ctx).await?;
|
|
||||||
let mut listeners_lock = listeners.lock().await;
|
|
||||||
let message = menu.message.read().await;
|
|
||||||
listeners_lock.remove(&*message);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn toggle_help(
|
|
||||||
ctx: &Context,
|
|
||||||
menu: &mut Menu<'_>,
|
|
||||||
_: Reaction,
|
|
||||||
) -> SerenityUtilsResult<()> {
|
|
||||||
log::debug!("Displaying help");
|
|
||||||
let show_help = menu
|
|
||||||
.data
|
|
||||||
.get::<HelpActiveContainer>()
|
|
||||||
.expect("Missing HelpActiveContainer in menu data")
|
|
||||||
.clone();
|
|
||||||
|
|
||||||
if show_help.load(Ordering::Relaxed) {
|
|
||||||
display_page(ctx, menu).await?;
|
|
||||||
show_help.store(false, Ordering::Relaxed);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let page = menu
|
|
||||||
.pages
|
|
||||||
.get(menu.current_page)
|
|
||||||
.ok_or(SerenityUtilsError::PageNotFound(menu.current_page))?
|
|
||||||
.get()
|
|
||||||
.await?;
|
|
||||||
let mut message = menu.get_message(ctx.http()).await?;
|
|
||||||
log::debug!("Building help entries");
|
|
||||||
let mut help_entries = menu
|
|
||||||
.help_entries
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(e, h)| Some((menu.controls.get(e)?, e, h)))
|
|
||||||
.collect::<Vec<(&ActionContainer, &String, &String)>>();
|
|
||||||
help_entries.sort_by_key(|(c, _, _)| c.position());
|
|
||||||
let help_message = help_entries
|
|
||||||
.into_iter()
|
|
||||||
.map(|(_, e, h)| format!(" - {} {}", e, h))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n");
|
|
||||||
log::trace!("Help message is {}", help_message);
|
|
||||||
|
|
||||||
message
|
|
||||||
.edit(ctx, |m| {
|
|
||||||
m.0.clone_from(&mut page.0.clone());
|
|
||||||
|
|
||||||
if let Some(embed) = m.0.get_mut("embed") {
|
|
||||||
let embed = embed.as_object_mut().unwrap();
|
|
||||||
let fields = embed
|
|
||||||
.entry("fields")
|
|
||||||
.or_insert_with(|| Value::Array(vec![]));
|
|
||||||
if let Value::Array(ref mut inner) = *fields {
|
|
||||||
inner.push(json!({
|
|
||||||
"inline": false,
|
|
||||||
"name": "Help".to_string(),
|
|
||||||
"value": help_message,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m.embed(|e| {
|
|
||||||
e.field("Help", help_message, false);
|
|
||||||
|
|
||||||
e
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
m
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
log::debug!("Help message displayed");
|
|
||||||
show_help.store(true, Ordering::Relaxed);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Displays the menu page
|
|
||||||
pub async fn display_page(ctx: &Context, menu: &mut Menu<'_>) -> SerenityUtilsResult<()> {
|
|
||||||
log::debug!("Displaying page {}", menu.current_page);
|
|
||||||
let page = menu
|
|
||||||
.pages
|
|
||||||
.get(menu.current_page)
|
|
||||||
.ok_or(SerenityUtilsError::PageNotFound(menu.current_page))?
|
|
||||||
.get()
|
|
||||||
.await?;
|
|
||||||
let mut msg = menu.get_message(ctx.http()).await?;
|
|
||||||
|
|
||||||
msg.edit(ctx, |e| {
|
|
||||||
e.0.clone_from(&mut page.0.clone());
|
|
||||||
e
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
log::debug!("Page displayed");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
@ -1,447 +0,0 @@
|
|||||||
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, toggle_help};
|
|
||||||
use crate::menu::traits::EventDrivenMessage;
|
|
||||||
use crate::menu::typedata::HelpActiveContainer;
|
|
||||||
use crate::menu::{EventDrivenMessagesRef, Page};
|
|
||||||
use futures::FutureExt;
|
|
||||||
use serenity::async_trait;
|
|
||||||
use serenity::client::Context;
|
|
||||||
use serenity::http::Http;
|
|
||||||
use serenity::model::channel::{Message, Reaction, ReactionType};
|
|
||||||
use serenity::model::id::ChannelId;
|
|
||||||
use serenity::prelude::{TypeMap, TypeMapKey};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use tokio::sync::{Mutex, RwLock};
|
|
||||||
|
|
||||||
pub static NEXT_PAGE_EMOJI: &str = "➡️";
|
|
||||||
pub static PREVIOUS_PAGE_EMOJI: &str = "⬅️";
|
|
||||||
pub static CLOSE_MENU_EMOJI: &str = "❌";
|
|
||||||
pub static HELP_EMOJI: &str = "❔";
|
|
||||||
|
|
||||||
pub type ControlActionResult<'b> =
|
|
||||||
Pin<Box<dyn Future<Output = SerenityUtilsResult<()>> + Send + 'b>>;
|
|
||||||
|
|
||||||
pub type ControlActionArc = Arc<
|
|
||||||
dyn for<'b> Fn(&'b Context, &'b mut Menu<'_>, Reaction) -> ControlActionResult<'b>
|
|
||||||
+ Send
|
|
||||||
+ Sync,
|
|
||||||
>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ActionContainer {
|
|
||||||
inner: ControlActionArc,
|
|
||||||
position: isize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActionContainer {
|
|
||||||
/// Creates a new control action
|
|
||||||
pub fn new<F: 'static>(position: isize, callback: F) -> Self
|
|
||||||
where
|
|
||||||
F: for<'b> Fn(&'b Context, &'b mut Menu<'_>, Reaction) -> ControlActionResult<'b>
|
|
||||||
+ Send
|
|
||||||
+ Sync,
|
|
||||||
{
|
|
||||||
Self {
|
|
||||||
inner: Arc::new(callback),
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the position of the action
|
|
||||||
pub fn position(&self) -> isize {
|
|
||||||
self.position
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A menu message
|
|
||||||
pub struct Menu<'a> {
|
|
||||||
pub message: Arc<RwLock<MessageHandle>>,
|
|
||||||
pub pages: Vec<Page<'a>>,
|
|
||||||
pub current_page: usize,
|
|
||||||
pub controls: HashMap<String, ActionContainer>,
|
|
||||||
pub timeout: Instant,
|
|
||||||
pub sticky: bool,
|
|
||||||
pub data: TypeMap,
|
|
||||||
pub help_entries: HashMap<String, String>,
|
|
||||||
closed: bool,
|
|
||||||
listeners: EventDrivenMessagesRef,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Menu<'_> {
|
|
||||||
/// Removes all reactions from the menu
|
|
||||||
pub(crate) async fn close(&mut self, http: &Http) -> SerenityUtilsResult<()> {
|
|
||||||
log::debug!("Closing menu...");
|
|
||||||
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, http: &Http) -> SerenityUtilsResult<Message> {
|
|
||||||
let handle = self.message.read().await;
|
|
||||||
let msg = http
|
|
||||||
.get_message(handle.channel_id, handle.message_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recreates the message completely
|
|
||||||
pub async fn recreate(&self, http: &Http) -> SerenityUtilsResult<()> {
|
|
||||||
log::debug!("Recreating message");
|
|
||||||
|
|
||||||
let old_handle = {
|
|
||||||
let handle = self.message.read().await;
|
|
||||||
(*handle).clone()
|
|
||||||
};
|
|
||||||
log::debug!("Getting current page");
|
|
||||||
let current_page = self
|
|
||||||
.pages
|
|
||||||
.get(self.current_page)
|
|
||||||
.cloned()
|
|
||||||
.ok_or(SerenityUtilsError::PageNotFound(self.current_page))?
|
|
||||||
.get()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
log::debug!("Creating new message");
|
|
||||||
let message = http
|
|
||||||
.send_message(
|
|
||||||
old_handle.channel_id,
|
|
||||||
&serde_json::to_value(current_page.0).unwrap(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let mut controls = self
|
|
||||||
.controls
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.collect::<Vec<(String, ActionContainer)>>();
|
|
||||||
controls.sort_by_key(|(_, a)| a.position);
|
|
||||||
|
|
||||||
for emoji in controls.into_iter().map(|(e, _)| e) {
|
|
||||||
http.create_reaction(
|
|
||||||
message.channel_id.0,
|
|
||||||
message.id.0,
|
|
||||||
&ReactionType::Unicode(emoji.clone()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
log::trace!("New message is {:?}", message);
|
|
||||||
|
|
||||||
let new_handle = {
|
|
||||||
let mut handle = self.message.write().await;
|
|
||||||
handle.message_id = message.id.0;
|
|
||||||
(*handle).clone()
|
|
||||||
};
|
|
||||||
{
|
|
||||||
log::debug!("Changing key of message");
|
|
||||||
let mut listeners_lock = self.listeners.lock().await;
|
|
||||||
let menu = listeners_lock.remove(&old_handle).unwrap();
|
|
||||||
listeners_lock.insert(new_handle, menu);
|
|
||||||
}
|
|
||||||
log::debug!("Deleting original message");
|
|
||||||
http.delete_message(old_handle.channel_id, old_handle.message_id)
|
|
||||||
.await?;
|
|
||||||
log::debug!("Message recreated");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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?;
|
|
||||||
} 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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A builder for messages
|
|
||||||
pub struct MenuBuilder {
|
|
||||||
pages: Vec<Page<'static>>,
|
|
||||||
current_page: usize,
|
|
||||||
controls: HashMap<String, ActionContainer>,
|
|
||||||
timeout: Duration,
|
|
||||||
sticky: bool,
|
|
||||||
data: TypeMap,
|
|
||||||
help_entries: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MenuBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
pages: vec![],
|
|
||||||
current_page: 0,
|
|
||||||
controls: HashMap::new(),
|
|
||||||
timeout: Duration::from_secs(60),
|
|
||||||
sticky: false,
|
|
||||||
data: TypeMap::new(),
|
|
||||||
help_entries: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MenuBuilder {
|
|
||||||
/// Creates a new paginaton menu
|
|
||||||
pub fn new_paginator() -> Self {
|
|
||||||
log::debug!("Creating new paginator");
|
|
||||||
let mut controls = HashMap::new();
|
|
||||||
let mut help_entries = HashMap::new();
|
|
||||||
controls.insert(
|
|
||||||
PREVIOUS_PAGE_EMOJI.to_string(),
|
|
||||||
ActionContainer::new(0, |c, m, r| previous_page(c, m, r).boxed()),
|
|
||||||
);
|
|
||||||
help_entries.insert(
|
|
||||||
PREVIOUS_PAGE_EMOJI.to_string(),
|
|
||||||
"Displays the previous page".to_string(),
|
|
||||||
);
|
|
||||||
controls.insert(
|
|
||||||
CLOSE_MENU_EMOJI.to_string(),
|
|
||||||
ActionContainer::new(1, |c, m, r| close_menu(c, m, r).boxed()),
|
|
||||||
);
|
|
||||||
help_entries.insert(
|
|
||||||
CLOSE_MENU_EMOJI.to_string(),
|
|
||||||
"Closes the menu buttons".to_string(),
|
|
||||||
);
|
|
||||||
controls.insert(
|
|
||||||
NEXT_PAGE_EMOJI.to_string(),
|
|
||||||
ActionContainer::new(2, |c, m, r| next_page(c, m, r).boxed()),
|
|
||||||
);
|
|
||||||
help_entries.insert(
|
|
||||||
NEXT_PAGE_EMOJI.to_string(),
|
|
||||||
"Displays the next page".to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
controls,
|
|
||||||
help_entries,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a page to the message builder
|
|
||||||
pub fn add_page(mut self, page: Page<'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 = Page<'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, F: 'static>(mut self, position: isize, emoji: S, action: F) -> Self
|
|
||||||
where
|
|
||||||
S: ToString,
|
|
||||||
F: for<'b> Fn(&'b Context, &'b mut Menu<'_>, Reaction) -> ControlActionResult<'b>
|
|
||||||
+ Send
|
|
||||||
+ Sync,
|
|
||||||
{
|
|
||||||
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 = (isize, S, ControlActionArc)>,
|
|
||||||
{
|
|
||||||
for (position, emoji, action) in controls {
|
|
||||||
self.controls.insert(
|
|
||||||
emoji.to_string(),
|
|
||||||
ActionContainer {
|
|
||||||
position,
|
|
||||||
inner: 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds data to the menu typemap
|
|
||||||
pub fn add_data<T>(mut self, value: T::Value) -> Self
|
|
||||||
where
|
|
||||||
T: TypeMapKey,
|
|
||||||
{
|
|
||||||
self.data.insert::<T>(value);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds a help entry
|
|
||||||
pub fn add_help<S: ToString>(mut self, button: S, help: S) -> Self {
|
|
||||||
self.help_entries
|
|
||||||
.insert(button.to_string(), help.to_string());
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Turns showing help for buttons on
|
|
||||||
pub fn show_help(self) -> Self {
|
|
||||||
self.add_control(100, HELP_EMOJI, |c, m, r| Box::pin(toggle_help(c, m, r)))
|
|
||||||
.add_data::<HelpActiveContainer>(Arc::new(AtomicBool::new(false)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// builds the menu
|
|
||||||
pub async fn build(
|
|
||||||
self,
|
|
||||||
ctx: &Context,
|
|
||||||
channel_id: ChannelId,
|
|
||||||
) -> SerenityUtilsResult<Arc<RwLock<MessageHandle>>> {
|
|
||||||
log::debug!("Building menu...");
|
|
||||||
let mut current_page = self
|
|
||||||
.pages
|
|
||||||
.get(self.current_page)
|
|
||||||
.ok_or(SerenityUtilsError::PageNotFound(self.current_page))?
|
|
||||||
.clone()
|
|
||||||
.get()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
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 message_handle = MessageHandle::new(message.channel_id, message.id);
|
|
||||||
let handle_lock = Arc::new(RwLock::new(message_handle));
|
|
||||||
|
|
||||||
let menu = Menu {
|
|
||||||
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,
|
|
||||||
data: self.data,
|
|
||||||
help_entries: self.help_entries,
|
|
||||||
};
|
|
||||||
|
|
||||||
log::debug!("Storing menu to listeners...");
|
|
||||||
{
|
|
||||||
let mut listeners_lock = listeners.lock().await;
|
|
||||||
log::trace!("Listeners locked.");
|
|
||||||
listeners_lock.insert(message_handle, 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(handle_lock)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
pub(crate) mod container;
|
|
||||||
pub(crate) mod controls;
|
|
||||||
pub(crate) mod menu;
|
|
||||||
pub(crate) mod page;
|
|
||||||
pub(crate) mod traits;
|
|
||||||
pub(crate) mod typedata;
|
|
||||||
|
|
||||||
pub use container::*;
|
|
||||||
pub use controls::*;
|
|
||||||
pub use menu::{
|
|
||||||
ActionContainer, ControlActionArc, Menu, MenuBuilder, CLOSE_MENU_EMOJI, NEXT_PAGE_EMOJI,
|
|
||||||
PREVIOUS_PAGE_EMOJI,
|
|
||||||
};
|
|
||||||
pub use page::*;
|
|
||||||
|
|
||||||
pub use traits::EventDrivenMessage;
|
|
@ -1,40 +0,0 @@
|
|||||||
use crate::error::SerenityUtilsResult;
|
|
||||||
use serenity::builder::CreateMessage;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub type MessageBuildOutput<'b> =
|
|
||||||
Pin<Box<dyn Future<Output = SerenityUtilsResult<CreateMessage<'b>>> + Send + 'b>>;
|
|
||||||
pub type MessageBuilderFn<'b> = Arc<dyn Fn() -> MessageBuildOutput<'b> + Send + Sync>;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
/// A page that stores a builder function for message pages
|
|
||||||
/// or static pages
|
|
||||||
pub enum Page<'b> {
|
|
||||||
Builder(MessageBuilderFn<'b>),
|
|
||||||
Static(CreateMessage<'b>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'b> Page<'b> {
|
|
||||||
/// Creates a new page with the given builder function
|
|
||||||
pub fn new_builder<F: 'static>(builder_fn: F) -> Self
|
|
||||||
where
|
|
||||||
F: Fn() -> MessageBuildOutput<'b> + Send + Sync,
|
|
||||||
{
|
|
||||||
Self::Builder(Arc::new(builder_fn))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new page with a static message
|
|
||||||
pub fn new_static(page: CreateMessage<'b>) -> Self {
|
|
||||||
Self::Static(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the CreateMessage of the page
|
|
||||||
pub async fn get(&self) -> SerenityUtilsResult<CreateMessage<'b>> {
|
|
||||||
match self {
|
|
||||||
Page::Builder(b) => b().await,
|
|
||||||
Page::Static(inner) => Ok(inner.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
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 {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fired periodically
|
|
||||||
async fn update(&mut self, _http: &Http) -> SerenityUtilsResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fired when the message was deleted
|
|
||||||
async fn on_deleted(&mut self, _ctx: &Context) -> SerenityUtilsResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fired when a reaction was added to the message
|
|
||||||
async fn on_reaction_add(
|
|
||||||
&mut self,
|
|
||||||
_ctx: &Context,
|
|
||||||
_reaction: Reaction,
|
|
||||||
) -> SerenityUtilsResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fired when a reaction was removed from the message
|
|
||||||
async fn on_reaction_remove(
|
|
||||||
&mut self,
|
|
||||||
_ctx: &Context,
|
|
||||||
_reaction: Reaction,
|
|
||||||
) -> SerenityUtilsResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
use serenity::prelude::TypeMapKey;
|
|
||||||
use std::sync::atomic::AtomicBool;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub struct HelpActiveContainer;
|
|
||||||
|
|
||||||
impl TypeMapKey for HelpActiveContainer {
|
|
||||||
type Value = Arc<AtomicBool>;
|
|
||||||
}
|
|
Loading…
Reference in New Issue