kovi_plugin_pet_cat/
lib.rs1mod config;
2
3use kovi::{
4 Message, PluginBuilder as plugin, RuntimeBot,
5 bot::runtimebot::kovi_api::SetAccessControlList,
6 event::GroupMsgEvent,
7 log::{error, info},
8 serde_json::{Value, json},
9 tokio::task::JoinSet,
10};
11use reqwest::Client;
12use std::sync::Arc;
13
14use crate::config::Condition;
15
16const PLUGIN_NAME: &str = "kovi-plugin-pet-cat";
17
18#[kovi::plugin]
19async fn main() {
20 let bot = plugin::get_runtime_bot();
21 let client = Arc::new(reqwest::ClientBuilder::new().build().unwrap());
22
23 let config = config::init(&bot).await.unwrap();
24
25 if let Some(groups) = &config.allow_groups {
26 bot.set_plugin_access_control(PLUGIN_NAME, true).unwrap();
27 bot.set_plugin_access_control_list(
28 PLUGIN_NAME,
29 true,
30 SetAccessControlList::Adds(groups.clone()),
31 )
32 .unwrap();
33 } else {
34 bot.set_plugin_access_control(PLUGIN_NAME, false).unwrap();
35 }
36
37 plugin::on_group_msg({
38 let bot = bot.clone();
39 let client = client.clone();
40 move |msg| on_group_msg(msg, bot.clone(), client.clone())
41 });
42
43 info!("[pet-cat] Ready to pet cats!");
44}
45
46async fn on_group_msg(event: Arc<GroupMsgEvent>, bot: Arc<RuntimeBot>, client: Arc<Client>) {
47 let imgs = event.message.get("image");
48
49 for img in imgs {
50 let map = img.data.as_object();
51 if img.data.as_object().is_none() {
52 info!("[pet-cat] No data provided by image segment. (Strange!)");
53 continue;
54 }
55
56 let url = map.unwrap().get("url");
57 if url.is_none() {
58 info!("[pet-cat] No url provided by image segment. (Strange!)");
59 continue;
60 }
61
62 let mut url = url.unwrap().as_str().unwrap().to_string();
63 if url.starts_with("https") {
64 url = url.replace("https", "http");
65 }
66 if predict_cat(&url, &client).await {
67 info!("[pet-cat] Cat detected, sending pet cat meme...");
68 send_pet_cat(event.group_id, &bot).await;
69 } else {
70 info!("[pet-cat] No cat detected.")
71 }
72 }
73}
74
75async fn predict_cat(url: &str, client: &Arc<Client>) -> bool {
76 info!("[pet-cat] Predicting cat for image: {url}");
77
78 let config = config::CONFIG.get().unwrap();
79 let mut set = JoinSet::new();
80
81 for c in &config.conditions {
82 set.spawn(predict_cond(url.to_string(), client.clone(), &c));
83 }
84
85 let mut flag = true;
86 while let Some(res) = set.join_next().await {
87 match res {
88 Ok(res) => flag &= res,
89 Err(e) => error!("[pet-cat] Error when querying llm: {e}"),
90 }
91 }
92
93 flag
94}
95
96async fn predict_cond(url: String, client: Arc<Client>, cond: &Condition) -> bool {
97 let config = config::CONFIG.get().unwrap();
98 let req = match client
99 .post(&config.api_url)
100 .bearer_auth(&config.api_key)
101 .json(&json!({
102 "model": config.model,
103 "messages": [
104 {
105 "role": "system",
106 "content": [
107 {
108 "type": "text",
109 "text": "你是一个专业的图片分辨专家,可以精确地依据用户的指示,分辨图片中是否包含某一特定物体。**你只能回答 是 或 否,不要做出多余的回答或进行解释**。"
110 }
111 ]
112 },
113 {
114 "role": "user",
115 "content": [
116 {
117 "type": "image_url",
118 "image_url": {
119 "url": url
120 }
121 },
122 {
123 "type": "text",
124 "text": cond.prompt
125 }
126 ]
127 }
128 ],
129 "stream": false,
130 }))
131 .build(){
132 Ok(req) => req,
133 Err(e) => {
134 error!("[pet-cat] Failed to build request: {e}");
135 return false;
136 }
137 };
138
139 let resp = match client.execute(req).await {
140 Ok(resp) => resp,
141 Err(e) => {
142 error!("[pet-cat] Failed to get response: {e}");
143 return false;
144 }
145 };
146
147 let resp: Value = match resp.json().await {
148 Ok(resp) => resp,
149 Err(e) => {
150 error!("[pet-cat] Failed to parse response: {e}");
151 return false;
152 }
153 };
154
155 let resp = resp.as_object().unwrap();
156
157 let Some(result) = resp.get("choices") else {
158 info!("[pet-cat] Invalid response: {resp:?}");
159 return false;
160 };
161
162 let Some(result) = result.as_array().unwrap().first() else {
163 info!("[pet-cat] No choice provided: {resp:?}");
164 return false;
165 };
166
167 let result = result["message"]["content"].as_str();
168
169 let mut flag = false;
170
171 if let Some(s) = result {
172 flag = s.trim() == cond.prediction;
173 }
174
175 info!("[pet-cat] Predicting \"{}\": {flag}", cond.name);
176 flag
177}
178
179async fn send_pet_cat(group: i64, bot: &Arc<RuntimeBot>) {
180 let config = config::CONFIG.get().unwrap();
181 bot.send_group_msg(
182 group,
183 Message::from_value(json!([
184 {
185 "type":"image",
186 "data": {
187 "file": config.pet_cat_img,
188 }
189 }
190 ]))
191 .unwrap(),
192 );
193}