diff --git a/Cargo.toml b/Cargo.toml index 4dba66d..974be77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,13 +5,14 @@ authors = ["Raatty <'me@raatty.club'>"] edition = "2018" [dependencies] -serenity = "0.7.1" -reqwest = { version = "0.9.22", default-features = false } +serenity = { version = "0.10.8", features = ["collector"]} +reqwest = { version = "0.11.3", default-features = false, features = ["rustls-tls"]} serde = { version = "1.0.101", features = ["derive"] } -rand = "0.6" +rand = "0.8.3" typemap = "0.3" chrono = "0.4" num-format = "0.4" +tokio = {version = "1.0", features= ["macros", "rt-multi-thread"]} [profile.release] codegen-units = 1 diff --git a/src/commands/discord.rs b/src/commands/discord.rs index 7902d57..338b974 100644 --- a/src/commands/discord.rs +++ b/src/commands/discord.rs @@ -1,25 +1,23 @@ use serenity::prelude::*; use serenity::{ - framework::standard::{macros::command, Args, CommandError, CommandResult}, + framework::standard::{macros::command, Args, 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) +pub async fn avatar(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + if let Ok(member) = &msg.member(&ctx).await { + if let Some(user_avatar) = &msg.author.avatar_url() { + msg.channel_id + .send_message(&ctx, |m| { + m.embed(|e| { + e.title(format!("Heres {}'s avatar", member.display_name())) + .image(user_avatar) + }) }) - }) - .ok(); - }; - + .await?; + }; + } Ok(()) } diff --git a/src/commands/dogs.rs b/src/commands/dogs.rs index 329286b..0fdba47 100644 --- a/src/commands/dogs.rs +++ b/src/commands/dogs.rs @@ -16,22 +16,22 @@ const DOG_URLS: [&'static str; 3] = [ "https://dog.ceo/api/breeds/image/random", ]; -fn get_dog(url: &str) -> Result { - let mut resp = reqwest::get(url)?; - let jresp: HashMap = resp.json()?; +async fn get_dog(url: &str) -> Result { + let resp = reqwest::get(url).await?; + let jresp: HashMap = resp.json().await?; 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"))?; +pub async fn pug(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let url = get_dog(&DOG_URLS[0].replace("{}", "pug")).await?; msg.channel_id .send_message(&ctx.http, |m| { m.embed(|e| e.image(url).colour(Colour::BLUE)) }) - .ok(); + .await?; Ok(()) } @@ -42,43 +42,43 @@ struct DogList { message: Vec, } -fn get_dog_list() -> Result { - let mut resp = reqwest::get(DOG_URLS[1])?; - let dlist: DogList = resp.json()?; +async fn get_dog_list() -> Result { + let resp = reqwest::get(DOG_URLS[1]).await?; + let dlist: DogList = resp.json().await?; 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 { +pub async fn dog(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { match args.single::() { Ok(ref breed) if breed == "list" => { - let list_of_breads = get_dog_list()?; + let list_of_breads = get_dog_list().await?; msg.channel_id .say( &ctx.http, format!( - "**Here is a list of valid dog breads**\n{:?}", + "**Here is a list of valid dog breeds**\n{:?}", list_of_breads.message ), ) - .ok(); + .await?; } Ok(bread) => { - let url = get_dog(&DOG_URLS[0].replace("{}", &bread))?; + let url = get_dog(&DOG_URLS[0].replace("{}", &bread)).await?; msg.channel_id .send_message(&ctx.http, |m| { m.embed(|e| e.image(url).colour(Colour::BLUE)) }) - .ok(); + .await?; } Err(_) => { - let url = get_dog(&DOG_URLS[2])?; + let url = get_dog(&DOG_URLS[2]).await?; msg.channel_id .send_message(&ctx.http, |m| { m.embed(|e| e.image(url).colour(Colour::BLUE)) }) - .ok(); + .await?; } } Ok(()) diff --git a/src/commands/fun/crc.rs b/src/commands/fun/crc.rs index 21bced1..c0c5556 100644 --- a/src/commands/fun/crc.rs +++ b/src/commands/fun/crc.rs @@ -1,35 +1,93 @@ +use rand::{seq::SliceRandom, thread_rng}; use serenity::prelude::*; use serenity::{ framework::standard::{macros::command, Args, CommandResult}, - model::channel::Message, + model::channel::{Message, ReactionType}, }; +const CRC_CHOICES: [&'static str; 3] = ["πŸ€", "🐱", "πŸ§€"]; + #[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(); +pub async fn cat_rat_cheese(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let mut sent_msg = msg + .channel_id + .say( + &ctx.http, + "react with your choice, wait for the 3 reactions to load tho", + ) + .await?; + for e in &['πŸ€', 'πŸ§€', '🐱'] { + sent_msg.react(&ctx, *e).await?; + } + let mut player_score = 0; + let mut bot_score = 0; + while player_score < 3 && bot_score < 3 { + if let Some(reaction) = sent_msg + .await_reaction(&ctx) + .timeout(std::time::Duration::from_secs(10)) + .author_id(msg.author.id) + .filter(|e| { + if let ReactionType::Unicode(emoji) = &e.emoji { + match emoji.as_ref() { + "🐱" | "πŸ€" | "πŸ§€" => true, + _ => false, + } + } else { + false + } + }) + .await + { + if let ReactionType::Unicode(emoji) = &reaction.as_inner_ref().emoji { + let player: &str = emoji.as_ref(); + let bot = *CRC_CHOICES.choose(&mut thread_rng()).unwrap(); + let message_tail = if player == bot { + "draw" + } else if player == "πŸ€" { + if bot == "πŸ§€" { + player_score += 1; + "your rat eats the cheese and you win!" + } else { + // bot = 🐱 + bot_score += 1; + "my cat eats your rat and I win" + } + } else if player == "πŸ§€" { + if bot == "🐱" { + player_score += 1; + "my cat ate your cheese but gets gets poisoned and dies, you win" + } else { + // bot = πŸ€ + bot_score += 1; + "bots rat eats the cheese and you lose" + } + } else if player == "🐱" { + if bot == "πŸ€" { + player_score += 1; + "your cat eats the rat so you win..." + } else { + // bot = πŸ§€ + bot_score += 1; + "your cat eats the cheese but it gets poisoned and dies, I win" + } + } else { + "you shouldn't get this message" + }; + let mut output = format!( + "your score: {} - my score {}\nbot picked: {}\n{}", + player_score, bot_score, bot, message_tail + ); + if player_score == 3 { + output.push_str("\n**You win!**"); + } else if bot_score == 3 { + output.push_str("\n**I win :)**"); + } + sent_msg.edit(&ctx, |m| m.content(output)).await?; + &reaction.as_inner_ref().delete(&ctx).await; + } } - 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 index f7b8c7e..01a0b3e 100644 --- a/src/commands/fun/echo.rs +++ b/src/commands/fun/echo.rs @@ -7,12 +7,12 @@ use serenity::{ #[command] #[description = "echos the message given"] -pub fn echo(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { +pub async fn echo(ctx: &Context, msg: &Message, args: Args) -> CommandResult { msg.channel_id .say( &ctx.http, - utils::content_safe(&ctx, args.rest(), &utils::ContentSafeOptions::default()), + utils::content_safe(&ctx, args.rest(), &utils::ContentSafeOptions::default()).await, ) - .ok(); + .await?; Ok(()) } diff --git a/src/commands/fun/eightball.rs b/src/commands/fun/eightball.rs index 83785fa..f23d6e7 100644 --- a/src/commands/fun/eightball.rs +++ b/src/commands/fun/eightball.rs @@ -6,7 +6,7 @@ use serenity::{ model::channel::Message, }; -const EIGHTBALL_RESPONCES: [&'static str; 20] = [ +const EIGHTBALL_RESPONSES: [&'static str; 20] = [ "it is certain", "it is decidedly so", "without a doubt", @@ -31,12 +31,12 @@ const EIGHTBALL_RESPONCES: [&'static str; 20] = [ #[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) { +pub async fn eightball(ctx: &Context, msg: &Message, _args: Args) -> CommandResult { + let choice = EIGHTBALL_RESPONSES.choose(&mut thread_rng()); + if let Some(choice) = choice { msg.channel_id .say(&ctx.http, format!("πŸ§€The cheese says {}", choice)) - .ok(); + .await?; } Ok(()) } diff --git a/src/commands/fun/spam.rs b/src/commands/fun/spam.rs index f8c01e6..0281032 100644 --- a/src/commands/fun/spam.rs +++ b/src/commands/fun/spam.rs @@ -8,7 +8,7 @@ use serenity::{ #[command] #[description = "spams a given message a maximum of 10 times"] #[usage(" ")] -pub fn spam(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { +pub async fn spam(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { if let Ok(count) = args.single::() { if count <= 10 { let to_spam = args.rest(); @@ -16,12 +16,13 @@ pub fn spam(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { msg.channel_id .say( &ctx.http, - utils::content_safe(&ctx, to_spam, &utils::ContentSafeOptions::default()), + utils::content_safe(&ctx, to_spam, &utils::ContentSafeOptions::default()) + .await, ) - .ok(); + .await?; } } else { - msg.channel_id.say(&ctx.http, "no thats too many").ok(); + msg.channel_id.say(&ctx.http, "no thats too many").await?; } }; Ok(()) diff --git a/src/commands/runescape.rs b/src/commands/runescape.rs index 56e5c84..a52e5cc 100644 --- a/src/commands/runescape.rs +++ b/src/commands/runescape.rs @@ -1,19 +1,20 @@ +use num_format::{Locale, ToFormattedString}; use serenity::prelude::*; use serenity::{ framework::standard::{macros::command, Args, CommandResult}, model::channel::Message, }; -use num_format::{Locale, ToFormattedString}; #[command] #[description = "echos the message given"] -pub fn profile(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { +pub async fn profile(ctx: &Context, msg: &Message, args: Args) -> CommandResult { let rsn = args.rest().replace(" ", "%20"); - let mut resp = reqwest::get(&format!( + let resp = reqwest::get(&format!( "https://apps.runescape.com/runemetrics/profile/profile?user={}&activities=20", rsn - ))?; - let profile: Profile = match resp.json::() { + )) + .await?; + let profile: Profile = match resp.json::().await { Ok(p) => p, Err(why) => { println!("{}", why); @@ -49,25 +50,27 @@ pub fn profile(ctx: &mut Context, msg: &Message, args: Args) -> CommandResult { 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) + 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) + }) }) - })?; + .await?; 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)?; + msg.channel_id.say(&ctx.http, &activities_out).await?; activities_out.clear(); } } if !activities_out.is_empty() { - msg.channel_id.say(&ctx.http, &activities_out)?; + msg.channel_id.say(&ctx.http, &activities_out).await?; } Ok(()) } @@ -157,12 +160,12 @@ static SKILL_NAMES: [&'static str; 28] = [ "Dungeoneering", "Divination", "Invention", - "Archeology" + "Archeology", ]; static ORDER: [u32; 28] = [ 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, 28 + 27, 28, ]; static EMOJIS: [&'static str; 28] = [ @@ -193,5 +196,5 @@ static EMOJIS: [&'static str; 28] = [ "<:Dungeoneering:406361343386451979>", "<:Divination:406361343374131211>", "<:Invention:406361343591972864>", - "<:Archeology:713541764874764289>" + "<:Archeology:713541764874764289>", ]; diff --git a/src/commands/utility.rs b/src/commands/utility.rs index 02d259e..444f455 100644 --- a/src/commands/utility.rs +++ b/src/commands/utility.rs @@ -1,21 +1,22 @@ -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(()) -} +use serenity::prelude::Context; +use serenity::{ + framework::standard::{macros::command, Args, CommandResult}, + model::channel::Message, +}; + +#[command] +#[description = "responds with pong"] +pub async fn ping(ctx: &Context, message: &Message, _args: Args) -> CommandResult { + let now = message.timestamp; + if let Ok(mut msg) = message.channel_id.say(&ctx.http, "Pong! πŸ“").await { + let msg_timestamp = msg.timestamp; + msg.edit(&ctx, |m| { + m.content(format!( + "Pong! πŸ“ **took {}ms**", + (msg_timestamp - now).num_milliseconds() + )) + }) + .await?; + } + Ok(()) +} diff --git a/src/events.rs b/src/events.rs index 2014f50..70d503b 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,173 +1,24 @@ -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>>; -} +use serenity::{ + async_trait, + model::prelude::*, + prelude::{Context, EventHandler}, +}; +pub struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async 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).await; + } + + async fn message(&self, ctx: Context, new_message: Message) { + if new_message.mentions_user_id(140652945032216576) { + new_message.react(&ctx, 'πŸ€').await.ok(); + } + } +} diff --git a/src/main.rs b/src/main.rs index fdbf250..0bcd8e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use serenity::{ client::Client, @@ -12,7 +12,6 @@ use serenity::{ }; use std::env; -use std::sync::{Arc, Mutex}; mod commands; use commands::{ AVATAR_COMMAND, CAT_RAT_CHEESE_COMMAND, DOG_COMMAND, ECHO_COMMAND, EIGHTBALL_COMMAND, @@ -21,66 +20,55 @@ use commands::{ mod events; #[help] -fn my_help( - ctx: &mut Context, +async fn my_help( + ctx: &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) + help_commands::with_embeds(ctx, msg, args, &help_options, groups, owners).await; + Ok(()) } -fn main() { - let mut client = Client::new(&env::var("DISCORD_TOKEN").expect("token"), events::Handler) +#[tokio::main] +async fn main() { + let mut client = Client::builder(&env::var("DISCORD_TOKEN").expect("token")) + .event_handler(events::Handler) + .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), + ) + .await .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() { + if let Err(why) = client.start().await { println!("An error occurred while running the client: {:?}", why); } } -group!({ - name: "fun", - options: {}, - commands: [cat_rat_cheese, echo, spam, eightball], -}); +#[group] +#[commands(cat_rat_cheese, echo, spam, eightball)] +struct Fun; -group!({ - name: "discord", - options: {}, - commands: [avatar] -}); +#[group] +#[commands(avatar)] +struct Discord; -group!({ - name: "utility", - options: {}, - commands: [ping] -}); +#[group] +#[commands(ping)] +struct Utility; -group!({ - name: "dogs", - options: {}, - commands: [pug, dog] -}); +#[group] +#[commands(pug, dog)] +struct Dogs; -group!({ - name: "runescape", - options: {}, - commands: [profile] -}); +#[group] +#[commands(profile)] +struct Runescape;