From 82c990806cc52641a5b545a87dd7600baea5d30d Mon Sep 17 00:00:00 2001 From: danthe1st Date: Sat, 7 Nov 2026 17:03:00 +0100 Subject: [PATCH 1/2] Allow self-assigning role colors --- .../data/config/guild/ModerationConfig.java | 5 + .../color_roles/ChangeColorCommand.java | 100 ++++++++++++++++++ .../systems/color_roles/ColorRole.java | 7 ++ .../systems/color_roles/ColorRoleService.java | 89 ++++++++++++++++ .../color_roles/dao/ColorRoleRepository.java | 62 +++++++++++ .../2026-11-07_create_color_roles.sql | 11 ++ src/main/resources/database/schema.sql | 12 +++ 7 files changed, 286 insertions(+) create mode 100644 src/main/java/net/discordjug/javabot/systems/color_roles/ChangeColorCommand.java create mode 100644 src/main/java/net/discordjug/javabot/systems/color_roles/ColorRole.java create mode 100644 src/main/java/net/discordjug/javabot/systems/color_roles/ColorRoleService.java create mode 100644 src/main/java/net/discordjug/javabot/systems/color_roles/dao/ColorRoleRepository.java create mode 100644 src/main/resources/database/migrations/2026-11-07_create_color_roles.sql diff --git a/src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java b/src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java index 37c78c8d5..fa154d6ea 100644 --- a/src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java +++ b/src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java @@ -27,6 +27,7 @@ public class ModerationConfig extends GuildConfigItem { private long staffRoleId = 0; private long adminRoleId = 0; private long expertRoleId = 0; + private long colorRoleChangeRoleId = 0; /** * The time window, in seconds, that the cross-channel spam automod looks back over when @@ -180,6 +181,10 @@ public Role getAdminRole() { public Role getExpertRole() { return this.getGuild().getRoleById(this.expertRoleId); } + + public Role getColorRoleChangeRole() { + return this.getGuild().getRoleById(this.colorRoleChangeRoleId); + } public TextChannel getNotificationThreadChannel() { return this.getGuild().getTextChannelById(this.notificationThreadChannelId); diff --git a/src/main/java/net/discordjug/javabot/systems/color_roles/ChangeColorCommand.java b/src/main/java/net/discordjug/javabot/systems/color_roles/ChangeColorCommand.java new file mode 100644 index 000000000..22b53e332 --- /dev/null +++ b/src/main/java/net/discordjug/javabot/systems/color_roles/ChangeColorCommand.java @@ -0,0 +1,100 @@ +package net.discordjug.javabot.systems.color_roles; + +import java.awt.Color; +import java.util.List; +import java.util.regex.Pattern; + +import net.discordjug.javabot.data.config.BotConfig; +import net.discordjug.javabot.util.Responses; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.AutoCompleteQuery; +import net.dv8tion.jda.api.interactions.commands.Command.Choice; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import xyz.dynxsty.dih4jda.interactions.AutoCompletable; +import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand; +import xyz.dynxsty.dih4jda.util.AutoCompleteUtils; + +public class ChangeColorCommand extends SlashCommand implements AutoCompletable { + + private static final Pattern COLOR_PATTERN = Pattern.compile("#[A-F0-9]{6}", Pattern.CASE_INSENSITIVE); + + private final BotConfig config; + private final ColorRoleService service; + + public ChangeColorCommand(BotConfig botConfig, ColorRoleService service) { + this.config = botConfig; + this.service = service; + setCommandData(Commands.slash("color", "Sets the color of your roles") + .addOption(OptionType.STRING, "color", "The hex code of the role color", false, true)); + } + + @Override + public void execute(SlashCommandInteractionEvent event) { + Role baseRole = config.get(event.getGuild()).getModerationConfig().getColorRoleChangeRole(); + if (baseRole == null) { + Responses.error(event, "This feature is not available.").queue(); + return; + } + if (!event.getMember().getRoles().contains(baseRole)) { + Responses.error(event, "Only users with the " + baseRole.getAsMention() + " role can change their color.") + .setAllowedMentions(List.of()) + .queue(); + return; + } + + String colorArgument = event.getOption("color", "", OptionMapping::getAsString); + + Color color; + if (colorArgument.isEmpty()) { + color = null; + } else { + if (!colorArgument.startsWith("#")) { + colorArgument = "#" + colorArgument; + } + if (!COLOR_PATTERN.matcher(colorArgument).matches()) { + Responses.error(event, "'%s' is not a valid color", colorArgument).queue(); + return; + } + try { + color = Color.decode(colorArgument); + } catch (NumberFormatException e) { + Responses.error(event, "'%s' is not a valid color", colorArgument).queue(); + return; + } + } + + boolean removed = service.removeColor(event.getMember()); + if (color == null) { + if (removed) { + Responses.success(event, "Color removed", "Your color role has been removed.").queue(); + } else { + Responses.error(event, "You don't have a color role.").queue(); + } + return; + } + service.addColor(event.getMember(), color); + Responses.success(event, "Color set", "Your color has been set to `%s`.", service.encodeColor(color)).queue(); + } + + @Override + public void handleAutoComplete(CommandAutoCompleteInteractionEvent event, AutoCompleteQuery target) { + Role baseRole = config.get(event.getGuild()).getModerationConfig().getColorRoleChangeRole(); + if (baseRole == null) { + event.replyChoice("Feature unavailable", "This feature is not available.").queue(); + return; + } + if (!event.getMember().getRoles().contains(baseRole)) { + event.replyChoice("Not permitted", "You cannot use this feature.").queue(); + return; + } + List choices = service.getColorRoles(event.getGuild()) + .stream() + .map(rc -> new Choice(service.encodeColor(rc.color()), service.encodeColor(rc.color()))) + .toList(); + event.replyChoices(AutoCompleteUtils.filterChoices(event, choices)).queue(); + } +} diff --git a/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRole.java b/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRole.java new file mode 100644 index 000000000..0da5183c4 --- /dev/null +++ b/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRole.java @@ -0,0 +1,7 @@ +package net.discordjug.javabot.systems.color_roles; + +import java.awt.Color; + +public record ColorRole(long guildId, long roleId, Color color) { + +} \ No newline at end of file diff --git a/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRoleService.java b/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRoleService.java new file mode 100644 index 000000000..e7e718fe6 --- /dev/null +++ b/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRoleService.java @@ -0,0 +1,89 @@ +package net.discordjug.javabot.systems.color_roles; + +import java.awt.Color; +import java.util.List; + +import net.discordjug.javabot.data.config.BotConfig; +import net.discordjug.javabot.systems.color_roles.dao.ColorRoleRepository; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent; +import net.dv8tion.jda.api.events.guild.member.GuildMemberUpdateEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.springframework.stereotype.Service; + +@Service +public class ColorRoleService extends ListenerAdapter { + + private final ColorRoleRepository repo; + private final BotConfig config; + + public ColorRoleService(ColorRoleRepository repo, BotConfig config) { + this.repo = repo; + this.config = config; + } + + public synchronized void addColor(Member member, Color color) { + repo.getFromColor(member.getGuild().getIdLong(), color) + .ifPresentOrElse(colorRole -> { + member.getGuild().addRoleToMember(member, member.getGuild().getRoleById(colorRole.roleId())).queue(); + repo.addRoleToMember(colorRole.roleId(), member.getIdLong()); + }, () -> { + member.getGuild().createRole() + .setName(encodeColor(color)) + .setColor(color) + .queue(role -> { + member.getGuild().addRoleToMember(member, role).queue(); + repo.addColorRole(new ColorRole(member.getGuild().getIdLong(), role.getIdLong(), color), member.getIdLong()); + member.getGuild().modifyRolePositions() + .selectPosition(role) + .moveAbove(config.get(member.getGuild()).getModerationConfig().getColorRoleChangeRole()) + .queue(); + }); + }); + } + + public synchronized boolean removeColor(Member member) { + boolean changed = false; + for (ColorRole color : repo.getColorRolesOfMember(member.getGuild().getIdLong(), member.getIdLong())) { + repo.removeRoleFromMember(color.roleId(), member.getIdLong()); + Role role = member.getGuild().getRoleById(color.roleId()); + if (repo.cleanRoleIfNoMembers(color.roleId())) { + role.delete().queue(); + } else { + member.getGuild().removeRoleFromMember(member, role).queue(); + } + changed = true; + } + return changed; + } + + @Override + public void onGuildMemberUpdate(GuildMemberUpdateEvent event) { + // Remove color roles if a member if they lost the relevant role + // since members are not cached, we cannot use specific event types + if (!event.getMember().getUnsortedRoles().contains(config.get(event.getGuild()).getModerationConfig().getColorRoleChangeRole())) { + removeColor(event.getMember()); + } + } + + @Override + public synchronized void onGuildMemberRemove(GuildMemberRemoveEvent event) { + for (ColorRole color : repo.getColorRolesOfMember(event.getGuild().getIdLong(), event.getUser().getIdLong())) { + repo.removeRoleFromMember(color.roleId(), event.getUser().getIdLong()); + Role role = event.getGuild().getRoleById(color.roleId()); + if (repo.cleanRoleIfNoMembers(color.roleId())) { + role.delete().queue(); + } + } + } + + public List getColorRoles(Guild guild) { + return repo.getColorRoles(guild.getIdLong()); + } + + public String encodeColor(Color color) { + return String.format("#%2h%2h%2h", color.getRed(), color.getGreen(), color.getBlue()).replace(' ', '0'); + } +} diff --git a/src/main/java/net/discordjug/javabot/systems/color_roles/dao/ColorRoleRepository.java b/src/main/java/net/discordjug/javabot/systems/color_roles/dao/ColorRoleRepository.java new file mode 100644 index 000000000..2271d81f0 --- /dev/null +++ b/src/main/java/net/discordjug/javabot/systems/color_roles/dao/ColorRoleRepository.java @@ -0,0 +1,62 @@ +package net.discordjug.javabot.systems.color_roles.dao; + +import java.awt.Color; +import java.util.List; +import java.util.Optional; + +import lombok.RequiredArgsConstructor; +import net.discordjug.javabot.systems.color_roles.ColorRole; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class ColorRoleRepository { + private final JdbcTemplate jdbcTemplate; + + public List getColorRoles(long guildId) { + return jdbcTemplate.query("SELECT ROLE_ID, COLOR FROM COLOR_ROLES WHERE GUILD_ID=?", + (rs, _) -> new ColorRole(guildId, rs.getLong(1), new Color(rs.getInt(2))), + guildId); + } + + public Optional getFromColor(long guildId, Color color) { + try { + ColorRole result = jdbcTemplate.queryForObject("SELECT ROLE_ID FROM COLOR_ROLES WHERE GUILD_ID=? AND COLOR=?", + ((rs, _) -> new ColorRole(guildId, rs.getLong(1), color)), + guildId, color.getRGB()); + return Optional.ofNullable(result); + } catch(EmptyResultDataAccessException _) { + return Optional.empty(); + } + } + + public List getColorRolesOfMember(long guildId, long memberId) { + return jdbcTemplate.query(""" + SELECT cr.ROLE_ID, cr.COLOR + FROM COLOR_ROLES cr + INNER JOIN COLOR_ROLE_MEMBERS crm ON cr.ROLE_ID = crm.ROLE_ID + WHERE cr.GUILD_ID=? AND crm.USER_ID=? + """, + (rs, _) -> new ColorRole(guildId, rs.getLong(1), new Color(rs.getInt(2))), + guildId, memberId); + } + + public void addColorRole(ColorRole color, long memberId) { + jdbcTemplate.update("INSERT INTO COLOR_ROLES (ROLE_ID, GUILD_ID, COLOR) VALUES (?,?,?)", + color.roleId(), color.guildId(), color.color().getRGB()); + jdbcTemplate.update("INSERT INTO COLOR_ROLE_MEMBERS (ROLE_ID, USER_ID) VALUES (?, ?)", color.roleId(), memberId); + } + + public void addRoleToMember(long roleId, long memberId) { + jdbcTemplate.update("INSERT INTO COLOR_ROLE_MEMBERS (ROLE_ID, USER_ID) VALUES (?,?)", roleId, memberId); + } + public void removeRoleFromMember(long roleId, long memberId) { + jdbcTemplate.update("DELETE FROM COLOR_ROLE_MEMBERS WHERE ROLE_ID=? AND USER_ID=?", roleId, memberId); + } + + public boolean cleanRoleIfNoMembers(long roleId) { + return jdbcTemplate.update("DELETE FROM COLOR_ROLES WHERE ROLE_ID=? AND (SELECT COUNT(*) FROM COLOR_ROLE_MEMBERS WHERE ROLE_ID=?)=0", roleId, roleId) > 0; + } +} diff --git a/src/main/resources/database/migrations/2026-11-07_create_color_roles.sql b/src/main/resources/database/migrations/2026-11-07_create_color_roles.sql new file mode 100644 index 000000000..f7bea543e --- /dev/null +++ b/src/main/resources/database/migrations/2026-11-07_create_color_roles.sql @@ -0,0 +1,11 @@ +CREATE TABLE COLOR_ROLES ( + ROLE_ID BIGINT NOT NULL PRIMARY KEY, + GUILD_ID BIGINT NOT NULL, + COLOR INTEGER NOT NULL UNIQUE +); +CREATE TABLE COLOR_ROLE_MEMBERS ( + ROLE_ID BIGINT NOT NULL, + USER_ID BIGINT NOT NULL, + PRIMARY KEY(ROLE_ID, USER_ID), + FOREIGN KEY(ROLE_ID) REFERENCES COLOR_ROLES(ROLE_ID) +); \ No newline at end of file diff --git a/src/main/resources/database/schema.sql b/src/main/resources/database/schema.sql index 725abcc9f..c2ddca5a0 100644 --- a/src/main/resources/database/schema.sql +++ b/src/main/resources/database/schema.sql @@ -165,3 +165,15 @@ CREATE TABLE form_submissions ( ); CREATE INDEX FORM_SUBMISSIONS_USER_ID_IDX ON form_submissions (user_id,form_id); + +CREATE TABLE COLOR_ROLES ( + ROLE_ID BIGINT NOT NULL PRIMARY KEY, + GUILD_ID BIGINT NOT NULL, + COLOR INTEGER NOT NULL UNIQUE +); +CREATE TABLE COLOR_ROLE_MEMBERS ( + ROLE_ID BIGINT NOT NULL, + USER_ID BIGINT NOT NULL, + PRIMARY KEY(ROLE_ID, USER_ID), + FOREIGN KEY(ROLE_ID) REFERENCES COLOR_ROLES(ROLE_ID) +); \ No newline at end of file From ed2ce98c87c2a53da43777f2a0ee721889c2f745 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Sat, 7 Nov 2026 17:33:09 +0100 Subject: [PATCH 2/2] add Javadoc comments --- .../color_roles/ChangeColorCommand.java | 10 +++- .../systems/color_roles/ColorRole.java | 13 ++++- .../systems/color_roles/ColorRoleService.java | 19 +++++++ .../color_roles/dao/ColorRoleRepository.java | 51 ++++++++++++++++++- 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/discordjug/javabot/systems/color_roles/ChangeColorCommand.java b/src/main/java/net/discordjug/javabot/systems/color_roles/ChangeColorCommand.java index 22b53e332..0a2418408 100644 --- a/src/main/java/net/discordjug/javabot/systems/color_roles/ChangeColorCommand.java +++ b/src/main/java/net/discordjug/javabot/systems/color_roles/ChangeColorCommand.java @@ -18,6 +18,9 @@ import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand; import xyz.dynxsty.dih4jda.util.AutoCompleteUtils; +/** + * The {@code /color} command which allows users to change their own color with color roles. + */ public class ChangeColorCommand extends SlashCommand implements AutoCompletable { private static final Pattern COLOR_PATTERN = Pattern.compile("#[A-F0-9]{6}", Pattern.CASE_INSENSITIVE); @@ -25,11 +28,16 @@ public class ChangeColorCommand extends SlashCommand implements AutoCompletable private final BotConfig config; private final ColorRoleService service; + /** + * The constructor which sets the corresponding {@link net.dv8tion.jda.api.interactions.commands.build.SlashCommandData}. + * @param botConfig The bot's configuration + * @param service The {@link ColorRoleService} to actually change color roles + */ public ChangeColorCommand(BotConfig botConfig, ColorRoleService service) { this.config = botConfig; this.service = service; setCommandData(Commands.slash("color", "Sets the color of your roles") - .addOption(OptionType.STRING, "color", "The hex code of the role color", false, true)); + .addOption(OptionType.STRING, "color", "The hex code of the role color (leave empty to remove your color roles)", false, true)); } @Override diff --git a/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRole.java b/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRole.java index 0da5183c4..0d1a83975 100644 --- a/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRole.java +++ b/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRole.java @@ -1,7 +1,18 @@ package net.discordjug.javabot.systems.color_roles; import java.awt.Color; +import java.util.Objects; +/** + * A self-assignable color role. + * @param guildId The Discord ID of the guild the role is a part of + * @param roleId The Discord ID of the role + * @param color The color of the role + * @see ChangeColorCommand + * @see ColorRoleService + */ public record ColorRole(long guildId, long roleId, Color color) { - + public ColorRole { + Objects.requireNonNull(color); + } } \ No newline at end of file diff --git a/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRoleService.java b/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRoleService.java index e7e718fe6..dd0a56f3a 100644 --- a/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRoleService.java +++ b/src/main/java/net/discordjug/javabot/systems/color_roles/ColorRoleService.java @@ -13,6 +13,11 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.springframework.stereotype.Service; +/** + * Service class for assigning and removing color roles. + * @see ColorRoleService#addColor(Member, Color) + * @see ColorRoleService#removeColor(Member) + */ @Service public class ColorRoleService extends ListenerAdapter { @@ -24,6 +29,13 @@ public ColorRoleService(ColorRoleRepository repo, BotConfig config) { this.config = config; } + /** + * Adds a color role to a user. + * + * If a color role with the given color exists already, it will be reused. Otherwise, it will create a new one. + * @param member The member to add the role to + * @param color The color of the role to assign + */ public synchronized void addColor(Member member, Color color) { repo.getFromColor(member.getGuild().getIdLong(), color) .ifPresentOrElse(colorRole -> { @@ -44,6 +56,13 @@ public synchronized void addColor(Member member, Color color) { }); } + /** + * Removes all color roles from a user. + * + * If no other members have that role, the role will be deleted. + * @param member The member to remove the role from + * @return {@code true} if any color roles were removed, else {@code false} + */ public synchronized boolean removeColor(Member member) { boolean changed = false; for (ColorRole color : repo.getColorRolesOfMember(member.getGuild().getIdLong(), member.getIdLong())) { diff --git a/src/main/java/net/discordjug/javabot/systems/color_roles/dao/ColorRoleRepository.java b/src/main/java/net/discordjug/javabot/systems/color_roles/dao/ColorRoleRepository.java index 2271d81f0..a003b1c1e 100644 --- a/src/main/java/net/discordjug/javabot/systems/color_roles/dao/ColorRoleRepository.java +++ b/src/main/java/net/discordjug/javabot/systems/color_roles/dao/ColorRoleRepository.java @@ -4,27 +4,43 @@ import java.util.List; import java.util.Optional; +import javax.annotation.CheckReturnValue; + import lombok.RequiredArgsConstructor; import net.discordjug.javabot.systems.color_roles.ColorRole; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; +/** + * Repository class for the COLOR_ROLES and COLOR_ROLE_MEMBERS tables which are used for role colors. + */ @Repository @RequiredArgsConstructor public class ColorRoleRepository { private final JdbcTemplate jdbcTemplate; + /** + * Gets all color roles of a guild. + * @param guildId The ID of the guild + * @return A {@link List} of all registered role colors in the guild + */ public List getColorRoles(long guildId) { return jdbcTemplate.query("SELECT ROLE_ID, COLOR FROM COLOR_ROLES WHERE GUILD_ID=?", (rs, _) -> new ColorRole(guildId, rs.getLong(1), new Color(rs.getInt(2))), guildId); } + /** + * Tries to find a color role given the color. + * @param guildId The ID of the guild containing the role + * @param color The color of the role to search for + * @return The color role if it exists or {@link Optional#empty()} + */ public Optional getFromColor(long guildId, Color color) { try { ColorRole result = jdbcTemplate.queryForObject("SELECT ROLE_ID FROM COLOR_ROLES WHERE GUILD_ID=? AND COLOR=?", - ((rs, _) -> new ColorRole(guildId, rs.getLong(1), color)), + (rs, _) -> new ColorRole(guildId, rs.getLong(1), color), guildId, color.getRGB()); return Optional.ofNullable(result); } catch(EmptyResultDataAccessException _) { @@ -32,6 +48,12 @@ public Optional getFromColor(long guildId, Color color) { } } + /** + * Gets all color roles of a given member. + * @param guildId The ID of the guild the roles should be part of + * @param memberId The ID of the member get the roles of + * @return All color roles of the given member in the given guild + */ public List getColorRolesOfMember(long guildId, long memberId) { return jdbcTemplate.query(""" SELECT cr.ROLE_ID, cr.COLOR @@ -43,19 +65,44 @@ public List getColorRolesOfMember(long guildId, long memberId) { guildId, memberId); } + /** + * Registers that a member has a given color role. + * @param color The color role + * @param memberId The member the role is being added to + */ public void addColorRole(ColorRole color, long memberId) { jdbcTemplate.update("INSERT INTO COLOR_ROLES (ROLE_ID, GUILD_ID, COLOR) VALUES (?,?,?)", color.roleId(), color.guildId(), color.color().getRGB()); - jdbcTemplate.update("INSERT INTO COLOR_ROLE_MEMBERS (ROLE_ID, USER_ID) VALUES (?, ?)", color.roleId(), memberId); + addRoleToMember(color.roleId(), memberId); } + /** + * Registers that a color role has been added to a member. + * @param roleId The ID of the role the member has + * @param memberId The ID of the member to update + */ public void addRoleToMember(long roleId, long memberId) { jdbcTemplate.update("INSERT INTO COLOR_ROLE_MEMBERS (ROLE_ID, USER_ID) VALUES (?,?)", roleId, memberId); } + + /** + * Registers that a color role has been added to a member. + * @param roleId The ID of the role removed from the member + * @param memberId The ID of the member to update + * @see #cleanRoleIfNoMembers(long) + */ public void removeRoleFromMember(long roleId, long memberId) { jdbcTemplate.update("DELETE FROM COLOR_ROLE_MEMBERS WHERE ROLE_ID=? AND USER_ID=?", roleId, memberId); } + /** + * Removes a role from the database if it has no members. + * + * Callers should make sure to actually delete the role if this method returns {@code true}. + * @param roleId The ID of the role to possibly remove + * @return {@code true} if the role is registered as removed due to not having any members, else {@code false} + */ + @CheckReturnValue public boolean cleanRoleIfNoMembers(long roleId) { return jdbcTemplate.update("DELETE FROM COLOR_ROLES WHERE ROLE_ID=? AND (SELECT COUNT(*) FROM COLOR_ROLE_MEMBERS WHERE ROLE_ID=?)=0", roleId, roleId) > 0; }