From 5f3f9d665253a3bcf842dd3d9d7d41b5b866e89f Mon Sep 17 00:00:00 2001 From: Raatty Date: Sat, 19 Oct 2019 10:46:29 +1300 Subject: [PATCH] init --- .gitignore | 4 + Cargo.toml | 19 ++++ src/commands/discord.rs | 25 +++++ src/commands/dogs.rs | 85 +++++++++++++++ src/commands/fun/crc.rs | 35 ++++++ src/commands/fun/echo.rs | 18 ++++ src/commands/fun/eightball.rs | 42 ++++++++ src/commands/fun/mod.rs | 9 ++ src/commands/fun/spam.rs | 28 +++++ src/commands/mod.rs | 11 ++ src/commands/runescape.rs | 194 ++++++++++++++++++++++++++++++++++ src/commands/utility.rs | 21 ++++ src/events.rs | 173 ++++++++++++++++++++++++++++++ src/main.rs | 86 +++++++++++++++ 14 files changed, 750 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/commands/discord.rs create mode 100644 src/commands/dogs.rs create mode 100644 src/commands/fun/crc.rs create mode 100644 src/commands/fun/echo.rs create mode 100644 src/commands/fun/eightball.rs create mode 100644 src/commands/fun/mod.rs create mode 100644 src/commands/fun/spam.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/runescape.rs create mode 100644 src/commands/utility.rs create mode 100644 src/events.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1250643 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +**/*.rs.bk +.vscode +Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..be6991a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rusty_rat" +version = "0.1.0" +authors = ["Raatty <'me@raatty.club'>"] +edition = "2018" + +[dependencies] +serenity = "0.7.1" +reqwest = { version = "0.9.22", default-features = false } +serde = { version = "1.0.101", features = ["derive"] } +rand = "0.6" +typemap = "0.3" +chrono = "0.4" + +[profile.release] +codegen-units = 1 +panic = 'abort' +lto = true +opt-level = "z" \ No newline at end of file diff --git a/src/commands/discord.rs b/src/commands/discord.rs new file mode 100644 index 0000000..7902d57 --- /dev/null +++ b/src/commands/discord.rs @@ -0,0 +1,25 @@ +use serenity::prelude::*; +use serenity::{ + framework::standard::{macros::command, Args, CommandError, CommandResult}, + model::channel::Message, +}; + +#[command] +#[description = "displays your avatar"] +pub fn avatar(ctx: &mut Context, msg: &Message, _args: Args) -> CommandResult { + let guild = &msg.guild_id.ok_or(CommandError("no guild".to_owned()))?; + let member = guild.member(&ctx, &msg.author)?; + let name = member.display_name(); + if let Some(user_avatar) = &msg.author.avatar_url() { + msg.channel_id + .send_message(&ctx.http, |m| { + m.embed(|e| { + e.title(format!("Heres {}'s avatar", name)) + .image(user_avatar) + }) + }) + .ok(); + }; + + Ok(()) +} diff --git a/src/commands/dogs.rs b/src/commands/dogs.rs new file mode 100644 index 0000000..329286b --- /dev/null +++ b/src/commands/dogs.rs @@ -0,0 +1,85 @@ +extern crate reqwest; +extern crate serde; +use serde::Deserialize; +use serenity::prelude::*; +use serenity::{ + framework::standard::{macros::command, Args, CommandResult}, + model::channel::Message, + utils::Colour, +}; + +use std::collections::HashMap; + +const DOG_URLS: [&'static str; 3] = [ + "https://dog.ceo/api/breed/{}/images/random", + "https://dog.ceo/api/breeds/list", + "https://dog.ceo/api/breeds/image/random", +]; + +fn get_dog(url: &str) -> Result { + let mut resp = reqwest::get(url)?; + let jresp: HashMap = resp.json()?; + let dog_url = &jresp["message"]; + Ok(dog_url.to_string()) +} + +#[command] +#[description = "sends a pic of a random pug"] +pub fn pug(ctx: &mut Context, msg: &Message, _args: Args) -> CommandResult { + let url = get_dog(&DOG_URLS[0].replace("{}", "pug"))?; + msg.channel_id + .send_message(&ctx.http, |m| { + m.embed(|e| e.image(url).colour(Colour::BLUE)) + }) + .ok(); + Ok(()) +} + +#[derive(Deserialize)] +struct DogList { + #[allow(dead_code)] + status: String, + message: Vec, +} + +fn get_dog_list() -> Result { + let mut resp = reqwest::get(DOG_URLS[1])?; + let dlist: DogList = resp.json()?; + Ok(dlist) +} + +#[command] +#[description = "sends a pic of a random dog of a given bread"] +pub fn dog(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { + match args.single::() { + Ok(ref breed) if breed == "list" => { + let list_of_breads = get_dog_list()?; + msg.channel_id + .say( + &ctx.http, + format!( + "**Here is a list of valid dog breads**\n{:?}", + list_of_breads.message + ), + ) + .ok(); + } + Ok(bread) => { + let url = get_dog(&DOG_URLS[0].replace("{}", &bread))?; + msg.channel_id + .send_message(&ctx.http, |m| { + m.embed(|e| e.image(url).colour(Colour::BLUE)) + }) + .ok(); + } + Err(_) => { + let url = get_dog(&DOG_URLS[2])?; + msg.channel_id + .send_message(&ctx.http, |m| { + m.embed(|e| e.image(url).colour(Colour::BLUE)) + }) + .ok(); + } + } + Ok(()) +} diff --git a/src/commands/fun/crc.rs b/src/commands/fun/crc.rs new file mode 100644 index 0000000..21bced1 --- /dev/null +++ b/src/commands/fun/crc.rs @@ -0,0 +1,35 @@ +use serenity::prelude::*; +use serenity::{ + framework::standard::{macros::command, Args, CommandResult}, + model::channel::Message, +}; + +#[command] +#[aliases("crc")] +#[description = "paper sizzors rock basicly"] +pub fn cat_rat_cheese(ctx: &mut Context, msg: &Message, _args: Args) -> CommandResult { + if let Ok(sent_msg) = msg.channel_id.say( + &ctx.http, + "react with your choice, wait for the 3 reactions to load tho", + ) { + for e in &['πŸ€', 'πŸ§€', '🐱'] { + sent_msg.react(&ctx, *e).ok(); + } + use crate::events; + ctx.data + .write() + .get_mut::() + .expect("Expected CRCGameStateStore") + .lock() + .unwrap() + .insert( + sent_msg.id.into(), + events::CRCGameState { + player_id: msg.author.id.into(), + player_score: 0, + bot_score: 0, + }, + ); + } + Ok(()) +} diff --git a/src/commands/fun/echo.rs b/src/commands/fun/echo.rs new file mode 100644 index 0000000..f7b8c7e --- /dev/null +++ b/src/commands/fun/echo.rs @@ -0,0 +1,18 @@ +use serenity::prelude::*; +use serenity::{ + framework::standard::{macros::command, Args, CommandResult}, + model::channel::Message, + utils, +}; + +#[command] +#[description = "echos the message given"] +pub fn echo(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { + msg.channel_id + .say( + &ctx.http, + utils::content_safe(&ctx, args.rest(), &utils::ContentSafeOptions::default()), + ) + .ok(); + Ok(()) +} diff --git a/src/commands/fun/eightball.rs b/src/commands/fun/eightball.rs new file mode 100644 index 0000000..83785fa --- /dev/null +++ b/src/commands/fun/eightball.rs @@ -0,0 +1,42 @@ +extern crate rand; +use rand::{seq::SliceRandom, thread_rng}; +use serenity::prelude::*; +use serenity::{ + framework::standard::{macros::command, Args, CommandResult}, + model::channel::Message, +}; + +const EIGHTBALL_RESPONCES: [&'static str; 20] = [ + "it is certain", + "it is decidedly so", + "without a doubt", + "yes definitely", + "you may rely on it", + "as I see it, yes", + "most likely", + "outlook good", + "yes", + "signs point to yes", + "reply hazy, try again", + "ask again later", + "better not tell you now", + "cannot predict now", + "concentrate and ask again", + "don't count on it", + "my reply is no", + "my sources say no", + "outlook not so good", + "very doubtful", +]; + +#[command("8ball")] +#[description = "asks the magic cheese ball you deepest desires"] +pub fn eightball(ctx: &mut Context, msg: &Message, _args: Args) -> CommandResult { + let mut rng = thread_rng(); + if let Some(choice) = EIGHTBALL_RESPONCES.choose(&mut rng) { + msg.channel_id + .say(&ctx.http, format!("πŸ§€The cheese says {}", choice)) + .ok(); + } + Ok(()) +} diff --git a/src/commands/fun/mod.rs b/src/commands/fun/mod.rs new file mode 100644 index 0000000..b338ef5 --- /dev/null +++ b/src/commands/fun/mod.rs @@ -0,0 +1,9 @@ +mod crc; +mod echo; +pub mod eightball; +mod spam; + +pub use crc::*; +pub use echo::*; +pub use eightball::*; +pub use spam::*; diff --git a/src/commands/fun/spam.rs b/src/commands/fun/spam.rs new file mode 100644 index 0000000..f8c01e6 --- /dev/null +++ b/src/commands/fun/spam.rs @@ -0,0 +1,28 @@ +use serenity::prelude::*; +use serenity::{ + framework::standard::{macros::command, Args, CommandResult}, + model::channel::Message, + utils, +}; + +#[command] +#[description = "spams a given message a maximum of 10 times"] +#[usage(" ")] +pub fn spam(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { + if let Ok(count) = args.single::() { + if count <= 10 { + let to_spam = args.rest(); + for _ in 0..count { + msg.channel_id + .say( + &ctx.http, + utils::content_safe(&ctx, to_spam, &utils::ContentSafeOptions::default()), + ) + .ok(); + } + } else { + msg.channel_id.say(&ctx.http, "no thats too many").ok(); + } + }; + Ok(()) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..237673e --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,11 @@ +mod discord; +mod dogs; +mod fun; +mod runescape; +mod utility; + +pub use discord::*; +pub use dogs::*; +pub use fun::*; +pub use runescape::*; +pub use utility::*; diff --git a/src/commands/runescape.rs b/src/commands/runescape.rs new file mode 100644 index 0000000..769f65e --- /dev/null +++ b/src/commands/runescape.rs @@ -0,0 +1,194 @@ +use serenity::prelude::*; +use serenity::{ + framework::standard::{macros::command, Args, CommandResult}, + model::channel::Message, +}; + +#[command] +#[description = "echos the message given"] +pub fn profile(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { + let rsn = args.rest().replace(" ", "%20"); + let mut resp = reqwest::get(&format!( + "https://apps.runescape.com/runemetrics/profile/profile?user={}&activities=20", + rsn + ))?; + let profile: Profile = match resp.json::() { + Ok(p) => p, + Err(why) => { + println!("{}", why); + return Ok(()); + } + }; + let quest_total = profile.questscomplete + profile.questsstarted + profile.questsnotstarted; + let right = format!( + "**Quests:** {}/{} ({} in progress) +**Rank:** {} +**Total level:** {} +**Total XP:** {} +**Combat level:** {}", + profile.questscomplete, + quest_total, + profile.questsstarted, + profile.rank.clone().unwrap_or("none".to_string()), + profile.totalskill, + profile.totalxp, + profile.combatlevel + ); + let mut stats_extended: Vec = profile + .skillvalues + .iter() + .map(|s| SkillValueExtended::from(s)) + .collect(); + stats_extended.sort_by(|s1, s2| ORDER[s1.id as usize].cmp(&ORDER[s2.id as usize])); + let mut left = String::new(); + for i in 0..27 { + if i % 3 == 0 { + left.push('\n') + } + let stat = &stats_extended[i]; + left.push_str(&format!("{}{}", EMOJIS[stat.id as usize], stat.level)); + } + msg.channel_id.send_message(&ctx.http, |m| { + m.embed(|e| { + e.author(|a| a.name(format!("RuneMetrics profile of: {}", profile.name))) + .field("Levels", left, true) + .field("Info", right, true) + }) + })?; + let mut activities_out = String::new(); + for (i, a) in profile.activities.iter().enumerate() { + { + activities_out.push_str(&format!("**{}** {}\n```{}```", a.date, a.text, a.details)); + } + if i == 10 { + msg.channel_id.say(&ctx.http, &activities_out)?; + activities_out.clear(); + } + } + if !activities_out.is_empty() { + msg.channel_id.say(&ctx.http, &activities_out)?; + } + Ok(()) +} + +use serde::Deserialize; + +#[derive(Deserialize, Debug)] +struct Activity { + date: String, + details: String, + text: String, +} + +#[derive(Deserialize, Debug)] +struct Profile { + magic: u32, + questsstarted: u32, + totalskill: u32, + questscomplete: u32, + questsnotstarted: u32, + totalxp: u32, + ranged: u32, + activities: Vec, + skillvalues: Vec, + name: String, + rank: Option, + melee: u32, + combatlevel: u32, + #[serde(alias = "loggedIn")] + logged_in: String, +} + +#[derive(Deserialize, Debug)] +struct SkillValue { + level: u32, + xp: u32, + rank: Option, + id: u32, +} + +#[derive(Debug)] +struct SkillValueExtended { + level: u32, + xp: u32, + rank: Option, + id: u32, + name: String, +} + +impl std::convert::From<&SkillValue> for SkillValueExtended { + fn from(sv: &SkillValue) -> Self { + Self { + name: SKILL_NAMES[sv.id as usize].to_string(), + level: sv.level, + xp: sv.xp, + rank: sv.rank, + id: sv.id, + } + } +} + +static SKILL_NAMES: [&'static str; 27] = [ + "Attack", + "Defence", + "Strength", + "Constitution", + "Ranged", + "Prayer", + "Magic", + "Cooking", + "Woodcutting", + "Fletching", + "Fishing", + "Firemaking", + "Crafting", + "Smithing", + "Mining", + "Herblore", + "Agility", + "Thieving", + "Slayer", + "Farming", + "Runecrafting", + "Hunter", + "Construction", + "Summoning", + "Dungeoneering", + "Divination", + "Invention", +]; + +static ORDER: [u32; 27] = [ + 1, 7, 4, 2, 10, 13, 16, 12, 18, 17, 9, 15, 14, 6, 3, 8, 5, 11, 20, 21, 19, 23, 22, 24, 25, 26, + 27, +]; + +static EMOJIS: [&'static str; 27] = [ + "<:Attack:406361343223136256>", + "<:Defence:406361343348834304>", + "<:Strength:406361343357222914>", + "<:Constitution:406361343222874112>", + "<:Ranged:406361343298502658>", + "<:Prayer:406361343445434390>", + "<:Magic:406361343608881152>", + "<:Cooking:406361343361548298>", + "<:Woodcutting:406361343718064128>", + "<:Fletching:406361343353159691>", + "<:Fishing:406361343583846410>", + "<:Firemaking:406361343718064129>", + "<:Crafting:406361343168610305>", + "<:Smithing:406361343487115265>", + "<:Mining:406361343583584256>", + "<:Herblore:406361343554355210>", + "<:Agility:406361343210553344>", + "<:Thieving:406361343302696962>", + "<:Slayer:406361343407685633>", + "<:Farming:406361343407423498>", + "<:Runecrafting:406361343596298250>", + "<:Hunter:406361343474532353>", + "<:Construction:406361343302565888>", + "<:Summoning:406361343843631107>", + "<:Dungeoneering:406361343386451979>", + "<:Divination:406361343374131211>", + "<:Invention:406361343591972864>", +]; diff --git a/src/commands/utility.rs b/src/commands/utility.rs new file mode 100644 index 0000000..02d259e --- /dev/null +++ b/src/commands/utility.rs @@ -0,0 +1,21 @@ +use serenity::prelude::Context; +use serenity::{ + framework::standard::{macros::command, Args, CommandResult}, + model::channel::Message, +}; + +#[command] +#[description = "responds with pong"] +pub fn ping(ctx: &mut Context, message: &Message, _args: Args) -> CommandResult { + let now = message.timestamp; + let mut msg = message.channel_id.say(&ctx.http, "Pong! πŸ“")?; + let msg_timestamp = msg.timestamp; + msg.edit(&ctx, |m| { + m.content(format!( + "Pong! πŸ“ **took {}ms**", + (msg_timestamp - now).num_milliseconds() + )) + }) + .ok(); + Ok(()) +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..2014f50 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,173 @@ +use serenity::{ + model::{channel::ReactionType, prelude::*}, + prelude::TypeMapKey, + prelude::{Context, EventHandler}, +}; + +extern crate rand; +use rand::{ + distributions::{Distribution, Standard}, + Rng, +}; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::sync::{Arc, Mutex}; +pub struct Handler; + +impl EventHandler for Handler { + fn ready(&self, ctx: Context, r: Ready) { + println!("Successfully logged in as {}", r.user.name); + println!("Can see {} guilds", r.guilds.len()); + let game = Activity::listening("CHEESE"); + let status = OnlineStatus::DoNotDisturb; + + ctx.set_presence(Some(game), status); + } + fn message(&self, ctx: Context, new_message: Message) { + if new_message.mentions_user_id(140652945032216576) { + new_message.react(&ctx, 'πŸ€').ok(); + } + } + fn reaction_add(&self, context: Context, reaction: Reaction) { + let reaction_clone = reaction.clone(); + if let ReactionType::Unicode(emoji) = reaction.emoji { + match emoji.as_ref() { + "🐱" | "πŸ€" | "πŸ§€" => play_crc(reaction_clone, context, &emoji), + _ => {} + } + } + } +} + +fn play_crc(reaction: Reaction, mut context: Context, emoji: &str) { + let game_store = { + let mut data = context.data.write(); + data.get_mut::() + .expect("Expected CRCGameStateStore") + .clone() + }; + let mut remove_message = false; + if let Some(current_game) = game_store + .lock() + .unwrap() + .get_mut(&u64::from(reaction.message_id)) + { + if reaction.user_id != current_game.player_id { + return; + } + let message = current_game + .play(CRCChoices::try_from(emoji).unwrap()) + .to_string(); + let mut score_message = format!( + "your score: {} - my score {}\n{}", + current_game.player_score, current_game.bot_score, message + ) + .to_string(); + if current_game.player_score == 3 { + score_message.push_str("\n**You win!**"); + remove_message = true; + } else if current_game.bot_score == 3 { + score_message.push_str("\n**I win :)**"); + remove_message = true; + } + if let Ok(mut discord_message) = reaction.message(&mut context.http) { + discord_message + .edit(&context, |e| e.content(score_message)) + .ok(); + }; + reaction.delete(&context).ok(); + }; + if remove_message { + game_store + .lock() + .unwrap() + .remove(&reaction.message_id.into()); + } +} +#[derive(PartialEq)] +pub enum CRCChoices { + Cat, + Rat, + Cheese, +} +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> CRCChoices { + match rng.gen_range(0, 3) { + 0 => CRCChoices::Cat, + 1 => CRCChoices::Rat, + _ => CRCChoices::Cheese, + } + } +} +impl TryFrom<&str> for CRCChoices { + type Error = String; + fn try_from(emoji: &str) -> Result { + use CRCChoices::*; + match emoji { + "πŸ€" => Ok(Rat), + "🐱" => Ok(Cat), + "πŸ§€" => Ok(Cheese), + _ => Err("naughty emoji".to_string()), + } + } +} + +pub struct CRCGameState { + pub player_id: u64, + pub player_score: u8, + pub bot_score: u8, +} + +impl CRCGameState { + pub fn play(&mut self, player: CRCChoices) -> String { + let bot = rand::random::(); + use CRCChoices::*; + + let message_tail = if bot == player { + "draw" + } else if player == Rat { + if bot == Cheese { + self.player_score += 1; + "your rat eats the cheese and you win!" + } else { + self.bot_score += 1; + "my cat eats your rat and I win" + } + } else if player == Cheese { + if bot == Rat { + self.bot_score += 1; + "bots rat eats the cheese and you lose" + } else { + self.player_score += 1; + "my cat eats your rat and I win" + } + } else if player == Cat { + if bot == Rat { + self.player_score += 1; + "your cat eats the rat so you win..." + } else { + self.bot_score += 1; + "your cat eats the cheese but it gets poisoned and dies, I win" + } + } else { + "bug report plz" + }; + format!("bot picked {} {}", bot, message_tail) + } +} + +impl std::fmt::Display for CRCChoices { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let string_form = match self { + CRCChoices::Rat => "πŸ€", + CRCChoices::Cat => "🐱", + CRCChoices::Cheese => "πŸ§€", + }; + write!(f, "{}", string_form) + } +} + +pub struct CRCGameStateStore; +impl TypeMapKey for CRCGameStateStore { + type Value = Arc>>; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fdbf250 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,86 @@ +use std::collections::{HashMap, HashSet}; + +use serenity::{ + client::Client, + framework::standard::{ + help_commands, + macros::{group, help}, + Args, CommandGroup, CommandResult, HelpOptions, StandardFramework, + }, + model::prelude::*, + prelude::*, +}; + +use std::env; +use std::sync::{Arc, Mutex}; +mod commands; +use commands::{ + AVATAR_COMMAND, CAT_RAT_CHEESE_COMMAND, DOG_COMMAND, ECHO_COMMAND, EIGHTBALL_COMMAND, + PING_COMMAND, PROFILE_COMMAND, PUG_COMMAND, SPAM_COMMAND, +}; +mod events; + +#[help] +fn my_help( + ctx: &mut Context, + msg: &Message, + args: Args, + help_options: &'static HelpOptions, + groups: &[&'static CommandGroup], + owners: HashSet, +) -> CommandResult { + help_commands::with_embeds(ctx, msg, args, &help_options, groups, owners) +} + +fn main() { + let mut client = Client::new(&env::var("DISCORD_TOKEN").expect("token"), events::Handler) + .expect("Error creating client"); + client.with_framework( + StandardFramework::new() + .configure(|c| c.prefix("&")) + .help(&MY_HELP) + .group(&FUN_GROUP) + .group(&DISCORD_GROUP) + .group(&UTILITY_GROUP) + .group(&DOGS_GROUP) + .group(&RUNESCAPE_GROUP), + ); + { + let mut data = client.data.write(); + let crc_game: HashMap = HashMap::new(); + data.insert::(Arc::new(Mutex::new(crc_game))); + } + if let Err(why) = client.start() { + println!("An error occurred while running the client: {:?}", why); + } +} + +group!({ + name: "fun", + options: {}, + commands: [cat_rat_cheese, echo, spam, eightball], +}); + +group!({ + name: "discord", + options: {}, + commands: [avatar] +}); + +group!({ + name: "utility", + options: {}, + commands: [ping] +}); + +group!({ + name: "dogs", + options: {}, + commands: [pug, dog] +}); + +group!({ + name: "runescape", + options: {}, + commands: [profile] +});