1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
//! Notice 事件解码(`group_*`/`reaction`/`notify` 子类型 → 统一的 `Notice`)。
use super::*;
pub(super) fn decode_notice(ev: RawEventJson) -> Event {
let nt = ev.notice_type.clone().unwrap_or_default();
let group = Uin(ev.group_id.unwrap_or(0));
let user = Uin(ev.user_id.unwrap_or(0));
let operator = Uin(ev.operator_id.unwrap_or(0));
let raw = event_raw_value(&ev);
let notice = match nt.as_str() {
"group_recall" => Notice::Recall {
peer: Peer::group(group.0),
id: MessageId { peer: Peer::group(group.0), seq: 0, onebot_id: ev.message_id },
sender: user,
operator,
suffix: ev.extra.get("tip").and_then(|v| v.as_str()).map(String::from),
},
"friend_recall" => Notice::Recall {
peer: Peer::friend(user.0),
id: MessageId { peer: Peer::friend(user.0), seq: 0, onebot_id: ev.message_id },
sender: user,
operator: user,
suffix: ev.extra.get("tip").and_then(|v| v.as_str()).map(String::from),
},
"group_increase" => Notice::MemberIncrease {
group,
user,
operator: ev.operator_id.map(Uin),
invitor: if ev.sub_type.as_deref() == Some("invite") { ev.operator_id.map(Uin) } else { None },
},
"group_decrease" => Notice::MemberDecrease {
group,
user,
operator: ev.operator_id.map(Uin),
// OFFICIAL: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md (§群成员减少)
// sub_type: `leave`(主动退群) / `kick`(成员被踢) / `kick_me`(登录号被踢)。
// NapCat 扩展 `disband`(群解散时逐成员下发):
// ENDPOINT NapCat OB11GroupDecreaseEvent.ts。
reason: match ev.sub_type.as_deref() {
Some("leave") => MemberDecreaseReason::Leave,
Some("kick") => MemberDecreaseReason::Kick,
Some("kick_me") => MemberDecreaseReason::KickMe,
Some("disband") => MemberDecreaseReason::Disband,
_ => MemberDecreaseReason::Unknown,
},
},
"group_admin" => {
Notice::AdminChange { group, user, operator: None, is_set: ev.sub_type.as_deref() == Some("set") }
}
"group_ban" => {
// OneBot 重载:user_id == 0 表示全群禁言。
if ev.user_id.unwrap_or(0) == 0 {
Notice::WholeMute {
group,
operator,
// 看 sub_type ban / lift_ban,或 duration 的正负。
is_mute: ev.sub_type.as_deref() != Some("lift_ban") && ev.duration.unwrap_or(0) != 0,
}
} else {
Notice::Mute { group, user, operator, duration: ev.duration.unwrap_or(0) as i32 }
}
}
"group_name_change" => {
Notice::GroupNameChange { group, new_name: ev.name.clone().unwrap_or_default(), operator }
}
"group_upload" => {
let f = ev.file.clone().unwrap_or_default();
Notice::GroupFileUpload {
group,
user,
file: FileMeta {
id: f.id.unwrap_or_default(),
name: f.name.unwrap_or_default(),
size: f.size.unwrap_or(0),
hash: None,
busid: f.busid,
// group_upload notice 的 file 只带 id/name/size/busid。
uploader: None,
upload_time: None,
dead_time: None,
download_times: None,
parent_folder_id: None,
},
}
}
"reaction" => {
let face_id = ev.code.clone().unwrap_or_default();
Notice::Reaction {
group,
user: Uin(ev.operator_id.unwrap_or(ev.user_id.unwrap_or(0))),
seq: ev.message_id.map(|m| m as i64).unwrap_or(0),
likes: vec![EmojiLike { face_id: face_id.clone(), count: ev.count }],
face_id,
kind: ReactionKind::Face,
is_add: ev.sub_type.as_deref() != Some("remove"),
// Lagrange 原始 sub_type(`add`/`remove`):typed 保留,is_add 由其派生。
sub_type: ev.sub_type.clone(),
count: ev.count,
}
}
"essence" => Notice::EssenceChange {
group,
seq: ev.message_id.map(|m| m as i64).unwrap_or(0),
// Lagrange `essence` 携带原作者 sender_id;缺省为 None。
sender: ev.sender_id.map(Uin),
operator: ev.operator_id.map(Uin),
is_set: ev.sub_type.as_deref() != Some("delete"),
},
"offline_file" => {
let f = ev.file.clone().unwrap_or_default();
Notice::FriendFileUpload {
user,
file: FileMeta {
id: f.id.unwrap_or_default(),
name: f.name.unwrap_or_default(),
size: f.size.unwrap_or(0),
hash: f.hash,
busid: f.busid,
// offline_file notice 的 file 只带 id/name/size/hash/busid。
uploader: None,
upload_time: None,
dead_time: None,
download_times: None,
parent_folder_id: None,
},
is_self: false,
}
}
"bot_offline" => Notice::BotOffline {
// Lagrange 把原因序列化在 `message`/`reason` JSON 键里。
reason: ev
.reason
.clone()
.or_else(|| match &ev.message {
Some(WireMessage::Cq(s)) if !s.is_empty() => Some(s.clone()),
_ => None,
})
.or_else(|| ev.tag.clone())
.unwrap_or_default(),
// `tag` 与 reason 不同维度:typed 单独保留(区分离线种类)。
tag: ev.tag.clone(),
},
"friend_add" => Notice::FriendAdd { user },
// ENDPOINT: LLOneBot src/onebot11/event/notice/(群解散 notice)。
// 群主解散整个群时下发一次 group_dismiss(区别于逐成员的 group_decrease/disband)。
"group_dismiss" => Notice::GroupDismiss {
group,
// 解散者通常为群主;wire 可能落在 operator_id,缺省回退 user_id。
operator: Uin(ev.operator_id.or(ev.user_id).unwrap_or(0)),
},
// ENDPOINT: NapCat(在线/临时文件 notice)。
// online_file_send / online_file_receive:在线文件收发提示,群场景带 group_id。
"online_file_send" | "online_file_receive" => Notice::OnlineFile {
direction: if nt == "online_file_receive" {
OnlineFileDirection::Receive
} else {
OnlineFileDirection::Send
},
user,
group: ev.group_id.map(Uin),
},
// ENDPOINT: LLOneBot src/onebot11/event/notice/OB11FlashTransferNoticeEvent.ts。
// flash_file:闪传进度 notice,sub_type=downloading/downloaded/uploading/uploaded。
// 完整闪传载荷保留在事件 raw(下游可读 file_set_id/title 等)。
"flash_file" => Notice::FlashFile {
phase: match ev.sub_type.as_deref() {
Some("downloading") => FlashFilePhase::Downloading,
Some("downloaded") => FlashFilePhase::Downloaded,
Some("uploading") => FlashFilePhase::Uploading,
Some("uploaded") => FlashFilePhase::Uploaded,
_ => FlashFilePhase::Unknown,
},
user,
group: ev.group_id.map(Uin),
},
// ENDPOINT: NapCat packages/napcat-onebot/event/notice/OB11MsgEmojiLikeEvent.ts
// (https://github.com/NapNeko/NapCatQQ);并对照 LLOneBot
// src/onebot11/event/notice/OB11MsgEmojiLikeEvent.ts。
// 表情回应:data 形如 {user_id,group_id,message_id,is_add,likes:[{emoji_id,count}]}。
// 一次 notice 可携带多个 emoji:全部上浮到 `likes`(忠实,不破坏单事件分发);
// face_id/count 同步取首元素以兼容只读单 emoji 的下游。
"group_msg_emoji_like" => {
let likes: Vec<EmojiLike> = ev
.extra
.get("likes")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.map(|like| EmojiLike {
face_id: like.get("emoji_id").and_then(value_as_string).unwrap_or_default(),
count: like.get("count").and_then(Value::as_i64),
})
.collect()
})
.unwrap_or_default();
let first = likes.first();
let face_id = first.map(|l| l.face_id.clone()).unwrap_or_default();
let count = first.and_then(|l| l.count);
Notice::Reaction {
group,
user,
// 无独立 seq 字段:从 message_id 派生(不可派生则 0)。
seq: ev.message_id.map(|m| m as i64).unwrap_or(0),
face_id,
kind: ReactionKind::Emoji,
// NapCat/LLOneBot 携带 is_add;缺省按新增(true)。
is_add: ev.extra.get("is_add").and_then(|v| v.as_bool()).unwrap_or(true),
// group_msg_emoji_like 无独立 sub_type 字段(is_add 即语义)。
sub_type: None,
count,
likes,
}
}
// ENDPOINT: NapCat packages/napcat-onebot/event/notice/OB11GroupCardEvent.ts
// (https://github.com/NapNeko/NapCatQQ);并对照 LLOneBot
// src/onebot11/event/notice/OB11GroupCardEvent.ts。
// 群名片变更:data 形如 {group_id,user_id,card_new,card_old}。
"group_card" => Notice::GroupCardChange {
group,
user,
old_card: ev.extra.get("card_old").and_then(value_as_string).unwrap_or_default(),
new_card: ev.extra.get("card_new").and_then(value_as_string).unwrap_or_default(),
},
// Lagrange `bot_online`:上线 notice,`reason` 携带上线原因文案。
"bot_online" => return Event::Meta(Meta::BotOnline { reason: ev.reason.clone() }),
"notify" => return decode_notify(ev),
_ => Notice::Other { protocol: PROTO, kind: nt, raw },
};
Event::Notice(notice)
}
/// `notify` 子类型:`poke`(戳一戳)、`honor`、`lucky_king` 等。
fn decode_notify(ev: RawEventJson) -> Event {
let display = NudgeDisplay {
action: ev.action.clone().unwrap_or_default(),
suffix: ev.suffix.clone().unwrap_or_default(),
action_img_url: ev.action_img_url.clone().filter(|s| !s.is_empty()),
};
match ev.sub_type.as_deref() {
Some("poke") => {
// 群戳一戳带 group_id;好友戳一戳带 sender_id、无 group。
if ev.group_id.is_some() {
Event::Notice(Notice::GroupNudge {
group: Uin(ev.group_id.unwrap_or(0)),
sender: Uin(ev.user_id.unwrap_or(0)),
receiver: Uin(ev.target_id.unwrap_or(0)),
display,
})
} else {
let sender = Uin(ev.sender_id.or(ev.user_id).unwrap_or(0));
let self_id = Uin(ev.self_id);
let target = Uin(ev.target_id.unwrap_or(0));
Event::Notice(Notice::FriendNudge {
user: if sender == self_id { target } else { sender },
is_self_send: sender == self_id,
is_self_receive: target == self_id,
display,
})
}
}
// ENDPOINT: NapCat packages/napcat-onebot/event/notice/OB11GroupNameEvent.ts
// (https://github.com/NapNeko/NapCatQQ).
// notify + sub_type=group_name:群名变更,新名在 `name_new`,操作者为 user_id。
Some("group_name") => Event::Notice(Notice::GroupNameChange {
group: Uin(ev.group_id.unwrap_or(0)),
new_name: ev.extra.get("name_new").and_then(value_as_string).unwrap_or_default(),
operator: Uin(ev.user_id.unwrap_or(0)),
}),
// OFFICIAL: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md (§群成员荣誉变更)
// notify + sub_type=honor:honor_type talkative/performer/emotion=龙王/群聊之火/快乐源泉。
Some("honor") => Event::Notice(Notice::Honor {
group: Uin(ev.group_id.unwrap_or(0)),
user: Uin(ev.user_id.unwrap_or(0)),
// wire.rs 已解析 `honor_type` 字段,直接复用。
honor: match ev.honor_type.as_deref() {
Some("talkative") => HonorKind::Talkative,
Some("performer") => HonorKind::Performer,
Some("emotion") => HonorKind::Emotion,
_ => HonorKind::Unknown,
},
}),
// OFFICIAL: https://github.com/botuniverse/onebot-11/blob/master/event/notice.md (§群红包运气王)
// notify + sub_type=lucky_king: group_id / user_id(发红包者) / target_id(运气王)。
Some("lucky_king") => Event::Notice(Notice::LuckyKing {
group: Uin(ev.group_id.unwrap_or(0)),
user: Uin(ev.user_id.unwrap_or(0)),
target: Uin(ev.target_id.unwrap_or(0)),
}),
// ENDPOINT: NapCat OB11GroupTitleEvent.ts; cross-checked LLOneBot。
// notify + sub_type=title:群头衔变更,新头衔在 `title`,成员为 user_id。
Some("title") => Event::Notice(Notice::GroupTitleChange {
group: Uin(ev.group_id.unwrap_or(0)),
user: Uin(ev.user_id.unwrap_or(0)),
title: ev.extra.get("title").and_then(value_as_string).unwrap_or_default(),
}),
// ENDPOINT: NapCat OB11InputStatusEvent.ts; cross-checked LLOneBot。
// notify + sub_type=input_status:对端正在输入。群场景带 group_id;
// status_text 为展示文案,event_type 为原始状态码。
Some("input_status") => Event::Notice(Notice::InputStatus {
user: Uin(ev.user_id.unwrap_or(0)),
group: ev.group_id.map(Uin),
status_text: ev.extra.get("status_text").and_then(value_as_string).unwrap_or_default(),
event_type: ev.extra.get("event_type").and_then(Value::as_i64).unwrap_or(0),
}),
// ENDPOINT: NapCat OB11ProfileLikeEvent.ts; cross-checked LLOneBot。
// notify + sub_type=profile_like:资料卡点赞。点赞者为 operator_id,
// 次数 times,昵称 operator_nick。
Some("profile_like") => Event::Notice(Notice::ProfileLike {
operator: Uin(ev.operator_id.or(ev.user_id).unwrap_or(0)),
operator_nick: ev.extra.get("operator_nick").and_then(value_as_string).unwrap_or_default(),
times: ev.extra.get("times").and_then(Value::as_i64).unwrap_or(0),
}),
// ENDPOINT: NapCat(gray tip notice); cross-checked LLOneBot。
// notify + sub_type=gray_tip:灰字系统提示。子类繁多,统一以 content 透出文本,
// 结构细节保留在事件 raw。
Some("gray_tip") => Event::Notice(Notice::GrayTip {
group: ev.group_id.map(Uin),
user: ev.user_id.map(Uin),
content: ev
.extra
.get("content")
.or_else(|| ev.extra.get("tip"))
.and_then(value_as_string)
.unwrap_or_default(),
}),
// ENDPOINT: LLOneBot(poke_recall notice)。
// notify + sub_type=poke_recall:戳一戳被撤回。群场景带 group_id。
Some("poke_recall") => {
Event::Notice(Notice::PokeRecall { group: ev.group_id.map(Uin), user: Uin(ev.user_id.unwrap_or(0)) })
}
_ => raw_event(&ev, "notify"),
}
}