This commit is contained in:
Raatty 2019-10-19 10:46:29 +13:00
commit 5f3f9d6652
14 changed files with 750 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
**/*.rs.bk
.vscode
Cargo.lock

19
Cargo.toml Normal file
View File

@ -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"

25
src/commands/discord.rs Normal file
View File

@ -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(())
}

85
src/commands/dogs.rs Normal file
View File

@ -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<String, reqwest::Error> {
let mut resp = reqwest::get(url)?;
let jresp: HashMap<String, String> = 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<String>,
}
fn get_dog_list() -> Result<DogList, reqwest::Error> {
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::<String>() {
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(())
}

35
src/commands/fun/crc.rs Normal file
View File

@ -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::<events::CRCGameStateStore>()
.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(())
}

18
src/commands/fun/echo.rs Normal file
View File

@ -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(())
}

View File

@ -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(())
}

9
src/commands/fun/mod.rs Normal file
View File

@ -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::*;

28
src/commands/fun/spam.rs Normal file
View File

@ -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("<count> <message>")]
pub fn spam(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
if let Ok(count) = args.single::<u32>() {
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(())
}

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

@ -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::*;

194
src/commands/runescape.rs Normal file
View File

@ -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::<Profile>() {
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<SkillValueExtended> = 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<Activity>,
skillvalues: Vec<SkillValue>,
name: String,
rank: Option<String>,
melee: u32,
combatlevel: u32,
#[serde(alias = "loggedIn")]
logged_in: String,
}
#[derive(Deserialize, Debug)]
struct SkillValue {
level: u32,
xp: u32,
rank: Option<u32>,
id: u32,
}
#[derive(Debug)]
struct SkillValueExtended {
level: u32,
xp: u32,
rank: Option<u32>,
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>",
];

21
src/commands/utility.rs Normal file
View File

@ -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(())
}

173
src/events.rs Normal file
View File

@ -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::<CRCGameStateStore>()
.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<CRCChoices> for Standard {
fn sample<R: Rng + ?Sized>(&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<CRCChoices, Self::Error> {
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::<CRCChoices>();
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<Mutex<HashMap<u64, CRCGameState>>>;
}

86
src/main.rs Normal file
View File

@ -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<UserId>,
) -> 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<u64, events::CRCGameState> = HashMap::new();
data.insert::<events::CRCGameStateStore>(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]
});