Add botrulez commands
This commit is contained in:
parent
12448c26c9
commit
4160d69a2b
8 changed files with 389 additions and 1 deletions
|
|
@ -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"] }
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
8
euphoxide-bot/src/command/botrulez.rs
Normal file
8
euphoxide-bot/src/command/botrulez.rs
Normal 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::*};
|
||||||
111
euphoxide-bot/src/command/botrulez/full_help.rs
Normal file
111
euphoxide-bot/src/command/botrulez/full_help.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
euphoxide-bot/src/command/botrulez/ping.rs
Normal file
68
euphoxide-bot/src/command/botrulez/ping.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
62
euphoxide-bot/src/command/botrulez/short_help.rs
Normal file
62
euphoxide-bot/src/command/botrulez/short_help.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
137
euphoxide-bot/src/command/botrulez/uptime.rs
Normal file
137
euphoxide-bot/src/command/botrulez/uptime.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue