use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use serenity::builder::CreateMessage;
use serenity::collector::ReactionAction;
use serenity::futures::StreamExt;
use serenity::model::prelude::{Message, Reaction, ReactionType};
use serenity::prelude::Context;
use crate::misc::add_reactions;
use crate::Error;
pub type MenuResult = Result<(), Error>;
pub struct Menu<'a> {
pub ctx: &'a Context,
pub msg: &'a Message,
pub pages: &'a [CreateMessage<'a>],
pub options: MenuOptions,
}
impl<'a> Menu<'a> {
pub fn new(
ctx: &'a Context,
msg: &'a Message,
pages: &'a [CreateMessage<'a>],
options: MenuOptions,
) -> Self {
Self {
ctx,
msg,
pages,
options,
}
}
pub async fn run(mut self) -> Result<Option<Message>, Error> {
loop {
match self.work().await {
Ok((index, reaction)) => match self.options.controls.get(index) {
Some(control) => {
Arc::clone(&control.function)(&mut self, reaction).await;
},
None => {
let _ = self.clean_reactions().await;
break;
},
},
Err(e) => {
let _ = self.clean_reactions().await;
if let Error::TimeoutError = e {
break;
} else {
return Err(e);
}
},
}
}
Ok(self.options.message)
}
async fn work(&mut self) -> Result<(usize, Reaction), Error> {
if self.pages.is_empty() {
return Err(Error::from("`pages` is empty."));
}
if self.options.page > self.pages.len() - 1 {
return Err(Error::from("`page` is out of bounds."));
}
let page = &self.pages[self.options.page];
match &mut self.options.message {
Some(m) => {
m.edit(&self.ctx.http, |m| {
m.0.clone_from(&page.0);
m
})
.await?;
},
None => {
let msg = self
.msg
.channel_id
.send_message(&self.ctx.http, |m| {
m.clone_from(page);
m
})
.await?;
self.add_reactions(&msg).await?;
self.options.message = Some(msg);
},
}
let message = self.options.message.as_ref().unwrap();
let mut reaction_collector = message
.await_reactions(&self.ctx)
.timeout(Duration::from_secs_f64(self.options.timeout))
.author_id(self.msg.author.id)
.build();
let (choice, reaction) = {
let mut choice = None;
let mut reaction = None;
let mut found_one = false;
while let Some(item) = reaction_collector.next().await {
if let ReactionAction::Added(r) = item.as_ref() {
if !found_one {
found_one = true;
}
let r = r.as_ref().clone();
if let Some(i) = self.process_reaction(&r) {
choice = Some(i);
reaction = Some(r);
break;
}
}
}
if !found_one {
return Err(Error::TimeoutError);
}
(choice, reaction)
};
match choice {
Some(c) => Ok((c, reaction.unwrap())),
None => Err(Error::InvalidChoice),
}
}
async fn add_reactions(&self, msg: &Message) -> MenuResult {
if self.options.non_blocking {
let emojis = self.options.controls.iter().map(|c| c.emoji.clone()).collect::<Vec<_>>();
add_reactions(self.ctx, msg, emojis).await?;
} else {
for control in &self.options.controls {
self.ctx.http.create_reaction(msg.channel_id.0, msg.id.0, &control.emoji).await?;
}
}
Ok(())
}
fn process_reaction(&self, reaction: &Reaction) -> Option<usize> {
let emoji = &reaction.emoji;
for (idx, control) in self.options.controls.iter().enumerate() {
if &control.emoji == emoji {
return Some(idx);
}
}
None
}
async fn clean_reactions(&self) -> MenuResult {
if let Some(msg) = &self.options.message {
msg.delete_reactions(&self.ctx.http).await?;
}
Ok(())
}
}
pub struct MenuOptions {
pub page: usize,
pub timeout: f64,
pub message: Option<Message>,
pub controls: Vec<Control>,
pub non_blocking: bool,
}
impl MenuOptions {
pub fn new(
page: usize,
timeout: f64,
message: Option<Message>,
controls: Vec<Control>,
non_blocking: bool,
) -> Self {
Self {
page,
timeout,
message,
controls,
non_blocking,
}
}
}
impl Default for MenuOptions {
fn default() -> Self {
let controls = vec![
Control::new('◀'.into(), Arc::new(|m, r| Box::pin(prev_page(m, r)))),
Control::new('❌'.into(), Arc::new(|m, r| Box::pin(close_menu(m, r)))),
Control::new('▶'.into(), Arc::new(|m, r| Box::pin(next_page(m, r)))),
];
Self {
page: 0,
timeout: 30.0,
message: None,
controls,
non_blocking: true,
}
}
}
pub struct Control {
pub emoji: ReactionType,
pub function: ControlFunction,
}
impl Control {
pub fn new(emoji: ReactionType, function: ControlFunction) -> Self {
Self {
emoji,
function,
}
}
}
pub type ControlFunction = Arc<
dyn for<'b> Fn(&'b mut Menu<'_>, Reaction) -> Pin<Box<dyn Future<Output = ()> + 'b + Send>>
+ Sync
+ Send,
>;
pub async fn next_page(menu: &mut Menu<'_>, reaction: Reaction) {
let _ = reaction.delete(&menu.ctx.http).await;
if menu.options.page == menu.pages.len() - 1 {
menu.options.page = 0;
} else {
menu.options.page += 1;
}
}
pub async fn prev_page(menu: &mut Menu<'_>, reaction: Reaction) {
let _ = reaction.delete(&menu.ctx.http).await;
if menu.options.page == 0 {
menu.options.page = menu.pages.len() - 1;
} else {
menu.options.page -= 1;
}
}
pub async fn close_menu(menu: &mut Menu<'_>, _reaction: Reaction) {
let _ = menu.options.message.as_ref().unwrap().delete(&menu.ctx.http).await;
}