Merge pull request #27 from Trivernis/develop

Develop
feature/events v0.8.0
Trivernis 4 years ago committed by GitHub
commit 5b14c17878
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

33
Cargo.lock generated

@ -73,8 +73,10 @@ dependencies = [
"futures-io", "futures-io",
"futures-util", "futures-util",
"log 0.4.14", "log 0.4.14",
"native-tls",
"pin-project", "pin-project",
"tokio", "tokio",
"tokio-native-tls",
"tokio-rustls", "tokio-rustls",
"tungstenite 0.11.1", "tungstenite 0.11.1",
"webpki-roots 0.20.0", "webpki-roots 0.20.0",
@ -207,18 +209,6 @@ dependencies = [
"tokio-diesel", "tokio-diesel",
] ]
[[package]]
name = "bot-serenityutils"
version = "0.2.4"
dependencies = [
"futures",
"log 0.4.14",
"serde_json",
"serenity",
"thiserror",
"tokio",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.6.1" version = "3.6.1"
@ -2167,6 +2157,20 @@ dependencies = [
"uwl", "uwl",
] ]
[[package]]
name = "serenity-rich-interaction"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f160ea25bd4cb331165ee7c0d0c34ea975a3d31230c251bb23f988ffc1b25e"
dependencies = [
"futures",
"log 0.4.14",
"serde_json",
"serenity",
"thiserror",
"tokio",
]
[[package]] [[package]]
name = "serenity-voice-model" name = "serenity-voice-model"
version = "0.1.0" version = "0.1.0"
@ -2522,12 +2526,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tobi-rs" name = "tobi-rs"
version = "0.7.1" version = "0.8.0"
dependencies = [ dependencies = [
"aspotify", "aspotify",
"bot-coreutils", "bot-coreutils",
"bot-database", "bot-database",
"bot-serenityutils",
"chrono", "chrono",
"chrono-tz", "chrono-tz",
"colored", "colored",
@ -2547,6 +2550,7 @@ dependencies = [
"serde_derive", "serde_derive",
"serde_json", "serde_json",
"serenity", "serenity",
"serenity-rich-interaction",
"songbird", "songbird",
"sysinfo", "sysinfo",
"thiserror", "thiserror",
@ -2722,6 +2726,7 @@ dependencies = [
"httparse", "httparse",
"input_buffer 0.3.1", "input_buffer 0.3.1",
"log 0.4.14", "log 0.4.14",
"native-tls",
"rand 0.7.3", "rand 0.7.3",
"sha-1", "sha-1",
"url", "url",

@ -1,6 +1,6 @@
[package] [package]
name = "tobi-rs" name = "tobi-rs"
version = "0.7.1" version = "0.8.0"
authors = ["trivernis <trivernis@protonmail.com>"] authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018" edition = "2018"
@ -12,7 +12,7 @@ panic = 'abort'
[dependencies] [dependencies]
bot-database = {path="./bot-database"} bot-database = {path="./bot-database"}
bot-coreutils = {path="./bot-coreutils"} bot-coreutils = {path="./bot-coreutils"}
bot-serenityutils = {path="./bot-serenityutils"} serenity-rich-interaction = "0.2.2"
serenity = "0.10.5" serenity = "0.10.5"
dotenv = "0.15.0" dotenv = "0.15.0"
tokio = { version = "1.4.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.4.0", features = ["macros", "rt-multi-thread"] }

@ -9,7 +9,6 @@ COPY Cargo.toml Cargo.lock ./
COPY src ./src COPY src ./src
COPY bot-coreutils ./bot-coreutils COPY bot-coreutils ./bot-coreutils
COPY bot-database ./bot-database COPY bot-database ./bot-database
COPY bot-serenityutils ./bot-serenityutils
RUN --mount=type=cache,target=/usr/local/cargo/registry \ RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=target \ --mount=type=cache,target=target \
cargo build --release cargo build --release

@ -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>;
}

@ -11,16 +11,16 @@ use serenity::Client;
use songbird::SerenityInit; use songbird::SerenityInit;
use crate::commands::*; use crate::commands::*;
use crate::handler::Handler; use crate::handler::{get_raw_event_handler, Handler};
use crate::providers::music::lavalink::{Lavalink, LavalinkHandler}; use crate::providers::music::lavalink::{Lavalink, LavalinkHandler};
use crate::utils::context_data::{ use crate::utils::context_data::{
get_database_from_context, DatabaseContainer, MusicPlayers, Store, StoreData, get_database_from_context, DatabaseContainer, MusicPlayers, Store, StoreData,
}; };
use crate::utils::error::{BotError, BotResult}; use crate::utils::error::{BotError, BotResult};
use bot_serenityutils::menu::EventDrivenMessageContainer;
use lavalink_rs::LavalinkClient; use lavalink_rs::LavalinkClient;
use serenity::framework::standard::buckets::LimitedFor; use serenity::framework::standard::buckets::LimitedFor;
use serenity::http::Http; use serenity_rich_interaction::menu::EventDrivenMessageContainer;
use serenity_rich_interaction::RegisterRichInteractions;
use std::env; use std::env;
use std::sync::Arc; use std::sync::Arc;
use std::time::SystemTime; use std::time::SystemTime;
@ -29,15 +29,23 @@ use tokio::sync::Mutex;
pub async fn get_client() -> BotResult<Client> { pub async fn get_client() -> BotResult<Client> {
let token = env::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?; let token = env::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?;
let database = get_database()?; let database = get_database()?;
let http = Http::new_with_token(&token);
let current_application = http.get_current_application_info().await?;
let client = Client::builder(token) let client = Client::builder(token)
.register_rich_interactions_with(get_raw_event_handler())
.event_handler(Handler) .event_handler(Handler)
.framework(get_framework().await) .framework(get_framework().await)
.register_songbird() .register_songbird()
.type_map_insert::<Store>(StoreData::new())
.type_map_insert::<DatabaseContainer>(database)
.type_map_insert::<MusicPlayers>(HashMap::new())
.await?; .await?;
let data = client.data.clone(); let data = client.data.clone();
let current_application = client
.cache_and_http
.http
.get_current_application_info()
.await?;
let lava_client = LavalinkClient::builder(current_application.id.0) let lava_client = LavalinkClient::builder(current_application.id.0)
.set_host(env::var("LAVALINK_HOST").unwrap_or("172.0.0.1".to_string())) .set_host(env::var("LAVALINK_HOST").unwrap_or("172.0.0.1".to_string()))
.set_password(env::var("LAVALINK_PASSWORD").expect("Missing lavalink password")) .set_password(env::var("LAVALINK_PASSWORD").expect("Missing lavalink password"))
@ -51,10 +59,6 @@ pub async fn get_client() -> BotResult<Client> {
.await?; .await?;
{ {
let mut data = client.data.write().await; let mut data = client.data.write().await;
data.insert::<Store>(StoreData::new());
data.insert::<DatabaseContainer>(database);
data.insert::<EventDrivenMessageContainer>(Arc::new(Mutex::new(HashMap::new())));
data.insert::<MusicPlayers>(HashMap::new());
data.insert::<Lavalink>(Arc::new(lava_client)); data.insert::<Lavalink>(Arc::new(lava_client));
} }

@ -1,11 +1,11 @@
use crate::utils::context_data::get_database_from_context; use crate::utils::context_data::get_database_from_context;
use bot_coreutils::url; use bot_coreutils::url;
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult}; use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[description("Adds media to the database")] #[description("Adds media to the database")]

@ -1,11 +1,11 @@
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use futures::future::BoxFuture; use futures::future::BoxFuture;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult}; use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity::Result as SerenityResult; use serenity::Result as SerenityResult;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[description("Clears the chat (maximum 100 messages)")] #[description("Clears the chat (maximum 100 messages)")]

@ -17,13 +17,13 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let second_timezone = args.single::<String>().ok(); let second_timezone = args.single::<String>().ok();
let from_timezone: Tz = if let Some(first) = &first_timezone { let from_timezone: Tz = if let Some(first) = &first_timezone {
forward_error!(ctx, msg.channel_id, first.parse::<Tz>()) crate::forward_error!(ctx, msg.channel_id, first.parse::<Tz>())
} else { } else {
Tz::UTC Tz::UTC
}; };
let to_timezone = if let Some(second) = &second_timezone { let to_timezone = if let Some(second) = &second_timezone {
forward_error!(ctx, msg.channel_id, second.parse::<Tz>()) crate::forward_error!(ctx, msg.channel_id, second.parse::<Tz>())
} else { } else {
Tz::UTC Tz::UTC
}; };
@ -33,7 +33,7 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
} else { } else {
let now = Utc::now(); let now = Utc::now();
if second_timezone.is_some() { if second_timezone.is_some() {
forward_error!( crate::forward_error!(
ctx, ctx,
msg.channel_id, msg.channel_id,
from_timezone.datetime_from_str( from_timezone.datetime_from_str(
@ -43,7 +43,7 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
) )
} else { } else {
let timezone: Tz = "UTC".parse().unwrap(); let timezone: Tz = "UTC".parse().unwrap();
forward_error!( crate::forward_error!(
ctx, ctx,
msg.channel_id, msg.channel_id,
timezone.datetime_from_str( timezone.datetime_from_str(

@ -1,13 +1,13 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{CommandResult, CommandError}; use serenity::framework::standard::{CommandError, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]

@ -6,8 +6,8 @@ use serenity::prelude::*;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use bot_serenityutils::core::{MEDIUM_TIMEOUT, SHORT_TIMEOUT}; use serenity_rich_interaction::core::{MEDIUM_TIMEOUT, SHORT_TIMEOUT};
use bot_serenityutils::ephemeral_message::EphemeralMessage; use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]

@ -6,9 +6,9 @@ use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_channel_for_author, get_music_player_for_guild, is_dj}; use crate::commands::music::{get_channel_for_author, get_music_player_for_guild, is_dj};
use crate::providers::music::player::MusicPlayer; use crate::providers::music::player::MusicPlayer;
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::model::id::ChannelId; use serenity::model::id::ChannelId;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -21,14 +21,14 @@ async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
if is_dj(ctx, guild.id, &msg.author).await? { if is_dj(ctx, guild.id, &msg.author).await? {
ChannelId(arg) ChannelId(arg)
} else { } else {
forward_error!( crate::forward_error!(
ctx, ctx,
msg.channel_id, msg.channel_id,
get_channel_for_author(&msg.author.id, &guild) get_channel_for_author(&msg.author.id, &guild)
) )
} }
} else { } else {
forward_error!( crate::forward_error!(
ctx, ctx,
msg.channel_id, msg.channel_id,
get_channel_for_author(&msg.author.id, &guild) get_channel_for_author(&msg.author.id, &guild)

@ -6,8 +6,8 @@ use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::DJ_CHECK; use crate::commands::music::DJ_CHECK;
use crate::utils::context_data::MusicPlayers; use crate::utils::context_data::MusicPlayers;
use bot_serenityutils::core::SHORT_TIMEOUT; use serenity_rich_interaction::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage; use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]

@ -1,12 +1,12 @@
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[description("Moves a song in the queue from one position to a new one")] #[description("Moves a song in the queue from one position to a new one")]

@ -6,8 +6,8 @@ use serenity::prelude::*;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use bot_serenityutils::core::SHORT_TIMEOUT; use serenity_rich_interaction::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage; use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]

@ -1,12 +1,12 @@
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[description("Removes a song from the queue")] #[description("Removes a song from the queue")]

@ -6,8 +6,8 @@ use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use bot_serenityutils::core::SHORT_TIMEOUT; use serenity_rich_interaction::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage; use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]

@ -6,8 +6,8 @@ use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_music_player_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use bot_serenityutils::core::SHORT_TIMEOUT; use serenity_rich_interaction::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage; use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]

@ -1,95 +1,48 @@
use serenity::async_trait; use serenity::async_trait;
use serenity::client::Context; use serenity::client::Context;
use serenity::model::channel::Reaction; use serenity::model::channel::GuildChannel;
use serenity::model::event::ResumedEvent; use serenity::model::event::ResumedEvent;
use serenity::model::gateway::{Activity, Ready}; use serenity::model::gateway::{Activity, Ready};
use serenity::model::guild::Member; use serenity::model::guild::Member;
use serenity::model::id::{ChannelId, GuildId, MessageId}; use serenity::model::id::{ChannelId, GuildId};
use serenity::model::voice::VoiceState; use serenity::model::voice::VoiceState;
use serenity::prelude::*; use serenity::prelude::*;
use crate::commands::music::get_music_player_for_guild; use crate::commands::music::get_music_player_for_guild;
use crate::utils::context_data::MusicPlayers;
use crate::utils::delete_messages_from_database; use crate::utils::delete_messages_from_database;
use bot_serenityutils::menu::{ use serenity::model::event;
handle_message_delete, handle_message_delete_bulk, handle_reaction_add, handle_reaction_remove, use serenity_rich_interaction::events::RichEventHandler;
start_update_loop, use serenity_rich_interaction::Result;
};
/// Returns the raw event handler built from a rich event handler
pub(crate) struct Handler; pub fn get_raw_event_handler() -> RichEventHandler {
let mut handler = RichEventHandler::default();
#[async_trait] handler
impl EventHandler for Handler { .add_event(|ctx, e: &event::ReadyEvent| Box::pin(ready(ctx, &e.ready)))
async fn cache_ready(&self, ctx: Context, _: Vec<GuildId>) { .add_event(|_ctx, _: &event::ResumedEvent| {
log::info!("Cache Ready"); Box::pin(async {
start_update_loop(&ctx).await; log::info!("Reconnected to Gateway");
if let Err(e) = delete_messages_from_database(&ctx).await { Ok(())
log::error!("Failed to delete expired messages {:?}", e); })
}
}
/// Fired when a message was deleted
async fn message_delete(
&self,
ctx: Context,
channel_id: ChannelId,
message_id: MessageId,
_: Option<GuildId>,
) {
tokio::spawn(async move {
log::trace!("Handling message delete event");
if let Err(e) = handle_message_delete(&ctx, channel_id, message_id).await {
log::error!("Failed to handle event: {:?}", e);
}
log::trace!("Message delete event handled");
});
}
/// Fired when multiple messages were deleted
async fn message_delete_bulk(
&self,
ctx: Context,
channel_id: ChannelId,
message_ids: Vec<MessageId>,
_: Option<GuildId>,
) {
tokio::spawn(async move {
log::trace!("Handling message delete bulk event");
if let Err(e) = handle_message_delete_bulk(&ctx, channel_id, &message_ids).await {
log::error!("Failed to handle event: {:?}", e);
}
log::debug!("Message delte bulk event handled");
});
}
/// Fired when a reaction was added to a message
async fn reaction_add(&self, ctx: Context, reaction: Reaction) {
tokio::spawn(async move {
log::trace!("Handling reaction add event...");
if let Err(e) = handle_reaction_add(&ctx, &reaction).await {
log::error!("Failed to handle event: {:?}", e);
}
log::trace!("Reaction add event handled");
}); });
}
/// Fired when a reaction was added to a message handler
async fn reaction_remove(&self, ctx: Context, reaction: Reaction) { }
tokio::spawn(async move {
log::trace!("Handling reaction remove event");
if let Err(e) = handle_reaction_remove(&ctx, &reaction).await {
log::error!("Failed to handle event: {:?}", e);
}
log::trace!("Reaction remove event handled");
});
}
async fn ready(&self, ctx: Context, ready: Ready) { async fn ready(ctx: &Context, _: &Ready) -> Result<()> {
log::info!("Connected as {}", ready.user.name); log::info!("Ready");
let prefix = dotenv::var("BOT_PREFIX").unwrap_or("~!".to_string()); delete_messages_from_database(&ctx).await?;
let prefix = std::env::var("BOT_PREFIX").unwrap_or("~!".to_string());
ctx.set_activity(Activity::listening(format!("{}help", prefix).as_str())) ctx.set_activity(Activity::listening(format!("{}help", prefix).as_str()))
.await; .await;
} Ok(())
}
pub(crate) struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn resume(&self, _: Context, _: ResumedEvent) { async fn resume(&self, _: Context, _: ResumedEvent) {
log::info!("Reconnected to gateway") log::info!("Reconnected to gateway")
} }
@ -109,12 +62,12 @@ impl EventHandler for Handler {
return; return;
}; };
if let Some(old_id) = old_state.and_then(|c| c.channel_id) { if let Some(old_id) = old_state.clone().and_then(|c| c.channel_id) {
member_count = get_own_channel_member_count(&ctx, &old_id).await; member_count = get_own_channel_member_count(&ctx, old_id).await;
} }
if member_count.is_none() { if member_count.is_none() {
if let Some(new_id) = new_state.channel_id { if let Some(new_id) = new_state.channel_id {
member_count = get_own_channel_member_count(&ctx, &new_id).await; member_count = get_own_channel_member_count(&ctx, new_id).await;
} }
} }
@ -126,14 +79,29 @@ impl EventHandler for Handler {
player.set_leave_flag(count == 0); player.set_leave_flag(count == 0);
} }
} }
// handle disconnects
if let (Some(state), None) = (old_state, new_state.channel_id) {
let current_user = ctx.cache.current_user().await;
if state.user_id == current_user.id {
let mut data = ctx.data.write().await;
let players = data.get_mut::<MusicPlayers>().unwrap();
if let Some(player) = players.remove(&guild_id.0) {
let mut player = player.lock().await;
let _ = player.delete_now_playing().await;
let _ = player.stop().await;
}
}
}
} }
} }
/// Returns the number of members in the channel if it's the bots voice channel /// Returns the number of members in the channel if it's the bots voice channel
async fn get_own_channel_member_count(ctx: &Context, channel_id: &ChannelId) -> Option<usize> { async fn get_own_channel_member_count(ctx: &Context, channel_id: ChannelId) -> Option<usize> {
let channel = ctx.http.get_channel(channel_id.0).await.ok()?; let guild_channel = get_guild_channel(ctx, channel_id).await?;
let guild_channel = channel.guild()?;
let current_user = ctx.http.get_current_user().await.ok()?; let current_user = ctx.cache.current_user().await;
let members = guild_channel.members(&ctx).await.ok()?; let members = guild_channel.members(&ctx).await.ok()?;
let own_channel = members let own_channel = members
@ -148,3 +116,12 @@ async fn get_own_channel_member_count(ctx: &Context, channel_id: &ChannelId) ->
Some(members.len()) Some(members.len())
} }
/// Returns the guild channel for a guild ID
async fn get_guild_channel(ctx: &Context, channel_id: ChannelId) -> Option<GuildChannel> {
if let Some(channel) = ctx.cache.channel(channel_id).await {
return channel.guild();
}
let channel = ctx.http.get_channel(channel_id.0).await.ok()?;
channel.guild()
}

@ -1,14 +1,12 @@
use crate::client::get_client; use crate::client::get_client;
use crate::utils::logging::init_logger; use crate::utils::logging::init_logger;
#[macro_use]
extern crate bot_serenityutils;
mod client; mod client;
mod commands; mod commands;
mod handler; mod handler;
mod messages; mod messages;
mod providers; mod providers;
#[macro_use]
mod utils; mod utils;
pub static VERSION: &str = env!("CARGO_PKG_VERSION"); pub static VERSION: &str = env!("CARGO_PKG_VERSION");

@ -1,9 +1,9 @@
use crate::utils::error::BotResult; use crate::utils::error::BotResult;
use bot_database::models::Media; use bot_database::models::Media;
use bot_serenityutils::menu::{MenuBuilder, Page};
use serenity::builder::CreateMessage; use serenity::builder::CreateMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::model::id::ChannelId; use serenity::model::id::ChannelId;
use serenity_rich_interaction::menu::{MenuBuilder, Page};
use std::time::Duration; use std::time::Duration;
/// Creates a new gifs embed /// Creates a new gifs embed

@ -1,7 +1,7 @@
use crate::utils::context_data::get_database_from_context; use crate::utils::context_data::get_database_from_context;
use crate::utils::error::BotResult; use crate::utils::error::BotResult;
use bot_serenityutils::core::MessageHandle;
use serenity::client::Context; use serenity::client::Context;
use serenity_rich_interaction::core::MessageHandle;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
pub mod gifs; pub mod gifs;

@ -1,13 +1,13 @@
use crate::commands::music::is_dj; use crate::commands::music::is_dj;
use crate::providers::music::player::MusicPlayer; use crate::providers::music::player::MusicPlayer;
use crate::utils::error::BotResult; use crate::utils::error::BotResult;
use bot_serenityutils::core::EXTRA_LONG_TIMEOUT;
use bot_serenityutils::error::SerenityUtilsResult;
use bot_serenityutils::menu::{display_page, Menu, MenuBuilder, Page};
use serenity::builder::{CreateEmbed, CreateMessage}; use serenity::builder::{CreateEmbed, CreateMessage};
use serenity::client::Context; use serenity::client::Context;
use serenity::model::channel::Reaction; use serenity::model::channel::Reaction;
use serenity::model::id::ChannelId; use serenity::model::id::ChannelId;
use serenity_rich_interaction::core::EXTRA_LONG_TIMEOUT;
use serenity_rich_interaction::menu::{display_page, Menu, MenuBuilder, Page};
use serenity_rich_interaction::Result as SerenityUtilsResult;
use std::sync::atomic::{AtomicU8, Ordering}; use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::Mutex; use tokio::sync::Mutex;

@ -1,8 +1,8 @@
use crate::utils::error::BotResult; use crate::utils::error::BotResult;
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::http::Http; use serenity::http::Http;
use serenity::model::prelude::ChannelId; use serenity::model::prelude::ChannelId;
use serenity_rich_interaction::core::SHORT_TIMEOUT;
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use std::sync::Arc; use std::sync::Arc;
/// Creates a not in a voicechannel message /// Creates a not in a voicechannel message

@ -11,12 +11,12 @@ use crate::providers::music::player::MusicPlayer;
use crate::providers::music::queue::Song; use crate::providers::music::queue::Song;
use crate::utils::context_data::{DatabaseContainer, MusicPlayers, Store}; use crate::utils::context_data::{DatabaseContainer, MusicPlayers, Store};
use crate::utils::error::*; 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::builder::CreateMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::model::channel::Reaction; use serenity::model::channel::Reaction;
use serenity_rich_interaction::core::MessageHandle;
use serenity_rich_interaction::menu::{Menu, MenuBuilder, Page};
use serenity_rich_interaction::Result as SerenityUtilsResult;
use std::env; use std::env;
use std::time::Duration; use std::time::Duration;
use tokio::sync::{Mutex, RwLock}; use tokio::sync::{Mutex, RwLock};

@ -1,9 +1,9 @@
use crate::providers::music::queue::Song; use crate::providers::music::queue::Song;
use crate::utils::error::BotResult; use crate::utils::error::BotResult;
use bot_serenityutils::menu::{MenuBuilder, Page};
use serenity::builder::CreateMessage; use serenity::builder::CreateMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::model::id::ChannelId; use serenity::model::id::ChannelId;
use serenity_rich_interaction::menu::{MenuBuilder, Page};
use std::time::Duration; use std::time::Duration;
/// Creates a new queue menu /// Creates a new queue menu

@ -7,8 +7,8 @@ use serenity::{model::channel::Message, prelude::*};
use bot_coreutils::url::get_domain_for_url; use bot_coreutils::url::get_domain_for_url;
use crate::utils::error::BotResult; use crate::utils::error::BotResult;
use bot_serenityutils::menu::{MenuBuilder, Page};
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use serenity_rich_interaction::menu::{MenuBuilder, Page};
use std::time::Duration; use std::time::Duration;
static MAX_RESULTS: usize = 6; static MAX_RESULTS: usize = 6;

@ -1,6 +1,6 @@
use crate::utils::context_data::MusicPlayers; use crate::utils::context_data::MusicPlayers;
use lavalink_rs::gateway::LavalinkEventHandler; use lavalink_rs::gateway::LavalinkEventHandler;
use lavalink_rs::model::{TrackFinish, TrackStart}; use lavalink_rs::model::{PlayerUpdate, Stats, TrackFinish, TrackStart};
use lavalink_rs::LavalinkClient; use lavalink_rs::LavalinkClient;
use serenity::async_trait; use serenity::async_trait;
use serenity::prelude::TypeMapKey; use serenity::prelude::TypeMapKey;
@ -17,6 +17,7 @@ impl LavalinkEventHandler for LavalinkHandler {
async fn track_start(&self, _client: LavalinkClient, event: TrackStart) { async fn track_start(&self, _client: LavalinkClient, event: TrackStart) {
log::info!("Track started!\nGuild: {}", event.guild_id); log::info!("Track started!\nGuild: {}", event.guild_id);
} }
async fn track_finish(&self, _: LavalinkClient, event: TrackFinish) { async fn track_finish(&self, _: LavalinkClient, event: TrackFinish) {
log::info!("Track finished!\nGuild: {}", event.guild_id); log::info!("Track finished!\nGuild: {}", event.guild_id);
let player = { let player = {
@ -35,6 +36,14 @@ impl LavalinkEventHandler for LavalinkHandler {
} }
} }
} }
async fn player_update(&self, _: LavalinkClient, event: PlayerUpdate) {
log::debug!("Received player update event: {:?}", event);
}
async fn stats(&self, _: LavalinkClient, event: Stats) {
log::debug!("Received stats event: {:?}", event);
}
} }
pub struct Lavalink; pub struct Lavalink;

@ -4,8 +4,6 @@ use crate::providers::music::lyrics::get_lyrics;
use crate::providers::music::queue::MusicQueue; use crate::providers::music::queue::MusicQueue;
use crate::utils::context_data::MusicPlayers; use crate::utils::context_data::MusicPlayers;
use crate::utils::error::{BotError, BotResult}; use crate::utils::error::{BotError, BotResult};
use bot_serenityutils::core::{MessageHandle, SHORT_TIMEOUT};
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use lavalink_rs::LavalinkClient; use lavalink_rs::LavalinkClient;
use serenity::prelude::TypeMap; use serenity::prelude::TypeMap;
use serenity::{ use serenity::{
@ -13,6 +11,8 @@ use serenity::{
http::Http, http::Http,
model::id::{ChannelId, GuildId}, model::id::{ChannelId, GuildId},
}; };
use serenity_rich_interaction::core::{MessageHandle, SHORT_TIMEOUT};
use serenity_rich_interaction::ephemeral_message::EphemeralMessage;
use songbird::Songbird; use songbird::Songbird;
use std::mem; use std::mem;
use std::sync::Arc; use std::sync::Arc;
@ -60,9 +60,14 @@ impl MusicPlayer {
msg_channel_id: ChannelId, msg_channel_id: ChannelId,
) -> BotResult<Arc<Mutex<MusicPlayer>>> { ) -> BotResult<Arc<Mutex<MusicPlayer>>> {
let manager = songbird::get(ctx).await.unwrap(); let manager = songbird::get(ctx).await.unwrap();
let (_, connection) = manager.join_gateway(guild_id, voice_channel_id).await; let (handler, connection) = manager.join_gateway(guild_id, voice_channel_id).await;
let connection = connection?; let connection = connection?;
{
let mut handler = handler.lock().await;
handler.deafen(true).await?;
}
let player = { let player = {
let mut data = ctx.data.write().await; let mut data = ctx.data.write().await;
let client = data.get::<Lavalink>().unwrap(); let client = data.get::<Lavalink>().unwrap();

@ -1,5 +1,5 @@
use bot_serenityutils::error::SerenityUtilsError;
use lavalink_rs::error::LavalinkError; use lavalink_rs::error::LavalinkError;
use serenity_rich_interaction::Error as SerenityUtilsError;
use thiserror::Error; use thiserror::Error;
pub type BotResult<T> = Result<T, BotError>; pub type BotResult<T> = Result<T, BotError>;
@ -34,7 +34,7 @@ pub enum BotError {
CliInject, CliInject,
#[error("Serenity Utils Error: {0}")] #[error("Serenity Utils Error: {0}")]
SerenityUtils(#[from] bot_serenityutils::error::SerenityUtilsError), SerenityUtils(#[from] serenity_rich_interaction::Error),
#[error("Track Error: {0}")] #[error("Track Error: {0}")]
TrackError(#[from] songbird::error::TrackError), TrackError(#[from] songbird::error::TrackError),

@ -12,6 +12,19 @@ pub(crate) mod context_data;
pub(crate) mod error; pub(crate) mod error;
pub(crate) mod logging; pub(crate) mod logging;
#[macro_export]
macro_rules! forward_error {
($ctx:expr,$channel_id:expr,$result:expr) => {
match $result {
Err(e) => {
$channel_id.say($ctx, format!("‼️ {}", e)).await?;
return Ok(());
}
Ok(v) => v,
}
};
}
/// Returns the message the given message is a reply to or the message sent before that /// Returns the message the given message is a reply to or the message sent before that
pub async fn get_previous_message_or_reply( pub async fn get_previous_message_or_reply(
ctx: &Context, ctx: &Context,

Loading…
Cancel
Save