Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ FROM docker.io/eclipse-temurin:25

COPY target/Nameless-Link.jar /app.jar

ENV WEBSERVER_PORT 80
ENV WEBSERVER_BIND 0.0.0.0
ENV JAVA_TOOL_OPTIONS -Xmx256M
ENV WEBSERVER_PORT=80
ENV WEBSERVER_BIND=0.0.0.0
ENV JAVA_TOOL_OPTIONS=-Xmx256M

CMD ["java", "-jar", "/app.jar"]
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ The "Nameless Link" Discord bot synchronizes user roles to and from a specific D

For documentation please consult the [wiki](https://github.com/NamelessMC/Nameless-Link/wiki).

## Configuration

### Bot Activity/Presence

You can customize the bot's activity status using environment variables:

- `BOT_ACTIVITY_TYPE` - The type of activity to display. Valid options:
- `PLAYING` - Shows "Playing {message}"
- `LISTENING` - Shows "Listening to {message}"
- `WATCHING` - Shows "Watching {message}"
- `COMPETING` - Shows "Competing in {message}"
- `STREAMING` - Shows "Streaming {message}" with a purple "LIVE" indicator (requires `BOT_ACTIVITY_URL`)
- `CUSTOM` - Shows a custom status message
- `BOT_ACTIVITY_MESSAGE` - The message to display in the activity status
- `BOT_ACTIVITY_URL` - (Optional, required for `STREAMING`) The URL for the stream (e.g., Twitch/YouTube URL)

**Example (docker-compose.yaml):**
```yaml
environment:
BOT_ACTIVITY_TYPE: PLAYING
BOT_ACTIVITY_MESSAGE: with NamelessMC
```

**Example (STREAMING):**
```yaml
environment:
BOT_ACTIVITY_TYPE: STREAMING
BOT_ACTIVITY_MESSAGE: NamelessMC Development
BOT_ACTIVITY_URL: https://www.twitch.tv/your_channel
```

## Translations

<a href="https://translate.namelessmc.com/engage/namelessmc/">
Expand Down
1 change: 1 addition & 0 deletions docker-compose.prod-postgres.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ services:
POSTGRES_DB: link
POSTGRES_USER: link
POSTGRES_PASSWORD: postgres
# VERIFY_COMMAND_NAME: verify # Optional: Customize /verify command name
restart: always
1 change: 1 addition & 0 deletions docker-compose.prod-stateless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ services:
STORAGE_TYPE: stateless
API_URL: # Your NamelessMC API URL
GUILD_ID: # Your Discord guild id
# VERIFY_COMMAND_NAME: verify # Optional: Customize /verify command name
restart: always
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ services:
POSTGRES_PASSWORD: postgres
API_DEBUG: 'true'
ALLOW_LOCAL_ADDRESSES: 'true'
# VERIFY_COMMAND_NAME: verify # Optional: Customize /verify command name
11 changes: 10 additions & 1 deletion src/main/java/com/namelessmc/bot/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,17 @@ public static Guild getGuildById(final long guildId) {
case "LISTENING" -> Activity.listening(message);
case "WATCHING" -> Activity.watching(message);
case "COMPETING" -> Activity.competing(message);
case "STREAMING" -> {
String url = System.getenv("BOT_ACTIVITY_URL");
if (url == null) {
LOGGER.warn("BOT_ACTIVITY_TYPE is STREAMING but BOT_ACTIVITY_URL is not set. Using default Twitch URL.");
url = "https://www.twitch.tv/discord";
}
yield Activity.streaming(message, url);
}
case "CUSTOM" -> Activity.customStatus(message);
default -> {
LOGGER.warn("Invalid BOT_ACTIVITY_TYPE: '{}'. Valid options: PLAYING, LISTENING, WATCHING, COMPETING.", typeEnv);
LOGGER.warn("Invalid BOT_ACTIVITY_TYPE: '{}'. Valid options: PLAYING, LISTENING, WATCHING, COMPETING, STREAMING, CUSTOM.", typeEnv);
yield null;
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/namelessmc/bot/commands/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public abstract void execute(final SlashCommandInteractionEvent event,
new PingCommand(),
new RegisterCommand(),
new URLCommand(),
new VerifyCommand(),
new VerifyCommand(System.getenv("VERIFY_COMMAND_NAME") != null ? System.getenv("VERIFY_COMMAND_NAME") : "verify"),
};

private static final Map<String, Command> BY_NAME = new HashMap<>();
Expand Down
51 changes: 26 additions & 25 deletions src/main/java/com/namelessmc/bot/commands/ConfigureCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import com.namelessmc.bot.Language;
import com.namelessmc.bot.Main;
import com.namelessmc.bot.connections.BackendStorageException;
import com.namelessmc.bot.util.EmbedUtil;
import com.namelessmc.bot.connections.ConnectionCache;
import com.namelessmc.bot.listeners.DiscordRoleListener;
import com.namelessmc.java_api.NamelessAPI;
Expand Down Expand Up @@ -91,7 +92,7 @@ public CommandData getCommandData(final Language language) {
public void execute(SlashCommandInteractionEvent event, InteractionHook hook, Language language, Guild guild, @Nullable NamelessAPI api) {
Main.canModifySettings(event.getUser(), guild, canModifySettings -> {
if (!canModifySettings) {
hook.sendMessage(language.get(ERROR_NO_PERMISSION)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_NO_PERMISSION))).queue();
LOGGER.info("User {} does not have permission to modify settings", event.getUser().getIdLong());
return;
}
Expand All @@ -112,18 +113,18 @@ public void execute(SlashCommandInteractionEvent event, InteractionHook hook, La
}
private void unlink(SlashCommandInteractionEvent event, InteractionHook hook, Language language, @Nullable NamelessAPI oldApi) {
if (oldApi == null) {
hook.sendMessage(language.get(CONFIGURE_UNLINK_NOT_LINKED)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_UNLINK_NOT_LINKED))).queue();
LOGGER.info("Cannot unlink, bot was not linked");
return;
}

final long guildId = event.getGuild().getIdLong();
try {
Main.getConnectionManager().removeConnection(guildId);
hook.sendMessage(language.get(CONFIGURE_UNLINK_SUCCESS)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_UNLINK_SUCCESS))).queue();
LOGGER.info("Unlinked from guild {}", guildId);
} catch (final BackendStorageException e) {
hook.sendMessage(language.get(ERROR_GENERIC)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_GENERIC))).queue();
LOGGER.error("storage backend", e);
}
}
Expand All @@ -137,7 +138,7 @@ private void link(SlashCommandInteractionEvent event, InteractionHook hook, Lang
try {
apiUrl = new URI(apiUrlString).toURL();
} catch (final MalformedURLException | URISyntaxException e) {
hook.sendMessage(language.get(APIURL_URL_MALFORMED)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(APIURL_URL_MALFORMED))).queue();
return;
}

Expand Down Expand Up @@ -169,24 +170,24 @@ private void link(SlashCommandInteractionEvent event, InteractionHook hook, Lang
if (oldApi == null) {
// User is setting up new connection
Main.getConnectionManager().createConnection(guildId, apiUrl, apiKey);
hook.sendMessage(language.get(CONFIGURE_LINK_SUCCESS)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_LINK_SUCCESS))).queue();
LOGGER.info("Set API URL for guild {} to {}", guildId, apiUrl);
} else {
// User is modifying API URL for existing connection
Main.getConnectionManager().updateConnection(guildId, apiUrl, apiKey);
hook.sendMessage(language.get(CONFIGURE_LINK_SUCCESS)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_LINK_SUCCESS))).queue();
LOGGER.info("Updated API URL for guild {} from {} to {}", guildId, oldApi, apiUrl);
}

DiscordRoleListener.sendRolesAsync(guildId);
} catch (final NamelessException e) {
hook.sendMessage("```\n" + Ascii.truncate(e.getMessage(), 1500, "[truncated]") + "\n```").queue();
hook.sendMessage(language.get(APIURL_FAILED_CONNECTION)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(APIURL_FAILED_CONNECTION))).queue();
Main.logConnectionError(LOGGER, e);
}
} catch (final BackendStorageException e) {
if (e.getCause() instanceof UnsupportedOperationException) {
hook.sendMessage(language.get(CONFIGURE_LINK_ALREADY_CONFIGURED))
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_LINK_ALREADY_CONFIGURED)))
.setEphemeral(true)
.queue(response -> {
LOGGER.info("The bot is ALREADY configured using environment variables, please update the config via environment settings instead. Used in guild: {}", guildId);
Expand All @@ -195,20 +196,20 @@ private void link(SlashCommandInteractionEvent event, InteractionHook hook, Lang
});
return;
}
hook.sendMessage(language.get(ERROR_GENERIC)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_GENERIC))).queue();
LOGGER.error("storage backend", e);
}
}

void testConnection(SlashCommandInteractionEvent event, InteractionHook hook, Language language, @Nullable NamelessAPI api) {
if (api == null) {
hook.sendMessage(language.get(ERROR_NOT_SET_UP)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_NOT_SET_UP))).queue();
return;
}

final long ping = this.ping(api, language, event.getHook());
if (ping >= 0) {
hook.sendMessage(language.get(CONFIGURE_TEST_WORKING, "time", ping)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_TEST_WORKING, "time", ping))).queue();
}
}

Expand All @@ -219,7 +220,7 @@ private long ping(final NamelessAPI api, final Language language, final Interact
!url.getQuery().equals("route=/api/v2") && !url.getQuery().equals("route=/api/v2/")
) {
LOGGER.info("Invalid URL with protocol '{}' host '{}' path '{}' query '{}'", url.getProtocol(), url.getHost(), url.getPath(), url.getQuery());
hook.sendMessage(language.get(APIURL_URL_INVALID)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(hook.getJDA(), language.get(APIURL_URL_INVALID))).queue();
return -1;
}

Expand All @@ -232,7 +233,7 @@ private long ping(final NamelessAPI api, final Language language, final Interact
// checking 172.16.0.0/12 is too much work...
)) {
LOGGER.info("Local host: '{}'", host);
hook.sendMessage(language.get(APIURL_URL_LOCAL)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(hook.getJDA(), language.get(APIURL_URL_LOCAL))).queue();
return -1;
}

Expand All @@ -242,21 +243,21 @@ private long ping(final NamelessAPI api, final Language language, final Interact
final Website info = api.website();
try {
if (!NamelessVersion.isSupportedByJavaApi(info.parsedVersion())) {
hook.sendMessage(language.get(ERROR_WEBSITE_VERSION, "version", info.rawVersion(), "compatibleVersions", supportedVersionsList())).queue();
hook.sendMessageEmbeds(EmbedUtil.message(hook.getJDA(), language.get(ERROR_WEBSITE_VERSION, "version", info.rawVersion(), "compatibleVersions", supportedVersionsList()))).queue();
LOGGER.info("Incompatible NamelessMC version");
return -1;
}

LOGGER.info("Website connection is working");
return System.currentTimeMillis() - start;
} catch (final UnknownNamelessVersionException e) {
hook.sendMessage(language.get(ERROR_WEBSITE_VERSION, "version", info.rawVersion(), "compatibleVersions", supportedVersionsList())).queue();
hook.sendMessageEmbeds(EmbedUtil.message(hook.getJDA(), language.get(ERROR_WEBSITE_VERSION, "version", info.rawVersion(), "compatibleVersions", supportedVersionsList()))).queue();
Main.logConnectionError(LOGGER, "unknown nameless version", e);
return -1;
}
} catch (final NamelessException e) {
hook.sendMessage("```\n" + Ascii.truncate(e.getMessage(), 1500, "[truncated]") + "\n```").queue();
hook.sendMessage(language.get(APIURL_FAILED_CONNECTION)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(hook.getJDA(), language.get(APIURL_FAILED_CONNECTION))).queue();
Main.logConnectionError(LOGGER, "NamelessException during ping", e);
return -1;
}
Expand All @@ -279,32 +280,32 @@ private void changeUsernameSync(SlashCommandInteractionEvent event, InteractionH
} catch (final HierarchyException ignored) {
// This is expected, changing the nickname of the owner is never allowed.
} catch (final InsufficientPermissionException e) {
hook.sendMessage(language.get(CONFIGURE_USERNAME_SYNC_MISSING_PERMISSION)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_USERNAME_SYNC_MISSING_PERMISSION))).queue();
return;
}

try {
Main.getConnectionManager().setUsernameSyncEnabled(event.getGuild().getIdLong(), state);
} catch (final BackendStorageException e) {
hook.sendMessage(language.get(ERROR_GENERIC)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_GENERIC))).queue();
LOGGER.error("storage backend", e);
return;
}

if (state) {
hook.sendMessage(language.get(CONFIGURE_USERNAME_SYNC_ENABLED)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_USERNAME_SYNC_ENABLED))).queue();
} else {
hook.sendMessage(language.get(CONFIGURE_USERNAME_SYNC_DISABLED)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_USERNAME_SYNC_DISABLED))).queue();
}
}

private void updateUsernames(SlashCommandInteractionEvent event, InteractionHook hook, Language language, @Nullable NamelessAPI api) {
if (api == null) {
hook.sendMessage(language.get(ERROR_NOT_SET_UP)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_NOT_SET_UP))).queue();
return;
}

hook.sendMessage(language.get(CONFIGURE_UPDATE_USERNAMES_DONE)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_UPDATE_USERNAMES_DONE))).queue();

event.getGuild().loadMembers().onSuccess(members -> {
final long[] discordIds = new long[members.size()];
Expand All @@ -317,12 +318,12 @@ private void updateUsernames(SlashCommandInteractionEvent event, InteractionHook
try {
api.discord().updateDiscordUsernames(discordIds, discordUsernames);
} catch (final NamelessException e) {
hook.sendMessage(language.get(ERROR_WEBSITE_CONNECTION)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_WEBSITE_CONNECTION))).queue();
Main.logConnectionError(LOGGER, e);
return;
}
hook.setEphemeral(true); // Ephemeral needs to be set again after last message
hook.sendMessage(language.get(CONFIGURE_UPDATE_USERNAMES_DONE)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(CONFIGURE_UPDATE_USERNAMES_DONE))).queue();
});
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/namelessmc/bot/commands/PingCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.checkerframework.checker.nullness.qual.Nullable;

import com.namelessmc.bot.Language;
import com.namelessmc.bot.util.EmbedUtil;
import com.namelessmc.java_api.NamelessAPI;

import net.dv8tion.jda.api.entities.Guild;
Expand Down Expand Up @@ -32,7 +33,7 @@ public void execute(final SlashCommandInteractionEvent event,
final Language language,
final Guild guild,
final @Nullable NamelessAPI api) {
hook.sendMessage("This command has been removed, please use '/configure test' instead.").queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), "This command has been removed, please use '/configure test' instead.")).queue();
}

}
21 changes: 11 additions & 10 deletions src/main/java/com/namelessmc/bot/commands/RegisterCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import com.namelessmc.bot.Language;
import com.namelessmc.bot.Main;
import com.namelessmc.bot.util.EmbedUtil;
import com.namelessmc.java_api.NamelessAPI;
import com.namelessmc.java_api.exception.ApiException;
import com.namelessmc.java_api.exception.NamelessException;
Expand Down Expand Up @@ -62,7 +63,7 @@ public void execute(final SlashCommandInteractionEvent event,
final String discordUsername = event.getUser().getName();

if (api == null) {
hook.sendMessage(language.get(ERROR_NOT_SET_UP)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_NOT_SET_UP))).queue();
LOGGER.info("Website connection not set up");
return;
}
Expand All @@ -72,39 +73,39 @@ public void execute(final SlashCommandInteractionEvent event,
final Optional<String> verificationUrl = api.registerUser(username, email, integrationData);
if (verificationUrl.isPresent()) {
LOGGER.info("Registration successful, sending registration URL");
hook.sendMessage(language.get(REGISTER_URL, "url", verificationUrl.get())).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(REGISTER_URL, "url", verificationUrl.get()))).queue();
} else {
LOGGER.info("Registration successful, registration URL has been sent in an email");
hook.sendMessage(language.get(REGISTER_EMAIL)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(REGISTER_EMAIL))).queue();
}
} catch (final NamelessException e) {
if (e instanceof final ApiException apiException) {
switch (apiException.apiError()) {
case CORE_INVALID_USERNAME:
hook.sendMessage(language.get(ERROR_INVALID_USERNAME)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_INVALID_USERNAME))).queue();
return;
case CORE_USERNAME_ALREADY_EXISTS:
hook.sendMessage(language.get(ERROR_DUPLICATE_USERNAME)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_DUPLICATE_USERNAME))).queue();
return;
case CORE_UNABLE_TO_SEND_REGISTRATION_EMAIL:
hook.sendMessage(language.get(ERROR_SEND_VERIFICATION_EMAIL)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_SEND_VERIFICATION_EMAIL))).queue();
return;
case CORE_INVALID_EMAIL_ADDRESS:
hook.sendMessage(language.get(ERROR_INVALID_EMAIL_ADDRESS)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_INVALID_EMAIL_ADDRESS))).queue();
return;
case CORE_EMAIL_ALREADY_EXISTS:
hook.sendMessage(language.get(ERROR_DUPLICATE_EMAIL_ADDRESS)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_DUPLICATE_EMAIL_ADDRESS))).queue();
return;
case CORE_INTEGRATION_IDENTIFIER_ERROR:
case CORE_INTEGRATION_USERNAME_ERROR:
hook.sendMessage(language.get(ERROR_DUPLICATE_DISCORD_INTEGRATION)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_DUPLICATE_DISCORD_INTEGRATION))).queue();
return;
default:
// generic error message is sent below
}
}

hook.sendMessage(language.get(ERROR_WEBSITE_CONNECTION)).queue();
hook.sendMessageEmbeds(EmbedUtil.message(event.getJDA(), language.get(ERROR_WEBSITE_CONNECTION))).queue();
Main.logConnectionError(LOGGER, e);
}
}
Expand Down
Loading