Skip to content

Commit ab75da4

Browse files
Avatars
1 parent 0b3b802 commit ab75da4

5 files changed

Lines changed: 133 additions & 13 deletions

File tree

src/main/java/net/aerh/discordbridge/DiscordBridgePlugin.java

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@
2020
import org.jetbrains.annotations.NotNull;
2121

2222
import java.awt.Color;
23+
import java.io.FileInputStream;
24+
import java.io.FileOutputStream;
2325
import java.io.IOException;
2426
import java.io.InputStream;
2527
import java.nio.file.Files;
2628
import java.nio.file.Path;
29+
import java.util.Map;
30+
import java.util.Properties;
31+
import java.util.concurrent.ConcurrentHashMap;
32+
import java.util.function.BiConsumer;
33+
import java.util.function.Consumer;
2734
import java.util.regex.Matcher;
2835
import java.util.regex.Pattern;
2936
import java.util.logging.Level;
@@ -41,6 +48,8 @@ public final class DiscordBridgePlugin extends JavaPlugin {
4148
private DiscordBotConnection botConnection;
4249
private boolean serverStartMessageSent;
4350
private boolean serverStopMessageSent;
51+
private final Map<String, String> discordUserToAvatarUrl = new ConcurrentHashMap<>();
52+
private final Map<String, String> hytaleUsernameToDiscordUserId = new ConcurrentHashMap<>();
4453

4554
public DiscordBridgePlugin(@NotNull JavaPluginInit init) {
4655
super(init);
@@ -58,6 +67,8 @@ protected void setup() {
5867

5968
DiscordBridgeConfig cfg = config.get();
6069
getLogger().at(Level.INFO).log("Configuration loaded successfully");
70+
loadMappings();
71+
getLogger().at(Level.INFO).log("Mappings loaded successfully");
6172

6273
getEventRegistry().registerGlobal(EventPriority.NORMAL, PlayerChatEvent.class, this::onPlayerChat);
6374
getEventRegistry().registerGlobal(PlayerConnectEvent.class, this::onPlayerConnect);
@@ -77,7 +88,19 @@ protected void setup() {
7788
return;
7889
}
7990

80-
this.botConnection = new DiscordBotConnection(cfg, getLogger(), this::relayDiscordMessage);
91+
BiConsumer<String, String> avatarSetter = (userId, url) -> {
92+
discordUserToAvatarUrl.put(userId, url);
93+
saveMappings();
94+
};
95+
BiConsumer<String, String> linkSetter = (username, userId) -> {
96+
hytaleUsernameToDiscordUserId.put(username, userId);
97+
saveMappings();
98+
};
99+
Consumer<String> unlinkUser = userId -> {
100+
hytaleUsernameToDiscordUserId.entrySet().removeIf(entry -> entry.getValue().equals(userId));
101+
saveMappings();
102+
};
103+
this.botConnection = new DiscordBotConnection(cfg, getLogger(), this::relayDiscordMessage, avatarSetter, linkSetter, unlinkUser);
81104
getLogger().at(Level.INFO).log("Discord bot connection initialized");
82105
}
83106

@@ -136,7 +159,10 @@ private void onPlayerChat(@NotNull PlayerChatEvent event) {
136159
getLogger().at(Level.WARNING).log("Webhook URL not configured; skipping chat message to Discord.");
137160
return;
138161
}
139-
botConnection.sendWebhookMessage(webhookUrl, event.getSender().getUsername(), cleaned);
162+
String username = event.getSender().getUsername();
163+
String discordUserId = hytaleUsernameToDiscordUserId.get(username);
164+
String avatarUrl = discordUserId != null ? discordUserToAvatarUrl.get(discordUserId) : null;
165+
botConnection.sendWebhookMessage(webhookUrl, event.getSender().getUsername(), cleaned, avatarUrl);
140166
}
141167

142168
private void onPlayerConnect(@NotNull PlayerConnectEvent event) {
@@ -361,4 +387,43 @@ private void ensureConfigExists(@NotNull Path dataDir) throws IOException {
361387
}
362388
}
363389
}
390+
391+
private void loadMappings() {
392+
Path mappingsFile = getDataDirectory().resolve("mappings.properties");
393+
if (!Files.exists(mappingsFile)) {
394+
return;
395+
}
396+
Properties props = new Properties();
397+
try (FileInputStream fis = new FileInputStream(mappingsFile.toFile())) {
398+
props.load(fis);
399+
for (String key : props.stringPropertyNames()) {
400+
String value = props.getProperty(key);
401+
if (key.startsWith("avatar.")) {
402+
String userId = key.substring(7);
403+
discordUserToAvatarUrl.put(userId, value);
404+
} else if (key.startsWith("link.")) {
405+
String username = key.substring(5);
406+
hytaleUsernameToDiscordUserId.put(username, value);
407+
}
408+
}
409+
} catch (IOException e) {
410+
getLogger().at(Level.WARNING).withCause(e).log("Failed to load mappings");
411+
}
412+
}
413+
414+
private void saveMappings() {
415+
Path mappingsFile = getDataDirectory().resolve("mappings.properties");
416+
Properties props = new Properties();
417+
for (Map.Entry<String, String> entry : discordUserToAvatarUrl.entrySet()) {
418+
props.setProperty("avatar." + entry.getKey(), entry.getValue());
419+
}
420+
for (Map.Entry<String, String> entry : hytaleUsernameToDiscordUserId.entrySet()) {
421+
props.setProperty("link." + entry.getKey(), entry.getValue());
422+
}
423+
try (FileOutputStream fos = new FileOutputStream(mappingsFile.toFile())) {
424+
props.store(fos, "Discord Chat Bridge Mappings");
425+
} catch (IOException e) {
426+
getLogger().at(Level.WARNING).withCause(e).log("Failed to save mappings");
427+
}
428+
}
364429
}

src/main/java/net/aerh/discordbridge/config/DiscordBridgeConfig.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ public final class DiscordBridgeConfig {
3131
(cfg, value) -> cfg.messagesConfig = value,
3232
cfg -> cfg.messagesConfig)
3333
.add()
34-
.append(new KeyedCodec<>("Events", EventsConfig.CODEC),
35-
(cfg, value) -> cfg.eventsConfig = value,
36-
cfg -> cfg.eventsConfig)
37-
.add()
38-
.build();
34+
.append(new KeyedCodec<>("Events", EventsConfig.CODEC),
35+
(cfg, value) -> cfg.eventsConfig = value,
36+
cfg -> cfg.eventsConfig)
37+
.add()
38+
.build();
3939

4040
private boolean enabled = true;
4141
private boolean relayGameToDiscord = true;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Removed, using Properties file instead

src/main/java/net/aerh/discordbridge/discord/BridgeListener.java

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@
99
import net.dv8tion.jda.api.entities.Role;
1010
import net.dv8tion.jda.api.entities.channel.ChannelType;
1111
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
12+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
1213
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
1314
import net.dv8tion.jda.api.events.session.ReadyEvent;
1415
import net.dv8tion.jda.api.hooks.ListenerAdapter;
16+
import net.dv8tion.jda.api.interactions.commands.OptionType;
17+
import net.dv8tion.jda.api.interactions.commands.build.Commands;
18+
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
1519
import org.jetbrains.annotations.NotNull;
1620

1721
import java.awt.*;
1822
import java.util.ArrayList;
1923
import java.util.List;
2024
import java.util.concurrent.CompletableFuture;
25+
import java.util.function.BiConsumer;
2126
import java.util.function.Consumer;
2227
import java.util.logging.Level;
2328

@@ -28,19 +33,28 @@ final class BridgeListener extends ListenerAdapter {
2833
private final CompletableFuture<Void> readyFuture;
2934
private final Consumer<DiscordMessage> relayToGameChat;
3035
private final Consumer<TextChannel> discordChannelUpdater;
36+
private final BiConsumer<String, String> avatarSetter;
37+
private final BiConsumer<String, String> linkSetter;
38+
private final Consumer<String> unlinkUser;
3139

3240
BridgeListener(
3341
@NotNull DiscordBridgeConfig config,
3442
@NotNull HytaleLogger logger,
3543
@NotNull CompletableFuture<Void> readyFuture,
3644
@NotNull Consumer<DiscordMessage> relayToGameChat,
37-
@NotNull Consumer<TextChannel> discordChannelUpdater
45+
@NotNull Consumer<TextChannel> discordChannelUpdater,
46+
@NotNull BiConsumer<String, String> avatarSetter,
47+
@NotNull BiConsumer<String, String> linkSetter,
48+
@NotNull Consumer<String> unlinkUser
3849
) {
3950
this.config = config;
4051
this.logger = logger;
4152
this.readyFuture = readyFuture;
4253
this.relayToGameChat = relayToGameChat;
4354
this.discordChannelUpdater = discordChannelUpdater;
55+
this.avatarSetter = avatarSetter;
56+
this.linkSetter = linkSetter;
57+
this.unlinkUser = unlinkUser;
4458
}
4559

4660
@Override
@@ -57,6 +71,13 @@ public void onReady(@NotNull ReadyEvent event) {
5771

5872
discordChannelUpdater.accept(channel);
5973
logger.at(Level.INFO).log("Discord bot connected as %s", event.getJDA().getSelfUser().getAsTag());
74+
event.getJDA().updateCommands().addCommands(
75+
Commands.slash("avatar", "Set your avatar")
76+
.addOption(OptionType.ATTACHMENT, "image", "The image file", true),
77+
Commands.slash("link", "Link your Discord account to a Hytale username")
78+
.addOption(OptionType.STRING, "username", "The Hytale username", true),
79+
Commands.slash("unlink", "Unlink your Discord account from Hytale UUID")
80+
).queue();
6081
readyFuture.complete(null);
6182
}
6283

@@ -105,4 +126,22 @@ public void onMessageReceived(@NotNull MessageReceivedEvent event) {
105126

106127
relayToGameChat.accept(bridgeMessage);
107128
}
129+
130+
@Override
131+
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
132+
String userId = event.getUser().getId();
133+
if (event.getName().equals("avatar")) {
134+
Message.Attachment attachment = event.getOption("image").getAsAttachment();
135+
String imageUrl = attachment.getUrl();
136+
avatarSetter.accept(userId, imageUrl);
137+
event.reply("Avatar set.").setEphemeral(true).queue();
138+
} else if (event.getName().equals("link")) {
139+
String username = event.getOption("username").getAsString();
140+
linkSetter.accept(username, userId);
141+
event.reply("Linked to username " + username).setEphemeral(true).queue();
142+
} else if (event.getName().equals("unlink")) {
143+
unlinkUser.accept(userId);
144+
event.reply("Unlinked.").setEphemeral(true).queue();
145+
}
146+
}
108147
}

src/main/java/net/aerh/discordbridge/discord/DiscordBotConnection.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import java.util.concurrent.CompletableFuture;
1717
import java.util.concurrent.atomic.AtomicBoolean;
18+
import java.util.function.BiConsumer;
1819
import java.util.function.Consumer;
1920
import java.util.logging.Level;
2021

@@ -23,6 +24,9 @@ public final class DiscordBotConnection implements AutoCloseable {
2324
private final DiscordBridgeConfig config;
2425
private final HytaleLogger logger;
2526
private final Consumer<DiscordMessage> relayToGameChat;
27+
private final BiConsumer<String, String> avatarSetter;
28+
private final BiConsumer<String, String> linkSetter;
29+
private final Consumer<String> unlinkUser;
2630
private final CompletableFuture<Void> readyFuture = new CompletableFuture<>();
2731
private final AtomicBoolean shuttingDown = new AtomicBoolean(false);
2832

@@ -32,18 +36,24 @@ public final class DiscordBotConnection implements AutoCloseable {
3236
public DiscordBotConnection(
3337
@NotNull DiscordBridgeConfig config,
3438
@NotNull HytaleLogger logger,
35-
@NotNull Consumer<DiscordMessage> relayToGameChat
39+
@NotNull Consumer<DiscordMessage> relayToGameChat,
40+
@NotNull BiConsumer<String, String> avatarSetter,
41+
@NotNull BiConsumer<String, String> linkSetter,
42+
@NotNull Consumer<String> unlinkUser
3643
) {
3744
this.config = config;
3845
this.logger = logger;
3946
this.relayToGameChat = relayToGameChat;
47+
this.avatarSetter = avatarSetter;
48+
this.linkSetter = linkSetter;
49+
this.unlinkUser = unlinkUser;
4050
}
4151

4252
@NotNull
4353
public CompletableFuture<Void> start() {
4454
try {
4555
DiscordConfig discordConfig = config.getDiscordConfig();
46-
BridgeListener listener = new BridgeListener(config, logger, readyFuture, relayToGameChat, this::onChannelReady);
56+
BridgeListener listener = new BridgeListener(config, logger, readyFuture, relayToGameChat, this::onChannelReady, avatarSetter, linkSetter, unlinkUser);
4757
JDABuilder builder = JDABuilder.createDefault(discordConfig.getBotToken())
4858
.setMemberCachePolicy(MemberCachePolicy.NONE)
4959
.enableIntents(GatewayIntent.GUILD_MESSAGES, GatewayIntent.MESSAGE_CONTENT)
@@ -96,15 +106,20 @@ public void sendEmbed(@NotNull String content, @NotNull String colorHex) {
96106
}
97107
}
98108

99-
public void sendWebhookMessage(@NotNull String webhookUrl, @NotNull String username, @NotNull String content) {
109+
public void sendWebhookMessage(@NotNull String webhookUrl, @NotNull String username, @NotNull String content, String avatarUrl) {
100110
if (webhookUrl.isEmpty()) {
101111
logger.at(Level.WARNING).log("Webhook URL is empty; cannot send webhook message.");
102112
return;
103113
}
104114

105115
// Send via HTTP POST to webhook URL
106116
java.net.http.HttpClient httpClient = java.net.http.HttpClient.newHttpClient();
107-
String json = String.format("{\"username\": \"%s\", \"content\": \"%s\"}", username.replace("\"", "\\\""), content.replace("\"", "\\\""));
117+
String json;
118+
if (avatarUrl != null && !avatarUrl.isEmpty()) {
119+
json = String.format("{\"username\": \"%s\", \"content\": \"%s\", \"avatar_url\": \"%s\"}", username.replace("\"", "\\\""), content.replace("\"", "\\\""), avatarUrl.replace("\"", "\\\""));
120+
} else {
121+
json = String.format("{\"username\": \"%s\", \"content\": \"%s\"}", username.replace("\"", "\\\""), content.replace("\"", "\\\""));
122+
}
108123
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder()
109124
.uri(java.net.URI.create(webhookUrl))
110125
.header("Content-Type", "application/json")
@@ -125,7 +140,7 @@ public void close() {
125140

126141
public void shutdown() {
127142
if (shuttingDown.compareAndSet(false, true) && jda != null) {
128-
jda.shutdownNow();
143+
jda.shutdown();
129144
}
130145
}
131146

0 commit comments

Comments
 (0)