#![allow(clippy::unreadable_literal)] #[macro_use] extern crate lazy_static; use colored::*; use rand::Rng; use serenity::{ client::{ bridge::gateway::{ShardId, ShardManager}, Client, }, framework::standard::{ macros::{check, command, group}, Args, CheckResult, CommandError, CommandOptions, CommandResult, DispatchError, Reason, StandardFramework, }, model::{ channel::Message, gateway::Ready, id::{ChannelId, UserId}, user::OnlineStatus, }, prelude::*, }; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::{env, process, sync::Arc}; struct Handler; struct ShardManagerContainer; impl TypeMapKey for ShardManagerContainer { type Value = Arc>; } impl EventHandler for Handler { fn ready(&self, ctx: Context, ready: Ready) { if let Some(shard) = ready.shard { println!( "INFO: {} is connected on shard {}/{}!\nuwu", ready.user.name, shard[0], shard[1] ); use serenity::model::gateway::Activity; let activity = Activity::listening("catgirls nyaaing"); let status = OnlineStatus::Online; ctx.set_presence(Some(activity), status); } } fn message(&self, ctx: Context, message: Message) { let text = &message.content.to_lowercase(); if text.contains("good") && (text.contains("discordinator") || text.contains("discordinyator")) { let _ = message.channel_id.say(&ctx.http, "nyaa~ 💞"); } } } #[group] #[commands( init, ping, halt, servers, host, status, ship, bottom_rng, headpat, uwu, gayculator, sausage, help, embed, define, owo, info, echo, desc, pinned, brainfuck, pfp )] struct General; lazy_static! { static ref OWNERS: std::vec::Vec = /* Agatha's Id Julia's Id */ vec![UserId(254310746450690048), UserId(687740609703706630)]; } // Calculates hash of a type that implements Hash fn calculate_hash(t: &T) -> u64 { let mut s = DefaultHasher::new(); t.hash(&mut s); s.finish() } fn main() { let mut client = Client::new(&env::var("DISCORD_TOKEN").expect("Invalid token"), Handler) .expect("Error creating client"); // Updates stored data, used for ping command { let mut data = client.data.write(); data.insert::(Arc::clone(&client.shard_manager)); } client.with_framework( StandardFramework::new() .configure(|c| { c.with_whitespace(true) .owners(OWNERS.clone().into_iter().collect()) .prefixes(vec!["owo!", "OwO!", "aga"]) .no_dm_prefix(true) .case_insensitivity(true) .by_space(false) }) .on_dispatch_error(|ctx, msg, error| { if let DispatchError::CheckFailed("Owner", Reason::Unknown) = error { // triggers if user is not owner let _ = msg.channel_id.say(&ctx.http, "nyo"); } else if let DispatchError::Ratelimited(_) = error { // triggers if rate limited eprintln!( "{}", format!( "Rate limited in {} with message {}", msg.channel_id.to_string().purple().bold(), msg.content.purple() ) ); } }) .after(|ctx, msg, cmd_name, error| { // prints error in chat if let Err(why) = error { let _ = msg.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title(format!("Error in **{}**", cmd_name)) .description(&why.0) /*.thumbnail("https://i.imgur.com/VzOEz2E.png") oh no */ .colour(0xff6961) }) }); // prints error in console eprintln!( "{}", format!("Error in {}: {}", cmd_name.purple(), &why.0.red().bold()) ); } }) .group(&GENERAL_GROUP), ); if let Err(e) = client.start() { eprintln!("An error occurred while running the client: {:?}", e); } } #[check] #[name = "Owner"] fn owner_check(_: &mut Context, msg: &Message, _: &mut Args, _: &CommandOptions) -> CheckResult { if OWNERS.clone().contains(&msg.author.id) { CheckResult::Success } else { CheckResult::Failure(Reason::Unknown) } } #[check] #[name = "Server"] fn server_check(_: &mut Context, msg: &Message, _: &mut Args, _: &CommandOptions) -> CheckResult { (msg.guild_id == Some(serenity::model::id::GuildId(687011389294116875))).into() } #[command] fn init(ctx: &mut Context, message: &Message) -> CommandResult { let responses = [ "Discordinator9000 is gonna hug nya'll!", "Nyaa~!", "Hewwo uwu", ]; let num = rand::thread_rng().gen_range(0, responses.len()); let _ = message.channel_id.say(&ctx.http, responses[num]); Ok(()) } #[command] fn ping(ctx: &mut Context, message: &Message) -> CommandResult { // I have no idea if this works but its 5æm and I need to sleep help let data = ctx.data.read(); let shard_manager = match data.get::() { Some(v) => v, None => { return Err(CommandError( "There was a problem getting the shard manager!".to_string(), )) } }; let manager = shard_manager.lock(); let runners = manager.runners.lock(); let runner = match runners.get(&ShardId(ctx.shard_id)) { Some(v) => v, None => return Err(CommandError("No shard found!".to_string())), }; let ping = match runner.latency { Some(v) => v.as_millis(), None => return Err(CommandError("Could not get latency!".to_string())), }; let _ = message .channel_id .say(&ctx, format!("Pong! Latency: {}ms", ping)); Ok(()) } #[command] #[checks(Owner)] fn echo(ctx: &mut Context, message: &Message, args: Args) -> CommandResult { let input: String = args.rest().trim().to_string(); if args.is_empty() { return Err(CommandError("Called without input".to_string())); } let _ = message.channel_id.say(&ctx.http, input); Ok(()) } #[command] #[checks(Owner)] fn halt(ctx: &mut Context) -> CommandResult { // Workaround for discord not doing this automatically ctx.set_presence(None, OnlineStatus::Offline); use std::{thread, time}; // Sleep for 1s thread::sleep(time::Duration::new(1, 0)); process::exit(0); } // set bot's status to input text #[command] #[checks(Owner)] fn status(ctx: &mut Context, message: &Message, mut args: Args) -> CommandResult { use serenity::model::gateway::Activity; if args.is_empty() { return Err(CommandError("Called without args!".to_string())); } let mut input = args.single::()?; input = input.trim().to_string(); let activity; // if args contain 'listening', use the appropriate activity type if input.contains("listening") { activity = Activity::listening(args.rest().trim()); // reset status } else if input.contains("reset") { activity = Activity::listening("catgirls nyaaing"); // otherwise default to playing } else { args.restore(); activity = Activity::playing(args.rest().trim()); }; let status = OnlineStatus::Online; ctx.set_presence(Some(activity), status); let _ = message.react(&ctx.http, "💜"); Ok(()) } #[command] #[checks(Owner)] fn servers(ctx: &mut Context, message: &Message) -> CommandResult { let mut list = String::new(); let cache = ctx.cache.read(); for (index, guild_lock) in cache.guilds.values().enumerate() { let guild = guild_lock.read(); list.push_str(&format!("{}: {}\n", index, guild.name)); } let _ = message .channel_id // Add zero width space to all mentions .say(&ctx.http, list.replace("@", "@\u{200B}")); Ok(()) } #[command] #[checks(Owner)] fn host(ctx: &mut Context, message: &Message) -> CommandResult { let _ = message.channel_id.say( &ctx.http, format!( "OS: {os}; {release}\nHost: {host}\nCPU: {cpu}MHz", os = sys_info::os_type()?, host = sys_info::hostname()?, release = sys_info::linux_os_release()? .pretty_name .unwrap_or_else(|| "Unknown".to_string()), cpu = sys_info::cpu_speed()? ), ); Ok(()) } #[command] fn embed(ctx: &mut Context, message: &Message, args: Args) -> CommandResult { use serde::Deserialize; use serenity::utils::Colour; use std::{fs, io::prelude::*}; #[derive(Deserialize)] struct EmbedProperties { author: Option<(String, String)>, colour: Option, description: Option, fields: Option>, footer: Option<(String, String)>, image: Option, thumbnail: Option, timestamp: Option, title: Option, url: Option, } // print documentation from src/embed-docs.txt if &args.rest().trim().to_string() == "help" { let mut file = fs::File::open("./src/embed-docs.txt")?; let mut help_string = String::new(); file.read_to_string(&mut help_string)?; let _ = message.channel_id.say(&ctx.http, help_string); return Ok(()); } let input_embed: EmbedProperties = match toml::from_str(&args.rest().trim()) { Ok(v) => v, Err(e) => { return Err(CommandError(format!("Deserialization error: {:?}", e))); } }; let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { // Set embed author unless empty if input_embed.author.is_some() { let auth = input_embed.author.unwrap(); e.author(|a| { //assuming first array element is name and second is icon url a.name(auth.0); a.icon_url(auth.1); a }); } // Set embed colour unless empty if input_embed.colour.is_some() { e.color(Colour::new( u32::from_str_radix(&input_embed.colour.unwrap(), 16) .ok() .unwrap_or(0x000000), )); } // Set embed description unless empty if input_embed.description.is_some() { e.description(input_embed.description.unwrap()); } // Set embed fields unless empty if input_embed.fields.is_some() { e.fields(input_embed.fields.unwrap()); } // Set embed footer unless empty if input_embed.footer.is_some() { let foot = input_embed.footer.unwrap(); e.footer(|f| { //assuming first array element is name and second is icon url f.text(foot.0); f.icon_url(foot.1); f }); } if input_embed.image.is_some() { e.image(input_embed.image.unwrap()); } if input_embed.thumbnail.is_some() { e.thumbnail(input_embed.thumbnail.unwrap()); } if input_embed.timestamp.is_some() { e.timestamp(input_embed.timestamp.unwrap()); } if input_embed.title.is_some() { e.title(input_embed.title.unwrap()); } if input_embed.url.is_some() { e.url(input_embed.url.unwrap()); } e }); m }); let _ = message .channel_id .say(&ctx.http, format!("Embed requested by: {}", message.author)); Ok(()) } // generate a random number using a keysmash as seed #[command] fn bottom_rng(ctx: &mut Context, message: &Message, mut args: Args) -> CommandResult { use rand::{rngs::StdRng, SeedableRng}; // get N last messages, otherwise 10 let num = args.single::().unwrap_or(10); let messages = message .channel_id .messages(&ctx.http, |get| get.before(message.id).limit(num)); if let Err(e) = messages { let _ = message.channel_id.say(&ctx.http, format!("Error: {}", e)); } else { let mut messages = messages?; // remove all messages by other users messages.retain(|v| v.author != message.mentions[0]); let mut input = String::new(); for msg in messages { input.push_str(&format!("{} ", msg.content)); } let result: u64 = StdRng::seed_from_u64(calculate_hash(&input)).gen_range(0, 100); let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title("Bottom RNG") .description(format!("Result: {}", result)) .color(0x800869) }) }); } Ok(()) } #[command] fn ship(ctx: &mut Context, message: &Message, args: Args) -> CommandResult { use rand::{rngs::StdRng, SeedableRng}; // Get input names let names: String = args.rest().trim().to_string(); // Calculate compatibility based on hash let compat: u64 = StdRng::seed_from_u64(calculate_hash(&names)).gen_range(50, 100); // Initialize a bar to display compatibility percentage let mut compbar = String::from("----------"); compbar.insert_str((compat / 10) as usize, ":purple_heart:"); // Convert names to a Vec let names = names .split_whitespace() .map(|x| x.to_owned()) .collect::>(); // Concatenate names together let shipname: Result = match names.len() { 0 => Err("Invalid input!".to_string()), 1 => Ok(names[0].clone()), _ => { let mut first_halves = String::new(); for name in &names[0..names.len() - 1] { first_halves.push_str(&name[0..name.len() / 2]); } let first_halves = first_halves.as_str(); let last_half = &names[names.len() - 1][(names.len() / 2) + 1..]; Ok(format!("{}{}", first_halves, last_half)) } }; if let Err(e) = shipname { let _ = message.channel_id.say(&ctx.http, e); } else { let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title(format!("Original names: {}", args.rest().trim())) .description(format!( "Ship name:\n**{}**\nCompatibility: **{}%**\n{}", shipname.unwrap(), compat, compbar )) .color(0xffd1dc) }) }); } Ok(()) } #[command] #[aliases("pat")] fn headpat(ctx: &mut Context, message: &Message, args: Args) -> CommandResult { let args = args.rest().trim(); if args.is_empty() { return Err(CommandError("Please specify a username!".to_string())); } // Get username from first mention, otherwise use input text let name = match message.mentions.len() { 0 => args, _ => message.mentions[0].name.as_str(), }; if let Err(e) = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title(format!("Sending headpats to **{}**...", name)) .image( "https://i.pinimg.com/originals/83/1a/90/831a903eab6d827dcfd298b9e3196e30.jpg", ) .description("[Source](https://www.pinterest.com/pin/377809856242075277/)") }) }) { let _ = message.channel_id.say(&ctx.http, format!("{:?}", e)); }; Ok(()) } // send a random uwu image #[command] fn uwu(ctx: &mut Context, message: &Message) -> CommandResult { let images = [ "https://i.redditmedia.com/qDD9W7NJqTAk31y061TuRW9R8qOcCuEmmCWyOsUEavE.png?fit=crop&crop=faces%2Centropy&arh=2&w=640&s=ebdd3f1970b4fe70ccd24a1958e7fc32", "https://www.shitpostbot.com/img/sourceimages/smash-that-mfuckn-uwu-button-57b5aa1de9fe4.jpeg", "https://www.shitpostbot.com/img/sourceimages/fallout-nv-owo-57e586ae15322.jpeg", "https://i.redditmedia.com/-JaK9YW7mPz2S2xBJmXvW4fZ58uGMa4l6GIgYt3dqZg.jpg?fit=crop&crop=faces%2Centropy&arh=2&w=640&s=ebab29a577346b4d18ec914538b69bb4", "https://preview.redd.it/ie48xuwurzt41.jpg?width=640&crop=smart&auto=webp&s=c4a27d5ed086430cd29530a3d3c8e846cad867d5", "https://i.redd.it/9aw0yzprztq41.jpg", "https://i.redd.it/gxjx9jb01ef41.jpg", "https://i.redd.it/n0liugufmks41.jpg", "https://preview.redd.it/fcfrmarhj9s41.jpg?width=640&crop=smart&auto=webp&s=5a4ff9a471dca7cad61b8e56bc65876ef083304a", "https://i.redd.it/ifwsmbme48q41.jpg" ]; let num = rand::thread_rng().gen_range(0, images.len()); let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.image(images[num]); e.footer(|f| f.text(format!("Source: {}", images[num]))); e }) }); Ok(()) } #[command] fn gayculator(ctx: &mut Context, message: &Message, mut args: Args) -> CommandResult { let number_32: i32 = args.single::().unwrap_or(1); let result = if number_32 % 2 == 0 { "much straight" } else { "large gay" }; let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title("Gayness level:") .description(result) .color(0xffd1dc) }) }); Ok(()) } #[command] #[aliases("sosig")] fn sausage(ctx: &mut Context, message: &Message) -> CommandResult { let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e .title("Dongle!") .image("https://cdn.discordapp.com/attachments/689023662489468966/712283397015470120/26029881886330_4.gif") }) }); Ok(()) } #[command] fn help(ctx: &mut Context, message: &Message) -> CommandResult { let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title("Availble commands:") .description("All commands are case-insensitive") .fields(vec![ ("owo!init", "Introduce me", true), ("owo!ping", "Pong", true), ("owo!sausage", "Sosig", true), ("owo!help", "Help the fellow humanz!", true), ("owo!info", "Show information about me!", true), ( "owo!what's this ``word``", "Find a definition of word", true, ), ( "owo!embed ``[args]`` *OR* help", "Create an embed from a Toml object", true, ), ("owo!desc", "Display channel's topic", true), ( "owo!pinned ``num`` ````", "Display channel's Nth pinned message. Channel name is optional", true, ), ("owo!pfp ``@username``", "Post user's profile picture", true), ("owo!brainfuck ``input``", "Execute input code", true), ("owo!ship ``[names]``", "*Shipping intensifies*", true), ("owo!headpat ``name``", "Headpat someone", true), ("owo!owo ``text``", "owoify input text", true), ("\u{200B}", "**Admin commands:**", false), ("owo!halt", "Kill the bot process", true), ("owo!status ``[args]``", "Sets the bot status", true), ("owo!servers", "List the servers I'm in", true), ("owo!host", "Display host info", true), ]) .color(0xffd1dc) }) }); Ok(()) } #[command] fn info(ctx: &mut Context, message: &Message, args: Args) -> CommandResult { if !args.is_empty() { return Err(CommandError("Called with args!".to_string())); } let num = ctx.cache.read().guilds.len(); // get developer's username let aganame = OWNERS.clone()[0].to_user(ctx.http.clone())?.tag(); let _ = message.channel_id.send_message(&ctx.http, |m| m .embed(|e| e .title("Discordinator9000's info:") .field("Author:", format!("{} / Agatha", aganame), false) .field("Server count:", num , false) .field("Invite:", "[Invite link](https://discordapp.com/api/oauth2/authorize?client_id=470350233419907129&permissions=2048&scope=bot)", true ) .field("source:", "[Gitlab](https://gitlab.com/agathasorceress/rustcord)", true) .footer(|f| f .text("Written in Rust using Serenity, OwOify and a few other libraries")) .thumbnail("https://cdn.discordapp.com/attachments/687011390434967621/704118007563157544/discordinator.png") .color(0xffd1dc) )); Ok(()) } // Urban Dictionary lookup #[command] #[aliases("what's this")] fn define(ctx: &mut Context, message: &Message, args: Args) -> CommandResult { let text: String = args.rest().trim().to_string(); let defs = &urbandict::get_definitions(&text); if !args.is_empty() { match defs { Err(_e) => { return Err(CommandError("Invalid query >w<".to_string())); } Ok(v) => { if !v.is_empty() { let def = &v[0]; let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title(format!("Query: {}, Author: {}", text, def.author)) .field( "Definition: ", def.definition.replace(|c| c == '[' || c == ']', ""), false, ) .color(0xffd1dc) }) }); } else { return Err(CommandError("No results!".to_string())); } } } } Ok(()) } #[command] fn pfp(ctx: &mut Context, message: &Message) -> CommandResult { // Get username from first mention, otherwise use current username let user = match message.mentions.len() { 0 => &message.author, _ => &message.mentions[0], }; let pfp = match user.avatar_url() { Some(v) => v, None => return Err(CommandError("The user does not have an avatar".to_string())), }; let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title(format!("{}'s profile picture", user.name)) .image(pfp) .color(0xffd1dc) }) }); Ok(()) } // Text owoification #[command] fn owo(ctx: &mut Context, message: &Message, args: Args) -> CommandResult { use owoify::OwOifiable; let input: String = args.rest().trim().to_string(); let _ = message.channel_id.say(&ctx.http, input.owoify()); Ok(()) } // Prints channel topic #[command] #[aliases("description", "topic")] fn desc(ctx: &mut Context, message: &Message) -> CommandResult { let channel_lock = match message.channel(&ctx) { Some(ch) => ch, None => { return Err(CommandError("Could not get channel!".to_string())); } }; let channel_lock = match channel_lock.guild() { Some(g) => g, None => { return Err(CommandError("Could not get guild!".to_string())); } }; let channel = channel_lock.read(); let topic = if channel.topic.clone().unwrap() != String::from("") { channel.topic.clone().unwrap() } else { String::from("No channel topic found") }; let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title("Channel's topic:") .description(topic) .color(0xffd1dc) }) }); Ok(()) } // Prints Nth pinned message #[command] fn pinned(ctx: &mut Context, message: &Message, mut args: Args) -> CommandResult { // defaults to latest pinned message if no args are provided let mut idx = args.single::().unwrap_or(1); // Makes pinned messages 1-indexed if idx != 0 { idx -= 1; } let target_channel = match args.single::() { Ok(v) => v, Err(_) => message.channel_id, }; let pinned = match target_channel.pins(&ctx.http) { Ok(v) => v, Err(e) => { return Err(CommandError( format!("Could not get pinned messages! Error: {}", e).to_string(), )); } }; if pinned.is_empty() { return Err(CommandError("No pinned messages found!".to_string())); } if idx > pinned.len() - 1 { return Err(CommandError("Index out of bounds!".to_string())); } let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title(format!("Pinned message #{}", idx + 1)) .description(&pinned[idx].content) .timestamp(&pinned[idx].timestamp); e.author(|a| { a.name(&pinned[idx].author.name) .icon_url(&pinned[idx].author.avatar_url().unwrap()) }); e.colour(0xffd1dc); if !&pinned[idx].attachments.is_empty() { e.image(&pinned[idx].attachments[0].url); } e }) }); Ok(()) } // brainfuck interpreter #[command] #[aliases("bf", "brainfrick")] fn brainfuck(ctx: &mut Context, message: &Message, args: Args) -> CommandResult { use brainfrick::Brainfuck; let input = match args.rest().trim() { "" => { return Err(CommandError("Called without input!".to_string())); } v @ _ => v, }; let output = Brainfuck::execute(input); match output { Ok(v) => { let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title("Brainfuck interpreter") .description(format!( "Input\n```brainfuck\n{}\n```\nOutput:\n```{}\n```", input, v )) .author(|a| { a.name(&message.author.name) .icon_url(message.author.avatar_url().unwrap()) }) .colour(0xffd1dc) }) }); } Err(err) => { let _ = message.channel_id.send_message(&ctx.http, |m| { m.embed(|e| { e.title("Brainfuck interpreter") .description(format!("Error at:\n```\n{}:{}\n```", err.line(), err.col())) .author(|a| { a.name(&message.author.name) .icon_url(message.author.avatar_url().unwrap()) }) .colour(0xff6961) }) }); } } Ok(()) }