1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
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<String> {
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<serenity::AutocompleteChoice> {
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<serenity::AutocompleteChoice> {
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<String> {
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![]
}
}
}
|