Skip to content

Commit fdea948

Browse files
committed
improve user autocomplete for commands, add db user lookup
1 parent 9be1e2f commit fdea948

6 files changed

Lines changed: 127 additions & 21 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ metrics-process = "2.4.3"
7474
aho-corasick = { version = "1.1.4", optional = true }
7575
async-watcher = { version = "0.4.0", optional = true }
7676
mimalloc = { version = "0.1.48", optional = true }
77+
fuzzy-matcher = { version = "0.3.7", optional = true }
7778
validator = { version = "0.20.0", features = ["derive"] }
7879

7980
# Analytics
@@ -111,7 +112,7 @@ docker-alpine-build = ["all", "mimalloc"]
111112
word-filter = ["dep:aho-corasick", "dep:async-watcher"]
112113
featured-levels = ["dep:google-sheets4"]
113114
default = []
114-
discord = ["dep:poise", "dep:plotters", "dep:image"]
115+
discord = ["dep:poise", "dep:plotters", "dep:image", "dep:fuzzy-matcher"]
115116
quic = ["server-shared/quic"]
116117
websocket = ["server-shared/websocket"]
117118
mimalloc = ["dep:mimalloc"]

src/discord/commands/moderation.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub async fn punish(
3939
#[description = "Punishment type"]
4040
punishment_type: String,
4141

42-
#[autocomplete = "online_user_autocomplete"]
42+
#[autocomplete = "online_and_db_user_autocomplete"]
4343
#[description = "Geometry Dash username or ID"]
4444
target_user: String,
4545

@@ -88,7 +88,7 @@ pub async fn unpunish(
8888
#[autocomplete = "punish_autocomplete"]
8989
#[description = "Punishment type"]
9090
punishment_type: String,
91-
#[autocomplete = "online_user_autocomplete"]
91+
#[autocomplete = "online_and_db_user_autocomplete"]
9292
#[description = "Geometry Dash username or ID"]
9393
target_user: String,
9494
) -> Result<(), BotError> {
@@ -300,7 +300,9 @@ pub async fn check_actions(
300300
#[poise::command(slash_command, guild_only = true)]
301301
pub async fn check_alts(
302302
ctx: Context<'_>,
303-
#[description = "GD username or account ID of the target user"] user: String,
303+
#[autocomplete = "db_user_autocomplete"]
304+
#[description = "GD username or account ID of the target user"]
305+
user: String,
304306
) -> Result<(), BotError> {
305307
check_moderator(ctx).await?;
306308

src/discord/commands/util.rs

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
use std::{sync::Arc, time::Duration};
1+
use std::{cmp::Reverse, sync::Arc, time::Duration};
22

3+
use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
4+
use itertools::Itertools;
35
use poise::{
46
CreateReply, ReplyHandle,
57
serenity_prelude::{self as serenity, AutocompleteChoice},
@@ -8,7 +10,7 @@ use server_shared::qunet::server::Server;
810
use thiserror::Error;
911

1012
use crate::{
11-
core::handler::ConnectionHandler,
13+
core::handler::{ClientStateHandle, ConnectionHandler},
1214
discord::{BotError, state::BotState},
1315
users::{ComputedRole, DbUser, UserPunishmentType, UsersModule},
1416
};
@@ -131,24 +133,75 @@ pub fn parse_duration_str(s: &str) -> Result<Duration, ParseDurationError> {
131133
Ok(Duration::from_secs(number * modifier))
132134
}
133135

134-
pub async fn online_user_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
136+
fn fuzzy_match(target: &str, candidate: &str) -> i64 {
137+
let matcher = SkimMatcherV2::default();
138+
matcher.fuzzy_match(target, candidate).unwrap_or(-1)
139+
}
140+
141+
fn wrap_user_autocomplete<'a>(
142+
query: &str,
143+
iter: impl Iterator<Item = (&'a str, i32)>,
144+
) -> Vec<AutocompleteChoice> {
145+
iter.sorted_by_key(|(username, id)| {
146+
if let Ok(query_id) = query.parse::<i32>() {
147+
if *id == query_id {
148+
return Reverse(i64::MAX); // highest priority if ID matches
149+
}
150+
}
151+
152+
if username.eq_ignore_ascii_case(query) {
153+
return Reverse(i64::MAX - 1); // second highest priority if username matches
154+
}
155+
156+
// fuzzy match on username
157+
Reverse(fuzzy_match(username, query))
158+
})
159+
.map(|(username, id)| AutocompleteChoice::new(username.to_owned(), id.to_string()))
160+
.take(10)
161+
.collect()
162+
}
163+
164+
fn get_online_users_matching(ctx: Context<'_>, partial: &str) -> Vec<ClientStateHandle> {
135165
let server = ctx.data().server().unwrap();
136-
let clients = server.handler().get_n_clients_matching(partial, 5);
166+
let mut clients = server.handler().get_n_clients_matching(partial, 10);
167+
168+
if let Ok(query_id) = partial.parse::<i32>() {
169+
if let Some(client) = server.handler().find_client(query_id) {
170+
clients.push(client);
171+
}
172+
}
137173

138174
clients
139-
.into_iter()
140-
.map(|c| AutocompleteChoice::new(c.username().to_string(), c.account_id().to_string()))
141-
.collect()
142175
}
143176

144-
// async fn db_user_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
145-
// let server = ctx.data().server().unwrap();
146-
// let users = server.handler().module::<UsersModule>();
177+
async fn get_db_users_matching(ctx: Context<'_>, partial: &str) -> Vec<DbUser> {
178+
let server = ctx.data().server().unwrap();
179+
let users = server.handler().module::<UsersModule>();
147180

148-
// users.query_user();
181+
users.query_matching_users(partial, 50).await.unwrap_or_default()
182+
}
149183

150-
// clients
151-
// .into_iter()
152-
// .map(|c| AutocompleteChoice::new(c.username().to_string(), c.account_id().to_string()))
153-
// .collect()
154-
// }
184+
pub async fn online_user_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
185+
let clients = get_online_users_matching(ctx, partial);
186+
wrap_user_autocomplete(partial, clients.iter().map(|c| (c.username(), c.account_id())))
187+
}
188+
189+
pub async fn db_user_autocomplete(ctx: Context<'_>, partial: &str) -> Vec<AutocompleteChoice> {
190+
let users = get_db_users_matching(ctx, partial).await;
191+
wrap_user_autocomplete(partial, users.iter().map(|u| (u.username(), u.account_id)))
192+
}
193+
194+
pub async fn online_and_db_user_autocomplete(
195+
ctx: Context<'_>,
196+
partial: &str,
197+
) -> Vec<AutocompleteChoice> {
198+
let mut vec = Vec::new();
199+
200+
let first = get_online_users_matching(ctx, partial);
201+
vec.extend(first.iter().map(|c| (c.username(), c.account_id())));
202+
203+
let second = get_db_users_matching(ctx, partial).await;
204+
vec.extend(second.iter().map(|u| (u.username(), u.account_id)));
205+
206+
wrap_user_autocomplete(partial, vec.into_iter())
207+
}

src/users/database/mod.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,34 @@ impl UsersDb {
227227
}
228228
}
229229

230+
pub async fn query_matching_users(
231+
&self,
232+
query: &str,
233+
limit: usize,
234+
) -> DatabaseResult<Vec<DbUser>> {
235+
let mut out = Vec::new();
236+
237+
// similar logic as query_user
238+
239+
if let Ok(id) = query.parse::<i32>() {
240+
if let Some(user) = User::find_by_id(id).one(&self.conn).await? {
241+
out.push(self.post_user_fetch(user).await?);
242+
}
243+
};
244+
245+
let users = User::find()
246+
.filter(user::Column::Username.contains(query))
247+
.limit(limit as u64)
248+
.all(&self.conn)
249+
.await?;
250+
251+
for user in users {
252+
out.push(self.post_user_fetch(user).await?);
253+
}
254+
255+
Ok(out)
256+
}
257+
230258
pub async fn query_user_with_role(&self, role_id: &str) -> DatabaseResult<Vec<DbUser>> {
231259
let users =
232260
User::find().filter(user::Column::Roles.contains(role_id)).all(&self.conn).await?;
@@ -894,7 +922,10 @@ impl DbUser {
894922
}
895923

896924
pub fn username(&self) -> &str {
897-
self.username.as_deref().unwrap_or("<unknown>")
925+
match self.username.as_deref() {
926+
Some(name) if !name.is_empty() => name,
927+
_ => "Unknown",
928+
}
898929
}
899930
}
900931

src/users/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,15 @@ impl UsersModule {
320320
self.db.query_user(query).await
321321
}
322322

323+
/// Queries users with a similar username
324+
pub async fn query_matching_users(
325+
&self,
326+
query: &str,
327+
max_users: usize,
328+
) -> DatabaseResult<Vec<DbUser>> {
329+
self.db.query_matching_users(query, max_users).await
330+
}
331+
323332
/// Query a user by account ID or username, creating them if they don't exist
324333
/// If the user does not exist, this will fetch the data from GD servers
325334
pub async fn query_or_create_user(&self, query: &str) -> Result<Option<DbUser>, Error> {

0 commit comments

Comments
 (0)