Update template with changes

This commit is contained in:
toast 2024-11-27 21:50:28 +11:00
parent 1afad09caf
commit 56e1419b32
19 changed files with 786 additions and 423 deletions

881
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ cargo_toml = "0.20.5"
poise = "0.6.1"
regex = "1.11.0"
serde = "1.0.210"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1.40.0", features = ["macros", "signal", "rt-multi-thread"] }
reqwest = { version = "0.12.8", features = ["native-tls-vendored"] }
[dependencies]

View File

@ -1,25 +1,22 @@
mod ready;
mod shards;
use poise::serenity_prelude::FullEvent;
use rustbot_lib::{
RustbotError,
RustbotData
};
use poise::{
FrameworkContext,
serenity_prelude::FullEvent
RustbotFwCtx,
RustbotResult
};
pub const RUSTBOT_EVENT: &str = "RustbotEvent";
struct EventProcessor<'a> {
framework: FrameworkContext<'a, RustbotData, RustbotError>
framework: RustbotFwCtx<'a>
}
pub async fn processor(
framework: FrameworkContext<'_, RustbotData, RustbotError>,
framework: RustbotFwCtx<'_>,
event: &FullEvent
) -> Result<(), RustbotError> {
) -> RustbotResult<()> {
let processor = EventProcessor { framework };
match event {

View File

@ -1,11 +1,11 @@
use crate::PoiseFwCtx;
use super::{
EventProcessor,
RUSTBOT_EVENT
};
use rustbot_lib::{
RustbotError,
RustbotFwCtx,
RustbotResult,
utils::{
BOT_VERSION,
GIT_COMMIT_HASH,
@ -15,7 +15,7 @@ use rustbot_lib::{
};
use std::sync::atomic::{
AtomicBool,
Ordering
Ordering::Relaxed
};
use poise::serenity_prelude::{
Ready,
@ -29,8 +29,8 @@ static READY_ONCE: AtomicBool = AtomicBool::new(false);
async fn ready_once(
ready: &Ready,
framework: PoiseFwCtx<'_>
) -> Result<(), RustbotError> {
framework: RustbotFwCtx<'_>
) -> RustbotResult<()> {
#[cfg(not(feature = "production"))]
{
println!("{RUSTBOT_EVENT}[Ready:Notice:S{}]: Detected a non-production environment!", framework.serenity_context.shard_id);
@ -57,8 +57,8 @@ impl EventProcessor<'_> {
pub async fn on_ready(
&self,
data_about_bot: &Ready
) -> Result<(), RustbotError> {
if !READY_ONCE.swap(true, Ordering::Relaxed) {
) -> RustbotResult<()> {
if !READY_ONCE.swap(true, Relaxed) {
ready_once(data_about_bot, self.framework).await.expect("Failed to call ready_once method");
}

View File

@ -4,14 +4,14 @@ use super::{
};
use std::num::NonZero;
use rustbot_lib::RustbotError;
use rustbot_lib::RustbotResult;
use poise::serenity_prelude::ShardStageUpdateEvent;
impl EventProcessor<'_> {
pub async fn on_shards_ready(
&self,
total_shards: &NonZero<u16>
) -> Result<(), RustbotError> {
) -> RustbotResult<()> {
let shards = if *total_shards == NonZero::new(1).unwrap() { "shard is" } else { "shards are" };
println!("{RUSTBOT_EVENT}[ShardsReady]: {total_shards} {shards} ready!");
@ -21,7 +21,7 @@ impl EventProcessor<'_> {
pub async fn on_shards_stageupdate(
&self,
event: &ShardStageUpdateEvent
) -> Result<(), RustbotError> {
) -> RustbotResult<()> {
println!("{RUSTBOT_EVENT}[ShardStageUpdate:S{}]: {event:#?}", event.shard_id);
Ok(())

View File

@ -1,19 +1,10 @@
pub mod events;
// use serde_json::json;
use rustbot_lib::{
RustbotData,
RustbotError
};
use poise::{
FrameworkContext,
/* serenity_prelude::{
/* use poise::serenity_prelude::{
Context,
WebhookId
} */
};
type PoiseFwCtx<'a> = FrameworkContext<'a, RustbotData, RustbotError>;
}; */
/* async fn hook_logger(
ctx: &Context,

View File

@ -1,7 +1,7 @@
use std::sync::LazyLock;
pub struct ConfigMeta {
pub env: String,
pub env: &'static str,
pub embed_color: u32,
pub rustbot_logs: u64,
pub developers: Vec<u64>
@ -13,16 +13,16 @@ pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(ConfigMeta::n
#[cfg(not(feature = "production"))]
pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(||
ConfigMeta::new()
.env(String::from("dev"))
.env("dev")
.embed_color(0xf1d63c)
);
impl ConfigMeta {
fn new() -> Self {
Self {
env: String::from("prod"),
env: "prod",
embed_color: 0xf1d63c,
rustbot_logs: 1276874302314254448,
rustbot_logs: 1311282815601741844,
developers: vec![
190407856527376384 // toast.ts
]
@ -31,7 +31,7 @@ impl ConfigMeta {
// Scalable functions below;
#[cfg(not(feature = "production"))]
fn env(mut self, env: String) -> Self {
fn env(mut self, env: &'static str) -> Self {
self.env = env;
self
}

View File

@ -3,5 +3,7 @@ mod data;
pub use data::RustbotData;
pub mod utils;
pub type RustbotError = Box<dyn std::error::Error + Send + Sync>;
pub type RustbotCtx<'a> = poise::Context<'a, RustbotData, RustbotError>;
type RustbotError = Box<dyn std::error::Error + Send + Sync>;
pub type RustbotContext<'a> = poise::Context<'a, RustbotData, RustbotError>;
pub type RustbotFwCtx<'a> = poise::FrameworkContext<'a, RustbotData, RustbotError>;
pub type RustbotResult<T> = Result<T, RustbotError>;

View File

@ -21,7 +21,7 @@ pub fn format_timestamp(timestamp: i64) -> String {
format!("<t:{timestamp}>\n<t:{timestamp}:R>")
}
pub fn mention_dev(ctx: super::RustbotCtx<'_>) -> Option<String> {
pub fn mention_dev(ctx: super::RustbotContext<'_>) -> Option<String> {
let devs = super::config::BINARY_PROPERTIES.developers.clone();
let app_owners = ctx.framework().options().owners.clone();
@ -40,7 +40,7 @@ pub fn mention_dev(ctx: super::RustbotCtx<'_>) -> Option<String> {
}
}
pub fn get_guild_name(ctx: super::RustbotCtx<'_>) -> String {
pub fn get_guild_name(ctx: super::RustbotContext<'_>) -> String {
match ctx.guild() {
Some(guild) => guild.name.clone().to_string(),
None => String::from("DM")
@ -63,7 +63,7 @@ pub fn format_duration(secs: u64) -> String {
let formatted_string: Vec<String> = components
.iter()
.filter(|&&(value, _)| value > 0)
.map(|&(value, suffix)| format!("{}{}", value, suffix))
.map(|&(value, suffix)| format!("{value}{suffix}"))
.collect();
formatted_string.join(", ")

View File

@ -8,8 +8,6 @@ pub use eightball::eightball;
pub use ping::ping;
pub use uptime::uptime;
type PoiseContext<'a> = rustbot_lib::RustbotCtx<'a>;
macro_rules! collect {
() => {
vec![

View File

@ -1,53 +1,82 @@
use crate::RustbotError;
use super::PoiseContext;
use rustbot_lib::{
RustbotContext,
RustbotResult
};
use poise::{
CreateReply,
serenity_prelude::ChannelId
serenity_prelude::{
ChannelId,
ShardId,
ShardRunnerInfo
}
};
async fn format_shard_info(
id: &ShardId,
runner: &ShardRunnerInfo,
ctx: &RustbotContext<'_>
) -> String {
let mut string = String::new();
let heartbeat = match runner.latency {
Some(lat) => format!("`{}ms`", lat.as_millis()),
None => "Waiting for heartbeat...".to_string()
};
let status = runner.stage.to_string();
let shard_count = ctx.cache().shard_count();
let guild_count = ctx.cache().guilds().into_iter().filter(|g| g.shard_id(shard_count) == id.0).count() as u64;
string.push_str(&format!("**Shard {id}**\n"));
string.push_str(&format!("> Heartbeat: {heartbeat}\n"));
string.push_str(&format!("> Status: `{status}`\n"));
string.push_str(&format!("> Guilds: **{guild_count}**"));
string
}
/// Developer commands
#[poise::command(
slash_command,
prefix_command,
slash_command,
owners_only,
install_context = "Guild|User",
interaction_context = "Guild|BotDm|PrivateChannel",
subcommands("deploy", "servers", "shards", "echo")
)]
pub async fn dev(_: PoiseContext<'_>) -> Result<(), RustbotError> {
pub async fn dev(_: RustbotContext<'_>) -> RustbotResult<()> {
Ok(())
}
/// Deploy commands to this guild or globally
#[poise::command(prefix_command)]
async fn deploy(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
async fn deploy(ctx: RustbotContext<'_>) -> RustbotResult<()> {
poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(())
}
/// View how many servers the bot is in
#[poise::command(prefix_command)]
async fn servers(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
#[poise::command(slash_command)]
async fn servers(ctx: RustbotContext<'_>) -> RustbotResult<()> {
poise::builtins::servers(ctx).await?;
Ok(())
}
/// View the status of available shards
#[poise::command(prefix_command)]
async fn shards(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
#[poise::command(slash_command)]
async fn shards(ctx: RustbotContext<'_>) -> RustbotResult<()> {
let shard_runners = ctx.framework().shard_manager().runners.clone();
let runners = shard_runners.lock().await;
if runners.is_empty() {
ctx.reply("`ShardsReady` event hasn't fired yet!").await?;
return Ok(())
}
let mut shard_info = Vec::new();
for (id, runner) in runners.iter() {
shard_info.push(format!(
"**Shard {}**\n> Heartbeat: {}\n> Status: `{}`",
id,
match runner.latency {
Some(lat) => format!("`{}ms`", lat.as_millis()),
None => "Waiting for heartbeat...".to_string()
},
runner.stage
))
let info = format_shard_info(id, runner, &ctx).await;
shard_info.push(info);
}
ctx.reply(shard_info.join("\n\n")).await?;
@ -58,11 +87,11 @@ async fn shards(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
/// Turn your message into a bot message
#[poise::command(slash_command)]
async fn echo(
ctx: super::PoiseContext<'_>,
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>
) -> Result<(), RustbotError> {
) -> RustbotResult<()> {
ctx.defer_ephemeral().await?;
let channel = match channel {

View File

@ -1,22 +1,32 @@
use crate::RustbotError;
use super::PoiseContext;
use rustbot_lib::config::BINARY_PROPERTIES;
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: PoiseContext<'_>,
#[description = "Your yes/no question"] question: String
) -> Result<(), RustbotError> {
if question.to_ascii_lowercase().contains("rustbot, show list") {
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();
@ -29,7 +39,7 @@ pub async fn eightball(
}
}
if question.to_ascii_lowercase().contains("rustbot, show chicken list") {
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();
@ -42,10 +52,9 @@ pub async fn eightball(
}
}
let rand_resp = if question.to_ascii_lowercase().contains("chicken") {
get_random_chicken_response()
} else {
get_random_response()
let rand_resp = match mode {
Some(ResponseMode::Chicken) => get_random_chicken_response(),
_ => get_random_response()
};
ctx.reply(format!("> {question}\n{rand_resp}")).await?;
@ -53,7 +62,7 @@ pub async fn eightball(
Ok(())
}
const RESPONSES: [&str; 30] = [
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
@ -88,9 +97,24 @@ const RESPONSES: [&str; 30] = [
"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; 35] = [
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
@ -126,6 +150,25 @@ const CHICKEN_RESPONSES: [&str; 35] = [
"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 {

View File

@ -1,7 +1,8 @@
use crate::RustbotError;
use super::PoiseContext;
use serde::Deserialize;
use rustbot_lib::{
RustbotContext,
RustbotResult
};
#[derive(Deserialize)]
struct StatusPage {
@ -18,9 +19,13 @@ struct Summary {
mean: f64
}
/// Check latency of the bot's WS connection and Discord's API
#[poise::command(slash_command)]
pub async fn ping(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
/// 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"
)]
pub async fn ping(ctx: RustbotContext<'_>) -> RustbotResult<()> {
let statuspage: StatusPage = reqwest::get("https://discordstatus.com/metrics-display/5k2rt9f7pmny/day.json")
.await.unwrap()
.json()
@ -29,6 +34,7 @@ pub async fn ping(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
let mut latencies = Vec::new();
latencies.push(format!("Discord: `{:.0?}ms`", statuspage.metrics[0].summary.mean));
latencies.push(format!("WebSocket: `{:.0?}`", ctx.ping().await));
latencies.push(format!("Shard ID: `{}`", ctx.serenity_context().shard_id));
ctx.reply(latencies.join("\n")).await?;

View File

@ -1,6 +1,3 @@
use crate::RustbotError;
use super::PoiseContext;
use sysinfo::System;
use uptime_lib::get;
use std::{
@ -17,11 +14,15 @@ use std::{
BufReader
}
};
use rustbot_lib::utils::{
use rustbot_lib::{
RustbotContext,
RustbotResult,
utils::{
BOT_VERSION,
GIT_COMMIT_HASH,
GIT_COMMIT_BRANCH,
format_duration
}
};
fn get_os_info() -> String {
@ -63,7 +64,7 @@ fn fmt_mem(bytes: u64) -> String {
/// Retrieve host and bot uptimes
#[poise::command(slash_command)]
pub async fn uptime(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
pub async fn uptime(ctx: RustbotContext<'_>) -> RustbotResult<()> {
let bot = ctx.http().get_current_user().await.unwrap();
let mut sys = System::new_all();
sys.refresh_all();

View File

@ -1,8 +1,9 @@
mod commands;
mod shutdown;
// https://cdn.toast-server.net/RustFSHiearchy.png
// Using the new filesystem hierarchy
use rustbot_tokens::token_path;
use rustbot_tokens::discord_token;
use poise::serenity_prelude::{
builder::CreateAllowedMentions,
ClientBuilder,
@ -14,7 +15,6 @@ use rustbot_lib::{
mention_dev,
get_guild_name
},
RustbotError,
RustbotData,
config::BINARY_PROPERTIES
};
@ -47,20 +47,19 @@ async fn main() {
};
println!(
"Discord[{}:S{}]: {} ran {}{} {}",
"Discord[{}:S{}]: {} ran {prefix}{} {get_guild_channel_name}",
get_guild_name(ctx),
ctx.serenity_context().shard_id,
ctx.author().name,
prefix,
ctx.command().qualified_name,
get_guild_channel_name);
);
}),
prefix_options: poise::PrefixFrameworkOptions {
prefix,
case_insensitive_commands: true,
ignore_bots: true,
execute_self_messages: false,
mention_as_prefix: false,
case_insensitive_commands: true,
execute_self_messages: false,
..Default::default()
},
on_error: |error| Box::pin(async move {
@ -118,7 +117,7 @@ async fn main() {
.build();
let mut client = ClientBuilder::new(
&token_path().await.main,
discord_token().await,
GatewayIntents::GUILDS
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::MESSAGE_CONTENT
@ -128,7 +127,14 @@ async fn main() {
.activity(ActivityData::custom("nep nep!"))
.await.expect("Error creating client");
let shard_manager = client.shard_manager.clone();
tokio::spawn(async move {
shutdown::gracefully_shutdown().await;
shard_manager.shutdown_all().await;
});
if let Err(why) = client.start_autosharded().await {
println!("Error starting client: {:#?}", why);
println!("Error starting client: {why:#?}");
}
}

23
src/shutdown.rs Normal file
View File

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

View File

@ -6,6 +6,6 @@ Only things that are needing to be changed before deploying this template to rea
- library (rustbot_lib), and its references
- tsclient (rustbot_tokens), and its references
Search by Rustbot in its usual form (all-caps, pascalcase, and such) to find all the references that need to be changed too.
Search by Rustbot in its usual form (ALL-CAPS, PascalCase, and such) to find all the references that need to be changed too.
(and delete this file too!)

View File

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

View File

@ -1,5 +1,9 @@
use std::sync::LazyLock;
use poise::serenity_prelude::Token;
use tokio::sync::Mutex;
use std::{
str::FromStr,
sync::LazyLock
};
use tokenservice_client::{
TokenService,
TokenServiceApi
@ -33,3 +37,8 @@ static TSCLIENT: LazyLock<Mutex<TSClient>> = LazyLock::new(|| Mutex::new(TSClien
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!")
}