Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ protected final String generateComponentId(String... args) {
*/
@SuppressWarnings({"OverloadedVarargsMethod", "WeakerAccess"})
protected final String generateComponentId(Lifespan lifespan, String... args) {
return componentIdGenerator.generate(new ComponentId(getName(), Arrays.asList(args)),
lifespan);
return componentIdGenerator
.generate(new ComponentId(UserInteractorPrefix.getPrefixedNameFromInstance(this),
Arrays.asList(args)), lifespan);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ public BotCore(JDA jda, Database database, Config config) {
componentIdParser = uuid -> componentIdStore.get(UUID.fromString(uuid));
Collection<UserInteractor> interactors = getInteractors();

interactors.forEach(slashCommand -> slashCommand
.acceptComponentIdGenerator(((componentId, lifespan) -> {
UUID uuid = UUID.randomUUID();
componentIdStore.putOrThrow(uuid, componentId, lifespan);
return uuid.toString();
})));
interactors.forEach(
interactor -> interactor.acceptComponentIdGenerator(((componentId, lifespan) -> {
UUID uuid = UUID.randomUUID();
componentIdStore.putOrThrow(uuid, componentId, lifespan);
return uuid.toString();
})));

if (logger.isInfoEnabled()) {
logger.info("Available user interactors: {}", interactors);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package org.togetherjava.tjbot.commands.tags;

import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.IMentionable;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.requests.restaction.interactions.ReplyCallbackAction;
import org.togetherjava.tjbot.commands.CommandVisibility;
import org.togetherjava.tjbot.commands.SlashCommandAdapter;

import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
* Implements the {@code /tag} command which lets the bot respond content of a tag that has been
Expand Down Expand Up @@ -46,20 +50,40 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {
String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
OptionMapping replyToUserOption = event.getOption(REPLY_TO_USER_OPTION);

if (tagSystem.handleIsUnknownTag(id, event)) {
if (tagSystem.handleIsUnknownTag(id, event, super::generateComponentId,
replyToUserOption)) {
return;
}

ReplyCallbackAction message = event
.replyEmbeds(new EmbedBuilder().setDescription(tagSystem.getTag(id).orElseThrow())
.setFooter(event.getUser().getName() + " • used " + event.getCommandString())
.setTimestamp(Instant.now())
.setColor(TagSystem.AMBIENT_COLOR)
.build());
sendTagReply(event, event.getUser().getName(), id, Optional.of(event.getCommandString()),
Optional.ofNullable(replyToUserOption)
.map(OptionMapping::getAsUser)
.map(IMentionable::getAsMention));
}

if (replyToUserOption != null) {
message = message.setContent(replyToUserOption.getAsUser().getAsMention());
@Override
public void onButtonClick(ButtonInteractionEvent event, List<String> args) {
if (!TagSystem.TAG_SUGGESTION_INDICATOR.equals(args.get(0))) {
return;
}
message.queue();

sendTagReply(event, event.getUser().getName(), args.get(1), Optional.empty(),
Optional.ofNullable(args.get(2)));
}

/**
* Sends the reply for a successfull /tag use (i.e. the given tag exists)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing dot

*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private void sendTagReply(IReplyCallback callback, String userName, String tag,
Optional<String> commandString, Optional<String> replyToUser) {
callback
.replyEmbeds(new EmbedBuilder().setDescription(tagSystem.getTag(tag).orElseThrow())
.setFooter(userName + commandString.map(s -> " • used " + s).orElse(""))
.setTimestamp(Instant.now())
.setColor(TagSystem.AMBIENT_COLOR)
.build())
.setContent(replyToUser.orElse(""))
.queue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ public void onSlashCommand(SlashCommandInteractionEvent event) {

private void rawTag(SlashCommandInteractionEvent event) {
String id = Objects.requireNonNull(event.getOption(ID_OPTION)).getAsString();
if (tagSystem.handleIsUnknownTag(id, event)) {
if (tagSystem.handleIsUnknownTag(id, event, super::generateComponentId, null)) {
return;
}

Expand Down Expand Up @@ -316,7 +316,7 @@ private Optional<String> getTagContent(Subcommand subcommand, String id) {
private boolean isWrongTagStatusAndHandle(TagStatus requiredTagStatus, String id,
IReplyCallback event) {
if (requiredTagStatus == TagStatus.EXISTS) {
return tagSystem.handleIsUnknownTag(id, event);
return tagSystem.handleIsUnknownTag(id, event, super::generateComponentId, null);
} else if (requiredTagStatus == TagStatus.NOT_EXISTS) {
if (tagSystem.hasTag(id)) {
event.reply("The tag with id '%s' already exists.".formatted(id))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,38 @@

import net.dv8tion.jda.api.entities.Emoji;
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.buttons.Button;
import net.dv8tion.jda.api.interactions.components.buttons.ButtonStyle;
import org.apache.commons.collections4.ListUtils;
import org.togetherjava.tjbot.commands.utils.StringDistances;
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.db.generated.tables.Tags;
import org.togetherjava.tjbot.db.generated.tables.records.TagsRecord;

import javax.annotation.Nullable;
import java.awt.Color;
import java.util.Optional;
import java.util.Set;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
* The core of the tag system. Provides methods to read and create tags, directly tied to the
* underlying database.
*/
public final class TagSystem {
/**
* The amount of candidates that are suggested as buttons in the message that tells the user
* that the tag they tried accessing doesn't exist
*/
private static final int CANDIDATE_SUGGESTIONS_PER_USAGE = 5;

/**
* todo document
*/
static final String TAG_SUGGESTION_INDICATOR = "tag suggestion";

/**
* The ambient color to use for tag system related messages.
*/
Expand Down Expand Up @@ -57,17 +72,40 @@ static Button createDeleteButton(String componentId) {
* @param event the event to send messages with
* @return whether the given tag is unknown to the system
*/
boolean handleIsUnknownTag(String id, IReplyCallback event) {
boolean handleIsUnknownTag(String id, IReplyCallback event,
Function<String[], String> componentIdGenerator,
@Nullable OptionMapping userToReplyTo) {
if (hasTag(id)) {
return false;
}
String suggestionText = StringDistances.closestMatch(id, getAllIds())
.map(", did you perhaps mean '%s'?"::formatted)
.orElse(".");

event.reply("Could not find any tag with id '%s'%s".formatted(id, suggestionText))
List<String> candidates = getAllIds().stream()
.sorted(Comparator
.comparingInt(candidate -> StringDistances.editDistance(id, candidate)))
.limit(CANDIDATE_SUGGESTIONS_PER_USAGE)
.toList();
List<ActionRow> rows = new ArrayList<>();
List<List<String>> partition = ListUtils.partition(candidates, 5);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move that down a few lines, to where its used first


for (List<String> part : partition) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming issue: partition and partitions, would be easier to understand.

Or batch and batches

rows.add(
ActionRow
.of(part.stream()
.map(i -> Button.secondary(componentIdGenerator.apply(new String[] {
TAG_SUGGESTION_INDICATOR, i,
userToReplyTo != null ? userToReplyTo.getAsUser().getAsMention()
: null}),
i))
.toList()));
}

event
.reply("Could not find any tag with id '%s'%s"
.formatted(id, candidates.isEmpty() ? "." : ", did you perhaps mean any of the following?"))
.setEphemeral(true)
.addActionRows(rows)
.queue();

return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

final class SlashCommandAdapterTest {
private static final String NAME = "foo";
private static final String PREFIXED_NAME =
UserInteractorPrefix.SLASH_COMMAND.getPrefix() + NAME;
private static final String DESCRIPTION = "Foo command";
private static final CommandVisibility VISIBILITY = CommandVisibility.GUILD;
private static final int UNIQUE_ID_ITERATIONS = 20;
Expand Down Expand Up @@ -71,15 +73,15 @@ void generateComponentId() {
String[] elements = {"foo", "bar", "baz"};
String[] componentIdText = adapter.generateComponentId(elements).split(";");
assertEquals(3, componentIdText.length);
assertEquals(NAME, componentIdText[0]);
assertEquals(PREFIXED_NAME, componentIdText[0]);
assertEquals(Integer.toString(elements.length), componentIdText[1]);
assertEquals(Lifespan.REGULAR.toString(), componentIdText[2]);

// Explicit lifespan
for (Lifespan lifespan : Lifespan.values()) {
componentIdText = adapter.generateComponentId(lifespan, elements).split(";");
assertEquals(3, componentIdText.length);
assertEquals(NAME, componentIdText[0]);
assertEquals(PREFIXED_NAME, componentIdText[0]);
assertEquals(Integer.toString(elements.length), componentIdText[1]);
assertEquals(lifespan.toString(), componentIdText[2]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.togetherjava.tjbot.commands.SlashCommand;
import org.togetherjava.tjbot.commands.componentids.ComponentId;
import org.togetherjava.tjbot.commands.componentids.ComponentIdGenerator;
import org.togetherjava.tjbot.commands.componentids.Lifespan;
import org.togetherjava.tjbot.db.Database;
import org.togetherjava.tjbot.db.generated.tables.Tags;
import org.togetherjava.tjbot.jda.JdaTester;
Expand All @@ -27,6 +30,8 @@ void setUp() {
system = spy(new TagSystem(database));
jdaTester = new JdaTester();
command = new TagCommand(system);

command.acceptComponentIdGenerator((componentId, lifespan) -> "foo");
}

private SlashCommandInteractionEvent triggerSlashCommand(String id,
Expand Down Expand Up @@ -65,7 +70,7 @@ void canNotFindTagSuggestDifferentTag() {

// THEN responds that the tag could not be found and instead suggests using the other tag
verify(event)
.reply("Could not find any tag with id 'second', did you perhaps mean 'first'?");
.reply("Could not find any tag with id 'second', did you perhaps mean any of the following?");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ void handleIsUnknownTag() {
SlashCommandInteractionEvent event =
jdaTester.createSlashCommandInteractionEvent(new TagCommand(system)).build();

assertFalse(system.handleIsUnknownTag("known", event));
assertFalse(system.handleIsUnknownTag("known", event, a -> "foo", null));
verify(event, never()).reply(anyString());

assertTrue(system.handleIsUnknownTag("unknown", event));
assertTrue(system.handleIsUnknownTag("unknown", event, a -> "foo", null));
verify(event).reply(anyString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import net.dv8tion.jda.api.exceptions.ErrorResponseException;
import net.dv8tion.jda.api.interactions.InteractionHook;
import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback;
import net.dv8tion.jda.api.interactions.components.ActionRow;
import net.dv8tion.jda.api.interactions.components.ItemComponent;
import net.dv8tion.jda.api.requests.ErrorResponse;
import net.dv8tion.jda.api.requests.Response;
Expand Down Expand Up @@ -151,6 +152,8 @@ public JdaTester() {
when(replyAction.addActionRow(anyCollection())).thenReturn(replyAction);
when(replyAction.addActionRow(ArgumentMatchers.<ItemComponent>any()))
.thenReturn(replyAction);
when(replyAction.addActionRows(anyCollection())).thenReturn(replyAction);
when(replyAction.addActionRows(ArgumentMatchers.<ActionRow>any())).thenReturn(replyAction);
when(replyAction.setContent(anyString())).thenReturn(replyAction);
when(replyAction.addFile(any(byte[].class), any(String.class), any(AttachmentOption.class)))
.thenReturn(replyAction);
Expand Down