Skip to main content

kovi_plugin_cmd/
lib.rs

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    // let data_path = bot.get_data_path();
78    // let cmd = CMDInfo {
79    //     cmd_start_with: ".kovi".to_string(),
80    // };
81    // let cmd: CMDInfo = load_json_data(cmd, data_path.join("cmd.json")).unwrap();
82    // let cmd = Arc::new(cmd);
83    P::on_admin_msg(move |e| {
84        let bot = bot.clone();
85        // let cmd = cmd.clone();
86        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(cmd.cmd_start_with.as_str()) {
94            //     return;
95            // }
96
97            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    // 计算运行时间
181    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    // 获取内存使用情况
187    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
440/// 设置插件访问控制列表
441fn 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
584/// 检查插件名是否为空或多个插件名并排除掉全匹配,返回第一个插件名或None,顺带回复
585fn 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        // 检测是否有全匹配
603        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}