rustcord/src/main.rs

527 lines
16 KiB
Rust
Executable File

#![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::UserId, user::OnlineStatus},
prelude::*,
};
use std::{env, process, sync::Arc};
#[macro_use]
mod utils;
use utils::*;
mod commands;
use commands::{brainfuck::*, define::*, embed::*, help::*, lyrics::*, pinned::*, ship::*};
struct Handler;
struct ShardManagerContainer;
impl TypeMapKey for ShardManagerContainer {
type Value = Arc<Mutex<ShardManager>>;
}
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, lyrics
)]
struct General;
lazy_static! {
static ref OWNERS: std::vec::Vec<serenity::model::id::UserId> =
/* Agatha's Id Julia's Id */
vec![UserId(254310746450690048), UserId(687740609703706630)];
}
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::<ShardManagerContainer>(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 {}",
s!(msg.channel_id).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::<ShardManagerContainer>() {
Some(v) => v,
None => {
return Err(CommandError(s!(
"There was a problem getting the shard manager!"
)))
}
};
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(s!("No shard found!"))),
};
let ping = match runner.latency {
Some(v) => v.as_millis(),
None => return Err(CommandError(s!("Could not get latency!"))),
};
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(s!("Called without input")));
}
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(s!("Called without args!")));
}
let mut input = args.single::<String>()?;
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 += &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(|| s!("Unknown")),
cpu = sys_info::cpu_speed()?
),
);
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::<u64>().unwrap_or(10);
let messages = message
.channel_id
.messages(&ctx.http, |get| get.before(message.id).limit(num));
if let Err(e) = messages {
return Err(CommandError(s!(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 += &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]
#[aliases("pat")]
fn headpat(ctx: &mut Context, message: &Message, args: Args) -> CommandResult {
let args = args.rest().trim();
if args.is_empty() {
return Err(CommandError(s!("Please specify a username!")));
}
// 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::<i32>().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 info(ctx: &mut Context, message: &Message, args: Args) -> CommandResult {
if !args.is_empty() {
return Err(CommandError(s!("Called with args!")));
}
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(())
}
#[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(s!("The user does not have an avatar"))),
};
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 lastmsg = match message
.channel_id
.messages(&ctx.http, |get| get.before(message.id).limit(1))
{
Ok(v) => v,
Err(_) => return Err(CommandError(s!("Could not get last message!"))),
};
let ref lastmsg = lastmsg[0].content;
let input: String = match args.is_empty() {
true => s!(lastmsg),
false => 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(s!("Could not get channel!")));
}
};
let channel_lock = match channel_lock.guild() {
Some(g) => g,
None => {
return Err(CommandError(s!("Could not get guild!")));
}
};
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(())
}