Initial Commit

This commit is contained in:
Oupson 2020-08-24 17:11:06 +02:00
commit 8055cbb78f
13 changed files with 1017 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
/target
/.vs/*
!/.vs/settings.json
/logging/
Cargo.lock
Conf.toml

5
.vs/settings.json Normal file
View File

@ -0,0 +1,5 @@
{
"rust.features": [
"music"
]
}

23
Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "rusty_bot"
version = "0.1.0"
authors = ["oupson"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
music = ["serenity/voice"]
[dependencies]
serenity = { version = "0.9.0-rc.0" }
toml = "0.5.6"
serde = { version = "1.0", features = ["derive"] }
reqwest = "0.10.7"
rand = "0.7.3"
lazy_static = "1.4.0"
async-trait = "0.1.36"
tokio = { version = "0.2", features = ["full"] }
futures = "0.3"
chrono = "0.4.15"
serde_json = "1.0"

34
src/api.rs Normal file
View File

@ -0,0 +1,34 @@
use crate::commands::Result;
use serenity::{model::channel::Message, prelude::Context};
/// Send a reply to the channel the message was received on.
pub(crate) async fn send_reply<'m, S: std::string::ToString>(
ctx: &Context,
msg: &Message,
message: S,
) -> Result<serenity::model::channel::Message> {
Ok(msg.channel_id.say(ctx, message.to_string()).await?)
}
pub(crate) async fn send_splitted_by_lines_in_card<S: std::string::ToString>(
ctx: &Context,
msg: &Message,
message: S,
) -> Result<()> {
let mut buffer = String::from("\x60\x60\x60\n");
for line in message.to_string().lines() {
if buffer.len() + 4 + line.len() >= 2000 {
buffer += "\n\x60\x60\x60";
send_reply(ctx, msg, &buffer).await?;
buffer = String::from("\x60\x60\x60\n");
buffer += line;
buffer += "\n";
} else {
buffer += line;
buffer += "\n";
}
}
buffer += "\n\x60\x60\x60";
send_reply(ctx, msg, &buffer).await?;
Ok(())
}

1
src/commands/admin.rs Normal file
View File

@ -0,0 +1 @@

269
src/commands/general.rs Normal file
View File

@ -0,0 +1,269 @@
use crate::{api, debugln};
use futures::StreamExt;
use serenity::{
framework::standard::{
macros::{command, group},
ArgError, Args, CommandResult,
},
model::prelude::*,
prelude::*,
};
#[group]
#[commands(longcode, image, older, ping, invite, infos, error)]
pub struct General;
#[command]
pub async fn error(_ctx: &Context, _msg: &Message) -> CommandResult {
std::fs::File::open("foobar.txt")?;
Ok(())
}
#[command]
#[description = "Split a huge code"]
#[bucket = "longcode"]
pub async fn longcode(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
if let Err(e) = _longcode(ctx, msg, args).await {
eprintln!("Error in longcode : {:?}", e);
}
Ok(())
}
async fn _longcode(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands::Result<()> {
debugln!("_longcode : {:?}", args);
if let Ok(language) = args.single::<String>() {
if !msg.attachments.is_empty() {
let att = msg.attachments[0].clone();
let text = reqwest::get(&att.url).await?.text().await?;
let header = format!("\x60\x60\x60{}\n", language);
let mut buf = header.clone();
for line in text.lines() {
if buf.len() + 4 + line.len() >= 2000 {
buf += "\n\x60\x60\x60";
api::send_reply(ctx, msg, buf).await?;
buf = header.clone();
buf += line;
buf += "\n";
} else {
buf += line;
buf += "\n";
}
}
buf += "\n\x60\x60\x60";
api::send_reply(ctx, msg, buf).await?;
} else {
api::send_reply(ctx, msg, "Error, missing code attachement").await?;
}
} else {
api::send_reply(
ctx,
msg,
"Error : Missing language (needed), Example : \n\t\x60?longcode rust\x60",
)
.await?;
}
Ok(())
}
#[command]
#[description("Print the bot invite link")]
async fn invite(ctx: &Context, msg: &Message) -> CommandResult {
let invite = unsafe { &crate::INVITE_URL };
if let Some(invite_url) = invite {
if let Err(e) = api::send_reply(ctx, msg, invite_url).await {
eprintln!("Error when sending invite : {:?}", e);
}
} else if let Err(e) =
api::send_reply(ctx, msg, "Error : Invite URL is not specified in config").await
{
eprintln!("Error in invite : {:?}", e);
}
Ok(())
}
#[command]
#[description("Pong !")]
async fn ping(ctx: &Context, msg: &Message) -> CommandResult {
let now = std::time::Instant::now();
msg.channel_id.say(&ctx.http, "Pong!").await?;
let elapsed = now.elapsed();
msg.channel_id
.say(
&ctx.http,
format!("Time elapsed : {}ms", elapsed.as_millis()),
)
.await?;
Ok(())
}
#[command]
#[description = "Find who is the older"]
pub async fn older(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
if let Err(e) = _older(ctx, msg, args).await {
eprintln!("Error in older : {:?}", e);
}
Ok(())
}
async fn _older(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands::Result<()> {
let mut number = 10;
if !args.is_empty() {
number = args.single::<usize>()?;
}
if let Some(guild) = msg.guild_id {
let mut members = guild.members_iter(&ctx).boxed();
let mut m: Vec<User> = Vec::with_capacity(number + 1);
while let Some(member_result) = members.next().await {
match member_result {
Ok(member) => {
if m.is_empty() {
m.push(member.user)
} else {
let mut added = false;
let user = member.user;
for i in 0..m.len() {
if m[i].created_at() > user.created_at() {
m.insert(i, user.clone());
if m.len() == number + 1 {
m.remove(number);
}
added = true;
break;
}
}
if !added && m.len() < number {
m.push(user);
}
}
}
Err(error) => eprintln!("Uh oh! Error: {}", error),
}
}
let mut res = String::new();
for (i, u) in m.iter().enumerate() {
res += &format!("{}. {} ({})\n", i + 1, u.name, u.created_at());
}
api::send_splitted_by_lines_in_card(&ctx, msg, res).await?;
}
Ok(())
}
#[command]
#[description = "Print bot infos"]
async fn infos(ctx: &Context, msg: &Message) -> CommandResult {
let res = format!(
"{} v{}, by {}\nFeatures : {:?}\nMode : {}",
option_env!("CARGO_PKG_NAME").unwrap_or("unknown"),
option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"),
option_env!("CARGO_PKG_AUTHORS").unwrap_or("unknows"),
get_features(),
{
if cfg!(debug_assertions) {
"debug"
} else {
"release"
}
}
);
if let Err(e) = api::send_splitted_by_lines_in_card(ctx, msg, res).await {
eprintln!("Error when sending bot infos : {:?}", e);
}
Ok(())
}
fn get_features<'m>() -> Vec<&'m str> {
let mut res = Vec::new();
if cfg!(feature = "music") {
res.push("music");
}
res
}
#[command]
#[description = "Image"]
#[bucket = "image"]
pub async fn image(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
if let Err(e) = _image(ctx, msg, args).await {
eprintln!("Error in image : {:?}", e);
}
Ok(())
}
async fn _image(ctx: &Context, msg: &Message, mut args: Args) -> crate::commands::Result<()> {
match args.single::<Image>() {
Ok(image) => {
msg.channel_id
.send_message(&ctx.http, |m| {
m.embed(|e| {
image.embed(e);
e
});
m
})
.await?;
}
Err(e) => {
if let ArgError::Parse(e) = e {
msg.channel_id.say(ctx, e).await?;
} else {
return Err(Box::new(e));
};
}
};
Ok(())
}
// TODO JSON FILE
enum Image {
HackerMan(),
Koding(),
BorrowCheckFailled(),
SafetyCheck(),
FerisBurn(),
EmploiStable(),
}
impl Image {
fn embed(&self, embed: &mut serenity::builder::CreateEmbed) {
match self {
Image::HackerMan() => {
embed.image("https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/d8328f77-2bec-4a20-9483-ea76dd62985e/dan31sc-80f18518-0ef0-4bdc-9c0a-11c9e62b769f.png?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ1cm46YXBwOjdlMGQxODg5ODIyNjQzNzNhNWYwZDQxNWVhMGQyNmUwIiwic3ViIjoidXJuOmFwcDo3ZTBkMTg4OTgyMjY0MzczYTVmMGQ0MTVlYTBkMjZlMCIsImF1ZCI6WyJ1cm46c2VydmljZTpmaWxlLmRvd25sb2FkIl0sIm9iaiI6W1t7InBhdGgiOiIvZi9kODMyOGY3Ny0yYmVjLTRhMjAtOTQ4My1lYTc2ZGQ2Mjk4NWUvZGFuMzFzYy04MGYxODUxOC0wZWYwLTRiZGMtOWMwYS0xMWM5ZTYyYjc2OWYucG5nIn1dXX0.upSIXFazVoJxWpPPle4gmgwfJgx7Gvc603Sbbe-KJB0");
}
Image::Koding() => {
embed.image("https://i.kym-cdn.com/photos/images/original/001/739/593/45d.jpg");
}
Image::BorrowCheckFailled() => {
embed.image("https://cdn.discordapp.com/attachments/592452150517301248/721104919058448434/brrowchk.jpg");
}
Image::SafetyCheck() => {
embed.image("https://cdn.discordapp.com/attachments/592452150517301248/695056442704527451/safety-check.png");
}
Image::FerisBurn() => {
embed.image("https://oupson.fr/rusty_bot/static_files/ferrisburn.gif");
}
Image::EmploiStable() => {
embed.image("https://i.imgur.com/q5scLGs.jpg");
}
};
}
}
impl std::str::FromStr for Image {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"hackerman" => Ok(Self::HackerMan()),
"koding" => Ok(Self::Koding()),
"borrowcheckfailled" => Ok(Self::BorrowCheckFailled()),
"safetycheck" => Ok(Self::SafetyCheck()),
"ferrisburn" => Ok(Self::FerisBurn()),
"emploistable" => Ok(Self::EmploiStable()),
_ => Err(format!("{} is not an image", s)),
}
}
}

8
src/commands/mod.rs Normal file
View File

@ -0,0 +1,8 @@
pub(crate) mod admin;
pub(crate) mod general;
pub(crate) mod roulette;
#[cfg(feature = "music")]
pub(crate) mod music;
pub(crate) type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

233
src/commands/music.rs Normal file
View File

@ -0,0 +1,233 @@
use serenity::{
client::{bridge::voice::ClientVoiceManager, Context},
framework::standard::{
macros::{command, group},
Args, CommandResult,
},
model::{channel::Message, misc::Mentionable},
prelude::*,
voice, Result as SerenityResult,
};
use std::sync::Arc;
pub(crate) struct VoiceManager;
impl TypeMapKey for VoiceManager {
type Value = Arc<Mutex<ClientVoiceManager>>;
}
#[group]
#[commands(join, leave, play, stop)]
struct Music;
#[command]
#[description("Join the channel you are connected")]
async fn join(ctx: &Context, msg: &Message) -> CommandResult {
let guild = match msg.guild(&ctx.cache).await {
Some(guild) => guild,
None => {
check_msg(msg.channel_id.say(&ctx.http, "DMs not supported").await);
return Ok(());
}
};
let guild_id = guild.id;
let channel_id = guild
.voice_states
.get(&msg.author.id)
.and_then(|voice_state| voice_state.channel_id);
let connect_to = match channel_id {
Some(channel) => channel,
None => {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
return Ok(());
}
};
let manager_lock = ctx
.data
.read()
.await
.get::<VoiceManager>()
.cloned()
.expect("Expected VoiceManager in TypeMap.");
let mut manager = manager_lock.lock().await;
if manager.join(guild_id, connect_to).is_some() {
check_msg(
msg.channel_id
.say(&ctx.http, &format!("Joined {}", connect_to.mention()))
.await,
);
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Error joining the channel")
.await,
);
}
Ok(())
}
#[command]
#[description("Leave the channel you are connected")]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = match ctx
.cache
.guild_channel_field(msg.channel_id, |channel| channel.guild_id)
.await
{
Some(id) => id,
None => {
check_msg(msg.channel_id.say(&ctx.http, "DMs not supported").await);
return Ok(());
}
};
let manager_lock = ctx
.data
.read()
.await
.get::<VoiceManager>()
.cloned()
.expect("Expected VoiceManager in TypeMap.");
let mut manager = manager_lock.lock().await;
let has_handler = manager.get(guild_id).is_some();
if has_handler {
manager.remove(guild_id);
check_msg(msg.channel_id.say(&ctx.http, "Left voice channel").await);
} else {
check_msg(msg.reply(ctx, "Not in a voice channel").await);
}
Ok(())
}
#[command]
#[description("Play a music (require an url)")]
async fn play(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let url = match args.single::<String>() {
Ok(url) => url,
Err(_) => {
check_msg(
msg.channel_id
.say(&ctx.http, "Must provide a URL to a video or audio")
.await,
);
return Ok(());
}
};
if !url.starts_with("http") {
check_msg(
msg.channel_id
.say(&ctx.http, "Must provide a valid URL")
.await,
);
return Ok(());
}
let guild_id = match ctx.cache.guild_channel(msg.channel_id).await {
Some(channel) => channel.guild_id,
None => {
check_msg(
msg.channel_id
.say(&ctx.http, "Error finding channel info")
.await,
);
return Ok(());
}
};
let manager_lock = ctx
.data
.read()
.await
.get::<VoiceManager>()
.cloned()
.expect("Expected VoiceManager in TypeMap.");
let mut manager = manager_lock.lock().await;
if let Some(handler) = manager.get_mut(guild_id) {
let source = match voice::ytdl(&url).await {
Ok(source) => source,
Err(why) => {
println!("Err starting source: {:?}", why);
check_msg(msg.channel_id.say(&ctx.http, "Error sourcing ffmpeg").await);
return Ok(());
}
};
handler.stop();
handler.play(source);
check_msg(msg.channel_id.say(&ctx.http, "Playing song").await);
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
}
Ok(())
}
#[command]
#[description("Stop the music")]
async fn stop(ctx: &Context, msg: &Message) -> CommandResult {
let guild_id = match ctx.cache.guild_channel(msg.channel_id).await {
Some(channel) => channel.guild_id,
None => {
check_msg(
msg.channel_id
.say(&ctx.http, "Error finding channel info")
.await,
);
return Ok(());
}
};
let manager_lock = ctx
.data
.read()
.await
.get::<VoiceManager>()
.cloned()
.expect("Expected VoiceManager in TypeMap.");
let mut manager = manager_lock.lock().await;
if let Some(handler) = manager.get_mut(guild_id) {
handler.stop();
check_msg(msg.channel_id.say(&ctx.http, "Stopping song").await);
} else {
check_msg(
msg.channel_id
.say(&ctx.http, "Not in a voice channel to play in")
.await,
);
}
Ok(())
}
/// Checks that a message successfully sent; if not, then logs why to stderr.
fn check_msg(result: SerenityResult<Message>) {
if let Err(why) = result {
eprintln!("Error sending message: {:?}", why);
}
}

87
src/commands/roulette.rs Normal file
View File

@ -0,0 +1,87 @@
use crate::{api, commands, debugln};
use rand::Rng;
use serenity::{
framework::standard::{
macros::{command, group},
Args, CommandResult,
},
model::prelude::*,
prelude::*,
};
use std::collections::HashMap;
pub struct BulletsContainer;
impl TypeMapKey for BulletsContainer {
type Value = HashMap<u64, u8>;
}
#[group]
#[default_command(shot)]
#[prefix("roulette")]
#[commands(reload, shot)]
struct Roulette;
#[command]
#[description = "PRESS THE TRIGGER !"]
#[bucket = "roulette"]
async fn shot(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let _message = args.message().trim_end();
if _message == "shot" || _message == "" {
if let Err(e) = _shot(ctx, msg).await {
eprintln!("{}", e);
}
} else if let Err(e) = api::send_reply(
ctx,
msg,
format!("Error : {} is not a valid argument", args.message()),
)
.await
{
eprintln!("Error : {:?}", e);
}
Ok(())
}
async fn _shot(ctx: &Context, msg: &Message) -> commands::Result<()> {
let mut data = ctx.data.write().await;
let bullets_map = data
.get_mut::<BulletsContainer>()
.expect("Expected CommandCounter in TypeMap.");
let bullets = bullets_map.entry(msg.author.id.0).or_insert(6);
if rand::thread_rng().gen_range(0, *bullets) == 0 {
api::send_reply(ctx, &msg, "BOOM !").await?;
*bullets = 6;
} else {
*bullets -= 1;
api::send_reply(
&ctx,
&msg,
format!("Click ! bullets remaining : {}", bullets),
)
.await?;
}
debugln!("{:?}", bullets_map);
Ok(())
}
#[command]
#[description = "Reload"]
#[bucket = "roulette"]
async fn reload(ctx: &Context, msg: &Message) -> CommandResult {
if let Err(e) = _reload(ctx, msg).await {
eprintln!("{}", e);
}
Ok(())
}
async fn _reload(ctx: &Context, msg: &Message) -> commands::Result<()> {
let mut data = ctx.data.write().await;
let bullets_map = data
.get_mut::<BulletsContainer>()
.expect("Expected CommandCounter in TypeMap.");
bullets_map.insert(msg.author.id.0, 6);
msg.react(ctx, ReactionType::Unicode(String::from("")))
.await?;
Ok(())
}

13
src/config.rs Normal file
View File

@ -0,0 +1,13 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub(crate) struct Conf {
pub(crate) bot: Bot,
}
#[derive(Debug, Deserialize)]
pub(crate) struct Bot {
pub(crate) token: String,
pub(crate) log_attachments: Option<bool>,
pub(crate) invite_url: Option<String>,
}

8
src/macros.rs Normal file
View File

@ -0,0 +1,8 @@
#[macro_export]
macro_rules! debugln {
($($arg:tt)*) => {
if cfg!(debug_assertions) {
eprintln!($($arg)*);
}
};
}

303
src/main.rs Normal file
View File

@ -0,0 +1,303 @@
use crate::commands::{
general::GENERAL_GROUP,
roulette::{BulletsContainer, ROULETTE_GROUP},
};
use async_trait::async_trait;
use serenity::{
framework::standard::{
help_commands,
macros::{help, hook},
Args, CommandGroup, CommandResult, DispatchError, HelpOptions, StandardFramework, CommandError,
},
http::Http,
model::prelude::*,
prelude::*,
};
use std::{
collections::{HashMap, HashSet},
fs::{self},
io::Result as IoResult,
path::Path,
time::Duration,
};
use tokio::{fs::File, io::AsyncWriteExt};
use serde_json::Value;
#[cfg(feature = "music")]
use crate::commands::music::{VoiceManager, MUSIC_GROUP};
mod api;
mod commands;
mod config;
mod macros;
mod presence;
const PREFIX: &str = "?";
static mut LOG_ATTACHMENTS: bool = false;
pub(crate) static mut INVITE_URL: Option<String> = None;
//TODO CLAP FOR CLI
#[tokio::main]
async fn main() -> IoResult<()> {
let conf: config::Conf = toml::from_str(&std::fs::read_to_string("Conf.toml")?)?;
debugln!("conf : {:?}", conf);
let token = conf.bot.token;
if conf.bot.log_attachments.unwrap_or(false) {
unsafe {
LOG_ATTACHMENTS = true;
}
}
if let Some(url) = conf.bot.invite_url {
unsafe {
INVITE_URL = Some(url);
}
}
debugln!("Log attachments : {}", unsafe { LOG_ATTACHMENTS });
let dir = Path::new("logging");
if !dir.exists() {
fs::create_dir(dir)?;
}
let http = Http::new_with_token(&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();
owners.insert(info.owner.id);
(owners, info.id)
}
Err(why) => panic!("Could not access application info: {:?}", why),
};
debugln!("Owners : {:?}", owners);
let mut framework = StandardFramework::new()
.configure(|c| {
c.with_whitespace(true)
.on_mention(Some(bot_id))
.prefix(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)
})
// 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 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)
.after(after_hook)
.help(&MY_HELP)
.group(&GENERAL_GROUP)
.group(&ROULETTE_GROUP);
#[cfg(feature = "music")]
{
framework = framework.group(&MUSIC_GROUP);
}
if cfg!(debug_assertions) {
// Set a function that's called whenever a message is not a command.
framework = framework.normal_message(normal_message)
}
let mut client = Client::new(&token)
.event_handler(Messages {})
.framework(framework)
.await
.unwrap();
{
let mut data = client.data.write().await;
data.insert::<BulletsContainer>(HashMap::default());
#[cfg(feature = "music")]
{
data.insert::<VoiceManager>(std::sync::Arc::clone(&client.voice_manager));
}
}
client.start().await.unwrap();
Ok(())
}
struct Messages {}
impl Messages {
async fn _reaction_add(
&self,
ctx: Context,
reaction: Reaction,
) -> Result<(), Box<dyn std::error::Error>> {
// TODO
let message: Message = reaction.message(&ctx.http).await?;
if message.is_own(&ctx).await && message.content.starts_with("Do you want to take the gun")
{
debugln!("My own garbage !");
let user = reaction.user(&ctx.http).await?;
if user == message.mentions[1] {
message.delete(&ctx.http).await?;
if reaction.emoji
== serenity::model::channel::ReactionType::Unicode(String::from(""))
{
let mut data = ctx.data.write().await;
let bullets_map = data
.get_mut::<BulletsContainer>()
.expect("Expected CommandCounter in TypeMap.");
if bullets_map.contains_key(&message.mentions[0].id.0) {
let bullet_count =
bullets_map.remove(&message.mentions[0].id.0).unwrap_or(6);
bullets_map.insert(message.mentions[1].id.0, bullet_count);
message.channel_id.say(&ctx, "Done").await?;
} else {
message
.channel_id
.say(&ctx, "Error : Your gun is empty")
.await?;
}
}
}
}
Ok(())
}
}
#[async_trait]
impl EventHandler for Messages {
async fn reaction_add(&self, _ctx: Context, reaction: Reaction) {
debugln!("Reaction added : {:?}", reaction);
/*
if let Err(e) = self._reaction_add(ctx, reaction).await {
eprintln!("{}", e);
}*/
}
async fn ready(&self, ctx: Context, ready: Ready) {
println!("{} connected to discord", ready.user.name);
ctx.set_presence(Some(Activity::listening("?")), OnlineStatus::Online)
.await;
let ctx_clone = ctx.clone();
tokio::spawn(async move {
let delay = Duration::from_secs(5);
let mut presence_generator = presence::Presences::new();
while let Some(act) = presence_generator.next(&ctx_clone).await {
ctx_clone
.set_presence(Some(act), OnlineStatus::Online)
.await;
tokio::time::delay_for(delay).await;
}
});
}
async fn message(&self, _ctx: Context, new_message: Message) {
if unsafe { LOG_ATTACHMENTS } && !new_message.attachments.is_empty() {
for att in new_message.attachments {
if let Err(e) = download_to_log(att).await {
eprintln!("Error while downloading to log : {:?}", e);
}
}
}
}
async fn unknown(&self, _ctx: Context, name: String, raw: Value) {
println!("Unknown event : {}, {:?}", name, raw);
}
}
async fn download_to_log(attachment: Attachment) -> commands::Result<()> {
debugln!("Download_to_log : {:?}", attachment);
let path = Path::new("logging").join(format!("{}-{}", attachment.id, attachment.filename));
let content = reqwest::get(&attachment.url).await?.bytes().await?;
let mut file = File::create(path).await?;
file.write_all(&content).await?;
Ok(())
}
#[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 dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) {
debugln!("Dispatch error : {:?}", error);
if let DispatchError::Ratelimited(seconds) = error {
let _ = msg
.channel_id
.say(
&ctx.http,
&format!("Try this again in {} seconds.", seconds),
)
.await;
}
}
#[hook]
async fn after_hook(_: &Context, _: &Message, cmd_name: &str, error: Result<(), CommandError>) {
// Print out an error if it happened
if let Err(why) = error {
println!("Error in {}: {:?}", cmd_name, why);
}
}
// 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!\n\
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: \x60{}\x60."]
// 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 hence our variant is `Nothing`.
#[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_{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(())
}

27
src/presence.rs Normal file
View File

@ -0,0 +1,27 @@
use serenity::{model::prelude::*, prelude::*};
const PRESENCE_COUNT: usize = 3;
pub(crate) struct Presences {
index: usize,
}
impl Presences {
pub(crate) fn new() -> Self {
Self { index: 0 }
}
pub(crate) async fn next(&mut self, ctx: &Context) -> Option<Activity> {
let res: Activity = match self.index {
0 => Activity::listening("?"),
1 => Activity::listening(&format!("{} servers", ctx.cache.guild_count().await)),
2 => Activity::playing("praising the borrow checker"),
_ => unimplemented!(),
};
self.index += 1;
if self.index >= PRESENCE_COUNT {
self.index = 0;
}
Some(res)
}
}