Update template with restructured project
This commit is contained in:
parent
f02775f9a0
commit
85c93f6f03
@ -1,8 +1,9 @@
|
|||||||
.vscode
|
.vscode
|
||||||
target
|
target
|
||||||
.env
|
|
||||||
.gitignore
|
.gitignore
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
Dockerfile
|
Dockerfile
|
||||||
renovate.json
|
renovate.json
|
||||||
run.sh
|
run.sh
|
||||||
|
|
||||||
|
stuff-to-change.log
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1 @@
|
|||||||
target
|
target
|
||||||
.env
|
|
||||||
|
940
Cargo.lock
generated
940
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
40
Cargo.toml
Normal file → Executable file
40
Cargo.toml
Normal file → Executable file
@ -3,21 +3,41 @@ name = "rustbot"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[workspace]
|
||||||
bb8 = "0.8.5"
|
members = [
|
||||||
bb8-postgres = "0.8.1"
|
"events",
|
||||||
cargo_toml = "0.20.4"
|
"jobs",
|
||||||
once_cell = "1.19.0"
|
"library",
|
||||||
|
"tsclient"
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
cargo_toml = "0.20.5"
|
||||||
poise = "0.6.1"
|
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"
|
sysinfo = "0.32.0"
|
||||||
tokenservice-client = { version = "0.4.0", registry = "gitea" }
|
time = "0.3.36"
|
||||||
tokio = { version = "1.39.2", features = ["macros", "signal", "rt-multi-thread"] }
|
tokio = { workspace = true }
|
||||||
tokio-postgres = "0.7.11"
|
|
||||||
uptime_lib = "0.3.1"
|
uptime_lib = "0.3.1"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
poise = { git = "https://github.com/serenity-rs/poise", branch = "serenity-next" }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
production = []
|
production = ["rustbot_lib/production", "rustbot_events/production"]
|
||||||
|
not_ready = ["rustbot_lib/not_ready"]
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "rustbot"
|
name = "rustbot"
|
||||||
|
20
Dockerfile
20
Dockerfile
@ -1,24 +1,10 @@
|
|||||||
FROM rust:1.82-alpine3.20@sha256:466dc9924d265455aa73e72fd9cdac9db69ce6a988e6f0e6baf852db3485d97d AS chef
|
FROM scratch AS base
|
||||||
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
|
|
||||||
WORKDIR /builder
|
WORKDIR /builder
|
||||||
|
|
||||||
FROM chef AS planner
|
|
||||||
COPY . .
|
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
|
FROM alpine:3.20@sha256:e72ad0747b9dc266fca31fb004580d316b6ae5b0fdbbb65f17bbe371a5b24cff
|
||||||
LABEL org.opencontainers.image.source="https://git.toast-server.net/toast/Rustbot"
|
LABEL org.opencontainers.image.source="https://git.toast-server.net/toast/Rustbot"
|
||||||
RUN apk add --no-cache libgcc
|
RUN apk add --no-cache libgcc
|
||||||
WORKDIR /rustbot
|
WORKDIR /rustbot
|
||||||
COPY --from=builder /builder/target/release/rustbot .
|
COPY --from=base /builder/target/x86_64-unknown-linux-musl/release/rustbot .
|
||||||
CMD ./rustbot
|
CMD [ "./rustbot" ]
|
||||||
|
5
build.sh
Executable file
5
build.sh
Executable 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
|
@ -1,23 +1,6 @@
|
|||||||
version: '3.8'
|
|
||||||
services:
|
services:
|
||||||
bot:
|
bot:
|
||||||
container_name: rustbot
|
container_name: rustbot
|
||||||
#image: 'git.toast-server.net/toast/rustbot:main'
|
#image: 'git.toast-server.net/toast/rustbot:main'
|
||||||
build: .
|
build: .
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
restart: unless-stopped
|
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
12
events/Cargo.toml
Executable 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
33
events/src/events.rs
Executable 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
67
events/src/events/ready.rs
Executable 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
29
events/src/events/shards.rs
Executable 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
45
events/src/lib.rs
Executable 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 = ¤t_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
7
jobs/Cargo.toml
Executable 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
3
jobs/src/lib.rs
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod tasks;
|
||||||
|
|
||||||
|
const RUSTBOT_SCHEDULER: &str = "RustbotScheduler";
|
43
jobs/src/tasks.rs
Executable file
43
jobs/src/tasks.rs
Executable 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
13
library/Cargo.toml
Executable 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
37
library/build.rs
Executable 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
44
library/src/config.rs
Executable 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
2
library/src/data.rs
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RustbotData {}
|
7
library/src/lib.rs
Executable file
7
library/src/lib.rs
Executable 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
70
library/src/utils.rs
Executable 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
3
run.sh
@ -1,3 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
export DOCKER_HOSTNAME=$(hostname)
|
||||||
|
export $(cat .env.bot | xargs)
|
||||||
clear && cargo run
|
clear && cargo run
|
||||||
|
unset DOCKER_HOSTNAME
|
||||||
|
32
src/commands.rs
Normal file → Executable file
32
src/commands.rs
Normal file → Executable file
@ -1,4 +1,28 @@
|
|||||||
pub mod midi;
|
mod dev;
|
||||||
pub mod ping;
|
mod eightball;
|
||||||
pub mod sample;
|
mod ping;
|
||||||
pub mod uptime;
|
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
50
src/commands/dev.rs
Executable 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
80
src/commands/eightball.rs
Executable 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()]
|
||||||
|
}
|
@ -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
36
src/commands/ping.rs
Normal file → Executable 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)]
|
#[poise::command(slash_command)]
|
||||||
pub async fn ping(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
|
pub async fn ping(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
|
||||||
ctx.reply(format!("Powong! `{:?}`", ctx.ping().await)).await?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -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
88
src/commands/uptime.rs
Normal file → Executable file
@ -1,16 +1,10 @@
|
|||||||
use crate::{
|
use crate::RustbotError;
|
||||||
Error,
|
use super::PoiseContext;
|
||||||
GIT_COMMIT_HASH,
|
|
||||||
internals::utils::{
|
|
||||||
BOT_VERSION,
|
|
||||||
format_duration,
|
|
||||||
concat_message
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
use sysinfo::System;
|
use sysinfo::System;
|
||||||
use uptime_lib::get;
|
use uptime_lib::get;
|
||||||
use std::{
|
use std::{
|
||||||
|
env::var,
|
||||||
fs::File,
|
fs::File,
|
||||||
path::Path,
|
path::Path,
|
||||||
time::{
|
time::{
|
||||||
@ -23,34 +17,54 @@ use std::{
|
|||||||
BufReader
|
BufReader
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
use rustbot_lib::utils::{
|
||||||
|
BOT_VERSION,
|
||||||
|
GIT_COMMIT_HASH,
|
||||||
|
GIT_COMMIT_BRANCH,
|
||||||
|
format_duration
|
||||||
|
};
|
||||||
|
|
||||||
fn get_os_info() -> String {
|
fn get_os_info() -> String {
|
||||||
let path = Path::new("/etc/os-release");
|
let path = Path::new("/etc/os-release");
|
||||||
let mut name = "BoringOS".to_string();
|
let mut name = "BoringOS".to_string();
|
||||||
let mut version = "v0.0".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);
|
let reader = BufReader::new(file);
|
||||||
for line in reader.lines() {
|
let set_value = |s: String| s.split('=').nth(1).unwrap_or_default().trim_matches('"').to_string();
|
||||||
if let Ok(line) = line {
|
reader.lines().map_while(Result::ok).for_each(|line| {
|
||||||
if line.starts_with("NAME=") {
|
match line {
|
||||||
name = line.split('=').nth(1).unwrap_or_default().trim_matches('"').to_string();
|
l if l.starts_with("NAME=") => name = set_value(l),
|
||||||
} else if line.starts_with("VERSION=") {
|
l if l.starts_with("VERSION=") => version = set_value(l),
|
||||||
version = line.split('=').nth(1).unwrap_or_default().trim_matches('"').to_string();
|
l if l.starts_with("VERSION_ID=") => version = set_value(l),
|
||||||
} else if line.starts_with("VERSION_ID=") {
|
_ => {}
|
||||||
version = line.split('=').nth(1).unwrap_or_default().trim_matches('"').to_string();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
/// Retrieve host and bot uptimes
|
||||||
#[poise::command(slash_command)]
|
#[poise::command(slash_command)]
|
||||||
pub async fn uptime(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
|
pub async fn uptime(ctx: PoiseContext<'_>) -> Result<(), RustbotError> {
|
||||||
let _bot = ctx.http().get_current_user().await.unwrap();
|
let bot = ctx.http().get_current_user().await.unwrap();
|
||||||
let mut sys = System::new_all();
|
let mut sys = System::new_all();
|
||||||
sys.refresh_all();
|
sys.refresh_all();
|
||||||
|
|
||||||
@ -60,7 +74,17 @@ pub async fn uptime(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
|
|||||||
// Fetch system's processor
|
// Fetch system's processor
|
||||||
let cpu = sys.cpus();
|
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 curr_pid = sysinfo::get_current_pid().unwrap();
|
||||||
let now = SystemTime::now();
|
let now = SystemTime::now();
|
||||||
let mut proc_uptime = 0;
|
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();
|
proc_uptime = now.duration_since(time_started).unwrap().as_secs();
|
||||||
}
|
}
|
||||||
|
|
||||||
let stat_msg = vec![
|
// Fetch the node hostname from envvar
|
||||||
format!("**{} {}** `{}`", _bot.name, BOT_VERSION.as_str(), GIT_COMMIT_HASH),
|
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!(">>> System: `{}`", format_duration(sys_uptime)),
|
||||||
format!("Process: `{}`", format_duration(proc_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())
|
format!("OS: `{}`", get_os_info())
|
||||||
];
|
];
|
||||||
ctx.reply(concat_message(stat_msg)).await?;
|
ctx.reply(stat_msg.join("\n")).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
pub mod database;
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
pub mod config;
|
|
||||||
pub mod tasks;
|
|
||||||
pub mod tsclient;
|
|
||||||
pub mod utils;
|
|
Binary file not shown.
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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));
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
260
src/main.rs
Normal file → Executable file
@ -1,187 +1,68 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod controllers;
|
|
||||||
mod models;
|
|
||||||
mod internals;
|
|
||||||
// https://cdn.toast-server.net/RustFSHiearchy.png
|
// https://cdn.toast-server.net/RustFSHiearchy.png
|
||||||
// Using the new filesystem hierarchy
|
// Using the new filesystem hierarchy
|
||||||
|
|
||||||
use crate::{
|
use rustbot_tokens::token_path;
|
||||||
internals::{
|
|
||||||
utils::{
|
|
||||||
BOT_VERSION,
|
|
||||||
token_path,
|
|
||||||
mention_dev
|
|
||||||
},
|
|
||||||
config::BINARY_PROPERTIES
|
|
||||||
},
|
|
||||||
controllers::database::DatabaseController
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
thread::current,
|
|
||||||
sync::{
|
|
||||||
Arc,
|
|
||||||
atomic::{
|
|
||||||
AtomicBool,
|
|
||||||
Ordering
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
use poise::serenity_prelude::{
|
use poise::serenity_prelude::{
|
||||||
builder::{
|
builder::CreateAllowedMentions,
|
||||||
CreateMessage,
|
|
||||||
CreateEmbed,
|
|
||||||
CreateEmbedAuthor
|
|
||||||
},
|
|
||||||
Ready,
|
|
||||||
Context,
|
|
||||||
FullEvent,
|
|
||||||
ClientBuilder,
|
ClientBuilder,
|
||||||
ChannelId,
|
ActivityData,
|
||||||
Command,
|
|
||||||
GatewayIntents
|
GatewayIntents
|
||||||
};
|
};
|
||||||
|
use rustbot_lib::{
|
||||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
utils::{
|
||||||
static TASK_RUNNING: AtomicBool = AtomicBool::new(false);
|
mention_dev,
|
||||||
|
get_guild_name
|
||||||
#[cfg(feature = "production")]
|
},
|
||||||
pub static GIT_COMMIT_HASH: &str = env!("GIT_COMMIT_HASH");
|
RustbotError,
|
||||||
#[cfg(not(feature = "production"))]
|
RustbotData,
|
||||||
pub static GIT_COMMIT_HASH: &str = "devel";
|
config::BINARY_PROPERTIES
|
||||||
|
};
|
||||||
async fn on_ready(
|
use rustbot_events::events::processor;
|
||||||
ctx: &Context,
|
use std::{
|
||||||
ready: &Ready,
|
sync::Arc,
|
||||||
framework: &poise::Framework<(), Error>
|
borrow::Cow
|
||||||
) -> 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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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()
|
let framework = poise::Framework::builder()
|
||||||
.options(poise::FrameworkOptions {
|
.options(poise::FrameworkOptions {
|
||||||
commands: vec![
|
commands,
|
||||||
commands::ping::ping(),
|
|
||||||
commands::sample::sample(),
|
|
||||||
commands::midi::midi_to_wav(),
|
|
||||||
commands::uptime::uptime()
|
|
||||||
],
|
|
||||||
pre_command: |ctx| Box::pin(async move {
|
pre_command: |ctx| Box::pin(async move {
|
||||||
let get_guild_name = match ctx.guild() {
|
let get_guild_channel_name = match ctx.guild_channel().await {
|
||||||
Some(guild) => guild.name.clone(),
|
Some(channel) => format!("in #{}", channel.name.clone()),
|
||||||
None => String::from("Direct Message")
|
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 {
|
on_error: |error| Box::pin(async move {
|
||||||
match error {
|
match error {
|
||||||
poise::FrameworkError::Command { error, ctx, .. } => {
|
poise::FrameworkError::Command { error, ctx, .. } => {
|
||||||
@ -192,32 +73,63 @@ async fn main() {
|
|||||||
)).await.expect("Error sending message");
|
)).await.expect("Error sending message");
|
||||||
},
|
},
|
||||||
poise::FrameworkError::EventHandler { error, event, .. } => println!("PoiseEventHandlerError({}): {}", event.snake_case_name(), error),
|
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!(
|
poise::FrameworkError::UnknownInteraction { interaction, .. } => println!(
|
||||||
"PoiseUnknownInteractionError: {} tried to execute an unknown interaction ({})",
|
"PoiseUnknownInteractionError: {} tried to execute an unknown interaction ({})",
|
||||||
interaction.user.name,
|
interaction.user.name,
|
||||||
interaction.data.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,
|
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()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.setup(|ctx, ready, framework| Box::pin(on_ready(ctx, ready, framework)))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let mut client = ClientBuilder::new(
|
let mut client = ClientBuilder::new(
|
||||||
token_path().await.main,
|
&token_path().await.main,
|
||||||
GatewayIntents::GUILDS
|
GatewayIntents::GUILDS
|
||||||
| GatewayIntents::MESSAGE_CONTENT
|
| GatewayIntents::GUILD_MESSAGES
|
||||||
| GatewayIntents::DIRECT_MESSAGES
|
| GatewayIntents::DIRECT_MESSAGES
|
||||||
|
| GatewayIntents::MESSAGE_CONTENT
|
||||||
)
|
)
|
||||||
.framework(framework)
|
.framework(framework)
|
||||||
|
.data(Arc::new(RustbotData {}))
|
||||||
|
.activity(ActivityData::custom("nep nep!"))
|
||||||
.await.expect("Error creating client");
|
.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);
|
println!("Error starting client: {:#?}", why);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
pub mod sample;
|
|
@ -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
11
stuff-to-change.log
Normal 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
8
tsclient/Cargo.toml
Executable 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
35
tsclient/src/lib.rs
Executable 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()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user