Update template with changes

This commit is contained in:
toast 2024-12-12 03:01:44 +11:00
parent ef52e95e03
commit 05c9bed1f8
27 changed files with 659 additions and 499 deletions

18
Cargo.lock generated
View File

@ -1463,15 +1463,23 @@ name = "rustbot"
version = "0.1.0"
dependencies = [
"poise",
"rand",
"reqwest",
"rustbot_cmds",
"rustbot_events",
"rustbot_lib",
"rustbot_tokens",
"tokio",
]
[[package]]
name = "rustbot_cmds"
version = "0.1.0"
dependencies = [
"poise",
"rand",
"reqwest",
"rustbot_lib",
"serde",
"sysinfo",
"time",
"tokio",
"uptime_lib",
]
@ -1492,7 +1500,7 @@ dependencies = [
[[package]]
name = "rustbot_lib"
version = "0.1.19"
version = "0.1.0"
dependencies = [
"cargo_toml",
"poise",

View File

@ -5,6 +5,7 @@ edition = "2021"
[workspace]
members = [
"cmds",
"events",
"jobs",
"library",
@ -16,21 +17,21 @@ cargo_toml = "0.21.0"
poise = "0.6.1"
regex = "1.11.0"
serde = "1.0.210"
tokio = { version = "1.40.0", features = ["macros", "signal", "rt-multi-thread"] }
reqwest = { version = "0.12.8", features = ["native-tls-vendored"] }
[dependencies]
rustbot_events = { path = "events" }
rustbot_lib = { path = "library" }
rustbot_tokens = { path = "tsclient" }
poise = { workspace = true }
rand = "0.8.5"
reqwest = { workspace = true }
serde = { workspace = true }
sysinfo = "0.33.0"
time = "0.3.36"
tokio = { workspace = true }
uptime_lib = "0.3.1"
tokio = { version = "1.40.0", features = ["macros", "signal", "rt-multi-thread"] }
reqwest = { version = "0.12.8", features = ["native-tls-vendored"] }
rustbot_lib = { path = "library" }
[dependencies]
poise = { workspace = true }
rustbot_cmds = { path = "cmds" }
rustbot_events = { path = "events" }
rustbot_lib = { workspace = true }
rustbot_tokens = { path = "tsclient" }
tokio = { workspace = true }
[patch.crates-io]
poise = { git = "https://github.com/serenity-rs/poise", branch = "serenity-next" }

13
cmds/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "rustbot_cmds"
version = "0.1.0"
edition = "2024"
[dependencies]
poise = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true }
rustbot_lib = { workspace = true }
serde = { workspace = true }
sysinfo = { workspace = true }
uptime_lib = { workspace = true }

26
cmds/src/dispatch.rs Normal file
View File

@ -0,0 +1,26 @@
mod dev;
mod eightball;
mod ping;
mod uptime;
pub use {
dev::dev,
eightball::eightball,
ping::ping,
uptime::uptime
};
#[macro_export]
macro_rules! collect {
() => {
vec![
// Developer command(s)
$crate::dev(),
// Utility commands
$crate::ping(),
$crate::uptime(),
// Unsorted mess
$crate::eightball(),
]
};
}

View File

@ -1,14 +1,16 @@
use rustbot_lib::{
RustbotContext,
RustbotResult
};
use poise::{
use {
poise::{
CreateReply,
serenity_prelude::{
ChannelId,
ShardId,
ShardRunnerInfo
}
},
rustbot_lib::{
RustbotContext,
RustbotResult
}
};
async fn format_shard_info(
@ -44,9 +46,7 @@ async fn format_shard_info(
interaction_context = "Guild|BotDm|PrivateChannel",
subcommands("deploy", "servers", "shards", "echo")
)]
pub async fn dev(_: RustbotContext<'_>) -> RustbotResult<()> {
Ok(())
}
pub async fn dev(_: RustbotContext<'_>) -> RustbotResult<()> { Ok(()) }
/// Deploy commands to this guild or globally
#[poise::command(prefix_command)]
@ -90,7 +90,8 @@ async fn echo(
ctx: RustbotContext<'_>,
#[description = "Message to be echoed as a bot"] message: String,
#[description = "Channel to send this to"]
#[channel_types("Text", "PublicThread", "PrivateThread")] channel: Option<ChannelId>
#[channel_types("Text", "PublicThread", "PrivateThread")]
channel: Option<ChannelId>
) -> RustbotResult<()> {
ctx.defer_ephemeral().await?;
@ -101,18 +102,10 @@ async fn echo(
match ChannelId::new(channel.get()).say(ctx.http(), message).await {
Ok(_) => {
ctx.send(
CreateReply::new()
.content("Sent!")
.ephemeral(true)
).await?;
ctx.send(CreateReply::new().content("Sent!").ephemeral(true)).await?;
},
Err(y) => {
ctx.send(
CreateReply::new()
.content(format!("Failed... `{y}`"))
.ephemeral(true)
).await?;
ctx.send(CreateReply::new().content(format!("Failed... `{y}`")).ephemeral(true)).await?;
return Ok(());
}
}

276
cmds/src/dispatch/eightball.rs Executable file
View File

@ -0,0 +1,276 @@
use {
poise::{
builtins::paginate,
serenity_prelude::UserId
},
rand::random,
rustbot_lib::{
RustbotContext,
RustbotResult,
config::BINARY_PROPERTIES
}
};
#[derive(poise::ChoiceParameter, Clone)]
enum ResponseMode {
Normal,
Chicken,
#[name = "Chaotic & Unhinged"]
Chaotic
}
/// Ask the Magic 8-Ball a yes/no question and get an unpredictable answer
#[poise::command(
slash_command,
install_context = "Guild|User",
interaction_context = "Guild|BotDm|PrivateChannel",
rename = "8ball"
)]
pub async fn eightball(
ctx: RustbotContext<'_>,
#[description = "Your yes/no question"] question: String,
#[description = "Response modes"] mode: Option<ResponseMode>
) -> RustbotResult<()> {
if question.to_ascii_lowercase().contains("niko, show list") {
show_list(ctx, mode.clone().unwrap_or(ResponseMode::Normal)).await?;
return Ok(())
}
let rand_resp = match mode {
Some(ResponseMode::Chicken) => get_random_chicken_response(),
Some(ResponseMode::Chaotic) => get_random_chaotic_response(),
_ => get_random_response()
};
ctx.reply(format!("> {question}\n{rand_resp}")).await?;
Ok(())
}
async fn show_list(
ctx: RustbotContext<'_>,
list_type: ResponseMode
) -> RustbotResult<()> {
if ctx.author().id != UserId::new(BINARY_PROPERTIES.developers[0]) {
ctx
.reply("The list knows you're looking, but it's playing a game of hide and seek. For now, it wins.")
.await?;
return Ok(());
}
let chunks: Vec<String> = match list_type {
ResponseMode::Normal => RESPONSES.chunks(10).map(|chunk| chunk.join("\n\n")).collect(),
ResponseMode::Chicken => CHICKEN_RESPONSES.chunks(10).map(|chunk| chunk.join("\n\n")).collect(),
ResponseMode::Chaotic => CHAOTIC_RESPONSES.chunks(10).map(|chunk| chunk.join("\n\n")).collect()
};
let pages: Vec<&str> = chunks.iter().map(|s| s.as_str()).collect();
paginate(ctx, &pages).await?;
Ok(())
}
const RESPONSES: [&str; 45] = [
"Reply hazy. Look it up on Google.", // no
"Meh — Figure it out yourself.", // no
"I don't know, what do you think?", // no
"Yes.", // yes
"No.", // no
"It is decidedly so", // yes
"Signs point to... maybe... depends on... hold on, let me get my glasses, this is getting pretty tiny... depends on whether you'd be up to \
getting to know your Magic 8-Ball a little better.", // no
"Signs point to... ~~yes~~ no.", // no
"Why do you want to know the answer? It's obviously a yes.", // yes
"Outlook not so good.", // no
"Outlook hazy.", // no
"What are you, stupid?", // no
"How the hell do you not know that?", // no
"Really? Making a decision based on what the plastic 8-Ball says? Jesus...", // no
"Try asking later...", // no
"I don't know, whip out the ouija board and try again?", // no
"The answer is yes.", // yes
"Yes, actually no. Wait, nevermind.", // no
"Maybeee...", // yes
"Definitely!", // yes
"It is decidedly so.", // yes
"My reply is no.", // no
"My sources confirms that the answer is no.\nSource: :sparkles: *i made it up* :sparkles:", // no
"As I see it, yes.", // yes
"Don't count on it.", // no
"Whoa! Why do I have to answer this?", // no
"Highly unlikely.", // no
"Sure, but with extreme cautions.", // yes
"What kind of stupid question is that?? No! I'm not answering that!", // no
"Try asking this to a chicken. Probably knows it better than I do!", // no
"Not in a million years!", // no
"As a matter of fact, yes.", // yes
"It's a no, better go ask someone else.", // no
"In the end, it's not a bad choice.", // yes
"Nope, not today.", // no
"Cross your fingers, the answer is yes!", // yes
"Nope. *shakes head*", // no
"The fortune cookie said yes.", // yes
"Sorry, the fortune cookie over there said no.", // no
"Sorry, not happening.", // no
"I'll have to consult my sources... *flips coin*... no.", // no
"I'll have to consult the magic 8-ball... *shakes*... no.", // no
"I'm not sure to be honest, let's ask your friend. Oh wait...", // no
"This question flew over my head, I'll pass.", // no
"Oops, the Magic 8-Ball shattered itself when you asked that! I'll take that as a no." // no
];
const CHICKEN_RESPONSES: [&str; 54] = [
"Cluck cluck... Reply hazy, try pecking Google.", // no
"Meh... Figure it out yourself, or scratch around a bit.", // no
"I don't know... what do you think? *pecks at ground*", // no
"BAWK! YES!", // yes
"Cluck... no.", // no
"It is decidedly so! *flaps wings*", // yes
"Signs point to... maybe... hold on, let me fluff my feathers... depends on whether you'd get to know your Magic Chicken a bit better.", // no
"Signs point to... ~~yes~~ cluck no.", // no
"Why do you want to know? It's a big cluckin' yes!", // yes
"Outlook not so clucking good.", // no
"Outlook cluckin' hazy.", // no
"What are you, a lost chick? Cluck!", // no
"How the cluck do you not know that?", // no
"Really? Asking a chicken to decide your fate? *clucks judgmentally*", // no
"Peck back later, I'm nesting...", // no
"I don't know, try flapping your wings and ask again?", // no
"The answer is a big ol' yes! *flaps happily*", // yes
"Yes... wait, actually... no. Cluck, I'm confused.", // no
"Maaaaybe... *chicken waddle*?", // yes
"Definitely! *struts confidently*", // yes
"It is decidedly so. *struts with pride*", // yes
"My reply is a solid *cluck* no.", // no
"My sources confirm it's a cluckin' no.\nSource: 🐔 *I made it up* 🐔", // no
"As I see it, yes! *pecks approvingly*", // yes
"Don't count on it. *cluck cluck*", // no
"Whoa, why do I have to answer this? *fluffs feathers*", // no
"Highly unlikely. *chicken stare*", // no
"Sure, but with extreme cluckin' caution.", // yes
"What kind of stupid question is that?? No! *angry clucks*", // no
"Try asking this to a fellow chicken. They probably know better than I do!", // no
"Cluck yes! *does a happy chicken dance*", // yes
"No way, not even for a big bag of feed.", // no
"Yes! *lays egg of approval*", // yes
"It's a no, better go scratch somewhere else.", // no
"Cluck-tastic! That's a definite yes.", // yes
"Cluck yeah! *struts proudly*", // yes
"Nope, not today. *shakes head*", // no
"Feathers crossed, the answer is yes!", // yes
"Chicken says nope. *tilts head*", // no
"Absolutely! *clucks happily*", // yes
"Not a chance. *fluffs feathers*", // no
"Eggcellent choice! Yes!", // yes
"Not in a million clucks!", // no
"As a matter of cluck, yes! *clucks approvingly*", // yes
"It's a nopity nope, better go ask another chicken.", // no
"In the end, it's not a bad cluck", // yes
"Nope, not today. *clucks sadly*", // no
"Cross your feathers, the answer is yes!", // yes
"The fortune cookie said yes. *clucks in agreement*", // yes
"Sorry, the fortune cookie over there said no. *clucks in disagreement*", // no
"I'll have to consult my sources... *flips corn*... no.", // no
"I'll have to consult the magic 8-cluck... *shakes*... no.", // no
"I'm not sure to be honest, let's ask your chicken friend. Oh wait...", // no
"This question floated over my head, I'll pass. *clucks dismissively*" // no
];
const CHAOTIC_RESPONSES: [&str; 90] = [
"Oops! The Magic 8-Ball shattered upon hearing your question. Coincidence?", // no
"Reply hazy. Ask Googles evil twin, Froogle.", // no
"Meh — Consult the ancient texts of Netflix subtitles.", // no
"I don't know, but your cat probably does.", // no
"Yes, but only if you wear a clown wig.", // yes
"No. Unless the moon winks at you first.", // no
"It is decidedly a resounding honk-honk!", // yes
"Signs point to... maybe... or not... or wait... oh look, a squirrel!", // no
"Signs point to... ~~yes~~ pancakes. Definitely pancakes.", // no
"Why do you want to know? Its obviously a yes — trust the donut prophecy.", // yes
"Outlook not so good. Blame Mercury retrograde or your Wi-Fi.", // no
"Outlook hazy. Consult the nearest fortune-telling hamster.", // no
"What are you, a toaster in disguise?", // no
"How the heck do you not know this? Ask a sock puppet!", // no
"Really? Making life choices based on a magic ball? Bold move, friend.", // no
"Try asking later... when Im less busy binge-watching.", // no
"I don't know, summon a raven and whisper your question into the void.", // no
"The answer is yes, as foretold by the mystical spaghetti.", // yes
"Yes, actually no. Wait, yes? Lets go with potato.", // no
"Maybeee... if the stars align and your pizza has extra cheese.", // yes
"Definitely! Unless gravity stops working.", // yes
"It is decidedly so. So what? Buy a llama and see what happens.", // yes
"My reply is no, and also banana pudding.", // no
"My sources confirm that the answer is no.\nSource: A suspicious pigeon.", // no
"As I see it, yes. As the chicken sees it, no. Trust who you like.", // yes
"Don't count on it. Count on marshmallows instead.", // no
"Whoa! Why do I have to answer this? Ask a rubber duck.", // no
"Highly unlikely. Unless its Tuesday on Mars.", // no
"Sure, but with extreme caution and a tinfoil hat.", // yes
"What kind of silly question is that?? No! Also, heres a kazoo.", // no
"Try asking this to a chicken. Theyre the true oracles.", // no
"Not in a million years! Unless the earth is made of cheese.", // no
"As a matter of fact, yes. And its raining tacos.", // yes
"It's a no, but the raccoons might know better.", // no
"In the end, its not a bad choice. Or is it? Mwahaha.", // yes
"Nope, not today. Try tomorrow after coffee.", // no
"Cross your fingers! Or better yet, cross the streams.", // yes
"Nope. *shakes head like a very judgmental parrot*", // no
"The fortune cookie said yes, but it was written in crayon.", // yes
"Sorry, the fortune cookie over there said no. Blame it.", // no
"Sorry, not happening. But you get a virtual sticker for trying!", // no
"I'll have to consult my sources... *flips a pancake*... no.", // no
"I'll have to consult the magic 8-ball... *shakes it violently*... still no.", // no
"I'm not sure, but your imaginary friend says yes.", // yes
"This question flew over my head, so Ill just say 'llama'.", // no
"The answer is yes, but only if you do it while wearing socks on your hands.", // yes
"No, and I think you broke the space-time continuum by asking.", // no
"Why not? Whats the worst that could happen? Oh wait...", // no
"The stars say yes, but the planets are still debating.", // yes
"The universe just facepalmed at your question.", // no
"Ask again while juggling flaming pineapples for a clearer answer.", // no
"Nope, not unless you bribe me with tacos.", // no
"I consulted the oracle... shes out to lunch. Try later.", // no
"Yes, but only if you can lick your elbow right now.", // yes
"No, because I said so and Im very wise. Also, Im a plastic ball.", // no
"Yes. No. Wait, Ive lost track. Did you hear that noise?", // no
"Absolutely, as long as you bring me a rubber chicken as tribute.", // yes
"I asked a wizard, and they just laughed hysterically.", // no
"The spirits say no, but the ghosts are nodding yes.", // no
"Yes, if you believe in unicorns and the power of friendship.", // yes
"No, and also you might want to move. Somethings behind you.", // no
"Ask again, but this time with interpretive dance.", // no
"Definitely! Unless the moon turns into cheese. Then no.", // yes
"I see... wait, no, I dont see. My crystal ball is buffering.", // no
"Sure! But only after a karaoke duet with a raccoon.", // yes
"Yes, but only if you promise not to tell the ducks.", // yes
"No way, unless you can recite the alphabet backwards in one breath.", // no
"Ask the magic mushroom. Its way more in touch with reality than I am.", // no
"No, because gravity disagrees with your premise.", // no
"Yes, but first you must complete the sacred quest for nachos.", // yes
"The answer is hidden in the folds of your laundry. Go check.", // no
"I would answer, but Im legally obligated to stay mysterious.", // no
"Absolutely! If you can solve this riddle: What walks on four legs in the morning, two legs at noon, and... oh wait, wrong universe.", // yes
"The council of frogs says yes, but only if you croak like one.", // yes
"No, but only because the Magic 8-Ball union forbids it.", // no
"Yes, if the dog wags its tail twice before the clock strikes midnight.", // yes
"Try again after doing three cartwheels and making a wish.", // no
"The ducks in my dreams say no. Theyre rarely wrong.", // no
"Not today, Satan. Not today.", // no
"Yes, but only on Wednesdays during a full moon.", // yes
"No, because bananas dont grow in winter.", // no
"The answer is locked in a time capsule. Check back in 50 years.", // no
"I dont know, but it smells like trouble.", // no
"Why not? The penguins approve, and thats good enough for me.", // yes
"Sure, but only if you say 'bubblegum' ten times fast.", // yes
"No, unless you can outsmart a sentient toaster.", // no
"The answer is yes, but it comes with a plot twist.", // yes
"Flip a coin, spin three times, and consult your nearest cactus. Good luck!", // no
"Only on the condition that you buy me a donut.", // yes
"Yes, but proceed at your own risk. The llamas are watching." // yes
];
fn get_random_response() -> &'static str { RESPONSES[random::<usize>() % RESPONSES.len()] }
fn get_random_chicken_response() -> &'static str { CHICKEN_RESPONSES[random::<usize>() % CHICKEN_RESPONSES.len()] }
fn get_random_chaotic_response() -> &'static str { CHAOTIC_RESPONSES[random::<usize>() % CHAOTIC_RESPONSES.len()] }

View File

@ -1,7 +1,9 @@
use serde::Deserialize;
use rustbot_lib::{
use {
rustbot_lib::{
RustbotContext,
RustbotResult
},
serde::Deserialize
};
#[derive(Deserialize)]
@ -20,16 +22,14 @@ struct Summary {
}
/// Check latency between bot and WebSocket as well as Discord's API latency
#[poise::command(
slash_command,
install_context = "Guild|User",
interaction_context = "Guild|BotDm|PrivateChannel"
)]
#[poise::command(slash_command, install_context = "Guild|User", interaction_context = "Guild|BotDm|PrivateChannel")]
pub async fn ping(ctx: RustbotContext<'_>) -> RustbotResult<()> {
let statuspage: StatusPage = reqwest::get("https://discordstatus.com/metrics-display/5k2rt9f7pmny/day.json")
.await.unwrap()
.await
.unwrap()
.json()
.await.unwrap();
.await
.unwrap();
let mut latencies = Vec::new();
latencies.push(format!("Discord: `{:.0?}ms`", statuspage.metrics[0].summary.mean));

View File

@ -1,28 +1,31 @@
use sysinfo::System;
use uptime_lib::get;
use std::{
use {
rustbot_lib::{
RustbotContext,
RustbotResult,
config::BINARY_PROPERTIES,
utils::{
BOT_VERSION,
GIT_COMMIT_BRANCH,
GIT_COMMIT_HASH,
format_duration
}
},
std::{
env::var,
fs::File,
io::{
BufRead,
BufReader
},
path::Path,
time::{
Duration,
SystemTime,
UNIX_EPOCH
}
},
io::{
BufRead,
BufReader
}
};
use rustbot_lib::{
RustbotContext,
RustbotResult,
utils::{
BOT_VERSION,
GIT_COMMIT_HASH,
GIT_COMMIT_BRANCH,
format_duration
}
sysinfo::System,
uptime_lib::get
};
fn get_os_info() -> String {
@ -33,13 +36,11 @@ fn get_os_info() -> String {
if let Ok(file) = File::open(path) {
let reader = BufReader::new(file);
let set_value = |s: String| s.split('=').nth(1).unwrap_or_default().trim_matches('"').to_string();
reader.lines().map_while(Result::ok).for_each(|line| {
match line {
reader.lines().map_while(Result::ok).for_each(|line| match line {
l if l.starts_with("NAME=") => name = set_value(l),
l if l.starts_with("VERSION=") => version = set_value(l),
l if l.starts_with("VERSION_ID=") => version = set_value(l),
_ => {}
}
});
}
@ -95,16 +96,21 @@ pub async fn uptime(ctx: RustbotContext<'_>) -> RustbotResult<()> {
}
// Fetch the node hostname from envvar
let docker_node = match var("DOCKER_HOSTNAME") {
let node_hostname = if BINARY_PROPERTIES.env.contains("prod") {
match var("DOCKER_HOSTNAME") {
Ok(h) => h.to_string(),
Err(_) => "DOCKER_HOSTNAME is empty!".to_string()
}
} else {
let hostname = std::process::Command::new("hostname").output().unwrap().stdout;
String::from_utf8(hostname).unwrap().trim().to_string()
};
let stat_msg = [
format!("**{} v{}** `{GIT_COMMIT_HASH}:{GIT_COMMIT_BRANCH}`", bot.name, *BOT_VERSION),
format!(">>> System: `{}`", format_duration(sys_uptime)),
format!("Process: `{}`", format_duration(proc_uptime)),
format!("Node: `{docker_node}`"),
format!("Node: `{node_hostname}`"),
format!("CPU: `{}`", cpu[0].brand()),
format!("RAM: `{pram}` (`{sram}/{sram_total}`)"),
format!("OS: `{}`", get_os_info())

2
cmds/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
mod dispatch;
pub use dispatch::*;

View File

@ -4,8 +4,8 @@ version = "0.1.0"
edition = "2021"
[dependencies]
rustbot_lib = { path = "../library" }
poise = { workspace = true }
rustbot_lib = { workspace = true }
[features]
production = ["rustbot_lib/production"]

View File

@ -1,10 +1,12 @@
mod ready;
mod shards;
use poise::serenity_prelude::FullEvent;
use rustbot_lib::{
use {
poise::serenity_prelude::FullEvent,
rustbot_lib::{
RustbotFwCtx,
RustbotResult
}
};
pub const RUSTBOT_EVENT: &str = "RustbotEvent";

View File

@ -3,26 +3,28 @@ use super::{
RUSTBOT_EVENT
};
use rustbot_lib::{
RustbotFwCtx,
RustbotResult,
use {
poise::serenity_prelude::{
ChannelId,
CreateEmbed,
CreateEmbedAuthor,
CreateMessage,
Ready
},
rustbot_lib::{
config::BINARY_PROPERTIES,
utils::{
BOT_VERSION,
GIT_COMMIT_HASH,
GIT_COMMIT_BRANCH
GIT_COMMIT_BRANCH,
GIT_COMMIT_HASH
},
config::BINARY_PROPERTIES
};
use std::sync::atomic::{
RustbotFwCtx,
RustbotResult
},
std::sync::atomic::{
AtomicBool,
Ordering::Relaxed
};
use poise::serenity_prelude::{
Ready,
ChannelId,
CreateMessage,
CreateEmbed,
CreateEmbedAuthor
}
};
static READY_ONCE: AtomicBool = AtomicBool::new(false);
@ -33,14 +35,26 @@ async fn ready_once(
) -> RustbotResult<()> {
#[cfg(not(feature = "production"))]
{
println!("{RUSTBOT_EVENT}[Ready:Notice:S{}]: Detected a non-production environment!", framework.serenity_context.shard_id);
println!(
"{RUSTBOT_EVENT}[Ready:Notice:S{}]: Detected a non-production environment!",
framework.serenity_context.shard_id
);
let gateway = framework.serenity_context.http.get_bot_gateway().await?;
let session = gateway.session_start_limit;
println!("{RUSTBOT_EVENT}[Ready:Notice:S{}]: Session limit: {}/{}", framework.serenity_context.shard_id, session.remaining, session.total);
println!(
"{RUSTBOT_EVENT}[Ready:Notice:S{}]: Session limit: {}/{}",
framework.serenity_context.shard_id, session.remaining, session.total
);
}
println!("{RUSTBOT_EVENT}[Ready:S{}]: Build version: {} ({}:{})", framework.serenity_context.shard_id, *BOT_VERSION, GIT_COMMIT_HASH, GIT_COMMIT_BRANCH);
println!("{RUSTBOT_EVENT}[Ready:S{}]: Connected to API as {}", framework.serenity_context.shard_id, ready.user.name);
println!(
"{RUSTBOT_EVENT}[Ready:S{}]: Build version: {} ({GIT_COMMIT_HASH}:{GIT_COMMIT_BRANCH})",
framework.serenity_context.shard_id, *BOT_VERSION
);
println!(
"{RUSTBOT_EVENT}[Ready:S{}]: Connected to API as {}",
framework.serenity_context.shard_id, ready.user.name
);
let message = CreateMessage::new();
let ready_embed = CreateEmbed::new()
@ -48,7 +62,9 @@ async fn ready_once(
.thumbnail(ready.user.avatar_url().unwrap_or_default())
.author(CreateEmbedAuthor::new(format!("{} is ready!", ready.user.name)));
ChannelId::new(BINARY_PROPERTIES.rustbot_logs).send_message(&framework.serenity_context.http, message.add_embed(ready_embed)).await?;
ChannelId::new(BINARY_PROPERTIES.rustbot_logs)
.send_message(&framework.serenity_context.http, message.add_embed(ready_embed))
.await?;
Ok(())
}
@ -59,7 +75,9 @@ impl EventProcessor<'_> {
data_about_bot: &Ready
) -> RustbotResult<()> {
if !READY_ONCE.swap(true, Relaxed) {
ready_once(data_about_bot, self.framework).await.expect("Failed to call ready_once method");
ready_once(data_about_bot, self.framework)
.await
.expect("Failed to call ready_once method");
}
Ok(())

View File

@ -3,16 +3,22 @@ use super::{
RUSTBOT_EVENT
};
use std::num::NonZero;
use rustbot_lib::RustbotResult;
use poise::serenity_prelude::ShardStageUpdateEvent;
use {
poise::serenity_prelude::ShardStageUpdateEvent,
rustbot_lib::RustbotResult,
std::num::NonZero
};
impl EventProcessor<'_> {
pub async fn on_shards_ready(
&self,
total_shards: &NonZero<u16>
) -> RustbotResult<()> {
let shards = if *total_shards == NonZero::new(1).unwrap() { "shard is" } else { "shards are" };
let shards = if *total_shards == NonZero::new(1).unwrap() {
"shard is"
} else {
"shards are"
};
println!("{RUSTBOT_EVENT}[ShardsReady]: {total_shards} {shards} ready!");
Ok(())

View File

@ -1,23 +1,23 @@
use crate::RUSTBOT_SCHEDULER;
use tokio::{
use {
std::{
future::Future,
sync::Arc
},
tokio::{
task,
time::{
interval,
Duration
}
};
use std::{
sync::Arc,
future::Future
}
};
pub struct Scheduler;
impl Scheduler {
pub fn new() -> Arc<Self> {
Arc::new(Self)
}
pub fn new() -> Arc<Self> { Arc::new(Self) }
pub async fn spawn_job<F, E>(
&self,

View File

@ -1,6 +1,6 @@
[package]
name = "rustbot_lib"
version = "0.1.19"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@ -23,15 +23,4 @@ fn main() {
println!("cargo:rustc-env=GIT_COMMIT_BRANCH=not_found");
}
}
{
let hostname = std::process::Command::new("hostname")
.output()
.expect("Command execution failed: hostname");
if hostname.status.success() {
let hostname = String::from_utf8(hostname.stdout).expect("Invalid UTF-8 sequence").trim().to_string();
println!("cargo:rustc-env=DOCKER_HOSTNAME={}", &hostname);
}
}
}

View File

@ -11,33 +11,35 @@ pub struct ConfigMeta {
pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(ConfigMeta::new);
#[cfg(not(feature = "production"))]
pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(||
ConfigMeta::new()
.env("dev")
.embed_color(0xf1d63c)
);
pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(|| ConfigMeta::new().env("dev").embed_color(0xF1D63C));
impl ConfigMeta {
fn new() -> Self {
Self {
env: "prod",
embed_color: 0xf1d63c,
embed_color: 0xF1D63C,
rustbot_logs: 1311282815601741844,
developers: vec![
190407856527376384 // toast.ts
190407856527376384, // toast.ts
]
}
}
// Scalable functions below;
#[cfg(not(feature = "production"))]
fn env(mut self, env: &'static str) -> Self {
fn env(
mut self,
env: &'static str
) -> Self {
self.env = env;
self
}
#[cfg(not(feature = "production"))]
fn embed_color(mut self, color: u32) -> Self {
fn embed_color(
mut self,
color: u32
) -> Self {
self.embed_color = color;
self
}

View File

@ -1,6 +1,8 @@
use poise::serenity_prelude::UserId;
use cargo_toml::Manifest;
use std::sync::LazyLock;
use {
cargo_toml::Manifest,
poise::serenity_prelude::UserId,
std::sync::LazyLock
};
#[cfg(feature = "production")]
pub static GIT_COMMIT_HASH: &str = env!("GIT_COMMIT_HASH");
@ -17,9 +19,7 @@ pub static BOT_VERSION: LazyLock<String> = LazyLock::new(|| {
.unwrap()
});
pub fn format_timestamp(timestamp: i64) -> String {
format!("<t:{timestamp}>\n<t:{timestamp}:R>")
}
pub fn format_timestamp(timestamp: i64) -> String { format!("<t:{timestamp}>\n<t:{timestamp}:R>") }
pub fn mention_dev(ctx: super::RustbotContext<'_>) -> Option<String> {
let devs = super::config::BINARY_PROPERTIES.developers.clone();
@ -53,12 +53,7 @@ pub fn format_duration(secs: u64) -> String {
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
let components = [
(days, "d"),
(hours, "h"),
(minutes, "m"),
(seconds, "s"),
];
let components = [(days, "d"), (hours, "h"), (minutes, "m"), (seconds, "s")];
let formatted_string: Vec<String> = components
.iter()

4
run.sh
View File

@ -1,6 +1,4 @@
#!/bin/bash
export DOCKER_HOSTNAME=$(hostname)
export $(cat .env.bot | xargs)
clear && cargo run
unset DOCKER_HOSTNAME
clear && cargo fmt && cargo run

2
rust-toolchain Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

20
rustfmt.toml Normal file
View File

@ -0,0 +1,20 @@
edition = "2024"
hex_literal_case = "Upper"
binop_separator = "Front"
brace_style = "SameLineWhere"
fn_params_layout = "Vertical"
imports_layout = "Vertical"
imports_granularity = "One"
fn_single_line = true
format_strings = true
max_width = 150
tab_spaces = 2
hard_tabs = false
trailing_comma = "Never"
match_block_trailing_comma = true
reorder_imports = true
reorder_modules = true
reorder_impl_items = true
trailing_semicolon = false
struct_field_align_threshold = 20
condense_wildcard_suffixes = true

View File

@ -1,26 +0,0 @@
mod dev;
mod eightball;
mod ping;
mod uptime;
pub use dev::dev;
pub use eightball::eightball;
pub use ping::ping;
pub use uptime::uptime;
macro_rules! collect {
() => {
vec![
// Developer command(s)
commands::dev(),
// Utility commands
commands::ping(),
commands::uptime(),
// Unsorted mess
commands::eightball(),
]
};
}
pub(crate) use collect;

View File

@ -1,180 +0,0 @@
use rustbot_lib::{
RustbotContext,
RustbotResult,
config::BINARY_PROPERTIES
};
use poise::{
serenity_prelude::UserId,
builtins::paginate
};
#[derive(poise::ChoiceParameter)]
enum ResponseMode {
Normal,
Chicken
}
/// Ask the Magic 8-Ball a yes/no question and get an unpredictable answer
#[poise::command(
slash_command,
install_context = "Guild|User",
interaction_context = "Guild|BotDm|PrivateChannel",
rename = "8ball"
)]
pub async fn eightball(
ctx: RustbotContext<'_>,
#[description = "Your yes/no question"] question: String,
#[description = "Response modes"] mode: Option<ResponseMode>
) -> RustbotResult<()> {
if question.to_ascii_lowercase().contains("niko, show list") {
if ctx.author().id == UserId::new(BINARY_PROPERTIES.developers[0]) {
let chunks: Vec<String> = RESPONSES.chunks(10).map(|chunk| chunk.join("\n\n")).collect();
let pages: Vec<&str> = chunks.iter().map(|s| s.as_str()).collect();
paginate(ctx, &pages).await?;
return Ok(());
} else {
ctx.reply("No.").await?;
return Ok(());
}
}
if question.to_ascii_lowercase().contains("niko, show chicken list") {
if ctx.author().id == UserId::new(BINARY_PROPERTIES.developers[0]) {
let chunks: Vec<String> = CHICKEN_RESPONSES.chunks(10).map(|chunk| chunk.join("\n\n")).collect();
let pages: Vec<&str> = chunks.iter().map(|s| s.as_str()).collect();
paginate(ctx, &pages).await?;
return Ok(());
} else {
ctx.reply("No.").await?;
return Ok(());
}
}
let rand_resp = match mode {
Some(ResponseMode::Chicken) => get_random_chicken_response(),
_ => get_random_response()
};
ctx.reply(format!("> {question}\n{rand_resp}")).await?;
Ok(())
}
const RESPONSES: [&str; 45] = [
"Reply hazy. Look it up on Google.", // no
"Meh — Figure it out yourself.", // no
"I don't know, what do you think?", // no
"Yes.", // yes
"No.", // no
"It is decidedly so", // yes
"Signs point to... maybe... depends on... \
hold on, let me get my glasses, this is getting \
pretty tiny... depends on whether you'd be up \
to getting to know your Magic 8-Ball a little better.", // no
"Signs point to... ~~yes~~ no.", // no
"Why do you want to know the answer? It's obviously a yes.", // yes
"Outlook not so good.", // no
"Outlook hazy.", // no
"What are you, stupid?", // no
"How the hell do you not know that?", // no
"Really? Making a decision based on what the plastic 8-Ball says? Jesus...", // no
"Try asking later...", // no
"I don't know, whip out the ouija board and try again?", // no
"The answer is yes.", // yes
"Yes, actually no. Wait, nevermind.", // no
"Maybeee...", // yes
"Definitely!", // yes
"It is decidedly so.", // yes
"My reply is no.", // no
"My sources confirms that the answer is no.\n\
Source: :sparkles: *i made it up* :sparkles:", // no
"As I see it, yes.", // yes
"Don't count on it.", // no
"Whoa! Why do I have to answer this?", // no
"Highly unlikely.", // no
"Sure, but with extreme cautions.", // yes
"What kind of stupid question is that?? No! I'm not answering that!", // no
"Try asking this to a chicken. Probably knows it better than I do!", // no
"Not in a million years!", // no
"As a matter of fact, yes.", // yes
"It's a no, better go ask someone else.", // no
"In the end, it's not a bad choice.", // yes
"Nope, not today.", // no
"Cross your fingers, the answer is yes!", // yes
"Nope. *shakes head*", // no
"The fortune cookie said yes.", // yes
"Sorry, the fortune cookie over there said no.", // no
"Sorry, not happening.", // no
"I'll have to consult my sources... *flips coin*... no.", // no
"I'll have to consult the magic 8-ball... *shakes*... no.", // no
"I'm not sure to be honest, let's ask your friend. Oh wait...", // no
"This question flew over my head, I'll pass.", // no
"Oops, the Magic 8-Ball shattered itself when you asked that! I'll take that as a no.", // no
];
const CHICKEN_RESPONSES: [&str; 54] = [
"Cluck cluck... Reply hazy, try pecking Google.", // no
"Meh... Figure it out yourself, or scratch around a bit.", // no
"I don't know... what do you think? *pecks at ground*", // no
"BAWK! YES!", // yes
"Cluck... no.", // no
"It is decidedly so! *flaps wings*", // yes
"Signs point to... maybe... hold on, let me fluff my feathers... depends on whether you'd get to know your Magic Chicken a bit better.", // no
"Signs point to... ~~yes~~ cluck no.", // no
"Why do you want to know? It's a big cluckin' yes!", // yes
"Outlook not so clucking good.", // no
"Outlook cluckin' hazy.", // no
"What are you, a lost chick? Cluck!", // no
"How the cluck do you not know that?", // no
"Really? Asking a chicken to decide your fate? *clucks judgmentally*", // no
"Peck back later, I'm nesting...", // no
"I don't know, try flapping your wings and ask again?", // no
"The answer is a big ol' yes! *flaps happily*", // yes
"Yes... wait, actually... no. Cluck, I'm confused.", // no
"Maaaaybe... *chicken waddle*?", // yes
"Definitely! *struts confidently*", // yes
"It is decidedly so. *struts with pride*", // yes
"My reply is a solid *cluck* no.", // no
"My sources confirm it's a cluckin' no.\nSource: 🐔 *I made it up* 🐔", // no
"As I see it, yes! *pecks approvingly*", // yes
"Don't count on it. *cluck cluck*", // no
"Whoa, why do I have to answer this? *fluffs feathers*", // no
"Highly unlikely. *chicken stare*", // no
"Sure, but with extreme cluckin' caution.", // yes
"What kind of stupid question is that?? No! *angry clucks*", // no
"Try asking this to a fellow chicken. They probably know better than I do!", // no
"Cluck yes! *does a happy chicken dance*", // yes
"No way, not even for a big bag of feed.", // no
"Yes! *lays egg of approval*", // yes
"It's a no, better go scratch somewhere else.", // no
"Cluck-tastic! That's a definite yes.", // yes
"Cluck yeah! *struts proudly*", // yes
"Nope, not today. *shakes head*", // no
"Feathers crossed, the answer is yes!", // yes
"Chicken says nope. *tilts head*", // no
"Absolutely! *clucks happily*", // yes
"Not a chance. *fluffs feathers*", // no
"Eggcellent choice! Yes!", // yes
"Not in a million clucks!", // no
"As a matter of cluck, yes! *clucks approvingly*", // yes
"It's a nopity nope, better go ask another chicken.", // no
"In the end, it's not a bad cluck", // yes
"Nope, not today. *clucks sadly*", // no
"Cross your feathers, the answer is yes!", // yes
"The fortune cookie said yes. *clucks in agreement*", // yes
"Sorry, the fortune cookie over there said no. *clucks in disagreement*", // no
"I'll have to consult my sources... *flips corn*... no.", // no
"I'll have to consult the magic 8-cluck... *shakes*... no.", // no
"I'm not sure to be honest, let's ask your chicken friend. Oh wait...", // no
"This question floated over my head, I'll pass. *clucks dismissively*", // no
];
fn get_random_response() -> &'static str {
RESPONSES[rand::random::<usize>() % RESPONSES.len()]
}
fn get_random_chicken_response() -> &'static str {
CHICKEN_RESPONSES[rand::random::<usize>() % CHICKEN_RESPONSES.len()]
}

View File

@ -1,27 +1,29 @@
mod commands;
mod shutdown;
// https://cdn.toast-server.net/RustFSHiearchy.png
// Using the new filesystem hierarchy
use rustbot_tokens::discord_token;
use poise::serenity_prelude::{
use {
poise::serenity_prelude::{
builder::CreateAllowedMentions,
ClientBuilder,
ActivityData,
ClientBuilder,
GatewayIntents
};
use rustbot_lib::{
utils::{
mention_dev,
get_guild_name
},
RustbotData,
config::BINARY_PROPERTIES
};
use rustbot_events::events::processor;
use std::{
sync::Arc,
borrow::Cow
rustbot_cmds::collect,
rustbot_events::events::processor,
rustbot_lib::{
config::BINARY_PROPERTIES,
utils::{
get_guild_name,
mention_dev
},
RustbotData
},
rustbot_tokens::discord_token,
std::{
borrow::Cow,
sync::Arc
}
};
#[tokio::main]
@ -32,11 +34,11 @@ async fn main() {
Some(Cow::Borrowed("pg!"))
};
let commands = commands::collect!();
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands,
pre_command: |ctx| Box::pin(async move {
commands: collect!(),
pre_command: |ctx| {
Box::pin(async move {
let get_guild_channel_name = match ctx.guild_channel().await {
Some(channel) => format!("in #{}", channel.name.clone()),
None => String::from("")
@ -53,7 +55,8 @@ async fn main() {
ctx.author().name,
ctx.command().qualified_name,
);
}),
})
},
prefix_options: poise::PrefixFrameworkOptions {
prefix,
ignore_bots: true,
@ -62,52 +65,69 @@ async fn main() {
execute_self_messages: false,
..Default::default()
},
on_error: |error| Box::pin(async move {
on_error: |error| {
Box::pin(async move {
match error {
poise::FrameworkError::Command { error, ctx, .. } => {
println!("PoiseCommandError({}): {}", ctx.command().qualified_name, error);
ctx.reply(format!(
ctx
.reply(format!(
"Encountered an error during command execution, ask {} to check console for more details!",
mention_dev(ctx).unwrap_or_default()
)).await.expect("Error sending message");
))
.await
.expect("Error sending message");
},
poise::FrameworkError::EventHandler { error, event, .. } => println!("PoiseEventHandlerError({}): {}", event.snake_case_name(), error),
poise::FrameworkError::NotAnOwner { ctx, .. } => {
println!("PoiseNotAnOwner: {} tried to execute a developer-level command ({})", ctx.author().name, ctx.command().qualified_name);
ctx.reply("Whoa, you discovered a hidden command! Too bad, I can't allow you to execute it as you're not my creator.").await.expect("Error sending message");
println!(
"PoiseNotAnOwner: {} tried to execute a developer-level command ({})",
ctx.author().name,
ctx.command().qualified_name
);
ctx
.reply("Whoa, you discovered a hidden command! Too bad, I can't allow you to execute it as you're not my creator.")
.await
.expect("Error sending message");
},
poise::FrameworkError::UnknownInteraction { interaction, .. } => println!(
"PoiseUnknownInteractionError: {} tried to execute an unknown interaction ({})",
interaction.user.name,
interaction.data.name
interaction.user.name, interaction.data.name
),
poise::FrameworkError::UnknownCommand { msg, .. } => println!(
"PoiseUnknownCommandError: {} tried to execute an unknown command ({})",
msg.author.name,
msg.content
msg.author.name, msg.content
),
poise::FrameworkError::ArgumentParse { ctx, error, .. } => {
println!("PoiseArgumentParseError: {}", error);
ctx.reply(format!("Error parsing argument(s): {error}")).await.expect("Error sending message");
ctx
.reply(format!("Error parsing argument(s): {error}"))
.await
.expect("Error sending message");
},
poise::FrameworkError::CommandPanic { ctx, payload, .. } => {
if let Some(payload) = payload.clone() {
println!("PoiseCommandPanic: {payload}");
ctx.reply(format!(
ctx
.reply(format!(
"The command panicked, please tell my developer about this!\n**Error:**```\n{payload}\n```"
)).await.expect("Error sending message");
))
.await
.expect("Error sending message");
} else {
println!("PoiseCommandPanic: No payload provided");
let uh_oh = [
"Well, this is concerning... Hopefully you notified my developer about this!",
"The command panicked, but didn't leave any trace behind... Suspicious!",
].join("\n");
"The command panicked, but didn't leave any trace behind... Suspicious!"
]
.join("\n");
ctx.reply(uh_oh).await.expect("Error sending message");
}
},
other => println!("PoiseOtherError: {other}")
}
}),
})
},
allowed_mentions: Some(CreateAllowedMentions::default().empty_users()),
initialize_owners: true,
skip_checks_for_owners: true,
@ -118,14 +138,13 @@ async fn main() {
let mut client = ClientBuilder::new(
discord_token().await,
GatewayIntents::GUILDS
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::MESSAGE_CONTENT
GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT
)
.framework(framework)
.data(Arc::new(RustbotData {}))
.activity(ActivityData::custom("nep nep!"))
.await.expect("Error creating client");
.await
.expect("Error creating client");
let shard_manager = client.shard_manager.clone();

View File

@ -7,16 +7,11 @@ use tokio::{
};
pub async fn gracefully_shutdown() {
let [mut s1, mut s2, mut s3] = [
signal(SignalKind::interrupt()).unwrap(),
signal(SignalKind::terminate()).unwrap(),
signal(SignalKind::hangup()).unwrap()
];
let [mut s1, mut s2] = [signal(SignalKind::interrupt()).unwrap(), signal(SignalKind::hangup()).unwrap()];
select!(
v = s1.recv() => v.unwrap(),
v = s2.recv() => v.unwrap(),
v = s3.recv() => v.unwrap()
v = s2.recv() => v.unwrap()
);
println!("\nRustbot says goodbye! 👋");

View File

@ -5,5 +5,5 @@ edition = "2021"
[dependencies]
poise = { workspace = true }
tokenservice-client = { version = "0.4.1", registry = "gitea" }
tokenservice-client = { version = "0.4.2", registry = "gitea" }
tokio = { workspace = true }

View File

@ -1,20 +1,20 @@
use poise::serenity_prelude::Token;
use tokio::sync::Mutex;
use std::{
use {
poise::serenity_prelude::Token,
std::{
str::FromStr,
sync::LazyLock
};
use tokenservice_client::{
},
tokenservice_client::{
TokenService,
TokenServiceApi
},
tokio::sync::Mutex
};
pub struct TSClient(TokenService);
impl Default for TSClient {
fn default() -> Self {
Self::new()
}
fn default() -> Self { Self::new() }
}
impl TSClient {
@ -34,11 +34,6 @@ impl TSClient {
static TSCLIENT: LazyLock<Mutex<TSClient>> = LazyLock::new(|| Mutex::new(TSClient::new()));
pub async fn token_path() -> TokenServiceApi {
TSCLIENT.lock().await.get().await.unwrap()
}
pub async fn token_path() -> TokenServiceApi { TSCLIENT.lock().await.get().await.unwrap() }
pub async fn discord_token() -> Token {
Token::from_str(&token_path().await.main)
.expect("Serenity couldn't parse the bot token!")
}
pub async fn discord_token() -> Token { Token::from_str(&token_path().await.main).expect("Serenity couldn't parse the bot token!") }