Skip to main content

nagisa_core/
extract.rs

1//! `FromContext` 提取器(路1 核心)。Handler 的每个参数都按类型从 `Ctx` 注入。
2//!
3//! `Reject::Skip` = 本 handler 不适用(类型/场景没中),dispatch 继续传播;
4//! `Reject::Error` = 提取出错(如缺失依赖),记日志、不触发本 handler。
5use crate::bot::Bot;
6use crate::ctx::{Ctx, StateMap};
7use crate::matcher::ParsedCommand;
8use async_trait::async_trait;
9use nagisa_types::event::{MessageEvent, Meta, Notice, Request};
10use nagisa_types::prelude::*;
11use nagisa_types::segment::Segment;
12use std::any::{type_name, TypeId};
13use std::sync::Arc;
14
15/// 提取失败的两种语义。
16#[derive(Debug)]
17pub enum Reject {
18    /// 本 handler 不适用(匹配器/类型过滤没中);dispatch 跳过它、继续。
19    Skip,
20    /// 提取真正出错(如缺失 `State`);记日志,不触发本 handler。
21    Error(Error),
22}
23
24/// 提取结果:成功得 `Self`,失败为 `Reject`(注意区别于 `nagisa_types::Result`)。
25pub type Extracted<T> = std::result::Result<T, Reject>;
26
27/// 让 `Extracted<()>`(即 `Result<(), Reject>`)能用 `?` 直接落进 handler 体——
28/// handler 返回 [`HandlerResult`](crate::handler::HandlerResult) = `Result<(), nagisa_types::Error>`,
29/// 故 `?` 需要 `Reject -> Error` 的转换(招牌写法
30/// `async fn h(cd: Cd, ..) { cd.gate(format!("bili:{aid}"), D60)?; reduce_gold().await?; .. }`
31/// 要求 `cd.gate(..)?` 与 `reduce_gold().await?` 在同一函数体里共用同一错误类型)。
32///
33/// 映射:
34/// - `Reject::Error(e)` → 原样的业务错误 `e`(提取真出错,照常上报)。
35/// - `Reject::Skip` → 一个 `BadParams` 分类、哨兵 retcode 的 `Error`:在 handler 体里
36///   `?`-早退即“本次不处理”(如冷却中、数据派生键命中冷却);dispatch 会把它记一条
37///   handler WARN 并继续传播。这是 handler **体内**的早退语义,区别于提取阶段
38///   `Skip` 的“静默跳过本 handler”——进了体内 handler 已在运行,没有再“跳过”的位置,
39///   故落为一次明确的、可见的早退。
40///
41/// 孤儿规则:源类型 [`Reject`] 是本 crate 本地类型,故可为外部 `Error` 实现 `From`。
42impl From<Reject> for Error {
43    fn from(r: Reject) -> Self {
44        match r {
45            Reject::Error(e) => e,
46            Reject::Skip => Error::action_kind(
47                ActionErrorKind::BadParams,
48                "handler skipped (Reject::Skip propagated via `?`, e.g. cooldown hit)",
49            ),
50        }
51    }
52}
53
54/// 从每事件上下文按类型提取一个值。`Skip` 即类型层过滤。
55#[async_trait]
56pub trait FromContext: Sized {
57    async fn from_context(ctx: &Ctx) -> Extracted<Self>;
58}
59
60#[async_trait]
61impl FromContext for Bot {
62    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
63        Ok(ctx.bot().clone())
64    }
65}
66
67/// 可选提取:内层 `Skip`(类型/场景没中)→ `None`,于是本 handler 仍会运行;
68/// 内层 `Error`(真出错)继续向上传播。把"否则整条 handler 被跳过"变成"该项可选"。
69#[async_trait]
70impl<T: FromContext + Send> FromContext for Option<T> {
71    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
72        match T::from_context(ctx).await {
73            Ok(v) => Ok(Some(v)),
74            Err(Reject::Skip) => Ok(None),
75            Err(Reject::Error(e)) => Err(Reject::Error(e)),
76        }
77    }
78}
79
80#[async_trait]
81impl FromContext for Arc<Event> {
82    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
83        Ok(Arc::clone(ctx.event()))
84    }
85}
86
87#[async_trait]
88impl FromContext for MessageEvent {
89    /// 非消息事件 → `Skip`(类型层过滤)。命中则克隆内层 `MessageEvent`。
90    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
91        ctx.message().cloned().ok_or(Reject::Skip)
92    }
93}
94
95/// 群消息提取器:仅当事件是 `Scene::Group` 的消息时命中,否则 `Skip`。
96pub struct GroupMessage(pub MessageEvent);
97
98#[async_trait]
99impl FromContext for GroupMessage {
100    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
101        match ctx.message() {
102            Some(m) if m.peer.scene == Scene::Group => Ok(GroupMessage(m.clone())),
103            _ => Err(Reject::Skip),
104        }
105    }
106}
107
108/// 私聊消息提取器:仅当事件是 `Scene::Friend`/`Scene::Temp` 的消息时命中,否则 `Skip`。
109pub struct PrivateMessage(pub MessageEvent);
110
111#[async_trait]
112impl FromContext for PrivateMessage {
113    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
114        match ctx.message() {
115            Some(m) if matches!(m.peer.scene, Scene::Friend | Scene::Temp) => Ok(PrivateMessage(m.clone())),
116            _ => Err(Reject::Skip),
117        }
118    }
119}
120
121/// 发送者 QQ 号(仅消息事件,否则 `Skip`)。
122pub struct Sender(pub Uin);
123
124#[async_trait]
125impl FromContext for Sender {
126    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
127        ctx.message().map(|m| Sender(m.sender)).ok_or(Reject::Skip)
128    }
129}
130
131/// 消息事件的对端寻址(仅消息事件,否则 `Skip`)。
132pub struct EventPeer(pub Peer);
133
134#[async_trait]
135impl FromContext for EventPeer {
136    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
137        ctx.message().map(|m| EventPeer(m.peer)).ok_or(Reject::Skip)
138    }
139}
140
141/// 会话场景提取器:复用既有 [`Scene`](nagisa_types::id::Scene)(`Friend`/`Group`/`Temp`),
142/// **不**新增第二个 `Scene` 枚举(那会与 prelude 同名类型冲突)。一个 handler 取它即可在
143/// 同一处按对端种类分支、再用 [`Reply`] 回到当下这个对端——于是好友/群两份重复 handler 收敛为一。
144///
145/// 配合 peer-agnostic 的角色门控(`group_admin() | superuser()` 两个场景都可用),
146/// 同一 handler 还能据此派生 waiter 的 `.block(scene)`(按场景条件吞没)。
147///
148/// 非消息事件 → `Skip`(无对端场景可言)。
149#[async_trait]
150impl FromContext for nagisa_types::id::Scene {
151    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
152        // `peer.scene` 已是 `Scene`(id.rs)。
153        ctx.message().map(|m| m.peer.scene).ok_or(Reject::Skip)
154    }
155}
156
157/// app 共享状态注入。从 `Router::data` 注册的状态表里取 `Arc<T>`;缺失 → `Reject::Error`。
158///
159/// 实现了 `Deref<Target = T>`,故 handler 可直接写 `state.field` 而非 `state.0.field`;
160/// 元组字段 `.0`(`Arc<T>`)仍保留,需要克隆 `Arc` 时可用。
161pub struct State<T: Send + Sync + 'static>(pub Arc<T>);
162
163impl<T: Send + Sync + 'static> std::ops::Deref for State<T> {
164    type Target = T;
165    fn deref(&self) -> &T {
166        &self.0
167    }
168}
169
170#[async_trait]
171impl<T: Send + Sync + 'static> FromContext for State<T> {
172    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
173        let state: &StateMap = ctx.state();
174        match state.get(&TypeId::of::<T>()) {
175            Some(any) => match Arc::clone(any).downcast::<T>() {
176                Ok(typed) => Ok(State(typed)),
177                // 同一 TypeId 必然同类型,downcast 不会失败;保守兜底。
178                Err(_) => Err(Reject::Error(Error::action(format!("state type mismatch for {}", type_name::<T>())))),
179            },
180            None => Err(Reject::Error(Error::action_kind(
181                ActionErrorKind::BadParams,
182                format!("missing app state `{}` (register via Router::data)", type_name::<T>()),
183            ))),
184        }
185    }
186}
187
188/// Notice 事件提取器:仅当事件是 `Event::Notice` 时命中,否则 `Skip`。
189#[async_trait]
190impl FromContext for Notice {
191    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
192        match ctx.event().as_ref() {
193            Event::Notice(n) => Ok(n.clone()),
194            _ => Err(Reject::Skip),
195        }
196    }
197}
198
199/// Request 事件提取器:仅当事件是 `Event::Request` 时命中,否则 `Skip`。
200#[async_trait]
201impl FromContext for Request {
202    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
203        match ctx.event().as_ref() {
204            Event::Request(r) => Ok(r.clone()),
205            _ => Err(Reject::Skip),
206        }
207    }
208}
209
210/// Meta 事件提取器:仅当事件是 `Event::Meta` 时命中,否则 `Skip`。
211#[async_trait]
212impl FromContext for Meta {
213    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
214        match ctx.event().as_ref() {
215            Event::Meta(m) => Ok(m.clone()),
216            _ => Err(Reject::Skip),
217        }
218    }
219}
220
221/// 便捷回复句柄:携带消息事件的 `Peer` + 触发消息的 `MessageId` + `Bot`,向同一会话回话。
222/// 仅消息事件可提取(否则 `Skip`)。
223pub struct Reply {
224    peer: Peer,
225    /// 触发本次回复的消息 id(来自 `MessageEvent::id`)。
226    /// 供 [`quote`](Self::quote)/[`reply`](Self::reply) 引用原消息。
227    trigger: MessageId,
228    bot: Bot,
229}
230
231impl Reply {
232    /// 向来源会话发送一段纯文本。
233    pub async fn text(&self, s: impl Into<String>) -> Result<MessageId> {
234        self.bot.send(&self.peer, &[Segment::text(s)]).await
235    }
236
237    /// 向来源会话发送任意消息段。
238    pub async fn send(&self, segs: &[Segment]) -> Result<MessageId> {
239        self.bot.send(&self.peer, segs).await
240    }
241
242    /// 引用触发消息回复任意消息段(在段前插入一个 `Reply` 段)。
243    pub async fn quote(&self, segs: &[Segment]) -> Result<MessageId> {
244        let mut out = Vec::with_capacity(segs.len() + 1);
245        out.push(Segment::reply(self.trigger.clone()));
246        out.extend_from_slice(segs);
247        self.bot.send(&self.peer, &out).await
248    }
249
250    /// 引用触发消息回复一段纯文本。
251    pub async fn reply(&self, s: impl Into<String>) -> Result<MessageId> {
252        self.quote(&[Segment::text(s)]).await
253    }
254
255    /// 向来源会话发送一张图片(URL)。
256    pub async fn image(&self, url: impl Into<String>) -> Result<MessageId> {
257        self.bot.send(&self.peer, &[Segment::image_url(url)]).await
258    }
259
260    /// 向来源会话发送一个 @ 提及。
261    pub async fn at(&self, user: impl Into<Uin>) -> Result<MessageId> {
262        self.bot.send(&self.peer, &[Segment::at(user)]).await
263    }
264
265    /// 向来源会话发送一个 QQ 表情。
266    pub async fn face(&self, id: impl Into<String>) -> Result<MessageId> {
267        self.bot.send(&self.peer, &[Segment::face(id)]).await
268    }
269
270    /// 链式构造一条回复,镜像 `Msg` 构建体:
271    /// `reply.msg().at(user).text("..").send().await?`。
272    /// 比手搓 `Vec<Segment>` 再 `reply.send(&segs)` 更顺手。
273    pub fn msg(&self) -> ReplyMsg<'_> {
274        ReplyMsg { reply: self, msg: Msg::new() }
275    }
276
277    /// 回复目标会话寻址。
278    pub fn peer(&self) -> &Peer {
279        &self.peer
280    }
281
282    /// 触发本次回复的消息 id。
283    pub fn trigger(&self) -> &MessageId {
284        &self.trigger
285    }
286}
287
288/// [`Reply::msg`] 返回的链式构建体:累积 [`Msg`] 段,终结于 [`send`](Self::send)
289/// (向来源会话发送)或 [`quote`](Self::quote)(引用触发消息再发送)。
290/// 字段方法照搬 `Msg`,故 `reply.msg().at(u).text("hi")` 与 `Msg` 同手感。
291pub struct ReplyMsg<'a> {
292    reply: &'a Reply,
293    msg: Msg,
294}
295
296impl ReplyMsg<'_> {
297    pub fn text(mut self, s: impl Into<String>) -> Self {
298        self.msg = self.msg.text(s);
299        self
300    }
301    pub fn at(mut self, user: impl Into<Uin>) -> Self {
302        self.msg = self.msg.at(user);
303        self
304    }
305    pub fn at_all(mut self) -> Self {
306        self.msg = self.msg.at_all();
307        self
308    }
309    pub fn face(mut self, id: impl Into<String>) -> Self {
310        self.msg = self.msg.face(id);
311        self
312    }
313    pub fn image_url(mut self, url: impl Into<String>) -> Self {
314        self.msg = self.msg.image_url(url);
315        self
316    }
317    /// 追加一张内存图片(PNG/GIF/… 字节)。镜像 [`Msg::image_bytes`],让现渲染现发的
318    /// 插件能走 `reply.msg().image_bytes(png).text("..").send()` 而非手搓 `Segment` 数组。
319    pub fn image_bytes(mut self, bytes: impl Into<bytes::Bytes>) -> Self {
320        self.msg = self.msg.image_bytes(bytes);
321        self
322    }
323    /// 追加一张本地文件图片(镜像 [`Msg::image_path`],三态与 `image_url`/`image_bytes` 对齐)。
324    pub fn image_path(mut self, path: impl Into<std::path::PathBuf>) -> Self {
325        self.msg = self.msg.image_path(path);
326        self
327    }
328    /// 在已累积段前引用触发消息(等价于 [`Reply::quote`] 的链式入口)。
329    pub fn reply_to_trigger(mut self) -> Self {
330        self.msg = self.msg.reply(self.reply.trigger.clone());
331        self
332    }
333    /// 追加任意段。
334    pub fn push(mut self, seg: Segment) -> Self {
335        self.msg = self.msg.push(seg);
336        self
337    }
338    /// 发送累积的消息到来源会话。
339    pub async fn send(self) -> Result<MessageId> {
340        self.reply.send(&self.msg.build()).await
341    }
342    /// 引用触发消息后发送累积的消息。
343    pub async fn quote(self) -> Result<MessageId> {
344        self.reply.quote(&self.msg.build()).await
345    }
346}
347
348#[async_trait]
349impl FromContext for Reply {
350    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
351        match ctx.message() {
352            Some(m) => Ok(Reply { peer: m.peer, trigger: m.id.clone(), bot: ctx.bot().clone() }),
353            None => Err(Reject::Skip),
354        }
355    }
356}
357
358// —— 命令提取器:读取匹配器在 dispatch 时存入 `ctx` 扩展的 `ParsedCommand`。——
359// 匹配器命中后 `ctx.insert_ext(parsed)`;故仅在「命令型 handler」内可提取,
360// 普通 handler(无匹配器)下这些提取器 `Skip`。
361
362/// 命令之后剩余的消息段(保留非文本段,如图片)。无 `ParsedCommand` 时 `Skip`。
363pub struct CommandArg(pub Vec<Segment>);
364
365#[async_trait]
366impl FromContext for CommandArg {
367    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
368        ctx.get_ext::<ParsedCommand>().map(|p| CommandArg(p.args)).ok_or(Reject::Skip)
369    }
370}
371
372/// 命令剩余段的纯文本。无 `ParsedCommand` 时 `Skip`。
373pub struct ArgText(pub String);
374
375#[async_trait]
376impl FromContext for ArgText {
377    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
378        ctx.get_ext::<ParsedCommand>().map(|p| ArgText(p.args_text)).ok_or(Reject::Skip)
379    }
380}
381
382/// 命中的命令字面量(或正则整段匹配)。无 `ParsedCommand` 时 `Skip`。
383pub struct Command(pub String);
384
385#[async_trait]
386impl FromContext for Command {
387    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
388        ctx.get_ext::<ParsedCommand>().map(|p| Command(p.command)).ok_or(Reject::Skip)
389    }
390}
391
392/// 正则捕获组(非正则匹配器时为空)。无 `ParsedCommand` 时 `Skip`。
393pub struct Captures(pub Vec<String>);
394
395#[async_trait]
396impl FromContext for Captures {
397    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
398        ctx.get_ext::<ParsedCommand>().map(|p| Captures(p.captures)).ok_or(Reject::Skip)
399    }
400}
401
402// —— 元素提取器:从「匹配后的消息链」抽取首个 @ / 图片,与 waiter 统一。——
403//
404// 招牌赌注:**读取内联元素的类型,正是 `recv::<Image>` 返回的类型** —— 于是
405// 「内联 OR 追问」是同一个类型:
406//   let img = match img_opt { Some(i) => i, None => {            // 内联 OR 追问,一个类型
407//       reply.text("发一张图").await?;
408//       waiter.recv::<Image>(D30).await.ok_or(Timeout)?         // recv::<T: FromContext> 已存在
409//   }};
410//
411// 统一靠同一份「匹配链」来源:命令型 handler 里读 `ParsedCommand.args`(命令头之后的
412// 剩余段);waiter 投递的新消息没有 `ParsedCommand`,回退读整条消息 `content`。两路都走
413// 同一个 `seg_as_at`/`seg_as_image` 投影,故 `recv::<Image>`(追问得来的内联图)与内联
414// `Image` 槽是**同一个类型**。
415//
416// 「匹配链」上跑投影 `f`:有 `ParsedCommand` 跑其 `args`(命令头之后的剩余段),否则跑
417// 整条消息 `content`(waiter 投递的新消息没有 `ParsedCommand`)。两路同一份投影,于是
418// 内联槽与 `recv::<T>` 是同一个类型。非消息且无 `ParsedCommand` → `None`(提取器据此 `Skip`)。
419fn first_in_chain<R>(ctx: &Ctx, f: impl Fn(&Segment) -> Option<R>) -> Option<R> {
420    if let Some(p) = ctx.get_ext::<ParsedCommand>() {
421        return p.args.iter().find_map(&f);
422    }
423    ctx.message()?.content.iter().find_map(&f)
424}
425
426/// 匹配链中首个 @ 提及,投影为被 @ 的 `Uin`。链中无 mention(或非消息事件)→ `Skip`。
427///
428/// 用于「内联 @ 某人 OR 追问」:`async fn h(At(target): At, ..)` 或 `Option<At>` 可选。
429pub struct At(pub Uin);
430
431#[async_trait]
432impl FromContext for At {
433    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
434        first_in_chain(ctx, crate::args::seg_as_at).map(At).ok_or(Reject::Skip)
435    }
436}
437
438/// 匹配链中首个图片段,投影为 [`Media`](含 `recv.url`)。链中无图片(或非消息事件)→ `Skip`。
439///
440/// 容忍前导文本/换行:在段流里 `find_map` 找首个图片,前面的文本段自然被跳过。
441///
442/// 这就是 `recv::<Image>` 返回的类型——内联图与追问得来的图统一为一个类型。
443pub struct Image(pub Media);
444
445#[async_trait]
446impl FromContext for Image {
447    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
448        first_in_chain(ctx, crate::args::seg_as_image).map(Image).ok_or(Reject::Skip)
449    }
450}
451
452/// 消息是否「to me」:私聊/Temp 场景,或群聊里首段是 `@self` mention。
453///
454/// 此提取器独立于 [`Matcher`]:即便没有命令匹配器,只要是消息事件就可提取。
455/// 非消息事件返回 `Skip`。
456///
457/// [`Matcher`]: crate::matcher::Matcher
458pub struct ToMe(pub bool);
459
460#[async_trait]
461impl FromContext for ToMe {
462    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
463        let msg = ctx.message().ok_or(Reject::Skip)?;
464        // 私聊 / Temp 天然 to_me。
465        if matches!(msg.peer.scene, Scene::Friend | Scene::Temp) {
466            return Ok(ToMe(true));
467        }
468        // 群聊:首段为 @self 则 to_me。
469        let self_id = ctx.bot().self_id();
470        let to_me =
471            msg.content.first().is_some_and(|seg| matches!(seg, Segment::Mention { user, .. } if *user == self_id));
472        Ok(ToMe(to_me))
473    }
474}