serverDisplayNames = new HashMap<>();
20 |
21 | public GlobalDiscordConfig discord = new GlobalDiscordConfig();
22 | public GlobalMinecraftConfig minecraft = new GlobalMinecraftConfig();
23 |
24 | public void load(Config config) {
25 | this.excludedServers = config.getOrDefault("exclude_servers", this.excludedServers);
26 | this.excludedServersReceiveMessages =
27 | config.getOrDefault("excluded_servers_receive_messages", this.excludedServersReceiveMessages);
28 |
29 | this.pingIntervalSeconds = config.getOrDefault("ping_interval", this.pingIntervalSeconds);
30 |
31 | this.serverDisplayNames = config.getMapOrDefault("server_names", this.serverDisplayNames);
32 |
33 | this.discord.load(config.getConfig("discord"));
34 | this.minecraft.load(config.getConfig("minecraft"));
35 | }
36 |
37 | public boolean pingIntervalEnabled() {
38 | return this.pingIntervalSeconds > 0;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/GlobalDiscordConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 |
4 | import ooo.foooooooooooo.velocitydiscord.config.Config;
5 | import ooo.foooooooooooo.velocitydiscord.config.definitions.commands.GlobalCommandConfig;
6 |
7 | import java.util.Optional;
8 |
9 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
10 | public class GlobalDiscordConfig {
11 | public String token;
12 |
13 | /**
14 | * Activity text of the bot to show in Discord
15 | *
16 | * Placeholders available: {amount}
17 | */
18 | public Optional activityText = Optional.of("with {amount} players online");
19 |
20 | /**
21 | * Set the interval (in minutes) for updating the channel topic
22 | *
23 | * Use a value of 0 to disable
24 | */
25 | public int updateChannelTopicIntervalMinutes = 0;
26 |
27 | public GlobalChatConfig chat = new GlobalChatConfig();
28 | public GlobalCommandConfig commands = new GlobalCommandConfig();
29 |
30 | public void load(Config config) {
31 | if (config == null) return;
32 |
33 | this.token = config.get("token");
34 | this.activityText = config.getDisableableStringOrDefault("activity_text", this.activityText);
35 | this.updateChannelTopicIntervalMinutes =
36 | config.getOrDefault("update_channel_topic_interval", this.updateChannelTopicIntervalMinutes);
37 |
38 | this.chat.load(config.getConfig("chat"));
39 | this.commands.load(config.getConfig("commands"));
40 | }
41 |
42 | public boolean updateChannelTopicEnabled() {
43 | return this.updateChannelTopicIntervalMinutes > 0;
44 | }
45 |
46 | public boolean isTokenUnset() {
47 | return this.token.equals("TOKEN") || this.token.isBlank();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/GlobalMinecraftConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | public class GlobalMinecraftConfig {
6 | public String pluginCommand = "discord";
7 |
8 | public void load(Config config) {
9 | if (config == null) return;
10 |
11 | this.pluginCommand = config.getOrDefault("plugin_command", this.pluginCommand);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/LocalConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 | import ooo.foooooooooooo.velocitydiscord.discord.MessageCategory;
5 |
6 | import java.util.ArrayList;
7 |
8 | public class LocalConfig {
9 | public DiscordConfig discord = new DiscordConfig();
10 | public MinecraftConfig minecraft = new MinecraftConfig();
11 |
12 | public void load(Config config) {
13 | this.discord.load(config.getConfig("discord"));
14 | this.minecraft.load(config.getConfig("minecraft"));
15 | }
16 |
17 |
18 | public String checkErrors() {
19 | if (this.discord.isWebhookUsed() && this.discord.webhook.isInvalid()) {
20 | var invalidCategories = new ArrayList();
21 |
22 | var chat = this.discord.chat;
23 |
24 | // check each message category
25 | if (chat.message.isInvalidWebhook()) invalidCategories.add(MessageCategory.MESSAGE);
26 | if (chat.join.isInvalidWebhook()) invalidCategories.add(MessageCategory.JOIN);
27 | if (chat.leave.isInvalidWebhook()) invalidCategories.add(MessageCategory.LEAVE);
28 | if (chat.disconnect.isInvalidWebhook()) invalidCategories.add(MessageCategory.DISCONNECT);
29 | if (chat.serverSwitch.isInvalidWebhook()) invalidCategories.add(MessageCategory.SERVER_SWITCH);
30 | if (chat.advancement.isInvalidWebhook()) invalidCategories.add(MessageCategory.ADVANCEMENT);
31 | if (chat.death.isInvalidWebhook()) invalidCategories.add(MessageCategory.DEATH);
32 |
33 | if (!invalidCategories.isEmpty()) {
34 | var errorFormat = """
35 | ERROR: `discord.webhook` and `discord.chat.%s.webhook` are unset or invalid, but `discord.chat.%s.type` is set to `webhook`""";
36 | var error = new StringBuilder();
37 |
38 | for (var category : invalidCategories) {
39 | error.append(String.format(errorFormat, category.toString(), category)).append("\n");
40 | }
41 |
42 | return error.toString();
43 | }
44 | }
45 |
46 | return null;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/MinecraftConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | import java.util.HashMap;
6 | import java.util.Optional;
7 |
8 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
9 | public class MinecraftConfig {
10 | /// Placeholders available: `discord`
11 | public String discordChunkFormat = "[<{discord_color}>Discord]";
12 |
13 | /// Placeholders available: `role_color`, `display_name`, `username`, `nickname`
14 | ///
15 | /// `` tag allows you to shift right-click the username to insert `@username` in the chat
16 | public String usernameChunkFormat =
17 | "<{role_color}>{nickname}";
18 |
19 | /// Placeholders available: {discord_chunk}, {username_chunk}, {attachments}, {message}
20 | public String messageFormat =
21 | "{discord_chunk} {role_prefix} {username_chunk}: {message} {attachments}";
22 |
23 | /// Placeholders available: `url`, `attachment_color`
24 | public String attachmentFormat =
25 | "[<{attachment_color}>Attachment]";
26 |
27 | /// Placeholders available: `url`, `link_color`
28 | public Optional linkFormat = Optional.of("""
29 | [<{link_color}>Link]""");
30 |
31 | public String discordColor = "#7289da";
32 | public String attachmentColor = "#4abdff";
33 | public String linkColor = "#4abdff";
34 |
35 | public HashMap rolePrefixes = new HashMap<>();
36 |
37 | public void load(Config config) {
38 | if (config == null) return;
39 |
40 | this.discordChunkFormat = config.getOrDefault("discord_chunk", this.discordChunkFormat);
41 | this.usernameChunkFormat = config.getOrDefault("username_chunk", this.usernameChunkFormat);
42 | this.messageFormat = config.getOrDefault("message", this.messageFormat);
43 | this.attachmentFormat = config.getOrDefault("attachments", this.attachmentFormat);
44 | this.linkFormat = config.getDisableableStringOrDefault("links", this.linkFormat);
45 |
46 | this.discordColor = config.getOrDefault("discord_color", this.discordColor);
47 | this.attachmentColor = config.getOrDefault("attachment_color", this.attachmentColor);
48 | this.linkColor = config.getOrDefault("link_color", this.linkColor);
49 |
50 | this.rolePrefixes = config.getMapOrDefault("role_prefixes", this.rolePrefixes);
51 | }
52 |
53 | public String getRolePrefix(String role) {
54 | return this.rolePrefixes.getOrDefault(role, "");
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/SystemMessageConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | import java.awt.*;
6 | import java.util.Optional;
7 |
8 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
9 | public class SystemMessageConfig {
10 | public SystemMessageType type = SystemMessageType.TEXT;
11 | public Optional channel = Optional.empty();
12 |
13 | public Optional format;
14 | public Optional embedColor;
15 |
16 | public SystemMessageConfig(String format, Color embedColor) {
17 | this.format = Optional.ofNullable(format);
18 | this.embedColor = Optional.ofNullable(embedColor);
19 | }
20 |
21 | public void load(Config config) {
22 | if (config == null) return;
23 |
24 | this.type = SystemMessageType.get(config, "type", this.type);
25 | this.channel = config.getDisableableStringOrDefault("channel", this.channel);
26 |
27 | this.format = config.getDisableableStringOrDefault("format", this.format);
28 | this.embedColor = config.getDisableableColorOrDefault("embed_color", this.embedColor);
29 | }
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/SystemMessageType.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | public enum SystemMessageType {
6 | TEXT, EMBED;
7 |
8 | public static SystemMessageType get(Config config, String key, SystemMessageType defaultValue) {
9 | var type = config.getOrDefault(key, defaultValue.toString());
10 | return switch (type) {
11 | case "text" -> TEXT;
12 | case "embed" -> EMBED;
13 | case "" -> defaultValue;
14 | default -> throw new IllegalArgumentException("Unknown system message type: " + type);
15 | };
16 | }
17 |
18 | @Override
19 | public String toString() {
20 | return switch (this) {
21 | case TEXT -> "text";
22 | case EMBED -> "embed";
23 | };
24 | }
25 | }
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/UserMessageConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | import java.awt.*;
6 | import java.util.Optional;
7 |
8 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
9 | public class UserMessageConfig {
10 | public UserMessageType type = UserMessageType.TEXT;
11 | public Optional channelId = Optional.empty();
12 |
13 | public Optional format;
14 | public Optional embedColor;
15 |
16 | public Optional webhook = Optional.empty();
17 |
18 | public UserMessageConfig(String format, Color embedColor) {
19 | this.format = Optional.ofNullable(format);
20 | this.embedColor = Optional.ofNullable(embedColor);
21 | }
22 |
23 | public void load(Config config) {
24 | if (config == null) return;
25 |
26 | this.type = UserMessageType.get(config, "type", this.type);
27 | this.channelId = config.getDisableableStringOrDefault("channel", this.channelId);
28 |
29 | this.format = config.getDisableableStringOrDefault("format", this.format);
30 | this.embedColor = config.getDisableableColorOrDefault("embed_color", this.embedColor);
31 |
32 | var webhookConfig = config.getConfig("webhook");
33 |
34 | if (webhookConfig == null) {
35 | this.webhook = Optional.empty();
36 | } else {
37 | this.webhook = Optional.of(new WebhookConfig());
38 | this.webhook.get().load(webhookConfig);
39 | }
40 | }
41 |
42 | public boolean isInvalidWebhook() {
43 | return this.type == UserMessageType.WEBHOOK && (
44 | this.webhook.isEmpty() || (this.webhook.get().isInvalid())
45 | );
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/UserMessageType.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | public enum UserMessageType {
6 | TEXT, EMBED, WEBHOOK;
7 |
8 | public static UserMessageType get(Config config, String key, UserMessageType defaultValue) {
9 | var type = config.getOrDefault(key, defaultValue.toString());
10 | return switch (type) {
11 | case "text" -> TEXT;
12 | case "embed" -> EMBED;
13 | case "webhook" -> WEBHOOK;
14 | case "" -> defaultValue;
15 | default -> throw new IllegalArgumentException("Unknown system message type: " + type);
16 | };
17 | }
18 |
19 | @Override
20 | public String toString() {
21 | return switch (this) {
22 | case TEXT -> "text";
23 | case EMBED -> "embed";
24 | case WEBHOOK -> "webhook";
25 | };
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/WebhookConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 |
7 | import java.util.regex.Pattern;
8 |
9 | public class WebhookConfig {
10 | private static final Logger logger = LoggerFactory.getLogger(WebhookConfig.class);
11 |
12 | private static final Pattern WEBHOOK_URL_REGEX = Pattern.compile(
13 | "https?://(?:[^\\s.]+\\.)?discord(?:app)?\\.com/api(?:/v\\d+)?/webhooks/(?\\d+)/(?[^\\s/]+)",
14 | Pattern.CASE_INSENSITIVE
15 | );
16 |
17 | /// Full webhook URL to send chat messages to
18 | public String url = "";
19 |
20 | /// Full URL of an avatar service to get the player's avatar from
21 | ///
22 | /// Placeholders available: `uuid`, `username`
23 | public String avatarUrl = "https://visage.surgeplay.com/face/96/{uuid}";
24 |
25 | /// The format of the webhook's username
26 | ///
27 | /// Placeholders available: `username`, `server`
28 | public String username = "{username}";
29 |
30 | public boolean valid = false;
31 | public String id;
32 |
33 | public void load(Config config) {
34 | if (config == null) return;
35 |
36 | this.url = config.getOrDefault("url", this.url);
37 | this.avatarUrl = config.getOrDefault("avatar_url", this.avatarUrl);
38 | this.username = config.getOrDefault("username", this.username);
39 |
40 | var matcher = WEBHOOK_URL_REGEX.matcher(this.url);
41 | this.valid = matcher.matches();
42 |
43 | if (this.valid) {
44 | this.id = matcher.group("id");
45 | } else if (!this.url.isEmpty()) {
46 | logger.warn("Invalid webhook URL: {}", this.url);
47 | }
48 | }
49 |
50 | public boolean isInvalid() {
51 | return this.url.isEmpty() || !this.valid;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/commands/CommandConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions.commands;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | public class CommandConfig {
6 | public ListCommandConfig list = new ListCommandConfig();
7 |
8 | public void load(Config config) {
9 | if (config == null) return;
10 |
11 | this.list.load(config.getConfig("list"));
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/commands/GlobalCommandConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions.commands;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | public class GlobalCommandConfig {
6 | public GlobalListCommandConfig list = new GlobalListCommandConfig();
7 |
8 | public void load(Config config) {
9 | if (config == null) return;
10 |
11 | this.list.load(config.getConfig("list"));
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/commands/GlobalListCommandConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions.commands;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | public class GlobalListCommandConfig {
6 | public boolean enabled = true;
7 |
8 | /**
9 | * Ephemeral messages are only visible to the user who sent the command
10 | */
11 | public boolean ephemeral = true;
12 |
13 | public String codeblockLang = "asciidoc";
14 |
15 | public void load(Config config) {
16 | if (config == null) return;
17 |
18 | this.enabled = config.getOrDefault("enabled", this.enabled);
19 | this.ephemeral = config.getOrDefault("ephemeral", this.ephemeral);
20 | this.codeblockLang = config.getOrDefault("codeblock_lang", this.codeblockLang);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/config/definitions/commands/ListCommandConfig.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config.definitions.commands;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.Config;
4 |
5 | import java.util.Optional;
6 |
7 | @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
8 | public class ListCommandConfig {
9 | /// Placeholders available: `server_name`, `online_players`, `max_players`
10 | public String serverFormat = "[{server_name} {online_players}/{max_players}]";
11 |
12 | /// Placeholders available: `username`
13 | public String playerFormat = "- {username}";
14 |
15 | public Optional noPlayersFormat = Optional.of("No players online");
16 |
17 | public Optional serverOfflineFormat = Optional.of("Server offline");
18 |
19 | public void load(Config config) {
20 | if (config == null) return;
21 |
22 | this.serverFormat = config.getOrDefault("server_format", this.serverFormat);
23 | this.playerFormat = config.getOrDefault("player_format", this.playerFormat);
24 | this.noPlayersFormat = config.getDisableableStringOrDefault("no_players", this.noPlayersFormat);
25 | this.serverOfflineFormat = config.getDisableableStringOrDefault("server_offline", this.serverOfflineFormat);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/discord/MessageCategory.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.discord;
2 |
3 | public enum MessageCategory {
4 | JOIN, SERVER_SWITCH, DISCONNECT, LEAVE, DEATH, ADVANCEMENT, MESSAGE;
5 |
6 | @Override
7 | public String toString() {
8 | return switch (this) {
9 | case JOIN -> "join";
10 | case SERVER_SWITCH -> "server_switch";
11 | case DISCONNECT -> "disconnect";
12 | case LEAVE -> "leave";
13 | case DEATH -> "death";
14 | case ADVANCEMENT -> "advancement";
15 | case MESSAGE -> "message";
16 | };
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/discord/MessageListener.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.discord;
2 |
3 | import net.dv8tion.jda.api.JDA;
4 | import net.dv8tion.jda.api.entities.Message;
5 | import net.dv8tion.jda.api.entities.channel.ChannelType;
6 | import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
7 | import net.dv8tion.jda.api.hooks.ListenerAdapter;
8 | import net.kyori.adventure.text.minimessage.MiniMessage;
9 | import ooo.foooooooooooo.velocitydiscord.VelocityDiscord;
10 | import ooo.foooooooooooo.velocitydiscord.util.StringTemplate;
11 |
12 | import javax.annotation.Nonnull;
13 | import java.awt.*;
14 | import java.net.URI;
15 | import java.util.*;
16 | import java.util.List;
17 | import java.util.regex.Matcher;
18 | import java.util.regex.Pattern;
19 |
20 | public class MessageListener extends ListenerAdapter {
21 | private static final Pattern LINK_REGEX = Pattern.compile(
22 | "[hH][tT]{2}[pP][sS]?://([a-zA-Z0-9\\u00a1-\\uffff]+(:[a-zA-Z0-9\\u00a1-\\uffff]+)?@)" +
23 | "?[a-zA-Z0-9\\u00a1-\\uffff][a-zA-Z0-9\\u00a1-\\uffff_-]{0,62}(?:\\.[a-zA-Z0-9\\u00a1-\\uffff_-]{1,62})*" +
24 | "(?::\\d{1,5})?(?:/[a-zA-Z0-9\\u00a1-\\uffff_\\-().]*)*(?:[?#][a-zA-Z0-9\\u00a1-\\uffff_\\-()?/=%.*]*)?");
25 |
26 | private final HashMap serverChannels;
27 | private final HashMap> channelToServersMap = new HashMap<>();
28 |
29 | private JDA jda;
30 |
31 | public MessageListener(HashMap serverChannels) {
32 | this.serverChannels = serverChannels;
33 | onServerChannelsUpdated();
34 | }
35 |
36 | public void onServerChannelsUpdated() {
37 | this.channelToServersMap.clear();
38 |
39 | for (var entry : this.serverChannels.entrySet()) {
40 | this.channelToServersMap
41 | .computeIfAbsent(entry.getValue().chatChannel.getIdLong(), (k) -> new ArrayList<>())
42 | .add(entry.getKey());
43 | }
44 | }
45 |
46 | @Override
47 | public void onMessageReceived(@Nonnull MessageReceivedEvent event) {
48 | if (!event.isFromType(ChannelType.TEXT)) {
49 | VelocityDiscord.LOGGER.trace("ignoring non text channel message");
50 | return;
51 | }
52 |
53 | if (this.jda == null) {
54 | this.jda = event.getJDA();
55 | }
56 |
57 | var channel = event.getChannel().asTextChannel();
58 | var targetServerNames = this.channelToServersMap.get(channel.getIdLong());
59 |
60 | if (targetServerNames == null) {
61 | return;
62 | }
63 |
64 | VelocityDiscord.LOGGER.trace(
65 | "Received message from Discord channel {} for servers {}",
66 | channel.getName(),
67 | targetServerNames
68 | );
69 |
70 | var messages = new HashMap();
71 | for (var serverName : targetServerNames) {
72 | messages.put(serverName, serializeMinecraftMessage(event, serverName));
73 | }
74 |
75 | for (var server : VelocityDiscord.SERVER.getAllServers()) {
76 | var serverName = server.getServerInfo().getName();
77 | if (!VelocityDiscord.CONFIG.global.excludedServersReceiveMessages &&
78 | VelocityDiscord.CONFIG.serverDisabled(serverName)) {
79 | continue;
80 | }
81 |
82 | var message = messages.get(serverName);
83 | if (message == null) continue;
84 |
85 | server.sendMessage(MiniMessage.miniMessage().deserialize(message).asComponent());
86 | }
87 | }
88 |
89 | private String serializeMinecraftMessage(MessageReceivedEvent event, String server) {
90 | var serverConfig = VelocityDiscord.CONFIG.getServerConfig(server);
91 | var serverMinecraftConfig = serverConfig.getMinecraftConfig();
92 | var serverDiscordConfig = serverConfig.getDiscordConfig();
93 |
94 | var author = event.getAuthor();
95 | if (!serverDiscordConfig.showBotMessages && author.isBot()) {
96 | VelocityDiscord.LOGGER.debug("ignoring bot message");
97 | return null;
98 | }
99 |
100 | if (author.getId().equals(this.jda.getSelfUser().getId()) || (
101 | author.getId().equals(serverDiscordConfig.webhook.id)
102 | )) {
103 | VelocityDiscord.LOGGER.debug("ignoring own message");
104 | return null;
105 | }
106 |
107 | var message = event.getMessage();
108 | var guild = event.getGuild();
109 |
110 | var color = Color.white;
111 | var nickname = author.getName(); // Nickname defaults to username
112 | var rolePrefix = "";
113 |
114 | var member = guild.getMember(author);
115 | if (member != null) {
116 | color = member.getColor();
117 | if (color == null) {
118 | color = Color.white;
119 | }
120 | nickname = member.getEffectiveName();
121 |
122 | // Get the role prefix
123 | var highestRole = member
124 | .getRoles()
125 | .stream()
126 | .filter(role -> !serverMinecraftConfig.getRolePrefix(role.getId()).isEmpty())
127 | .findFirst();
128 |
129 | rolePrefix = highestRole.map(role -> serverMinecraftConfig.getRolePrefix(role.getId())).orElse("");
130 | }
131 |
132 | var hex = "#" + Integer.toHexString(color.getRGB()).substring(2);
133 |
134 | // parse configured message formats
135 | var discord_chunk = new StringTemplate(serverMinecraftConfig.discordChunkFormat)
136 | .add("discord_color", serverMinecraftConfig.discordColor)
137 | .toString();
138 |
139 | var display_name = author.getGlobalName();
140 |
141 | if (display_name == null) {
142 | display_name = author.getName();
143 | }
144 |
145 | var username_chunk = new StringTemplate(serverMinecraftConfig.usernameChunkFormat)
146 | .add("role_color", hex)
147 | .add("username", escapeTags(author.getName()))
148 | .add("display_name", escapeTags(display_name))
149 | .add("nickname", escapeTags(nickname))
150 | .toString();
151 |
152 | var attachment_chunk = serverMinecraftConfig.attachmentFormat;
153 | var message_chunk = new StringTemplate(serverMinecraftConfig.messageFormat)
154 | .add("discord_chunk", discord_chunk)
155 | .add("role_prefix", escapeTags(rolePrefix))
156 | .add("username_chunk", username_chunk)
157 | .add("message", message.getContentDisplay());
158 |
159 | var attachmentChunks = new ArrayList();
160 |
161 | List attachments = new ArrayList<>();
162 | if (serverDiscordConfig.showAttachmentsIngame) {
163 | attachments = message.getAttachments();
164 | }
165 |
166 | for (var attachment : attachments) {
167 | var chunk = new StringTemplate(attachment_chunk)
168 | .add("url", attachment.getUrl())
169 | .add("attachment_color", serverMinecraftConfig.attachmentColor)
170 | .toString();
171 |
172 | attachmentChunks.add(chunk);
173 | }
174 |
175 | var content = message.getContentDisplay();
176 |
177 | // Remove leading whitespace from attachments if there's no content
178 | if (content.isBlank()) {
179 | message_chunk = message_chunk.replace(" {attachments}", "{attachments}");
180 | }
181 |
182 | if (serverMinecraftConfig.linkFormat.isPresent()) {
183 | // Replace links with the link format
184 | content = LINK_REGEX.matcher(content).replaceAll(match -> {
185 | var url = match.group();
186 | if (validateUrl(url)) {
187 | var replacement = new StringTemplate(serverMinecraftConfig.linkFormat.get())
188 | .add("url", url)
189 | .add("link_color", serverMinecraftConfig.linkColor)
190 | .toString();
191 |
192 | return Matcher.quoteReplacement(replacement);
193 | } else {
194 | return Matcher.quoteReplacement(url);
195 | }
196 | });
197 | }
198 |
199 | message_chunk.add("message", content);
200 | message_chunk.add("attachments", String.join(" ", attachmentChunks));
201 |
202 | return message_chunk.toString();
203 | }
204 |
205 | private static final Set SUPPORTED_URI_PROTOCOLS = Set.of("http", "https");
206 |
207 | private boolean validateUrl(String url) {
208 | try {
209 | return SUPPORTED_URI_PROTOCOLS.contains(new URI(url).getScheme().toLowerCase(Locale.ROOT));
210 | } catch (Exception e) {
211 | return false;
212 | }
213 | }
214 |
215 | /**
216 | * `<` and `>` within another tag break everything, and `‹` `›` are very close in minecraft font
217 | */
218 | private String escapeTags(String input) {
219 | return input.replace("<", "‹").replace(">", "›");
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/discord/commands/ICommand.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.discord.commands;
2 |
3 | import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
4 |
5 | public interface ICommand {
6 | void handle(SlashCommandInteraction interaction);
7 |
8 | String description();
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/discord/commands/ListCommand.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.discord.commands;
2 |
3 | import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
4 | import ooo.foooooooooooo.velocitydiscord.VelocityDiscord;
5 | import ooo.foooooooooooo.velocitydiscord.util.StringTemplate;
6 |
7 | public class ListCommand implements ICommand {
8 | public static final String COMMAND_NAME = "list";
9 |
10 | public ListCommand() {}
11 |
12 | @Override
13 | public void handle(SlashCommandInteraction interaction) {
14 | final var servers = VelocityDiscord.SERVER.getAllServers();
15 |
16 | final var sb = new StringBuilder();
17 | sb.append("```").append(VelocityDiscord.CONFIG.global.discord.commands.list.codeblockLang).append('\n');
18 |
19 | for (var server : servers) {
20 | var name = server.getServerInfo().getName();
21 |
22 | if (VelocityDiscord.CONFIG.serverDisabled(name)) {
23 | continue;
24 | }
25 |
26 | var serverDiscordConfig = VelocityDiscord.CONFIG.getServerConfig(name).getDiscordConfig();
27 |
28 | var players = server.getPlayersConnected();
29 |
30 | var state = VelocityDiscord.getListener().getServerState(server);
31 |
32 | var serverInfo = new StringTemplate(serverDiscordConfig.commands.list.serverFormat)
33 | .add("server_name", VelocityDiscord.CONFIG.serverName(name))
34 | .add("online_players", state.players)
35 | .add("max_players", state.maxPlayers)
36 | .toString();
37 |
38 | sb.append(serverInfo).append('\n');
39 |
40 | if (!state.online && serverDiscordConfig.commands.list.serverOfflineFormat.isPresent()) {
41 | sb.append(serverDiscordConfig.commands.list.serverOfflineFormat.get()).append('\n');
42 | } else if (state.players == 0 && serverDiscordConfig.commands.list.noPlayersFormat.isPresent()) {
43 | sb.append(serverDiscordConfig.commands.list.noPlayersFormat.get()).append('\n');
44 | } else {
45 | for (var player : players) {
46 | var user = new StringTemplate(serverDiscordConfig.commands.list.playerFormat)
47 | .add("username", player.getUsername())
48 | .toString();
49 |
50 | sb.append(user).append('\n');
51 | }
52 | }
53 |
54 | sb.append('\n');
55 | }
56 | sb.append("```");
57 |
58 | interaction
59 | .reply(sb.toString())
60 | .setEphemeral(VelocityDiscord.CONFIG.global.discord.commands.list.ephemeral)
61 | .queue();
62 | }
63 |
64 | @Override
65 | public String description() {
66 | return "List all servers and their players";
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/discord/message/IQueuedMessage.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.discord.message;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.discord.Discord;
4 |
5 | public interface IQueuedMessage {
6 | void send(Discord discord);
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/util/StringTemplate.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.util;
2 |
3 | import javax.annotation.Nonnull;
4 | import java.util.HashMap;
5 | import java.util.Map;
6 |
7 | public class StringTemplate {
8 | private final Map variables = new HashMap<>();
9 | @Nonnull
10 | private String template;
11 |
12 | public StringTemplate(@Nonnull String template) {
13 | this.template = template;
14 | }
15 |
16 | public StringTemplate add(@Nonnull String key, @Nonnull String value) {
17 | this.variables.put(key, value);
18 |
19 | return this;
20 | }
21 |
22 | public StringTemplate add(@Nonnull String key, int value) {
23 | this.variables.put(key, String.valueOf(value));
24 |
25 | return this;
26 | }
27 |
28 | public StringTemplate add(@Nonnull String key, boolean value) {
29 | this.variables.put(key, String.valueOf(value));
30 |
31 | return this;
32 | }
33 |
34 | public StringTemplate add(@Nonnull String key, double value) {
35 | this.variables.put(key, String.valueOf(value));
36 |
37 | return this;
38 | }
39 |
40 | @Override
41 | @Nonnull
42 | public String toString() {
43 | var result = this.template;
44 |
45 | for (var entry : this.variables.entrySet()) {
46 | result = result.replace("{" + entry.getKey() + "}", entry.getValue());
47 | }
48 |
49 | return result;
50 | }
51 |
52 | public StringTemplate replace(@Nonnull String target, @Nonnull String replacement) {
53 | this.template = this.template.replace(target, replacement);
54 |
55 | return this;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/ooo/foooooooooooo/velocitydiscord/yep/YepListener.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.yep;
2 |
3 | import cc.unilock.yeplib.api.event.YepAdvancementEvent;
4 | import cc.unilock.yeplib.api.event.YepDeathEvent;
5 | import cc.unilock.yeplib.api.event.YepMessageEvent;
6 | import com.velocitypowered.api.event.Subscribe;
7 | import ooo.foooooooooooo.velocitydiscord.VelocityDiscord;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 |
11 | public class YepListener {
12 | private static final Logger logger = LoggerFactory.getLogger(YepListener.class);
13 |
14 | public YepListener() {
15 | VelocityDiscord.LOGGER.info("YepListener created");
16 | }
17 |
18 | @Subscribe
19 | public void onYepMessage(YepMessageEvent event) {
20 | logger.debug("Received YepMessageEvent: {}", event);
21 | }
22 |
23 | @Subscribe
24 | public void onYepAdvancement(YepAdvancementEvent event) {
25 | if (VelocityDiscord.CONFIG.serverDisabled(event.getSource().getServer().getServerInfo().getName())) return;
26 |
27 | var uuid = event.getPlayer().getUniqueId().toString();
28 | var server = event.getSource().getServer().getServerInfo().getName();
29 |
30 | VelocityDiscord
31 | .getDiscord()
32 | .onPlayerAdvancement(
33 | event.getUsername(),
34 | uuid,
35 | server,
36 | event.getDisplayName(),
37 | event.getTitle(),
38 | event.getDescription()
39 | );
40 | }
41 |
42 | @Subscribe
43 | public void onYepDeath(YepDeathEvent event) {
44 | if (VelocityDiscord.CONFIG.serverDisabled(event.getSource().getServer().getServerInfo().getName())) return;
45 |
46 | var uuid = event.getPlayer().getUniqueId().toString();
47 | var server = event.getSource().getServer().getServerInfo().getName();
48 |
49 | VelocityDiscord
50 | .getDiscord()
51 | .onPlayerDeath(event.getUsername(), uuid, server, event.getDisplayName(), event.getMessage());
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/resources/config.toml:
--------------------------------------------------------------------------------
1 | #:schema https://raw.githubusercontent.com/fooooooooooooooo/VelocityDiscord/refs/heads/master/schema.json
2 |
3 | # Don't change this
4 | config_version = "2.0"
5 |
6 | # Comma separated list of server names to exclude from the bridge (defined under [servers] inside your velocity.toml)
7 | # e.g., exclude_servers = ["lobby", "survival"]
8 | exclude_servers = []
9 | excluded_servers_receive_messages = false
10 |
11 | # How often to ping all servers to check for online status (seconds)
12 | # Excluded servers will not be pinged
13 | # Use a value of 0 to disable
14 | ping_interval = 30
15 |
16 | # Server display names
17 | # If a server is not found in this list, the server name (from velocity.toml) will be used instead
18 | [server_names]
19 | # lobby = "Lobby"
20 |
21 | [discord]
22 | # Bot token from https://discordapp.com/developers/applications/
23 | # Not server overridable
24 | token = "TOKEN"
25 | # Default channel ID to send Minecraft chat messages to
26 | channel = "000000000000000000"
27 |
28 | # Show messages from bots in Minecraft chat
29 | show_bot_messages = false
30 | # Show clickable links for attachments in Minecraft chat
31 | show_attachments_ingame = true
32 |
33 | # Activity text of the bot to show in Discord
34 | # Placeholders available: {amount}
35 | # Can be disabled with "" or false
36 | # Not server overridable
37 | activity_text = "with {amount} players online"
38 |
39 | # Enable mentioning Discord users from Minecraft chat
40 | enable_mentions = true
41 | # Enable @everyone and @here pings from Minecraft chat
42 | enable_everyone_and_here = false
43 |
44 | # Interval (in minutes) for updating the channel topic
45 | # Use a value of 0 to disable
46 | # Not server overridable
47 | update_channel_topic_interval = 0
48 |
49 | # Channel topic config (if enabled)
50 | [discord.channel_topic]
51 | # Template for the channel topic
52 | # Placeholders available:
53 | # {players} - Total number of players online
54 | # {player_list} - List of players (format is defined below)
55 | # {servers} - Number of servers
56 | # {server_list} - List of server names
57 | # {hostname} - Server hostname
58 | # {port} - Server port
59 | # {motd} - Message of the Day (MOTD)
60 | # {query_port} - Query port
61 | # {max_players} - Maximum number of players
62 | # {plugins} - Number of plugins
63 | # {plugin_list} - List of plugin names
64 | # {version} - Server version
65 | # {software} - Software name
66 | # {average_ping} - Average ping of all players
67 | # {uptime} - Server uptime in hours and minutes
68 | # {server[SERVERNAME]} - Dynamic placeholder for each server's name and status (e.g., {server[MyServer]}, {server[AnotherServer]}, {server[Lobby]}, etc.)
69 | format = """{players}/{max_players}
70 | {player_list}
71 | {hostname}:{port}
72 | Uptime: {uptime}"""
73 |
74 | # Template for server[SERVERNAME] placeholder in the channel topic
75 | # Placeholders available: {name}, {players}, {max_players}, {motd}, {version}, {protocol}
76 | server = "{name}: {players}/{max_players}"
77 |
78 | # Template for server[SERVERNAME] placeholder in the channel topic when the server is offline
79 | # Placeholders available: {name}
80 | server_offline = "{name}: Offline"
81 |
82 | # Can be disabled with "" or false to hide the list completely when no players are online
83 | player_list_no_players_header = "No players online"
84 |
85 | # Can be disabled with "" or false to hide the header and only show the player list
86 | player_list_header = "Players: "
87 |
88 | # Placeholders available: {username}, {ping}
89 | player_list_player = "{username}"
90 |
91 | # Separator between players in the list, \n can be used for new line
92 | player_list_separator = ", "
93 |
94 | # Maximum number of players to show in the topic
95 | # Set to 0 to show all players
96 | player_list_max_count = 10
97 |
98 | [discord.webhook]
99 | # Full webhook URL to send chat messages to
100 | url = ""
101 | # Full URL of an avatar service to get the player's avatar from
102 | # Placeholders available: {uuid}, {username}
103 | avatar_url = "https://visage.surgeplay.com/face/96/{uuid}"
104 |
105 | # The format of the webhook's username
106 | # Placeholders available: {username}, {server}
107 | username = "{username}"
108 |
109 | # Minecraft > Discord message formats
110 | # Uses the same formatting as the Discord client (a subset of markdown)
111 | #
112 | # Messages can be disabled by setting format to empty string ("") or false
113 | #
114 | # type can be one of the following:
115 | # "text" - Normal text only message with the associated x_message format
116 | # "embed" - Discord embed with the associated x_message format as the description field
117 | # Default for all is "text"
118 | #
119 | # embed_color is the color of the embed, in #RRGGBB format
120 | [discord.chat.message]
121 | # Placeholders available: {username}, {prefix}, {server}, {message}
122 | # Can be disabled with "" or false
123 | format = "{username}: {message}"
124 |
125 | # for user messages, the following types can be used
126 | # "text" - Normal text only message with the above
127 | #
128 | # "webhook" - Use a Discord webhook to have the bot use the player's username and avatar when sending messages
129 | # Requires a webhook URL to be set below
130 | # Ignores the above message format, and just sends the message as the content of the webhook
131 | #
132 | # "embed" - Discord embed with the above format as the description field
133 | type = "text"
134 | # Can be disabled with "" or false
135 | embed_color = ""
136 | # Channel override for this message type, set to "" or false or remove to use the default channel
137 | # Can be applied to all message types
138 | # channel = "000000000000000000"
139 |
140 | [discord.chat.join]
141 | # Placeholders available: {username}, {prefix}, {server}
142 | # Can be disabled with "" or false
143 | format = "**{username} joined the game**"
144 | type = "text"
145 | # Can be disabled with "" or false
146 | embed_color = "#40bf4f"
147 |
148 | [discord.chat.leave]
149 | # Placeholders available: {username}, {prefix}, {server}
150 | # Can be disabled with "" or false
151 | format = "**{username} left the game**"
152 | type = "text"
153 | # Can be disabled with "" or false
154 | embed_color = "#bf4040"
155 |
156 | [discord.chat.disconnect]
157 | # Possible different format for timeouts or other terminating connections
158 | # Placeholders available: {username}, {prefix}
159 | # Can be disabled with "" or false
160 | format = "**{username} disconnected**"
161 | type = "text"
162 | # Can be disabled with "" or false
163 | embed_color = "#bf4040"
164 |
165 | [discord.chat.server_switch]
166 | # Placeholders available: {username}, {prefix}, {current}, {previous}
167 | # Can be disabled with "" or false
168 | format = "**{username} moved to {current} from {previous}**"
169 | type = "text"
170 | # Can be disabled with "" or false
171 | embed_color = "#40bf4f"
172 |
173 | [discord.chat.death]
174 | # Placeholders available: {username}, {death_message}
175 | # death_message includes the username just as it is shown ingame
176 | # Can be disabled with "" or false
177 | format = "**{death_message}**"
178 | type = "text"
179 | # Can be disabled with "" or false
180 | embed_color = "#bf4040"
181 |
182 | [discord.chat.advancement]
183 | # Placeholders available: {username}, {advancement_title}, {advancement_description}
184 | # Can be disabled with "" or false
185 | format = "**{username} has made the advancement __{advancement_title}__**\n_{advancement_description}_"
186 | type = "text"
187 | # Can be disabled with "" or false
188 | embed_color = "#40bf4f"
189 |
190 | # Not server overridable
191 | [discord.chat.proxy_start]
192 | # Can be disabled with "" or false
193 | format = "**Proxy started**"
194 | type = "text"
195 | # Can be disabled with "" or false
196 | embed_color = "#40bf4f"
197 |
198 | # Not server overridable
199 | [discord.chat.proxy_stop]
200 | # Can be disabled with "" or false
201 | format = "**Proxy stopped**"
202 | type = "text"
203 | # Can be disabled with "" or false
204 | embed_color = "#bf4040"
205 |
206 | [discord.chat.server_start]
207 | # Placeholders available: {server}
208 | # Can be disabled with "" or false
209 | format = "**{server} has started**"
210 | type = "text"
211 | # Can be disabled with "" or false
212 | embed_color = "#40bf4f"
213 |
214 | [discord.chat.server_stop]
215 | # Placeholders available: {server}
216 | # Can be disabled with "" or false
217 | format = "**{server} has stopped**"
218 | type = "text"
219 | # Can be disabled with "" or false
220 | embed_color = "#bf4040"
221 |
222 | [discord.commands.list]
223 | # Not server overridable
224 | enabled = true
225 |
226 | # Ephemeral messages are only visible to the user who sent the command
227 | # Not server overridable
228 | ephemeral = true
229 |
230 | # Placeholders available: {server_name}, {online_players}, {max_players}
231 | server_format = "[{server_name} {online_players}/{max_players}]"
232 |
233 | # Placeholders available: {username}
234 | player_format = "- {username}"
235 |
236 | # Can be disabled with "" or false
237 | no_players = "No players online"
238 |
239 | # Can be disabled with "" or false
240 | server_offline = "Server offline"
241 | # Not server overridable
242 | codeblock_lang = "asciidoc"
243 |
244 | # Discord > Minecraft message formats
245 | # Uses XML-like formatting with https://docs.advntr.dev/minimessage/format.html
246 | [minecraft]
247 | # Ingame command for plugin
248 | # Not server overridable
249 | # e.g., /discord, /discord reload, /discord topic preview
250 | plugin_command = "discord"
251 |
252 | # Placeholders available: {discord}
253 | discord_chunk = "[<{discord_color}>Discord]"
254 |
255 | # Placeholders available: {role_color}, {display_name}, {username}, {nickname}
256 | # tag allows you to shift right-click the username to insert @{username} in the chat
257 | username_chunk = "<{role_color}>{nickname}"
258 |
259 | # Placeholders available: {discord_chunk}, {username_chunk}, {attachments}, {message}
260 | message = "{discord_chunk} {role_prefix} {username_chunk}: {message} {attachments}"
261 |
262 | # Placeholders available: {url}, {attachment_color}
263 | attachments = "[<{attachment_color}>Attachment]"
264 |
265 | # Placeholders available: {url}, {link_color}
266 | # Can be disabled with "" or false
267 | links = "[<{link_color}>Link]"
268 |
269 | # Colors for the <{discord_color}>, <{attachment_color}> and <{link_color}> tags
270 | discord_color = "#7289da"
271 | attachment_color = "#4abdff"
272 | link_color = "#4abdff"
273 |
274 | # Role prefix configuration
275 | # Format: "role_id" = "prefix format using MiniMessage"
276 | [minecraft.role_prefixes]
277 | # "123456789" = "[OWNER]"
278 | # "987654321" = "[ADMIN]"
279 | # "456789123" = "[MOD]"
280 | # "789123456" = "[HELPER]"
281 |
282 | # Override config for specific servers
283 | # Any config option under [discord] or [minecraft] can be overridden (other than options labelled not server overridable)
284 | # Format: [override.(velocity.toml server name).discord] or [override.(velocity.toml server name).minecraft]
285 | # Example:
286 | # [override.lobby.discord]
287 | # channel = "000000000000000000"
288 |
--------------------------------------------------------------------------------
/src/test/java/ooo/foooooooooooo/velocitydiscord/config/PluginConfigTests.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.definitions.UserMessageType;
4 | import org.junit.jupiter.api.Test;
5 | import org.junit.jupiter.api.io.TempDir;
6 |
7 | import java.awt.*;
8 | import java.nio.file.Path;
9 | import java.util.List;
10 | import java.util.Optional;
11 |
12 | import static org.junit.jupiter.api.Assertions.*;
13 |
14 | public class PluginConfigTests {
15 | @Test
16 | public void allConfigKeysLoadedCorrectly(@TempDir Path tempDir) {
17 | var config = TestUtils.createConfig(TestUtils.getResource("config.toml"), tempDir);
18 | var pluginConfig = new PluginConfig(config);
19 |
20 | // global config
21 | assertEquals(List.of("survival"), pluginConfig.global.excludedServers);
22 | assertTrue(pluginConfig.global.excludedServersReceiveMessages);
23 | assertEquals(123, pluginConfig.global.pingIntervalSeconds);
24 | assertEquals("lobby_test_name", pluginConfig.global.serverDisplayNames.get("lobby"));
25 |
26 | // global discord config
27 | var globalDiscord = pluginConfig.global.discord;
28 | assertEquals("test_token", globalDiscord.token);
29 | assertEquals(Optional.of("activity_text_test"), globalDiscord.activityText);
30 | assertEquals(123, globalDiscord.updateChannelTopicIntervalMinutes);
31 |
32 | // local discord config
33 | var discord = pluginConfig.local.discord;
34 | assertEquals("123456789012345678", discord.mainChannelId);
35 | assertTrue(discord.showBotMessages);
36 | assertFalse(discord.showAttachmentsIngame);
37 | assertFalse(discord.enableMentions);
38 | assertTrue(discord.enableEveryoneAndHere);
39 |
40 | // channel topic config
41 | var topic = discord.channelTopic;
42 | assertEquals(Optional.of("format_test"), topic.format);
43 | assertEquals(Optional.of("server_test"), topic.serverFormat);
44 | assertEquals(Optional.of("server_offline_test"), topic.serverOfflineFormat);
45 | assertEquals(Optional.of("players_no_players_header_test"), topic.playerListNoPlayersHeader);
46 | assertEquals(Optional.of("player_list_header_test"), topic.playerListHeader);
47 | assertEquals("player_list_player_test", topic.playerListPlayerFormat);
48 | assertEquals("player_list_separator_test", topic.playerListSeparator);
49 | assertEquals(123, topic.playerListMaxCount);
50 |
51 | // webhook config
52 | var webhook = discord.webhook;
53 | assertEquals("url_test", webhook.url);
54 | assertEquals("avatar_url_test", webhook.avatarUrl);
55 | assertEquals("username_test", webhook.username);
56 |
57 | // chat message config
58 | var chat = discord.chat;
59 | var messageConfig = chat.message;
60 | assertEquals(Optional.of("format_test"), messageConfig.format);
61 | assertEquals(UserMessageType.EMBED, messageConfig.type);
62 | assertEquals(Optional.of(Color.decode("#ff00ff")), messageConfig.embedColor);
63 |
64 | // join config
65 | var joinConfig = chat.join;
66 | assertEquals(Optional.of("format_test"), joinConfig.format);
67 | assertEquals(UserMessageType.EMBED, joinConfig.type);
68 | assertEquals(Optional.of(Color.decode("#ff00ff")), joinConfig.embedColor);
69 |
70 | // leave config
71 | var leaveConfig = chat.leave;
72 | assertEquals(Optional.of("format_test"), leaveConfig.format);
73 | assertEquals(UserMessageType.EMBED, leaveConfig.type);
74 | assertEquals(Optional.of(Color.decode("#ff00ff")), leaveConfig.embedColor);
75 |
76 | // server switch config
77 | var serverSwitchConfig = chat.serverSwitch;
78 | assertEquals(Optional.of("format_test"), serverSwitchConfig.format);
79 | assertEquals(UserMessageType.EMBED, serverSwitchConfig.type);
80 | assertEquals(Optional.of(Color.decode("#ff00ff")), serverSwitchConfig.embedColor);
81 |
82 | // death config
83 | var deathConfig = chat.death;
84 | assertEquals(Optional.of("format_test"), deathConfig.format);
85 | assertEquals(UserMessageType.EMBED, deathConfig.type);
86 | assertEquals(Optional.of(Color.decode("#ff00ff")), deathConfig.embedColor);
87 |
88 | // minecraft config
89 | var minecraft = pluginConfig.local.minecraft;
90 | assertEquals("discord_chunk_test", minecraft.discordChunkFormat);
91 | assertEquals("username_chunk_test", minecraft.usernameChunkFormat);
92 | assertEquals("message_test", minecraft.messageFormat);
93 | assertEquals("attachments_test", minecraft.attachmentFormat);
94 | assertEquals(Optional.of("links_test"), minecraft.linkFormat);
95 | assertEquals("#ff00ff", minecraft.discordColor);
96 | assertEquals("#ff00ff", minecraft.attachmentColor);
97 | assertEquals("#ff00ff", minecraft.linkColor);
98 | assertEquals("role_prefix_test_1", minecraft.rolePrefixes.get("123456789"));
99 | assertEquals("role_prefix_test_2", minecraft.rolePrefixes.get("987654321"));
100 |
101 | // list command config
102 | var globalListCommand = pluginConfig.global.discord.commands.list;
103 | assertFalse(globalListCommand.enabled);
104 | assertFalse(globalListCommand.ephemeral);
105 | assertEquals("codeblock_lang_test", globalListCommand.codeblockLang);
106 |
107 | var listCommand = discord.commands.list;
108 | assertEquals("server_format_test", listCommand.serverFormat);
109 | assertEquals("player_format_test", listCommand.playerFormat);
110 | assertEquals(Optional.of("no_players_test"), listCommand.noPlayersFormat);
111 | assertEquals(Optional.of("server_offline_test"), listCommand.serverOfflineFormat);
112 | }
113 |
114 | @Test
115 | public void serverDisplayNamesWorks(@TempDir Path tempDir) {
116 | var config = TestUtils.createConfig(TestUtils.getResource("real_test_config.toml"), tempDir);
117 | var pluginConfig = new PluginConfig(config);
118 |
119 | assertEquals("Server A", pluginConfig.global.serverDisplayNames.get("server_a"));
120 | assertEquals("Server B", pluginConfig.global.serverDisplayNames.get("server_b"));
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/test/java/ooo/foooooooooooo/velocitydiscord/config/TestUtils.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config;
2 |
3 | import com.electronwill.nightconfig.core.file.FileConfig;
4 | import org.jetbrains.annotations.NotNull;
5 |
6 | import java.io.FileWriter;
7 | import java.io.IOException;
8 | import java.nio.file.Path;
9 |
10 | public class TestUtils {
11 | public static Config createConfig(String s, @NotNull Path tempDir) {
12 | var test = tempDir.resolve("test.toml");
13 |
14 | try (var w = new FileWriter(test.toFile())) {
15 | w.write(s);
16 | } catch (IOException e) {
17 | throw new RuntimeException(e);
18 | }
19 |
20 | var toml = FileConfig.of(test);
21 | toml.load();
22 |
23 | return new Config(toml);
24 | }
25 |
26 | public static String getResource(String name) {
27 | var resource = PluginConfigTests.class.getClassLoader().getResourceAsStream(name);
28 |
29 | try (resource) {
30 | if (resource == null) throw new RuntimeException("Resource not found: " + name);
31 | return new String(resource.readAllBytes());
32 | } catch (IOException e) {
33 | throw new RuntimeException("Failed to read resource: " + name, e);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/test/java/ooo/foooooooooooo/velocitydiscord/config/WebhookConfigTests.java:
--------------------------------------------------------------------------------
1 | package ooo.foooooooooooo.velocitydiscord.config;
2 |
3 | import ooo.foooooooooooo.velocitydiscord.config.definitions.WebhookConfig;
4 | import org.junit.jupiter.api.Test;
5 | import org.junit.jupiter.api.io.TempDir;
6 |
7 | import java.nio.file.Path;
8 |
9 | import static org.junit.jupiter.api.Assertions.*;
10 |
11 | public class WebhookConfigTests {
12 | @Test
13 | public void webhookIdParsedCorrectly(@TempDir Path tempDir) {
14 | var content = """
15 | url = "https://discord.com/api/webhooks/1290368230789893527/tokentokentokentokentokentokentokentokentokentokentokentokentoken"
16 | username = "{username}"
17 | """;
18 |
19 | var config = TestUtils.createConfig(content, tempDir);
20 | var webhookConfig = new WebhookConfig();
21 | webhookConfig.load(config);
22 |
23 | assertNotNull(webhookConfig.id);
24 | assertEquals("1290368230789893527", webhookConfig.id);
25 | assertFalse(webhookConfig.isInvalid());
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/test/resources/config.toml:
--------------------------------------------------------------------------------
1 | #:schema https://raw.githubusercontent.com/fooooooooooooooo/VelocityDiscord/refs/heads/master/schema.json
2 |
3 | # Don't change this
4 | config_version = "2.0"
5 |
6 | # Comma separated list of server names to exclude from the bridge (defined under [servers] inside your velocity.toml)
7 | # e.g., exclude_servers = ["lobby", "survival"]
8 | exclude_servers = [
9 | "survival"
10 | ]
11 | excluded_servers_receive_messages = true
12 |
13 | # How often to ping all servers to check for online status (seconds)
14 | # Excluded servers will not be pinged
15 | # Use a value of 0 to disable
16 | ping_interval = 123
17 |
18 | # Server display names
19 | # If a server is not found in this list, the server name (from velocity.toml) will be used instead
20 | [server_names]
21 | lobby = "lobby_test_name"
22 |
23 | [discord]
24 | # Bot token from https://discordapp.com/developers/applications/
25 | # Not server overridable
26 | token = "test_token"
27 | # Default channel ID to send Minecraft chat messages to
28 | channel = "123456789012345678"
29 |
30 | # Show messages from bots in Minecraft chat
31 | show_bot_messages = true
32 | # Show clickable links for attachments in Minecraft chat
33 | show_attachments_ingame = false
34 |
35 | # Activity text of the bot to show in Discord
36 | # Placeholders available: {amount}
37 | # Can be disabled with "" or false
38 | # Not server overridable
39 | activity_text = "activity_text_test"
40 |
41 | # Enable mentioning Discord users from Minecraft chat
42 | enable_mentions = false
43 | # Enable @everyone and @here pings from Minecraft chat
44 | enable_everyone_and_here = true
45 |
46 | # Set the interval (in minutes) for updating the channel topic
47 | # Use a value of 0 to disable
48 | # Not server overridable
49 | update_channel_topic_interval = 123
50 |
51 | # Channel topic config (if enabled)
52 | [discord.channel_topic]
53 | # Template for the channel topic
54 | # Placeholders available:
55 | # {players} - Total number of players online
56 | # {player_list} - List of players (format is defined below)
57 | # {servers} - Number of servers
58 | # {server_list} - List of server names
59 | # {hostname} - Server hostname
60 | # {port} - Server port
61 | # {motd} - Message of the Day (MOTD)
62 | # {query_port} - Query port
63 | # {max_players} - Maximum number of players
64 | # {plugins} - Number of plugins
65 | # {plugin_list} - List of plugin names
66 | # {version} - Server version
67 | # {software} - Software name
68 | # {average_ping} - Average ping of all players
69 | # {uptime} - Server uptime in hours and minutes
70 | # {server[SERVERNAME]} - Dynamic placeholder for each server's name and status (e.g., {server[MyServer]}, {server[AnotherServer]}, {server[Lobby]}, etc.)
71 | format = "format_test"
72 |
73 | # Template for server[SERVERNAME] placeholder in the channel topic
74 | # Placeholders available: {name}, {players}, {max_players}, {motd}, {version}, {protocol}
75 | server = "server_test"
76 |
77 | # Template for server[SERVERNAME] placeholder in the channel topic when the server is offline
78 | # Placeholders available: {name}
79 | server_offline = "server_offline_test"
80 |
81 | # Can be disabled with "" or false to hide the list completely when no players are online
82 | player_list_no_players_header = "players_no_players_header_test"
83 |
84 | # Can be disabled with "" or false to hide the header and only show the player list
85 | player_list_header = "player_list_header_test"
86 |
87 | # Placeholders available: {username}, {ping}
88 | player_list_player = "player_list_player_test"
89 |
90 | # Separator between players in the list, \n can be used for new line
91 | player_list_separator = "player_list_separator_test"
92 |
93 | # Maximum number of players to show in the topic
94 | # Set to < 1 to show all players
95 | player_list_max_count = 123
96 |
97 | [discord.webhook]
98 | # Full webhook URL to send more fancy Minecraft chat messages to
99 | url = "url_test"
100 | # Full URL of an avatar service to get the player's avatar from
101 | # Placeholders available: {uuid}, {username}
102 | avatar_url = "avatar_url_test"
103 | # The format of the webhook's username
104 | # Placeholders available: {username}, {server}
105 | username = "username_test"
106 |
107 | # Minecraft > Discord message formats
108 | # Uses the same formatting as the Discord client (a subset of markdown)
109 | # Messages can be disabled with empty string ("") or false
110 | #
111 | # x_message_type can be one of the following:
112 | # "text" - Normal text only message with the associated x_message format
113 | # "embed" - Discord embed with the associated x_message format as the description field
114 | # Default for all is "text"
115 | #
116 | # x_message_embed_color is the color of the embed, in #RRGGBB format
117 | [discord.chat.message]
118 | # Placeholders available: {username}, {prefix}, {server}, {message}
119 | # Can be disabled with "" or false
120 | format = "format_test"
121 |
122 | # for user messages, the following types can be used
123 | # "text" - Normal text only message with the above
124 | #
125 | # "webhook" - Use a Discord webhook to have the bot use the player's username and avatar when sending messages
126 | # Requires a webhook URL to be set below
127 | # Ignores the above message format, and just sends the message as the content of the webhook
128 | #
129 | # "embed" - Discord embed with the above format as the description field
130 | type = "embed"
131 | # Can be disabled with "" or false
132 | embed_color = "#ff00ff"
133 | # Channel override for this message type, set to "" or false or remove to use the default channel
134 | # Can be applied to all message types
135 | # channel = "000000000000000000"
136 | [discord.chat.message.webhook]
137 | url = "message_webhook_url_test"
138 | username = "message_webhook_username_test"
139 | avatar_url = "message_webhook_avatar_url_test"
140 |
141 | [discord.chat.join]
142 | # Placeholders available: {username}, {prefix}, {server}
143 | # Can be disabled with "" or false
144 | format = "format_test"
145 | type = "embed"
146 | # Can be disabled with "" or false
147 | embed_color = "#ff00ff"
148 | [discord.chat.join.webhook]
149 | url = "join_webhook_url_test"
150 | username = "join_webhook_username_test"
151 | avatar_url = "join_webhook_avatar_url_test"
152 |
153 | [discord.chat.leave]
154 | # Placeholders available: {username}, {prefix}, {server}
155 | # Can be disabled with "" or false
156 | format = "format_test"
157 | type = "embed"
158 | # Can be disabled with "" or false
159 | embed_color = "#ff00ff"
160 | [discord.chat.leave.webhook]
161 | url = "leave_webhook_url_test"
162 | username = "leave_webhook_username_test"
163 | avatar_url = "leave_webhook_avatar_url_test"
164 |
165 | [discord.chat.disconnect]
166 | # Possible different format for timeouts or other terminating connections
167 | # Placeholders available: {username}, {prefix}
168 | # Can be disabled with "" or false
169 | format = "format_test"
170 | type = "embed"
171 | # Can be disabled with "" or false
172 | embed_color = "#ff00ff"
173 | [discord.chat.disconnect.webhook]
174 | url = "disconnect_webhook_url_test"
175 | username = "disconnect_webhook_username_test"
176 | avatar_url = "disconnect_webhook_avatar_url_test"
177 |
178 | [discord.chat.server_switch]
179 | # Placeholders available: {username}, {prefix}, {current}, {previous}
180 | # Can be disabled with "" or false
181 | format = "format_test"
182 | type = "embed"
183 | # Can be disabled with "" or false
184 | embed_color = "#ff00ff"
185 | [discord.chat.server_switch.webhook]
186 | url = "server_switch_webhook_url_test"
187 | username = "server_switch_webhook_username_test"
188 | avatar_url = "server_switch_webhook_avatar_url_test"
189 |
190 | [discord.chat.death]
191 | # Placeholders available: {username}, {death_message}
192 | # death_message includes the username just as it is shown ingame
193 | # Can be disabled with "" or false
194 | format = "format_test"
195 | type = "embed"
196 | # Can be disabled with "" or false
197 | embed_color = "#ff00ff"
198 | [discord.chat.death.webhook]
199 | url = "death_webhook_url_test"
200 | username = "death_webhook_username_test"
201 | avatar_url = "death_webhook_avatar_url_test"
202 |
203 | [discord.chat.advancement]
204 | # Placeholders available: {username}, {advancement_title}, {advancement_description}
205 | # Can be disabled with "" or false
206 | format = "format_test"
207 | type = "embed"
208 | # Can be disabled with "" or false
209 | embed_color = "#ff00ff"
210 | [discord.chat.advancement.webhook]
211 | url = "advancement_webhook_url_test"
212 | username = "advancement_webhook_username_test"
213 | avatar_url = "advancement_webhook_avatar_url_test"
214 |
215 | # Not server overridable
216 | [discord.chat.proxy_start]
217 | # Can be disabled with "" or false
218 | format = "format_test"
219 | type = "embed"
220 | # Can be disabled with "" or false
221 | embed_color = "#ff00ff"
222 |
223 | # Not server overridable
224 | [discord.chat.proxy_stop]
225 | # Can be disabled with "" or false
226 | format = "format_test"
227 | type = "embed"
228 | # Can be disabled with "" or false
229 | embed_color = "#ff00ff"
230 |
231 | [discord.chat.server_start]
232 | # Placeholders available: {server}
233 | # Can be disabled with "" or false
234 | format = "format_test"
235 | type = "embed"
236 | # Can be disabled with "" or false
237 | embed_color = "#ff00ff"
238 |
239 | [discord.chat.server_stop]
240 | # Placeholders available: {server}
241 | # Can be disabled with "" or false
242 | format = "format_test"
243 | type = "embed"
244 | # Can be disabled with "" or false
245 | embed_color = "#ff00ff"
246 |
247 | [discord.commands.list]
248 | # Not server overridable
249 | enabled = false
250 |
251 | # Ephemeral messages are only visible to the user who sent the command
252 | # Not server overridable
253 | ephemeral = false
254 |
255 | # Placeholders available: {server_name}, {online_players}, {max_players}
256 | server_format = "server_format_test"
257 |
258 | # Placeholders available: {username}
259 | player_format = "player_format_test"
260 |
261 | # Can be disabled with "" or false
262 | no_players = "no_players_test"
263 |
264 | # Can be disabled with "" or false
265 | server_offline = "server_offline_test"
266 | # Not server overridable
267 | codeblock_lang = "codeblock_lang_test"
268 |
269 | # Discord > Minecraft message formats
270 | # Uses XML-like formatting with https://docs.advntr.dev/minimessage/format.html
271 | [minecraft]
272 | # Ingame command for plugin
273 | # Not server overridable
274 | # e.g., /discord, /discord reload, /discord topic preview
275 | plugin_command = "discord"
276 |
277 | # Placeholders available: {discord}
278 | discord_chunk = "discord_chunk_test"
279 |
280 | # Placeholders available: {role_color}, {display_name}, {username}, {nickname}
281 | # tag allows you to shift right-click the username to insert @{username} in the chat
282 | username_chunk = "username_chunk_test"
283 |
284 | # Placeholders available: {discord_chunk}, {username_chunk}, {attachments}, {message}
285 | message = "message_test"
286 |
287 | # Placeholders available: {url}, {attachment_color}
288 | attachments = "attachments_test"
289 |
290 | # Placeholders available: {url}, {link_color}
291 | # Can be disabled with "" or false
292 | links = "links_test"
293 |
294 | # Colors for the <{discord_color}>, <{attachment_color}> and <{link_color}> tags
295 | discord_color = "#ff00ff"
296 | attachment_color = "#ff00ff"
297 | link_color = "#ff00ff"
298 |
299 | # Role prefix configuration
300 | # Format: "role_id" = "prefix format using MiniMessage"
301 | [minecraft.role_prefixes]
302 | "123456789" = "role_prefix_test_1"
303 | "987654321" = "role_prefix_test_2"
304 |
305 | # Override config for specific servers
306 | # Any config option under [discord] or [minecraft] can be overridden (other than options labelled not server overridable)
307 | # Format: [override.(velocity.toml server name).discord] or [override.(velocity.toml server name).minecraft]
308 | # Example:
309 | # [override.lobby.discord]
310 | # channel = "000000000000000000"
311 |
--------------------------------------------------------------------------------
/src/test/resources/real_test_config.toml:
--------------------------------------------------------------------------------
1 | #:schema https://raw.githubusercontent.com/fooooooooooooooo/VelocityDiscord/refs/heads/master/schema.json
2 |
3 | # Don't change this
4 | config_version = "2.0"
5 |
6 | # Comma separated list of server names to exclude from the bridge (defined under [servers] inside your velocity.toml)
7 | # e.g., exclude_servers = ["lobby", "survival"]
8 | exclude_servers = []
9 | excluded_servers_receive_messages = false
10 |
11 | # How often to ping all servers to check for online status (seconds)
12 | # Set to 0 to disable
13 | # Excluded servers will not be pinged
14 | ping_interval = 30
15 |
16 | # Server display names
17 | # If a server is not found in this list, the server name will be used instead
18 | [server_names]
19 | server_a = "Server A"
20 | server_b = "Server B"
21 |
22 | [discord]
23 | # Bot token from https://discordapp.com/developers/applications/
24 | token = "test_token"
25 | # Channel ID to send Minecraft chat messages to
26 | channel = "0000000000000000000"
27 |
28 | # Show messages from bots in Minecraft chat
29 | show_bot_messages = false
30 | # Show clickable links for attachments in Minecraft chat
31 | show_attachments_ingame = true
32 |
33 | # Show a text as playing activity of the bot
34 | show_activity = true
35 | # Activity text of the bot to show in Discord
36 | # Placeholders available: {amount}
37 | activity_text = "with {amount} players online"
38 |
39 | # Enable mentioning Discord users from Minecraft chat
40 | enable_mentions = true
41 | # Enable @everyone and @here pings from Minecraft chat
42 | enable_everyone_and_here = false
43 |
44 | # OPTIONAL - Configuration for updating the Discord channel topic
45 | # Set the interval (in minutes) for updating the channel topic.
46 | # Use a value less than 10 to disable this feature.
47 | update_channel_topic_interval = 0
48 |
49 | [discord.channel_topic]
50 | # Template for the channel topic.
51 | # Placeholders available:
52 | # {players} - Total number of players online
53 | # {player_list} - List of players (format is defined below)
54 | # {servers} - Number of servers
55 | # {server_list} - List of server names
56 | # {hostname} - Server hostname
57 | # {port} - Server port
58 | # {motd} - Message of the Day (MOTD)
59 | # {query_port} - Query port
60 | # {max_players} - Maximum number of players
61 | # {plugins} - Number of plugins
62 | # {plugin_list} - List of plugin names
63 | # {version} - Server version
64 | # {software} - Software name
65 | # {average_ping} - Average ping of all players
66 | # {uptime} - Server uptime in hours and minutes
67 | # {server[SERVERNAME]} - Dynamic placeholder for each server's name and status (e.g., {server[MyServer]}, {server[AnotherServer]}, {server[Lobby]}, etc.)
68 | format = """{players}/{max_players}
69 | {player_list}
70 | {hostname}:{port}
71 | Uptime: {uptime}"""
72 |
73 | # Template for server[SERVERNAME] placeholder in the channel topic.
74 | # Placeholders available: {name}, {players}, {max_players}, {motd}, {version}, {protocol}
75 | server = "{name}: {players}/{max_players}"
76 |
77 | # Template for server[SERVERNAME] placeholder in the channel topic when the server is offline.
78 | # Placeholders available: {name}
79 | server_offline = "{name}: Offline"
80 |
81 | # Can be disabled to hide the list completely when no players are online
82 | player_list_no_players_header = "No players online"
83 |
84 | # Can be disabled to hide the header and only show the player list
85 | player_list_header = "Players: "
86 |
87 | # Placeholders available: {username}, {ping}
88 | player_list_player = "{username}"
89 |
90 | # Separator between players in the list, \n can be used for new line
91 | player_list_separator = ", "
92 |
93 | # Maximum number of players to show in the topic
94 | # Set to < 1 to show all players
95 | player_list_max_count = 10
96 |
97 | [discord.webhook]
98 | # Full webhook URL to send more fancy Minecraft chat messages to
99 | url = "https://discord.com/api/webhooks/0000000000000000000/test"
100 | # Full URL of an avatar service to get the player's avatar from
101 | # Placeholders available: {uuid}, {username}
102 | avatar_url = "https://visage.surgeplay.com/face/96/{uuid}"
103 |
104 | # The format of the webhook's username
105 | # Placeholders available: {username}, {server}
106 | username = "{username}"
107 |
108 | # Minecraft > Discord message formats
109 | # Uses the same formatting as the Discord client (a subset of markdown)
110 | # Messages can be disabled with empty string ("") or false
111 | #
112 | # type can be one of the following:
113 | # "text" - Normal text only message with the associated x_message format
114 | # "embed" - Discord embed with the associated x_message format as the description field
115 | # Default for all is "text"
116 | #
117 | # embed_color is the color of the embed, in #RRGGBB format
118 | [discord.chat.message]
119 | # Placeholders available: {username}, {prefix}, {server}, {message}
120 | # Can be disabled
121 | format = "{username}: {message}"
122 |
123 | # for user messages, the following types can be used
124 | # "text" - Normal text only message with the above
125 | #
126 | # "webhook" - Use a Discord webhook to have the bot use the player's username and avatar when sending messages
127 | # Requires a webhook URL to be set below
128 | # Ignores the above message format, and just sends the message as the content of the webhook
129 | #
130 | # "embed" - Discord embed with the above format as the description field
131 | type = "webhook"
132 | # Can be disabled
133 | embed_color = ""
134 | # Channel override for this message type, set to "" or false or remove to use the default channel
135 | # Can be applied to all message types
136 | channel = "0000000000000000000"
137 |
138 | # [discord.chat.message.webhook]
139 | # url = "https://discord.com/api/webhooks/0000000000000000000/test"
140 | # username = "{username}"
141 | # avatar_url = "https://visage.surgeplay.com/face/96/{uuid}"
142 |
143 | [discord.chat.join]
144 | # Placeholders available: {username}, {prefix}, {server}
145 | # Can be disabled
146 | format = "**{username} joined the game**"
147 | type = "embed"
148 | # Can be disabled
149 | embed_color = "#40bf4f"
150 | channel = "0000000000000000000"
151 |
152 | [discord.chat.leave]
153 | # Placeholders available: {username}, {prefix}, {server}
154 | # Can be disabled
155 | format = "**{username} left the game**"
156 | type = "text"
157 | # Can be disabled
158 | embed_color = "#bf4040"
159 | channel = "0000000000000000000"
160 |
161 | [discord.chat.disconnect]
162 | # Possible different format for timeouts or other terminating connections
163 | # Placeholders available: {username}, {prefix}
164 | # Can be disabled
165 | format = "**{username} disconnected**"
166 | type = "webhook"
167 | # Can be disabled
168 | embed_color = "#bf4040"
169 | channel = "0000000000000000000"
170 |
171 | [discord.chat.server_switch]
172 | # Placeholders available: {username}, {prefix}, {current}, {previous}
173 | # Can be disabled
174 | format = "**{username} moved to {current} from {previous}**"
175 | type = "webhook"
176 | # Can be disabled
177 | embed_color = "#40bf4f"
178 | channel = "0000000000000000000"
179 |
180 | [discord.chat.death]
181 | # Placeholders available: {username}, {death_message}
182 | # death_message includes the username just as it is shown ingame
183 | # Can be disabled
184 | format = "**{death_message}**"
185 | type = "webhook"
186 | # Can be disabled
187 | embed_color = "#bf4040"
188 | channel = "0000000000000000000"
189 |
190 | [discord.chat.advancement]
191 | # Placeholders available: {username}, {advancement_title}, {advancement_description}
192 | # Can be disabled
193 | format = "**{username} has made the advancement __{advancement_title}__**\n_{advancement_description}_"
194 | type = "webhook"
195 | # Can be disabled
196 | embed_color = "#40bf4f"
197 | channel = "0000000000000000000"
198 |
199 | [discord.chat.server_start]
200 | # Placeholders available: {server}
201 | # Can be disabled
202 | format = "**{server} has started**"
203 | type = "text"
204 | # Can be disabled
205 | embed_color = "#40bf4f"
206 | channel = "0000000000000000000"
207 |
208 | [discord.chat.server_stop]
209 | # Placeholders available: {server}
210 | # Can be disabled
211 | format = "**{server} has stopped**"
212 | type = "text"
213 | # Can be disabled
214 | embed_color = "#bf4040"
215 | channel = "0000000000000000000"
216 |
217 | [discord.chat.proxy_start]
218 | # Can be disabled
219 | format = "**Proxy started**"
220 | type = "text"
221 | # Can be disabled
222 | embed_color = "#40bf4f"
223 | channel = "0000000000000000000"
224 |
225 | [discord.chat.proxy_stop]
226 | # Can be disabled
227 | format = "**Proxy stopped**"
228 | type = "text"
229 | # Can be disabled
230 | embed_color = "#bf4040"
231 | channel = "0000000000000000000"
232 |
233 | [discord.commands.list]
234 | enabled = true
235 |
236 | # Ephemeral messages are only visible to the user who sent the command
237 | ephemeral = true
238 |
239 | # Placeholders available: {server_name}, {online_players}, {max_players}
240 | server_format = "[{server_name} {online_players}/{max_players}]"
241 |
242 | # Placeholders available: {username}
243 | player_format = "- {username}"
244 |
245 | # Can be disabled
246 | no_players = "No players online"
247 |
248 | # Can be disabled
249 | server_offline = "Server offline"
250 | codeblock_lang = "asciidoc"
251 |
252 | # Discord > Minecraft message formats
253 | # Uses XML-like formatting with https://docs.advntr.dev/minimessage/format.html
254 | [minecraft]
255 | # Ingame command for plugin
256 | # Not server overridable
257 | # e.g., /discord, /discord reload, /discord topic preview
258 | plugin_command = "discord"
259 |
260 | # Placeholders available: {discord}
261 | discord_chunk = "[<{discord_color}>Discord]"
262 |
263 | # Placeholders available: {role_color}, {display_name}, {username}, {nickname}
264 | # tag allows you to shift right-click the username to insert @{username} in the chat
265 | username_chunk = "<{role_color}>{nickname}"
266 |
267 | # Placeholders available: {discord_chunk}, {username_chunk}, {attachments}, {message}
268 | message = "{discord_chunk} {role_prefix} {username_chunk}: {message} {attachments}"
269 |
270 | # Placeholders available: {url}, {attachment_color}
271 | attachments = "[<{attachment_color}>Attachment]"
272 |
273 | # Placeholders available: {url}, {link_color}
274 | # Can be disabled
275 | links = "[<{link_color}>Link]"
276 |
277 | # Colors for the <{discord_color}>, <{attachment_color}> and <{link_color}> tags
278 | discord_color = "#7289da"
279 | attachment_color = "#4abdff"
280 | link_color = "#55FF55"
281 |
282 | # Role prefix configuration
283 | # Format: "role_id" = "prefix format using MiniMessage"
284 | [minecraft.role_prefixes]
285 | # "123456789" = "[OWNER]"
286 | # "987654321" = "[ADMIN]"
287 | # "456789123" = "[MOD]"
288 | # "789123456" = "[HELPER]"
289 |
290 | # Override config for specific servers
291 | # Any config option under [discord] or [minecraft] can be overridden (other than discord.token)
292 | # Format: [override.(velocity.toml server name).discord] or [override.(velocity.toml server name).minecraft]
293 | [override.server_b.discord]
294 | channel = "0000000000000000000"
295 |
296 | [override.server_b.discord.chat.message]
297 | channel = "0000000000000000000"
298 |
299 | [override.server_b.discord.chat.message.webhook]
300 | url = "https://discord.com/api/webhooks/0000000000000000000/test"
301 | username = "{username}"
302 | avatar_url = "https://visage.surgeplay.com/face/96/{uuid}"
303 |
304 | [override.server_b.discord.chat.join]
305 | channel = "0000000000000000000"
306 |
307 | [override.server_b.discord.chat.leave]
308 | channel = "0000000000000000000"
309 |
310 | [override.server_b.discord.chat.disconnect]
311 | channel = "0000000000000000000"
312 |
313 | [override.server_b.discord.chat.server_switch]
314 | channel = "0000000000000000000"
315 |
316 | [override.server_b.discord.chat.death]
317 | # channel = "0000000000000000000"
318 |
--------------------------------------------------------------------------------
/test.nu:
--------------------------------------------------------------------------------
1 | open .env | from toml | load-env
2 |
3 | if $env.TEST_SERVER_DIR == '' {
4 | print 'TEST_SERVER_DIR is not set'
5 | exit 1
6 | }
7 |
8 | # ./gradlew.bat build
9 |
10 | let dest_dir = ($env.TEST_SERVER_DIR | path join 'plugins')
11 |
12 | let old_jars_paths = (ls $dest_dir | get name | where $it =~ '(?i)velocitydiscord-.*\.jar')
13 |
14 | $old_jars_paths | each { rm $in }
15 |
16 | let new_jar_path = (ls 'build/libs' | sort-by modified | last | get name)
17 | let new_jar_name = ($new_jar_path | path split | last)
18 |
19 | let dest_jar_path = $'($dest_dir)/($new_jar_name)'
20 |
21 | print $'($new_jar_path) -> ($dest_jar_path)'
22 |
23 | cp $new_jar_path $dest_jar_path
24 |
--------------------------------------------------------------------------------