aboutsummaryrefslogtreecommitdiffstats
path: root/src/events
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/events/mod.rs368
1 files changed, 368 insertions, 0 deletions
diff --git a/src/events/mod.rs b/src/events/mod.rs
new file mode 100644
index 0000000..457ebcd
--- /dev/null
+++ b/src/events/mod.rs
@@ -0,0 +1,368 @@
1use chrono::{Local, Utc};
2use poise::serenity_prelude as serenity;
3use rand::seq::IndexedRandom;
4use tracing::{info, warn};
5
6use crate::Data;
7use crate::database::{self, VoiceActivity};
8use crate::error::Result;
9use crate::lang;
10
11pub async fn handle_ready(_ctx: &serenity::Context) -> Result<()> {
12 info!("{}", lang::L_EV_READY);
13 Ok(())
14}
15
16fn format_duration(seconds: f64) -> String {
17 let total = seconds as u64;
18 let hours = total / 3600;
19 let minutes = (total % 3600) / 60;
20 let secs = total % 60;
21 if hours > 0 {
22 format!("{hours}h {minutes}m {secs}s")
23 } else if minutes > 0 {
24 format!("{minutes}m {secs}s")
25 } else {
26 format!("{secs}s")
27 }
28}
29
30async fn play_tts_in_channel(
31 _ctx: &serenity::Context,
32 data: &Data,
33 guild_id: serenity::GuildId,
34 channel_id: serenity::ChannelId,
35 text: &str,
36) {
37 let tts_url = format!(
38 "https://translate.google.com/translate_tts?ie=UTF-8&q={}&tl=pl&client=tw-ob",
39 crate::commands::tts::urlencoding(text),
40 );
41
42 let audio_data = match data.http_client.get(&tts_url).send().await {
43 Ok(resp) => match resp.bytes().await {
44 Ok(b) => b,
45 Err(e) => {
46 warn!("Failed to read TTS audio bytes: {e}");
47 return;
48 }
49 },
50 Err(e) => {
51 warn!("Failed to fetch TTS audio: {e}");
52 return;
53 }
54 };
55
56 let call = match data.voice_manager.connect(guild_id, channel_id).await {
57 Ok(c) => c,
58 Err(e) => {
59 warn!("Failed to connect to voice for TTS: {e}");
60 return;
61 }
62 };
63
64 let input = match crate::voice::decode_audio(&audio_data) {
65 Ok(i) => i,
66 Err(e) => {
67 warn!("Failed to decode TTS audio: {e}");
68 return;
69 }
70 };
71
72 let mut call_lock = call.lock().await;
73 if call_lock.current_channel().is_some() {
74 call_lock.play_input(input);
75 }
76}
77
78pub async fn handle_voice_state_update(
79 ctx: &serenity::Context,
80 data: &Data,
81 old: Option<serenity::VoiceState>,
82 new: &serenity::VoiceState,
83) -> Result<()> {
84 let log_channel = data.config.status_channel_id.map(serenity::ChannelId::new);
85 let current_ts = Utc::now().timestamp();
86 let current_time = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
87
88 let user_id = new.user_id;
89 let guild_id = new.guild_id;
90
91 // skip bots (Python behavior)
92 let member = guild_id
93 .and_then(|gid| ctx.cache.guild(gid))
94 .and_then(|g| g.members.get(&user_id).cloned());
95 if let Some(ref m) = member {
96 if m.user.bot {
97 return Ok(());
98 }
99 }
100
101 let member_name = member
102 .as_ref()
103 .map(|m| m.user.name.clone())
104 .unwrap_or_default();
105 let display_name = member
106 .as_ref()
107 .map(|m| m.display_name().to_string())
108 .unwrap_or_default();
109
110 let now = current_ts as f64;
111
112 let user_id_str = user_id.to_string();
113 let mut va = database::get_voice_activity(&data.db_pool, &user_id_str)
114 .await?
115 .unwrap_or(VoiceActivity {
116 user_id: user_id_str.clone(),
117 join_time: 0.0,
118 total_active: 0.0,
119 total_afk: 0.0,
120 is_afk: 0,
121 afk_start: 0.0,
122 active_start: 0.0,
123 channel_name: String::new(),
124 last_updated: None,
125 });
126
127 let old_channel = old.as_ref().and_then(|v| v.channel_id);
128 let new_channel = new.channel_id;
129
130 let mut tts_text: Option<String> = None;
131 let mut tts_channel: Option<serenity::ChannelId> = None;
132
133 // ── JOIN ──
134 if old_channel.is_none() && new_channel.is_some() {
135 let cid = new_channel.unwrap();
136 let is_afk = new.self_mute || new.self_deaf;
137
138 va.join_time = now;
139 va.total_active = 0.0;
140 va.total_afk = 0.0;
141 va.is_afk = if is_afk { 1 } else { 0 };
142 va.afk_start = if is_afk { now } else { 0.0 };
143 va.active_start = if !is_afk { now } else { 0.0 };
144 va.channel_name = guild_id
145 .and_then(|gid| ctx.cache.guild(gid))
146 .map(|g| {
147 g.channels
148 .get(&cid)
149 .map(|c| c.name.clone())
150 .unwrap_or_default()
151 })
152 .unwrap_or_default();
153 database::upsert_voice_activity(&data.db_pool, &va).await?;
154 info!("User {user_id} joined voice");
155
156 if let Some(lc) = log_channel {
157 let msg = format!(
158 "[{current_time}] {member_name} dołączył na {} <t:{current_ts}:R>",
159 va.channel_name
160 );
161 lc.say(ctx, &msg).await.ok();
162 }
163 tts_text = Some(lang::fmt_ev_joined(&display_name));
164 tts_channel = Some(cid);
165 }
166 // ── LEAVE ──
167 else if old_channel.is_some() && new_channel.is_none() {
168 let ocid = old_channel.unwrap();
169 let channel_name = guild_id
170 .and_then(|gid| ctx.cache.guild(gid))
171 .and_then(|g| g.channels.get(&ocid).map(|c| c.name.clone()))
172 .unwrap_or_default();
173
174 if va.join_time > 0.0 {
175 if va.is_afk == 1 {
176 va.total_afk += now - va.afk_start;
177 } else {
178 va.total_active += (now - va.active_start).max(0.0);
179 }
180
181 let active_str = format_duration(va.total_active);
182 let afk_str = format_duration(va.total_afk);
183
184 if let Some(lc) = log_channel {
185 let msg = format!(
186 "[{current_time}] {member_name} rozłączył się z {} <t:{current_ts}:R>\n\
187 Aktywny: {active_str} | AFK: {afk_str}",
188 channel_name,
189 );
190 lc.say(ctx, &msg).await.ok();
191 }
192 info!("User {user_id} left voice");
193
194 database::upsert_voice_activity(&data.db_pool, &va).await?;
195 } else {
196 if let Some(lc) = log_channel {
197 let msg = format!(
198 "[{current_time}] {member_name} rozłączył się z {channel_name} <t:{current_ts}:R>"
199 );
200 lc.say(ctx, &msg).await.ok();
201 }
202 info!("User {user_id} left voice (no data)");
203 }
204
205 tts_text = Some(lang::fmt_ev_left(&display_name));
206 tts_channel = Some(ocid);
207 }
208 // ── AFK STATE CHANGE (only for tracked users already in channel) ──
209 else if new_channel.is_some() && va.join_time > 0.0 {
210 let is_afk = new.self_mute || new.self_deaf;
211 let was_afk = va.is_afk == 1;
212
213 if was_afk && !is_afk {
214 // Return from AFK
215 let afk_duration = now - va.afk_start;
216 va.total_afk += afk_duration;
217 va.is_afk = 0;
218 va.afk_start = 0.0;
219 va.active_start = now;
220 database::upsert_voice_activity(&data.db_pool, &va).await?;
221
222 let afk_str = format_duration(afk_duration);
223 // Python behavior: after.self_mute is always false here so always "odgłuchnął"
224 let activity = if new.self_mute {
225 "odciszył"
226 } else {
227 "odgłuchnął"
228 };
229
230 if let Some(lc) = log_channel {
231 let msg = format!(
232 "[{current_time}] {member_name} {activity} się na {} <t:{current_ts}:R>\n\
233 AFK: {afk_str}",
234 va.channel_name,
235 );
236 lc.say(ctx, &msg).await.ok();
237 }
238 info!("User {user_id} returned from AFK");
239 } else if !was_afk && is_afk {
240 // Went AFK
241 let active_duration = if va.active_start > 0.0 {
242 (now - va.active_start).max(0.0)
243 } else {
244 0.0
245 };
246 va.total_active += active_duration;
247 va.is_afk = 1;
248 va.afk_start = now;
249 va.active_start = 0.0;
250 database::upsert_voice_activity(&data.db_pool, &va).await?;
251
252 let active_str = format_duration(active_duration);
253 let activity = if new.self_mute {
254 "wyciszył"
255 } else {
256 "ogłuchnął"
257 };
258
259 if let Some(lc) = log_channel {
260 let msg = format!(
261 "[{current_time}] {member_name} {activity} się na {} <t:{current_ts}:R>\n\
262 Aktywny: {active_str}",
263 va.channel_name,
264 );
265 lc.say(ctx, &msg).await.ok();
266 }
267 info!("User {user_id} went AFK");
268 }
269 }
270
271 // ── TTS for mute/deafen (deaf takes priority with early return) ──
272 if old.as_ref().map_or(false, |v| v.self_deaf) != new.self_deaf {
273 let phrase = if new.self_deaf {
274 lang::fmt_ev_deafened(&display_name)
275 } else {
276 lang::fmt_ev_undeafened(&display_name)
277 };
278 if let Some(gid) = guild_id {
279 if let Some(cid) = tts_channel.or(new_channel) {
280 play_tts_in_channel(ctx, data, gid, cid, &phrase).await;
281 }
282 }
283 return Ok(());
284 }
285
286 if old.as_ref().map_or(false, |v| v.self_mute) != new.self_mute {
287 let phrase = if new.self_mute {
288 lang::fmt_ev_muted(&display_name)
289 } else {
290 lang::fmt_ev_unmuted(&display_name)
291 };
292 if let Some(gid) = guild_id {
293 if let Some(cid) = tts_channel.or(new_channel) {
294 play_tts_in_channel(ctx, data, gid, cid, &phrase).await;
295 }
296 }
297 return Ok(());
298 }
299
300 // ── Play join/leave TTS if no mute/deafen change ──
301 if let (Some(text), Some(cid)) = (tts_text, tts_channel) {
302 if let Some(gid) = guild_id {
303 play_tts_in_channel(ctx, data, gid, cid, &text).await;
304 }
305 }
306
307 Ok(())
308}
309
310pub async fn handle_message(
311 ctx: &serenity::Context,
312 data: &Data,
313 new_message: &serenity::Message,
314) -> Result<()> {
315 if new_message.author.id == ctx.cache.current_user().id {
316 return Ok(());
317 }
318
319 let content_lower = new_message.content.to_lowercase();
320 let response_to_send = {
321 let triggers = data.triggers.lock().await;
322 let mut chosen = None;
323 for (_trigger, keywords, responses) in triggers.iter() {
324 if keywords.iter().any(|k| content_lower.contains(k)) {
325 chosen = responses.choose(&mut rand::rng()).cloned();
326 break;
327 }
328 }
329 chosen
330 };
331 if let Some(response) = response_to_send {
332 new_message.channel_id.say(ctx, response).await.ok();
333 }
334
335 let is_mention = new_message.mentions_user_id(ctx.cache.current_user().id);
336 let has_keyword = content_lower.contains(&data.config.assistant_keyword);
337
338 if (is_mention || has_keyword) && data.config.openrouter_api_key.is_some() {
339 let api_key = data.config.openrouter_api_key.clone().unwrap();
340 let model = data.config.openrouter_model.clone().unwrap_or_else(|| {
341 "cognitivecomputations/dolphin-mistral-24b-venice-edition:free".into()
342 });
343
344 let client = crate::openrouter::OpenRouterClient::new(api_key, model);
345 let assistant_prompt = lang::ASSISTANT_SYSTEM_PROMPT;
346
347 let mut messages = Vec::new();
348 let mut current_msg = Some(new_message.clone());
349 for _ in 0..5 {
350 if let Some(msg) = current_msg {
351 messages.push(("user".into(), msg.content.clone()));
352 current_msg = match msg.message_reference.and_then(|r| r.message_id) {
353 Some(ref_id) => msg.channel_id.message(ctx, ref_id).await.ok(),
354 None => None,
355 };
356 } else {
357 break;
358 }
359 }
360 messages.reverse();
361
362 if let Ok(response) = client.chat_with_history(assistant_prompt, messages).await {
363 new_message.reply(ctx, response).await.ok();
364 }
365 }
366
367 Ok(())
368}