382 lines
14 KiB
Rust
382 lines
14 KiB
Rust
//! Requires the 'framework' feature flag be enabled in your project's `Cargo.toml`.
|
|
//!
|
|
//! This can be enabled by specifying the feature in the dependency section:
|
|
//!
|
|
//! ```toml
|
|
//! [dependencies.serenity]
|
|
//! git = "https://github.com/serenity-rs/serenity.git"
|
|
//! features = ["framework", "standard_framework"]
|
|
//! ```
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::env;
|
|
use std::fmt::Write;
|
|
use std::sync::Arc;
|
|
|
|
use rand::seq::SliceRandom;
|
|
use rand::thread_rng;
|
|
use serenity::async_trait;
|
|
use serenity::builder::{CreateEmbed, CreateEmbedFooter, CreateMessage};
|
|
use serenity::cache::Cache;
|
|
use serenity::framework::standard::buckets::LimitedFor;
|
|
use serenity::framework::standard::macros::{command, group, help, hook};
|
|
use serenity::framework::standard::{
|
|
help_commands, Args, BucketBuilder, CommandGroup, CommandResult, Configuration, DispatchError,
|
|
HelpOptions, StandardFramework,
|
|
};
|
|
use serenity::gateway::ShardManager;
|
|
use serenity::http::Http;
|
|
use serenity::model::channel::Message;
|
|
use serenity::model::gateway::{GatewayIntents, Ready};
|
|
use serenity::model::id::UserId;
|
|
use serenity::prelude::*;
|
|
|
|
// A container type is created for inserting into the Client's `data`, which allows for data to be
|
|
// accessible across all events and framework commands, or anywhere else that has a copy of the
|
|
// `data` Arc.
|
|
struct ShardManagerContainer;
|
|
impl TypeMapKey for ShardManagerContainer {
|
|
type Value = Arc<ShardManager>;
|
|
}
|
|
|
|
struct CommandCounter;
|
|
impl TypeMapKey for CommandCounter {
|
|
type Value = HashMap<String, u64>;
|
|
}
|
|
|
|
struct HttpCache(Arc<Cache>, Http);
|
|
impl HttpCache {
|
|
fn as_ref(&self) -> (&Arc<Cache>, &Http) {
|
|
(&self.0, &self.1)
|
|
}
|
|
}
|
|
impl TypeMapKey for HttpCache {
|
|
type Value = HttpCache;
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct Champion {
|
|
name: String,
|
|
url: String,
|
|
}
|
|
|
|
struct ChampionList;
|
|
impl TypeMapKey for ChampionList {
|
|
type Value = Vec<Champion>;
|
|
}
|
|
|
|
struct Handler;
|
|
|
|
#[async_trait]
|
|
impl EventHandler for Handler {
|
|
async fn ready(&self, _: Context, ready: Ready) {
|
|
println!("{} is connected!", ready.user.name);
|
|
}
|
|
}
|
|
|
|
#[group]
|
|
#[commands(commands, champions)]
|
|
struct General;
|
|
|
|
// The framework provides two built-in help commands for you to use. But you can also make your own
|
|
// customized help command that forwards to the behaviour of either of them.
|
|
#[help]
|
|
// This replaces the information that a user can pass a command-name as argument to gain specific
|
|
// information about it.
|
|
#[individual_command_tip = "Hello! If you want more information about a specific command, just pass the command as argument."]
|
|
// Some arguments require a `{}` in order to replace it with contextual information.
|
|
// In this case our `{}` refers to a command's name.
|
|
#[command_not_found_text = "Could not find: `{}`."]
|
|
// Define the maximum Levenshtein-distance between a searched command-name and commands. If the
|
|
// distance is lower than or equal the set distance, it will be displayed as a suggestion.
|
|
// Setting the distance to 0 will disable suggestions.
|
|
#[max_levenshtein_distance(3)]
|
|
// When you use sub-groups, Serenity will use the `indention_prefix` to indicate how deeply an item
|
|
// is indented. The default value is "-", it will be changed to "+".
|
|
#[indention_prefix = "+"]
|
|
// On another note, you can set up the help-menu-filter-behaviour.
|
|
// Here are all possible settings shown on all possible options.
|
|
// First case is if a user lacks permissions for a command, we can hide the command.
|
|
#[lacking_permissions = "Hide"]
|
|
// If the user is nothing but lacking a certain role, we just display it.
|
|
#[lacking_role = "Nothing"]
|
|
// The last `enum`-variant is `Strike`, which ~~strikes~~ a command.
|
|
#[wrong_channel = "Strike"]
|
|
// Serenity will automatically analyse and generate a hint/tip explaining the possible cases of
|
|
// ~~strikethrough-commands~~, but only if `strikethrough_commands_tip_in_{dm, guild}` aren't
|
|
// specified. If you pass in a value, it will be displayed instead.
|
|
async fn my_help(
|
|
context: &Context,
|
|
msg: &Message,
|
|
args: Args,
|
|
help_options: &'static HelpOptions,
|
|
groups: &[&'static CommandGroup],
|
|
owners: HashSet<UserId>,
|
|
) -> CommandResult {
|
|
let _ = help_commands::with_embeds(context, msg, args, help_options, groups, owners).await;
|
|
Ok(())
|
|
}
|
|
|
|
#[hook]
|
|
async fn before(ctx: &Context, msg: &Message, command_name: &str) -> bool {
|
|
println!(
|
|
"Got command '{}' by user '{}'",
|
|
command_name, msg.author.name
|
|
);
|
|
|
|
// Increment the number of times this command has been run once. If the command's name does not
|
|
// exist in the counter, add a default value of 0.
|
|
let mut data = ctx.data.write().await;
|
|
let counter = data
|
|
.get_mut::<CommandCounter>()
|
|
.expect("Expected CommandCounter in TypeMap.");
|
|
let entry = counter.entry(command_name.to_string()).or_insert(0);
|
|
*entry += 1;
|
|
|
|
true // if `before` returns false, command processing doesn't happen.
|
|
}
|
|
|
|
#[hook]
|
|
async fn after(_ctx: &Context, _msg: &Message, command_name: &str, command_result: CommandResult) {
|
|
match command_result {
|
|
Ok(()) => println!("Processed command '{command_name}'"),
|
|
Err(why) => println!("Command '{command_name}' returned error {why:?}"),
|
|
}
|
|
}
|
|
|
|
#[hook]
|
|
async fn unknown_command(_ctx: &Context, _msg: &Message, unknown_command_name: &str) {
|
|
println!("Could not find command named '{unknown_command_name}'");
|
|
}
|
|
|
|
#[hook]
|
|
async fn normal_message(_ctx: &Context, msg: &Message) {
|
|
println!("Message is not a command '{}'", msg.content);
|
|
}
|
|
|
|
#[hook]
|
|
async fn delay_action(ctx: &Context, msg: &Message) {
|
|
// You may want to handle a Discord rate limit if this fails.
|
|
let _ = msg.react(ctx, '⏱').await;
|
|
}
|
|
|
|
#[hook]
|
|
async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError, _command_name: &str) {
|
|
if let DispatchError::Ratelimited(info) = error {
|
|
// We notify them only once.
|
|
if info.is_first_try {
|
|
let _ = msg
|
|
.channel_id
|
|
.say(
|
|
&ctx.http,
|
|
&format!("Try this again in {} seconds.", info.as_secs()),
|
|
)
|
|
.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
// You can construct a hook without the use of a macro, too.
|
|
// This requires some boilerplate though and the following additional import.
|
|
use serenity::futures::future::BoxFuture;
|
|
use serenity::FutureExt;
|
|
fn _dispatch_error_no_macro<'fut>(
|
|
ctx: &'fut mut Context,
|
|
msg: &'fut Message,
|
|
error: DispatchError,
|
|
_command_name: &str,
|
|
) -> BoxFuture<'fut, ()> {
|
|
async move {
|
|
if let DispatchError::Ratelimited(info) = error {
|
|
if info.is_first_try {
|
|
let _ = msg
|
|
.channel_id
|
|
.say(
|
|
&ctx.http,
|
|
&format!("Try this again in {} seconds.", info.as_secs()),
|
|
)
|
|
.await;
|
|
}
|
|
};
|
|
}
|
|
.boxed()
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() {
|
|
let _ = dotenv::from_filename(".env");
|
|
|
|
let champions: Vec<Champion> =
|
|
serde_json::from_str::<HashMap<String, String>>(include_str!("../champions.json"))
|
|
.expect("failed to parse champions")
|
|
.into_iter()
|
|
.map(|(name, url)| Champion { name, url })
|
|
.collect();
|
|
|
|
// 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 http = Http::new(&token);
|
|
|
|
// We will fetch your bot's owners and id
|
|
let (owners, bot_id) = match http.get_current_application_info().await {
|
|
Ok(info) => {
|
|
let mut owners = HashSet::new();
|
|
if let Some(team) = info.team {
|
|
owners.insert(team.owner_user_id);
|
|
} else if let Some(owner) = &info.owner {
|
|
owners.insert(owner.id);
|
|
}
|
|
match http.get_current_user().await {
|
|
Ok(bot_id) => (owners, bot_id.id),
|
|
Err(why) => panic!("Could not access the bot id: {:?}", why),
|
|
}
|
|
}
|
|
Err(why) => panic!("Could not access application info: {:?}", why),
|
|
};
|
|
|
|
let framework = StandardFramework::new()
|
|
// Set a function to be called prior to each command execution. This provides the context
|
|
// of the command, the message that was received, and the full name of the command that
|
|
// will be called.
|
|
//
|
|
// Avoid using this to determine whether a specific command should be executed. Instead,
|
|
// prefer using the `#[check]` macro which gives you this functionality.
|
|
//
|
|
// **Note**: Async closures are unstable, you may use them in your application if you are
|
|
// fine using nightly Rust. If not, we need to provide the function identifiers to the
|
|
// hook-functions (before, after, normal, ...).
|
|
.before(before)
|
|
// Similar to `before`, except will be called directly _after_ command execution.
|
|
.after(after)
|
|
// Set a function that's called whenever an attempted command-call's command could not be
|
|
// found.
|
|
.unrecognised_command(unknown_command)
|
|
// Set a function that's called whenever a message is not a command.
|
|
.normal_message(normal_message)
|
|
// Set a function that's called whenever a command's execution didn't complete for one
|
|
// reason or another. For example, when a user has exceeded a rate-limit or a command can
|
|
// only be performed by the bot owner.
|
|
.on_dispatch_error(dispatch_error)
|
|
// Can't be used more than 2 times per 30 seconds, with a 5 second delay applying per
|
|
// channel. Optionally `await_ratelimits` will delay until the command can be executed
|
|
// instead of cancelling the command invocation.
|
|
.bucket(
|
|
"complicated",
|
|
BucketBuilder::default()
|
|
.limit(2)
|
|
.time_span(30)
|
|
.delay(5)
|
|
// The target each bucket will apply to.
|
|
.limit_for(LimitedFor::Channel)
|
|
// The maximum amount of command invocations that can be delayed per target.
|
|
// Setting this to 0 (default) will never await/delay commands and cancel the invocation.
|
|
.await_ratelimits(1)
|
|
// A function to call when a rate limit leads to a delay.
|
|
.delay_action(delay_action),
|
|
)
|
|
.await
|
|
// The `#[group]` macro generates `static` instances of the options set for the group.
|
|
// They're made in the pattern: `#name_GROUP` for the group instance and `#name_GROUP_OPTIONS`.
|
|
// #name is turned all uppercase
|
|
.help(&MY_HELP)
|
|
.group(&GENERAL_GROUP);
|
|
|
|
framework.configure(
|
|
Configuration::new()
|
|
.with_whitespace(true)
|
|
.on_mention(Some(bot_id))
|
|
.prefix("!")
|
|
// In this case, if "," would be first, a message would never be delimited at ", ",
|
|
// forcing you to trim your arguments if you want to avoid whitespaces at the start of
|
|
// each.
|
|
.delimiters(vec![", ", ","])
|
|
// Sets the bot's owners. These will be used for commands that are owners only.
|
|
.owners(owners),
|
|
);
|
|
|
|
// For this example to run properly, the "Presence Intent" and "Server Members Intent" options
|
|
// need to be enabled.
|
|
// These are needed so the `required_permissions` macro works on the commands that need to use
|
|
// it.
|
|
// You will need to enable these 2 options on the bot application, and possibly wait up to 5
|
|
// minutes.
|
|
let intents = GatewayIntents::all();
|
|
let mut client = Client::builder(&token, intents)
|
|
.event_handler(Handler)
|
|
.framework(framework)
|
|
.type_map_insert::<CommandCounter>(HashMap::default())
|
|
.type_map_insert::<HttpCache>(HttpCache(Arc::new(Cache::new()), Http::new(&token)))
|
|
.type_map_insert::<ChampionList>(champions)
|
|
.await
|
|
.expect("Failed to create client");
|
|
|
|
{
|
|
let mut data = client.data.write().await;
|
|
data.insert::<ShardManagerContainer>(Arc::clone(&client.shard_manager));
|
|
}
|
|
|
|
if let Err(why) = client.start().await {
|
|
println!("Client error: {why:?}");
|
|
}
|
|
}
|
|
|
|
// Commands can be created via the attribute `#[command]` macro.
|
|
#[command]
|
|
// Options are passed via subsequent attributes.
|
|
// Make this command use the "complicated" bucket.
|
|
#[bucket = "complicated"]
|
|
async fn commands(ctx: &Context, msg: &Message) -> CommandResult {
|
|
let mut contents = "Commands used:\n".to_string();
|
|
|
|
let data = ctx.data.read().await;
|
|
let counter = data
|
|
.get::<CommandCounter>()
|
|
.expect("Expected CommandCounter in TypeMap.");
|
|
|
|
for (name, amount) in counter {
|
|
writeln!(contents, "- {name}: {amount}")?;
|
|
}
|
|
|
|
msg.channel_id.say(&ctx.http, &contents).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Repeats what the user passed as argument but ensures that user and role mentions are replaced
|
|
// with a safe textual alternative.
|
|
// In this example channel mentions are excluded via the `ContentSafeOptions`.
|
|
#[command]
|
|
#[description("generate n random champions")]
|
|
async fn champions(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
|
|
let data = ctx.data.read().await;
|
|
let cache = data
|
|
.get::<HttpCache>()
|
|
.expect("Expected HttpCache in TypeMap.");
|
|
|
|
let champions = data
|
|
.get::<ChampionList>()
|
|
.expect("Expected ChampionList in TypeMap.");
|
|
|
|
let mut shuffle = (0..champions.len()).collect::<Vec<usize>>();
|
|
shuffle.shuffle(&mut thread_rng());
|
|
|
|
let n = args.single::<usize>().unwrap_or(5);
|
|
|
|
let embeds = shuffle
|
|
.into_iter()
|
|
.take(n)
|
|
.map(|i| {
|
|
let champion = champions[i].clone();
|
|
CreateEmbed::new()
|
|
.thumbnail(champion.url)
|
|
.footer(CreateEmbedFooter::new(champion.name))
|
|
})
|
|
.collect();
|
|
|
|
msg.channel_id
|
|
.send_message(cache.as_ref(), CreateMessage::new().add_embeds(embeds))
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|