kovi_plugin_live_agent/
util.rsuse kovi::{
tokio::time::{interval, sleep},
Message,
};
use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize};
use std::{future::Future, path::PathBuf, time::Duration};
use time::{
macros::{format_description, offset},
OffsetDateTime,
};
use crate::{
db_warn, exception::PluginResult, global_state, std_db_error, std_info, store, BOT_QQ, CONFIG,
};
pub async fn schedule_task_blocking<F, Fut>(duration: Duration, mut task: F)
where
F: FnMut() -> Fut,
Fut: Future<Output = ()>,
{
let mut timer = interval(duration);
loop {
timer.tick().await;
task().await;
}
}
pub async fn sleep_rand_time() {
let config = CONFIG.get().unwrap();
let max_sleep_sec = config.global.max_sleep_sec as f64;
let rand_time = {
let mut rng = thread_rng();
rng.gen_range(0.0..max_sleep_sec)
};
std_info!("Sleep {rand_time} seconds.");
sleep(Duration::from_secs_f64(rand_time)).await;
}
pub async fn extract_text(msg: &Message) -> String {
let text_segs = msg.get("text");
let mut buf = String::new();
for seg in text_segs {
let Ok(content) = serde_json::from_value::<String>(seg.data["text"].clone()) else {
std_db_error!(
"
Extract text: data object inside text segment has no text field
Data: {}
",
seg.data
);
return buf;
};
buf.push_str(&content);
buf.push('\n');
}
buf
}
pub async fn extract_segments<T>(msg: T) -> Vec<(String, String)>
where
T: Into<Message>,
{
let message = msg.into();
let len = message.iter().len();
let mut list = Vec::with_capacity(len);
for seg in message.iter() {
let seg_type = seg.type_.clone();
let content: Option<String> = match seg_type.as_str() {
"text" => serde_json::from_value(seg.data["text"].clone()).ok(),
"image" | "record" | "video" => serde_json::from_value(seg.data["file"].clone()).ok(),
"at" => serde_json::from_value(seg.data["qq"].clone()).ok(),
"share" => serde_json::from_value(seg.data["url"].clone()).ok(),
"contact" | "reply" | "forward" | "node" => {
serde_json::from_value(seg.data["id"].clone()).ok()
}
_ => None,
};
let Some(content) = content else {
db_warn!(
"
Skip extract segment that is not pre-defined:
Data: {}
",
seg.data
);
continue;
};
list.push((seg_type, content));
}
list
}
pub fn cur_time_iso8601() -> String {
let offset = offset!(+8);
let datetime = OffsetDateTime::now_utc().to_offset(offset);
let desc = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
datetime.format(desc).unwrap()
}
pub fn iso8601_from_timestamp(timestamp: i64) -> PluginResult<String> {
let offset = offset!(+8);
let datetime = OffsetDateTime::from_unix_timestamp(timestamp)?.to_offset(offset);
let desc = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]");
Ok(datetime.format(desc)?)
}
pub enum TimeRepr {
Iso8601(String),
UnixTimeStamp(i64),
}
impl TimeRepr {
pub async fn to_iso8601(&self) -> Option<String> {
match self {
Self::Iso8601(t) => Some(t.clone()),
Self::UnixTimeStamp(t) => match iso8601_from_timestamp(*t) {
Ok(t) => Some(t),
Err(err) => {
std_db_error!("{err}");
None
}
},
}
}
}
impl Default for TimeRepr {
fn default() -> Self {
Self::Iso8601(cur_time_iso8601())
}
}
pub async fn get_name_in_group(group_id: i64, user_id: i64) -> String {
let config = CONFIG.get().unwrap();
if let Some(ref groups) = config.groups {
if let Some(group) = groups.iter().find(|&g| g.id == group_id) {
if let Some(ref agent) = group.agent {
if let Some((name, _)) = agent.known_members.get(&user_id.to_string()) {
return name.to_string();
}
}
}
}
let bot = global_state::get_bot();
let group_member_api = bot.get_group_member_info(group_id, user_id, false).await;
match group_member_api {
Ok(api) => {
let group_member_info =
serde_json::from_value::<GroupMemberInfoResponse>(api.data.clone());
match group_member_info {
Ok(info) => {
let first_non_empty = [info.card, info.nickname]
.into_iter()
.find(|x| !x.is_empty());
match first_non_empty {
Some(name) => name,
None => user_id.to_string(),
}
}
Err(err) => {
std_db_error!(
"
GroupMemberInfo deserialize failed.
Cause: {err}
Data: {}
",
api.data
);
user_id.to_string()
}
}
}
Err(err) => {
std_db_error!(
"
GroupMemberInfo api request failed.
Cause: {err}
"
);
user_id.to_string()
}
}
}
pub async fn send_group_and_log<T>(group_id: i64, message: T)
where
T: Into<Message>,
T: Serialize,
{
let bot = global_state::get_bot();
let message: Message = message.into();
let sender_id = *BOT_QQ.get().unwrap();
bot.send_group_msg(group_id, message.clone());
store::write_group_msg(group_id, 0, None, sender_id, message).await;
}
pub async fn call_upload(file_path_str: &str) -> String {
let config = CONFIG.get().unwrap();
let Some(ref obj) = config.object_storage else {
return file_path_str.to_string();
};
let exec_path_str = &obj.script_path;
let exec_path = PathBuf::from(exec_path_str);
let Ok(abs_exec) = exec_path.canonicalize() else {
std_db_error!("Script path cannot be parsed to an absolute path: {exec_path_str}");
return file_path_str.to_string();
};
let abs_exec_str = abs_exec.to_string_lossy().to_string();
let file_path = PathBuf::from(file_path_str);
let abs_file_str = file_path.to_string_lossy().to_string();
std_info!("Execute script: {abs_exec_str}, Argument: {abs_file_str}");
let mut cmd = kovi::tokio::process::Command::new(abs_exec_str);
let output = match cmd.arg(abs_file_str).output().await {
Ok(out) => out,
Err(err) => {
std_db_error!("Launch process failed: {err}");
return file_path_str.to_string();
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
std_db_error!(
"
Upload script failed.
Stderr: {stderr}
"
);
return file_path_str.to_string();
}
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
std_info!("Upload script succeed with online path: {stdout}");
stdout
}
#[derive(Deserialize, Debug, PartialEq, Eq)]
pub struct GroupMemberInfoResponse {
group_id: i64,
user_id: i64,
nickname: String,
card: String,
sex: String,
age: i32,
area: String,
join_time: i32,
last_sent_time: i32,
level: String,
role: String,
unfriendly: bool,
title: String,
title_expire_time: i32,
card_changeable: bool,
}