Skip to content

Commit 0eb88ff

Browse files
committed
/names command
1 parent 96756fd commit 0eb88ff

File tree

8 files changed

+220
-0
lines changed

8 files changed

+220
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ Invite link: [bot.2b2t.vc](https://bot.2b2t.vc)
2626
* `/wordcount` -> Counts how many times a word has been seen in chat
2727
* `/watch` -> DM Notifications on player joins, leaves, deaths, kills, and chats
2828
* `/watch-guild` -> Discord server channel notifications on player joins, leaves, deaths, kills, and chats
29+
* `/names` -> Searches for accounts with a given username currently and previously
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package vc.api;
2+
3+
import org.springframework.http.client.ClientHttpRequestFactory;
4+
import org.springframework.stereotype.Component;
5+
import org.springframework.web.client.RestClient;
6+
import vc.api.model.LabyProfileSearchResponse;
7+
8+
@Component
9+
public class LabyRestClient {
10+
private final RestClient restClient;
11+
12+
public LabyRestClient(ClientHttpRequestFactory requestFactory) {
13+
this.restClient = RestClient.builder()
14+
.baseUrl("https://laby.net/api/v3")
15+
.requestFactory(requestFactory)
16+
.defaultHeader("User-Agent", "2b2t.vc-discord")
17+
.build();
18+
}
19+
20+
public LabyProfileSearchResponse searchProfiles(String username) {
21+
return restClient.get()
22+
.uri("/search/profiles/{username}", username)
23+
.retrieve()
24+
.body(LabyProfileSearchResponse.class);
25+
}
26+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package vc.api.model;
2+
3+
public record LabyNameHistoryProfile(String name) {
4+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package vc.api.model;
2+
3+
import java.util.List;
4+
import java.util.UUID;
5+
6+
public record LabyProfile(String name, UUID uuid, List<LabyNameHistoryProfile> history) implements ProfileData {
7+
public List<String> nameHistory() {
8+
return history.stream().map(LabyNameHistoryProfile::name).toList();
9+
}
10+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package vc.api.model;
2+
3+
import org.jspecify.annotations.Nullable;
4+
5+
import java.util.*;
6+
7+
public record LabyProfileSearchResponse(List<LabyProfile> users) {
8+
public @Nullable ProfileData currentProfile() {
9+
return users != null && !users.isEmpty() ? users.getFirst() : null;
10+
}
11+
12+
public List<ProfileData> historicalProfiles() {
13+
if (users == null || users.size() < 2) return Collections.emptyList();
14+
return (List) users.subList(1, users.size());
15+
}
16+
17+
public List<String> previousUsernames() {
18+
if (users == null || users.isEmpty()) return Collections.emptyList();
19+
var currentProfile = currentProfile();
20+
if (currentProfile == null) return Collections.emptyList();
21+
LabyProfile currentLabyProfile = users.getFirst();
22+
return currentLabyProfile.history().stream()
23+
.map(LabyNameHistoryProfile::name)
24+
.distinct()
25+
.filter(name -> !name.equals(currentProfile.name()))
26+
.toList();
27+
}
28+
29+
public List<String> associatedUsernames() {
30+
if (users == null || users.size() < 2) return Collections.emptyList();
31+
var currentProfile = currentProfile();
32+
if (currentProfile == null) return Collections.emptyList();
33+
final Set<String> previousUsernames = new HashSet<>();
34+
previousUsernames().forEach(u -> {
35+
previousUsernames.add(u.toLowerCase());
36+
});
37+
previousUsernames.add(currentProfile.name().toLowerCase());
38+
List<String> usernames = new ArrayList<>();
39+
var historicalProfiles = users.subList(1, users.size());
40+
for (LabyProfile profile : historicalProfiles) {
41+
var name = profile.name();
42+
previousUsernames.add(name.toLowerCase());
43+
var historyList = profile.history();
44+
if (historyList != null) {
45+
for (var h : historyList) {
46+
var hName = h.name();
47+
if (!previousUsernames.contains(hName.toLowerCase())) {
48+
usernames.add(hName);
49+
}
50+
}
51+
}
52+
}
53+
return usernames.stream().distinct().toList();
54+
}
55+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package vc.commands;
2+
3+
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent;
4+
import discord4j.core.object.entity.Message;
5+
import discord4j.core.spec.EmbedCreateSpec;
6+
import discord4j.rest.util.Color;
7+
import org.slf4j.Logger;
8+
import org.springframework.stereotype.Component;
9+
import reactor.core.publisher.Mono;
10+
import vc.api.LabyRestClient;
11+
import vc.api.model.LabyProfileSearchResponse;
12+
import vc.openapi.handler.ApiException;
13+
import vc.util.DiscordMarkdownEscape;
14+
15+
import java.net.http.HttpTimeoutException;
16+
17+
import static org.slf4j.LoggerFactory.getLogger;
18+
19+
@Component
20+
public class NamesCommand implements SlashCommand {
21+
private static final Logger LOGGER = getLogger(NamesCommand.class);
22+
private final LabyRestClient labyRestClient;
23+
24+
public NamesCommand(final LabyRestClient labyRestClient) {
25+
this.labyRestClient = labyRestClient;
26+
}
27+
28+
@Override
29+
public String getName() {
30+
return "names";
31+
}
32+
33+
@Override
34+
public Mono<Message> handle(final ChatInputInteractionEvent event) {
35+
var player = event.getOptionAsString("player").orElse(null);
36+
if (player == null) {
37+
return error(event, "`player` option is required");
38+
}
39+
LabyProfileSearchResponse searchResponse;
40+
try {
41+
searchResponse = labyRestClient.searchProfiles(player);
42+
} catch (Exception e) {
43+
if (e instanceof ApiException apiException) {
44+
if (apiException.getCause() instanceof HttpTimeoutException httpTimeoutException) {
45+
LOGGER.error("Timed out searching for names: {}", player, httpTimeoutException);
46+
return error(event, "Timed out searching. Try again in a minute");
47+
}
48+
}
49+
LOGGER.error("Error searching for names: {}", player, e);
50+
return error(event, "Error searching. Try again later");
51+
}
52+
var builder = EmbedCreateSpec.builder()
53+
.title("Name Search")
54+
.addField("Searched Name", DiscordMarkdownEscape.escape(player), true)
55+
.addField("\u200B", "\u200B", true)
56+
.addField("\u200B", "\u200B", true)
57+
.addField("Source", "[LabyMod API](https://laby.net/@" + player + ")", true)
58+
.color(Color.CYAN);
59+
var sb = new StringBuilder();
60+
var currentProfile = searchResponse.currentProfile();
61+
sb.append("**Current Profile**\n\n");
62+
if (currentProfile != null) {
63+
sb.append(currentProfile.toDiscordFieldValue()).append("\n");
64+
builder.thumbnail(currentProfile.getAvatarURL());
65+
} else {
66+
sb.append("(none)\n");
67+
}
68+
var prevUsernames = searchResponse.previousUsernames();
69+
sb.append("\n**Previous Usernames**\n\n");
70+
if (!prevUsernames.isEmpty()) {
71+
for (var name : prevUsernames) {
72+
sb.append(DiscordMarkdownEscape.escape(name)).append("\n");
73+
}
74+
} else {
75+
sb.append("(none)\n");
76+
}
77+
var historicalProfiles = searchResponse.historicalProfiles();
78+
sb.append("\n**Previous Accounts**\n\n");
79+
if (!historicalProfiles.isEmpty()) {
80+
for (var historicalProfile : historicalProfiles) {
81+
sb.append(historicalProfile.toDiscordFieldValue()).append("\n");
82+
}
83+
} else {
84+
sb.append("(none)\n");
85+
}
86+
var names = searchResponse.associatedUsernames();
87+
sb.append("\n**Previous Account Usernames**\n\n");
88+
if (!names.isEmpty()) {
89+
for (var name : names) {
90+
sb.append(name).append("\n");
91+
}
92+
} else {
93+
sb.append("(none)\n");
94+
}
95+
builder.description(sb.toString());
96+
return event.createFollowup().withEmbeds(builder.build());
97+
}
98+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "names",
3+
"description": "Searches for all associated profiles with a username",
4+
"options": [
5+
{
6+
"name": "player",
7+
"description": "Player",
8+
"type": 3,
9+
"required": true
10+
}
11+
]
12+
}

src/test/java/vc/LabyApiTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package vc;
2+
3+
import vc.api.LabyRestClient;
4+
5+
public class LabyApiTest {
6+
private final Application app = new Application();
7+
private final LabyRestClient api = new LabyRestClient(app.clientHttpRequestFactory());
8+
9+
// @Test
10+
public void test() {
11+
var response = api.searchProfiles("Fit");
12+
var a = 0;
13+
}
14+
}

0 commit comments

Comments
 (0)