[AB-97] adding react command

we are now actively loading messages in case its a parameter, because the provided message is only partially available
This commit is contained in:
Sheldan
2021-04-13 23:47:22 +02:00
parent 23379e4498
commit 537fd85be8
36 changed files with 820 additions and 22 deletions

View File

@@ -57,6 +57,10 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>

View File

@@ -12,7 +12,7 @@ import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.model.ChooseResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.ChooseResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

View File

@@ -13,7 +13,7 @@ import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.model.EightBallResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.EightBallResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

View File

@@ -12,7 +12,7 @@ import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.model.LoveCalcResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.LoveCalcResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

View File

@@ -13,7 +13,7 @@ import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.model.MockResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.MockResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;

View File

@@ -0,0 +1,71 @@
package dev.sheldan.abstracto.entertainment.command;
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.HelpInfo;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.service.ReactionService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.exception.ReactTooManyReactionsException;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.CompletableFuture;
@Component
public class React extends AbstractConditionableCommand {
@Autowired
private EntertainmentService entertainmentService;
@Autowired
private ReactionService reactionService;
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
Message message = (Message) parameters.get(0);
String text = (String) parameters.get(1);
List<String> reactionChars = entertainmentService.convertTextToEmojis(text);
int existingReactions = message.getReactions().size();
if(reactionChars.size() + existingReactions > Message.MAX_REACTIONS) {
throw new ReactTooManyReactionsException();
}
List<CompletableFuture<Void>> futures = new ArrayList<>();
reactionChars.forEach(s -> futures.add(reactionService.addDefaultReactionToMessageAsync(s, message)));
return FutureUtils.toSingleFutureGeneric(futures)
.thenApply(unused -> CommandResult.fromSuccess());
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
parameters.add(Parameter.builder().name("message").type(Message.class).templated(true).build());
parameters.add(Parameter.builder().name("text").type(String.class).remainder(true).templated(true).build());
HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
return CommandConfiguration.builder()
.name("react")
.module(EntertainmentModuleDefinition.ENTERTAINMENT)
.templated(true)
.causesReaction(true)
.async(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return EntertainmentFeatureDefinition.ENTERTAINMENT;
}
}

View File

@@ -15,7 +15,7 @@ import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureConfig;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.model.RollResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.RollResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

View File

@@ -12,7 +12,7 @@ import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureDefinition;
import dev.sheldan.abstracto.entertainment.config.EntertainmentModuleDefinition;
import dev.sheldan.abstracto.entertainment.model.RouletteResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.RouletteResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

View File

@@ -1,16 +1,26 @@
package dev.sheldan.abstracto.entertainment.service;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureConfig;
import dev.sheldan.abstracto.entertainment.exception.ReactDuplicateCharacterException;
import dev.sheldan.abstracto.entertainment.model.ReactMapping;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.io.InputStreamReader;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import java.util.*;
@Component
@Slf4j
public class EntertainmentServiceBean implements EntertainmentService {
public static final List<String> EIGHT_BALL_ANSWER_KEYS = Arrays.asList(
@@ -20,12 +30,20 @@ public class EntertainmentServiceBean implements EntertainmentService {
"DONT_COUNT", "REPLY_NO", "SOURCES_NO", "OUTLOOK_NOT_GOOD", "DOUBTFUL" // negative
);
private ReactMapping reactMapping;
@Autowired
private SecureRandom secureRandom;
@Autowired
private ConfigService configService;
@Value("classpath:react_mappings.json")
private Resource reactMappingSource;
@Autowired
private Gson gson;
@Override
public String getEightBallValue(String text) {
return EIGHT_BALL_ANSWER_KEYS.get(secureRandom.nextInt(EIGHT_BALL_ANSWER_KEYS.size()));
@@ -57,6 +75,9 @@ public class EntertainmentServiceBean implements EntertainmentService {
@Override
public String createMockText(String text, Member memberExecuting, Member mockedUser) {
if(text == null) {
return "";
}
char[] textChars = text.toLowerCase().toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0, textCharsLength = textChars.length; i < textCharsLength; i++) {
@@ -69,4 +90,101 @@ public class EntertainmentServiceBean implements EntertainmentService {
}
return sb.toString();
}
@Override
public List<String> convertTextToEmojis(String text) {
return convertTextToEmojis(text, false);
}
@Override
public String convertTextToEmojisAString(String text) {
return String.join("", convertTextToEmojis(text));
}
@Override
public List<String> convertTextToEmojis(String text, boolean allowDuplicates) {
if(text == null) {
return new ArrayList<>();
}
text = text.toLowerCase();
// we have to have a separate set to check for combo duplicates, because the checks are different:
// first check is if we already used it as an replacement
// the second check below is whether or not we used it as a replacement, that way we allow
// unicode cars from users as well, this leads to things like sos[sos] not being allowed, because the
// unicode chars get removed, and the first sos gets replaced with the unicode
Set<String> replacedCombos = new HashSet<>();
List<String> result = new ArrayList<>();
// this is used to replace the replacements for more than one character
for (String s : this.reactMapping.getCombinationKeys()) {
if (text.contains(s)) {
String replacement = this.reactMapping.getCombination().get(s);
if(!replacedCombos.contains(replacement) || allowDuplicates) {
if(allowDuplicates) {
text = text.replaceAll(s, replacement);
} else {
text = text.replaceFirst(s, replacement);
}
replacedCombos.add(replacement);
}
}
}
Set<String> usedReplacements = new HashSet<>();
char[] split = text.toCharArray();
for (int i = 0, splitLength = split.length; i < splitLength; i++) {
char normalCharacter = split[i];
String charAsString = Character.toString(normalCharacter);
// the split, also splits surrogate chars (naturally), therefore we need this additional checks
// to ignore the first part, and connect the chars again in order to check them
if(Character.isHighSurrogate(normalCharacter)) {
continue;
}
// in this case we already have unicode, this can either come from the multiple char replacement
// or because we already got unicode to begin with (multi char only), in that case, we also do a duplicate check
// and add it directly
if(Character.isLowSurrogate(normalCharacter)) {
String usedUnicode = split[i - 1] + charAsString;
if(!usedReplacements.contains(usedUnicode) || allowDuplicates) {
usedReplacements.add(usedUnicode);
result.add(usedUnicode);
}
continue;
}
// reject any other character, as the ones we can deal with
if (!this.reactMapping.getSingle().containsKey(charAsString)) {
continue;
}
List<String> listToUse = this.reactMapping.getSingle().get(charAsString);
boolean foundReplacement = false;
for (String replacementChar : listToUse) {
if (!usedReplacements.contains(replacementChar) || allowDuplicates) {
result.add(replacementChar);
usedReplacements.add(replacementChar);
foundReplacement = true;
break;
}
}
if (!foundReplacement) {
throw new ReactDuplicateCharacterException();
}
}
return result;
}
@Override
public String convertTextToEmojisAsString(String text, boolean allowDuplicates) {
return String.join("", convertTextToEmojis(text, allowDuplicates));
}
@PostConstruct
public void postConstruct() {
try {
JsonReader reader = new JsonReader(new InputStreamReader(reactMappingSource.getInputStream()));
this.reactMapping = gson.fromJson(reader, ReactMapping.class);
this.reactMapping.populateKeys();
} catch (IOException e) {
log.error("Failed to load react bindings.", e);
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog ../dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext ../dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/pro ../dbchangelog-3.8.xsd" >
<include file="entertainment-seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,20 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog ../../dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext ../../dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/pro ../../dbchangelog-3.8.xsd" >
<property name="entertainmentModule" value="(SELECT id FROM module WHERE name = 'entertainment')"/>
<property name="entertainmentFeature" value="(SELECT id FROM feature WHERE key = 'entertainment')"/>
<changeSet author="Sheldan" id="react-command">
<insert tableName="command">
<column name="name" value="react"/>
<column name="module_id" valueComputed="${entertainmentModule}"/>
<column name="feature_id" valueComputed="${entertainmentFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog ../../dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext ../../dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/pro ../../dbchangelog-3.8.xsd" >
<include file="command.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -8,4 +8,5 @@
http://www.liquibase.org/xml/ns/pro dbchangelog-3.8.xsd" >
<include file="1.0-entertainment/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.8-entertainment/collection.xml" relativeToChangelogFile="true"/>
<include file="1.2.9-entertainment/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,280 @@
{
"source": "https://github.com/TrustyJAID/Trusty-cogs/blob/master/fun/constants.py with additions",
"single": {
"a": [
"🇦",
"🅰",
"🍙",
"🔼",
"4⃣",
"🇱🇨",
"🎄",
"🌲",
"🖇️",
"🔼"
],
"b": [
"🇧",
"🅱",
"8⃣"
],
"c": [
"🇨",
"🗜"
],
"d": [
"🇩",
"↩"
],
"e": [
"🇪",
"3⃣",
"📧",
"💶"
],
"f": [
"🇫",
"🎏",
"🚩",
"🏳️️",
"🏴",
"🏁"
],
"g": [
"🇬",
"🗜",
"6⃣",
"9⃣",
"⛽"
],
"h": [
"🇭",
"♓",
"🏩",
"⛩️",
"🪜"
],
"i": [
"🇮",
"",
"🚹",
"1⃣",
"📍"
],
"j": [
"🇯",
"🗾"
],
"k": [
"🇰",
"🎋"
],
"l": [
"🇱",
"1⃣",
"🇮",
"👢",
"💷"
],
"m": [
"🇲",
"Ⓜ",
"📉"
],
"n": [
"🇳",
"♑",
"🎵"
],
"o": [
"🇴",
"🅾",
"0⃣",
"⭕",
"🔘",
"⏺",
"⚪",
"⚫",
"🔵",
"🔴",
"💫",
"⚽",
"🏀",
"⚾",
"🥎",
"🎾",
"🏐",
"💿",
"📀",
"🧭",
"☯️"
],
"p": [
"🇵",
"🅿",
"🚏"
],
"q": [
"🇶",
"♌"
],
"r": [
"🇷"
],
"s": [
"🇸",
"💲",
"5⃣",
"⚡",
"💰",
"💵"
],
"t": [
"🇹",
"✝",
"",
"🎚",
"🌴",
"7⃣"
],
"u": [
"🇺",
"⛎",
"🐉"
],
"v": [
"🇻",
"♈",
"☑"
],
"w": [
"🇼",
"〰",
"📈"
],
"x": [
"🇽",
"❎",
"✖",
"❌",
"⚒",
"🏴‍☠",
"✂️️"
],
"y": [
"🇾",
"✌",
"💴"
],
"z": [
"🇿",
"2⃣"
],
"0": [
"0⃣",
"🅾",
"0⃣",
"⭕",
"🔘",
"⏺",
"⚪",
"⚫",
"🔵",
"🔴",
"💫",
"⚽",
"🏀",
"⚾",
"🥎",
"🎾",
"🏐",
"💿",
"📀",
"🧭",
"☯️"
],
"1": [
"1⃣",
"🇮"
],
"2": [
"2⃣",
"🇿"
],
"3": [
"3⃣"
],
"4": [
"4⃣"
],
"5": [
"5⃣",
"🇸",
"💲",
"⚡"
],
"6": [
"6⃣"
],
"7": [
"7⃣"
],
"8": [
"8⃣",
"🎱",
"🇧",
"🅱"
],
"9": [
"9⃣"
],
"?": [
"❓"
],
"!": [
"❗",
"❕",
"⚠",
"❣"
]
},
"combination": {
"abcd": "🔠",
"1234": "🔢",
"cool": "🆒",
"back": "🔙",
"777": "🎰",
"soon": "🔜",
"free": "🆓",
"end": "🔚",
"top": "🔝",
"abc": "🔤",
"atm": "🏧",
"new": "🆕",
"sos": "🆘",
"100": "💯",
"loo": "💯",
"zzz": "💤",
"...": "💬",
"ng": "🆖",
"id": "🆔",
"vs": "🆚",
"wc": "🚾",
"ab": "🆎",
"cl": "🆑",
"ok": "🆗",
"up": "🆙",
"10": "🔟",
"24": "🏪",
"11": "⏸",
"ll": "⏸",
"ii": "⏸",
"18": "🏪",
"tm": "™",
"on": "🔛",
"oo": "🈁",
"!?": "⁉",
"!!": "‼",
"17": "📅"
}
}

View File

@@ -5,7 +5,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.entertainment.model.ChooseResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.ChooseResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -5,7 +5,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.entertainment.model.EightBallResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.EightBallResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -5,7 +5,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.entertainment.model.LoveCalcResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.LoveCalcResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -6,7 +6,7 @@ import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.entertainment.model.RollResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.RollResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -5,7 +5,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.test.command.CommandConfigValidator;
import dev.sheldan.abstracto.core.test.command.CommandTestUtilities;
import dev.sheldan.abstracto.entertainment.model.RouletteResponseModel;
import dev.sheldan.abstracto.entertainment.model.command.RouletteResponseModel;
import dev.sheldan.abstracto.entertainment.service.EntertainmentService;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -1,21 +1,34 @@
package dev.sheldan.abstracto.entertainment.service;
import com.google.gson.Gson;
import com.google.gson.stream.JsonReader;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.entertainment.config.EntertainmentFeatureConfig;
import dev.sheldan.abstracto.entertainment.exception.ReactDuplicateCharacterException;
import dev.sheldan.abstracto.entertainment.model.ReactMapping;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.core.io.Resource;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.Reader;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.TreeSet;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
@@ -29,6 +42,19 @@ public class EntertainmentServiceBeanTest {
@Mock
private ConfigService configService;
// requires the org.mockito.plugins.MockMaker file
@Mock
private Gson gson;
@Mock
private Resource resource;
@Mock
private Member member;
@Mock
private Member secondMember;
private static final String INPUT_TEXT = "input";
private static final int RANDOM_VALUE = 0;
@@ -94,4 +120,99 @@ public class EntertainmentServiceBeanTest {
Assert.assertEquals(choices.get(0), choiceTaken);
}
@Test
public void testMocking() {
Assert.assertEquals("AsDf", testUnit.createMockText("asdf", member, secondMember));
}
@Test
public void testMockingUpperCase() {
Assert.assertEquals("AsDf", testUnit.createMockText("ASDF", member, secondMember));
}
@Test
public void testMockingNull() {
Assert.assertEquals("", testUnit.createMockText(null, member, secondMember));
}
@Test
public void testConvertTextToEmojis() throws IOException {
setupMappings();
Assert.assertEquals("bceg", testUnit.convertTextToEmojisAsString("asdf", false));
}
@Test(expected = ReactDuplicateCharacterException.class)
public void testConvertTextToEmojisDuplicateReplacement() throws IOException {
setupMappings();
testUnit.convertTextToEmojisAsString("aa", false);
}
@Test
public void testConvertTextToEmojisNoReplacementFound() throws IOException {
setupMappings();
Assert.assertEquals("", testUnit.convertTextToEmojisAsString("e", false));
}
@Test
public void testConvertTextToEmojisNullInput() throws IOException {
setupMappings();
Assert.assertEquals("", testUnit.convertTextToEmojisAsString(null, false));
}
@Test
public void testConvertTextToEmojisDoubleUnicodePassThrough() throws IOException {
setupMappings();
Assert.assertEquals("\uD83C\uDD98", testUnit.convertTextToEmojisAsString("\uD83C\uDD98", false));
}
@Test
public void testConvertTextToEmojisDuplicate() throws IOException {
setupMappings();
Assert.assertEquals("bb", testUnit.convertTextToEmojisAsString("aa", true));
}
@Test
public void testConvertTextToEmojisCombinations() throws IOException {
setupMappings();
Assert.assertEquals("\uD83C\uDD98", testUnit.convertTextToEmojisAsString("kk", true));
}
@Test
public void testConvertTextToEmojisCombinationWithNormalText() throws IOException {
setupMappings();
Assert.assertEquals("\uD83C\uDD98l", testUnit.convertTextToEmojisAsString("kkk", false));
}
@Test
public void testConvertTextToEmojisCombinationWithNormalTextMixed() throws IOException {
setupMappings();
Assert.assertEquals("\uD83C\uDD98lm", testUnit.convertTextToEmojisAsString("kkkk", false));
}
@Test
public void testConvertTextToEmojisCombinationDuplicates() throws IOException {
setupMappings();
Assert.assertEquals("\uD83C\uDD98\uD83C\uDD98", testUnit.convertTextToEmojisAsString("kkkk", true));
}
private void setupMappings() throws IOException {
ReactMapping mapping = Mockito.mock(ReactMapping.class);
HashMap<String, List<String>> singleMappings = new HashMap<>();
singleMappings.put("a", Arrays.asList("b"));
singleMappings.put("s", Arrays.asList("c"));
singleMappings.put("d", Arrays.asList("e"));
singleMappings.put("f", Arrays.asList("g"));
singleMappings.put("k", Arrays.asList("l", "m"));
when(resource.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[]{1}));
when(mapping.getSingle()).thenReturn(singleMappings);
HashMap<String, String> combinations = new HashMap<>();
combinations.put("kk", "\uD83C\uDD98");
when(mapping.getCombination()).thenReturn(combinations);
TreeSet<String> combinationKeys = new TreeSet<>();
combinationKeys.add("kk");
when(mapping.getCombinationKeys()).thenReturn(combinationKeys);
when(gson.fromJson(any(JsonReader.class), eq(ReactMapping.class))).thenReturn(mapping);
testUnit.postConstruct();
}
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.entertainment.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.templating.Templatable;
public class ReactDuplicateCharacterException extends AbstractoRunTimeException implements Templatable {
public ReactDuplicateCharacterException() {
super("Could not replace all characters to be duplicate free.");
}
@Override
public String getTemplateName() {
return "react_duplicate_character_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.entertainment.exception;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.templating.Templatable;
public class ReactTooManyReactionsException extends AbstractoRunTimeException implements Templatable {
public ReactTooManyReactionsException() {
super("Adding reactions would lead to too many reactions.");
}
@Override
public String getTemplateName() {
return "react_too_many_reactions_exception";
}
@Override
public Object getTemplateModel() {
return new Object();
}
}

View File

@@ -0,0 +1,36 @@
package dev.sheldan.abstracto.entertainment.model;
import lombok.*;
import java.util.*;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ReactMapping {
@Builder.Default
private HashMap<String, List<String>> single = new HashMap<>();
@Builder.Default
private HashMap<String, String> combination = new HashMap<>();
@Builder.Default
private SortedSet<String> combinationKeys = new TreeSet<>((o1, o2) -> {
if(o2.length() == o1.length()) {
return o2.compareTo(o1);
} else {
return Integer.compare(o2.length(), o1.length());
}
});
@Builder.Default
private Set<String> combinationReplacements = new HashSet<>();
public void populateKeys() {
combinationKeys.addAll(combination.keySet());
combinationKeys.forEach(s -> combinationReplacements.add(this.combination.get(s)));
}
}

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.entertainment.model;
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.context.SlimUserInitiatedServerContext;
import lombok.Getter;

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.entertainment.model;
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.context.SlimUserInitiatedServerContext;
import lombok.Getter;

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.entertainment.model;
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.context.SlimUserInitiatedServerContext;
import lombok.Getter;

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.entertainment.model;
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.context.SlimUserInitiatedServerContext;
import lombok.Getter;

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.entertainment.model;
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.context.SlimUserInitiatedServerContext;
import lombok.Getter;

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.entertainment.model;
package dev.sheldan.abstracto.entertainment.model.command;
import dev.sheldan.abstracto.core.models.context.SlimUserInitiatedServerContext;
import lombok.Getter;

View File

@@ -1,5 +1,6 @@
package dev.sheldan.abstracto.entertainment.service;
import dev.sheldan.abstracto.entertainment.exception.ReactDuplicateCharacterException;
import net.dv8tion.jda.api.entities.Member;
import java.util.List;
@@ -11,4 +12,64 @@ public interface EntertainmentService {
boolean executeRoulette(Member memberExecuting);
String takeChoice(List<String> choices, Member memberExecuting);
String createMockText(String text, Member memberExecuting, Member mockedUser);
/**
* Converts the given text to unicode characters (with predefined values from a manual mapping) and returns the matched
* characters as a list. If the given text is null, an empty list will be returned. This method will actively try
* to avoid duplicates, and try to use alternatives, and throw an exception in case it was not possible to return unique values.
* The size of the list might not be equal to the length of the provided string, because sometimes multiple
* characters are combined into one unicode char.
* @throws ReactDuplicateCharacterException In case it was not possible to replace all text with appropriate unicode
* in case there too many duplicated characters
* @param text The text to convert
* @return A {@link List} of unicode characters, represented as strings, which look similar to the individual characters
* from the text
*/
List<String> convertTextToEmojis(String text);
/**
* Converts the given text to unicode characters (with predefined values from a manual mapping) and returns the matched
* characters as a string. If the given text is null, an empty string will be returned. This method will actively try
* to avoid duplicates, and try to use alternatives, and throw an exception in case it was not possible to return unique values.
* The length of the string might not be equal to the length of the provided string, because sometimes multiple
* characters are combined into one unicode char.
* @throws ReactDuplicateCharacterException In case it was not possible to replace all text with appropriate unicode
* in case there too many duplicated characters
* @param text The text to convert
* @return A string of unicode characters which look similar to the individual characters from the text
*/
String convertTextToEmojisAString(String text);
/**
* Converts the given text to unicode characters (with predefined values from a manual mapping) and returns the matched
* characters as a list. If the given text is null, an empty list will be returned. This method will actively try
* to avoid duplicates (if requested), and try to use alternatives, and throw an exception in case it was not possible
* to return unique values. In case duplicates are allowed, the first possible replacement value will be used,
* leading to all 1:1 replacements being of the same character.
* The size of the list might not be equal to the length of the provided string, because sometimes multiple
* characters are combined into one unicode char.
* @throws ReactDuplicateCharacterException In case it was not possible to replace all text with appropriate unicode
* in case there too many duplicated characters
* @param text The text to convert
* @param allowDuplicates Whether or not to allow duplicates
* @return A list of characters, represented as strings, which look similar to the individual characters
* from the text, possible with duplicates, if requested
*/
List<String> convertTextToEmojis(String text, boolean allowDuplicates);
/**
* Converts the given text to unicode characters (with predefined values from a manual mapping) and returns the matched
* characters as a string. If the given text is null, an empty string will be returned. This method will actively try
* to avoid duplicates (if requested), and try to use alternatives, and throw an exception in case it was not possible
* to return unique values. In case duplicates are allowed, the first possible replacement value will be used,
* leading to all 1:1 replacements being of the same character.
* The length of the string might not be equal to the length of the provided string, because sometimes multiple
* characters are combined into one unicode char.
* @throws ReactDuplicateCharacterException In case it was not possible to replace all text with appropriate unicode
* in case there too many duplicated characters
* @param text The text to convert
* @param allowDuplicates Whether or not to allow duplicates
* @return A string of unicode characters which look similar to the individual characters from the text
*/
String convertTextToEmojisAsString(String text, boolean allowDuplicates);
}

View File

@@ -5,15 +5,27 @@ import dev.sheldan.abstracto.core.command.CommandConstants;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.UnparsedCommandParameterPiece;
import dev.sheldan.abstracto.core.command.handler.provided.MessageParameterHandler;
import dev.sheldan.abstracto.core.service.MessageService;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
@Component
public class MessageParameterHandlerImpl implements MessageParameterHandler {
@Autowired
private MessageService messageService;
@Override
public Object handle(UnparsedCommandParameterPiece input, CommandParameterIterators iterators, Parameter param, Message context, Command command) {
return context.getReferencedMessage();
public CompletableFuture<Object> handleAsync(UnparsedCommandParameterPiece input, CommandParameterIterators iterators, Parameter param, Message context, Command command) {
return messageService.loadMessage(context.getReferencedMessage()).thenApply(message -> message);
}
@Override
public boolean async() {
return true;
}
@Override

View File

@@ -194,6 +194,11 @@ public class MessageServiceBean implements MessageService {
return channelService.retrieveMessageInChannel(serverId, channelId, messageId);
}
@Override
public CompletableFuture<Message> loadMessage(Message message) {
return loadMessage(message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong());
}
@Override
public MessageAction editMessage(Message message, MessageEmbed messageEmbed) {
metricService.incrementCounter(MESSAGE_EDIT_METRIC);

View File

@@ -59,6 +59,7 @@ public class TemplateServiceBeanTest {
@Mock
private ConfigService configService;
// requires the org.mockito.plugins.MockMaker file
@Mock
private Gson gson;

View File

@@ -34,6 +34,7 @@ public interface MessageService {
CompletableFuture<Void> editMessageInDMChannel(User user, MessageToSend messageToSend, Long messageId);
CompletableFuture<Message> loadMessageFromCachedMessage(CachedMessage cachedMessage);
CompletableFuture<Message> loadMessage(Long serverId, Long channelId, Long messageId);
CompletableFuture<Message> loadMessage(Message message);
MessageAction editMessage(Message message, MessageEmbed messageEmbed);
MessageAction editMessage(Message message, String text, MessageEmbed messageEmbed);
AuditableRestAction<Void> deleteMessageWithAction(Message message);

View File

@@ -194,4 +194,8 @@ Roll a virtual die::
Mock the message of another user::
* Usage: `mock <text/message>`
* Description: Takes the `text` and prints the text with the characters with alternating upper and lower case. If no text is provided, this command requires that the command has been executed in a message which replies to another message. In this case the text to be mocked will be the content of the message which has been replied to.
* Description: Takes the `text` and prints the text with the characters with alternating upper and lower case. If no text is provided, this command requires that the command has been executed in a message which replies to another message. In this case the text to be mocked will be the content of the message which has been replied to.
Add text as reactions to another message::
* Usage: `react <message> <text>`
* Description: Takes the `text`, converts it into unicode characters, while trying to avoid duplicates, and adds the reactions to the given `message`. If it was not possible to avoid duplicates, or the overall reactions (including already existing reactions) would go over the Discord limit, this command will show an error message, without adding any reaction. Some characters can be replaced with one unicode character, for example 'SOS'.