Skip to content

Commit e88cb2e

Browse files
authored
Add per user feature opt-in/out (#1083)
* Update command permissions * Add per user feature opt-in/out * Fix webhook errors * Refactor component configuration
1 parent ebfb56a commit e88cb2e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+478
-489
lines changed

ninbot-app/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@
166166
<dependency>
167167
<groupId>org.springframework.cloud</groupId>
168168
<artifactId>spring-cloud-starter-openfeign</artifactId>
169+
<exclusions>
170+
<exclusion>
171+
<groupId>io.github.openfeign</groupId>
172+
<artifactId>feign-core</artifactId>
173+
</exclusion>
174+
</exclusions>
169175
</dependency>
170176
<dependency>
171177
<groupId>org.testcontainers</groupId>

ninbot-app/src/main/java/dev/nincodedo/ninbot/NinbotCommandLineRunner.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ private void waitForShardStartup(JDA jda) {
3939
jda.awaitReady();
4040
log.info("Shard ID {}: Connected to {} server(s)", jda.getShardInfo().getShardId(), jda.getGuilds().size());
4141
statManager.upsertCount("Connected Servers", "server", null, jda.getGuilds().size());
42-
jda.getGuilds().forEach(guild -> log.info(FormatLogObject.guildInfo(guild)));
42+
jda.getGuilds()
43+
.forEach(guild -> log.info("Shard ID {} connected to {}", jda.getShardInfo()
44+
.getShardId(), FormatLogObject.guildInfo(guild)));
4345
} catch (InterruptedException e) {
4446
log.error("Failed to wait for shard to start", e);
4547
}

ninbot-app/src/main/java/dev/nincodedo/ninbot/components/channel/text/TopicChangeCommand.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import dev.nincodedo.nincord.config.db.ConfigService;
88
import dev.nincodedo.nincord.message.MessageExecutor;
99
import dev.nincodedo.nincord.message.SlashCommandEventMessageExecutor;
10+
import net.dv8tion.jda.api.Permission;
1011
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
12+
import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
1113
import org.jetbrains.annotations.NotNull;
1214
import org.springframework.stereotype.Component;
1315

@@ -26,8 +28,8 @@ public String getName() {
2628
}
2729

2830
@Override
29-
public boolean isCommandEnabledByDefault() {
30-
return false;
31+
public DefaultMemberPermissions getPermissions() {
32+
return DefaultMemberPermissions.enabledFor(Permission.MANAGE_CHANNEL);
3133
}
3234

3335
@Override

ninbot-app/src/main/java/dev/nincodedo/ninbot/components/dad/Dadbot.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import dev.nincodedo.nincord.config.db.ConfigConstants;
77
import dev.nincodedo.nincord.config.db.ConfigService;
88
import dev.nincodedo.nincord.config.db.component.ComponentService;
9+
import dev.nincodedo.nincord.config.db.component.ComponentType;
910
import dev.nincodedo.nincord.message.MessageExecutor;
1011
import dev.nincodedo.nincord.message.MessageReceivedEventMessageExecutor;
1112
import dev.nincodedo.nincord.message.impersonation.Impersonation;
@@ -44,14 +45,15 @@ public Dadbot(StatManager statManager, @Qualifier("statCounterThreadPool") Execu
4445
this.configService = configService;
4546
componentName = "dad";
4647
this.componentService = componentService;
48+
this.componentService.registerComponent(componentName, ComponentType.ACTION);
4749
this.dadbotMessageParser = new DadbotMessageParser();
4850
this.dadbotImpersonation = Impersonation.of("Dadbot", "https://i.imgur.com/zfKodNp.png");
4951
}
5052

5153
@Override
5254
public void onMessageReceived(MessageReceivedEvent event) {
5355
if (event.isFromGuild() && !event.getAuthor().isBot()
54-
&& !componentService.isDisabled(componentName, event.getGuild().getId())) {
56+
&& !componentService.isDisabled(componentName, event.getGuild().getId(), event.getAuthor().getId())) {
5557
resourceBundle = LocaleService.getResourceBundleOrDefault(event.getGuild());
5658
parseMessage(event).executeActions();
5759
}

ninbot-app/src/main/java/dev/nincodedo/ninbot/components/haiku/HaikuListener.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public HaikuListener(StatManager statManager, @Qualifier("statCounterThreadPool"
4343
@Override
4444
public void onMessageReceived(MessageReceivedEvent event) {
4545
if (!event.isFromGuild() || event.getAuthor().isBot()
46-
|| componentService.isDisabled(componentName, event.getGuild().getId())) {
46+
|| componentService.isDisabled(componentName, event.getGuild().getId(), event.getAuthor().getId())) {
4747
return;
4848
}
4949
var message = event.getMessage().getContentStripped();

ninbot-app/src/main/java/dev/nincodedo/ninbot/components/reaction/numbers/GoodNumbersListener.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected GoodNumbersListener(ConfigService configService, StatManager statManag
4747
@Override
4848
public void onMessageReceived(@NotNull MessageReceivedEvent event) {
4949
if (event.getAuthor().isBot() || !event.isFromGuild()
50-
|| componentService.isDisabled(componentName, event.getGuild().getId())) {
50+
|| componentService.isDisabled(componentName, event.getGuild().getId(), event.getAuthor().getId())) {
5151
return;
5252
}
5353
List<Integer> numbers = collectNumbers(event);
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
package dev.nincodedo.ninbot.components.users;
22

3+
import dev.nincodedo.nincord.config.db.component.ComponentConfiguration;
34
import dev.nincodedo.nincord.persistence.BaseEntity;
45
import lombok.Data;
6+
import lombok.EqualsAndHashCode;
7+
import lombok.NoArgsConstructor;
58

69
import jakarta.persistence.Column;
710
import jakarta.persistence.Entity;
11+
import jakarta.persistence.Transient;
12+
import java.util.ArrayList;
13+
import java.util.List;
814

15+
@EqualsAndHashCode(callSuper = true)
916
@Data
1017
@Entity
18+
@NoArgsConstructor
1119
public class NinbotUser extends BaseEntity {
12-
@Column(nullable = false)
20+
@Column(nullable = false, unique = true)
1321
private String userId;
14-
@Column(nullable = false)
15-
private String serverId;
16-
private String birthday;
17-
@Column(nullable = false)
18-
private Boolean announceBirthday = false;
22+
@Transient
23+
private List<ComponentConfiguration> userSettings = new ArrayList<>();
24+
25+
public NinbotUser(String userId) {
26+
this.userId = userId;
27+
}
1928
}

ninbot-app/src/main/java/dev/nincodedo/ninbot/components/users/UserCommand.java

+51-33
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,79 @@
11
package dev.nincodedo.ninbot.components.users;
22

33
import dev.nincodedo.ninbot.components.users.UserCommandName.Subcommand;
4-
import dev.nincodedo.nincord.Emojis;
54
import dev.nincodedo.nincord.command.slash.SlashSubCommand;
5+
import dev.nincodedo.nincord.config.db.component.ComponentConfiguration;
6+
import dev.nincodedo.nincord.config.db.component.ComponentService;
67
import dev.nincodedo.nincord.message.MessageExecutor;
78
import dev.nincodedo.nincord.message.SlashCommandEventMessageExecutor;
9+
import lombok.RequiredArgsConstructor;
10+
import net.dv8tion.jda.api.EmbedBuilder;
811
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
9-
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
10-
import net.dv8tion.jda.api.interactions.commands.OptionType;
11-
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
1212
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
13+
import net.dv8tion.jda.api.interactions.components.ActionRow;
14+
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
15+
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
16+
import net.dv8tion.jda.api.utils.messages.MessageEditBuilder;
17+
import org.apache.commons.text.WordUtils;
1318
import org.jetbrains.annotations.NotNull;
1419
import org.springframework.stereotype.Component;
1520

1621
import java.util.Arrays;
1722
import java.util.List;
1823

1924
@Component
25+
@RequiredArgsConstructor
2026
public class UserCommand implements SlashSubCommand<Subcommand> {
2127

22-
private UserService userService;
23-
24-
public UserCommand(UserService userService) {
25-
this.userService = userService;
26-
}
28+
private final UserService userService;
29+
private final ComponentService componentService;
2730

2831
@Override
2932
public MessageExecutor execute(@NotNull SlashCommandInteractionEvent event,
3033
@NotNull SlashCommandEventMessageExecutor messageExecutor, @NotNull Subcommand subcommand) {
31-
if (subcommand == Subcommand.BIRTHDAY) {
32-
updateBirthday(event);
33-
messageExecutor.addEphemeralMessage(Emojis.THUMBS_UP);
34-
} else if (subcommand == Subcommand.ANNOUNCEMENT) {
35-
toggleAnnouncement(event.getMember().getId());
36-
messageExecutor.addEphemeralMessage(Emojis.THUMBS_UP);
34+
if (subcommand == Subcommand.FEATURES) {
35+
event.deferReply(true).queue();
36+
var userToggleableComponents = componentService.findUserToggleableComponents();
37+
38+
var usersDisabledComponents = userService.getUserById(event.getUser().getId()).getUserSettings()
39+
.stream()
40+
.filter(ComponentConfiguration::getDisabled)
41+
.map(ComponentConfiguration::getComponent)
42+
.map(component -> SelectOption.of(createSelectOptionLabel(component),
43+
createSelectOptionValue(component)))
44+
.toList();
45+
46+
var selectOptions = userToggleableComponents.stream()
47+
.map(component -> SelectOption.of(createSelectOptionLabel(component),
48+
createSelectOptionValue(component)))
49+
.toList();
50+
51+
var editedMessage = new MessageEditBuilder().setReplace(true)
52+
.setEmbeds(new EmbedBuilder().setTitle("Ninbot Feature User Settings")
53+
.appendDescription("This is a list of all Ninbot features you currently have disabled. "
54+
+ "Add items to the list to disable those features for your user in any server "
55+
+ "with Ninbot.")
56+
.build())
57+
.setComponents(ActionRow.of(StringSelectMenu.create("user-disabled-id")
58+
.addOptions(selectOptions)
59+
.setRequiredRange(0, userToggleableComponents.size())
60+
.setDefaultOptions(usersDisabledComponents)
61+
.build()))
62+
.build();
63+
64+
event.getHook().editOriginal(editedMessage).queue();
3765
}
3866
return messageExecutor;
3967
}
4068

41-
private void toggleAnnouncement(String userId) {
42-
userService.toggleBirthdayAnnouncement(userId);
69+
private @NotNull String createSelectOptionValue(dev.nincodedo.nincord.config.db.component.Component component) {
70+
return String.format("component-%s-%s", component.getName().replace('-', '_'),
71+
component.getId());
4372
}
4473

45-
private void updateBirthday(SlashCommandInteractionEvent event) {
46-
var birthday = event.getOption(UserCommandName.Option.MONTH.get(), OptionMapping::getAsString) + "-"
47-
+ event.getOption(UserCommandName.Option.DAY.get(), OptionMapping::getAsString);
48-
var userId = event.getMember().getId();
49-
var guildId = event.getGuild().getId();
50-
userService.updateBirthday(userId, guildId, birthday);
74+
private String createSelectOptionLabel(dev.nincodedo.nincord.config.db.component.Component component) {
75+
return WordUtils.capitalizeFully(component.getName()
76+
.replace('-', ' '));
5177
}
5278

5379
@Override
@@ -57,16 +83,8 @@ public String getName() {
5783

5884
@Override
5985
public List<SubcommandData> getSubcommandDatas() {
60-
return Arrays.asList(
61-
new SubcommandData(Subcommand.BIRTHDAY.get(), "Set your birthday for announcements.")
62-
.addOptions(new OptionData(OptionType.INTEGER, UserCommandName.Option.MONTH.get(), "Month of "
63-
+ "your birthday.",
64-
true, true).setMinValue(1).setMaxValue(12))
65-
.addOptions(new OptionData(OptionType.INTEGER, UserCommandName.Option.DAY.get(), "Day of your"
66-
+ " birthday.", true, true).setMinValue(1)
67-
.setMaxValue(31)),
68-
new SubcommandData(Subcommand.ANNOUNCEMENT.get(), "Toggles your birthday announcement"
69-
+ " on or off."));
86+
return Arrays.asList(new SubcommandData(Subcommand.FEATURES.get(), "Opt in or out of various Ninbot "
87+
+ "features."));
7088
}
7189

7290
@Override

ninbot-app/src/main/java/dev/nincodedo/ninbot/components/users/UserCommandName.java

+1-5
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ enum UserCommandName implements CommandNameEnum {
66
USER;
77

88
enum Subcommand implements CommandNameEnum {
9-
BIRTHDAY, ANNOUNCEMENT
10-
}
11-
12-
enum Option implements CommandNameEnum {
13-
MONTH, DAY
9+
FEATURES
1410
}
1511
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package dev.nincodedo.ninbot.components.users;
2+
3+
import dev.nincodedo.nincord.command.component.ComponentData;
4+
import dev.nincodedo.nincord.command.component.StringSelectMenuInteraction;
5+
import dev.nincodedo.nincord.message.MessageExecutor;
6+
import dev.nincodedo.nincord.message.StringSelectMenuInteractionCommandMessageExecutor;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
import net.dv8tion.jda.api.EmbedBuilder;
10+
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
11+
import net.dv8tion.jda.api.interactions.components.selections.SelectOption;
12+
import org.apache.commons.text.WordUtils;
13+
import org.jetbrains.annotations.NotNull;
14+
import org.slf4j.Logger;
15+
import org.springframework.stereotype.Component;
16+
17+
import java.util.stream.Collectors;
18+
19+
@Slf4j
20+
@Component
21+
@RequiredArgsConstructor
22+
public class UserFeaturesSelectMenuCommand implements StringSelectMenuInteraction {
23+
24+
private final UserService userService;
25+
26+
@Override
27+
public String getName() {
28+
return UserCommandName.USER.get();
29+
}
30+
31+
@Override
32+
public MessageExecutor execute(@NotNull StringSelectInteractionEvent event,
33+
@NotNull StringSelectMenuInteractionCommandMessageExecutor messageExecutor,
34+
@NotNull ComponentData componentData) {
35+
var disabledComponents = event.getSelectedOptions();
36+
var disabledComponentNames = disabledComponents.stream()
37+
.map(SelectOption::getValue)
38+
.map(value -> value.split("-")[1].replace('_', '-'))
39+
.toList();
40+
userService.setDisableComponentsByUser(event.getUser().getId(), disabledComponentNames);
41+
if (disabledComponents.isEmpty()) {
42+
event.editMessageEmbeds(new EmbedBuilder(event.getMessage()
43+
.getEmbeds()
44+
.getFirst()).setDescription("All Ninbot features are now enabled.").build())
45+
.setComponents()
46+
.queue();
47+
} else {
48+
var disabledComponentsString = disabledComponentNames.stream()
49+
.map(name -> WordUtils.capitalizeFully(name.replace('-', ' ')))
50+
.collect(Collectors.joining(", "));
51+
event.editMessageEmbeds(new EmbedBuilder(event.getMessage()
52+
.getEmbeds()
53+
.getFirst()).setDescription(String.format("The following Ninbot features are now disabled: %s",
54+
disabledComponentsString))
55+
.build()).setComponents().queue();
56+
}
57+
return messageExecutor;
58+
}
59+
60+
@Override
61+
public Logger log() {
62+
return log;
63+
}
64+
}

ninbot-app/src/main/java/dev/nincodedo/ninbot/components/users/UserRepository.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@
66

77
public interface UserRepository extends BaseRepository<NinbotUser> {
88

9-
Optional<NinbotUser> getFirstByUserId(String userId);
9+
Optional<NinbotUser> getByUserId(String userId);
1010
}
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,31 @@
11
package dev.nincodedo.ninbot.components.users;
22

3-
import dev.nincodedo.nincord.Scheduler;
3+
import dev.nincodedo.nincord.config.db.component.ComponentService;
4+
import lombok.RequiredArgsConstructor;
45
import org.springframework.stereotype.Service;
56

6-
import jakarta.transaction.Transactional;
77
import java.util.List;
88

99
@Service
10-
public class UserService implements Scheduler<NinbotUser, UserRepository> {
11-
private UserRepository userRepository;
12-
13-
UserService(UserRepository userRepository) {
14-
this.userRepository = userRepository;
15-
}
16-
17-
@Transactional
18-
public void updateBirthday(String userId, String guildId, String birthday) {
19-
var optionalUser = userRepository.getFirstByUserId(userId);
20-
NinbotUser ninbotUser;
10+
@RequiredArgsConstructor
11+
public class UserService {
12+
private final UserRepository userRepository;
13+
private final ComponentService componentService;
14+
15+
public NinbotUser getUserById(String userId) {
16+
NinbotUser user;
17+
var optionalUser = userRepository.getByUserId(userId);
2118
if (optionalUser.isPresent()) {
22-
ninbotUser = optionalUser.get();
19+
user = optionalUser.get();
2320
} else {
24-
ninbotUser = new NinbotUser();
25-
ninbotUser.setUserId(userId);
26-
ninbotUser.setServerId(guildId);
21+
user = new NinbotUser(userId);
22+
userRepository.saveAndFlush(user);
2723
}
28-
ninbotUser.setBirthday(birthday);
29-
userRepository.save(ninbotUser);
30-
}
31-
32-
public void toggleBirthdayAnnouncement(String userId) {
33-
userRepository.getFirstByUserId(userId)
34-
.ifPresent(user -> {
35-
user.setAnnounceBirthday(!user.getAnnounceBirthday());
36-
userRepository.save(user);
37-
});
38-
}
39-
40-
@Override
41-
public List<NinbotUser> findAllOpenItems() {
42-
return userRepository.findAll();
24+
user.setUserSettings(componentService.findUserConfigurations(userId));
25+
return user;
4326
}
4427

45-
@Override
46-
public UserRepository getRepository() {
47-
return userRepository;
28+
public void setDisableComponentsByUser(String userId, List<String> disabledComponentNames) {
29+
componentService.setDisabledComponentsByUser(userId, disabledComponentNames);
4830
}
4931
}

0 commit comments

Comments
 (0)