Add basic commands
This commit is contained in:
parent
39ab887b47
commit
9a1ba8f1ce
7 changed files with 589 additions and 41 deletions
|
|
@ -7,6 +7,7 @@ version = "0.5.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
async-trait = "0.1.83"
|
||||||
caseless = "0.2.1"
|
caseless = "0.2.1"
|
||||||
cookie = "0.18.1"
|
cookie = "0.18.1"
|
||||||
futures-util = "0.3.31"
|
futures-util = "0.3.31"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ version = { workspace = true }
|
||||||
edition = { workspace = true }
|
edition = { workspace = true }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
async-trait = { workspace = true }
|
||||||
cookie = { workspace = true }
|
cookie = { workspace = true }
|
||||||
euphoxide = { path = "../euphoxide" }
|
euphoxide = { path = "../euphoxide" }
|
||||||
jiff = { workspace = true }
|
jiff = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,64 +1,76 @@
|
||||||
use std::sync::Arc;
|
use std::{fmt::Debug, sync::Arc};
|
||||||
|
|
||||||
use jiff::Timestamp;
|
use jiff::Timestamp;
|
||||||
|
use log::error;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
command::Commands,
|
||||||
instance::ServerConfig,
|
instance::ServerConfig,
|
||||||
instances::{Event, Instances, InstancesConfig},
|
instances::{Event, Instances, InstancesConfig},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct BotConfig<S> {
|
pub struct Bot<S = (), E = euphoxide::Error> {
|
||||||
pub server: ServerConfig,
|
|
||||||
pub instances: InstancesConfig,
|
|
||||||
pub state: S,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> BotConfig<S> {
|
|
||||||
pub fn with_state<S2>(self, state: S2) -> BotConfig<S2> {
|
|
||||||
BotConfig {
|
|
||||||
server: self.server,
|
|
||||||
instances: self.instances,
|
|
||||||
state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(self, event_tx: mpsc::Sender<Event>) -> Bot<S> {
|
|
||||||
Bot::new(self, event_tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for BotConfig<()> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
server: ServerConfig::default(),
|
|
||||||
instances: InstancesConfig::default(),
|
|
||||||
state: (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Bot<S> {
|
|
||||||
pub server_config: ServerConfig,
|
pub server_config: ServerConfig,
|
||||||
|
pub commands: Arc<Commands<S, E>>,
|
||||||
pub state: Arc<S>,
|
pub state: Arc<S>,
|
||||||
pub instances: Instances,
|
pub instances: Instances,
|
||||||
pub start_time: Timestamp,
|
pub start_time: Timestamp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> Bot<S> {
|
impl Bot {
|
||||||
pub fn new(config: BotConfig<S>, event_tx: mpsc::Sender<Event>) -> Self {
|
pub fn new_simple(commands: Commands, event_tx: mpsc::Sender<Event>) -> Self {
|
||||||
Self {
|
Self::new(
|
||||||
server_config: config.server,
|
ServerConfig::default(),
|
||||||
state: Arc::new(config.state),
|
InstancesConfig::default(),
|
||||||
instances: Instances::new(config.instances, event_tx),
|
commands,
|
||||||
start_time: Timestamp::now(),
|
(),
|
||||||
|
event_tx,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<S, E> Bot<S, E> {
|
||||||
|
pub fn new(
|
||||||
|
server_config: ServerConfig,
|
||||||
|
instances_config: InstancesConfig,
|
||||||
|
commands: Commands<S, E>,
|
||||||
|
state: S,
|
||||||
|
event_tx: mpsc::Sender<Event>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
server_config,
|
||||||
|
commands: Arc::new(commands),
|
||||||
|
state: Arc::new(state),
|
||||||
|
instances: Instances::new(instances_config, event_tx),
|
||||||
|
start_time: Timestamp::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<S, E> Bot<S, E>
|
||||||
|
where
|
||||||
|
S: Send + Sync + 'static,
|
||||||
|
E: Debug + 'static,
|
||||||
|
{
|
||||||
pub fn handle_event(&self, event: Event) {
|
pub fn handle_event(&self, event: Event) {
|
||||||
todo!()
|
let bot = self.clone();
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
if let Err(err) = bot.commands.on_event(event, &bot).await {
|
||||||
|
error!("while handling event: {err:#?}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> Clone for Bot<S, E> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
server_config: self.server_config.clone(),
|
||||||
|
commands: self.commands.clone(),
|
||||||
|
state: self.state.clone(),
|
||||||
|
instances: self.instances.clone(),
|
||||||
|
start_time: self.start_time,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
186
euphoxide-bot/src/command.rs
Normal file
186
euphoxide-bot/src/command.rs
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
pub mod bang;
|
||||||
|
pub mod basic;
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use euphoxide::{
|
||||||
|
api::{self, Data, Message, MessageId, SendEvent, SendReply},
|
||||||
|
client::{
|
||||||
|
conn::ClientConnHandle,
|
||||||
|
state::{Joined, State},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{bot::Bot, instance::Instance, instances::Event};
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub struct Context {
|
||||||
|
pub instance: Instance,
|
||||||
|
pub conn: ClientConnHandle,
|
||||||
|
pub joined: Joined,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
pub async fn send(
|
||||||
|
&self,
|
||||||
|
content: impl ToString,
|
||||||
|
) -> euphoxide::Result<impl Future<Output = euphoxide::Result<SendReply>>> {
|
||||||
|
self.conn
|
||||||
|
.send(api::Send {
|
||||||
|
content: content.to_string(),
|
||||||
|
parent: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn send_only(&self, content: impl ToString) -> euphoxide::Result<()> {
|
||||||
|
let _ignore = self.send(content).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reply(
|
||||||
|
&self,
|
||||||
|
parent: MessageId,
|
||||||
|
content: impl ToString,
|
||||||
|
) -> euphoxide::Result<impl Future<Output = euphoxide::Result<SendReply>>> {
|
||||||
|
self.conn
|
||||||
|
.send(api::Send {
|
||||||
|
content: content.to_string(),
|
||||||
|
parent: Some(parent),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn reply_only(
|
||||||
|
&self,
|
||||||
|
parent: MessageId,
|
||||||
|
content: impl ToString,
|
||||||
|
) -> euphoxide::Result<()> {
|
||||||
|
let _ignore = self.reply(parent, content).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Info {
|
||||||
|
pub trigger: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<S = (), E = euphoxide::Error> {
|
||||||
|
fn info(&self, ctx: &Context) -> Info {
|
||||||
|
Info::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
arg: &str,
|
||||||
|
msg: &Message,
|
||||||
|
ctx: &Context,
|
||||||
|
bot: &Bot<S, E>,
|
||||||
|
) -> Result<Propagate, E>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Commands<B = (), E = euphoxide::Error> {
|
||||||
|
commands: Vec<Box<dyn Command<B, E> + Sync + Send>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, E> Commands<S, E> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { commands: vec![] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, command: impl Command<S, E> + Sync + Send + 'static) {
|
||||||
|
self.commands.push(Box::new(command));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn then(mut self, command: impl Command<S, E> + Sync + Send + 'static) -> Self {
|
||||||
|
self.add(command);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn infos(&self, ctx: &Context) -> Vec<Info> {
|
||||||
|
self.commands.iter().map(|c| c.info(ctx)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn on_event(&self, event: Event, bot: &Bot<S, E>) -> Result<Propagate, E> {
|
||||||
|
let Event::Packet {
|
||||||
|
instance,
|
||||||
|
conn,
|
||||||
|
state,
|
||||||
|
packet,
|
||||||
|
} = event
|
||||||
|
else {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 {
|
||||||
|
instance,
|
||||||
|
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<S, E> Default for Commands<S, E> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
232
euphoxide-bot/src/command/bang.rs
Normal file
232
euphoxide-bot/src/command/bang.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
//! Euphoria-style `!foo` and `!foo @bar` command wrappers.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use euphoxide::{api::Message, nick};
|
||||||
|
|
||||||
|
use crate::bot::Bot;
|
||||||
|
|
||||||
|
use super::{Command, Context, Info, Propagate};
|
||||||
|
|
||||||
|
// TODO Don't ignore leading whitespace?
|
||||||
|
// I'm not entirely happy with how commands handle whitespace, and on euphoria,
|
||||||
|
// prefixing commands with whitespace is traditionally used to not trigger them.
|
||||||
|
|
||||||
|
/// Parse leading whitespace followed by an prefix-initiated command.
|
||||||
|
///
|
||||||
|
/// Returns the command name and the remaining text with one leading whitespace
|
||||||
|
/// removed. The remaining text may be the empty string.
|
||||||
|
pub fn parse_prefix_initiated<'a>(text: &'a str, prefix: &str) -> Option<(&'a str, &'a str)> {
|
||||||
|
let text = text.trim_start();
|
||||||
|
let text = text.strip_prefix(prefix)?;
|
||||||
|
let (name, rest) = text.split_once(char::is_whitespace).unwrap_or((text, ""));
|
||||||
|
if name.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((name, rest))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Global<C> {
|
||||||
|
prefix: String,
|
||||||
|
name: String,
|
||||||
|
inner: C,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Global<C> {
|
||||||
|
pub fn new<S: ToString>(name: S, inner: C) -> Self {
|
||||||
|
Self {
|
||||||
|
prefix: "!".to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_prefix<S: ToString>(mut self, prefix: S) -> Self {
|
||||||
|
self.prefix = prefix.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S, E, C> Command<S, E> for Global<C>
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
C: Command<S, E> + Sync,
|
||||||
|
{
|
||||||
|
fn info(&self, ctx: &Context) -> Info {
|
||||||
|
self.inner
|
||||||
|
.info(ctx)
|
||||||
|
.with_prepended_trigger(format!("{}{}", self.prefix, self.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
arg: &str,
|
||||||
|
msg: &Message,
|
||||||
|
ctx: &Context,
|
||||||
|
bot: &Bot<S, E>,
|
||||||
|
) -> Result<Propagate, E> {
|
||||||
|
let Some((name, rest)) = parse_prefix_initiated(arg, &self.prefix) else {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
};
|
||||||
|
|
||||||
|
if name != self.name {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inner.execute(rest, msg, ctx, bot).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct General<C> {
|
||||||
|
prefix: String,
|
||||||
|
name: String,
|
||||||
|
inner: C,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> General<C> {
|
||||||
|
pub fn new<S: ToString>(name: S, inner: C) -> Self {
|
||||||
|
Self {
|
||||||
|
prefix: "!".to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_prefix<S: ToString>(mut self, prefix: S) -> Self {
|
||||||
|
self.prefix = prefix.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S, E, C> Command<S, E> for General<C>
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
C: Command<S, E> + Sync,
|
||||||
|
{
|
||||||
|
fn info(&self, ctx: &Context) -> Info {
|
||||||
|
self.inner
|
||||||
|
.info(ctx)
|
||||||
|
.with_prepended_trigger(format!("{}{}", self.prefix, self.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
arg: &str,
|
||||||
|
msg: &Message,
|
||||||
|
ctx: &Context,
|
||||||
|
bot: &Bot<S, E>,
|
||||||
|
) -> Result<Propagate, E> {
|
||||||
|
let Some((name, rest)) = parse_prefix_initiated(arg, &self.prefix) else {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
};
|
||||||
|
|
||||||
|
if name != self.name {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if parse_prefix_initiated(rest, "@").is_some() {
|
||||||
|
// The command looks like a specific command. If we treated it like
|
||||||
|
// a general command match, we would interpret other bots' specific
|
||||||
|
// commands as general commands.
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inner.execute(rest, msg, ctx, bot).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Specific<C> {
|
||||||
|
prefix: String,
|
||||||
|
name: String,
|
||||||
|
inner: C,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Specific<C> {
|
||||||
|
pub fn new<S: ToString>(name: S, inner: C) -> Self {
|
||||||
|
Self {
|
||||||
|
prefix: "!".to_string(),
|
||||||
|
name: name.to_string(),
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_prefix<S: ToString>(mut self, prefix: S) -> Self {
|
||||||
|
self.prefix = prefix.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S, E, C> Command<S, E> for Specific<C>
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
C: Command<S, E> + Sync,
|
||||||
|
{
|
||||||
|
fn info(&self, ctx: &Context) -> Info {
|
||||||
|
let nick = nick::mention(&ctx.joined.session.name);
|
||||||
|
self.inner
|
||||||
|
.info(ctx)
|
||||||
|
.with_prepended_trigger(format!("{}{} @{nick}", self.prefix, self.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
arg: &str,
|
||||||
|
msg: &Message,
|
||||||
|
ctx: &Context,
|
||||||
|
bot: &Bot<S, E>,
|
||||||
|
) -> Result<Propagate, E> {
|
||||||
|
let Some((name, rest)) = parse_prefix_initiated(arg, &self.prefix) else {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
};
|
||||||
|
|
||||||
|
if name != self.name {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((nick, rest)) = parse_prefix_initiated(rest, "@") else {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
};
|
||||||
|
|
||||||
|
if nick::normalize(nick) != nick::normalize(&ctx.joined.session.name) {
|
||||||
|
return Ok(Propagate::Yes);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.inner.execute(rest, msg, ctx, bot).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::parse_prefix_initiated;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_prefixed() {
|
||||||
|
assert_eq!(parse_prefix_initiated("!foo", "!"), Some(("foo", "")));
|
||||||
|
assert_eq!(parse_prefix_initiated(" !foo", "!"), Some(("foo", "")));
|
||||||
|
assert_eq!(
|
||||||
|
parse_prefix_initiated("!foo ", "!"),
|
||||||
|
Some(("foo", " "))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_prefix_initiated(" !foo ", "!"),
|
||||||
|
Some(("foo", " "))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_prefix_initiated("!foo @bar", "!"),
|
||||||
|
Some(("foo", "@bar"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_prefix_initiated("!foo @bar", "!"),
|
||||||
|
Some(("foo", " @bar"))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
parse_prefix_initiated("!foo @bar ", "!"),
|
||||||
|
Some(("foo", "@bar "))
|
||||||
|
);
|
||||||
|
assert_eq!(parse_prefix_initiated("! foo @bar", "!"), None);
|
||||||
|
assert_eq!(parse_prefix_initiated("!", "!"), None);
|
||||||
|
assert_eq!(parse_prefix_initiated("?foo", "!"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
115
euphoxide-bot/src/command/basic.rs
Normal file
115
euphoxide-bot/src/command/basic.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
//! Basic command wrappers.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use euphoxide::api::Message;
|
||||||
|
|
||||||
|
use crate::bot::Bot;
|
||||||
|
|
||||||
|
use super::{Command, Context, Info, Propagate};
|
||||||
|
|
||||||
|
/// Rewrite or hide command info.
|
||||||
|
pub struct Described<C> {
|
||||||
|
pub inner: C,
|
||||||
|
pub trigger: Option<Option<String>>,
|
||||||
|
pub description: Option<Option<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Described<C> {
|
||||||
|
pub fn new(inner: C) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
trigger: None,
|
||||||
|
description: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hidden(inner: C) -> Self {
|
||||||
|
Self::new(inner)
|
||||||
|
.with_trigger_hidden()
|
||||||
|
.with_description_hidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_trigger(mut self, trigger: impl ToString) -> Self {
|
||||||
|
self.trigger = Some(Some(trigger.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_trigger_hidden(mut self) -> Self {
|
||||||
|
self.trigger = Some(None);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_description(mut self, description: impl ToString) -> Self {
|
||||||
|
self.description = Some(Some(description.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_description_hidden(mut self) -> Self {
|
||||||
|
self.description = Some(None);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S, E, C> Command<S, E> for Described<C>
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
C: Command<S, E> + Sync,
|
||||||
|
{
|
||||||
|
fn info(&self, ctx: &Context) -> Info {
|
||||||
|
let info = self.inner.info(ctx);
|
||||||
|
Info {
|
||||||
|
trigger: self.trigger.clone().unwrap_or(info.trigger),
|
||||||
|
description: self.description.clone().unwrap_or(info.description),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
arg: &str,
|
||||||
|
msg: &Message,
|
||||||
|
ctx: &Context,
|
||||||
|
bot: &Bot<S, E>,
|
||||||
|
) -> Result<Propagate, E> {
|
||||||
|
self.inner.execute(arg, msg, ctx, bot).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Prefixed<C> {
|
||||||
|
prefix: String,
|
||||||
|
inner: C,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Prefixed<C> {
|
||||||
|
pub fn new<S: ToString>(prefix: S, inner: C) -> Self {
|
||||||
|
Self {
|
||||||
|
prefix: prefix.to_string(),
|
||||||
|
inner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<S, E, C> Command<S, E> for Prefixed<C>
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
C: Command<S, E> + Sync,
|
||||||
|
{
|
||||||
|
fn info(&self, ctx: &Context) -> Info {
|
||||||
|
self.inner.info(ctx).with_prepended_trigger(&self.prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
arg: &str,
|
||||||
|
msg: &Message,
|
||||||
|
ctx: &Context,
|
||||||
|
bot: &Bot<S, E>,
|
||||||
|
) -> Result<Propagate, E> {
|
||||||
|
if let Some(rest) = arg.trim_start().strip_prefix(&self.prefix) {
|
||||||
|
self.inner.execute(rest, msg, ctx, bot).await
|
||||||
|
} else {
|
||||||
|
Ok(Propagate::Yes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod bot;
|
pub mod bot;
|
||||||
|
pub mod command;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
pub mod instances;
|
pub mod instances;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue