kovi_plugin_live_agent/
util.rs1use kovi::{
4 tokio::time::{interval, sleep},
5 Message,
6};
7use rand::{thread_rng, Rng};
8use serde::{Deserialize, Serialize};
9use std::{future::Future, path::PathBuf, time::Duration};
10use time::{
11 macros::{format_description, offset},
12 OffsetDateTime,
13};
14
15use crate::{
16 db_warn, exception::PluginResult, global_state, std_db_error, std_info, store, BOT_QQ, CONFIG,
17};
18
19pub async fn schedule_task_blocking<F, Fut>(duration: Duration, mut task: F)
21where
22 F: FnMut() -> Fut,
23 Fut: Future<Output = ()>,
24{
25 let mut timer = interval(duration);
26 loop {
27 timer.tick().await;
28 task().await;
29 }
30}
31
32pub async fn sleep_rand_time() {
33 let config = CONFIG.get().unwrap();
34 let max_sleep_sec = config.global.max_sleep_sec as f64;
35 let rand_time = {
36 let mut rng = thread_rng();
37 rng.gen_range(0.0..max_sleep_sec)
38 };
39 std_info!("Sleep {rand_time} seconds.");
40 sleep(Duration::from_secs_f64(rand_time)).await;
41}
42
43pub async fn extract_text(msg: &Message) -> String {
44 let text_segs = msg.get("text");
45 let mut buf = String::new();
46 for seg in text_segs {
47 let Ok(content) = serde_json::from_value::<String>(seg.data["text"].clone()) else {
48 std_db_error!(
49 "
50 Extract text: data object inside text segment has no text field
51 Data: {}
52 ",
53 seg.data
54 );
55 return buf;
56 };
57 buf.push_str(&content);
58 buf.push('\n');
59 }
60 buf
61}
62
63pub async fn extract_segments<T>(msg: T) -> Vec<(String, String)>
64where
65 T: Into<Message>,
66{
67 let message = msg.into();
68 let len = message.iter().len();
69 let mut list = Vec::with_capacity(len);
70 for seg in message.iter() {
71 let seg_type = seg.type_.clone();
72 let content: Option<String> = match seg_type.as_str() {
73 "text" => serde_json::from_value(seg.data["text"].clone()).ok(),
74 "image" | "record" | "video" => serde_json::from_value(seg.data["file"].clone()).ok(),
75 "at" => serde_json::from_value(seg.data["qq"].clone()).ok(),
76 "share" => serde_json::from_value(seg.data["url"].clone()).ok(),
77 "contact" | "reply" | "forward" | "node" => {
78 serde_json::from_value(seg.data["id"].clone()).ok()
79 }
80 _ => None,
81 };
82 let Some(content) = content else {
83 db_warn!(
84 "
85 Skip extract segment that is not pre-defined:
86 Data: {}
87 ",
88 seg.data
89 );
90 continue;
91 };
92 list.push((seg_type, content));
93 }
94 list
95}
96
97pub fn cur_time_iso8601() -> String {
99 let offset = offset!(+8);
100 let datetime = OffsetDateTime::now_utc().to_offset(offset);
101 let desc = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
102 datetime.format(desc).unwrap()
103}
104
105pub fn iso8601_from_timestamp(timestamp: i64) -> PluginResult<String> {
108 let offset = offset!(+8);
109 let datetime = OffsetDateTime::from_unix_timestamp(timestamp)?.to_offset(offset);
110 let desc = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
111 Ok(datetime.format(desc)?)
112}
113
114pub enum TimeRepr {
117 Iso8601(String),
118 UnixTimeStamp(i64),
119}
120
121impl TimeRepr {
122 pub async fn to_iso8601(&self) -> Option<String> {
124 match self {
125 Self::Iso8601(t) => Some(t.clone()),
126 Self::UnixTimeStamp(t) => match iso8601_from_timestamp(*t) {
127 Ok(t) => Some(t),
128 Err(err) => {
129 std_db_error!("{err}");
130 None
131 }
132 },
133 }
134 }
135}
136
137impl Default for TimeRepr {
138 fn default() -> Self {
139 Self::Iso8601(cur_time_iso8601())
140 }
141}
142
143pub async fn get_name_in_group(group_id: i64, user_id: i64) -> String {
151 let config = CONFIG.get().unwrap();
154 if let Some(ref groups) = config.groups {
155 if let Some(group) = groups.iter().find(|&g| g.id == group_id) {
156 if let Some(ref agent) = group.agent {
157 if let Some((name, _)) = agent.known_members.get(&user_id.to_string()) {
159 return name.to_string();
160 }
161 }
162 }
163 }
164
165 let bot = global_state::get_bot();
167 let group_member_api = bot.get_group_member_info(group_id, user_id, false).await;
168
169 match group_member_api {
170 Ok(api) => {
171 let group_member_info =
173 serde_json::from_value::<GroupMemberInfoResponse>(api.data.clone());
174 match group_member_info {
175 Ok(info) => {
176 let first_non_empty = [info.card, info.nickname]
179 .into_iter()
180 .find(|x| !x.is_empty());
181 match first_non_empty {
182 Some(name) => name,
183 None => user_id.to_string(),
185 }
186 }
187 Err(err) => {
188 std_db_error!(
191 "
192 GroupMemberInfo deserialize failed.
193 Cause: {err}
194 Data: {}
195 ",
196 api.data
197 );
198 user_id.to_string()
200 }
201 }
202 }
203 Err(err) => {
204 std_db_error!(
207 "
208 GroupMemberInfo api request failed.
209 Cause: {err}
210 "
211 );
212 user_id.to_string()
213 }
214 }
215}
216
217pub async fn send_group_and_log<T>(group_id: i64, message: T)
220where
221 T: Into<Message>,
222 T: Serialize,
223{
224 let bot = global_state::get_bot();
225 let message: Message = message.into();
226 let sender_id = *BOT_QQ.get().unwrap();
227 bot.send_group_msg(group_id, message.clone());
228 store::write_group_msg(group_id, 0, None, sender_id, message).await;
229}
230
231pub async fn call_upload(file_path_str: &str) -> String {
237 let config = CONFIG.get().unwrap();
238 let Some(ref obj) = config.object_storage else {
240 return file_path_str.to_string();
241 };
242
243 let exec_path_str = &obj.script_path;
245 let exec_path = PathBuf::from(exec_path_str);
246 let Ok(abs_exec) = exec_path.canonicalize() else {
247 std_db_error!("Script path cannot be parsed to an absolute path: {exec_path_str}");
248 return file_path_str.to_string();
249 };
250 let abs_exec_str = abs_exec.to_string_lossy().to_string();
251
252 let file_path = PathBuf::from(file_path_str);
254 let abs_file_str = file_path.to_string_lossy().to_string();
255
256 std_info!("Execute script: {abs_exec_str}, Argument: {abs_file_str}");
257
258 let mut cmd = kovi::tokio::process::Command::new(abs_exec_str);
260 let output = match cmd.arg(abs_file_str).output().await {
261 Ok(out) => out,
262 Err(err) => {
263 std_db_error!("Launch process failed: {err}");
264 return file_path_str.to_string();
265 }
266 };
267
268 if !output.status.success() {
269 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
271 std_db_error!(
272 "
273 Upload script failed.
274 Stderr: {stderr}
275 "
276 );
277 return file_path_str.to_string();
278 }
279 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
280 std_info!("Upload script succeed with online path: {stdout}");
281 stdout
282}
283
284#[derive(Deserialize, Debug, PartialEq, Eq)]
285pub struct GroupMemberInfoResponse {
286 group_id: i64,
287 user_id: i64,
288 nickname: String,
289 card: String,
290 sex: String,
291 age: i32,
292 area: String,
293 join_time: i32,
294 last_sent_time: i32,
295 level: String,
296 role: String,
297 unfriendly: bool,
298 title: String,
299 title_expire_time: i32,
300 card_changeable: bool,
301}