Add command helpers
This commit is contained in:
parent
8a7d414a01
commit
48f75a1efd
4 changed files with 332 additions and 0 deletions
|
|
@ -1,3 +1,7 @@
|
||||||
|
mod bang;
|
||||||
|
mod hidden;
|
||||||
|
mod prefixed;
|
||||||
|
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
|
@ -9,6 +13,8 @@ use euphoxide::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use self::{bang::*, hidden::*, prefixed::*};
|
||||||
|
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
pub conn: ClientConnHandle,
|
pub conn: ClientConnHandle,
|
||||||
|
|
|
||||||
228
euphoxide-bot/src/command/bang.rs
Normal file
228
euphoxide-bot/src/command/bang.rs
Normal file
|
|
@ -0,0 +1,228 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use euphoxide::{api::Message, nick};
|
||||||
|
|
||||||
|
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<B, E, C> Command<B, E> for Global<C>
|
||||||
|
where
|
||||||
|
B: Send,
|
||||||
|
C: Command<B, E> + Send + 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: &mut B,
|
||||||
|
) -> 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<B, E, C> Command<B, E> for General<C>
|
||||||
|
where
|
||||||
|
B: Send,
|
||||||
|
C: Command<B, E> + Send + 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: &mut B,
|
||||||
|
) -> 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<B, E, C> Command<B, E> for Specific<C>
|
||||||
|
where
|
||||||
|
B: Send,
|
||||||
|
C: Command<B, E> + Send + 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: &mut B,
|
||||||
|
) -> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
euphoxide-bot/src/command/hidden.rs
Normal file
55
euphoxide-bot/src/command/hidden.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use euphoxide::api::Message;
|
||||||
|
|
||||||
|
use super::{Command, Context, Info, Propagate};
|
||||||
|
|
||||||
|
pub struct Hidden<C> {
|
||||||
|
pub inner: C,
|
||||||
|
pub allow_trigger: bool,
|
||||||
|
pub allow_description: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> Hidden<C> {
|
||||||
|
pub fn new(inner: C) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
allow_trigger: false,
|
||||||
|
allow_description: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_allow_trigger(mut self, allow: bool) -> Self {
|
||||||
|
self.allow_trigger = allow;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_allow_description(mut self, allow: bool) -> Self {
|
||||||
|
self.allow_description = allow;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<B, E, C> Command<B, E> for Hidden<C>
|
||||||
|
where
|
||||||
|
B: Send,
|
||||||
|
C: Command<B, E> + Send + Sync,
|
||||||
|
{
|
||||||
|
fn info(&self, ctx: &Context) -> Info {
|
||||||
|
let info = self.inner.info(ctx);
|
||||||
|
Info {
|
||||||
|
trigger: info.trigger.filter(|_| self.allow_trigger),
|
||||||
|
description: info.description.filter(|_| self.allow_description),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute(
|
||||||
|
&self,
|
||||||
|
arg: &str,
|
||||||
|
msg: &Message,
|
||||||
|
ctx: &Context,
|
||||||
|
bot: &mut B,
|
||||||
|
) -> Result<Propagate, E> {
|
||||||
|
self.inner.execute(arg, msg, ctx, bot).await
|
||||||
|
}
|
||||||
|
}
|
||||||
43
euphoxide-bot/src/command/prefixed.rs
Normal file
43
euphoxide-bot/src/command/prefixed.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use euphoxide::api::Message;
|
||||||
|
|
||||||
|
use super::{Command, Context, Info, Propagate};
|
||||||
|
|
||||||
|
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<B, E, C> Command<B, E> for Prefixed<C>
|
||||||
|
where
|
||||||
|
B: Send,
|
||||||
|
C: Command<B, E> + Send + 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: &mut B,
|
||||||
|
) -> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue