use poise::serenity_prelude as serenity; use crate::Data; use crate::copyparty::{html_decode, percent_decode}; use crate::error::Error; type Context<'a> = poise::Context<'a, Data, Error>; const MAX_CHOICE_LEN: usize = 99; fn filename_label(raw_path: &str) -> Option { let raw_name = raw_path.rsplit('/').next()?; if raw_name.is_empty() { return None; } let decoded = html_decode(&percent_decode(raw_name)); if decoded.len() > MAX_CHOICE_LEN { let mut s: String = decoded.chars().take(MAX_CHOICE_LEN - 1).collect(); s.push('\u{2026}'); Some(s) } else { Some(decoded) } } pub async fn audio_file<'a>( ctx: Context<'_>, partial: &'a str, ) -> Vec { eprintln!("[DEBUG] autocomplete audio_file(partial={partial:?})"); let results = ctx .data() .copyparty .search(partial, 25, &["mp3", "wav", "ogg", "flac", "m4a", "opus"]) .await; match results { Ok(hits) => { eprintln!("[DEBUG] audio_file got {} raw hits", hits.len()); let mut choices = Vec::new(); for (i, hit) in hits.into_iter().enumerate() { let rp = match &hit.rp { Some(p) => p.clone(), None => continue, }; eprintln!( "[DEBUG] hit[{i}]: rp={rp:?}, url={:?}, name={:?}, path={:?}", hit.url, hit.name, hit.path ); let Some(label) = filename_label(&rp) else { continue; }; // Stash under the choice VALUE (`rp`), not the display label: // the command receives `rp` as its argument and looks the hit // up by that same key, so the cached link is reused instead of // triggering a fresh copyparty search. ctx.data().ac_stash(ctx.author().id, rp.clone(), hit).await; eprintln!("[DEBUG] -> cached as {rp:?}"); choices.push(serenity::AutocompleteChoice::new(label.clone(), rp)); if choices.len() >= 25 { break; } } eprintln!("[DEBUG] audio_file returning {} choices", choices.len()); choices } Err(e) => { eprintln!("[DEBUG] audio_file search failed: {e}"); vec![] } } } pub async fn any_file<'a>(ctx: Context<'_>, partial: &'a str) -> Vec { eprintln!("[DEBUG] autocomplete any_file(partial={partial:?})"); let results = ctx.data().copyparty.search(partial, 25, &[]).await; match results { Ok(hits) => { eprintln!("[DEBUG] any_file got {} raw hits", hits.len()); let mut choices = Vec::new(); for (i, hit) in hits.into_iter().enumerate() { let rp = match &hit.rp { Some(p) => p.clone(), None => continue, }; eprintln!( "[DEBUG] hit[{i}]: rp={rp:?}, url={:?}, name={:?}, path={:?}", hit.url, hit.name, hit.path ); let Some(label) = filename_label(&rp) else { continue; }; ctx.data() .ac_stash(ctx.author().id, label.clone(), hit) .await; eprintln!("[DEBUG] -> cached as {label:?}"); choices.push(serenity::AutocompleteChoice::new(label.clone(), label)); if choices.len() >= 25 { break; } } eprintln!("[DEBUG] any_file returning {} choices", choices.len()); choices } Err(e) => { eprintln!("[DEBUG] any_file search failed: {e}"); vec![] } } } pub async fn trigger_name<'a>(ctx: Context<'_>, partial: &'a str) -> Vec { eprintln!("[DEBUG] autocomplete trigger_name(partial={partial:?})"); let db_pool = &ctx.data().db_pool; match crate::database::list_triggers(db_pool).await { Ok(triggers) => { let names: Vec<_> = triggers .into_iter() .map(|t| t.name) .filter(|name| partial.is_empty() || name.starts_with(partial)) .take(25) .collect(); eprintln!("[DEBUG] trigger_name returning {} names", names.len()); names } Err(e) => { eprintln!("[DEBUG] trigger_name query failed: {e}"); vec![] } } }