Skip to main content

nagisa_core/
event_trigger.rs

1//! 事件触发器:`EventKind` 判别式 + 细粒度 `FromContext` 提取器,使
2//! `#[event(Kind)]` handler 能在事件上触发并读到类型化数据,同时启用开关门控统一套用
3//! (经 `Event::peer()`)。
4use crate::ctx::Ctx;
5use crate::extract::{Extracted, FromContext, Reject};
6use async_trait::async_trait;
7use nagisa_types::event::{Event, Meta, Notice, Request};
8use nagisa_types::id::{Peer, Uin};
9
10/// 给 `#[event(Kind)]` 用的友好选择器。覆盖 nagisa 的事件面;若干 `Notice`/`Meta`
11/// 变体被合并(如两种 nudge 变体都 → `Nudge`)。
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
13pub enum EventKind {
14    Message,
15    MemberJoin,
16    MemberLeave,
17    Mute,
18    WholeMute,
19    AdminChange,
20    GroupNameChange,
21    Honor,
22    GroupCardChange,
23    Recall,
24    Reaction,
25    EssenceChange,
26    LuckyKing,
27    Nudge,
28    FriendAdd,
29    GroupFileUpload,
30    FriendFileUpload,
31    PeerPin,
32    GroupDismiss,
33    GroupTitleChange,
34    InputStatus,
35    ProfileLike,
36    GrayTip,
37    PokeRecall,
38    OnlineFile,
39    FlashFile,
40    FriendRequest,
41    GroupJoinRequest,
42    GroupInvitedJoin,
43    GroupInvite,
44    Connect,
45    Disconnect,
46    Ready,
47    Heartbeat,
48    BotOnline,
49    BotOffline,
50    Raw,
51}
52
53impl EventKind {
54    /// 该具体事件匹配的 `EventKind`(如有;`Notice::Other` 及未知的未来变体 → `None`)。
55    pub fn of(event: &Event) -> Option<EventKind> {
56        use EventKind as K;
57        Some(match event {
58            Event::Message(_) => K::Message,
59            Event::Notice(n) => match n {
60                Notice::Recall { .. } => K::Recall,
61                Notice::MemberIncrease { .. } => K::MemberJoin,
62                Notice::MemberDecrease { .. } => K::MemberLeave,
63                Notice::AdminChange { .. } => K::AdminChange,
64                Notice::Mute { .. } => K::Mute,
65                Notice::WholeMute { .. } => K::WholeMute,
66                Notice::GroupNameChange { .. } => K::GroupNameChange,
67                Notice::Honor { .. } => K::Honor,
68                Notice::GroupCardChange { .. } => K::GroupCardChange,
69                Notice::FriendNudge { .. } | Notice::GroupNudge { .. } => K::Nudge,
70                Notice::Reaction { .. } => K::Reaction,
71                Notice::EssenceChange { .. } => K::EssenceChange,
72                Notice::GroupFileUpload { .. } => K::GroupFileUpload,
73                Notice::FriendFileUpload { .. } => K::FriendFileUpload,
74                Notice::FriendAdd { .. } => K::FriendAdd,
75                Notice::PeerPinChange { .. } => K::PeerPin,
76                Notice::BotOffline { .. } => K::BotOffline,
77                Notice::LuckyKing { .. } => K::LuckyKing,
78                Notice::GroupDismiss { .. } => K::GroupDismiss,
79                Notice::GroupTitleChange { .. } => K::GroupTitleChange,
80                Notice::InputStatus { .. } => K::InputStatus,
81                Notice::ProfileLike { .. } => K::ProfileLike,
82                Notice::GrayTip { .. } => K::GrayTip,
83                Notice::PokeRecall { .. } => K::PokeRecall,
84                Notice::OnlineFile { .. } => K::OnlineFile,
85                Notice::FlashFile { .. } => K::FlashFile,
86                Notice::Other { .. } => return None,
87                _ => return None,
88            },
89            Event::Request(r) => match r {
90                Request::Friend { .. } => K::FriendRequest,
91                Request::GroupJoin { .. } => K::GroupJoinRequest,
92                Request::GroupInvitedJoin { .. } => K::GroupInvitedJoin,
93                Request::GroupInvite { .. } => K::GroupInvite,
94                _ => return None,
95            },
96            Event::Meta(m) => match m {
97                Meta::Connect => K::Connect,
98                Meta::Disconnect { .. } => K::Disconnect,
99                Meta::Ready { .. } => K::Ready,
100                Meta::Heartbeat { .. } => K::Heartbeat,
101                Meta::BotOnline { .. } => K::BotOnline,
102                Meta::BotOffline => K::BotOffline,
103                _ => return None,
104            },
105            Event::Raw(_) => K::Raw,
106            _ => K::Raw,
107        })
108    }
109}
110
111// ── 细粒度事件提取器 ────────────────────────────────────────────
112//
113// 每个结构体只匹配某个 EventKind 的底层变体,否则返回 `Reject::Skip`。Nudge 额外在行为主体
114// 是 bot 自己时跳过(自事件过滤——防止 bot 对自己的戳一戳作出反应)。
115
116/// 有成员入群(`Notice::MemberIncrease`)。
117pub struct MemberJoin {
118    pub group: Uin,
119    pub user: Uin,
120    pub operator: Option<Uin>,
121    pub invitor: Option<Uin>,
122}
123#[async_trait]
124impl FromContext for MemberJoin {
125    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
126        match ctx.event().as_ref() {
127            Event::Notice(Notice::MemberIncrease { group, user, operator, invitor }) => {
128                Ok(MemberJoin { group: *group, user: *user, operator: *operator, invitor: *invitor })
129            }
130            _ => Err(Reject::Skip),
131        }
132    }
133}
134
135/// 成员退群或被踢(`Notice::MemberDecrease`)。
136pub struct MemberLeave {
137    pub group: Uin,
138    pub user: Uin,
139    pub operator: Option<Uin>,
140    pub reason: nagisa_types::event::MemberDecreaseReason,
141}
142#[async_trait]
143impl FromContext for MemberLeave {
144    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
145        match ctx.event().as_ref() {
146            Event::Notice(Notice::MemberDecrease { group, user, operator, reason }) => {
147                Ok(MemberLeave { group: *group, user: *user, operator: *operator, reason: *reason })
148            }
149            _ => Err(Reject::Skip),
150        }
151    }
152}
153
154/// 群里有成员被禁言(`Notice::Mute`)。
155pub struct Mute {
156    pub group: Uin,
157    pub user: Uin,
158    pub operator: Uin,
159    pub duration: i32,
160}
161#[async_trait]
162impl FromContext for Mute {
163    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
164        match ctx.event().as_ref() {
165            Event::Notice(Notice::Mute { group, user, operator, duration }) => {
166                Ok(Mute { group: *group, user: *user, operator: *operator, duration: *duration })
167            }
168            _ => Err(Reject::Skip),
169        }
170    }
171}
172
173/// 一条消息被撤回(`Notice::Recall`)。同时携带原始 `sender`(作者)与撤回它的 `operator`,
174/// 使 handler 能套自己的策略(如防撤回类插件可在 `author != bot OR operator == bot` 时触发)。
175/// 刻意**不**做自过滤——在这里写死一套自策略会挡掉防撤回功能需要的「bot 自己的消息被管理员
176/// 撤回」这一情形。
177pub struct Recall {
178    pub peer: Peer,
179    pub id: nagisa_types::id::MessageId,
180    pub sender: Uin,
181    pub operator: Uin,
182    pub suffix: Option<String>,
183}
184#[async_trait]
185impl FromContext for Recall {
186    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
187        match ctx.event().as_ref() {
188            Event::Notice(Notice::Recall { peer, id, sender, operator, suffix }) => {
189                Ok(Recall { peer: *peer, id: id.clone(), sender: *sender, operator: *operator, suffix: suffix.clone() })
190            }
191            _ => Err(Reject::Skip),
192        }
193    }
194}
195
196/// 戳一戳事件——统一 `Notice::FriendNudge` 与 `Notice::GroupNudge`,归一为
197/// `{ peer, sender, receiver }`。自过滤:`sender == self_id` 时跳过(防止 bot 对自己的戳一戳反应)。
198pub struct Nudge {
199    pub peer: Peer,
200    pub sender: Uin,
201    pub receiver: Uin,
202}
203#[async_trait]
204impl FromContext for Nudge {
205    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
206        let self_id = ctx.bot().self_id();
207        match ctx.event().as_ref() {
208            Event::Notice(Notice::FriendNudge { user, is_self_send, .. }) => {
209                if *is_self_send {
210                    return Err(Reject::Skip);
211                }
212                // user 是发戳一戳的好友;bot 是被戳者。
213                Ok(Nudge { peer: Peer::friend(*user), sender: *user, receiver: self_id })
214            }
215            Event::Notice(Notice::GroupNudge { group, sender, receiver, .. }) => {
216                if *sender == self_id {
217                    return Err(Reject::Skip);
218                }
219                Ok(Nudge { peer: Peer::group(*group), sender: *sender, receiver: *receiver })
220            }
221            _ => Err(Reject::Skip),
222        }
223    }
224}
225
226/// 新加了一个好友(`Notice::FriendAdd`)。
227pub struct FriendAdd {
228    pub user: Uin,
229}
230#[async_trait]
231impl FromContext for FriendAdd {
232    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
233        match ctx.event().as_ref() {
234            Event::Notice(Notice::FriendAdd { user }) => Ok(FriendAdd { user: *user }),
235            _ => Err(Reject::Skip),
236        }
237    }
238}
239
240/// 好友请求(`Request::Friend`)。
241pub struct FriendRequest {
242    pub initiator: Uin,
243    pub comment: String,
244    pub token: nagisa_types::event::RequestToken,
245}
246#[async_trait]
247impl FromContext for FriendRequest {
248    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
249        match ctx.event().as_ref() {
250            Event::Request(Request::Friend { initiator, comment, token, .. }) => {
251                Ok(FriendRequest { initiator: *initiator, comment: comment.clone(), token: token.clone() })
252            }
253            _ => Err(Reject::Skip),
254        }
255    }
256}
257
258/// 入群请求(`Request::GroupJoin`)。
259pub struct GroupJoinRequest {
260    pub group: Uin,
261    pub initiator: Uin,
262    pub comment: String,
263    pub token: nagisa_types::event::RequestToken,
264}
265#[async_trait]
266impl FromContext for GroupJoinRequest {
267    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
268        match ctx.event().as_ref() {
269            Event::Request(Request::GroupJoin { group, initiator, comment, token, .. }) => Ok(GroupJoinRequest {
270                group: *group,
271                initiator: *initiator,
272                comment: comment.clone(),
273                token: token.clone(),
274            }),
275            _ => Err(Reject::Skip),
276        }
277    }
278}
279
280/// 群管理员被设置/取消(`Notice::AdminChange`)。`is_set` = 升为管理员。
281/// (`user == self_id` 时即 bot 自身权限变更,可据此监听 bot 被设/免管理员。)
282pub struct AdminChange {
283    pub group: Uin,
284    pub user: Uin,
285    pub operator: Option<Uin>,
286    pub is_set: bool,
287}
288#[async_trait]
289impl FromContext for AdminChange {
290    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
291        match ctx.event().as_ref() {
292            Event::Notice(Notice::AdminChange { group, user, operator, is_set }) => {
293                Ok(AdminChange { group: *group, user: *user, operator: *operator, is_set: *is_set })
294            }
295            _ => Err(Reject::Skip),
296        }
297    }
298}
299
300/// 某成员的群名片(昵称)变更(`Notice::GroupCardChange`)。
301/// (可在此对新名片做文本审核等处理。)
302pub struct GroupCardChange {
303    pub group: Uin,
304    pub user: Uin,
305    pub old_card: String,
306    pub new_card: String,
307}
308#[async_trait]
309impl FromContext for GroupCardChange {
310    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
311        match ctx.event().as_ref() {
312            Event::Notice(Notice::GroupCardChange { group, user, old_card, new_card }) => Ok(GroupCardChange {
313                group: *group,
314                user: *user,
315                old_card: old_card.clone(),
316                new_card: new_card.clone(),
317            }),
318            _ => Err(Reject::Skip),
319        }
320    }
321}
322
323/// 群荣誉变更(龙王/群聊之火/快乐源泉)(`Notice::Honor`)。
324pub struct Honor {
325    pub group: Uin,
326    pub user: Uin,
327    pub honor: nagisa_types::event::HonorKind,
328}
329#[async_trait]
330impl FromContext for Honor {
331    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
332        match ctx.event().as_ref() {
333            Event::Notice(Notice::Honor { group, user, honor }) => {
334                Ok(Honor { group: *group, user: *user, honor: *honor })
335            }
336            _ => Err(Reject::Skip),
337        }
338    }
339}
340
341/// 群红包运气王结果(`Notice::LuckyKing`)。`user` = 发红包者,`target` = 运气王。
342pub struct LuckyKing {
343    pub group: Uin,
344    pub user: Uin,
345    pub target: Uin,
346}
347#[async_trait]
348impl FromContext for LuckyKing {
349    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
350        match ctx.event().as_ref() {
351            Event::Notice(Notice::LuckyKing { group, user, target }) => {
352                Ok(LuckyKing { group: *group, user: *user, target: *target })
353            }
354            _ => Err(Reject::Skip),
355        }
356    }
357}
358
359// ── 生命周期事件提取器 ───────────────────────────────────────────────
360//
361// 每个生命周期 `EventKind` 一个类型化提取器,使 `#[event(Ready)]`/`#[event(Disconnect)]`/… 的
362// handler 能直接命名其载荷(如 `async fn(r: Ready, bot: Bot)`),而非匹配裸 `Meta`。其余事件一律
363// `Reject::Skip`。不需要载荷的 handler 直接省掉该提取器、只取 `Bot`(或什么都不取)即可。
364
365/// 协议端(传输层)连接成功([`Meta::Connect`],框架事件源每次(重)连发出)。
366pub struct Connect;
367#[async_trait]
368impl FromContext for Connect {
369    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
370        match ctx.event().as_ref() {
371            Event::Meta(Meta::Connect) => Ok(Connect),
372            _ => Err(Reject::Skip),
373        }
374    }
375}
376
377/// 协议端(传输层)断开([`Meta::Disconnect`])。`reason` 为底层错误文案(如有)。
378pub struct Disconnect {
379    pub reason: Option<String>,
380}
381#[async_trait]
382impl FromContext for Disconnect {
383    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
384        match ctx.event().as_ref() {
385            Event::Meta(Meta::Disconnect { reason }) => Ok(Disconnect { reason: reason.clone() }),
386            _ => Err(Reject::Skip),
387        }
388    }
389}
390
391/// 框架就绪:已解析出可用账号([`Meta::Ready`],每次 `run_*` 仅一次)。`self_id` 为机器人账号,
392/// `nickname` 为其昵称(来自 `get_login_info`,未知时为空串)。
393pub struct Ready {
394    pub self_id: Uin,
395    pub nickname: String,
396}
397#[async_trait]
398impl FromContext for Ready {
399    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
400        match ctx.event().as_ref() {
401            Event::Meta(Meta::Ready { self_id, nickname }) => {
402                Ok(Ready { self_id: *self_id, nickname: nickname.clone() })
403            }
404            _ => Err(Reject::Skip),
405        }
406    }
407}
408
409/// 机器人账号上线([`Meta::BotOnline`])。`reason` 为上线原因文案(Lagrange 提供,OneBot 为 None)。
410pub struct BotOnline {
411    pub reason: Option<String>,
412}
413#[async_trait]
414impl FromContext for BotOnline {
415    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
416        match ctx.event().as_ref() {
417            Event::Meta(Meta::BotOnline { reason }) => Ok(BotOnline { reason: reason.clone() }),
418            _ => Err(Reject::Skip),
419        }
420    }
421}
422
423/// 机器人账号掉线([`Meta::BotOffline`])。注意与传输层 [`Disconnect`] 区分。
424pub struct BotOffline;
425#[async_trait]
426impl FromContext for BotOffline {
427    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
428        match ctx.event().as_ref() {
429            Event::Meta(Meta::BotOffline) => Ok(BotOffline),
430            _ => Err(Reject::Skip),
431        }
432    }
433}
434
435/// 心跳([`Meta::Heartbeat`])。`interval` 为心跳间隔(ms),`online`/`good` 取自 status。
436pub struct Heartbeat {
437    pub interval: i64,
438    pub online: bool,
439    pub good: bool,
440}
441#[async_trait]
442impl FromContext for Heartbeat {
443    async fn from_context(ctx: &Ctx) -> Extracted<Self> {
444        match ctx.event().as_ref() {
445            Event::Meta(Meta::Heartbeat { interval, status }) => {
446                Ok(Heartbeat { interval: *interval, online: status.online, good: status.good })
447            }
448            _ => Err(Reject::Skip),
449        }
450    }
451}