Update template with restructured project

This commit is contained in:
toast 2024-10-21 17:36:18 +11:00
parent f02775f9a0
commit 85c93f6f03
44 changed files with 1154 additions and 1478 deletions

View File

@ -1,8 +1,9 @@
.vscode
target
.env
.gitignore
docker-compose.yml
Dockerfile
renovate.json
run.sh
stuff-to-change.log

1
.gitignore vendored
View File

@ -1,2 +1 @@
target
.env

940
Cargo.lock generated

File diff suppressed because it is too large Load Diff

40
Cargo.toml Normal file → Executable file
View File

@ -3,21 +3,41 @@ name = "rustbot"
version = "0.1.0"
edition = "2021"
[dependencies]
bb8 = "0.8.5"
bb8-postgres = "0.8.1"
cargo_toml = "0.20.4"
once_cell = "1.19.0"
[workspace]
members = [
"events",
"jobs",
"library",
"tsclient"
]
[workspace.dependencies]
cargo_toml = "0.20.5"
poise = "0.6.1"
regex = "1.10.6"
regex = "1.11.0"
serde = "1.0.210"
tokio = { version = "1.40.0", features = ["macros", "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.32.0"
tokenservice-client = { version = "0.4.0", registry = "gitea" }
tokio = { version = "1.39.2", features = ["macros", "signal", "rt-multi-thread"] }
tokio-postgres = "0.7.11"
time = "0.3.36"
tokio = { workspace = true }
uptime_lib = "0.3.1"
[patch.crates-io]
poise = { git = "https://github.com/serenity-rs/poise", branch = "serenity-next" }
[features]
production = []
production = ["rustbot_lib/production", "rustbot_events/production"]
not_ready = ["rustbot_lib/not_ready"]
[[bin]]
name = "rustbot"

View File

@ -1,24 +1,10 @@
FROM rust:1.82-alpine3.20@sha256:466dc9924d265455aa73e72fd9cdac9db69ce6a988e6f0e6baf852db3485d97d AS chef
ENV RUSTFLAGS="-C target-feature=-crt-static"
ARG GIT_HASH
ENV GIT_COMMIT_HASH=${GIT_HASH}
RUN apk add --no-cache openssl-dev musl-dev
RUN cargo install cargo-chef
FROM scratch AS base
WORKDIR /builder
FROM chef AS planner
COPY . .
RUN cargo chef prepare
FROM chef AS builder
COPY --from=planner /builder/recipe.json recipe.json
RUN cargo chef cook --release
COPY . .
RUN cargo build --offline -rF production
FROM alpine:3.20@sha256:e72ad0747b9dc266fca31fb004580d316b6ae5b0fdbbb65f17bbe371a5b24cff
LABEL org.opencontainers.image.source="https://git.toast-server.net/toast/Rustbot"
RUN apk add --no-cache libgcc
WORKDIR /rustbot
COPY --from=builder /builder/target/release/rustbot .
CMD ./rustbot
COPY --from=base /builder/target/x86_64-unknown-linux-musl/release/rustbot .
CMD [ "./rustbot" ]

5
build.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
export GIT_COMMIT_HASH=$(git rev-parse HEAD) && \
cargo zigbuild --target x86_64-unknown-linux-musl --locked -rF production && \
docker compose build bot

View File

@ -1,23 +1,6 @@
version: '3.8'
services:
bot:
container_name: rustbot
#image: 'git.toast-server.net/toast/rustbot:main'
build: .
env_file:
- .env
restart: unless-stopped
depends_on:
- db
db:
container_name: rustbot-database
image: postgres:17.0-alpine3.20@sha256:14195b0729fce792f47ae3c3704d6fd04305826d57af3b01d5b4d004667df174
restart: unless-stopped
ports:
- 37931:5432/tcp
volumes:
- /var/lib/docker/volumes/rustbot-database:/var/lib/postgresql/data:rw
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}

12
events/Cargo.toml Executable file
View File

@ -0,0 +1,12 @@
[package]
name = "rustbot_events"
version = "0.1.0"
edition = "2021"
[dependencies]
rustbot_lib = { path = "../library" }
poise = { workspace = true }
[features]
production = ["rustbot_lib/production"]
not_ready = ["rustbot_lib/not_ready"]

33
events/src/events.rs Executable file
View File

@ -0,0 +1,33 @@
mod ready;
mod shards;
use rustbot_lib::{
RustbotError,
RustbotData
};
use poise::{
FrameworkContext,
serenity_prelude::FullEvent
};
pub const RUSTBOT_EVENT: &str = "RustbotEvent";
struct EventProcessor<'a> {
framework: FrameworkContext<'a, RustbotData, RustbotError>
}
pub async fn processor(
framework: FrameworkContext<'_, RustbotData, RustbotError>,
event: &FullEvent
) -> Result<(), RustbotError> {
let processor = EventProcessor { framework };
match event {
FullEvent::Ready { data_about_bot } => processor.on_ready(data_about_bot).await?,
FullEvent::ShardsReady { total_shards } => processor.on_shards_ready(total_shards).await?,
FullEvent::ShardStageUpdate { event } => processor.on_shards_stageupdate(event).await?,
_ => {}
}
Ok(())
}

67
events/src/events/ready.rs Executable file
View File

@ -0,0 +1,67 @@
use crate::PoiseFwCtx;
use super::{
EventProcessor,
RUSTBOT_EVENT
};
use rustbot_lib::{
RustbotError,
utils::{
BOT_VERSION,
GIT_COMMIT_HASH,
GIT_COMMIT_BRANCH
},
config::BINARY_PROPERTIES
};
use std::sync::atomic::{
AtomicBool,
Ordering
};
use poise::serenity_prelude::{
Ready,
ChannelId,
CreateMessage,
CreateEmbed,
CreateEmbedAuthor
};
static READY_ONCE: AtomicBool = AtomicBool::new(false);
async fn ready_once(
ready: &Ready,
framework: PoiseFwCtx<'_>
) -> Result<(), RustbotError> {
#[cfg(not(feature = "production"))]
{
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: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);
let message = CreateMessage::new();
let ready_embed = CreateEmbed::new()
.color(BINARY_PROPERTIES.embed_color)
.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?;
Ok(())
}
impl EventProcessor<'_> {
pub async fn on_ready(
&self,
data_about_bot: &Ready
) -> Result<(), RustbotError> {
if !READY_ONCE.swap(true, Ordering::Relaxed) {
ready_once(data_about_bot, self.framework).await.expect("Failed to call ready_once method");
}
Ok(())
}
}

29
events/src/events/shards.rs Executable file
View File

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

45
events/src/lib.rs Executable file
View File

@ -0,0 +1,45 @@
pub mod events;
// use serde_json::json;
use rustbot_lib::{
RustbotData,
RustbotError
};
use poise::{
FrameworkContext,
/* serenity_prelude::{
Context,
WebhookId
} */
};
type PoiseFwCtx<'a> = FrameworkContext<'a, RustbotData, RustbotError>;
/* async fn hook_logger(
ctx: &Context,
hook_id: WebhookId,
token: &str,
content: String
) -> Result<bool, rustbot_lib::RustbotError> {
let current_app = ctx.http.get_current_user().await.unwrap();
let bot_avatar = current_app.avatar_url().unwrap();
let bot_username = &current_app.name;
if let Err(e) = ctx.http.execute_webhook(
hook_id,
None,
token,
true,
vec![],
&json!({
"content": content,
"avatar_url": bot_avatar,
"username": bot_username
})
).await {
println!("{}[EventWebhook]: Failed to send webhook message: {e}", events::RUSTBOT_EVENT);
Ok(false)
} else {
Ok(true)
}
} */

7
jobs/Cargo.toml Executable file
View File

@ -0,0 +1,7 @@
[package]
name = "rustbot_jobs"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { workspace = true }

3
jobs/src/lib.rs Executable file
View File

@ -0,0 +1,3 @@
pub mod tasks;
const RUSTBOT_SCHEDULER: &str = "RustbotScheduler";

43
jobs/src/tasks.rs Executable file
View File

@ -0,0 +1,43 @@
use crate::RUSTBOT_SCHEDULER;
use 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 async fn spawn_job<F, E>(
&self,
interval_secs: u64,
job: Arc<dyn Fn() -> F + Send + Sync + 'static>
) where
F: Future<Output = Result<(), E>> + Send + 'static,
E: std::fmt::Debug
{
let mut interval = interval(Duration::from_secs(interval_secs));
loop {
interval.tick().await;
let job_clone = job.clone();
task::spawn(async move {
if let Err(y) = job_clone().await {
eprintln!("{RUSTBOT_SCHEDULER}[Job:Error]: {y:?}");
}
});
}
}
}

13
library/Cargo.toml Executable file
View File

@ -0,0 +1,13 @@
[package]
name = "rustbot_lib"
version = "0.1.19"
edition = "2021"
[dependencies]
cargo_toml = { workspace = true }
poise = { workspace = true }
[features]
production = ["docker"]
docker = []
not_ready = []

37
library/build.rs Executable file
View File

@ -0,0 +1,37 @@
fn main() {
#[cfg(feature = "production")]
{
if let Ok(git_commit_hash) = std::env::var("GIT_COMMIT_HASH") {
println!("cargo:rustc-env=GIT_COMMIT_HASH={}", &git_commit_hash[..7]);
} else {
println!("cargo:warning=GIT_COMMIT_HASH not found, falling back to 'not_found'");
println!("cargo:rustc-env=GIT_COMMIT_HASH=not_found");
}
}
{
let git_branch = std::process::Command::new("git")
.args(["rev-parse", "--abbrev-ref", "HEAD"])
.output()
.expect("Command execution failed: git");
if git_branch.status.success() {
let git_branch = String::from_utf8(git_branch.stdout).expect("Invalid UTF-8 sequence").trim().to_string();
println!("cargo:rustc-env=GIT_COMMIT_BRANCH={}", &git_branch);
} else {
println!("cargo:warning=GIT_COMMIT_BRANCH not found, falling back to 'not_found'");
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);
}
}
}

44
library/src/config.rs Executable file
View File

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

2
library/src/data.rs Executable file
View File

@ -0,0 +1,2 @@
#[derive(Debug)]
pub struct RustbotData {}

7
library/src/lib.rs Executable file
View File

@ -0,0 +1,7 @@
pub mod config;
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>;

70
library/src/utils.rs Executable file
View File

@ -0,0 +1,70 @@
use poise::serenity_prelude::UserId;
use cargo_toml::Manifest;
use std::sync::LazyLock;
#[cfg(feature = "production")]
pub static GIT_COMMIT_HASH: &str = env!("GIT_COMMIT_HASH");
#[cfg(not(feature = "production"))]
pub static GIT_COMMIT_HASH: &str = "devel";
pub static GIT_COMMIT_BRANCH: &str = env!("GIT_COMMIT_BRANCH");
pub static BOT_VERSION: LazyLock<String> = LazyLock::new(|| {
Manifest::from_str(include_str!("../../Cargo.toml"))
.unwrap()
.package
.unwrap()
.version
.unwrap()
});
pub fn format_timestamp(timestamp: i64) -> String {
format!("<t:{timestamp}>\n<t:{timestamp}:R>")
}
pub fn mention_dev(ctx: super::RustbotCtx<'_>) -> Option<String> {
let devs = super::config::BINARY_PROPERTIES.developers.clone();
let app_owners = ctx.framework().options().owners.clone();
let mut mentions = Vec::new();
for dev in devs {
if app_owners.contains(&UserId::new(dev)) {
mentions.push(format!("<@{dev}>"));
}
}
if mentions.is_empty() {
None
} else {
Some(mentions.join(", "))
}
}
pub fn get_guild_name(ctx: super::RustbotCtx<'_>) -> String {
match ctx.guild() {
Some(guild) => guild.name.clone().to_string(),
None => String::from("DM")
}
}
pub fn format_duration(secs: u64) -> String {
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
let mut formatted_string = String::new();
match (days, hours, minutes, seconds) {
(d, _, _, _) if d > 0 => formatted_string.push_str(&format!("{d}d, ")),
(_, h, _, _) if h > 0 => formatted_string.push_str(&format!("{h}h, ")),
(_, _, m, _) if m > 0 => formatted_string.push_str(&format!("{m}m, ")),
(_, _, _, s) if s > 0 => formatted_string.push_str(&format!("{s}s")),
_ => {}
}
if formatted_string.ends_with(", ") {
formatted_string.truncate(formatted_string.len() - 2);
}
formatted_string
}

3
run.sh
View File

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

32
src/commands.rs Normal file → Executable file
View File

@ -1,4 +1,28 @@
pub mod midi;
pub mod ping;
pub mod sample;
pub mod uptime;
mod dev;
mod eightball;
mod ping;
mod uptime;
pub use dev::dev;
pub use eightball::eightball;
pub use ping::ping;
pub use uptime::uptime;
type PoiseContext<'a> = rustbot_lib::RustbotCtx<'a>;
macro_rules! collect {
() => {
vec![
// Developer command(s)
commands::dev(),
// Utility commands
commands::ping(),
commands::uptime(),
// Unsorted mess
commands::eightball(),
]
};
}
pub(crate) use collect;

50
src/commands/dev.rs Executable file
View File

@ -0,0 +1,50 @@
use crate::RustbotError;
use super::PoiseContext;
/// Developer commands
#[poise::command(
prefix_command,
owners_only,
subcommands("deploy", "servers", "shards")
)]
pub async fn dev(_: PoiseContext<'_>) -> Result<(), RustbotError> {
Ok(())
}
/// Deploy commands to this guild or globally
#[poise::command(prefix_command)]
async fn deploy(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
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::builtins::servers(ctx).await?;
Ok(())
}
/// View the status of available shards
#[poise::command(prefix_command)]
async fn shards(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
let shard_runners = ctx.framework().shard_manager().runners.clone();
let runners = shard_runners.lock().await;
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
))
}
ctx.reply(shard_info.join("\n\n")).await?;
Ok(())
}

80
src/commands/eightball.rs Executable file
View File

@ -0,0 +1,80 @@
use crate::RustbotError;
use super::PoiseContext;
use rustbot_lib::config::BINARY_PROPERTIES;
use poise::{
serenity_prelude::UserId,
builtins::paginate
};
/// Ask the Magic 8-Ball a yes/no question and get an unpredictable answer
#[poise::command(
slash_command,
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") {
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(());
}
}
ctx.reply(format!(
"> {}\n{}",
question,
get_random_response()
)).await?;
Ok(())
}
const RESPONSES: [&str; 30] = [
"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
];
fn get_random_response() -> &'static str {
RESPONSES[rand::random::<usize>() % RESPONSES.len()]
}

View File

@ -1,101 +0,0 @@
use crate::{
Error,
internals::utils::{
mention_dev,
format_bytes
}
};
use regex::Regex;
use std::{
os::unix::fs::MetadataExt,
fs::{
write,
remove_file,
metadata
}
};
use poise::{
CreateReply,
serenity_prelude::CreateAttachment
};
/// Convert MIDI file to WAV
#[poise::command(context_menu_command = "MIDI -> WAV")]
pub async fn midi_to_wav(
ctx: poise::Context<'_, (), Error>,
#[description = "MIDI file to be converted"] message: poise::serenity_prelude::Message
) -> Result<(), Error> {
let re = Regex::new(r"(?i)\.mid$").unwrap();
if !message.embeds.is_empty() || message.attachments.is_empty() || !re.is_match(&message.attachments[0].filename) {
ctx.reply("That ain't a MIDI file! What are you even doing??").await?;
return Ok(());
}
ctx.defer().await?;
let bytes = match message.attachments[0].download().await {
Ok(bytes) => bytes,
Err(y) => {
ctx.send(CreateReply::default()
.content(format!(
"Download failed, ask {} to check console for more information!",
mention_dev(ctx).unwrap_or_default()
))
)
.await.unwrap();
return Err(Error::from(format!("Failed to download the file: {}", y)))
}
};
let midi_path = &message.attachments[0].filename;
write(midi_path, bytes)?;
let wav_path = re.replace(&midi_path, ".wav");
let sf2_path = "/tmp/FluidR3_GM.sf2";
write(sf2_path, include_bytes!("../internals/assets/FluidR3_GM.sf2"))?;
let output = std::process::Command::new("fluidsynth")
.args(&["-ni", sf2_path, midi_path, "-F", &wav_path])
.output();
// Just to add an info to console to tell what the bot is doing when MIDI file is downloaded.
println!("Discord[{}:{}]: Processing MIDI file: \"{}\"", ctx.guild().unwrap().name, ctx.command().qualified_name, midi_path);
match output {
Ok(_) => {
let reply = ctx.send(CreateReply::default()
.attachment(CreateAttachment::path(&*wav_path).await.unwrap())
).await;
if reply.is_err() {
println!(
"Discord[{}:{}]: Processed file couldn't be uploaded back to Discord channel due to upload limit",
ctx.guild().unwrap().name, ctx.command().qualified_name
);
ctx.send(CreateReply::default()
.content(format!(
"Couldn't upload the processed file (`{}`, `{}`) due to upload limit",
&*wav_path, format_bytes(metadata(&*wav_path).unwrap().size())
))
).await.unwrap();
} else if reply.is_ok() {
remove_file(midi_path)?;
remove_file(&*wav_path)?;
}
},
Err(y) => {
ctx.send(CreateReply::default()
.content("Command didn't execute successfully, check console for more information!")
).await.unwrap();
return Err(Error::from(format!("Midi conversion failed: {}", y)))
}
}
Ok(())
}

36
src/commands/ping.rs Normal file → Executable file
View File

@ -1,8 +1,36 @@
use crate::Error;
use crate::RustbotError;
use super::PoiseContext;
/// Check if the bot is alive
use serde::Deserialize;
#[derive(Deserialize)]
struct StatusPage {
metrics: Vec<Metrics>
}
#[derive(Deserialize)]
struct Metrics {
summary: Summary
}
#[derive(Deserialize)]
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: poise::Context<'_, (), Error>) -> Result<(), Error> {
ctx.reply(format!("Powong! `{:?}`", ctx.ping().await)).await?;
pub async fn ping(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
let statuspage: StatusPage = reqwest::get("https://discordstatus.com/metrics-display/5k2rt9f7pmny/day.json")
.await.unwrap()
.json()
.await.unwrap();
let mut latencies = Vec::new();
latencies.push(format!("Discord: `{:.0?}ms`", statuspage.metrics[0].summary.mean));
latencies.push(format!("WebSocket: `{:.0?}`", ctx.ping().await));
ctx.reply(latencies.join("\n")).await?;
Ok(())
}

View File

@ -1,83 +0,0 @@
use crate::{
Error,
models::sample::SampleData
};
use poise::CreateReply;
/// Perform sample CRUD operations in database
#[poise::command(
slash_command,
subcommands("list", "create", "update", "delete"),
subcommand_required
)]
pub async fn sample(_: poise::Context<'_, (), Error>) -> Result<(), Error> {
Ok(())
}
/// List sample data
#[poise::command(slash_command)]
pub async fn list(
ctx: poise::Context<'_, (), Error>,
#[description = "ID of the sample data"] id: u64
) -> Result<(), Error> {
let samples = SampleData::list_data(id).await?;
let mut response = String::new();
for sample in samples {
response.push_str(&format!("ID: {}\n", sample.id));
response.push_str(&format!("Text: {}\n", sample.text_val));
response.push_str(&format!("Int: {}\n", sample.int_val));
response.push_str(&format!("Boolean: {}\n\n", sample.boolean_val));
}
ctx.send(CreateReply::default()
.content(response)
).await?;
Ok(())
}
/// Create sample data
#[poise::command(slash_command)]
pub async fn create(
ctx: poise::Context<'_, (), Error>,
#[description = "Text value"] text: String,
#[description = "Int value"] int: u64,
#[description = "Boolean value"] boolean: bool
) -> Result<(), Error> {
SampleData::create_data(text, int as i64, boolean).await?;
ctx.send(CreateReply::default().content("Done!")).await?;
Ok(())
}
/// Update sample data
#[poise::command(slash_command)]
pub async fn update(
ctx: poise::Context<'_, (), Error>,
#[description = "ID of the sample data"] id: u64,
#[description = "Text value"] text: String,
#[description = "Int value"] int: u64,
#[description = "Boolean value"] boolean: bool
) -> Result<(), Error> {
SampleData::update_data(id, text, int as i64, boolean).await?;
ctx.send(CreateReply::default().content("Done!")).await?;
Ok(())
}
/// Delete sample data
#[poise::command(slash_command)]
pub async fn delete(
ctx: poise::Context<'_, (), Error>,
#[description = "ID of the sample data"] id: u64
) -> Result<(), Error> {
SampleData::delete_data(id).await?;
ctx.send(CreateReply::default().content("Done!")).await?;
Ok(())
}

88
src/commands/uptime.rs Normal file → Executable file
View File

@ -1,16 +1,10 @@
use crate::{
Error,
GIT_COMMIT_HASH,
internals::utils::{
BOT_VERSION,
format_duration,
concat_message
}
};
use crate::RustbotError;
use super::PoiseContext;
use sysinfo::System;
use uptime_lib::get;
use std::{
env::var,
fs::File,
path::Path,
time::{
@ -23,34 +17,54 @@ use std::{
BufReader
}
};
use rustbot_lib::utils::{
BOT_VERSION,
GIT_COMMIT_HASH,
GIT_COMMIT_BRANCH,
format_duration
};
fn get_os_info() -> String {
let path = Path::new("/etc/os-release");
let mut name = "BoringOS".to_string();
let mut version = "v0.0".to_string();
if let Ok(file) = File::open(&path) {
if let Ok(file) = File::open(path) {
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(line) = line {
if line.starts_with("NAME=") {
name = line.split('=').nth(1).unwrap_or_default().trim_matches('"').to_string();
} else if line.starts_with("VERSION=") {
version = line.split('=').nth(1).unwrap_or_default().trim_matches('"').to_string();
} else if line.starts_with("VERSION_ID=") {
version = line.split('=').nth(1).unwrap_or_default().trim_matches('"').to_string();
}
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 {
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),
_ => {}
}
}
});
}
format!("{} {}", name, version)
format!("{name} {version}")
}
fn fmt_mem(bytes: u64) -> String {
let units = ["B", "KB", "MB", "GB"];
let mut bytes = bytes as f64;
let mut unit = units[0];
for &u in &units {
if bytes < 1024.0 {
unit = u;
break;
}
bytes /= 1024.0;
}
format!("{bytes:.2} {unit}")
}
/// Retrieve host and bot uptimes
#[poise::command(slash_command)]
pub async fn uptime(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
let _bot = ctx.http().get_current_user().await.unwrap();
pub async fn uptime(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
let bot = ctx.http().get_current_user().await.unwrap();
let mut sys = System::new_all();
sys.refresh_all();
@ -60,7 +74,17 @@ pub async fn uptime(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
// Fetch system's processor
let cpu = sys.cpus();
// Fetch bot's process uptime
// Fetch system memory usage
let sram = fmt_mem(sys.used_memory());
let sram_total = fmt_mem(sys.total_memory());
// Fetch process memory usage
let pram = match sys.process(sysinfo::get_current_pid().unwrap()) {
Some(proc) => fmt_mem(proc.memory()),
None => String::from("Unavailable")
};
// Fetch process uptime
let curr_pid = sysinfo::get_current_pid().unwrap();
let now = SystemTime::now();
let mut proc_uptime = 0;
@ -69,14 +93,22 @@ pub async fn uptime(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
proc_uptime = now.duration_since(time_started).unwrap().as_secs();
}
let stat_msg = vec![
format!("**{} {}** `{}`", _bot.name, BOT_VERSION.as_str(), GIT_COMMIT_HASH),
// Fetch the node hostname from envvar
let docker_node = match var("DOCKER_HOSTNAME") {
Ok(h) => h.to_string(),
Err(_) => "DOCKER_HOSTNAME is empty!".to_string()
};
let stat_msg = [
format!("**{} v{}** `{}:{}`", bot.name, BOT_VERSION.as_str(), GIT_COMMIT_HASH, GIT_COMMIT_BRANCH),
format!(">>> System: `{}`", format_duration(sys_uptime)),
format!("Process: `{}`", format_duration(proc_uptime)),
format!("CPU: `{}`", format!("{}", cpu[0].brand())),
format!("Node: `{docker_node}`"),
format!("CPU: `{}`", cpu[0].brand()),
format!("RAM: `{pram}` (`{sram}/{sram_total}`)"),
format!("OS: `{}`", get_os_info())
];
ctx.reply(concat_message(stat_msg)).await?;
ctx.reply(stat_msg.join("\n")).await?;
Ok(())
}

View File

@ -1 +0,0 @@
pub mod database;

View File

@ -1,84 +0,0 @@
use crate::internals::utils::token_path;
use bb8::{Pool, PooledConnection};
use bb8_postgres::PostgresConnectionManager;
use tokio::time::{
Duration,
sleep
};
use tokio_postgres::{
Client,
NoTls,
Error,
config::Config
};
use std::{
ops::Deref,
str::FromStr,
sync::{
Mutex,
LazyLock
}
};
pub static DATABASE: LazyLock<Mutex<Option<DatabaseController>>> = LazyLock::new(|| Mutex::new(None));
pub struct DatabaseController {
pub pool: Pool<PostgresConnectionManager<NoTls>>
}
impl DatabaseController {
pub async fn new() -> Result<(), Error> {
let manager = PostgresConnectionManager::new(Config::from_str(token_path().await.postgres_uri.as_str())?, NoTls);
let pool = bb8::Pool::builder().build(manager).await?;
let err_name = "Postgres[Error]";
let pool_clone = pool.clone();
tokio::spawn(async move {
loop {
match Self::attempt_connect(&pool_clone).await {
Ok(conn) => {
println!("Postgres[Info]: Successfully connected");
let client: &Client = conn.deref();
// Sample model
client.batch_execute("
CREATE TABLE IF NOT EXISTS sample (
id BIGSERIAL PRIMARY KEY,
text_val VARCHAR(255) NOT NULL,
int_val BIGINT NOT NULL,
boolean_val BOOLEAN NOT NULL
);
").await.unwrap();
},
Err(e) => {
eprintln!("{}: {}", err_name, e);
sleep(Duration::from_secs(5)).await;
}
}
break;
}
});
let controller = Self { pool };
*DATABASE.lock().unwrap() = Some(controller);
Ok(())
}
async fn attempt_connect<'a>(pool: &'a bb8::Pool<PostgresConnectionManager<NoTls>>) -> Result<PooledConnection<'a, PostgresConnectionManager<NoTls>>, bb8::RunError<Error>> {
let mut backoff = 1;
loop {
match pool.get().await {
Ok(conn) => return Ok(conn),
Err(e) => {
eprintln!("Postgres[ConnError]: {}, retrying in {} seconds", e, backoff);
sleep(Duration::from_secs(backoff)).await;
if backoff < 64 {
backoff *= 2;
}
}
}
}
}
}

View File

@ -1,4 +0,0 @@
pub mod config;
pub mod tasks;
pub mod tsclient;
pub mod utils;

Binary file not shown.

View File

@ -1,62 +0,0 @@
use std::sync::LazyLock;
pub struct ConfigMeta {
pub embed_color: i32,
pub ready_notify: u64,
pub rss_channel: u64,
pub rustbot_logs: u64,
pub deploy_commands: bool,
pub developers: Vec<u64>
}
#[cfg(feature = "production")]
pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(|| ConfigMeta::new());
#[cfg(not(feature = "production"))]
pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(||
ConfigMeta::new()
.embed_color(0xf1d63c)
.ready_notify(865673694184996888)
.rss_channel(865673694184996888)
.deploy_commands(false)
);
impl ConfigMeta {
fn new() -> Self {
Self {
embed_color: 0x5a99c7,
ready_notify: 865673694184996888,
rss_channel: 865673694184996888,
rustbot_logs: 1268493237912604672,
deploy_commands: false,
developers: vec![
190407856527376384 // toast.ts
]
}
}
// Scalable functions below;
#[cfg(not(feature = "production"))]
fn embed_color(mut self, color: i32) -> Self {
self.embed_color = color;
self
}
#[cfg(not(feature = "production"))]
fn ready_notify(mut self, channel_id: u64) -> Self {
self.ready_notify = channel_id;
self
}
#[cfg(not(feature = "production"))]
fn rss_channel(mut self, channel_id: u64) -> Self {
self.rss_channel = channel_id;
self
}
#[cfg(not(feature = "production"))]
fn deploy_commands(mut self, deploy: bool) -> Self {
self.deploy_commands = deploy;
self
}
}

View File

@ -1,9 +0,0 @@
pub mod sample;
fn task_info(name: &str, message: &str) {
println!("{}", format!("TaskScheduler[{}]: {}", name, message));
}
fn task_err(name: &str, message: &str) {
eprintln!("{}", format!("TaskScheduler[{}:Error]: {}", name, message));
}

View File

@ -1,43 +0,0 @@
use crate::Error;
use super::{
super::config::BINARY_PROPERTIES,
task_info,
task_err
};
use std::sync::Arc;
use poise::serenity_prelude::{
Context,
ChannelId,
CreateMessage
};
use tokio::time::{
Duration,
interval
};
pub async fn sample(ctx: Arc<Context>) -> Result<(), Error> {
let task_name = "SampleTask";
let mut interval = interval(Duration::from_secs(10));
task_info(&task_name, "Task loaded!");
loop {
interval.tick().await;
task_info(&task_name, "Task running!");
if BINARY_PROPERTIES.rss_channel == 0 {
task_err(&task_name, "RSS channel ID is not set!");
ChannelId::new(BINARY_PROPERTIES.rustbot_logs).send_message(
&ctx.http,
CreateMessage::new().content("RSS channel ID is not set!")
).await.unwrap();
continue;
}
ChannelId::new(BINARY_PROPERTIES.rss_channel).send_message(
&ctx.http,
CreateMessage::new().content("This is a sample message executed by a task!")
).await.unwrap();
}
}

View File

@ -1,21 +0,0 @@
use tokenservice_client::{
TokenService,
TokenServiceApi
};
pub struct TSClient(TokenService);
impl TSClient {
pub fn new() -> Self {
let args: Vec<String> = std::env::args().collect();
let service = if args.len() > 1 { &args[1] } else { "pgbot" };
Self(TokenService::new(service))
}
pub async fn get(&self) -> Result<TokenServiceApi, crate::Error> {
match self.0.connect().await {
Ok(api) => Ok(api),
Err(e) => Err(e)
}
}
}

View File

@ -1,86 +0,0 @@
use poise::serenity_prelude::UserId;
use std::sync::LazyLock;
use tokio::sync::Mutex;
use tokenservice_client::TokenServiceApi;
use super::tsclient::TSClient;
pub static BOT_VERSION: LazyLock<String> = LazyLock::new(|| {
let cargo_version = cargo_toml::Manifest::from_str(&include_str!("../../Cargo.toml"))
.unwrap()
.package
.unwrap()
.version
.unwrap();
format!("v{}", cargo_version)
});
static TSCLIENT: LazyLock<Mutex<TSClient>> = LazyLock::new(|| Mutex::new(TSClient::new()));
pub async fn token_path() -> TokenServiceApi {
TSCLIENT.lock().await.get().await.unwrap()
}
pub fn concat_message(messages: Vec<String>) -> String {
messages.join("\n")
}
pub fn mention_dev(ctx: poise::Context<'_, (), crate::Error>) -> Option<String> {
let devs = super::config::BINARY_PROPERTIES.developers.clone();
let app_owners = ctx.framework().options().owners.clone();
let mut mentions = Vec::new();
for dev in devs {
if app_owners.contains(&UserId::new(dev)) {
mentions.push(format!("<@{}>", dev));
}
}
if mentions.is_empty() {
None
} else {
Some(mentions.join(", "))
}
}
pub fn format_duration(secs: u64) -> String {
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
let mut formatted_string = String::new();
if days > 0 {
formatted_string.push_str(&format!("{}d, ", days));
}
if hours > 0 || days > 0 {
formatted_string.push_str(&format!("{}h, ", hours));
}
if minutes > 0 || hours > 0 {
formatted_string.push_str(&format!("{}m, ", minutes));
}
formatted_string.push_str(&format!("{}s", seconds));
formatted_string
}
pub fn format_bytes(bytes: u64) -> String {
let units = ["B", "KB", "MB", "GB", "TB", "PB"];
let mut value = bytes as f64;
let mut unit = units[0];
for &u in &units[1..] {
if value < 1024.0 {
break;
}
value /= 1024.0;
unit = u;
}
if unit == "B" {
format!("{}{}", value, unit)
} else {
format!("{:.2}{}", value, unit)
}
}

260
src/main.rs Normal file → Executable file
View File

@ -1,187 +1,68 @@
mod commands;
mod controllers;
mod models;
mod internals;
// https://cdn.toast-server.net/RustFSHiearchy.png
// Using the new filesystem hierarchy
use crate::{
internals::{
utils::{
BOT_VERSION,
token_path,
mention_dev
},
config::BINARY_PROPERTIES
},
controllers::database::DatabaseController
};
use std::{
thread::current,
sync::{
Arc,
atomic::{
AtomicBool,
Ordering
}
}
};
use rustbot_tokens::token_path;
use poise::serenity_prelude::{
builder::{
CreateMessage,
CreateEmbed,
CreateEmbedAuthor
},
Ready,
Context,
FullEvent,
builder::CreateAllowedMentions,
ClientBuilder,
ChannelId,
Command,
ActivityData,
GatewayIntents
};
type Error = Box<dyn std::error::Error + Send + Sync>;
static TASK_RUNNING: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "production")]
pub static GIT_COMMIT_HASH: &str = env!("GIT_COMMIT_HASH");
#[cfg(not(feature = "production"))]
pub static GIT_COMMIT_HASH: &str = "devel";
async fn on_ready(
ctx: &Context,
ready: &Ready,
framework: &poise::Framework<(), Error>
) -> Result<(), Error> {
#[cfg(not(feature = "production"))]
{
println!("Event[Ready][Notice]: Detected a non-production environment!");
let gateway = ctx.http.get_bot_gateway().await?;
let session = gateway.session_start_limit;
println!("Event[Ready][Notice]: Session limit: {}/{}", session.remaining, session.total);
}
println!("Event[Ready]: Build version: {} ({})", BOT_VERSION.to_string(), GIT_COMMIT_HASH);
println!("Event[Ready]: Connected to API as {}", ready.user.name);
let message = CreateMessage::new();
let ready_embed = CreateEmbed::new()
.color(BINARY_PROPERTIES.embed_color)
.thumbnail(ready.user.avatar_url().unwrap_or_default())
.author(CreateEmbedAuthor::new(format!("{} is ready!", ready.user.name)));
ChannelId::new(BINARY_PROPERTIES.ready_notify).send_message(&ctx.http, message.add_embed(ready_embed)).await?;
if BINARY_PROPERTIES.deploy_commands {
let builder = poise::builtins::create_application_commands(&framework.options().commands);
let commands = Command::set_global_commands(&ctx.http, builder).await;
let mut commands_deployed = std::collections::HashSet::new();
match commands {
Ok(cmdmap) => for command in cmdmap.iter() {
commands_deployed.insert(command.name.clone());
},
Err(y) => eprintln!("Error registering commands: {:?}", y)
}
if commands_deployed.len() > 0 {
println!("Event[Ready]: Deployed the commands globally:\n- {}", commands_deployed.into_iter().collect::<Vec<_>>().join("\n- "));
}
}
Ok(())
}
async fn event_processor(
ctx: &Context,
event: &FullEvent,
framework: poise::FrameworkContext<'_, (), Error>
) -> Result<(), Error> {
match event {
FullEvent::Ratelimit { data } => {
println!("Event[Ratelimit]: {:#?}", data);
}
FullEvent::Message { new_message } => {
if new_message.author.bot || !new_message.guild_id.is_none() {
return Ok(());
}
if new_message.content.to_lowercase().starts_with("deploy") && new_message.author.id == BINARY_PROPERTIES.developers[0] {
let builder = poise::builtins::create_application_commands(&framework.options().commands);
let commands = Command::set_global_commands(&ctx.http, builder).await;
let mut commands_deployed = std::collections::HashSet::new();
match commands {
Ok(cmdmap) => for command in cmdmap.iter() {
commands_deployed.insert(command.name.clone());
},
Err(y) => {
eprintln!("Error registering commands: {:?}", y);
new_message.reply(&ctx.http, "Deployment failed, check console for more details!").await?;
}
}
if commands_deployed.len() > 0 {
new_message.reply(&ctx.http, format!(
"Deployed the commands globally:\n- {}",
commands_deployed.into_iter().collect::<Vec<_>>().join("\n- ")
)).await?;
}
}
}
FullEvent::Ready { .. } => {
let thread_id = format!("{:?}", current().id());
let thread_num: String = thread_id.chars().filter(|c| c.is_digit(10)).collect();
println!("Event[Ready]: Task Scheduler operating on thread {}", thread_num);
let ctx = Arc::new(ctx.clone());
if !TASK_RUNNING.load(Ordering::SeqCst) {
TASK_RUNNING.store(true, Ordering::SeqCst);
tokio::spawn(async move {
match internals::tasks::sample::sample(ctx).await {
Ok(_) => {},
Err(y) => {
eprintln!("TaskScheduler[Main:Sample:Error]: Task execution failed: {}", y);
if let Some(source) = y.source() {
eprintln!("TaskScheduler[Main:Sample:Error]: Task execution failed caused by: {:#?}", source);
}
}
}
TASK_RUNNING.store(false, Ordering::SeqCst);
});
} else {
println!("TaskScheduler[Main:Notice]: Another thread is already running, ignoring");
}
}
_ => {}
}
Ok(())
}
use rustbot_lib::{
utils::{
mention_dev,
get_guild_name
},
RustbotError,
RustbotData,
config::BINARY_PROPERTIES
};
use rustbot_events::events::processor;
use std::{
sync::Arc,
borrow::Cow
};
#[tokio::main]
async fn main() {
DatabaseController::new().await.expect("Error initializing database");
let prefix = if BINARY_PROPERTIES.env.contains("prod") {
Some(Cow::Borrowed("pg."))
} else {
Some(Cow::Borrowed("pg!"))
};
let commands = commands::collect!();
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![
commands::ping::ping(),
commands::sample::sample(),
commands::midi::midi_to_wav(),
commands::uptime::uptime()
],
commands,
pre_command: |ctx| Box::pin(async move {
let get_guild_name = match ctx.guild() {
Some(guild) => guild.name.clone(),
None => String::from("Direct Message")
let get_guild_channel_name = match ctx.guild_channel().await {
Some(channel) => format!("in #{}", channel.name.clone()),
None => String::from("")
};
println!("Discord[{}]: {} ran /{}", get_guild_name, ctx.author().name, ctx.command().qualified_name);
let prefix = match ctx.command().prefix_action {
Some(_) => ctx.framework().options.prefix_options.prefix.as_ref().unwrap(),
None => "/"
};
println!(
"Discord[{}:S{}]: {} ran {}{} {}",
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,
..Default::default()
},
on_error: |error| Box::pin(async move {
match error {
poise::FrameworkError::Command { error, ctx, .. } => {
@ -192,32 +73,63 @@ async fn main() {
)).await.expect("Error sending message");
},
poise::FrameworkError::EventHandler { error, event, .. } => println!("PoiseEventHandlerError({}): {}", event.snake_case_name(), error),
poise::FrameworkError::Setup { error, .. } => println!("PoiseSetupError: {}", 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");
},
poise::FrameworkError::UnknownInteraction { interaction, .. } => println!(
"PoiseUnknownInteractionError: {} tried to execute an unknown interaction ({})",
interaction.user.name,
interaction.data.name
),
other => println!("PoiseOtherError: {}", other)
poise::FrameworkError::UnknownCommand { msg, .. } => println!(
"PoiseUnknownCommandError: {} tried to execute an unknown command ({})",
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");
},
poise::FrameworkError::CommandPanic { ctx, payload, .. } => {
if let Some(payload) = payload.clone() {
println!("PoiseCommandPanic: {payload}");
ctx.reply(format!(
"The command panicked, please tell my developer about this!\n**Error:**```\n{payload}\n```"
)).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");
ctx.reply(uh_oh).await.expect("Error sending message");
}
},
other => println!("PoiseOtherError: {other}")
}
}),
allowed_mentions: Some(CreateAllowedMentions::default().empty_users()),
initialize_owners: true,
event_handler: |ctx, event, framework, _| Box::pin(event_processor(ctx, event, framework)),
skip_checks_for_owners: true,
event_handler: |framework, event| Box::pin(processor(framework, event)),
..Default::default()
})
.setup(|ctx, ready, framework| Box::pin(on_ready(ctx, ready, framework)))
.build();
let mut client = ClientBuilder::new(
token_path().await.main,
&token_path().await.main,
GatewayIntents::GUILDS
| GatewayIntents::MESSAGE_CONTENT
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::DIRECT_MESSAGES
| GatewayIntents::MESSAGE_CONTENT
)
.framework(framework)
.data(Arc::new(RustbotData {}))
.activity(ActivityData::custom("nep nep!"))
.await.expect("Error creating client");
if let Err(why) = client.start().await {
if let Err(why) = client.start_autosharded().await {
println!("Error starting client: {:#?}", why);
}
}

View File

@ -1 +0,0 @@
pub mod sample;

View File

@ -1,96 +0,0 @@
use crate::controllers::database::DATABASE;
use std::ops::Deref;
pub struct SampleData {
pub id: i64,
pub text_val: String,
pub int_val: i64,
pub boolean_val: bool
}
impl SampleData {
pub async fn list_data(id: u64) -> Result<Vec<Self>, tokio_postgres::Error> {
let pool = {
let db = DATABASE.lock().unwrap();
let controller = db.as_ref().unwrap();
controller.pool.clone()
};
let conn = pool.get().await.unwrap();
let client = conn.deref();
let rows = client.query("
SELECT * FROM sample
WHERE id = $1
", &[&(id as i64)]).await?;
let mut data = Vec::new();
for row in rows {
data.push(Self {
id: row.get("id"),
text_val: row.get("text_val"),
int_val: row.get("int_val"),
boolean_val: row.get("boolean_val")
});
}
Ok(data)
}
pub async fn create_data(
text: String,
int: i64,
boolean: bool
) -> Result<(), tokio_postgres::Error> {
let pool = {
let db = DATABASE.lock().unwrap();
let controller = db.as_ref().unwrap();
controller.pool.clone()
};
let conn = pool.get().await.unwrap();
let client = conn.deref();
client.execute("
INSERT INTO sample (text_val, int_val, boolean_val)
VALUES ($1, $2, $3)
", &[&text, &int, &boolean]).await?;
Ok(())
}
pub async fn update_data(
id: u64,
text: String,
int: i64,
boolean: bool
) -> Result<(), tokio_postgres::Error> {
let pool = {
let db = DATABASE.lock().unwrap();
let controller = db.as_ref().unwrap();
controller.pool.clone()
};
let conn = pool.get().await.unwrap();
let client = conn.deref();
client.execute("
UPDATE sample
SET text_val = $1, int_val = $2, boolean_val = $3
WHERE id = $4
", &[&text, &int, &boolean, &(id as i64)]).await?;
Ok(())
}
pub async fn delete_data(id: u64) -> Result<(), tokio_postgres::Error> {
let pool = {
let db = DATABASE.lock().unwrap();
let controller = db.as_ref().unwrap();
controller.pool.clone()
};
let conn = pool.get().await.unwrap();
let client = conn.deref();
client.execute("
DELETE FROM sample
WHERE id = $1
", &[&(id as i64)]).await?;
Ok(())
}
}

11
stuff-to-change.log Normal file
View File

@ -0,0 +1,11 @@
Only things that are needing to be changed before deploying this template to real-world use;
- The project name (Rustbot)
- Dockerfile image tag and its service in compose file
- events (rustbot_events), and its references
- jobs (rustbot_jobs), and its references
- 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.
(and delete this file too!)

8
tsclient/Cargo.toml Executable file
View File

@ -0,0 +1,8 @@
[package]
name = "rustbot_tokens"
version = "0.1.0"
edition = "2021"
[dependencies]
tokenservice-client = { version = "0.4.1", registry = "gitea" }
tokio = { workspace = true }

35
tsclient/src/lib.rs Executable file
View File

@ -0,0 +1,35 @@
use std::sync::LazyLock;
use tokio::sync::Mutex;
use tokenservice_client::{
TokenService,
TokenServiceApi
};
pub struct TSClient(TokenService);
impl Default for TSClient {
fn default() -> Self {
Self::new()
}
}
impl TSClient {
pub fn new() -> Self {
let args: Vec<String> = std::env::args().collect();
let service = if args.len() > 1 { &args[1] } else { "pgbot" };
Self(TokenService::new(service))
}
pub async fn get(&self) -> Result<TokenServiceApi, Box<dyn std::error::Error + Send + Sync>> {
match self.0.connect().await {
Ok(api) => Ok(api),
Err(e) => Err(e)
}
}
}
static TSCLIENT: LazyLock<Mutex<TSClient>> = LazyLock::new(|| Mutex::new(TSClient::new()));
pub async fn token_path() -> TokenServiceApi {
TSCLIENT.lock().await.get().await.unwrap()
}