1#[cfg(any(feature = "onebot", feature = "milky"))]
2use cmd::{AccControlCmd, CmdSetAccessControlList, HelpItem, KoviArgs, KoviCmd, PluginCmd};
3use kovi::bot::AccessControlMode;
4use kovi::bot::runtimebot::kovi_api::SetAccessControlList;
5use kovi::error::BotError;
6use kovi::event::MessageEventTrait;
7use kovi::event::id::ID;
8use kovi::log;
9#[cfg(any(feature = "onebot", feature = "milky"))]
10use kovi::{PluginBuilder as P, RuntimeBot, serde_json};
11use std::sync::{Arc, Mutex};
12use std::time::{SystemTime, UNIX_EPOCH};
13use sysinfo::{Pid, ProcessesToUpdate, System};
14
15#[cfg(not(any(feature = "onebot", feature = "milky")))]
16compile_error!("请至少启用一个协议 feature: \"onebot\" 或 \"milky\"");
17
18#[cfg(all(feature = "onebot", feature = "milky"))]
19compile_error!("不能同时启用 onebot 和 milky feature");
20
21#[cfg(feature = "onebot")]
22use kovi_onebot::*;
23
24#[cfg(feature = "milky")]
25use kovi_milky::*;
26
27mod cmd;
28
29#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize, Default)]
30struct Info {
31 start_time: u64,
32 accept_msg: u64,
33 send_msg: u64,
34}
35impl Info {
36 fn accept(&mut self) {
37 self.accept_msg += 1;
38 }
39 fn send(&mut self) {
40 self.send_msg += 1;
41 }
42}
43
44#[cfg(any(feature = "onebot", feature = "milky"))]
45#[kovi::plugin]
46async fn main() {
47 let start = SystemTime::now()
48 .duration_since(UNIX_EPOCH)
49 .unwrap()
50 .as_secs();
51
52 let info = Arc::new(Mutex::new(Info {
53 start_time: start,
54 accept_msg: 0,
55 send_msg: 0,
56 }));
57
58 let info_clone = info.clone();
59 P::on_msg(move |_| {
60 let info_clone = info_clone.clone();
61 async move {
62 let mut info = info_clone.lock().unwrap();
63 info.accept();
64 }
65 });
66
67 let info_clone = info.clone();
68 P::on(move |_: Arc<MsgSendFromKoviEvent>| {
69 let info_clone = info_clone.clone();
70 async move {
71 let mut info = info_clone.lock().unwrap();
72 info.send();
73 }
74 });
75
76 let bot = P::get_runtime_bot();
77 P::on_admin_msg(move |e| {
84 let bot = bot.clone();
85 let info = info.clone();
87 async move {
88 let text = if let Some(v) = e.borrow_text() {
89 v
90 } else {
91 return;
92 };
93 if !text.starts_with(".kovi") {
98 return;
99 }
100
101 let vec_text: Vec<&str> = text.split_whitespace().collect();
102
103 let cmd = KoviArgs::parse(vec_text.iter().map(|v| v.to_string()).collect());
104
105 match cmd.command {
106 KoviCmd::Help(item) => {
107 help(&e, item);
108 }
109 KoviCmd::Plugin(plugin_cmd) => match plugin_cmd {
110 PluginCmd::Status => plugin_status(&e, &bot),
111 PluginCmd::Start { name } => {
112 plugin_start(&e, &bot, &name);
113 }
114 PluginCmd::Stop { name } => {
115 plugin_stop(&e, &bot, &name);
116 }
117 PluginCmd::ReStart { name } => {
118 plugin_restart(&e, &bot, &name).await;
119 }
120 },
121 KoviCmd::Status => status(&e, &bot, info).await,
122 KoviCmd::Acc { name, acc_cmd } => acc(&e, &bot, &name, acc_cmd),
123 }
124 }
125 });
126}
127
128static HELP_MSG: &str = r#"┄ 📜 帮助列表 ┄
129.kovi plugin <T>: 插件管理
130.kovi acc <name> <T>: 访问控制
131.kovi status: 状态信息
132部分命令可缩写为第一个字母"#;
133
134static HELP_PLUGIN: &str = r#"┄ 📜 插件管理 ┄:
135.kovi plugin <T>
136
137<T>:
138list: 列出所有插件
139start <name>: 启动插件
140stop <name>: 停止插件
141restart <name>: 重载插件"#;
142
143static ACC_CONTROL_PLUGIN: &str = r#"┄ 📜 访问控制 ┄:
144.kovi acc <name> <T>
145
146<T>:
147status: 列出插件访问控制信息
148enable: 启用插件访问控制
149disable: 禁用插件访问控制
150mode <white | black>: 插件访问控制模式
151on: 添加本群到列表
152off: 移除本群到列表
153add <friend | group> [id]: 添加多个
154remove <friend | group> [id]: 移除多个"#;
155
156fn help(e: &AdminMsgEvent, item: HelpItem) {
157 match item {
158 HelpItem::Plugin => {
159 e.reply(HELP_PLUGIN);
160 }
161 HelpItem::Acc => {
162 e.reply(ACC_CONTROL_PLUGIN);
163 }
164 HelpItem::None => {
165 e.reply(HELP_MSG);
166 }
167 }
168}
169
170async fn status(e: &AdminMsgEvent, bot: &RuntimeBot, info: Arc<Mutex<Info>>) {
171 let now = SystemTime::now()
172 .duration_since(UNIX_EPOCH)
173 .unwrap()
174 .as_secs();
175
176 let info = { *info.lock().unwrap() };
177
178 let duration = now - info.start_time;
179
180 let days = duration / (24 * 3600);
182 let hours = (duration % (24 * 3600)) / 3600;
183 let minutes = (duration % 3600) / 60;
184 let seconds = duration % 60;
185
186 let mut sys = System::new();
188
189 let pid = Pid::from_u32(std::process::id());
190 sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
191 sys.refresh_memory();
192
193 let self_memory_usage = sys
194 .process(pid)
195 .map(|process| process.memory() as f64 / 1024.0 / 1024.0)
196 .unwrap_or(0.0);
197
198 let total_memory = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
199 let used_memory = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
200 let memory_usage_percent = (used_memory / total_memory) * 100.0;
201
202 let time_str = if days > 0 {
203 format!("{}d{}h{}m{}s", days, hours, minutes, seconds)
204 } else if hours > 0 {
205 format!("{}h{}m{}s", hours, minutes, seconds)
206 } else if minutes > 0 {
207 format!("{}m{}s", minutes, seconds)
208 } else {
209 format!("{}s", seconds)
210 };
211
212 let plugin_info = bot.get_plugin_info().unwrap();
213
214 let plugin_start_len = plugin_info.iter().filter(|v| v.enabled).count();
215
216 let server_info_str = get_server_info(bot).await;
217
218 let plugin_info_len = plugin_info.len();
219
220 let accept_msg = info.accept_msg;
221 let send_msg = info.send_msg;
222
223 let reply = format!(
224 "┄ 📑 状态 ┄\n\
225 🕑 运行时间: {time_str}\n\
226 ✉️ 消息状况: 收发{accept_msg}/{send_msg}\n\
227 📦 插件数量: {plugin_info_len} 启用 {plugin_start_len} 个\n\
228 🔋 内存使用: {self_memory_usage:.2}MB\n\
229 💻 系统内存:\n {:.2}GB/{:.2}GB({:.0}%)\n\
230 🔗 服务端:\n {}",
231 used_memory, total_memory, memory_usage_percent, server_info_str
232 );
233
234 e.reply(reply);
235}
236
237#[cfg(feature = "onebot")]
238async fn get_server_info(bot: &RuntimeBot) -> String {
239 use kovi_onebot::OnebotTrait;
240
241 #[derive(Debug, serde::Deserialize)]
242 struct OnebotInfo {
243 app_name: Option<String>,
244 app_version: Option<String>,
245 }
246
247 match bot.get_version_info().await {
248 Ok(v) => match serde_json::from_value::<OnebotInfo>(v.data) {
249 Ok(info) => {
250 let mut msg = String::new();
251 if let Some(name) = info.app_name {
252 msg.push_str(&name);
253 }
254 if let Some(ver) = info.app_version {
255 msg.push_str(&format!("({})", ver));
256 }
257 if msg.is_empty() {
258 "信息获取失败".to_string()
259 } else {
260 msg
261 }
262 }
263 Err(_) => "信息获取失败".to_string(),
264 },
265 Err(_) => "信息获取失败".to_string(),
266 }
267}
268
269#[cfg(feature = "milky")]
270async fn get_server_info(bot: &RuntimeBot) -> String {
271 use kovi_milky::MilkySystemApi;
272
273 #[derive(Debug, serde::Deserialize)]
274 struct ImplInfo {
275 impl_name: Option<String>,
276 impl_version: Option<String>,
277 qq_protocol_version: Option<String>,
278 qq_protocol_type: Option<String>,
279 milky_version: Option<String>,
280 }
281
282 match bot.get_impl_info().await {
283 Ok(v) => match serde_json::from_value::<ImplInfo>(v.data) {
284 Ok(info) => {
285 let name = info.impl_name.unwrap_or_else(|| "未知".to_string());
286 let ver = info.impl_version.unwrap_or_default();
287 let qq_ver = info.qq_protocol_version.unwrap_or_default();
288 let qq_type = info.qq_protocol_type.unwrap_or_default();
289 let milky_ver = info.milky_version.unwrap_or_default();
290
291 let mut msg = format!("{} ({})", name, ver);
292 if !qq_ver.is_empty() || !qq_type.is_empty() {
293 msg.push_str(&format!("\n QQ: {} {}", qq_type, qq_ver));
294 }
295 if !milky_ver.is_empty() {
296 msg.push_str(&format!("\n Milky: v{}", milky_ver));
297 }
298 msg
299 }
300 Err(_) => "信息获取失败".to_string(),
301 },
302 Err(_) => "信息获取失败".to_string(),
303 }
304}
305
306fn acc(e: &AdminMsgEvent, bot: &RuntimeBot, plugin_name: &str, acc_cmd: AccControlCmd) {
307 let plugin_name = is_not_empty_or_more_times_and_reply(e, bot, plugin_name);
308
309 let plugin_name = match plugin_name {
310 Some(v) => v,
311 None => return,
312 };
313
314 if plugin_is_self(&plugin_name) && acc_cmd != AccControlCmd::Status {
315 e.reply("⛔ 不允许修改CMD插件");
316 return;
317 }
318 match acc_cmd {
319 AccControlCmd::Enable(b) => match bot.set_plugin_access_control(&plugin_name, b) {
320 Ok(_) => {
321 e.reply("✅ 设置成功");
322 }
323 Err(err) => match err {
324 BotError::PluginNotFound(_) => {
325 e.reply(format!("🔎 插件{}不存在", plugin_name));
326 }
327 BotError::RefExpired => {
328 panic!("CMD: Bot RefExpired");
329 }
330 },
331 },
332 AccControlCmd::SetMode(v) => match bot.set_plugin_access_control_mode(&plugin_name, v) {
333 Ok(_) => {
334 e.reply("✅ 设置成功");
335 }
336 Err(err) => match err {
337 BotError::PluginNotFound(_) => {
338 e.reply(format!("🔎 插件{}不存在", plugin_name));
339 }
340 BotError::RefExpired => {
341 panic!("CMD: Bot RefExpired");
342 }
343 },
344 },
345 AccControlCmd::Change(change) => match change {
346 CmdSetAccessControlList::GroupAdds(v) => {
347 process_ids(v, true, true, &plugin_name, bot, e);
348 }
349 CmdSetAccessControlList::GroupRemoves(v) => {
350 process_ids(v, true, false, &plugin_name, bot, e);
351 }
352 CmdSetAccessControlList::FriendAdds(v) => {
353 process_ids(v, false, true, &plugin_name, bot, e);
354 }
355 CmdSetAccessControlList::FriendRemoves(v) => {
356 process_ids(v, false, false, &plugin_name, bot, e);
357 }
358 },
359 AccControlCmd::Status => {
360 let plugin_infos = match bot.get_plugin_info() {
361 Ok(v) => v,
362 Err(_) => panic!("CMD: Bot RefExpired"),
363 };
364
365 for info in plugin_infos {
366 if info.name == plugin_name {
367 let boo = if info.access_control { "✅" } else { "❎" };
368 let mode = match info.list_mode {
369 AccessControlMode::BlackList => "黑名单",
370 AccessControlMode::WhiteList => "白名单",
371 };
372 let list = info.access_list;
373 let group_list = list.groups;
374 let friend_list = list.friends;
375 let group_list_str = if group_list.is_empty() {
376 "无".to_string()
377 } else {
378 group_list
379 .iter()
380 .map(|v| v.to_string())
381 .collect::<Vec<String>>()
382 .join(", ")
383 };
384 let friend_list = if friend_list.is_empty() {
385 "无".to_string()
386 } else {
387 friend_list
388 .iter()
389 .map(|v| v.to_string())
390 .collect::<Vec<String>>()
391 .join(", ")
392 };
393
394 let msg = format!(
395 "📦 插件{}\n访问控制:{}\n模式:{}\n群组:{}\n好友列表:{}",
396 plugin_name, boo, mode, group_list_str, friend_list
397 );
398 e.reply(msg);
399 return;
400 }
401 }
402
403 e.reply("🔎 插件不存在");
404 }
405 AccControlCmd::GroupIsEnable(boo) => {
406 if e.is_private() {
407 e.reply("⛔ 只能在群聊中使用");
408 return;
409 }
410
411 let group_id = e.get_group_id().unwrap();
412 let set_access = if boo {
413 SetAccessControlList::Add(group_id.into())
414 } else {
415 SetAccessControlList::Remove(group_id.into())
416 };
417
418 match bot.set_plugin_access_control_list(&plugin_name, true, set_access) {
419 Ok(_) => {
420 let msg = if boo {
421 format!("✅ 插件{}访问控制已添加{}", plugin_name, group_id)
422 } else {
423 format!("✅ 插件{}访问控制已移除{}", plugin_name, group_id)
424 };
425 e.reply(msg);
426 }
427 Err(err) => match err {
428 BotError::PluginNotFound(_) => {
429 e.reply(format!("🔎 插件{}不存在", plugin_name));
430 }
431 BotError::RefExpired => {
432 panic!("CMD: Bot RefExpired");
433 }
434 },
435 }
436 }
437 }
438}
439
440fn process_ids(
442 v: Vec<String>,
443 is_group: bool,
444 is_add: bool,
445 plugin_name: &str,
446 bot: &RuntimeBot,
447 e: &AdminMsgEvent,
448) {
449 let mut vec_id: Vec<ID> = Vec::new();
450
451 for str in v {
452 match str.parse::<i64>() {
453 Ok(v) => {
454 vec_id.push(ID::new(v));
455 }
456 Err(_) => {
457 e.reply("❎ 设置失败");
458 return;
459 }
460 }
461 }
462
463 let vec_i64 = if is_add {
464 SetAccessControlList::Adds(vec_id)
465 } else {
466 SetAccessControlList::Removes(vec_id)
467 };
468
469 match bot.set_plugin_access_control_list(plugin_name, is_group, vec_i64) {
470 Ok(_) => {
471 e.reply("✅ 设置成功");
472 }
473 Err(err) => match err {
474 BotError::PluginNotFound(_) => {
475 e.reply(format!("🔎 插件{}不存在", plugin_name));
476 }
477 BotError::RefExpired => {
478 panic!("CMD: Bot RefExpired");
479 }
480 },
481 }
482}
483
484fn plugin_start(e: &AdminMsgEvent, bot: &RuntimeBot, name: &str) {
485 let name = is_not_empty_or_more_times_and_reply(e, bot, name);
486
487 let name = match name {
488 Some(v) => v,
489 None => return,
490 };
491
492 if plugin_is_self(&name) {
493 e.reply("🏳️ 这么做...,你想干嘛");
494 return;
495 }
496 match bot.enable_plugin(&name) {
497 Ok(_) => {
498 e.reply(format!("✅ 插件{}启动成功", name));
499 }
500 Err(err) => match err {
501 BotError::PluginNotFound(_) => {
502 e.reply(format!("🔎 插件{}不存在", name));
503 }
504 BotError::RefExpired => {
505 panic!("CMD: Bot RefExpired");
506 }
507 },
508 }
509}
510
511fn plugin_stop(e: &AdminMsgEvent, bot: &RuntimeBot, name: &str) {
512 let name = is_not_empty_or_more_times_and_reply(e, bot, name);
513
514 let name = match name {
515 Some(v) => v,
516 None => return,
517 };
518
519 if plugin_is_self(&name) {
520 e.reply("⛔ 不允许关闭CMD插件");
521 return;
522 }
523 match bot.disable_plugin(&name) {
524 Ok(_) => {
525 e.reply(format!("✅ 插件{}关闭成功", name));
526 }
527 Err(err) => match err {
528 BotError::PluginNotFound(_) => {
529 e.reply(format!("🔎 插件{}不存在", name));
530 }
531 BotError::RefExpired => {
532 panic!("CMD: Bot RefExpired");
533 }
534 },
535 }
536}
537
538async fn plugin_restart(e: &AdminMsgEvent, bot: &RuntimeBot, name: &str) {
539 let name = is_not_empty_or_more_times_and_reply(e, bot, name);
540
541 let name = match name {
542 Some(v) => v,
543 None => return,
544 };
545
546 if plugin_is_self(&name) {
547 e.reply("⛔ 不允许重载CMD插件");
548 return;
549 }
550 match bot.restart_plugin(&name).await {
551 Ok(_) => {
552 e.reply(format!("✅ 插件{}重载成功", name));
553 }
554 Err(err) => match err {
555 BotError::PluginNotFound(_) => {
556 e.reply(format!("🔎 插件{}不存在", name));
557 }
558 BotError::RefExpired => {
559 panic!("CMD: Bot RefExpired");
560 }
561 },
562 }
563}
564
565fn plugin_status(e: &AdminMsgEvent, bot: &RuntimeBot) {
566 let plugin_info = bot.get_plugin_info().unwrap();
567 if plugin_info.is_empty() {
568 e.reply("🔎 插件列表为空");
569 return;
570 }
571
572 let mut msg = "┄ 📑 插件列表 ┄\n".to_string();
573
574 plugin_info.iter().for_each(|info| {
575 let boo = if info.enabled { "✅" } else { "❎" };
576
577 let msg_ = format!("{} {}(v{})\n", boo, info.name, info.version);
578 msg.push_str(&msg_);
579 });
580
581 e.reply(msg.trim());
582}
583
584fn is_not_empty_or_more_times_and_reply(
586 e: &AdminMsgEvent,
587 bot: &RuntimeBot,
588 name: &str,
589) -> Option<String> {
590 let names = match get_plugin_full_name(bot, name) {
591 Ok(names) => names,
592 Err(err) => {
593 log::error!("CMD: {}", err);
594 panic!("{err}")
595 }
596 };
597
598 if names.is_empty() {
599 e.reply("🔎 插件列表为空");
600 return None;
601 } else if names.len() > 1 {
602 let full_name = names.iter().find(|n| n == &name);
604 if let Some(full_name) = full_name {
605 return Some(full_name.clone());
606 }
607
608 e.reply(format!("┄ 🔎 寻找到多个插件 ┄\n{}", names.join("\n")));
609 return None;
610 }
611
612 names.into_iter().next()
613}
614
615fn get_plugin_full_name(bot: &RuntimeBot, name: &str) -> Result<Vec<String>, BotError> {
616 let plugins = match bot.get_plugin_info() {
617 Ok(plugins) => plugins,
618 Err(err) => {
619 log::error!("CMD: {}", err);
620 return Err(err);
621 }
622 };
623
624 let names = plugins
625 .iter()
626 .filter_map(|v| {
627 if v.name.contains(name) {
628 Some(v.name.clone())
629 } else {
630 None
631 }
632 })
633 .collect();
634
635 Ok(names)
636}
637
638fn plugin_is_self(name: &str) -> bool {
639 name == env!("CARGO_PKG_NAME")
640}
641
642#[test]
643fn test_parse() {
644 let cmd = KoviArgs::parse(vec![".kovi".to_string()]);
645
646 println!("{:?}", cmd);
647}