kovi_plugin_cmd/
lib.rs

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