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 P::on_admin_msg(move |e| {
71 let bot = bot.clone();
72 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(".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 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 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
393fn 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
537fn 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 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}