User-App support
All checks were successful
Build and push Docker image / build (push) Successful in 7m22s
Build and push Docker image / deploy (push) Successful in 1m7s

This commit is contained in:
toast 2024-11-20 03:42:58 +11:00
parent f98eb4fd7b
commit 47453acecd
15 changed files with 507 additions and 205 deletions

564
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,27 @@
[package]
name = "kon"
version = "0.4.1"
version = "0.5.0"
edition = "2021"
[dependencies]
bb8 = "0.8.5"
bb8 = "0.8.6"
bb8-redis = "0.17.0"
cargo_toml = "0.20.5"
feed-rs = "2.1.1"
feed-rs = "2.2.0"
once_cell = "1.20.2"
poise = "0.6.1"
regex = "1.11.0"
reqwest = { version = "0.12.8", features = ["json", "native-tls-vendored"] }
serde = "1.0.210"
serde_json = "1.0.128"
regex = "1.11.1"
reqwest = { version = "0.12.9", features = ["json", "native-tls-vendored"] }
serde = "1.0.215"
serde_json = "1.0.133"
sysinfo = "0.32.0"
tokenservice-client = { version = "0.4.0", registry = "gitea" }
tokio = { version = "1.40.0", features = ["macros", "signal", "rt-multi-thread"] }
tokio = { version = "1.41.1", features = ["macros", "signal", "rt-multi-thread"] }
uptime_lib = "0.3.1"
[patch.crates-io]
poise = { git = "https://github.com/serenity-rs/poise", branch = "next" }
[features]
production = []

View File

@ -8,7 +8,7 @@ services:
- cache
cache:
container_name: kon-redis
image: redis/redis-stack-server:7.4.0-v0
image: redis/redis-stack-server:7.4.0-v1
restart: unless-stopped
ports:
- 37935:6379/tcp

View File

@ -6,13 +6,15 @@ pub mod ping;
pub mod status;
pub mod uptime;
type PoiseCtx<'a> = poise::Context<'a, (), Error>;
/// Deploy the commands globally or in a guild
#[poise::command(
prefix_command,
owners_only,
guild_only
)]
pub async fn deploy(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn deploy(ctx: PoiseCtx<'_>) -> Result<(), Error> {
poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(())
}

View File

@ -187,15 +187,17 @@ async fn ilo_data(endpoint: RedfishEndpoint) -> Result<Box<dyn std::any::Any + S
/// Retrieve data from the HP iLO4 interface
#[poise::command(
slash_command,
install_context = "Guild|User",
interaction_context = "Guild|BotDm|PrivateChannel",
subcommands("temperature", "power", "system")
)]
pub async fn ilo(_: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn ilo(_: super::PoiseCtx<'_>) -> Result<(), Error> {
Ok(())
}
/// Retrieve the server's temperature data
#[poise::command(slash_command)]
pub async fn temperature(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn temperature(ctx: super::PoiseCtx<'_>) -> Result<(), Error> {
ctx.defer().await?;
let ilo = ilo_data(RedfishEndpoint::Thermal).await.unwrap();
let data = ilo.downcast_ref::<Chassis>().unwrap();
@ -246,7 +248,7 @@ pub async fn temperature(ctx: poise::Context<'_, (), Error>) -> Result<(), Error
/// Retrieve the server's power data
#[poise::command(slash_command)]
pub async fn power(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn power(ctx: super::PoiseCtx<'_>) -> Result<(), Error> {
ctx.defer().await?;
let ilo = ilo_data(RedfishEndpoint::Power).await.unwrap();
let data = ilo.downcast_ref::<Power>().unwrap();
@ -272,7 +274,7 @@ pub async fn power(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
/// Retrieve the server's system data
#[poise::command(slash_command)]
pub async fn system(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn system(ctx: super::PoiseCtx<'_>) -> Result<(), Error> {
ctx.defer().await?;
let (ilo_sys, ilo_event) = tokio::join!(

View File

@ -21,9 +21,13 @@ use poise::{
};
/// Convert MIDI file to WAV
#[poise::command(context_menu_command = "MIDI -> WAV")]
#[poise::command(
context_menu_command = "MIDI -> WAV",
install_context = "User",
interaction_context = "Guild|BotDm|PrivateChannel",
)]
pub async fn midi_to_wav(
ctx: poise::Context<'_, (), Error>,
ctx: super::PoiseCtx<'_>,
#[description = "MIDI file to be converted"] message: poise::serenity_prelude::Message
) -> Result<(), Error> {
let re = Regex::new(r"(?i)\.mid$").unwrap();
@ -46,7 +50,7 @@ pub async fn midi_to_wav(
)
.await.unwrap();
return Err(Error::from(format!("Failed to download the file: {}", y)))
return Err(Error::from(format!("Failed to download the file: {y}")))
}
};
@ -63,7 +67,7 @@ pub async fn midi_to_wav(
.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);
println!("Discord[{}]: Processing MIDI file: \"{}\"", ctx.command().qualified_name, midi_path);
match output {
Ok(_) => {
@ -73,8 +77,8 @@ pub async fn midi_to_wav(
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
"Discord[{}]: Processed file couldn't be uploaded back to Discord channel due to upload limit",
ctx.command().qualified_name
);
ctx.send(CreateReply::default()
@ -93,7 +97,7 @@ pub async fn midi_to_wav(
.content("Command didn't execute successfully, check console for more information!")
).await.unwrap();
return Err(Error::from(format!("Midi conversion failed: {}", y)))
return Err(Error::from(format!("Midi conversion failed: {y}")))
}
}

View File

@ -2,7 +2,7 @@ use crate::Error;
/// Check if the bot is alive
#[poise::command(slash_command)]
pub async fn ping(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn ping(ctx: super::PoiseCtx<'_>) -> Result<(), Error> {
ctx.reply(format!("Powong! `{:?}`", ctx.ping().await)).await?;
Ok(())
}

View File

@ -71,13 +71,13 @@ fn process_pms_statuses(servers: Vec<(String, Vec<Value>)>) -> Vec<(String, Stri
slash_command,
subcommands("wg")
)]
pub async fn status(_: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn status(_: super::PoiseCtx<'_>) -> Result<(), Error> {
Ok(())
}
/// Retrieve the server statuses from Wargaming
#[poise::command(slash_command)]
pub async fn wg(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn wg(ctx: super::PoiseCtx<'_>) -> Result<(), Error> {
let pms_asia = token_path().await.wg_pms;
let pms_eu = pms_asia.replace("asia", "eu");
let embed = CreateEmbed::new().color(BINARY_PROPERTIES.embed_color);

View File

@ -42,12 +42,12 @@ fn get_os_info() -> String {
});
}
format!("{} {}", name, version)
format!("{name} {version}")
}
/// Retrieve host and bot uptimes
#[poise::command(slash_command)]
pub async fn uptime(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
pub async fn uptime(ctx: super::PoiseCtx<'_>) -> Result<(), Error> {
let _bot = ctx.http().get_current_user().await.unwrap();
let mut sys = System::new_all();
sys.refresh_all();
@ -68,7 +68,7 @@ pub async fn uptime(ctx: poise::Context<'_, (), Error>) -> Result<(), Error> {
}
let stat_msg = [
format!("**{} {}** `{}:{}`", _bot.name, BOT_VERSION.as_str(), GIT_COMMIT_HASH, GIT_COMMIT_BRANCH),
format!("**{} {}** `{GIT_COMMIT_HASH}:{GIT_COMMIT_BRANCH}`", _bot.name, BOT_VERSION.as_str()),
format!(">>> System: `{}`", format_duration(sys_uptime)),
format!("Process: `{}`", format_duration(proc_uptime)),
format!("CPU: `{}`", cpu[0].brand()),

View File

@ -27,6 +27,7 @@ impl RedisController {
async fn create_pool(manager: RedisConnectionManager) -> Pool<RedisConnectionManager> {
let mut backoff = 1;
let redis_err = "Redis[Error]: {{ e }}, retrying in {{ backoff }} seconds";
loop {
match Pool::builder().max_size(20).retry_connection(true).build(manager.clone()).await {
@ -40,19 +41,19 @@ impl RedisController {
return pool.clone();
},
Err(e) => {
eprintln!("Redis[Error]: {}, retrying in {} seconds", e, backoff);
eprintln!("{}", redis_err.replace("{{ e }}", &e.to_string()).replace("{{ backoff }}", &backoff.to_string()));
Self::apply_backoff(&mut backoff).await;
}
}
},
Err(e) => {
eprintln!("Redis[ConnError]: {}, retrying in {} seconds", e, backoff);
eprintln!("{}", redis_err.replace("{{ e }}", &e.to_string()).replace("{{ backoff }}", &backoff.to_string()));
Self::apply_backoff(&mut backoff).await;
}
}
}
Err(e) => {
eprintln!("Redis[PoolError]: {}, retrying in {} seconds", e, backoff);
eprintln!("Redis[PoolError]: {e}, retrying in {backoff} seconds");
Self::apply_backoff(&mut backoff).await;
}
}

View File

@ -1,6 +1,7 @@
use std::sync::LazyLock;
pub struct ConfigMeta {
pub env: String,
pub embed_color: i32,
pub ready_notify: u64,
pub rss_channel: u64,
@ -14,6 +15,7 @@ pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(ConfigMeta::n
#[cfg(not(feature = "production"))]
pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(||
ConfigMeta::new()
.env("dev")
.embed_color(0xf1d63c)
.ready_notify(865673694184996888)
.rss_channel(865673694184996888)
@ -22,6 +24,7 @@ pub static BINARY_PROPERTIES: LazyLock<ConfigMeta> = LazyLock::new(||
impl ConfigMeta {
fn new() -> Self {
Self {
env: "prod".to_string(),
embed_color: 0x5a99c7,
ready_notify: 865673694184996888,
rss_channel: 865673694184996888,
@ -33,6 +36,12 @@ impl ConfigMeta {
}
// Scalable functions below;
#[cfg(not(feature = "production"))]
fn env(mut self, env: &str) -> Self {
self.env = env.to_string();
self
}
#[cfg(not(feature = "production"))]
fn embed_color(mut self, color: i32) -> Self {
self.embed_color = color;

View File

@ -17,7 +17,7 @@ impl HttpClient {
pub async fn get(&self, url: &str, ua: &str) -> Result<Response, Error> {
let response = self.0.get(url).header(
reqwest::header::USER_AGENT,
format!("Kon ({}-{}) - {}/reqwest", super::utils::BOT_VERSION.as_str(), crate::GIT_COMMIT_HASH, ua)
format!("Kon ({}-{}) - {ua}/reqwest", super::utils::BOT_VERSION.as_str(), crate::GIT_COMMIT_HASH)
)
.timeout(Duration::from_secs(30))
.send()
@ -26,11 +26,11 @@ impl HttpClient {
match response {
Ok(res) => Ok(res),
Err(y) if y.is_timeout() => {
eprintln!("{ERROR_PREFIX} Request timed out for \"{}\"", url);
eprintln!("{ERROR_PREFIX} Request timed out for \"{url}\"");
Err(y)
},
Err(y) if y.is_connect() => {
eprintln!("{ERROR_PREFIX} Connection failed for \"{}\"", url);
eprintln!("{ERROR_PREFIX} Connection failed for \"{url}\"");
Err(y)
},
Err(y) => Err(y)

View File

@ -16,11 +16,11 @@ use std::{
};
fn task_info(name: &str, message: &str) {
println!("TaskScheduler[{}]: {}", name, message)
println!("TaskScheduler[{name}]: {message}")
}
fn task_err(name: &str, message: &str) {
eprintln!("TaskScheduler[{}:Error]: {}", name, message)
eprintln!("TaskScheduler[{name}:Error]: {message}")
}
static TASK_RUNNING: AtomicBool = AtomicBool::new(false);
@ -36,9 +36,9 @@ where
TASK_RUNNING.store(true, Ordering::SeqCst);
spawn(async move {
if let Err(y) = task(ctx_cl).await {
eprintln!("TaskScheduler[Main:Error]: Failed to execute the task, error reason: {}", y);
eprintln!("TaskScheduler[Main:Error]: Failed to execute the task, error reason: {y}");
if let Some(source) = y.source() {
eprintln!("TaskScheduler[Main:Error]: Failed to execute the task, this is caused by: {:#?}", source);
eprintln!("TaskScheduler[Main:Error]: Failed to execute the task, this is caused by: {source:#?}");
}
}
TASK_RUNNING.store(false, Ordering::SeqCst);

View File

@ -11,7 +11,7 @@ pub static BOT_VERSION: LazyLock<String> = LazyLock::new(|| {
.unwrap()
.version
.unwrap();
format!("v{}", cargo_version)
format!("v{cargo_version}")
});
static TSCLIENT: LazyLock<Mutex<TSClient>> = LazyLock::new(|| Mutex::new(TSClient::new()));
@ -28,7 +28,7 @@ pub fn mention_dev(ctx: poise::Context<'_, (), crate::Error>) -> Option<String>
for dev in devs {
if app_owners.contains(&UserId::new(dev)) {
mentions.push(format!("<@{}>", dev));
mentions.push(format!("<@{dev}>"));
}
}
@ -47,15 +47,15 @@ pub fn format_duration(secs: u64) -> String {
let mut formatted_string = String::new();
if days > 0 {
formatted_string.push_str(&format!("{}d, ", days));
formatted_string.push_str(&format!("{days}d, "));
}
if hours > 0 || days > 0 {
formatted_string.push_str(&format!("{}h, ", hours));
formatted_string.push_str(&format!("{hours}h, "));
}
if minutes > 0 || hours > 0 {
formatted_string.push_str(&format!("{}m, ", minutes));
formatted_string.push_str(&format!("{minutes}m, "));
}
formatted_string.push_str(&format!("{}s", seconds));
formatted_string.push_str(&format!("{seconds}s"));
formatted_string
}
@ -75,8 +75,8 @@ pub fn format_bytes(bytes: u64) -> String {
}
if unit == "B" {
format!("{}{}", value, unit)
format!("{value}{unit}")
} else {
format!("{:.2}{}", value, unit)
format!("{value:.2}{unit}")
}
}

View File

@ -19,6 +19,7 @@ use crate::internals::{
use std::{
sync::Arc,
borrow::Cow,
thread::current
};
use poise::serenity_prelude::{
@ -57,7 +58,7 @@ async fn on_ready(
println!("Event[Ready][Notice]: Session limit: {}/{}", session.remaining, session.total);
}
println!("Event[Ready]: Build version: {} ({}:{})", *BOT_VERSION, GIT_COMMIT_HASH, GIT_COMMIT_BRANCH);
println!("Event[Ready]: Build version: {} ({GIT_COMMIT_HASH}:{GIT_COMMIT_BRANCH})", *BOT_VERSION);
println!("Event[Ready]: Connected to API as {}", ready.user.name);
let message = CreateMessage::new();
@ -72,16 +73,15 @@ async fn on_ready(
}
async fn event_processor(
ctx: &Context,
event: &FullEvent,
_framework: poise::FrameworkContext<'_, (), Error>
framework: poise::FrameworkContext<'_, (), Error>,
event: &FullEvent
) -> Result<(), Error> {
if let FullEvent::Ready { .. } = event {
let thread_id = format!("{:?}", current().id());
let thread_num: String = thread_id.chars().filter(|c| c.is_ascii_digit()).collect();
println!("Event[Ready]: Task Scheduler operating on thread {}", thread_num);
println!("Event[Ready]: Task Scheduler operating on thread {thread_num}");
let ctx = Arc::new(ctx.clone());
let ctx = Arc::new(framework.serenity_context.clone());
run_task(ctx.clone(), rss).await;
}
@ -90,6 +90,12 @@ async fn event_processor(
#[tokio::main]
async fn main() {
let prefix = if BINARY_PROPERTIES.env.contains("dev") {
Some(Cow::Borrowed("kon!"))
} else {
Some(Cow::Borrowed("k!"))
};
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![
@ -101,7 +107,7 @@ async fn main() {
commands::uptime::uptime()
],
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some(String::from("konata")),
prefix,
mention_as_prefix: false,
case_insensitive_commands: true,
ignore_bots: true,
@ -113,29 +119,32 @@ async fn main() {
Some(guild) => guild.name.clone(),
None => String::from("Direct Message")
};
println!("Discord[{}]: {} ran /{}", get_guild_name, ctx.author().name, ctx.command().qualified_name);
println!(
"Discord[{get_guild_name}]: {} ran /{}",
ctx.author().name,
ctx.command().qualified_name
);
}),
on_error: |error| Box::pin(async move {
match error {
poise::FrameworkError::Command { error, ctx, .. } => {
println!("PoiseCommandError({}): {}", ctx.command().qualified_name, error);
println!("PoiseCommandError({}): {error}", ctx.command().qualified_name);
ctx.reply(format!(
"Encountered an error during command execution, ask {} to check console for more details!",
mention_dev(ctx).unwrap_or_default()
)).await.expect("Error sending message");
},
poise::FrameworkError::EventHandler { error, event, .. } => println!("PoiseEventHandlerError({}): {}", event.snake_case_name(), error),
poise::FrameworkError::Setup { error, .. } => println!("PoiseSetupError: {}", error),
poise::FrameworkError::EventHandler { error, event, .. } => println!("PoiseEventHandlerError({}): {error}", event.snake_case_name()),
poise::FrameworkError::UnknownInteraction { interaction, .. } => println!(
"PoiseUnknownInteractionError: {} tried to execute an unknown interaction ({})",
interaction.user.name,
interaction.data.name
),
other => println!("PoiseOtherError: {}", other)
other => println!("PoiseOtherError: {other}")
}
}),
initialize_owners: true,
event_handler: |ctx, event, framework, _| Box::pin(event_processor(ctx, event, framework)),
event_handler: |framework, event| Box::pin(event_processor(framework, event)),
..Default::default()
})
.setup(|ctx, ready, framework| Box::pin(on_ready(ctx, ready, framework)))
@ -151,6 +160,6 @@ async fn main() {
.await.expect("Error creating client");
if let Err(why) = client.start().await {
println!("Error starting client: {:#?}", why);
println!("Error starting client: {why:#?}");
}
}