Skip to content

Commit 82c9908

Browse files
committed
Allow self-assigning role colors
1 parent 57d8dff commit 82c9908

7 files changed

Lines changed: 286 additions & 0 deletions

File tree

‎src/main/java/net/discordjug/javabot/data/config/guild/ModerationConfig.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class ModerationConfig extends GuildConfigItem {
2727
private long staffRoleId = 0;
2828
private long adminRoleId = 0;
2929
private long expertRoleId = 0;
30+
private long colorRoleChangeRoleId = 0;
3031

3132
/**
3233
* The time window, in seconds, that the cross-channel spam automod looks back over when
@@ -180,6 +181,10 @@ public Role getAdminRole() {
180181
public Role getExpertRole() {
181182
return this.getGuild().getRoleById(this.expertRoleId);
182183
}
184+
185+
public Role getColorRoleChangeRole() {
186+
return this.getGuild().getRoleById(this.colorRoleChangeRoleId);
187+
}
183188

184189
public TextChannel getNotificationThreadChannel() {
185190
return this.getGuild().getTextChannelById(this.notificationThreadChannelId);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package net.discordjug.javabot.systems.color_roles;
2+
3+
import java.awt.Color;
4+
import java.util.List;
5+
import java.util.regex.Pattern;
6+
7+
import net.discordjug.javabot.data.config.BotConfig;
8+
import net.discordjug.javabot.util.Responses;
9+
import net.dv8tion.jda.api.entities.Role;
10+
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
11+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
12+
import net.dv8tion.jda.api.interactions.AutoCompleteQuery;
13+
import net.dv8tion.jda.api.interactions.commands.Command.Choice;
14+
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
15+
import net.dv8tion.jda.api.interactions.commands.OptionType;
16+
import net.dv8tion.jda.api.interactions.commands.build.Commands;
17+
import xyz.dynxsty.dih4jda.interactions.AutoCompletable;
18+
import xyz.dynxsty.dih4jda.interactions.commands.application.SlashCommand;
19+
import xyz.dynxsty.dih4jda.util.AutoCompleteUtils;
20+
21+
public class ChangeColorCommand extends SlashCommand implements AutoCompletable {
22+
23+
private static final Pattern COLOR_PATTERN = Pattern.compile("#[A-F0-9]{6}", Pattern.CASE_INSENSITIVE);
24+
25+
private final BotConfig config;
26+
private final ColorRoleService service;
27+
28+
public ChangeColorCommand(BotConfig botConfig, ColorRoleService service) {
29+
this.config = botConfig;
30+
this.service = service;
31+
setCommandData(Commands.slash("color", "Sets the color of your roles")
32+
.addOption(OptionType.STRING, "color", "The hex code of the role color", false, true));
33+
}
34+
35+
@Override
36+
public void execute(SlashCommandInteractionEvent event) {
37+
Role baseRole = config.get(event.getGuild()).getModerationConfig().getColorRoleChangeRole();
38+
if (baseRole == null) {
39+
Responses.error(event, "This feature is not available.").queue();
40+
return;
41+
}
42+
if (!event.getMember().getRoles().contains(baseRole)) {
43+
Responses.error(event, "Only users with the " + baseRole.getAsMention() + " role can change their color.")
44+
.setAllowedMentions(List.of())
45+
.queue();
46+
return;
47+
}
48+
49+
String colorArgument = event.getOption("color", "", OptionMapping::getAsString);
50+
51+
Color color;
52+
if (colorArgument.isEmpty()) {
53+
color = null;
54+
} else {
55+
if (!colorArgument.startsWith("#")) {
56+
colorArgument = "#" + colorArgument;
57+
}
58+
if (!COLOR_PATTERN.matcher(colorArgument).matches()) {
59+
Responses.error(event, "'%s' is not a valid color", colorArgument).queue();
60+
return;
61+
}
62+
try {
63+
color = Color.decode(colorArgument);
64+
} catch (NumberFormatException e) {
65+
Responses.error(event, "'%s' is not a valid color", colorArgument).queue();
66+
return;
67+
}
68+
}
69+
70+
boolean removed = service.removeColor(event.getMember());
71+
if (color == null) {
72+
if (removed) {
73+
Responses.success(event, "Color removed", "Your color role has been removed.").queue();
74+
} else {
75+
Responses.error(event, "You don't have a color role.").queue();
76+
}
77+
return;
78+
}
79+
service.addColor(event.getMember(), color);
80+
Responses.success(event, "Color set", "Your color has been set to `%s`.", service.encodeColor(color)).queue();
81+
}
82+
83+
@Override
84+
public void handleAutoComplete(CommandAutoCompleteInteractionEvent event, AutoCompleteQuery target) {
85+
Role baseRole = config.get(event.getGuild()).getModerationConfig().getColorRoleChangeRole();
86+
if (baseRole == null) {
87+
event.replyChoice("Feature unavailable", "This feature is not available.").queue();
88+
return;
89+
}
90+
if (!event.getMember().getRoles().contains(baseRole)) {
91+
event.replyChoice("Not permitted", "You cannot use this feature.").queue();
92+
return;
93+
}
94+
List<Choice> choices = service.getColorRoles(event.getGuild())
95+
.stream()
96+
.map(rc -> new Choice(service.encodeColor(rc.color()), service.encodeColor(rc.color())))
97+
.toList();
98+
event.replyChoices(AutoCompleteUtils.filterChoices(event, choices)).queue();
99+
}
100+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.discordjug.javabot.systems.color_roles;
2+
3+
import java.awt.Color;
4+
5+
public record ColorRole(long guildId, long roleId, Color color) {
6+
7+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package net.discordjug.javabot.systems.color_roles;
2+
3+
import java.awt.Color;
4+
import java.util.List;
5+
6+
import net.discordjug.javabot.data.config.BotConfig;
7+
import net.discordjug.javabot.systems.color_roles.dao.ColorRoleRepository;
8+
import net.dv8tion.jda.api.entities.Guild;
9+
import net.dv8tion.jda.api.entities.Member;
10+
import net.dv8tion.jda.api.entities.Role;
11+
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
12+
import net.dv8tion.jda.api.events.guild.member.GuildMemberUpdateEvent;
13+
import net.dv8tion.jda.api.hooks.ListenerAdapter;
14+
import org.springframework.stereotype.Service;
15+
16+
@Service
17+
public class ColorRoleService extends ListenerAdapter {
18+
19+
private final ColorRoleRepository repo;
20+
private final BotConfig config;
21+
22+
public ColorRoleService(ColorRoleRepository repo, BotConfig config) {
23+
this.repo = repo;
24+
this.config = config;
25+
}
26+
27+
public synchronized void addColor(Member member, Color color) {
28+
repo.getFromColor(member.getGuild().getIdLong(), color)
29+
.ifPresentOrElse(colorRole -> {
30+
member.getGuild().addRoleToMember(member, member.getGuild().getRoleById(colorRole.roleId())).queue();
31+
repo.addRoleToMember(colorRole.roleId(), member.getIdLong());
32+
}, () -> {
33+
member.getGuild().createRole()
34+
.setName(encodeColor(color))
35+
.setColor(color)
36+
.queue(role -> {
37+
member.getGuild().addRoleToMember(member, role).queue();
38+
repo.addColorRole(new ColorRole(member.getGuild().getIdLong(), role.getIdLong(), color), member.getIdLong());
39+
member.getGuild().modifyRolePositions()
40+
.selectPosition(role)
41+
.moveAbove(config.get(member.getGuild()).getModerationConfig().getColorRoleChangeRole())
42+
.queue();
43+
});
44+
});
45+
}
46+
47+
public synchronized boolean removeColor(Member member) {
48+
boolean changed = false;
49+
for (ColorRole color : repo.getColorRolesOfMember(member.getGuild().getIdLong(), member.getIdLong())) {
50+
repo.removeRoleFromMember(color.roleId(), member.getIdLong());
51+
Role role = member.getGuild().getRoleById(color.roleId());
52+
if (repo.cleanRoleIfNoMembers(color.roleId())) {
53+
role.delete().queue();
54+
} else {
55+
member.getGuild().removeRoleFromMember(member, role).queue();
56+
}
57+
changed = true;
58+
}
59+
return changed;
60+
}
61+
62+
@Override
63+
public void onGuildMemberUpdate(GuildMemberUpdateEvent event) {
64+
// Remove color roles if a member if they lost the relevant role
65+
// since members are not cached, we cannot use specific event types
66+
if (!event.getMember().getUnsortedRoles().contains(config.get(event.getGuild()).getModerationConfig().getColorRoleChangeRole())) {
67+
removeColor(event.getMember());
68+
}
69+
}
70+
71+
@Override
72+
public synchronized void onGuildMemberRemove(GuildMemberRemoveEvent event) {
73+
for (ColorRole color : repo.getColorRolesOfMember(event.getGuild().getIdLong(), event.getUser().getIdLong())) {
74+
repo.removeRoleFromMember(color.roleId(), event.getUser().getIdLong());
75+
Role role = event.getGuild().getRoleById(color.roleId());
76+
if (repo.cleanRoleIfNoMembers(color.roleId())) {
77+
role.delete().queue();
78+
}
79+
}
80+
}
81+
82+
public List<ColorRole> getColorRoles(Guild guild) {
83+
return repo.getColorRoles(guild.getIdLong());
84+
}
85+
86+
public String encodeColor(Color color) {
87+
return String.format("#%2h%2h%2h", color.getRed(), color.getGreen(), color.getBlue()).replace(' ', '0');
88+
}
89+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package net.discordjug.javabot.systems.color_roles.dao;
2+
3+
import java.awt.Color;
4+
import java.util.List;
5+
import java.util.Optional;
6+
7+
import lombok.RequiredArgsConstructor;
8+
import net.discordjug.javabot.systems.color_roles.ColorRole;
9+
import org.springframework.dao.EmptyResultDataAccessException;
10+
import org.springframework.jdbc.core.JdbcTemplate;
11+
import org.springframework.stereotype.Repository;
12+
13+
@Repository
14+
@RequiredArgsConstructor
15+
public class ColorRoleRepository {
16+
private final JdbcTemplate jdbcTemplate;
17+
18+
public List<ColorRole> getColorRoles(long guildId) {
19+
return jdbcTemplate.query("SELECT ROLE_ID, COLOR FROM COLOR_ROLES WHERE GUILD_ID=?",
20+
(rs, _) -> new ColorRole(guildId, rs.getLong(1), new Color(rs.getInt(2))),
21+
guildId);
22+
}
23+
24+
public Optional<ColorRole> getFromColor(long guildId, Color color) {
25+
try {
26+
ColorRole result = jdbcTemplate.queryForObject("SELECT ROLE_ID FROM COLOR_ROLES WHERE GUILD_ID=? AND COLOR=?",
27+
((rs, _) -> new ColorRole(guildId, rs.getLong(1), color)),
28+
guildId, color.getRGB());
29+
return Optional.ofNullable(result);
30+
} catch(EmptyResultDataAccessException _) {
31+
return Optional.empty();
32+
}
33+
}
34+
35+
public List<ColorRole> getColorRolesOfMember(long guildId, long memberId) {
36+
return jdbcTemplate.query("""
37+
SELECT cr.ROLE_ID, cr.COLOR
38+
FROM COLOR_ROLES cr
39+
INNER JOIN COLOR_ROLE_MEMBERS crm ON cr.ROLE_ID = crm.ROLE_ID
40+
WHERE cr.GUILD_ID=? AND crm.USER_ID=?
41+
""",
42+
(rs, _) -> new ColorRole(guildId, rs.getLong(1), new Color(rs.getInt(2))),
43+
guildId, memberId);
44+
}
45+
46+
public void addColorRole(ColorRole color, long memberId) {
47+
jdbcTemplate.update("INSERT INTO COLOR_ROLES (ROLE_ID, GUILD_ID, COLOR) VALUES (?,?,?)",
48+
color.roleId(), color.guildId(), color.color().getRGB());
49+
jdbcTemplate.update("INSERT INTO COLOR_ROLE_MEMBERS (ROLE_ID, USER_ID) VALUES (?, ?)", color.roleId(), memberId);
50+
}
51+
52+
public void addRoleToMember(long roleId, long memberId) {
53+
jdbcTemplate.update("INSERT INTO COLOR_ROLE_MEMBERS (ROLE_ID, USER_ID) VALUES (?,?)", roleId, memberId);
54+
}
55+
public void removeRoleFromMember(long roleId, long memberId) {
56+
jdbcTemplate.update("DELETE FROM COLOR_ROLE_MEMBERS WHERE ROLE_ID=? AND USER_ID=?", roleId, memberId);
57+
}
58+
59+
public boolean cleanRoleIfNoMembers(long roleId) {
60+
return jdbcTemplate.update("DELETE FROM COLOR_ROLES WHERE ROLE_ID=? AND (SELECT COUNT(*) FROM COLOR_ROLE_MEMBERS WHERE ROLE_ID=?)=0", roleId, roleId) > 0;
61+
}
62+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
CREATE TABLE COLOR_ROLES (
2+
ROLE_ID BIGINT NOT NULL PRIMARY KEY,
3+
GUILD_ID BIGINT NOT NULL,
4+
COLOR INTEGER NOT NULL UNIQUE
5+
);
6+
CREATE TABLE COLOR_ROLE_MEMBERS (
7+
ROLE_ID BIGINT NOT NULL,
8+
USER_ID BIGINT NOT NULL,
9+
PRIMARY KEY(ROLE_ID, USER_ID),
10+
FOREIGN KEY(ROLE_ID) REFERENCES COLOR_ROLES(ROLE_ID)
11+
);

‎src/main/resources/database/schema.sql‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,15 @@ CREATE TABLE form_submissions (
165165
);
166166

167167
CREATE INDEX FORM_SUBMISSIONS_USER_ID_IDX ON form_submissions (user_id,form_id);
168+
169+
CREATE TABLE COLOR_ROLES (
170+
ROLE_ID BIGINT NOT NULL PRIMARY KEY,
171+
GUILD_ID BIGINT NOT NULL,
172+
COLOR INTEGER NOT NULL UNIQUE
173+
);
174+
CREATE TABLE COLOR_ROLE_MEMBERS (
175+
ROLE_ID BIGINT NOT NULL,
176+
USER_ID BIGINT NOT NULL,
177+
PRIMARY KEY(ROLE_ID, USER_ID),
178+
FOREIGN KEY(ROLE_ID) REFERENCES COLOR_ROLES(ROLE_ID)
179+
);

0 commit comments

Comments
 (0)