From 8a7d414a013a329c3fbf173a835c5c0ea3c46652 Mon Sep 17 00:00:00 2001 From: Joscha Date: Fri, 27 Dec 2024 15:35:45 +0100 Subject: [PATCH] Add command abstraction --- Cargo.toml | 1 + euphoxide-bot/Cargo.toml | 1 + euphoxide-bot/src/command.rs | 172 +++++++++++++++++++++++++++++++++++ euphoxide-bot/src/lib.rs | 1 + 4 files changed, 175 insertions(+) create mode 100644 euphoxide-bot/src/command.rs diff --git a/Cargo.toml b/Cargo.toml index 97995e8..be45ac0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ version = "0.5.1" edition = "2021" [workspace.dependencies] +async-trait = "0.1.83" caseless = "0.2.1" cookie = "0.18.1" futures-util = "0.3.31" diff --git a/euphoxide-bot/Cargo.toml b/euphoxide-bot/Cargo.toml index e3ad556..9f657c5 100644 --- a/euphoxide-bot/Cargo.toml +++ b/euphoxide-bot/Cargo.toml @@ -4,6 +4,7 @@ version = { workspace = true } edition = { workspace = true } [dependencies] +async-trait = { workspace = true } cookie = { workspace = true } euphoxide = { path = "../euphoxide" } log = { workspace = true } diff --git a/euphoxide-bot/src/command.rs b/euphoxide-bot/src/command.rs new file mode 100644 index 0000000..a36bc4f --- /dev/null +++ b/euphoxide-bot/src/command.rs @@ -0,0 +1,172 @@ +use std::future::Future; + +use async_trait::async_trait; +use euphoxide::{ + api::{Data, Message, MessageId, ParsedPacket, Send, SendEvent, SendReply}, + client::{ + conn::ClientConnHandle, + state::{Joined, State}, + }, +}; + +#[non_exhaustive] +pub struct Context { + pub conn: ClientConnHandle, + pub joined: Joined, +} + +impl Context { + pub async fn send( + &self, + content: S, + ) -> euphoxide::Result>> { + self.conn + .send(Send { + content: content.to_string(), + parent: None, + }) + .await + } + + pub async fn send_only(&self, content: S) -> euphoxide::Result<()> { + let _ignore = self.send(content).await?; + Ok(()) + } + + pub async fn reply( + &self, + parent: MessageId, + content: S, + ) -> euphoxide::Result>> { + self.conn + .send(Send { + content: content.to_string(), + parent: Some(parent), + }) + .await + } + + pub async fn reply_only( + &self, + parent: MessageId, + content: S, + ) -> euphoxide::Result<()> { + let _ignore = self.reply(parent, content).await?; + Ok(()) + } +} + +#[derive(Default)] +pub struct Info { + pub trigger: Option, + pub description: Option, +} + +impl Info { + pub fn new() -> Self { + Self::default() + } + + pub fn with_trigger(mut self, trigger: impl ToString) -> Self { + self.trigger = Some(trigger.to_string()); + self + } + + pub fn with_description(mut self, description: impl ToString) -> Self { + self.description = Some(description.to_string()); + self + } + + pub fn prepend_trigger(&mut self, trigger: impl ToString) { + let cur_trigger = self.trigger.get_or_insert_default(); + if !cur_trigger.is_empty() { + cur_trigger.insert(0, ' '); + } + cur_trigger.insert_str(0, &trigger.to_string()); + } + + pub fn with_prepended_trigger(mut self, trigger: impl ToString) -> Self { + self.prepend_trigger(trigger); + self + } +} + +/// Whether a message should propagate to subsequent commands. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Propagate { + No, + Yes, +} + +#[allow(unused_variables)] +#[async_trait] +pub trait Command { + fn info(&self, ctx: &Context) -> Info { + Info::default() + } + + async fn execute( + &self, + arg: &str, + msg: &Message, + ctx: &Context, + bot: &mut B, + ) -> Result; +} + +pub struct Commands { + commands: Vec>>, +} + +impl Commands { + pub fn new() -> Self { + Self { commands: vec![] } + } + + pub fn add(&mut self, command: impl Command + 'static) { + self.commands.push(Box::new(command)); + } + + pub fn then(mut self, command: impl Command + 'static) -> Self { + self.add(command); + self + } + + pub fn infos(&self, ctx: &Context) -> Vec { + self.commands.iter().map(|c| c.info(ctx)).collect() + } + + pub async fn execute( + &self, + conn: ClientConnHandle, + state: State, + packet: ParsedPacket, + bot: &mut B, + ) -> Result { + let Ok(Data::SendEvent(SendEvent(msg))) = &packet.content else { + return Ok(Propagate::Yes); + }; + + let State::Joined(joined) = state else { + return Ok(Propagate::Yes); + }; + + let ctx = Context { conn, joined }; + + for command in &self.commands { + let propagate = command.execute(&msg.content, msg, &ctx, bot).await?; + if propagate == Propagate::No { + return Ok(Propagate::No); + } + } + + Ok(Propagate::Yes) + } +} + +// Has fewer restrictions on generic types than #[derive(Default)]. +impl Default for Commands { + fn default() -> Self { + Self::new() + } +} diff --git a/euphoxide-bot/src/lib.rs b/euphoxide-bot/src/lib.rs index 6eeeba5..3a6acc7 100644 --- a/euphoxide-bot/src/lib.rs +++ b/euphoxide-bot/src/lib.rs @@ -1,2 +1,3 @@ pub mod bot; +pub mod command; pub mod instance;