added starboard functionality (starboard/starstats)

added module to store/retrieve configuration (double and string values)
replaced a few null values with optionals (emote loading)
fixed creating channels on startup
added delete message/get emote utility to bot service
extended emote service to have utility methods to use emotes
added reactions to message cache
added empty message handling to  post target service, in case the template evaluates to empty
added ability to edit a message in a post target (standard message and embed)
added principle of config listeners, so default config can be created, for example starboard thresholds
added abstract reaction listeners for adding/removing/clearing
fixed foreign keys between channel and server
added emote utils to handle AEmote and Emotes
renamed emotes to be camelCase, so they are easier to type
This commit is contained in:
Sheldan
2020-04-01 14:16:04 +02:00
parent 03e81a025b
commit 089862bf15
72 changed files with 1795 additions and 121 deletions

View File

@@ -1,8 +1,10 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.models.ServerChannelUser;
import dev.sheldan.abstracto.core.models.database.AEmote;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.TextChannel;
@@ -55,6 +57,20 @@ public class BotService implements Bot {
}
}
@Override
public void deleteMessage(Long serverId, Long channelId, Long messageId) {
getTextChannelFromServer(serverId, channelId).deleteMessageById(messageId).queue();
}
@Override
public Emote getEmote(Long serverId, AEmote emote) {
if(!emote.getCustom()) {
return null;
}
Guild guildById = getGuildById(serverId);
return guildById.getEmoteById(emote.getEmoteId());
}
@Override
public TextChannel getTextChannelFromServer(Long serverId, Long textChannelId) {
Guild guild = getGuildById(serverId);

View File

@@ -0,0 +1,27 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.management.ConfigManagementService;
import dev.sheldan.abstracto.core.models.database.AConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ConfigServiceBean implements ConfigService{
@Autowired
private ConfigManagementService configManagementService;
@Override
public Double getDoubleValue(String name, Long serverId) {
return getDoubleValue(name, serverId, 0D);
}
@Override
public Double getDoubleValue(String name, Long serverId, Double defaultValue) {
AConfig config = configManagementService.loadConfig(serverId, name);
if(config == null) {
return defaultValue;
}
return config.getDoubleValue();
}
}

View File

@@ -1,11 +1,15 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.models.database.AEmote;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.MessageReaction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class EmoteServiceBean implements EmoteService {
@Autowired
@@ -21,4 +25,36 @@ public class EmoteServiceBean implements EmoteService {
}
return false;
}
@Override
public AEmote buildAEmoteFromReaction(MessageReaction.ReactionEmote reaction) {
if(reaction.isEmote()) {
return AEmote.builder().emoteKey(reaction.getName()).custom(true).emoteId(reaction.getEmote().getIdLong()).animated(reaction.getEmote().isAnimated()).build();
} else {
return AEmote.builder().emoteKey(reaction.getEmoji()).custom(false).build();
}
}
@Override
public String getEmoteAsMention(AEmote emote, Long serverId, String defaultText) {
if(emote != null && emote.getCustom()) {
Emote emote1 = botService.getEmote(serverId, emote);
if (emote1 != null) {
return emote1.getAsMention();
} else {
log.warn("Emote {} with name {} in server {} defined, but not usable.", emote.getEmoteId(), emote.getName(), serverId);
return defaultText;
}
} else {
if(emote == null) {
return defaultText;
}
return emote.getEmoteKey();
}
}
@Override
public String getEmoteAsMention(AEmote emote, Long serverId) {
return this.getEmoteAsMention(emote, serverId, " ");
}
}

View File

@@ -1,15 +1,22 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.management.EmoteManagementService;
import dev.sheldan.abstracto.core.management.UserManagementService;
import dev.sheldan.abstracto.core.models.CachedMessage;
import dev.sheldan.abstracto.core.models.CachedReaction;
import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.embed.*;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.MessageReaction;
import net.dv8tion.jda.api.entities.TextChannel;
import net.dv8tion.jda.api.requests.restaction.pagination.ReactionPaginationAction;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.awt.*;
@@ -18,54 +25,71 @@ import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@Component @Slf4j
@CacheConfig(cacheNames = {"messages"})
@Component
@Slf4j
public class MessageCacheBean implements MessageCache {
@Autowired
private Bot bot;
@Autowired
private UserManagementService userManagementService;
@Autowired
private EmoteManagementService emoteManagementService;
@Autowired
private EmoteService emoteService;
@Autowired
@Lazy
private MessageCache self;
@Override
@CachePut(key = "#message.id")
public CachedMessage putMessageInCache(Message message) {
log.debug("Adding message {} to cache", message.getId());
return buildCachedMessageFromMessage(message);
@CachePut(key = "#message.id", cacheNames = "messages")
public CompletableFuture<CachedMessage> putMessageInCache(Message message) {
log.info("Adding message {} to cache", message.getId());
CompletableFuture<CachedMessage> future = new CompletableFuture<>();
self.buildCachedMessageFromMessage(future, message);
return future;
}
@CachePut(key = "#message.messageId")
public CachedMessage putMessageInCache(CachedMessage message) {
return message;
public CompletableFuture<CachedMessage> putMessageInCache(CachedMessage message) {
log.info("Adding cached message to cache");
return CompletableFuture.completedFuture(message);
}
@Override
public CachedMessage getMessageFromCache(Message message) throws ExecutionException, InterruptedException {
log.debug("Retrieving message {}", message.getId());
@Cacheable(key = "#message.id", cacheNames = "messages")
public CompletableFuture<CachedMessage> getMessageFromCache(Message message) {
log.info("Retrieving message {}", message.getId());
return getMessageFromCache(message.getGuild().getIdLong(), message.getChannel().getIdLong(), message.getIdLong());
}
@Override
@Cacheable(key = "#messageId.toString()")
public CachedMessage getMessageFromCache(Long guildId, Long textChannelId, Long messageId) throws ExecutionException, InterruptedException {
@Cacheable(key = "#messageId.toString()", cacheNames = "messages")
public CompletableFuture<CachedMessage> getMessageFromCache(Long guildId, Long textChannelId, Long messageId) {
log.info("Retrieving message with parameters");
CompletableFuture<CachedMessage> cachedMessageCompletableFuture =
getMessage(guildId, textChannelId, messageId)
.thenApply(jdaMessage -> {
CachedMessage cachedMessage = buildCachedMessageFromMessage(jdaMessage);
putMessageInCache(cachedMessage);
return cachedMessage;
});
CompletableFuture<CachedMessage> cachedMessageCompletableFuture = new CompletableFuture<>();
self.loadMessage(cachedMessageCompletableFuture, guildId, textChannelId, messageId);
return cachedMessageCompletableFuture;
}
return cachedMessageCompletableFuture.get();
@Async
@Override
public void loadMessage(CompletableFuture<CachedMessage> future, Long guildId, Long textChannelId, Long messageId) {
TextChannel textChannelById = bot.getTextChannelFromServer(guildId, textChannelId);
textChannelById.retrieveMessageById(messageId).queue(message -> {
buildCachedMessageFromMessage(future, message);
});
}
@Override
public CompletableFuture<Message> getMessage(Long guildId, Long textChannelId, Long messageId) {
TextChannel textChannelById = bot.getTextChannelFromServer(guildId, textChannelId);
return textChannelById.retrieveMessageById(messageId).submit();
}
private CachedMessage buildCachedMessageFromMessage(Message message) {
@Async
public void buildCachedMessageFromMessage(CompletableFuture<CachedMessage> future, Message message) {
List<String> attachmentUrls = new ArrayList<>();
message.getAttachments().forEach(attachment -> {
attachmentUrls.add(attachment.getProxyUrl());
@@ -74,16 +98,55 @@ public class MessageCacheBean implements MessageCache {
message.getEmbeds().forEach(embed -> {
embeds.add(getCachedEmbedFromEmbed(embed));
});
return CachedMessage.builder()
.authorId(message.getAuthor().getIdLong())
.serverId(message.getGuild().getIdLong())
.messageId(message.getIdLong())
.channelId(message.getChannel().getIdLong())
.content(message.getContentRaw())
.embeds(embeds)
.timeCreated(message.getTimeCreated())
.attachmentUrls(attachmentUrls)
.build();
List<CompletableFuture<CachedReaction>> futures = new ArrayList<>();
message.getReactions().forEach(messageReaction -> {
CompletableFuture<CachedReaction> future1 = new CompletableFuture<>();
self.getCachedReactionFromReaction(future1, messageReaction);
futures.add(future1);
});
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).thenAccept(aVoid -> {
future.complete(CachedMessage.builder()
.authorId(message.getAuthor().getIdLong())
.serverId(message.getGuild().getIdLong())
.messageId(message.getIdLong())
.channelId(message.getChannel().getIdLong())
.content(message.getContentRaw())
.embeds(embeds)
.reactions(getFutures(futures))
.timeCreated(message.getTimeCreated())
.attachmentUrls(attachmentUrls)
.build());
});
}
private List<CachedReaction> getFutures(List<CompletableFuture<CachedReaction>> futures) {
List<CachedReaction> reactions = new ArrayList<>();
futures.forEach(future -> {
try {
CachedReaction cachedReaction = future.get();
reactions.add(cachedReaction);
} catch (InterruptedException | ExecutionException e) {
log.error("Error while executing future to retrieve reaction.", e);
}
});
return reactions;
}
@Override
@Async
public void getCachedReactionFromReaction(CompletableFuture<CachedReaction> future, MessageReaction reaction) {
ReactionPaginationAction users = reaction.retrieveUsers().cache(false);
CachedReaction.CachedReactionBuilder builder = CachedReaction.builder();
List<AUser> ausers = new ArrayList<>();
users.forEachAsync(user -> {
ausers.add(AUser.builder().id(user.getIdLong()).build());
return false;
}).thenAccept(o -> future.complete(builder.build()));
builder.users(ausers);
builder.emote(emoteService.buildAEmoteFromReaction(reaction.getReactionEmote()));
}
private CachedEmbed getCachedEmbedFromEmbed(MessageEmbed embed) {

View File

@@ -9,6 +9,8 @@ import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
@Slf4j
public class MessageServiceBean implements MessageService {
@@ -22,16 +24,21 @@ public class MessageServiceBean implements MessageService {
@Override
public void addReactionToMessage(String emoteKey, Long serverId, Message message) {
Guild guildById = bot.getGuildById(serverId);
AEmote emote = emoteManagementService.loadEmoteByName(emoteKey, serverId);
if(emote.getCustom()) {
Emote emoteById = guildById.getEmoteById(emote.getEmoteId());
if(emoteById != null) {
message.addReaction(emoteById).queue();
Optional<AEmote> aEmote = emoteManagementService.loadEmoteByName(emoteKey, serverId);
if(aEmote.isPresent()) {
AEmote emote = aEmote.get();
if(emote.getCustom()) {
Emote emoteById = guildById.getEmoteById(emote.getEmoteId());
if(emoteById != null) {
message.addReaction(emoteById).queue();
} else {
log.warn("Emote with key {} and id {} for guild {} was not found.", emoteKey, emote.getEmoteId(), guildById.getId());
}
} else {
log.warn("Emote with key {} and id {} for guild {} was not found.", emoteKey, emote.getEmoteId(), guildById.getId());
message.addReaction(emote.getEmoteKey()).queue();
}
} else {
message.addReaction(emote.getEmoteKey()).queue();
log.warn("Cannot add reaction, emote {} not defined for server {}.", emoteKey, serverId);
}
}
}

View File

@@ -11,6 +11,7 @@ import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.TextChannel;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -93,7 +94,60 @@ public class PostTargetServiceBean implements PostTargetService {
@Override
public CompletableFuture<Message> sendEmbedInPostTarget(MessageToSend message, PostTarget target) {
TextChannel textChannelForPostTarget = getTextChannelForPostTarget(target);
return textChannelForPostTarget.sendMessage(message.getMessage()).embed(message.getEmbed()).submit();
String messageText = message.getMessage();
if(StringUtils.isBlank(messageText)) {
return textChannelForPostTarget.sendMessage(message.getEmbed()).submit();
} else {
return textChannelForPostTarget.sendMessage(messageText).embed(message.getEmbed()).submit();
}
}
@Override
public CompletableFuture<Message> editEmbedInPostTarget(Long messageId, MessageToSend message, PostTarget target) {
TextChannel textChannelForPostTarget = getTextChannelForPostTarget(target);
String messageText = message.getMessage();
if(StringUtils.isBlank(messageText)) {
return textChannelForPostTarget.editMessageById(messageId, message.getEmbed()).submit();
} else {
return textChannelForPostTarget.editMessageById(messageId, messageText).embed(message.getEmbed()).submit();
}
}
@Override
public void editOrCreatedInPostTarget(Long messageId, MessageToSend messageToSend, PostTarget target, CompletableFuture<Message> future) {
TextChannel textChannelForPostTarget = getTextChannelForPostTarget(target);
if(StringUtils.isBlank(messageToSend.getMessage().trim())) {
textChannelForPostTarget
.retrieveMessageById(messageId)
.queue(
existingMessage -> existingMessage
.editMessage(messageToSend.getEmbed())
.submit().thenAccept(future::complete),
throwable -> sendEmbedInPostTarget(messageToSend, target)
.thenAccept(future::complete));
} else {
textChannelForPostTarget
.retrieveMessageById(messageId)
.queue(
existingMessage -> existingMessage
.editMessage(messageToSend.getMessage())
.embed(messageToSend.getEmbed())
.submit().thenAccept(future::complete),
throwable -> sendEmbedInPostTarget(messageToSend, target)
.thenAccept(future::complete));
}
}
@Override
public void editOrCreatedInPostTarget(Long messageId, MessageToSend messageToSend, String postTargetName, Long serverId, CompletableFuture<Message> future) {
PostTarget postTarget = this.getPostTarget(postTargetName, serverId);
this.editOrCreatedInPostTarget(messageId, messageToSend, postTarget, future);
}
@Override
public CompletableFuture<Message> editEmbedInPostTarget(Long messageId, MessageToSend message, String postTargetName, Long serverId) {
PostTarget postTarget = this.getPostTarget(postTargetName, serverId);
return editEmbedInPostTarget(messageId, message, postTarget);
}
@Override

View File

@@ -1,6 +1,7 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.SnowflakeUtils;
import dev.sheldan.abstracto.core.listener.ServerConfigListener;
import dev.sheldan.abstracto.core.utils.SnowflakeUtils;
import dev.sheldan.abstracto.core.management.ChannelManagementService;
import dev.sheldan.abstracto.core.management.RoleManagementService;
import dev.sheldan.abstracto.core.management.ServerManagementService;
@@ -42,6 +43,9 @@ public class StartupManager implements Startup {
@Autowired
private RoleManagementService roleManagementService;
@Autowired
private List<ServerConfigListener> configListeners;
@Override
public void startBot() throws LoginException {
@@ -68,6 +72,9 @@ public class StartupManager implements Startup {
if(newGuild != null){
synchronizeRolesOf(newGuild, newAServer);
synchronizeChannelsOf(newGuild, newAServer);
configListeners.forEach(serverConfigListener -> {
serverConfigListener.updateServerConfig(newAServer);
});
}
});

View File

@@ -0,0 +1,91 @@
package dev.sheldan.abstracto.core.service.management;
import dev.sheldan.abstracto.core.management.ServerManagementService;
import dev.sheldan.abstracto.core.models.database.AConfig;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.management.ConfigManagementService;
import dev.sheldan.abstracto.repository.ConfigRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ConfigManagementServiceBean implements ConfigManagementService {
@Autowired
private ConfigRepository configRepository;
@Autowired
private ServerManagementService serverManagementService;
@Override
public void setOrCreateStringValue(Long serverId, String name, String value) {
AConfig config = loadConfig(serverId, name);
if(config == null) {
createConfig(serverId, name, value);
} else {
config.setStringValue(value);
configRepository.save(config);
}
}
@Override
public void setOrCreateDoubleValue(Long serverId, String name, Double value) {
AConfig config = loadConfig(serverId, name);
if(config == null) {
createConfig(serverId, name, value);
} else {
config.setDoubleValue(value);
configRepository.save(config);
}
}
@Override
public AConfig createConfig(Long serverId, String name, String value) {
AServer server = serverManagementService.loadServer(serverId);
AConfig config = AConfig
.builder()
.stringValue(value)
.server(server)
.name(name)
.build();
configRepository.save(config);
return config;
}
@Override
public AConfig createConfig(Long serverId, String name, Double value) {
AServer server = serverManagementService.loadServer(serverId);
AConfig config = AConfig
.builder()
.doubleValue(value)
.server(server)
.name(name)
.build();
configRepository.save(config);
return config;
}
@Override
public AConfig createIfNotExists(Long serverId, String name, String value) {
AConfig config = loadConfig(serverId, name);
if(config == null) {
return this.createConfig(serverId, name, value);
}
return config;
}
@Override
public AConfig createIfNotExists(Long serverId, String name, Double value) {
AConfig config = loadConfig(serverId, name);
if(config == null) {
return this.createConfig(serverId, name, value);
}
return config;
}
@Override
public AConfig loadConfig(Long serverId, String name) {
return configRepository.findAConfigByServerIdAndName(serverId, name);
}
}

View File

@@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
@Component
public class EmoteManagementServiceBean implements EmoteManagementService {
@@ -73,24 +74,25 @@ public class EmoteManagementServiceBean implements EmoteManagementService {
}
@Override
public AEmote loadEmoteByName(String name, Long serverId) {
public Optional<AEmote> loadEmoteByName(String name, Long serverId) {
AServer server = serverManagementService.loadServer(serverId);
return loadEmoteByName(name, server);
}
@Override
public AEmote loadEmoteByName(String name, AServer server) {
return repository.findAEmoteByNameAndServerRef(name, server);
public Optional<AEmote> loadEmoteByName(String name, AServer server) {
return Optional.ofNullable(repository.findAEmoteByNameAndServerRef(name, server));
}
@Override
public AEmote setEmoteToCustomEmote(String name, String emoteKey, Long emoteId, Boolean animated, Long serverId) {
AServer server = serverManagementService.loadServer(serverId);
AEmote emote;
if(!emoteExists(name, server)) {
Optional<AEmote> emoteOptional = loadEmoteByName(name, server);
if(!emoteOptional.isPresent()) {
emote = this.createCustomEmote(name, emoteKey, emoteId, animated, server);
} else {
emote = loadEmoteByName(name, server);
emote = emoteOptional.get();
emote.setEmoteKey(emoteKey);
emote.setEmoteId(emoteId);
emote.setAnimated(animated);
@@ -104,11 +106,14 @@ public class EmoteManagementServiceBean implements EmoteManagementService {
public AEmote setEmoteToCustomEmote(String name, Emote emote, Long serverId) {
AServer server = serverManagementService.loadServer(serverId);
AEmote emoteBeingSet;
if(!emoteExists(name, server)) {
emoteBeingSet = this.createDefaultEmote(name, emote.getName(), server);
Optional<AEmote> emoteOptional = loadEmoteByName(name, serverId);
if(!emoteOptional.isPresent()) {
emoteBeingSet = this.createCustomEmote(name, emote.getName(), emote.getIdLong(), emote.isAnimated(), server);
} else {
emoteBeingSet = loadEmoteByName(name, serverId);
emoteBeingSet.setCustom(false);
emoteBeingSet = emoteOptional.get();
emoteBeingSet.setCustom(true);
emoteBeingSet.setEmoteId(emote.getIdLong());
emoteBeingSet.setAnimated(emote.isAnimated());
emoteBeingSet.setEmoteKey(emote.getName());
repository.save(emoteBeingSet);
}
@@ -117,11 +122,18 @@ public class EmoteManagementServiceBean implements EmoteManagementService {
@Override
public AEmote setEmoteToDefaultEmote(String name, String emoteKey, Long serverId) {
AEmote existing = loadEmoteByName(name, serverId);
existing.setEmoteKey(emoteKey);
existing.setCustom(false);
repository.save(existing);
return existing;
AServer server = serverManagementService.loadServer(serverId);
AEmote emoteBeingSet;
Optional<AEmote> emoteOptional = loadEmoteByName(name, serverId);
if(!emoteOptional.isPresent()) {
emoteBeingSet = this.createDefaultEmote(name, emoteKey, server);
} else {
emoteBeingSet = emoteOptional.get();
emoteBeingSet.setEmoteKey(emoteKey);
emoteBeingSet.setCustom(false);
repository.save(emoteBeingSet);
}
return emoteBeingSet;
}
@Override

View File

@@ -10,6 +10,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@Slf4j
public class ServerManagementServiceBean implements ServerManagementService {
@@ -43,6 +45,8 @@ public class ServerManagementServiceBean implements ServerManagementService {
@Override
public void addChannelToServer(AServer server, AChannel channel) {
server.getChannels().add(channel);
channel.setServer(server);
repository.save(server);
}
@Override
@@ -83,5 +87,10 @@ public class ServerManagementServiceBean implements ServerManagementService {
return getPostTarget(server, target);
}
@Override
public List<AServer> getAllServers() {
return repository.findAll();
}
}

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto;
package dev.sheldan.abstracto.core.utils;
import dev.sheldan.abstracto.core.models.SnowFlake;
import net.dv8tion.jda.api.entities.ISnowflake;

View File

@@ -73,13 +73,11 @@ public class MessageEmbedListener extends ListenerAdapter {
Long serverIdLong = Long.parseLong(serverId);
Long channelIdLong = Long.parseLong(channelId);
Long messageIdLong = Long.parseLong(messageId);
try {
CachedMessage messageFromCache = messageCache.getMessageFromCache(serverIdLong, channelIdLong, messageIdLong);
messageRaw = messageRaw.replace(wholeLink, "");
self.createEmbedAndPostEmbed(event, messageFromCache);
} catch (ExecutionException | InterruptedException e) {
log.warn("Failed to load message from cache", e);
}
messageRaw = messageRaw.replace(wholeLink, "");
messageCache.getMessageFromCache(serverIdLong, channelIdLong, messageIdLong).thenAccept(cachedMessage -> {
self.createEmbedAndPostEmbed(event, cachedMessage);
});
}
}
if(StringUtils.isBlank(messageRaw) && matched) {
@@ -92,7 +90,11 @@ public class MessageEmbedListener extends ListenerAdapter {
public void createEmbedAndPostEmbed(@Nonnull GuildMessageReceivedEvent event, CachedMessage message) {
MessageEmbeddedModel messageEmbeddedModel = buildTemplateParameter(event, message);
MessageToSend embed = templateService.renderEmbedTemplate(MESSAGE_EMBED_TEMPLATE, messageEmbeddedModel);
event.getChannel().sendMessage(embed.getMessage()).embed(embed.getEmbed()).queue();
if(StringUtils.isBlank(embed.getMessage())) {
event.getChannel().sendMessage(embed.getEmbed()).queue();
} else {
event.getChannel().sendMessage(embed.getMessage()).embed(embed.getEmbed()).queue();
}
}
private MessageEmbeddedModel buildTemplateParameter(GuildMessageReceivedEvent event, CachedMessage embeddedMessage) {

View File

@@ -1,6 +1,6 @@
package dev.sheldan.abstracto.listener;
import dev.sheldan.abstracto.core.MessageTextUpdatedListener;
import dev.sheldan.abstracto.core.listener.MessageTextUpdatedListener;
import dev.sheldan.abstracto.core.models.CachedMessage;
import dev.sheldan.abstracto.core.service.MessageCache;
import lombok.extern.slf4j.Slf4j;
@@ -9,10 +9,10 @@ import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.concurrent.ExecutionException;
@Component
@Slf4j
@@ -24,17 +24,23 @@ public class MessageUpdatedListener extends ListenerAdapter {
@Autowired
private MessageCache messageCache;
@Autowired
private MessageUpdatedListener self;
@Override
public void onGuildMessageUpdate(@Nonnull GuildMessageUpdateEvent event) {
Message message = event.getMessage();
try {
CachedMessage fromCache = messageCache.getMessageFromCache(message.getGuild().getIdLong(), message.getTextChannel().getIdLong(), event.getMessageIdLong());
listener.forEach(messageTextUpdatedListener -> {
messageTextUpdatedListener.execute(fromCache, message);
});
messageCache.getMessageFromCache(message.getGuild().getIdLong(), message.getTextChannel().getIdLong(), event.getMessageIdLong()).thenAccept(cachedMessage -> {
self.executeListener(message, cachedMessage);
messageCache.putMessageInCache(message);
} catch (ExecutionException | InterruptedException e) {
log.warn("Failed to load message", e);
}
});
}
@Transactional
public void executeListener(Message message, CachedMessage cachedMessage) {
listener.forEach(messageTextUpdatedListener -> {
messageTextUpdatedListener.execute(cachedMessage, message);
});
}
}

View File

@@ -0,0 +1,137 @@
package dev.sheldan.abstracto.listener;
import dev.sheldan.abstracto.core.listener.ReactedAddedListener;
import dev.sheldan.abstracto.core.listener.ReactedRemovedListener;
import dev.sheldan.abstracto.core.management.UserManagementService;
import dev.sheldan.abstracto.core.models.CachedMessage;
import dev.sheldan.abstracto.core.models.CachedReaction;
import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.EmoteService;
import dev.sheldan.abstracto.core.service.MessageCache;
import dev.sheldan.abstracto.core.utils.EmoteUtils;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionAddEvent;
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveAllEvent;
import net.dv8tion.jda.api.events.message.guild.react.GuildMessageReactionRemoveEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Component
@Slf4j
public class ReactionUpdatedListener extends ListenerAdapter {
@Autowired
private MessageCache messageCache;
@Autowired
private EmoteService emoteService;
@Autowired
private UserManagementService userManagementService;
@Autowired
private List<ReactedAddedListener> addedListenerList;
@Autowired
private List<ReactedRemovedListener> reactionRemovedListener;
@Autowired
private ReactionUpdatedListener self;
@Override
@Transactional
public void onGuildMessageReactionAdd(@Nonnull GuildMessageReactionAddEvent event) {
CompletableFuture<CachedMessage> asyncMessageFromCache = messageCache.getMessageFromCache(event.getGuild().getIdLong(), event.getChannel().getIdLong(), event.getMessageIdLong());
asyncMessageFromCache.thenAccept(cachedMessage -> {
CompletableFuture<CachedReaction> future = new CompletableFuture<>();
messageCache.getCachedReactionFromReaction(future, event.getReaction());
future.thenAccept(reaction -> {
self.callAddedListeners(event, cachedMessage, reaction);
messageCache.putMessageInCache(cachedMessage);
});
});
}
private void addReactionIfNotThere(CachedMessage message, CachedReaction reaction, AUser userReacting) {
Optional<CachedReaction> existingReaction = message.getReactions().stream().filter(reaction1 -> {
return EmoteUtils.compareAEmote(reaction1.getEmote(), reaction.getEmote());
}).findAny();
if(!existingReaction.isPresent()) {
message.getReactions().add(reaction);
} else {
CachedReaction cachedReaction = existingReaction.get();
Optional<AUser> any = cachedReaction.getUsers().stream().filter(user -> user.getId().equals(userReacting.getId())).findAny();
if(!any.isPresent()){
cachedReaction.getUsers().add(userReacting);
}
}
}
private void removeReactionIfThere(CachedMessage message, CachedReaction reaction, AUser userReacting) {
Optional<CachedReaction> existingReaction = message.getReactions().stream().filter(reaction1 -> {
return EmoteUtils.compareAEmote(reaction1.getEmote(), reaction.getEmote());
}).findAny();
if(existingReaction.isPresent()) {
CachedReaction cachedReaction = existingReaction.get();
cachedReaction.getUsers().removeIf(user -> user.getId().equals(userReacting.getId()));
message.getReactions().removeIf(reaction1 -> reaction1.getUsers().isEmpty());
}
}
@Transactional
public void callAddedListeners(@Nonnull GuildMessageReactionAddEvent event, CachedMessage cachedMessage, CachedReaction reaction) {
AUserInAServer userInAServer = userManagementService.loadUser(event.getGuild().getIdLong(), event.getUserIdLong());
addReactionIfNotThere(cachedMessage, reaction, userInAServer.getUserReference());
try {
addedListenerList.forEach(reactedAddedListener -> {
reactedAddedListener.executeReactionAdded(cachedMessage, event.getReaction(), userInAServer);
});
} catch (Exception e) {
log.warn("Exception on reaction added handler:", e);
}
}
@Override
@Transactional
public void onGuildMessageReactionRemove(@Nonnull GuildMessageReactionRemoveEvent event) {
CompletableFuture<CachedMessage> asyncMessageFromCache = messageCache.getMessageFromCache(event.getGuild().getIdLong(), event.getChannel().getIdLong(), event.getMessageIdLong());
asyncMessageFromCache.thenAccept(cachedMessage -> {
CompletableFuture<CachedReaction> future = new CompletableFuture<>();
messageCache.getCachedReactionFromReaction(future, event.getReaction());
future.thenAccept(reaction -> {
self.callRemoveListeners(event, cachedMessage, reaction);
});
messageCache.putMessageInCache(cachedMessage);
});
}
@Transactional
public void callRemoveListeners(@Nonnull GuildMessageReactionRemoveEvent event, CachedMessage cachedMessage, CachedReaction reaction) {
AUserInAServer userInAServer = userManagementService.loadUser(event.getGuild().getIdLong(), event.getUserIdLong());
removeReactionIfThere(cachedMessage, reaction, userInAServer.getUserReference());
reactionRemovedListener.forEach(reactedAddedListener -> {
reactedAddedListener.executeReactionRemoved(cachedMessage, event.getReaction(), userInAServer);
});
}
@Override
@Transactional
public void onGuildMessageReactionRemoveAll(@Nonnull GuildMessageReactionRemoveAllEvent event) {
CompletableFuture<CachedMessage> asyncMessageFromCache = messageCache.getMessageFromCache(event.getGuild().getIdLong(), event.getChannel().getIdLong(), event.getMessageIdLong());
asyncMessageFromCache.thenAccept(cachedMessage -> {
cachedMessage.getReactions().clear();
messageCache.putMessageInCache(cachedMessage);
});
}
}

View File

@@ -0,0 +1,8 @@
package dev.sheldan.abstracto.repository;
import dev.sheldan.abstracto.core.models.database.AConfig;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ConfigRepository extends JpaRepository<AConfig, Long> {
AConfig findAConfigByServerIdAndName(Long serverId, String name);
}

View File

@@ -1,7 +1,7 @@
{
"author": {
"name": "${author.effectiveName}",
"avatar": "${author.user.avatarUrl}"
"avatar": "${author.user.effectiveAvatarUrl}"
},
"color" : {
"r": 200,

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.core;
package dev.sheldan.abstracto.core.listener;
import dev.sheldan.abstracto.core.models.CachedMessage;
import net.dv8tion.jda.api.entities.Message;

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.core.listener;
import dev.sheldan.abstracto.core.models.CachedMessage;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import net.dv8tion.jda.api.entities.MessageReaction;
public interface ReactedAddedListener {
void executeReactionAdded(CachedMessage message, MessageReaction reaction, AUserInAServer userAdding);
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.core.listener;
import dev.sheldan.abstracto.core.models.CachedMessage;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import net.dv8tion.jda.api.entities.MessageReaction;
public interface ReactedRemovedListener {
void executeReactionRemoved(CachedMessage message, MessageReaction reaction, AUserInAServer userRemoving);
}

View File

@@ -0,0 +1,7 @@
package dev.sheldan.abstracto.core.listener;
import dev.sheldan.abstracto.core.models.database.AServer;
public interface ServerConfigListener {
void updateServerConfig(AServer server);
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.core.management;
import dev.sheldan.abstracto.core.models.database.AConfig;
public interface ConfigManagementService {
void setOrCreateStringValue(Long serverId, String name, String value);
void setOrCreateDoubleValue(Long serverId, String name, Double value);
AConfig createConfig(Long serverId, String name, String value);
AConfig createConfig(Long serverId, String name, Double value);
AConfig createIfNotExists(Long serverId, String name, String value);
AConfig createIfNotExists(Long serverId, String name, Double value);
AConfig loadConfig(Long serverId, String name);
}

View File

@@ -4,14 +4,16 @@ import dev.sheldan.abstracto.core.models.database.AEmote;
import dev.sheldan.abstracto.core.models.database.AServer;
import net.dv8tion.jda.api.entities.Emote;
import java.util.Optional;
public interface EmoteManagementService {
AEmote loadEmote(Long id);
AEmote createCustomEmote(String name, String emoteKey, Long emoteId, Boolean animated, Long serverId);
AEmote createCustomEmote(String name, String emoteKey, Long emoteId, Boolean animated, AServer server);
AEmote createDefaultEmote(String name, String emoteKey, Long serverId);
AEmote createDefaultEmote(String name, String emoteKey, AServer server);
AEmote loadEmoteByName(String name, Long serverId);
AEmote loadEmoteByName(String name, AServer server);
Optional<AEmote> loadEmoteByName(String name, Long serverId);
Optional<AEmote> loadEmoteByName(String name, AServer server);
AEmote setEmoteToCustomEmote(String name, String emoteKey, Long emoteId, Boolean animated, Long serverId);
AEmote setEmoteToCustomEmote(String name, Emote emote, Long serverId);
AEmote setEmoteToDefaultEmote(String name, String emoteKey, Long serverId);

View File

@@ -2,6 +2,8 @@ package dev.sheldan.abstracto.core.management;
import dev.sheldan.abstracto.core.models.database.*;
import java.util.List;
public interface ServerManagementService {
AServer createServer(Long id);
AServer loadServer(Long id);
@@ -12,4 +14,5 @@ public interface ServerManagementService {
AChannel getPostTarget(Long serverId, PostTarget target);
AChannel getPostTarget(AServer server, PostTarget target);
AChannel getPostTarget(AServer server, String name);
List<AServer> getAllServers();
}

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.core.models;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class AServerChannelMessage {
private AServer server;
private AChannel channel;
private Long messageId;
}

View File

@@ -6,6 +6,7 @@ import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.List;
@@ -21,6 +22,7 @@ public class CachedMessage {
private String content;
private List<CachedEmbed> embeds;
private List<String> attachmentUrls;
private List<CachedReaction> reactions;
public String getMessageUrl() {
return MessageUtils.buildMessageUrl(this.serverId ,this.channelId, this.messageId);

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.core.models;
import dev.sheldan.abstracto.core.models.database.AEmote;
import dev.sheldan.abstracto.core.models.database.AUser;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
@Builder
public class CachedReaction {
private AEmote emote;
private List<AUser> users;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.core.models;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class ServerChannelMessage {
private Long serverId;
private Long channelId;
private Long messageId;
}

View File

@@ -17,6 +17,7 @@ public class AChannel implements SnowFlake {
@Id
@Getter
@Column(name = "id")
public Long id;
@Getter
@@ -24,7 +25,9 @@ public class AChannel implements SnowFlake {
private Set<AChannelGroup> groups;
@ManyToOne(fetch = FetchType.LAZY)
@Getter @Setter
@Getter
@Setter
@JoinColumn(name = "server_id")
private AServer server;
@Getter

View File

@@ -0,0 +1,35 @@
package dev.sheldan.abstracto.core.models.database;
import lombok.*;
import javax.persistence.*;
@Entity
@Table(name="systemConfig")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class AConfig {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer Id;
@Column
private String name;
@Column
@Setter
private String stringValue;
@Column
@Setter
private Double doubleValue;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "server_id", nullable = false)
@Getter
@Setter
private AServer server;
}

View File

@@ -16,6 +16,7 @@ import java.util.List;
public class AServer implements SnowFlake {
@Id
@Column(name = "id")
private Long id;
private String name;
@@ -26,10 +27,10 @@ public class AServer implements SnowFlake {
@OneToMany(
fetch = FetchType.LAZY,
mappedBy = "server",
cascade = CascadeType.ALL,
orphanRemoval = true)
@Builder.Default
@JoinColumn(name = "server_id")
private List<AChannel> channels = new ArrayList<>();
@OneToMany(

View File

@@ -1,7 +1,9 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.models.ServerChannelUser;
import dev.sheldan.abstracto.core.models.database.AEmote;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.TextChannel;
@@ -15,6 +17,8 @@ public interface Bot {
JDA getInstance();
ServerChannelUser getServerChannelUser(Long serverId, Long channelId, Long userId);
Member getMemberInServer(Long serverId, Long memberId);
void deleteMessage(Long serverId, Long channelId, Long messageId);
Emote getEmote(Long serverId, AEmote emote);
TextChannel getTextChannelFromServer(Long serverId, Long textChannelId);
Guild getGuildById(Long serverId);
void shutdown();

View File

@@ -0,0 +1,7 @@
package dev.sheldan.abstracto.core.service;
public interface ConfigService {
Double getDoubleValue(String name, Long serverId);
Double getDoubleValue(String name, Long serverId, Double defaultValue);
}

View File

@@ -1,7 +1,12 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.models.database.AEmote;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.MessageReaction;
public interface EmoteService {
boolean isEmoteUsableByBot(Emote emote);
AEmote buildAEmoteFromReaction(MessageReaction.ReactionEmote reaction);
String getEmoteAsMention(AEmote emote, Long serverId, String defaultText);
String getEmoteAsMention(AEmote emote, Long serverId);
}

View File

@@ -1,14 +1,19 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.models.CachedMessage;
import dev.sheldan.abstracto.core.models.CachedReaction;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageReaction;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public interface MessageCache {
CachedMessage putMessageInCache(Message message);
CachedMessage getMessageFromCache(Message message) throws ExecutionException, InterruptedException;
CachedMessage getMessageFromCache(Long guildId, Long textChannelId, Long messageId) throws ExecutionException, InterruptedException;
CompletableFuture<Message> getMessage(Long serverId, Long textChannelId, Long messageId);
CompletableFuture<CachedMessage> putMessageInCache(Message message);
CompletableFuture<CachedMessage> getMessageFromCache(Long guildId, Long textChannelId, Long messageId);
CompletableFuture<CachedMessage> getMessageFromCache(Message message);
CompletableFuture<CachedMessage> putMessageInCache(CachedMessage message);
void loadMessage(CompletableFuture<CachedMessage> future, Long guildId, Long textChannelId, Long messageId);
void getCachedReactionFromReaction(CompletableFuture<CachedReaction> future, MessageReaction reaction);
void buildCachedMessageFromMessage(CompletableFuture<CachedMessage> future, Message message);
}

View File

@@ -15,6 +15,10 @@ public interface PostTargetService {
CompletableFuture<Message> sendEmbedInPostTarget(MessageEmbed embed, String postTargetName, Long serverId);
CompletableFuture<Message> sendEmbedInPostTarget(MessageToSend message, String postTargetName, Long serverId);
CompletableFuture<Message> sendEmbedInPostTarget(MessageToSend message, PostTarget target);
CompletableFuture<Message> editEmbedInPostTarget(Long messageId, MessageToSend message, PostTarget target);
CompletableFuture<Message> editEmbedInPostTarget(Long messageId, MessageToSend message, String postTargetName, Long serverId);
void editOrCreatedInPostTarget(Long messageId, MessageToSend messageToSend, PostTarget target, CompletableFuture<Message> future);
void editOrCreatedInPostTarget(Long messageId, MessageToSend messageToSend, String postTarget, Long serverId, CompletableFuture<Message> future);
boolean validPostTarget(String name);
List<String> getAvailablePostTargets();
}

View File

@@ -0,0 +1,42 @@
package dev.sheldan.abstracto.core.utils;
import dev.sheldan.abstracto.core.models.CachedMessage;
import dev.sheldan.abstracto.core.models.CachedReaction;
import dev.sheldan.abstracto.core.models.database.AEmote;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.MessageReaction;
import java.util.Optional;
public class EmoteUtils {
public static boolean isReactionEmoteAEmote(MessageReaction.ReactionEmote reaction, AEmote emote, Optional<Emote> emoteInGuildOptional) {
if(reaction.isEmote() && emote.getCustom()) {
if(emoteInGuildOptional.isPresent()) {
Emote emoteInGuild = emoteInGuildOptional.get();
return emoteInGuild.equals(reaction.getEmote());
} else {
return false;
}
} else {
return reaction.getEmoji().equals(emote.getEmoteKey());
}
}
public static Optional<CachedReaction> getReactionFromMessageByEmote(CachedMessage message, AEmote emote) {
return message.getReactions().stream().filter(reaction -> compareAEmote(reaction.getEmote(), emote)).findFirst();
}
public static boolean compareAEmote(AEmote a, AEmote b) {
if(a.getCustom() && b.getCustom()) {
return a.getEmoteId().equals(b.getEmoteId());
} else {
if(!a.getCustom() && !b.getCustom()) {
return a.getEmoteKey().equals(b.getEmoteKey());
} else {
return false;
}
}
}
}