Add botrulez commands

This commit is contained in:
Joscha 2024-12-27 18:12:44 +01:00
parent 12448c26c9
commit 4160d69a2b
8 changed files with 389 additions and 1 deletions

View file

@ -9,7 +9,7 @@ edition = "2021"
[workspace.dependencies] [workspace.dependencies]
async-trait = "0.1.83" async-trait = "0.1.83"
caseless = "0.2.1" caseless = "0.2.1"
clap = { version = "4.5.23", default-features = false, features = ["std"] } clap = { version = "4.5.23", default-features = false, features = ["std", "derive"] }
cookie = "0.18.1" cookie = "0.18.1"
futures-util = "0.3.31" futures-util = "0.3.31"
jiff = { version = "0.1.15", default-features = false, features = ["std"] } jiff = { version = "0.1.15", default-features = false, features = ["std"] }

View file

@ -11,6 +11,7 @@ async-trait = { workspace = true }
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
cookie = { workspace = true } cookie = { workspace = true }
euphoxide = { path = "../euphoxide" } euphoxide = { path = "../euphoxide" }
jiff = { workspace = true }
log = { workspace = true } log = { workspace = true }
tokio = { workspace = true, features = ["rt"] } tokio = { workspace = true, features = ["rt"] }
tokio-tungstenite = { workspace = true } tokio-tungstenite = { workspace = true }

View file

@ -1,5 +1,6 @@
pub mod bang; pub mod bang;
pub mod basic; pub mod basic;
pub mod botrulez;
#[cfg(feature = "clap")] #[cfg(feature = "clap")]
pub mod clap; pub mod clap;

View file

@ -0,0 +1,8 @@
//! The main [botrulez](https://github.com/jedevc/botrulez) commands.
mod full_help;
mod ping;
mod short_help;
mod uptime;
pub use self::{full_help::*, ping::*, short_help::*, uptime::*};

View file

@ -0,0 +1,111 @@
use async_trait::async_trait;
#[cfg(feature = "clap")]
use clap::Parser;
use euphoxide::api::Message;
#[cfg(feature = "clap")]
use crate::command::clap::ClapCommand;
use crate::command::{Command, Context, Info, Propagate};
pub trait HasCommandInfos {
fn command_infos(&self, ctx: &Context) -> Vec<Info>;
}
#[derive(Default)]
pub struct FullHelp {
pub before: String,
pub after: String,
}
impl FullHelp {
pub fn new() -> Self {
Self::default()
}
pub fn with_before(mut self, before: impl ToString) -> Self {
self.before = before.to_string();
self
}
pub fn with_after(mut self, after: impl ToString) -> Self {
self.after = after.to_string();
self
}
fn formulate_reply<B: HasCommandInfos>(&self, ctx: &Context, bot: &B) -> String {
let mut result = String::new();
if !self.before.is_empty() {
result.push_str(&self.before);
result.push('\n');
}
for info in bot.command_infos(ctx) {
if let Some(trigger) = &info.trigger {
result.push_str(trigger);
if let Some(description) = &info.description {
result.push_str(" - ");
result.push_str(description);
}
result.push('\n');
}
}
if !self.after.is_empty() {
result.push_str(&self.after);
result.push('\n');
}
result
}
}
#[async_trait]
impl<B, E> Command<B, E> for FullHelp
where
B: HasCommandInfos + Send,
E: From<euphoxide::Error>,
{
async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<Propagate, E> {
if arg.trim().is_empty() {
let reply = self.formulate_reply(ctx, bot);
ctx.reply_only(msg.id, reply).await?;
Ok(Propagate::No)
} else {
Ok(Propagate::Yes)
}
}
}
/// Show full bot help.
#[cfg(feature = "clap")]
#[derive(Parser)]
pub struct FullHelpArgs {}
#[cfg(feature = "clap")]
#[async_trait]
impl<B, E> ClapCommand<B, E> for FullHelp
where
B: HasCommandInfos + Send,
E: From<euphoxide::Error>,
{
type Args = FullHelpArgs;
async fn execute(
&self,
_args: Self::Args,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<Propagate, E> {
let reply = self.formulate_reply(ctx, bot);
ctx.reply_only(msg.id, reply).await?;
Ok(Propagate::No)
}
}

View file

@ -0,0 +1,68 @@
use async_trait::async_trait;
#[cfg(feature = "clap")]
use clap::Parser;
use euphoxide::api::Message;
#[cfg(feature = "clap")]
use crate::command::clap::ClapCommand;
use crate::command::{Command, Context, Propagate};
pub struct Ping(pub String);
impl Ping {
pub fn new<S: ToString>(reply: S) -> Self {
Self(reply.to_string())
}
}
impl Default for Ping {
fn default() -> Self {
Self::new("Pong!")
}
}
#[async_trait]
impl<B, E> Command<B, E> for Ping
where
E: From<euphoxide::Error>,
{
async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
_bot: &mut B,
) -> Result<Propagate, E> {
if arg.trim().is_empty() {
ctx.reply_only(msg.id, &self.0).await?;
Ok(Propagate::No)
} else {
Ok(Propagate::Yes)
}
}
}
/// Trigger a short reply.
#[cfg(feature = "clap")]
#[derive(Parser)]
pub struct PingArgs {}
#[cfg(feature = "clap")]
#[async_trait]
impl<B, E> ClapCommand<B, E> for Ping
where
E: From<euphoxide::Error>,
{
type Args = PingArgs;
async fn execute(
&self,
_args: Self::Args,
msg: &Message,
ctx: &Context,
_bot: &mut B,
) -> Result<Propagate, E> {
ctx.reply_only(msg.id, &self.0).await?;
Ok(Propagate::No)
}
}

View file

@ -0,0 +1,62 @@
use async_trait::async_trait;
#[cfg(feature = "clap")]
use clap::Parser;
use euphoxide::api::Message;
#[cfg(feature = "clap")]
use crate::command::clap::ClapCommand;
use crate::command::{Command, Context, Propagate};
pub struct ShortHelp(pub String);
impl ShortHelp {
pub fn new<S: ToString>(text: S) -> Self {
Self(text.to_string())
}
}
#[async_trait]
impl<B, E> Command<B, E> for ShortHelp
where
E: From<euphoxide::Error>,
{
async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
_bot: &mut B,
) -> Result<Propagate, E> {
if arg.trim().is_empty() {
ctx.reply_only(msg.id, &self.0).await?;
Ok(Propagate::No)
} else {
Ok(Propagate::Yes)
}
}
}
/// Show short bot help.
#[cfg(feature = "clap")]
#[derive(Parser)]
pub struct ShortHelpArgs {}
#[cfg(feature = "clap")]
#[async_trait]
impl<B, E> ClapCommand<B, E> for ShortHelp
where
E: From<euphoxide::Error>,
{
type Args = ShortHelpArgs;
async fn execute(
&self,
_args: Self::Args,
msg: &Message,
ctx: &Context,
_bot: &mut B,
) -> Result<Propagate, E> {
ctx.reply_only(msg.id, &self.0).await?;
Ok(Propagate::No)
}
}

View file

@ -0,0 +1,137 @@
use async_trait::async_trait;
#[cfg(feature = "clap")]
use clap::Parser;
use euphoxide::api::Message;
use jiff::{Span, Timestamp, Unit};
#[cfg(feature = "clap")]
use crate::command::clap::ClapCommand;
use crate::command::{Command, Context, Propagate};
pub fn format_time(t: Timestamp) -> String {
t.strftime("%Y-%m-%d %H:%M:%S UTC").to_string()
}
pub fn format_relative_time(d: Span) -> String {
if d.is_positive() {
format!("in {}", format_duration(d.abs()))
} else {
format!("{} ago", format_duration(d.abs()))
}
}
pub fn format_duration(d: Span) -> String {
let total = d.abs().total(Unit::Second).unwrap() as i64;
let secs = total % 60;
let mins = (total / 60) % 60;
let hours = (total / 60 / 60) % 24;
let days = total / 60 / 60 / 24;
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() {
segments
} else {
format!("-{segments}")
}
}
pub struct Uptime;
pub trait HasStartTime {
fn start_time(&self) -> Timestamp;
}
impl Uptime {
fn formulate_reply<B: HasStartTime>(&self, ctx: &Context, bot: &B, connected: bool) -> String {
let start = bot.start_time();
let now = Timestamp::now();
let mut reply = format!(
"/me has been up since {} ({})",
format_time(start),
format_relative_time(start - now),
);
if connected {
let since = ctx.joined.since;
reply.push_str(&format!(
", connected since {} ({})",
format_time(since),
format_relative_time(since - now),
));
}
reply
}
}
#[async_trait]
impl<B, E> Command<B, E> for Uptime
where
B: HasStartTime + Send,
E: From<euphoxide::Error>,
{
async fn execute(
&self,
arg: &str,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<Propagate, E> {
if arg.trim().is_empty() {
let reply = self.formulate_reply(ctx, bot, false);
ctx.reply_only(msg.id, reply).await?;
Ok(Propagate::No)
} else {
Ok(Propagate::Yes)
}
}
}
/// Show how long the bot has been online.
#[cfg(feature = "clap")]
#[derive(Parser)]
pub struct UptimeArgs {
/// Show how long the bot has been connected without interruption.
#[arg(long, short)]
pub connected: bool,
}
#[cfg(feature = "clap")]
#[async_trait]
impl<B, E> ClapCommand<B, E> for Uptime
where
B: HasStartTime + Send,
E: From<euphoxide::Error>,
{
type Args = UptimeArgs;
async fn execute(
&self,
args: Self::Args,
msg: &Message,
ctx: &Context,
bot: &mut B,
) -> Result<Propagate, E> {
let reply = self.formulate_reply(ctx, bot, args.connected);
ctx.reply_only(msg.id, reply).await?;
Ok(Propagate::No)
}
}