Merge pull request #9 from Trivernis/develop

Develop
pull/16/head v0.4.0
Trivernis 3 years ago committed by GitHub
commit 2877d34ebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

5
Cargo.lock generated

@ -205,10 +205,11 @@ dependencies = [
[[package]]
name = "bot-serenityutils"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"futures",
"log 0.4.14",
"serde_json",
"serenity",
"thiserror",
"tokio",
@ -2281,7 +2282,7 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tobi-rs"
version = "0.3.0"
version = "0.4.0"
dependencies = [
"aspotify",
"bot-coreutils",

@ -1,6 +1,6 @@
[package]
name = "tobi-rs"
version = "0.3.0"
version = "0.4.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"

@ -3,5 +3,6 @@ mod tests;
pub mod process;
pub mod shuffle;
pub mod string;
/// Utilities to quickly check strings that represent urls
pub mod url;

@ -1,26 +1,19 @@
use std::io;
use std::process::Stdio;
use tokio::io::AsyncReadExt;
use tokio::process::Command;
/// Asynchronously runs a given command and returns the output
pub async fn run_command_async(command: &str, args: &[&str]) -> io::Result<String> {
log::trace!("Running command '{}' with args {:?}", command, args);
let cmd = Command::new(command)
.args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stderr = String::new();
let mut output = String::new();
let process_output: std::process::Output = Command::new(command).args(args).output().await?;
cmd.stderr.unwrap().read_to_string(&mut stderr).await?;
log::trace!("Reading from stderr...");
let stderr = String::from_utf8_lossy(&process_output.stderr[..]);
let stdout = String::from_utf8_lossy(&process_output.stdout[..]);
if stderr.len() != 0 {
log::debug!("STDERR of command {}: {}", command, stderr);
}
cmd.stdout.unwrap().read_to_string(&mut output).await?;
log::trace!("Command output is {}", output);
log::trace!("Command output is {}", stdout);
Ok(output)
Ok(stdout.to_string())
}

@ -0,0 +1,5 @@
/// Enquotes a string in a safe way
pub fn enquote<S: ToString>(value: S) -> String {
let value = value.to_string();
format!("\"{}\"", value.replace("\"", "\\\""))
}

@ -1,2 +1,5 @@
#[cfg(test)]
mod url_tests;
mod url_tests;
#[cfg(test)]
mod string_tests;

@ -0,0 +1,8 @@
use crate::string::enquote;
#[test]
fn test_enquote() {
assert_eq!(enquote("hello"), r#""hello""#);
assert_eq!(enquote(r#"hello "there""#), r#""hello \"there\"""#);
assert_eq!(enquote(""), r#""""#);
}

@ -68,10 +68,11 @@ dependencies = [
[[package]]
name = "bot-serenityutils"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"futures",
"log",
"serde_json",
"serenity",
"thiserror",
"tokio",

@ -1,6 +1,6 @@
[package]
name = "bot-serenityutils"
version = "0.1.0"
version = "0.2.0"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
@ -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"

@ -1,4 +1,38 @@
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;
pub type MessageHandle = (u64, u64);
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)
}
}

@ -12,4 +12,7 @@ pub enum SerenityUtilsError {
#[error("Serenity Utils not fully initialized")]
Uninitialized,
#[error("{0}")]
Msg(String),
}

@ -74,9 +74,10 @@ pub async fn 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)) {
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(&(channel_id.0, message_id.0));
listeners_lock.remove(&handle);
}
}
log::trace!("Listener unlocked");
@ -102,9 +103,10 @@ pub async fn handle_message_delete_bulk(
log::trace!("Listener locked.");
for message_id in message_ids {
if let Some(msg) = listeners_lock.get_mut(&(channel_id.0, message_id.0)) {
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(&(channel_id.0, message_id.0));
listeners_lock.remove(&handle);
}
}
}
@ -126,10 +128,9 @@ pub async fn handle_reaction_add(ctx: &Context, reaction: &Reaction) -> Serenity
let mut listeners_lock = listeners.lock().await;
log::trace!("Listener locked.");
let message_id = reaction.message_id;
let channel_id = reaction.channel_id;
let handle = MessageHandle::new(reaction.channel_id, reaction.message_id);
if let Some(msg) = listeners_lock.get_mut(&(channel_id.0, message_id.0)) {
if let Some(msg) = listeners_lock.get_mut(&handle) {
affected_messages.push(Arc::clone(&msg));
}
}
@ -151,10 +152,9 @@ pub async fn handle_reaction_remove(ctx: &Context, reaction: &Reaction) -> Seren
let mut listeners_lock = listeners.lock().await;
log::trace!("Listener locked.");
let message_id = reaction.message_id;
let channel_id = reaction.channel_id;
let handle = MessageHandle::new(reaction.channel_id, reaction.message_id);
if let Some(msg) = listeners_lock.get_mut(&(channel_id.0, message_id.0)) {
if let Some(msg) = listeners_lock.get_mut(&handle) {
affected_messages.push(Arc::clone(&msg));
}
}

@ -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(())
}
@ -48,8 +49,10 @@ 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?;
.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());

@ -3,9 +3,9 @@ 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, Page};
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};
@ -15,32 +15,39 @@ 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 = "⬅️";
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>>
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: ControlAction,
inner: ControlActionArc,
position: usize,
}
impl ActionContainer {
/// Creates a new control action
pub fn new(position: usize, inner: ControlAction) -> Self {
Self { inner, position }
pub fn new<F: 'static>(position: usize, 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
@ -56,31 +63,98 @@ impl ActionContainer {
}
/// A menu message
#[derive(Clone)]
pub struct Menu<'a> {
pub message: MessageHandle,
pub pages: Vec<CreateMessage<'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,
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.0, self.message.1)
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.0, self.message.1).await?;
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]
@ -94,6 +168,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(())
@ -134,10 +224,11 @@ impl<'a> EventDrivenMessage for Menu<'a> {
/// A builder for messages
pub struct MenuBuilder {
pages: Vec<CreateMessage<'static>>,
pages: Vec<Page<'static>>,
current_page: usize,
controls: HashMap<String, ActionContainer>,
timeout: Duration,
sticky: bool,
}
impl Default for MenuBuilder {
@ -147,6 +238,7 @@ impl Default for MenuBuilder {
current_page: 0,
controls: HashMap::new(),
timeout: Duration::from_secs(60),
sticky: false,
}
}
}
@ -158,15 +250,15 @@ impl MenuBuilder {
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())),
ActionContainer::new(0, |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())),
ActionContainer::new(1, |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())),
ActionContainer::new(2, |c, m, r| next_page(c, m, r).boxed()),
);
Self {
@ -176,7 +268,7 @@ impl MenuBuilder {
}
/// Adds a page to the message builder
pub fn add_page(mut self, page: CreateMessage<'static>) -> Self {
pub fn add_page(mut self, page: Page<'static>) -> Self {
self.pages.push(page);
self
@ -185,7 +277,7 @@ impl MenuBuilder {
/// Adds multiple pages to the message
pub fn add_pages<I>(mut self, pages: I) -> Self
where
I: IntoIterator<Item = CreateMessage<'static>>,
I: IntoIterator<Item = Page<'static>>,
{
let mut pages = pages.into_iter().collect();
self.pages.append(&mut pages);
@ -194,12 +286,13 @@ impl MenuBuilder {
}
/// Adds a single control to the message
pub fn add_control<S: ToString>(
mut self,
position: usize,
emoji: S,
action: ControlAction,
) -> Self {
pub fn add_control<S, F: 'static>(mut self, position: usize, 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));
@ -210,11 +303,16 @@ impl MenuBuilder {
pub fn add_controls<S, I>(mut self, controls: I) -> Self
where
S: ToString,
I: IntoIterator<Item = (usize, S, ControlAction)>,
I: IntoIterator<Item = (usize, S, ControlActionArc)>,
{
for (position, emoji, action) in controls {
self.controls
.insert(emoji.to_string(), ActionContainer::new(position, action));
self.controls.insert(
emoji.to_string(),
ActionContainer {
position,
inner: action,
},
);
}
self
@ -234,14 +332,28 @@ 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<()> {
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();
.clone()
.get()
.await?;
let message = channel_id.send_message(ctx, |_| &mut current_page).await?;
log::trace!("Message is {:?}", message);
@ -255,23 +367,25 @@ impl MenuBuilder {
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: (message.channel_id.0, message.id.0),
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...");
{
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))),
);
listeners_lock.insert(message_handle, Arc::new(Mutex::new(Box::new(menu))));
}
log::debug!("Adding controls...");
@ -282,6 +396,6 @@ impl MenuBuilder {
}
log::debug!("Menu successfully created.");
Ok(())
Ok(handle_lock)
}
}

@ -1,12 +1,15 @@
pub(crate) mod container;
pub(crate) mod controls;
pub(crate) mod menu;
pub(crate) mod page;
pub(crate) mod traits;
pub use container::*;
pub use controls::*;
pub use menu::{
ActionContainer, ControlAction, Menu, MenuBuilder, CLOSE_MENU_EMOJI, NEXT_PAGE_EMOJI,
ActionContainer, ControlActionArc, Menu, MenuBuilder, CLOSE_MENU_EMOJI, NEXT_PAGE_EMOJI,
PREVIOUS_PAGE_EMOJI,
};
pub use page::*;
pub use traits::EventDrivenMessage;

@ -0,0 +1,40 @@
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()),
}
}
}

@ -15,6 +15,7 @@ use crate::handler::Handler;
use crate::utils::context_data::{DatabaseContainer, Store, StoreData};
use crate::utils::error::{BotError, BotResult};
use bot_serenityutils::menu::EventDrivenMessageContainer;
use serenity::framework::standard::buckets::LimitedFor;
use std::sync::Arc;
use tokio::sync::Mutex;
@ -24,7 +25,7 @@ pub async fn get_client() -> BotResult<Client> {
let client = Client::builder(token)
.event_handler(Handler)
.framework(get_framework())
.framework(get_framework().await)
.register_songbird()
.await?;
{
@ -37,7 +38,7 @@ pub async fn get_client() -> BotResult<Client> {
Ok(client)
}
pub fn get_framework() -> StandardFramework {
pub async fn get_framework() -> StandardFramework {
let mut owners = HashSet::new();
if let Some(owner) = dotenv::var("BOT_OWNER").ok().and_then(|o| o.parse().ok()) {
owners.insert(UserId(owner));
@ -61,6 +62,22 @@ pub fn get_framework() -> StandardFramework {
.before(before_hook)
.on_dispatch_error(dispatch_error)
.help(&HELP)
.bucket("music_api", |b| {
b.delay(1)
.time_span(60)
.limit(30)
.limit_for(LimitedFor::User)
})
.await
.bucket("sauce_api", |b| {
b.delay(1)
.time_span(60)
.limit(10)
.limit_for(LimitedFor::User)
})
.await
.bucket("general", |b| b.time_span(10).limit(5))
.await
}
#[hook]

@ -11,6 +11,7 @@ use crate::utils::context_data::Store;
#[example("unbreaking")]
#[min_args(1)]
#[aliases("ench")]
#[bucket("general")]
pub(crate) async fn enchantment(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let data = ctx.data.read().await;
let store = data.get::<Store>().expect("Failed to get store");

@ -11,6 +11,7 @@ use crate::utils::context_data::Store;
#[example("bread")]
#[min_args(1)]
#[aliases("i")]
#[bucket("general")]
pub(crate) async fn item(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let data = ctx.data.read().await;
let store = data.get::<Store>().expect("Failed to get store");

@ -5,6 +5,7 @@ use serenity::model::channel::Message;
#[command]
#[description("Displays information about the bot")]
#[bucket("general")]
async fn about(ctx: &Context, msg: &Message) -> CommandResult {
msg.channel_id
.send_message(ctx, |m| {

@ -21,6 +21,7 @@ static PEKOS: &[&str] = &[
#[usage("(<content>)")]
#[example("Hello")]
#[aliases("peko")]
#[bucket("general")]
async fn pekofy(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let mut reference_message = msg.id;
let mut content = args.message().to_string();

@ -6,6 +6,7 @@ use serenity::model::channel::Message;
#[command]
#[description("Simple ping test command")]
#[usage("")]
#[bucket("general")]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
msg.reply(ctx, "Pong!").await?;

@ -13,6 +13,7 @@ static QALC_HELP: &[&str] = &["help", "--help", "-h", "h"];
#[min_args(1)]
#[usage("<expression>")]
#[example("1 * 1 + 1 / sqrt(2)")]
#[bucket("general")]
async fn qalc(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let expression = args.message();
lazy_static::lazy_static! {

@ -14,6 +14,7 @@ use crate::utils::get_previous_message_or_reply;
#[description("Searches for the source of a previously posted image or an image replied to.")]
#[usage("")]
#[aliases("source")]
#[bucket("sauce_api")]
async fn sauce(ctx: &Context, msg: &Message) -> CommandResult {
log::debug!("Got sauce command");
let source_msg = get_previous_message_or_reply(ctx, msg).await?;

@ -15,6 +15,7 @@ const VERSION: &'static str = env!("CARGO_PKG_VERSION");
#[command]
#[description("Shows some statistics about the bot")]
#[usage("")]
#[bucket("general")]
async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
log::debug!("Reading system stats");
let mut system = sysinfo::System::new_all();

@ -10,6 +10,7 @@ use serenity::model::channel::Message;
#[min_args(1)]
#[max_args(3)]
#[usage("<%H:%M/now> (<from-timezone>) (<to-timezone>)")]
#[bucket("general")]
async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let when = args.single::<String>().unwrap_or("now".to_string());
let first_timezone = args.single::<String>().ok();

@ -8,6 +8,7 @@ use serenity::model::channel::Message;
#[min_args(1)]
#[usage("<query...>")]
#[example("Europe Berlin")]
#[bucket("general")]
async fn timezones(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let query = args
.iter::<String>()

@ -11,6 +11,7 @@ use crate::commands::music::{get_queue_for_guild, is_dj};
#[description("Clears the queue")]
#[usage("")]
#[aliases("cl")]
#[bucket("general")]
async fn clear_queue(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();

@ -7,28 +7,36 @@ use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::commands::music::get_queue_for_guild;
use crate::messages::music::NowPlayingMessage;
use crate::messages::music::create_now_playing_msg;
#[command]
#[only_in(guilds)]
#[description("Displays the currently playing song")]
#[usage("")]
#[aliases("nowplaying", "np")]
#[bucket("general")]
async fn current(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Displaying current song for queue in {}", guild.id);
let queue = get_queue_for_guild(ctx, &guild.id).await?;
let mut queue_lock = queue.lock().await;
if let Some(current) = queue_lock.current() {
let current = {
let queue_lock = queue.lock().await;
queue_lock.current().clone()
};
if let Some(current) = current {
let metadata = current.metadata().clone();
log::trace!("Metadata is {:?}", metadata);
let np_msg =
NowPlayingMessage::create(ctx.http.clone(), &msg.channel_id, &metadata).await?;
let np_msg = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?;
let mut queue_lock = queue.lock().await;
if let Some(old_np) = mem::replace(&mut queue_lock.now_playing_msg, Some(np_msg)) {
let _ = old_np.inner().delete().await;
let old_np = old_np.read().await;
if let Ok(message) = old_np.get_message(&ctx.http).await {
let _ = message.delete(ctx).await;
}
}
}
handle_autodelete(ctx, msg).await?;

@ -1,18 +1,28 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_channel_for_author, join_channel};
use crate::commands::music::{get_channel_for_author, is_dj, join_channel};
use serenity::model::id::ChannelId;
#[command]
#[only_in(guilds)]
#[description("Joins a voice channel")]
#[usage("")]
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
#[bucket("general")]
async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
let channel_id = if let Ok(arg) = args.single::<u64>() {
if is_dj(ctx, guild.id, &msg.author).await? {
ChannelId(arg)
} else {
get_channel_for_author(&msg.author.id, &guild)?
}
} else {
get_channel_for_author(&msg.author.id, &guild)?
};
log::debug!("Joining channel {} for guild {}", channel_id, guild.id);
join_channel(ctx, channel_id, guild.id).await;
handle_autodelete(ctx, msg).await?;

@ -11,6 +11,7 @@ use crate::commands::music::{get_queue_for_guild, get_voice_manager, is_dj};
#[description("Leaves a voice channel")]
#[usage("")]
#[aliases("stop")]
#[bucket("general")]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Leave request received for guild {}", guild.id);

@ -11,6 +11,7 @@ use crate::providers::music::lyrics::get_lyrics;
#[only_in(guilds)]
#[description("Shows the lyrics for the currently playing song")]
#[usage("")]
#[bucket("general")]
async fn lyrics(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Fetching lyrics for song playing in {}", guild.id);

@ -31,6 +31,7 @@ use save_playlist::SAVE_PLAYLIST_COMMAND;
use shuffle::SHUFFLE_COMMAND;
use skip::SKIP_COMMAND;
use crate::messages::music::update_now_playing_msg;
use crate::providers::music::queue::{MusicQueue, Song};
use crate::providers::music::youtube_dl;
use crate::providers::settings::{get_setting, Setting};
@ -191,7 +192,7 @@ fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<Channe
}
/// Returns the voice manager from the context
async fn get_voice_manager(ctx: &Context) -> Arc<Songbird> {
pub async fn get_voice_manager(ctx: &Context) -> Arc<Songbird> {
songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
@ -250,13 +251,18 @@ async fn play_next_in_queue(
let track = handler_lock.play_only_source(source);
log::trace!("Track is {:?}", track);
if let Some(np) = &mut queue_lock.now_playing_msg {
let _ = np.refresh(track.metadata()).await;
if let Some(np) = &queue_lock.now_playing_msg {
if let Err(e) = update_now_playing_msg(http, np, track.metadata(), false).await {
log::error!("Failed to update now playing message: {:?}", e);
}
}
queue_lock.set_current(track);
} else {
if let Some(np) = mem::take(&mut queue_lock.now_playing_msg) {
let _ = np.inner().delete().await;
let np = np.read().await;
if let Ok(message) = np.get_message(http).await {
let _ = message.delete(http).await;
}
}
queue_lock.clear_current();
}
@ -375,7 +381,7 @@ async fn added_multiple_msg(ctx: &Context, msg: &Message, songs: &mut Vec<Song>)
/// Returns if the given user is a dj in the given guild based on the
/// setting for the name of the dj role
async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult<bool> {
pub async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult<bool> {
let dj_role = get_setting::<String>(ctx, guild, Setting::MusicDjRole).await?;
if let Some(role_name) = dj_role {

@ -5,11 +5,13 @@ use serenity::prelude::*;
use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_queue_for_guild, is_dj};
use crate::messages::music::update_now_playing_msg;
#[command]
#[only_in(guilds)]
#[description("Pauses playback")]
#[usage("")]
#[bucket("general")]
async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Pausing playback for guild {}", guild.id);
@ -26,9 +28,17 @@ async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
if queue_lock.paused() {
log::debug!("Paused");
msg.channel_id.say(ctx, "Paused playback").await?;
if let (Some(menu), Some(current)) = (&queue_lock.now_playing_msg, queue_lock.current())
{
update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?;
}
} else {
log::debug!("Resumed");
msg.channel_id.say(ctx, "Resumed playback").await?;
if let (Some(menu), Some(current)) = (&queue_lock.now_playing_msg, queue_lock.current())
{
update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?;
}
}
} else {
msg.channel_id.say(ctx, "Nothing to pause").await?;

@ -16,6 +16,7 @@ use crate::providers::settings::{get_setting, Setting};
#[usage("(<spotify_url,youtube_url,query>)")]
#[min_args(1)]
#[aliases("p")]
#[bucket("music_api")]
async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let query = args.message();

@ -14,7 +14,8 @@ use crate::commands::music::{
#[description("Puts a song as the next to play in the queue")]
#[usage("<song-url>")]
#[min_args(1)]
#[aliases("pn", "play-next")]
#[aliases("pn", "play-next", "playnext")]
#[bucket("music_api")]
async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let query = args.message();

@ -10,6 +10,7 @@ use crate::utils::context_data::get_database_from_context;
#[only_in(guilds)]
#[description("Displays a list of all saved playlists")]
#[usage("")]
#[bucket("general")]
async fn playlists(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::debug!("Displaying playlists for guild {}", guild.id);

@ -13,6 +13,7 @@ use crate::commands::music::get_queue_for_guild;
#[description("Shows the song queue")]
#[usage("")]
#[aliases("q")]
#[bucket("general")]
async fn queue(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
log::trace!("Displaying queue for guild {}", guild.id);

@ -13,6 +13,7 @@ use crate::utils::context_data::get_database_from_context;
#[example("anime https://www.youtube.com/playlist?list=PLqaM77H_o5hykROCe3uluvZEaPo6bZj-C")]
#[min_args(2)]
#[aliases("add-playlist", "save-playlist")]
#[bucket("general")]
async fn save_playlist(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();

@ -11,6 +11,7 @@ use crate::commands::music::{get_queue_for_guild, is_dj};
#[description("Shuffles the queue")]
#[usage("")]
#[aliases("sh")]
#[bucket("general")]
async fn shuffle(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();

@ -11,6 +11,7 @@ use crate::commands::music::{get_queue_for_guild, is_dj};
#[description("Skips to the next song")]
#[usage("")]
#[aliases("next")]
#[bucket("general")]
async fn skip(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).await.unwrap();
if !is_dj(ctx, guild.id, &msg.author).await? {

@ -14,6 +14,7 @@ use crate::utils::context_data::get_database_from_context;
#[min_args(0)]
#[max_args(1)]
#[required_permissions("MANAGE_GUILD")]
#[bucket("general")]
async fn get(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let database = get_database_from_context(ctx).await;
let guild = msg.guild(&ctx.cache).await.unwrap();

@ -16,6 +16,7 @@ use crate::utils::context_data::get_database_from_context;
#[min_args(1)]
#[max_args(2)]
#[required_permissions("MANAGE_GUILD")]
#[bucket("general")]
async fn set(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let key = args.single::<String>().unwrap().to_lowercase();
let all_settings: Vec<String> = ALL_SETTINGS.iter().map(|s| s.to_string()).collect();

@ -5,79 +5,212 @@ use serenity::http::Http;
use serenity::model::prelude::ChannelId;
use songbird::input::Metadata;
use crate::utils::error::BotResult;
use crate::utils::messages::ShareableMessage;
use crate::commands::music::{get_queue_for_guild, get_voice_manager, is_dj};
use crate::providers::music::queue::MusicQueue;
use crate::utils::error::*;
use bot_serenityutils::core::MessageHandle;
use bot_serenityutils::error::SerenityUtilsResult;
use bot_serenityutils::menu::{Menu, MenuBuilder, Page};
use serenity::builder::CreateMessage;
use serenity::client::Context;
use serenity::model::channel::Reaction;
use std::time::Duration;
use tokio::sync::{Mutex, RwLock};
#[derive(Clone)]
pub struct NowPlayingMessage {
inner: ShareableMessage,
static PAUSE_BUTTON: &str = "⏯️";
static SKIP_BUTTON: &str = "⏭️";
static STOP_BUTTON: &str = "⏹️";
/// Creates a new now playing message and returns the embed for that message
pub async fn create_now_playing_msg(
ctx: &Context,
queue: Arc<Mutex<MusicQueue>>,
channel_id: ChannelId,
) -> BotResult<Arc<RwLock<MessageHandle>>> {
log::debug!("Creating now playing menu");
let handle = MenuBuilder::default()
.add_control(0, STOP_BUTTON, |c, m, r| {
Box::pin(stop_button_action(c, m, r))
})
.add_control(1, PAUSE_BUTTON, |c, m, r| {
Box::pin(play_pause_button_action(c, m, r))
})
.add_control(2, SKIP_BUTTON, |c, m, r| {
Box::pin(skip_button_action(c, m, r))
})
.add_page(Page::new_builder(move || {
let queue = Arc::clone(&queue);
Box::pin(async move {
log::debug!("Creating now playing embed for page");
let queue = queue.lock().await;
log::debug!("Queue locked");
let mut page = CreateMessage::default();
if let Some(current) = queue.current() {
page.embed(|e| create_now_playing_embed(current.metadata(), e, queue.paused()));
} else {
page.embed(|e| e.description("Queue is empty"));
}
log::debug!("Embed created");
Ok(page)
})
}))
.sticky(true)
.timeout(Duration::from_secs(60 * 60 * 24))
.build(ctx, channel_id)
.await?;
Ok(handle)
}
impl NowPlayingMessage {
/// Creates a new now playing message
pub async fn create(
ctx: Arc<Http>,
channel_id: &ChannelId,
meta: &Metadata,
) -> BotResult<Self> {
let inner = ShareableMessage::create(ctx, channel_id, |f| {
f.embed(|e| Self::create_embed(meta, e))
/// Updates the now playing message with new content
pub async fn update_now_playing_msg(
http: &Arc<Http>,
handle: &Arc<RwLock<MessageHandle>>,
meta: &Metadata,
paused: bool,
) -> 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, paused))
})
.await?;
log::debug!("Message updated.");
Ok(())
}
/// Creates the embed of the now playing message
fn create_now_playing_embed<'a>(
meta: &Metadata,
mut embed: &'a mut CreateEmbed,
paused: bool,
) -> &'a mut CreateEmbed {
embed = embed
.title(if paused { "Paused" } else { "Playing" })
.description(format!(
"[{}]({}) by {}",
meta.title.clone().unwrap(),
meta.source_url.clone().unwrap(),
meta.artist.clone().unwrap()
));
Ok(Self { inner })
if let Some(thumb) = meta.thumbnail.clone() {
embed = embed.thumbnail(thumb);
}
/// Returns the inner shareable message
pub fn inner(&self) -> &ShareableMessage {
&self.inner
embed
}
/// Toggled when the pause button is pressed
async fn play_pause_button_action(
ctx: &Context,
_: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
log::debug!("Play/Pause button pressed");
let guild_id = reaction.guild_id.unwrap();
let user = reaction.user(&ctx).await?;
if !is_dj(ctx, guild_id, &user).await? {
return Ok(());
}
{
let queue = get_queue_for_guild(ctx, &guild_id).await?;
/// Refreshes the now playing message
pub async fn refresh(&mut self, meta: &Metadata) -> BotResult<()> {
let channel_id = self.inner.channel_id();
let messages = channel_id
.messages(self.inner.http(), |p| p.limit(1))
.await?;
let (current, message, paused) = {
log::debug!("Queue is locked");
let mut queue = queue.lock().await;
queue.pause();
(
queue.current().clone(),
queue.now_playing_msg.clone().unwrap(),
queue.paused(),
)
};
log::debug!("Queue is unlocked");
if let Some(current) = current {
update_now_playing_msg(&ctx.http, &message, current.metadata(), paused).await?;
}
}
let needs_recreate = messages
.first()
.map(|m| m.id != self.inner.message_id())
.unwrap_or(true);
Ok(())
}
// recreates the message if needed
if needs_recreate {
log::debug!("Song info message will be recreated");
let http = self.inner.http().clone();
let _ = self.inner.delete().await;
/// Triggered when the skip button is pressed
async fn skip_button_action(
ctx: &Context,
_: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
let guild_id = reaction.guild_id.unwrap();
let user = reaction.user(&ctx).await?;
self.inner = ShareableMessage::create(http, &channel_id, |f| {
f.embed(|e| Self::create_embed(meta, e))
})
.await?;
} else {
log::debug!("Reusing old song info");
self.inner
.edit(|m| m.embed(|e| Self::create_embed(meta, e)))
.await?;
if !is_dj(ctx, guild_id, &user).await? {
return Ok(());
}
{
let current = {
let queue = get_queue_for_guild(ctx, &guild_id).await?;
let queue = queue.lock().await;
queue.current().clone()
};
if let Some(current) = current {
let _ = current.stop();
}
}
Ok(())
}
Ok(())
/// Triggered when the stop button is pressed
async fn stop_button_action(
ctx: &Context,
menu: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
let guild_id = reaction.guild_id.unwrap();
let user = reaction.user(&ctx).await?;
if !is_dj(ctx, guild_id, &user).await? {
return Ok(());
}
{
let manager = get_voice_manager(ctx).await;
let queue = get_queue_for_guild(ctx, &guild_id).await?;
let queue = queue.lock().await;
/// Creates the embed of the now playing message
fn create_embed<'a>(meta: &Metadata, mut embed: &'a mut CreateEmbed) -> &'a mut CreateEmbed {
embed = embed.description(format!(
"Now Playing [{}]({}) by {}",
meta.title.clone().unwrap(),
meta.source_url.clone().unwrap(),
meta.artist.clone().unwrap()
));
let handler = manager.get(guild_id);
if let Some(thumb) = meta.thumbnail.clone() {
embed = embed.thumbnail(thumb);
if let Some(handler) = handler {
let mut handler_lock = handler.lock().await;
handler_lock.remove_all_global_events();
}
if let Some(current) = queue.current() {
current.stop().map_err(BotError::from)?;
}
embed
if manager.get(guild_id).is_some() {
manager.remove(guild_id).await.map_err(BotError::from)?;
log::debug!("Left the voice channel");
} else {
log::debug!("Not in a voice channel");
}
}
{
let handle = &menu.message;
let handle = handle.read().await;
ctx.http
.delete_message(handle.channel_id, handle.message_id)
.await?;
}
Ok(())
}

@ -7,7 +7,7 @@ use serenity::{model::channel::Message, prelude::*};
use bot_coreutils::url::get_domain_for_url;
use crate::utils::error::BotResult;
use bot_serenityutils::menu::MenuBuilder;
use bot_serenityutils::menu::{MenuBuilder, Page};
use rand::prelude::SliceRandom;
use std::time::Duration;
@ -26,7 +26,7 @@ pub async fn show_sauce_menu(
msg: &Message,
sources: Vec<SauceResult>,
) -> BotResult<()> {
let pages: Vec<CreateMessage> = sources.into_iter().map(create_sauce_page).collect();
let pages: Vec<Page> = sources.into_iter().map(create_sauce_page).collect();
if pages.len() == 1 {
MenuBuilder::default()
@ -46,7 +46,7 @@ pub async fn show_sauce_menu(
}
/// Creates a single sauce page
fn create_sauce_page<'a>(mut result: SauceResult) -> CreateMessage<'a> {
fn create_sauce_page<'a>(mut result: SauceResult) -> Page<'a> {
let mut message = CreateMessage::default();
let mut description_lines = Vec::new();
let original = result.original_url;
@ -95,5 +95,5 @@ fn create_sauce_page<'a>(mut result: SauceResult) -> CreateMessage<'a> {
.footer(|f| f.text("Powered by SauceNAO"))
});
message
Page::new_static(message)
}

@ -5,16 +5,18 @@ use songbird::tracks::TrackHandle;
use bot_coreutils::shuffle::Shuffle;
use crate::messages::music::NowPlayingMessage;
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<NowPlayingMessage>,
pub now_playing_msg: Option<Arc<RwLock<MessageHandle>>>,
pub leave_flag: bool,
}

@ -10,6 +10,7 @@ use bot_coreutils::process::run_command_async;
use crate::providers::music::queue::Song;
use crate::providers::music::responses::{PlaylistEntry, VideoInformation};
use crate::utils::error::BotResult;
use bot_coreutils::string::enquote;
static THREAD_LIMIT: u8 = 64;
@ -44,7 +45,7 @@ pub(crate) async fn search_video_information(query: String) -> BotResult<Option<
"--no-warnings",
"--dump-json",
"-i",
format!("ytsearch:\"{}\"", query).as_str(),
format!("ytsearch:{}", enquote(query)).as_str(),
])
.await?;
let information = serde_json::from_str(&*output)?;

@ -1,13 +1,10 @@
use bot_coreutils::process::run_command_async;
use crate::utils::error::BotResult;
use bot_coreutils::string::enquote;
/// Runs the qalc command with the given expression
pub async fn qalc(expression: &str) -> BotResult<String> {
let result = run_command_async(
"qalc",
&["-m", "1000", format!("\"{}\"", &*expression).as_str()],
)
.await?;
let result = run_command_async("qalc", &["-m", "1000", enquote(expression).as_str()]).await?;
Ok(result)
}

@ -1,3 +1,4 @@
use bot_serenityutils::error::SerenityUtilsError;
use thiserror::Error;
pub type BotResult<T> = Result<T, BotError>;
@ -34,6 +35,12 @@ pub enum BotError {
#[error("Serenity Utils Error: {0}")]
SerenityUtils(#[from] bot_serenityutils::error::SerenityUtilsError),
#[error("Track Error: {0}")]
TrackError(#[from] songbird::error::TrackError),
#[error("JoinError: {0}")]
JoinError(#[from] songbird::error::JoinError),
#[error("{0}")]
Msg(String),
}
@ -43,3 +50,9 @@ impl From<&str> for BotError {
Self::Msg(s.to_string())
}
}
impl From<BotError> for SerenityUtilsError {
fn from(e: BotError) -> Self {
Self::Msg(format!("{:?}", e))
}
}

@ -1,80 +0,0 @@
use std::sync::Arc;
use serenity::builder::{CreateMessage, EditMessage};
use serenity::http::{CacheHttp, Http};
use serenity::model::channel::Message;
use serenity::model::id::{ChannelId, MessageId};
use crate::utils::error::BotResult;
#[derive(Clone)]
pub struct ShareableMessage {
http: Arc<Http>,
channel_id: u64,
message_id: u64,
}
impl ShareableMessage {
/// Creates a new active message
pub async fn create<'a, F>(http: Arc<Http>, channel_id: &ChannelId, f: F) -> BotResult<Self>
where
for<'b> F: FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>,
{
let msg = channel_id.send_message(http.http(), f).await?;
Ok(Self::new(http, &msg.channel_id, &msg.id))
}
/// Creates a new active message
pub fn new(http: Arc<Http>, channel_id: &ChannelId, message_id: &MessageId) -> Self {
Self {
http,
channel_id: channel_id.0,
message_id: message_id.0,
}
}
/// Deletes the underlying message
pub async fn delete(&self) -> BotResult<()> {
let msg = self.get_discord_message().await?;
msg.delete(&self.http).await?;
Ok(())
}
/// Edits the active message
pub async fn edit<F>(&self, f: F) -> BotResult<()>
where
F: FnOnce(&mut EditMessage) -> &mut EditMessage,
{
let mut message = self.get_discord_message().await?;
message.edit(&self.http, f).await?;
Ok(())
}
/// Returns the underlying message
pub async fn get_discord_message(&self) -> BotResult<Message> {
let message = self
.http
.get_message(self.channel_id, self.message_id)
.await?;
Ok(message)
}
/// Returns the channel of the message
pub fn channel_id(&self) -> ChannelId {
ChannelId(self.channel_id)
}
/// Returns the message id of the message
pub fn message_id(&self) -> MessageId {
MessageId(self.message_id)
}
/// Returns the reference to the http object
pub fn http(&self) -> &Arc<Http> {
&self.http
}
}

@ -6,7 +6,6 @@ use crate::utils::error::BotResult;
pub(crate) mod context_data;
pub(crate) mod error;
pub(crate) mod logging;
pub(crate) mod messages;
/// Returns the message the given message is a reply to or the message sent before that
pub async fn get_previous_message_or_reply(

Loading…
Cancel
Save