From fa5bc18721fe36f2f0cae5103f17d6faf361db3d Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 29 Apr 2023 08:42:31 +0000 Subject: [PATCH] mx-sanebot: add command to search for torrents --- pkgs/mx-sanebot/src/msg_handler.rs | 142 +++++++++++++++++++++++++++-- pkgs/mx-sanebot/src/parsing.rs | 80 ++++++++++++++++ 2 files changed, 214 insertions(+), 8 deletions(-) diff --git a/pkgs/mx-sanebot/src/msg_handler.rs b/pkgs/mx-sanebot/src/msg_handler.rs index 4c69d7f4..c2a3b124 100644 --- a/pkgs/mx-sanebot/src/msg_handler.rs +++ b/pkgs/mx-sanebot/src/msg_handler.rs @@ -10,21 +10,107 @@ mod tt { pub(super) use super::parsing::{ Either, Lit, + Maybe, + Nul, + OneOrMore, Then, }; - use crate::ilit; + use crate::{ilit, lit}; // grammar: - // REQUEST = (HELP | BT | BT-ADD) + // REQUEST = (HELP | BT-REQ) + // // HELP = - // BT = - // BT-ADD = MAYBE_ARGS + // BT-REQ = [(BT-SEARCH | BT-ADD)] + // + // BT-SEARCH = ARGS + // BT-ADD = ARGS + // // MAYBE_ARGS = [SPACE [ARGS]] // ARGS = ARG MAYBE_ARGS - // ARG = (not SPACE) [ARG] - pub(super) type Request = Then>; + // ARG = NOT-SPACE [ARG] + // + // NOT-SPACE = (ALPHA | NUM | SPECIAL) + pub(super) type Request = Then>; pub(super) type Bang = Lit<{ '!' as u8 }>; + pub(super) type Space = Lit<{ ' ' as u8 }>; + pub(super) type Alpha = Either< + ilit!('A'), Either< + ilit!('B'), Either< + ilit!('C'), Either< + ilit!('D'), Either< + ilit!('E'), Either< + ilit!('F'), Either< + ilit!('G'), Either< + ilit!('H'), Either< + ilit!('I'), Either< + ilit!('J'), Either< + ilit!('K'), Either< + ilit!('L'), Either< + ilit!('M'), Either< + ilit!('N'), Either< + ilit!('O'), Either< + ilit!('P'), Either< + ilit!('Q'), Either< + ilit!('R'), Either< + ilit!('S'), Either< + ilit!('T'), Either< + ilit!('U'), Either< + ilit!('V'), Either< + ilit!('W'), Either< + ilit!('X'), Either< + ilit!('Y'), + ilit!('Z'), + >>>>>>>>>>>>>>>>>>>>>>>>>; + pub(super) type Dig = Either< + lit!('0'), Either< + lit!('1'), Either< + lit!('2'), Either< + lit!('3'), Either< + lit!('4'), Either< + lit!('5'), Either< + lit!('6'), Either< + lit!('7'), Either< + lit!('8'), + lit!('9'), + >>>>>>>>>; + pub(super) type Symbol = Either< + Bang, Either< + lit!('@'), Either< + lit!('#'), Either< + lit!('$'), Either< + lit!('%'), Either< + lit!('^'), Either< + lit!('&'), Either< + lit!('*'), Either< + lit!('('), Either< + lit!(')'), Either< + lit!('-'), Either< + lit!('_'), Either< + lit!('+'), Either< + lit!('='), Either< + lit!('['), Either< + lit!('{'), Either< + lit!(']'), Either< + lit!('}'), Either< + lit!('\\'), Either< + lit!('|'), Either< + lit!(';'), Either< + lit!(':'), Either< + lit!('\''), Either< + lit!('"'), Either< + lit!(','), Either< + lit!('<'), Either< + lit!('.'), Either< + lit!('>'), Either< + lit!('/'), Either< + lit!('?'), Either< + lit!('`'), + lit!('~'), + >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>; + // pub(super) type Whitespace = Then>>; + pub(super) type Whitespace = OneOrMore; pub(super) type Help = Then< ilit!('H'), Then< @@ -33,10 +119,30 @@ mod tt { ilit!('P'), >>>; + pub(super) type BtReq = Then>>; + pub(super) type Bt = Then< ilit!('B'), ilit!('T'), >; + + pub(super) type BtSearch = Then>; + + pub(super) type Search = Then< + ilit!('S'), Then< + ilit!('E'), Then< + ilit!('A'), Then< + ilit!('R'), Then< + ilit!('C'), + ilit!('H'), + >>>>>; + + pub(super) type SpacedArg = Then; + pub(super) type Arg = OneOrMore>>; } pub struct MessageHandler; @@ -63,13 +169,20 @@ impl MessageHandler { enum Request { Help, Bt, + BtSearch(String), } impl From for Request { fn from(t: tt::Request) -> Self { match t { tt::Then(_bang, tt::Either::A(_help)) => Self::Help, - tt::Then(_bang, tt::Either::B(_bt)) => Self::Bt, + tt::Then(_bang, tt::Either::B(bt_req)) => match bt_req { + tt::Then(_bt, tt::Either::A(tt::Then(_ws, bt_search))) => { + let tt::Then(_, spaced_args) = bt_search; + Self::BtSearch(spaced_args.to_string()) + }, + tt::Then(_bt, tt::Either::B(tt::Nul)) => Self::Bt, + } } } } @@ -87,6 +200,16 @@ impl Request { ).unwrap_or_else(|| "failed to retrieve torrent status".to_owned()) ), + Request::BtSearch(phrase) => Response::BtSearch( + process::Command::new("sane-bt-search") + .args([&*phrase]) + .output() + .ok() + .and_then(|output| + str::from_utf8(&output.stdout).ok().map(ToOwned::to_owned) + ).unwrap_or_else(|| + "failed to complete torrent search".to_owned()) + ), } } } @@ -94,6 +217,7 @@ impl Request { enum Response { Help, Bt(String), + BtSearch(String), } impl fmt::Display for Response { @@ -103,8 +227,10 @@ impl fmt::Display for Response { write!(f, "commands:\n")?; write!(f, " !help => show this message\n")?; write!(f, " !bt => show torrent statuses\n")?; + write!(f, " !bt search => search for torrents\n")?; }, - Response::Bt(stdout) => write!(f, "{}", stdout)? + Response::Bt(stdout) => write!(f, "{}", stdout)?, + Response::BtSearch(stdout) => write!(f, "{}", stdout)?, } Ok(()) } diff --git a/pkgs/mx-sanebot/src/parsing.rs b/pkgs/mx-sanebot/src/parsing.rs index 307ecb36..af289603 100644 --- a/pkgs/mx-sanebot/src/parsing.rs +++ b/pkgs/mx-sanebot/src/parsing.rs @@ -1,3 +1,5 @@ +use std::fmt; + /// for internal use. /// parses only if the parser has no more bytes to yield. struct Eof; @@ -5,6 +7,10 @@ struct Eof; /// literal byte (character). pub struct Lit; +/// parses without consuming any bytes from the parser. +/// used to construct strictly optional constructs. +pub struct Nul; + /// the two-item sequence of A followed by B. pub struct Then(pub A, pub B); @@ -14,6 +20,23 @@ pub enum Either { B(B), } +/// parse A if possible, but don't error if it isn't present. +pub type Maybe = Either; + +/// exists because Rust doesn't allow recursive type *aliases*. +pub struct OneOrMore(Then< + A, + Maybe>> +>); + +// case-sensitive u8 character. +#[macro_export] +macro_rules! lit { + ($BYTE:literal) => { + Lit<{ $BYTE as u8 }> + } +} + // case-insensitive u8 character. #[macro_export] macro_rules! ilit { @@ -72,6 +95,18 @@ impl Parse for Eof { } } +impl Parse for Nul { + fn consume(p: P) -> PResult { + Ok((Self, p)) + } +} + +impl fmt::Display for Nul { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "") + } +} + impl Parse for Lit { fn consume(p: P) -> PResult { let (_, p) = p.expect_byte(Some(BYTE))?; @@ -79,6 +114,12 @@ impl Parse for Lit { } } +impl fmt::Display for Lit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", BYTE as char) + } +} + impl Parse for Then { fn consume(p: P) -> PResult { let (a, p) = p.expect()?; @@ -87,6 +128,12 @@ impl Parse for Then { } } +impl fmt::Display for Then { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", self.0, self.1) + } +} + impl Parse for Either { fn consume(p: P) -> PResult { let p = match p.expect() { @@ -101,3 +148,36 @@ impl Parse for Either { } } +impl fmt::Display for Either { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::A(a) => write!(f, "{}", a), + Self::B(b) => write!(f, "{}", b), + } + } +} + +impl Parse for Box { + fn consume(p: P) -> PResult { + match T::consume(p) { + Ok((t, p)) => Ok((Box::new(t), p)), + Err(p) => Err(p), + } + } +} + +impl Parse for OneOrMore { + fn consume(p: P) -> PResult { + match p.expect() { + Ok((t, p)) => Ok((Self(t), p)), + Err(p) => Err(p), + } + } +} + +impl fmt::Display for OneOrMore { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} +