1use crate::ctx::Ctx;
9use crate::extract::{Extracted, FromContext, Reject};
10use crate::matcher::ParsedCommand;
11use async_trait::async_trait;
12use nagisa_types::id::{MessageId, Uin};
13use nagisa_types::resource::Media;
14use nagisa_types::segment::Segment;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum ArgError {
19 Missing(&'static str),
21 Parse { field: &'static str, value: String, expected: &'static str },
23}
24
25impl std::fmt::Display for ArgError {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 match self {
28 ArgError::Missing(field) => write!(f, "missing required argument `{field}`"),
29 ArgError::Parse { field, value, expected } => {
30 write!(f, "argument `{field}`: cannot parse {value:?} as {expected}")
31 }
32 }
33 }
34}
35impl std::error::Error for ArgError {}
36
37pub trait FromArg: Sized {
39 const TYPE_NAME: &'static str;
41 fn from_arg(s: &str) -> Option<Self>;
42}
43
44macro_rules! from_arg_via_fromstr {
45 ($($t:ty => $name:literal),* $(,)?) => {$(
46 impl FromArg for $t {
47 const TYPE_NAME: &'static str = $name;
48 fn from_arg(s: &str) -> Option<Self> { s.parse().ok() }
49 }
50 )*};
51}
52from_arg_via_fromstr! {
53 String => "string", i64 => "int", i32 => "int", i16 => "int", i8 => "int",
54 u64 => "uint", u32 => "uint", u16 => "uint", u8 => "uint",
55 f64 => "number", bool => "bool",
56}
57
58impl FromArg for Uin {
59 const TYPE_NAME: &'static str = "uin";
60 fn from_arg(s: &str) -> Option<Self> {
61 s.trim_start_matches('@').parse::<i64>().ok().map(Uin)
63 }
64}
65
66#[derive(Clone, Copy, Debug)]
68pub enum ArgToken<'a> {
69 Word(&'a str),
70 Element(&'a Segment),
71}
72
73pub fn tokenize_segments(args: &[Segment]) -> Vec<ArgToken<'_>> {
75 let mut out = Vec::new();
76 for seg in args {
77 match seg {
78 Segment::Text(t) => out.extend(t.split_whitespace().map(ArgToken::Word)),
79 other => out.push(ArgToken::Element(other)),
80 }
81 }
82 out
83}
84
85pub fn seg_as_image(seg: &Segment) -> Option<Media> {
87 match seg {
88 Segment::Image { res, .. } => Some(res.clone()),
89 _ => None,
90 }
91}
92pub fn seg_as_record(seg: &Segment) -> Option<Media> {
93 match seg {
94 Segment::Record { res, .. } => Some(res.clone()),
95 _ => None,
96 }
97}
98pub fn seg_as_video(seg: &Segment) -> Option<Media> {
99 match seg {
100 Segment::Video { res, .. } => Some(res.clone()),
101 _ => None,
102 }
103}
104pub fn seg_as_at(seg: &Segment) -> Option<Uin> {
105 match seg {
106 Segment::Mention { user, .. } => Some(*user),
107 _ => None,
108 }
109}
110pub fn seg_as_reply(seg: &Segment) -> Option<MessageId> {
111 match seg {
112 Segment::Reply { id, .. } => Some(id.clone()),
113 _ => None,
114 }
115}
116pub fn seg_as_face(seg: &Segment) -> Option<String> {
117 match seg {
118 Segment::Face { id, .. } => Some(id.clone()),
119 _ => None,
120 }
121}
122
123pub fn skip_words(text: &str, k: usize) -> String {
126 let mut rest = text.trim_start();
127 for _ in 0..k {
128 match rest.find(char::is_whitespace) {
129 Some(pos) => rest = rest[pos..].trim_start(),
130 None => {
131 rest = "";
132 break;
133 }
134 }
135 }
136 rest.to_string()
137}
138
139pub trait ParseArgs: Sized {
141 fn parse_args(tokens: &[ArgToken<'_>], raw_text: &str) -> std::result::Result<Self, ArgError>;
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146pub enum ArgKind {
147 Flag,
149 Opt,
151 Positional,
153 Rest,
155 AtOrId,
157 Element,
159}
160
161#[derive(Clone, Copy, Debug)]
163pub struct ArgSpec {
164 pub name: &'static str,
166 pub kind: ArgKind,
168 pub short: &'static str,
170 pub long: &'static str,
172 pub required: bool,
174 pub default: &'static str,
176 pub desc: &'static str,
178}
179
180pub trait ArgsMeta {
183 const SPECS: &'static [ArgSpec];
185}
186
187pub struct Args<T>(pub T);
190
191#[async_trait]
192impl<T: ParseArgs + Send> FromContext for Args<T> {
193 async fn from_context(ctx: &Ctx) -> Extracted<Self> {
194 let parsed = ctx.get_ext::<ParsedCommand>().ok_or(Reject::Skip)?;
195 let tokens = tokenize_segments(&parsed.args);
196 match T::parse_args(&tokens, &parsed.args_text) {
198 Ok(v) => Ok(Args(v)),
199 Err(e) => {
200 let usage = ctx.get_ext::<crate::matcher::CommandUsage>().map(|crate::matcher::CommandUsage(u)| u);
203 on_parse_miss(ctx, &parsed.command, &e, usage.as_deref()).await;
204 Err(Reject::Skip)
205 }
206 }
207 }
208}
209
210pub(crate) async fn on_parse_miss(ctx: &Ctx, command: &str, err: &ArgError, usage: Option<&str>) {
218 if let Some(usage) = usage {
220 tracing::debug!(command = %command, error = %err, "parse failed; replying explicit usage");
221 if let Some(m) = ctx.message() {
222 let _ = ctx.bot().send(&m.peer, &[Segment::text(usage)]).await;
223 }
224 return;
225 }
226 if ctx.is_dev() {
228 let hint = usage_hint(command, err);
229 tracing::warn!(
230 command = %command,
231 error = %err,
232 "[dev] parse failed; skipping handler — {hint}"
233 );
234 if let Some(m) = ctx.message() {
235 let _ = ctx.bot().send(&m.peer, &[Segment::text(hint)]).await;
236 }
237 } else {
238 tracing::debug!(error = %err, "parse failed; skipping handler");
239 }
240}
241
242fn usage_hint(command: &str, err: &ArgError) -> String {
246 format!("用法错误: 命令 `{command}` — {err}")
247}