From a0d55482cc7b0142d498f27ef973a5437177857d Mon Sep 17 00:00:00 2001 From: Joscha Date: Tue, 24 Jan 2023 15:44:08 +0100 Subject: [PATCH] Add botrulez --- Cargo.toml | 2 +- src/bot.rs | 1 + src/bot/botrulez.rs | 10 ++++ src/bot/botrulez/full_help.rs | 76 ++++++++++++++++++++++++++ src/bot/botrulez/ping.rs | 43 +++++++++++++++ src/bot/botrulez/short_help.rs | 37 +++++++++++++ src/bot/botrulez/uptime.rs | 98 ++++++++++++++++++++++++++++++++++ 7 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 src/bot/botrulez.rs create mode 100644 src/bot/botrulez/full_help.rs create mode 100644 src/bot/botrulez/ping.rs create mode 100644 src/bot/botrulez/short_help.rs create mode 100644 src/bot/botrulez/uptime.rs diff --git a/Cargo.toml b/Cargo.toml index ec9de74..31389f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ tokio-tungstenite = { version = "0.18.0", features = ["rustls-tls-native-roots"] version = "4.1.3" optional = true default-features = false -features = ["std"] +features = ["std", "derive", "deprecated"] [dev-dependencies] # For example bot tokio = { version = "1.23.0", features = ["rt-multi-thread"] } diff --git a/src/bot.rs b/src/bot.rs index 03ed2ef..5076211 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,5 +1,6 @@ //! Building blocks for bots. +pub mod botrulez; pub mod command; pub mod commands; pub mod instance; diff --git a/src/bot/botrulez.rs b/src/bot/botrulez.rs new file mode 100644 index 0000000..3161ad6 --- /dev/null +++ b/src/bot/botrulez.rs @@ -0,0 +1,10 @@ +//! The main [botrulez](https://github.com/jedevc/botrulez) commands. +mod full_help; +mod ping; +mod short_help; +mod uptime; + +pub use self::full_help::{FullHelp, HasDescriptions}; +pub use self::ping::Ping; +pub use self::short_help::ShortHelp; +pub use self::uptime::{format_duration, format_time, HasStartTime, Uptime}; diff --git a/src/bot/botrulez/full_help.rs b/src/bot/botrulez/full_help.rs new file mode 100644 index 0000000..33af357 --- /dev/null +++ b/src/bot/botrulez/full_help.rs @@ -0,0 +1,76 @@ +use async_trait::async_trait; +use clap::Parser; + +use crate::api::Message; +use crate::bot::command::{ClapCommand, Context}; +use crate::bot::commands::CommandInfo; +use crate::conn; + +/// Show full bot help. +#[derive(Parser)] +pub struct Args {} + +pub struct FullHelp { + pub before: String, + pub after: String, +} + +impl FullHelp { + pub fn new(before: S1, after: S2) -> Self { + Self { + before: before.to_string(), + after: after.to_string(), + } + } +} + +pub trait HasDescriptions { + fn descriptions(&self) -> &[CommandInfo]; +} + +#[async_trait] +impl ClapCommand for FullHelp +where + B: HasDescriptions + Send, + E: From, +{ + type Args = Args; + + async fn execute( + &self, + _args: Self::Args, + msg: &Message, + ctx: &Context, + bot: &mut B, + ) -> Result<(), E> { + let mut result = String::new(); + + if !self.before.is_empty() { + result.push_str(&self.before); + result.push('\n'); + } + + for help in bot.descriptions() { + if !help.visible { + continue; + } + + let usage = help.kind.usage(&help.name, &ctx.joined.session.name); + let line = if let Some(description) = &help.description { + format!("{usage} - {description}\n") + } else { + format!("{usage}\n") + }; + + result.push_str(&line); + } + + if !self.after.is_empty() { + result.push_str(&self.after); + result.push('\n'); + } + + ctx.reply(msg.id, result).await?; + Ok(()) + } +} diff --git a/src/bot/botrulez/ping.rs b/src/bot/botrulez/ping.rs new file mode 100644 index 0000000..056ac35 --- /dev/null +++ b/src/bot/botrulez/ping.rs @@ -0,0 +1,43 @@ +use async_trait::async_trait; +use clap::Parser; + +use crate::api::Message; +use crate::bot::command::{ClapCommand, Context}; +use crate::conn; + +/// Trigger a short reply. +#[derive(Parser)] +pub struct Args {} + +pub struct Ping(pub String); + +impl Ping { + pub fn new(reply: S) -> Self { + Self(reply.to_string()) + } +} + +impl Default for Ping { + fn default() -> Self { + Self::new("Pong!") + } +} + +#[async_trait] +impl ClapCommand for Ping +where + E: From, +{ + type Args = Args; + + async fn execute( + &self, + _args: Self::Args, + msg: &Message, + ctx: &Context, + _bot: &mut B, + ) -> Result<(), E> { + ctx.reply(msg.id, &self.0).await?; + Ok(()) + } +} diff --git a/src/bot/botrulez/short_help.rs b/src/bot/botrulez/short_help.rs new file mode 100644 index 0000000..d8d1d73 --- /dev/null +++ b/src/bot/botrulez/short_help.rs @@ -0,0 +1,37 @@ +use async_trait::async_trait; +use clap::Parser; + +use crate::api::Message; +use crate::bot::command::{ClapCommand, Context}; +use crate::conn; + +/// Show short bot help. +#[derive(Parser)] +pub struct Args {} + +pub struct ShortHelp(pub String); + +impl ShortHelp { + pub fn new(text: S) -> Self { + Self(text.to_string()) + } +} + +#[async_trait] +impl ClapCommand for ShortHelp +where + E: From, +{ + type Args = Args; + + async fn execute( + &self, + _args: Self::Args, + msg: &Message, + ctx: &Context, + _bot: &mut B, + ) -> Result<(), E> { + ctx.reply(msg.id, &self.0).await?; + Ok(()) + } +} diff --git a/src/bot/botrulez/uptime.rs b/src/bot/botrulez/uptime.rs new file mode 100644 index 0000000..c5ccaef --- /dev/null +++ b/src/bot/botrulez/uptime.rs @@ -0,0 +1,98 @@ +use async_trait::async_trait; +use clap::Parser; +use time::macros::format_description; +use time::{Duration, OffsetDateTime, UtcOffset}; + +use crate::api::Message; +use crate::bot::command::{ClapCommand, Context}; +use crate::conn; + +/// Show how long the bot has been online. +#[derive(Parser)] +pub struct Args { + /// Show how long the bot has been connected without interruption. + #[arg(long, short)] + connected: bool, +} + +pub struct Uptime; + +pub trait HasStartTime { + fn start_time(&self) -> OffsetDateTime; +} + +pub fn format_time(t: OffsetDateTime) -> String { + let t = t.to_offset(UtcOffset::UTC); + let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second] UTC"); + t.format(format).unwrap() +} + +pub fn format_duration(d: Duration) -> String { + let d_abs = d.abs(); + let days = d_abs.whole_days(); + let hours = d_abs.whole_hours() % 60; + let mins = d_abs.whole_minutes() % 60; + let secs = d_abs.whole_seconds() % 60; + + let mut segments = vec![]; + if days > 0 { + segments.push(format!("{days}d")); + } + if hours > 0 { + segments.push(format!("{hours}h")); + } + if mins > 0 { + segments.push(format!("{mins}m")); + } + if secs > 0 { + segments.push(format!("{secs}s")); + } + if segments.is_empty() { + segments.push("0s".to_string()); + } + + let segments = segments.join(" "); + if d.is_positive() { + format!("in {segments}") + } else { + format!("{segments} ago") + } +} + +#[async_trait] +impl ClapCommand for Uptime +where + B: HasStartTime + Send, + E: From, +{ + type Args = Args; + + async fn execute( + &self, + args: Self::Args, + msg: &Message, + ctx: &Context, + bot: &mut B, + ) -> Result<(), E> { + let start = bot.start_time(); + let now = OffsetDateTime::now_utc(); + + let mut reply = format!( + "/me has been up since {} ({})", + format_time(start), + format_duration(start - now), + ); + + if args.connected { + let since = ctx.joined.since; + reply.push_str(&format!( + ", connected since {} ({})", + format_time(since), + format_duration(since - now), + )); + } + + ctx.reply(msg.id, reply).await?; + Ok(()) + } +}