Skip to content

Commit

Permalink
Add new Example about Eventing and Timing (#568)
Browse files Browse the repository at this point in the history
  • Loading branch information
Lakelezz authored and arqunis committed May 24, 2019
1 parent aae22a2 commit 10b9cc2
Show file tree
Hide file tree
Showing 2 changed files with 289 additions and 0 deletions.
13 changes: 13 additions & 0 deletions examples/12_timing_and_events/Cargo.toml
@@ -0,0 +1,13 @@
[package]
name = "12_timing_and_events"
version = "0.1.0"
authors = ["my name <my@email.address>"]
edition = "2018"

[dependencies.serenity]
features = ["framework", "standard_framework", "rustls_backend"]
path = "../../"

[dependencies]
hey_listen = "0.4.0"
white_rabbit = "0.1.1"
276 changes: 276 additions & 0 deletions examples/12_timing_and_events/src/main.rs
@@ -0,0 +1,276 @@
//! This example will showcase one way on how to extend Serentiy with a
//! time-scheduler and an event-trigger-system.
//! We will create a remind-me command that will send a message after a
//! a demanded amount of time. Once the message has been sent, the user can
//! react to it, triggering an event to send another message.
use std::{collections::HashSet, env, hash::{BuildHasher, Hash, Hasher},
sync::Arc,
};
use serenity::{
prelude::*,
framework::standard::{
Args, CommandResult, CommandGroup,
DispatchError, HelpOptions, help_commands, StandardFramework,
macros::{command, group, help},
},
http::Http,
model::prelude::*,
};
// We will use this crate as event dispatcher.
use hey_listen::sync::{ParallelDispatcher as Dispatcher,
ParallelDispatcherRequest as DispatcherRequest};
// And this crate to schedule our tasks.
use white_rabbit::{Utc, Scheduler, DateResult, Duration};

// This enum represents possible events a listener might wait for.
// In this case, we want to dispatch an event when a reaction is added.
// Serenity's event-enum is not suitable for this.
// First it offers too many variants we do not need, but most importantly,
// it lacks the `Default`-trait which makes sense
// as the enum-fields have no clear logical default value. But without it,
// constructing mock-variants becomes difficult.
//
// As a result, we make our own slick event-enum!
#[derive(Clone)]
enum DispatchEvent {
ReactEvent(MessageId, UserId),
}

// We need to implement equality for our enum.
// One could test variants only. In this case, we want to know who reacted
// on which message.
impl PartialEq for DispatchEvent {
fn eq(&self, other: &DispatchEvent) -> bool {
match (self, other) {
(DispatchEvent::ReactEvent(self_message_id, self_user_id),
DispatchEvent::ReactEvent(other_message_id, other_user_id)) => {
self_message_id == other_message_id &&
self_user_id == other_user_id
}
}
}
}

impl Eq for DispatchEvent {}

// See following Clippy-lint:
// https://rust-lang.github.io/rust-clippy/master/index.html#derive_hash_xor_eq
impl Hash for DispatchEvent {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
DispatchEvent::ReactEvent(msg_id, user_id) => {
msg_id.hash(state);
user_id.hash(state);
}
}
}
}

struct DispatcherKey;
impl TypeMapKey for DispatcherKey {
type Value = Arc<RwLock<Dispatcher<DispatchEvent>>>;
}

struct SchedulerKey;
impl TypeMapKey for SchedulerKey {
type Value = Arc<RwLock<Scheduler>>;
}

struct Handler;
impl EventHandler for Handler {
// We want to dispatch an event whenever a new reaction has been added.
fn reaction_add(&self, context: Context, reaction: Reaction) {
let dispatcher = {
let mut context = context.data.write();
context.get_mut::<DispatcherKey>().expect("Expected Dispatcher.").clone()
};

dispatcher.write().dispatch_event(
&DispatchEvent::ReactEvent(reaction.message_id, reaction.user_id));
}
}

group!({
name: "remind_me",
options: {
prefixes: ["rm", "reminder"],
},
commands: [set_reminder],
});

#[help]
fn my_help(
context: &mut Context,
msg: &Message,
args: Args,
help_options: &'static HelpOptions,
groups: &[&'static CommandGroup],
owners: HashSet<UserId, impl BuildHasher>
) -> CommandResult {
help_commands::with_embeds(context, msg, args, &help_options, groups, owners)
}

fn main() {
// Configure the client with your Discord bot token in the environment.
let token = env::var("DISCORD_TOKEN").expect(
"Expected a token in the environment",
);
let mut client = Client::new(&token, Handler)
.expect("Err creating client");

{
let mut data = client.data.write();
// We create a new scheduler with 4 internal threads. Why 4? It really
// is just an arbitrary number, you are often better setting this
// based on your CPU.
// When a task is due, a thread from the threadpool will be used to
// avoid blocking the scheduler thread.
let scheduler = Scheduler::new(4);
let scheduler = Arc::new(RwLock::new(scheduler));

let mut dispatcher: Dispatcher<DispatchEvent> = Dispatcher::default();
// Once receiving an event to dispatch, the amount of threads
// set via `num_threads` will dispatch in parallel.
dispatcher.num_threads(4).expect("Could not construct threadpool");

data.insert::<DispatcherKey>(Arc::new(RwLock::new(dispatcher)));
data.insert::<SchedulerKey>(scheduler);
}

// We will fetch your bot's id.
let bot_id = match client.cache_and_http.http.get_current_application_info() {
Ok(info) => {
info.id
},
Err(why) => panic!("Could not access application info: {:?}", why),
};

client.with_framework(
// Configures the client, allowing for options to mutate how the
// framework functions.
StandardFramework::new()
.configure(|c| c
.with_whitespace(true)
.on_mention(Some(bot_id))
.prefix("~")
.delimiters(vec![", ", ","]))
.on_dispatch_error(|ctx, msg, error| {
if let DispatchError::Ratelimited(seconds) = error {
let _ = msg.channel_id.say(&ctx.http, &format!("Try this again in {} seconds.", seconds));
}
})
.after(|_ctx, _msg, cmd_name, error| {

if let Err(why) = error {
println!("Error in {}: {:?}", cmd_name, why);
}
})
.help(&MY_HELP_HELP_COMMAND)
.group(&REMIND_ME_GROUP)
);

if let Err(why) = client.start() {
println!("Client error: {:?}", why);
}
}

// Just a helper-function for creating the closure we want to use as listener.
// It saves us from writing the same trigger twice for repeated and non-repeated
// tasks (see remind-me command below).
fn thanks_for_reacting(http: Arc<Http>, channel: ChannelId) ->
Box<Fn(&DispatchEvent) -> Option<DispatcherRequest> + Send + Sync> {

Box::new(move |_| {
if let Err(why) = channel.say(&http, "Thanks for reacting!") {
println!("Could not send message: {:?}", why);
}

Some(DispatcherRequest::StopListening)
})
}

#[command]
#[aliases("add")]
fn set_reminder(context: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
// It might be smart to set a moderately high minimum value for `time`
// to avoid abuse like tasks that repeat every 100ms, especially since
// channels have send-message rate limits.
let time: u64 = args.single()?;
let repeat: bool = args.single()?;
let args = args.rest().to_string();

let scheduler = {
let mut context = context.data.write();
context.get_mut::<SchedulerKey>().expect("Expected Scheduler.").clone()
};

let dispatcher = {
let mut context = context.data.write();
context.get_mut::<DispatcherKey>().expect("Expected Dispatcher.").clone()
};

let http = context.http.clone();
let msg = msg.clone();

let mut scheduler = scheduler.write();

// First, we check if the user wants a repeated task or not.
if repeat {
// Chrono's duration can also be negative
// and therefore we cast to `i64`.
scheduler.add_task_duration(Duration::milliseconds(time as i64), move |_| {
let bot_msg = match msg.channel_id.say(&http, &args) {
Ok(msg) => msg,
// We could not send the message, thus we will try sending it
// again in five seconds.
// It might be wise to keep a counter for maximum tries.
// If the channel got deleted, trying to send a message will
// always fail.
Err(why) => {
println!("Error sending message: {:?}.", why);

return DateResult::Repeat(
Utc::now() + Duration::milliseconds(5000))
},
};

let http = http.clone();

// We add a function to dispatch for a certain event.
dispatcher.write()
.add_fn(DispatchEvent::ReactEvent(bot_msg.id, msg.author.id),
// The `thanks_for_reacting`-function creates a function
// to schedule.
thanks_for_reacting(http, bot_msg.channel_id));

// We return that our date shall happen again, therefore we need
// to tell when this shall be.
DateResult::Repeat(Utc::now() + Duration::milliseconds(time as i64))
});
} else {
// Pretty much identical with the `true`-case except for the returned
// variant.
scheduler.add_task_duration(Duration::milliseconds(time as i64), move |_| {
let bot_msg = match msg.channel_id.say(&http, &args) {
Ok(msg) => msg,
Err(why) => {
println!("Error sending message: {:?}.", why);

return DateResult::Repeat(
Utc::now() + Duration::milliseconds(5000)
)
},
};
let http = http.clone();

dispatcher.write()
.add_fn(DispatchEvent::ReactEvent(bot_msg.id, msg.author.id),
thanks_for_reacting(http, bot_msg.channel_id));

// The task is done and that's it, we do not to repeat it.
DateResult::Done
});
};

Ok(())
}

0 comments on commit 10b9cc2

Please sign in to comment.