Initial Commit
This commit is contained in:
commit
8055cbb78f
|
@ -0,0 +1,6 @@
|
||||||
|
/target
|
||||||
|
/.vs/*
|
||||||
|
!/.vs/settings.json
|
||||||
|
/logging/
|
||||||
|
Cargo.lock
|
||||||
|
Conf.toml
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"rust.features": [
|
||||||
|
"music"
|
||||||
|
]
|
||||||
|
}
|
|
@ -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"
|
|
@ -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(())
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>>;
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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>,
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! debugln {
|
||||||
|
($($arg:tt)*) => {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
|
eprintln!($($arg)*);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue