use crate::ctx::Ctx;
use crate::extract::{Extracted, FromContext, Reject};
use crate::matcher::ParsedCommand;
use async_trait::async_trait;
use nagisa_types::id::{MessageId, Uin};
use nagisa_types::resource::Media;
use nagisa_types::segment::Segment;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ArgError {
Missing(&'static str),
Parse { field: &'static str, value: String, expected: &'static str },
}
impl std::fmt::Display for ArgError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArgError::Missing(field) => write!(f, "missing required argument `{field}`"),
ArgError::Parse { field, value, expected } => {
write!(f, "argument `{field}`: cannot parse {value:?} as {expected}")
}
}
}
}
impl std::error::Error for ArgError {}
pub trait FromArg: Sized {
const TYPE_NAME: &'static str;
fn from_arg(s: &str) -> Option<Self>;
}
macro_rules! from_arg_via_fromstr {
($($t:ty => $name:literal),* $(,)?) => {$(
impl FromArg for $t {
const TYPE_NAME: &'static str = $name;
fn from_arg(s: &str) -> Option<Self> { s.parse().ok() }
}
)*};
}
from_arg_via_fromstr! {
String => "string", i64 => "int", i32 => "int", i16 => "int", i8 => "int",
u64 => "uint", u32 => "uint", u16 => "uint", u8 => "uint",
f64 => "number", bool => "bool",
}
impl FromArg for Uin {
const TYPE_NAME: &'static str = "uin";
fn from_arg(s: &str) -> Option<Self> {
s.trim_start_matches('@').parse::<i64>().ok().map(Uin)
}
}
#[derive(Clone, Copy, Debug)]
pub enum ArgToken<'a> {
Word(&'a str),
Element(&'a Segment),
}
pub fn tokenize_segments(args: &[Segment]) -> Vec<ArgToken<'_>> {
let mut out = Vec::new();
for seg in args {
match seg {
Segment::Text(t) => out.extend(t.split_whitespace().map(ArgToken::Word)),
other => out.push(ArgToken::Element(other)),
}
}
out
}
pub fn seg_as_image(seg: &Segment) -> Option<Media> {
match seg {
Segment::Image { res, .. } => Some(res.clone()),
_ => None,
}
}
pub fn seg_as_record(seg: &Segment) -> Option<Media> {
match seg {
Segment::Record { res, .. } => Some(res.clone()),
_ => None,
}
}
pub fn seg_as_video(seg: &Segment) -> Option<Media> {
match seg {
Segment::Video { res, .. } => Some(res.clone()),
_ => None,
}
}
pub fn seg_as_at(seg: &Segment) -> Option<Uin> {
match seg {
Segment::Mention { user, .. } => Some(*user),
_ => None,
}
}
pub fn seg_as_reply(seg: &Segment) -> Option<MessageId> {
match seg {
Segment::Reply { id, .. } => Some(id.clone()),
_ => None,
}
}
pub fn seg_as_face(seg: &Segment) -> Option<String> {
match seg {
Segment::Face { id, .. } => Some(id.clone()),
_ => None,
}
}
pub fn skip_words(text: &str, k: usize) -> String {
let mut rest = text.trim_start();
for _ in 0..k {
match rest.find(char::is_whitespace) {
Some(pos) => rest = rest[pos..].trim_start(),
None => {
rest = "";
break;
}
}
}
rest.to_string()
}
pub trait ParseArgs: Sized {
fn parse_args(tokens: &[ArgToken<'_>], raw_text: &str)
-> std::result::Result<Self, ArgError>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArgKind {
Flag,
Opt,
Positional,
Rest,
AtOrId,
Element,
}
#[derive(Clone, Copy, Debug)]
pub struct ArgSpec {
pub name: &'static str,
pub kind: ArgKind,
pub short: &'static str,
pub long: &'static str,
pub required: bool,
pub default: &'static str,
pub desc: &'static str,
}
pub trait ArgsMeta {
const SPECS: &'static [ArgSpec];
}
pub struct Args<T>(pub T);
#[async_trait]
impl<T: ParseArgs + Send> FromContext for Args<T> {
async fn from_context(ctx: &Ctx) -> Extracted<Self> {
let parsed = ctx.get_ext::<ParsedCommand>().ok_or(Reject::Skip)?;
let tokens = tokenize_segments(&parsed.args);
match T::parse_args(&tokens, &parsed.args_text) {
Ok(v) => Ok(Args(v)),
Err(e) => {
let usage =
ctx.get_ext::<crate::matcher::CommandUsage>().map(|crate::matcher::CommandUsage(u)| u);
on_parse_miss(ctx, &parsed.command, &e, usage.as_deref()).await;
Err(Reject::Skip)
}
}
}
}
pub(crate) async fn on_parse_miss(ctx: &Ctx, command: &str, err: &ArgError, usage: Option<&str>) {
if let Some(usage) = usage {
tracing::debug!(command = %command, error = %err, "parse failed; replying explicit usage");
if let Some(m) = ctx.message() {
let _ = ctx.bot().send(&m.peer, &[Segment::text(usage)]).await;
}
return;
}
if ctx.is_dev() {
let hint = usage_hint(command, err);
tracing::warn!(
command = %command,
error = %err,
"[dev] parse failed; skipping handler — {hint}"
);
if let Some(m) = ctx.message() {
let _ = ctx.bot().send(&m.peer, &[Segment::text(hint)]).await;
}
} else {
tracing::debug!(error = %err, "parse failed; skipping handler");
}
}
fn usage_hint(command: &str, err: &ArgError) -> String {
format!("用法错误: 命令 `{command}` — {err}")
}