added scheduling support

added remind command
added support for parameters with spaces (they are contained by ")
fixed support for remainder parameters
added maxlength support for parameters
added ability to embed templates, to have a text as well
moved properties to a more appropriate position
added method do parse a duration
This commit is contained in:
Sheldan
2020-03-28 20:12:59 +01:00
parent e0474a4c98
commit 03e81a025b
64 changed files with 1318 additions and 125 deletions

View File

@@ -6,9 +6,9 @@ import dev.sheldan.abstracto.core.service.MessageCache;
import dev.sheldan.abstracto.core.service.PostTargetService;
import dev.sheldan.abstracto.moderation.models.template.listener.MessageDeletedAttachmentLog;
import dev.sheldan.abstracto.moderation.models.template.listener.MessageDeletedLog;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import dev.sheldan.abstracto.templating.TemplateService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.message.guild.GuildMessageDeleteEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
@@ -53,13 +53,13 @@ public class MessageDeletedListener extends ListenerAdapter {
logModel.setMessage(messageFromCache);
String simpleMessageUpdatedMessage = templateService.renderTemplate(MESSAGE_DELETED_TEMPLATE, logModel);
postTargetService.sendTextInPostTarget(simpleMessageUpdatedMessage, DELETE_LOG_TARGET, event.getGuild().getIdLong());
MessageEmbed embed = templateService.renderEmbedTemplate(MESSAGE_DELETED_TEMPLATE, logModel);
postTargetService.sendEmbedInPostTarget(embed, DELETE_LOG_TARGET, event.getGuild().getIdLong());
MessageToSend message = templateService.renderEmbedTemplate(MESSAGE_DELETED_TEMPLATE, logModel);
postTargetService.sendEmbedInPostTarget(message, DELETE_LOG_TARGET, event.getGuild().getIdLong());
for (int i = 0; i < messageFromCache.getAttachmentUrls().size(); i++) {
MessageDeletedAttachmentLog log = (MessageDeletedAttachmentLog) contextUtils.fromMessage(messageFromCache, MessageDeletedAttachmentLog.class);
log.setImageUrl(messageFromCache.getAttachmentUrls().get(i));
log.setCounter(i + 1);
MessageEmbed attachmentEmbed = templateService.renderEmbedTemplate(MESSAGE_DELETED_ATTACHMENT_TEMPLATE, log);
MessageToSend attachmentEmbed = templateService.renderEmbedTemplate(MESSAGE_DELETED_ATTACHMENT_TEMPLATE, log);
postTargetService.sendEmbedInPostTarget(attachmentEmbed, DELETE_LOG_TARGET, event.getGuild().getIdLong());
}
}

View File

@@ -2,13 +2,13 @@ package dev.sheldan.abstracto.moderation.listener;
import dev.sheldan.abstracto.core.MessageTextUpdatedListener;
import dev.sheldan.abstracto.core.models.CachedMessage;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import dev.sheldan.abstracto.core.service.MessageCache;
import dev.sheldan.abstracto.core.service.PostTargetService;
import dev.sheldan.abstracto.moderation.models.template.listener.MessageEditedLog;
import dev.sheldan.abstracto.templating.TemplateService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -45,8 +45,8 @@ public class MessageEditedListener implements MessageTextUpdatedListener {
.member(messageAfter.getMember()).build();
String simpleMessageUpdatedMessage = templateService.renderTemplate(MESSAGE_EDITED_TEMPLATE, log);
postTargetService.sendTextInPostTarget(simpleMessageUpdatedMessage, EDIT_LOG_TARGET, messageAfter.getGuild().getIdLong());
MessageEmbed embed = templateService.renderEmbedTemplate(MESSAGE_EDITED_TEMPLATE, log);
postTargetService.sendEmbedInPostTarget(embed, EDIT_LOG_TARGET, messageBefore.getServerId());
MessageToSend message = templateService.renderEmbedTemplate(MESSAGE_EDITED_TEMPLATE, log);
postTargetService.sendEmbedInPostTarget(message, EDIT_LOG_TARGET, messageBefore.getServerId());
}
}

View File

@@ -3,6 +3,7 @@ package dev.sheldan.abstracto.moderation.service;
import dev.sheldan.abstracto.core.models.ServerContext;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import dev.sheldan.abstracto.moderation.models.template.WarnLog;
import dev.sheldan.abstracto.moderation.models.template.WarnNotification;
import dev.sheldan.abstracto.moderation.models.Warning;
@@ -17,7 +18,6 @@ import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -86,7 +86,7 @@ public class WarnServiceBean implements WarnService {
private void sendWarnLog(ServerContext warnLogModel) {
String warnLogMessage = templateService.renderContextAwareTemplate(WARN_LOG_TEMPLATE, warnLogModel);
postTargetService.sendTextInPostTarget(warnLogMessage, WARN_LOG_TARGET, warnLogModel.getServer().getId());
MessageEmbed embed = templateService.renderEmbedTemplate("warn_log", warnLogModel);
postTargetService.sendEmbedInPostTarget(embed, WARN_LOG_TARGET, warnLogModel.getServer().getId());
MessageToSend message = templateService.renderEmbedTemplate("warn_log", warnLogModel);
postTargetService.sendEmbedInPostTarget(message, WARN_LOG_TARGET, warnLogModel.getServer().getId());
}
}

View File

@@ -17,6 +17,12 @@
<artifactId>utility-int</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling-int</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,52 @@
package dev.sheldan.abstracto.utility.command.remind;
import dev.sheldan.abstracto.command.Command;
import dev.sheldan.abstracto.command.HelpInfo;
import dev.sheldan.abstracto.command.execution.*;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.utility.Utility;
import dev.sheldan.abstracto.utility.models.template.ReminderModel;
import dev.sheldan.abstracto.utility.service.ReminderService;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
@Component
public class Remind implements Command {
@Autowired
private ReminderService remindService;
@Override
public Result execute(CommandContext commandContext) {
List<Object> parameters = commandContext.getParameters().getParameters();
Duration remindTime = (Duration) parameters.get(0);
String text = (String) parameters.get(1);
AUserInAServer aUserInAServer = commandContext.getUserInitiatedContext().getAUserInAServer();
ReminderModel remindModel = (ReminderModel) ContextConverter.fromCommandContext(commandContext, ReminderModel.class);
remindModel.setMessage(commandContext.getMessage());
remindModel.setRemindText(text);
remindService.createReminderInForUser(aUserInAServer, text, remindTime, remindModel);
return Result.fromSuccess();
}
@Override
public CommandConfiguration getConfiguration() {
List<Parameter> parameters = new ArrayList<>();
parameters.add(Parameter.builder().name("remindTime").type(Duration.class).build());
parameters.add(Parameter.builder().name("remindText").type(String.class).maxLength(MessageEmbed.TEXT_MAX_LENGTH).remainder(true).build());
HelpInfo helpInfo = HelpInfo.builder().templated(true).build();
return CommandConfiguration.builder()
.name("remind")
.module(Utility.UTILITY)
.templated(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)
.build();
}
}

View File

@@ -0,0 +1,37 @@
package dev.sheldan.abstracto.utility.jobs;
import dev.sheldan.abstracto.utility.service.ReminderService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
@Slf4j
@DisallowConcurrentExecution
@Component
@PersistJobDataAfterExecution
public class ReminderJob extends QuartzJobBean {
private Long reminderId;
@Autowired
private ReminderService reminderService;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
reminderService.executeReminder(reminderId);
log.info("executing reminder job for reminder {}", reminderId);
}
public Long getReminderId() {
return reminderId;
}
public void setReminderId(Long reminderId) {
this.reminderId = reminderId;
}
}

View File

@@ -0,0 +1,8 @@
package dev.sheldan.abstracto.utility.repository;
import dev.sheldan.abstracto.utility.models.Reminder;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ReminderRepository extends JpaRepository<Reminder, Long> {
}

View File

@@ -0,0 +1,101 @@
package dev.sheldan.abstracto.utility.service;
import dev.sheldan.abstracto.core.management.ChannelManagementService;
import dev.sheldan.abstracto.core.models.AServerAChannelAUser;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import dev.sheldan.abstracto.core.service.Bot;
import dev.sheldan.abstracto.scheduling.model.SchedulerService;
import dev.sheldan.abstracto.templating.TemplateService;
import dev.sheldan.abstracto.utility.models.Reminder;
import dev.sheldan.abstracto.utility.models.template.ExecutedReminderModel;
import dev.sheldan.abstracto.utility.models.template.ReminderModel;
import dev.sheldan.abstracto.utility.service.management.ReminderManagementService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.TextChannel;
import org.quartz.JobDataMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class RemindServiceBean implements ReminderService {
public static final String REMINDER_EMBED_KEY = "remind_response";
@Autowired
private ReminderManagementService reminderManagementService;
@Autowired
private ChannelManagementService channelManagementService;
@Autowired
private TemplateService templateService;
@Autowired
private SchedulerService schedulerService;
@Autowired
private Bot bot;
@Autowired
private ReminderService self;
@Override
public void createReminderInForUser(AUserInAServer user, String remindText, Duration remindIn, ReminderModel reminderModel) {
AChannel channel = channelManagementService.loadChannel(reminderModel.getChannel().getId());
AServerAChannelAUser aServerAChannelAUser = AServerAChannelAUser
.builder()
.user(user.getUserReference())
.aUserInAServer(user)
.guild(user.getServerReference())
.channel(channel)
.build();
Instant remindAt = Instant.now().plusNanos(remindIn.toNanos());
Reminder reminder = reminderManagementService.createReminder(aServerAChannelAUser, remindText, remindAt, reminderModel.getMessage().getIdLong());
reminderModel.setReminder(reminder);
MessageToSend message = templateService.renderEmbedTemplate(REMINDER_EMBED_KEY, reminderModel);
reminderModel.getTextChannel().sendMessage(message.getMessage()).embed(message.getEmbed()).queue();
if(remindIn.getSeconds() < 60) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> {
self.executeReminder(reminder.getId());
}, remindIn.toNanos(), TimeUnit.NANOSECONDS);
} else {
JobDataMap parameters = new JobDataMap();
parameters.putAsString("reminderId", reminder.getId());
schedulerService.executeJobWithParametersOnce("reminderJob", "utility", parameters, Date.from(reminder.getTargetDate()));
}
}
@Override
@Transactional
public void executeReminder(Long reminderId) {
Reminder reminderToRemindFor = reminderManagementService.loadReminder(reminderId);
AServer server = reminderToRemindFor.getServer();
AChannel channel = reminderToRemindFor.getChannel();
AUser userReference = reminderToRemindFor.getToBeReminded().getUserReference();
Member memberInServer = bot.getMemberInServer(server.getId(), userReference.getId());
ExecutedReminderModel build = ExecutedReminderModel
.builder()
.reminder(reminderToRemindFor)
.member(memberInServer)
.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate("remind_reminder", build);
// todo, if channel does not exist anymore
TextChannel channelToAnswerIn = bot.getTextChannelFromServer(server.getId(), channel.getId());
channelToAnswerIn.sendMessage(messageToSend.getMessage()).embed(messageToSend.getEmbed()).queue();
}
}

View File

@@ -2,6 +2,7 @@ package dev.sheldan.abstracto.utility.service;
import dev.sheldan.abstracto.core.management.EmoteManagementService;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import dev.sheldan.abstracto.core.service.Bot;
import dev.sheldan.abstracto.core.service.MessageService;
import dev.sheldan.abstracto.core.service.PostTargetService;
@@ -55,12 +56,12 @@ public class SuggestionServiceBean implements SuggestionService {
Suggestion suggestion = suggestionManagementService.createSuggestion(member, text);
suggestionLog.setSuggestion(suggestion);
suggestionLog.setText(text);
MessageEmbed embed = templateService.renderEmbedTemplate(SUGGESTION_LOG_TEMPLATE, suggestionLog);
MessageToSend messageToSend = templateService.renderEmbedTemplate(SUGGESTION_LOG_TEMPLATE, suggestionLog);
long guildId = member.getGuild().getIdLong();
JDA instance = botService.getInstance();
Guild guildById = instance.getGuildById(guildId);
if(guildById != null) {
postTargetService.sendEmbedInPostTarget(embed, SUGGESTIONS_TARGET, guildId).thenAccept(message -> {
postTargetService.sendEmbedInPostTarget(messageToSend, SUGGESTIONS_TARGET, guildId).thenAccept(message -> {
messageService.addReactionToMessage(SUGGESTION_YES_EMOTE, guildId, message);
messageService.addReactionToMessage(SUGGESTION_NO_EMOTE, guildId, message);
suggestionManagementService.setPostedMessage(suggestion, message);
@@ -110,8 +111,8 @@ public class SuggestionServiceBean implements SuggestionService {
MessageEmbed suggestionEmbed = embedOptional.get();
suggestionLog.setReason(text);
suggestionLog.setText(suggestionEmbed.getDescription());
MessageEmbed embed = templateService.renderEmbedTemplate(SUGGESTION_LOG_TEMPLATE, suggestionLog);
postTargetService.sendEmbedInPostTarget(embed, SUGGESTIONS_TARGET, suggestionLog.getServer().getId());
MessageToSend messageToSend = templateService.renderEmbedTemplate(SUGGESTION_LOG_TEMPLATE, suggestionLog);
postTargetService.sendEmbedInPostTarget(messageToSend, SUGGESTIONS_TARGET, suggestionLog.getServer().getId());
}
}

View File

@@ -0,0 +1,39 @@
package dev.sheldan.abstracto.utility.service.management;
import dev.sheldan.abstracto.core.models.AServerAChannelAUser;
import dev.sheldan.abstracto.utility.models.Reminder;
import dev.sheldan.abstracto.utility.repository.ReminderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Component
public class ReminderManagementServiceBean implements ReminderManagementService {
@Autowired
private ReminderRepository reminderRepository;
@Override
public Reminder createReminder(AServerAChannelAUser userToBeReminded, String text, Instant timeToBeRemindedAt, Long messageId) {
Reminder reminder = Reminder.builder()
.channel(userToBeReminded.getChannel())
.server(userToBeReminded.getGuild())
.toBeReminded(userToBeReminded.getAUserInAServer())
.reminded(false)
.text(text)
.reminderDate(Instant.now())
.targetDate(timeToBeRemindedAt)
.messageId(messageId)
.build();
reminderRepository.save(reminder);
return reminder;
}
@Override
public Reminder loadReminder(Long reminderId) {
return reminderRepository.getOne(reminderId);
}
}

View File

@@ -0,0 +1 @@
Reminds you in the given period of the given text.

View File

@@ -0,0 +1,13 @@
{
"author": {
"name": "${member.effectiveName}",
"avatar": "${member.user.effectiveAvatarUrl}"
},
"color" : {
"r": 200,
"g": 0,
"b": 255
},
"description": "You wanted to be reminded of: '${reminder.text}'. Original message was [here](${messageUrl}).",
"additionalMessage": "${member.asMention}"
}

View File

@@ -0,0 +1,13 @@
{
"author": {
"name": "${member.effectiveName}",
"avatar": "${member.user.effectiveAvatarUrl}"
},
"color" : {
"r": 200,
"g": 0,
"b": 255
},
"description": "Scheduled reminder ${reminder.id} to remind you of [this](${message.jumpUrl})",
"additionalMessage": "${member.asMention}"
}

View File

@@ -1,2 +1,8 @@
abstracto.postTargets.utility=suggestions
abstracto.emoteNames.suggestion=SUGGESTION_YES,SUGGESTION_NO
abstracto.emoteNames.suggestion=SUGGESTION_YES,SUGGESTION_NO
abstracto.scheduling.jobs.reminderJob.name=reminderJob
abstracto.scheduling.jobs.reminderJob.group=utility
abstracto.scheduling.jobs.reminderJob.clazz=dev.sheldan.abstracto.utility.jobs.ReminderJob
abstracto.scheduling.jobs.reminderJob.standAlone=false
abstracto.scheduling.jobs.reminderJob.active=true

View File

@@ -0,0 +1,54 @@
package dev.sheldan.abstracto.utility.models;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.*;
import javax.persistence.*;
import java.time.Instant;
@Entity
@Table(name="reminder")
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter @Setter
public class Reminder {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Getter
private Long id;
@Getter
@ManyToOne
@JoinColumn(name = "remindedUser")
private AUserInAServer toBeReminded;
@Getter
private Long messageId;
@Getter
@ManyToOne
@JoinColumn(name = "channelId")
private AChannel channel;
@Getter
@ManyToOne
@JoinColumn(name = "serverId")
private AServer server;
@Getter
private Instant reminderDate;
@Getter
private Instant targetDate;
@Getter
private String text;
@Getter
private boolean reminded;
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.utility.models.template;
import dev.sheldan.abstracto.core.models.ServerContext;
import dev.sheldan.abstracto.core.utils.MessageUtils;
import dev.sheldan.abstracto.utility.models.Reminder;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.dv8tion.jda.api.entities.Member;
@Getter
@Setter
@SuperBuilder
public class ExecutedReminderModel extends ServerContext {
private Reminder reminder;
private Member member;
public String getMessageUrl() {
return MessageUtils.buildMessageUrl(this.reminder.getServer().getId() ,this.reminder.getChannel().getId(), this.reminder.getMessageId());
}
}

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.utility.models.template;
import dev.sheldan.abstracto.core.models.UserInitiatedServerContext;
import dev.sheldan.abstracto.utility.models.Reminder;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.SuperBuilder;
import net.dv8tion.jda.api.entities.Message;
@Getter
@Setter
@SuperBuilder
public class ReminderModel extends UserInitiatedServerContext {
private String remindText;
private Reminder reminder;
private Message message;
}

View File

@@ -0,0 +1,11 @@
package dev.sheldan.abstracto.utility.service;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.utility.models.template.ReminderModel;
import java.time.Duration;
public interface ReminderService {
void createReminderInForUser(AUserInAServer user, String remindText, Duration remindIn, ReminderModel reminderModel);
void executeReminder(Long reminderId);
}

View File

@@ -0,0 +1,11 @@
package dev.sheldan.abstracto.utility.service.management;
import dev.sheldan.abstracto.core.models.AServerAChannelAUser;
import dev.sheldan.abstracto.utility.models.Reminder;
import java.time.Instant;
public interface ReminderManagementService {
Reminder createReminder(AServerAChannelAUser userToBeReminded, String text, Instant timeToBeRemindedAt, Long messageId);
Reminder loadReminder(Long reminderId);
}

View File

@@ -11,4 +11,5 @@ public class Parameter {
private String description;
private boolean optional;
private boolean remainder;
private Integer maxLength;
}

View File

@@ -1,11 +1,38 @@
package dev.sheldan.abstracto.command.meta;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Getter @Setter
@Getter
public class UnParsedCommandParameter {
private static Pattern SPLIT_REGEX = Pattern.compile("\"([^\"]*)\"|(\\S+)");
public UnParsedCommandParameter(String parameters) {
this.parameters = new ArrayList<>();
Matcher m = SPLIT_REGEX.matcher(parameters);
boolean skippedCommand = false;
while (m.find()) {
if(!skippedCommand) {
skippedCommand = true;
continue;
}
if (m.group(1) != null) {
String group = m.group(1);
if(!group.equals("")) {
this.parameters.add(group);
}
} else {
String group = m.group(2);
if(!group.equals("")) {
this.parameters.add(group);
}
}
}
}
private List<String> parameters;
}

View File

@@ -5,6 +5,8 @@ import dev.sheldan.abstracto.command.PostCommandExecution;
import dev.sheldan.abstracto.command.execution.*;
import dev.sheldan.abstracto.command.meta.UnParsedCommandParameter;
import dev.sheldan.abstracto.commands.management.exception.IncorrectParameterException;
import dev.sheldan.abstracto.commands.management.exception.ParameterTooLongException;
import dev.sheldan.abstracto.core.Constants;
import dev.sheldan.abstracto.core.management.ChannelManagementService;
import dev.sheldan.abstracto.core.management.ServerManagementService;
import dev.sheldan.abstracto.core.management.UserManagementService;
@@ -12,6 +14,7 @@ import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.UserInitiatedServerContext;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.utils.ParseUtils;
import net.dv8tion.jda.api.entities.Emote;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
@@ -24,6 +27,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Nonnull;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
@@ -64,14 +68,11 @@ public class CommandReceivedHandler extends ListenerAdapter {
.userInitiatedContext(buildTemplateParameter(event));
Command foundCommand = null;
try {
List<String> parameters = Arrays.asList(event.getMessage().getContentStripped().split(" "));
parameters = parameters.stream().filter(s -> {
return !s.equals("");
}).collect(Collectors.toList());
UnParsedCommandParameter unparsedParameter = new UnParsedCommandParameter();
unparsedParameter.setParameters(parameters.subList(1, parameters.size()));
String withoutPrefix = parameters.get(0).substring(1);
foundCommand = commandManager.findCommandByParameters(withoutPrefix, unparsedParameter);
String contentStripped = event.getMessage().getContentStripped();
List<String> parameters = Arrays.asList(contentStripped.split(" "));
UnParsedCommandParameter unparsedParameter = new UnParsedCommandParameter(contentStripped);
String commandName = parameters.get(0).substring(1);
foundCommand = commandManager.findCommandByParameters(commandName, unparsedParameter);
Parameters parsedParameters = getParsedParameters(unparsedParameter, foundCommand, event.getMessage());
CommandContext commandContext = commandContextBuilder.parameters(parsedParameters).build();
Result result = foundCommand.execute(commandContext);
@@ -106,12 +107,24 @@ public class CommandReceivedHandler extends ListenerAdapter {
public Parameters getParsedParameters(UnParsedCommandParameter unParsedCommandParameter, Command command, Message message){
List<Object> parsedParameters = new ArrayList<>();
if(command.getConfiguration().getParameters().size() == 0) {
return Parameters.builder().parameters(parsedParameters).build();
}
Iterator<TextChannel> channelIterator = message.getMentionedChannels().iterator();
Iterator<Emote> emoteIterator = message.getEmotes().iterator();
Iterator<Member> memberIterator = message.getMentionedMembers().iterator();
for (int i = 0; i < unParsedCommandParameter.getParameters().size(); i++) {
Parameter param = command.getConfiguration().getParameters().get(i);
Parameter param = command.getConfiguration().getParameters().get(0);
boolean reminderActive = false;
for (int i = 0; i < unParsedCommandParameter.getParameters().size(); i++) {
if(i < command.getConfiguration().getParameters().size() && !param.isRemainder()) {
param = command.getConfiguration().getParameters().get(i);
} else {
reminderActive = true;
}
String value = unParsedCommandParameter.getParameters().get(i);
if(param.getMaxLength() != null && (value.length() + Constants.PARAMETER_LIMIT) > param.getMaxLength()) {
throw new ParameterTooLongException("The passed parameter was too long.", command, param.getName(), value.length(), param.getMaxLength());
}
try {
if(param.getType().equals(Integer.class)){
parsedParameters.add(Integer.parseInt(value));
@@ -126,13 +139,20 @@ public class CommandReceivedHandler extends ListenerAdapter {
} else if(param.getType().equals(Emote.class)) {
// TODO maybe rework, this fails if two emotes are needed, and the second one is an emote, the first one a default one
// the second one shadows the first one, and there are too little parameters to go of
if(emoteIterator.hasNext()) {
if (emoteIterator.hasNext()) {
parsedParameters.add(emoteIterator.next());
} else {
parsedParameters.add(value);
}
} else if (param.getType().equals(Duration.class)) {
parsedParameters.add(ParseUtils.parseDuration(value));
} else {
parsedParameters.add(value);
if(!reminderActive) {
parsedParameters.add(value);
} else {
int lastIndex = parsedParameters.size() - 1;
parsedParameters.set(lastIndex, parsedParameters.get(lastIndex) + " " + value);
}
}
} catch (NoSuchElementException e) {
throw new IncorrectParameterException("The passed parameters did not have the correct type.", command, param.getType(), param.getName());

View File

@@ -0,0 +1,38 @@
package dev.sheldan.abstracto.commands.management.exception;
import dev.sheldan.abstracto.command.Command;
import dev.sheldan.abstracto.command.TemplatedException;
import java.util.HashMap;
public class ParameterTooLongException extends RuntimeException implements TemplatedException {
private Command command;
private String parameterName;
private Integer actualLength;
private Integer maximumLength;
public ParameterTooLongException(String s, Command command, String parameterName, Integer actualLength, Integer maximumLength) {
super(s);
this.command = command;
this.parameterName = parameterName;
this.actualLength = actualLength;
this.maximumLength = maximumLength;
}
@Override
public String getTemplateName() {
return "parameter_too_long";
}
@Override
public Object getTemplateModel() {
HashMap<String, Object> model = new HashMap<>();
model.put("parameterName", parameterName);
model.put("actualLength", actualLength);
model.put("maximumLength", maximumLength);
model.put("command", command);
return model;
}
}

View File

@@ -0,0 +1 @@
The parameter ${parameterName} had a too large value: ${actualLength}. The maximum is: ${maximumLength}

View File

@@ -28,10 +28,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.core.exception.ConfigurationException;
import dev.sheldan.abstracto.core.management.PostTargetManagement;
import dev.sheldan.abstracto.core.management.ServerManagementService;
import dev.sheldan.abstracto.core.models.database.PostTarget;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Message;
@@ -83,6 +84,18 @@ public class PostTargetServiceBean implements PostTargetService {
return this.sendEmbedInPostTarget(embed, postTarget);
}
@Override
public CompletableFuture<Message> sendEmbedInPostTarget(MessageToSend message, String postTargetName, Long serverId) {
PostTarget postTarget = this.getPostTarget(postTargetName, serverId);
return this.sendEmbedInPostTarget(message, postTarget);
}
@Override
public CompletableFuture<Message> sendEmbedInPostTarget(MessageToSend message, PostTarget target) {
TextChannel textChannelForPostTarget = getTextChannelForPostTarget(target);
return textChannelForPostTarget.sendMessage(message.getMessage()).embed(message.getEmbed()).submit();
}
@Override
public boolean validPostTarget(String name) {
List<String> possiblePostTargets = dynamicKeyLoader.getPostTargetsAsList();

View File

@@ -10,10 +10,10 @@ import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.models.listener.MessageEmbeddedModel;
import dev.sheldan.abstracto.core.service.Bot;
import dev.sheldan.abstracto.core.service.MessageCache;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import dev.sheldan.abstracto.templating.TemplateService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.apache.commons.lang3.StringUtils;
@@ -91,8 +91,8 @@ public class MessageEmbedListener extends ListenerAdapter {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createEmbedAndPostEmbed(@Nonnull GuildMessageReceivedEvent event, CachedMessage message) {
MessageEmbeddedModel messageEmbeddedModel = buildTemplateParameter(event, message);
MessageEmbed embed = templateService.renderEmbedTemplate(MESSAGE_EMBED_TEMPLATE, messageEmbeddedModel);
event.getChannel().sendMessage(embed).queue();
MessageToSend embed = templateService.renderEmbedTemplate(MESSAGE_EMBED_TEMPLATE, messageEmbeddedModel);
event.getChannel().sendMessage(embed.getMessage()).embed(embed.getEmbed()).queue();
}
private MessageEmbeddedModel buildTemplateParameter(GuildMessageReceivedEvent event, CachedMessage embeddedMessage) {

View File

@@ -2,19 +2,19 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/abstracto
spring.datasource.username= abstracto
spring.datasource.password= abstracto
spring.jpa.hibernate.default_schema=abstracto
spring.jpa.show-sql = true
spring.jpa.show-sql = false
spring.jpa.hibernate.ddl-auto = update
spring.jpa.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.naming_strategy = org.hibernate.cfg.ImprovedNamingStrategy
log4j.logger.org.hibernate.SQL=debug
log4j.logger.org.hibernate.SQL=info
log4j.logger.org.hibernate.type.descriptor.sql=trace
log4j.logger.org.hibernate.type=trace
log4j.logger.dev.sheldan=debug
log4j.logger.dev.sheldan=info
spring.cache.cache-names=messages
spring.cache.caffeine.spec=maximumSize=500,expireAfterAccess=600s
abstracto.startup.synchronize=true
logging.level.dev.sheldan=DEBUG
abstracto.parameter.lowerBound=50

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.core;
import org.springframework.beans.factory.annotation.Value;
public class Constants {
@Value("${abstracto.parameter.lowerBound}")
public static int PARAMETER_LIMIT = 0;
}

View File

@@ -0,0 +1,19 @@
package dev.sheldan.abstracto.core.models;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class AServerAChannelAUser {
private AServer guild;
private AChannel channel;
private AUserInAServer aUserInAServer;
private AUser user;
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.core.models.embed;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import net.dv8tion.jda.api.entities.MessageEmbed;
@Getter
@Setter
@Builder
public class MessageToSend {
private MessageEmbed embed;
private String message;
}

View File

@@ -1,6 +1,7 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.models.database.PostTarget;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
@@ -12,6 +13,8 @@ public interface PostTargetService {
CompletableFuture<Message> sendEmbedInPostTarget(MessageEmbed embed, PostTarget target);
CompletableFuture<Message> sendTextInPostTarget(String text, String postTargetName, Long serverId);
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);
boolean validPostTarget(String name);
List<String> getAvailablePostTargets();
}

View File

@@ -0,0 +1,28 @@
package dev.sheldan.abstracto.core.utils;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ParseUtils {
private static Pattern messageRegex = Pattern.compile("(?<number>\\d+)(?<unit>[ywdhms]+)");
public static Duration parseDuration(String textToParseFrom) {
Matcher matcher = ParseUtils.messageRegex.matcher(textToParseFrom);
Duration start = Duration.ZERO;
while(matcher.find()) {
String unit = matcher.group("unit");
String number = matcher.group("number");
long parsed = Long.parseLong(number);
switch (unit) {
case "w": start = start.plus(Duration.ofDays(parsed * 7)); break;
case "d": start = start.plus(Duration.ofDays(parsed)); break;
case "h": start = start.plus(Duration.ofHours(parsed)); break;
case "m": start = start.plus(Duration.ofMinutes(parsed)); break;
case "s": start = start.plus(Duration.ofSeconds(parsed)); break;
}
}
return start;
}
}

View File

@@ -0,0 +1,44 @@
package dev.sheldan.abstracto.core.utils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import java.time.Duration;
import static org.junit.Assert.*;
@RunWith(MockitoJUnitRunner.class)
public class ParseUtilsTest {
@Test
public void oneDay() {
Duration duration = ParseUtils.parseDuration("1d");
assertEquals(Duration.ofDays(1), duration);
}
@Test
public void twoWeeks() {
Duration duration = ParseUtils.parseDuration("2w");
assertEquals(Duration.ofDays(14), duration);
}
@Test
public void aDayWithMinutes() {
Duration duration = ParseUtils.parseDuration("1d3m");
assertEquals(Duration.ofDays(1).plus(Duration.ofMinutes(3)), duration);
}
@Test
public void allTimeFormats() {
Duration duration = ParseUtils.parseDuration("2w3d4h2m1s");
assertEquals(Duration.ofDays(17).plus(Duration.ofHours(4)).plus(Duration.ofMinutes(2)).plus(Duration.ofSeconds(1)), duration);
}
@Test
public void overFlowingTimeFormats() {
Duration duration = ParseUtils.parseDuration("70s");
assertEquals(Duration.ofMinutes(1).plus(Duration.ofSeconds(10)), duration);
}
}

View File

@@ -74,6 +74,12 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling-impl</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

View File

@@ -20,6 +20,7 @@
<module>executable</module>
<module>templating</module>
<module>abstracto-modules</module>
<module>scheduling</module>
</modules>
@@ -64,6 +65,21 @@
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -79,6 +95,18 @@
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.sheldan.abstracto</groupId>
<artifactId>abstracto-application</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling</artifactId>
<packaging>pom</packaging>
<modules>
<module>scheduling-int</module>
<module>scheduling-impl</module>
</modules>
</project>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>scheduling-impl</artifactId>
<dependencies>
<dependency>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling-int</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,16 @@
package dev.sheldan.abstracto.scheduling.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.HashMap;
@Component
@Getter
@Setter
@ConfigurationProperties(prefix = "abstracto.scheduling")
public class JobConfigLoader {
private HashMap<String, SchedulerJobProperties> jobs = new HashMap<>();
}

View File

@@ -0,0 +1,42 @@
package dev.sheldan.abstracto.scheduling.config;
import dev.sheldan.abstracto.scheduling.factory.SchedulerJobFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.quartz.QuartzProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
public class SchedulerConfig {
@Autowired
private DataSource dataSource;
@Autowired
private ApplicationContext applicationContext;
@Autowired
private QuartzProperties quartzProperties;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerJobFactory jobFactory = new SchedulerJobFactory();
jobFactory.setApplicationContext(applicationContext);
Properties properties = new Properties();
properties.putAll(quartzProperties.getProperties());
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setOverwriteExistingJobs(true);
factory.setDataSource(dataSource);
factory.setQuartzProperties(properties);
factory.setJobFactory(jobFactory);
return factory;
}
}

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.scheduling.config;
import lombok.*;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SchedulerJobProperties {
private String name;
private String group;
private String cronExpression;
private String clazz;
private Boolean active;
}

View File

@@ -0,0 +1,64 @@
package dev.sheldan.abstracto.scheduling.factory;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.TimeZone;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.TriggerBuilder.*;
@Component
@Slf4j
public class QuartzConfigFactory {
public JobDetail createJob(Class<? extends QuartzJobBean> jobClass, boolean isDurable,
ApplicationContext context, String jobName, String jobGroup, boolean requestsRecovery) {
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setJobClass(jobClass);
factoryBean.setDurability(isDurable);
factoryBean.setApplicationContext(context);
factoryBean.setRequestsRecovery(requestsRecovery);
factoryBean.setName(jobName);
factoryBean.setGroup(jobGroup);
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put(jobName + jobGroup, jobClass.getName());
factoryBean.setJobDataMap(jobDataMap);
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
public CronTrigger createBasicCronTrigger(Date startTime, String cronExpression) {
return newTrigger()
.withSchedule(cronSchedule(cronExpression).inTimeZone(TimeZone.getTimeZone("UTC")).withMisfireHandlingInstructionIgnoreMisfires())
.startAt(startTime)
.build();
}
public Trigger createSimpleOnceOnlyTrigger(String triggerName, Date startTime) {
return newTrigger()
.startAt(startTime)
.withSchedule(simpleSchedule())
.build();
}
public Trigger createOnceOnlyTriggerForJob(String jobName, String jobGroup, Date startTime, JobDataMap jobDataMap) {
return newTrigger()
.startAt(startTime)
.forJob(jobName, jobGroup)
.withSchedule(simpleSchedule())
.usingJobData(jobDataMap)
.build();
}
}

View File

@@ -0,0 +1,20 @@
package dev.sheldan.abstracto.scheduling.factory;
import dev.sheldan.abstracto.scheduling.config.SchedulerJobProperties;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import org.springframework.stereotype.Component;
@Component
public class SchedulerJobConverter {
public SchedulerJob fromJobProperties(SchedulerJobProperties properties) {
return SchedulerJob
.builder()
.name(properties.getName())
.groupName(properties.getGroup())
.active(properties.getActive())
.cronExpression(properties.getCronExpression())
.clazz(properties.getClazz())
.build();
}
}

View File

@@ -0,0 +1,24 @@
package dev.sheldan.abstracto.scheduling.factory;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
public class SchedulerJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {
private AutowireCapableBeanFactory beanFactory;
@Override
public void setApplicationContext(final ApplicationContext context) {
beanFactory = context.getAutowireCapableBeanFactory();
}
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
beanFactory.autowireBean(job);
return job;
}
}

View File

@@ -0,0 +1,9 @@
package dev.sheldan.abstracto.scheduling.repository;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SchedulerJobRepository extends JpaRepository<SchedulerJob, Long> {
boolean existsByName(String name);
SchedulerJob findByName(String name);
}

View File

@@ -0,0 +1,15 @@
package dev.sheldan.abstracto.scheduling.service;
import org.quartz.SchedulerException;
import org.quartz.spi.InstanceIdGenerator;
import java.util.UUID;
public class IdGenerationService implements InstanceIdGenerator {
@Override
public String generateInstanceId() throws SchedulerException {
return UUID.randomUUID().toString();
}
}

View File

@@ -0,0 +1,162 @@
package dev.sheldan.abstracto.scheduling.service;
import dev.sheldan.abstracto.scheduling.factory.QuartzConfigFactory;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import dev.sheldan.abstracto.scheduling.model.SchedulerService;
import dev.sheldan.abstracto.scheduling.service.management.SchedulerJobManagementServiceBean;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
@Component
@Slf4j
public class SchedulerServiceBean implements SchedulerService {
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@Autowired
private ApplicationContext context;
@Autowired
private QuartzConfigFactory scheduleCreator;
@Autowired
private SchedulerJobManagementServiceBean schedulerJobManagementServiceBean;
@Override
public void startScheduledJobs() {
List<SchedulerJob> jobs = schedulerJobManagementServiceBean.findAll();
Scheduler scheduler = schedulerFactoryBean.getScheduler();
jobs.forEach(schedulerJob -> {
if(schedulerJob.isActive()) {
scheduleNewJob(scheduler, schedulerJob);
}
});
}
private boolean isRecurringJob(SchedulerJob job) {
return job.getCronExpression() != null && CronExpression.isValidExpression(job.getCronExpression());
}
private void scheduleNewJob(Scheduler scheduler, SchedulerJob schedulerJob) {
if(!schedulerJob.isActive()) {
return;
}
try {
JobDetail jobDetail = JobBuilder.newJob((Class<? extends QuartzJobBean>) Class.forName(schedulerJob.getClazz()))
.withIdentity(schedulerJob.getName(), schedulerJob.getGroupName()).build();
if (!scheduler.checkExists(jobDetail.getKey())) {
// if its only started by triggers, it needs to be durable
boolean recurringJob = isRecurringJob(schedulerJob);
jobDetail = scheduleCreator.createJob((Class<? extends QuartzJobBean>) Class.forName(schedulerJob.getClazz()),
!recurringJob, context, schedulerJob.getName(), schedulerJob.getGroupName(), false);
if(recurringJob) {
Trigger trigger = scheduleCreator.createBasicCronTrigger(new Date(),
schedulerJob.getCronExpression());
scheduler.scheduleJob(jobDetail, trigger);
} else {
scheduler.addJob(jobDetail, true);
}
} else {
log.info("Not scheduling job {}, because it was already scheduled.", schedulerJob.getName());
}
} catch (ClassNotFoundException | SchedulerException e) {
log.error("Failed to schedule job", e);
}
}
@Override
public void scheduleJob(SchedulerJob job) {
log.info("Scheduling job {}", job.getName());
this.scheduleNewJob(schedulerFactoryBean.getScheduler(), job);
}
@Override
public void updateJob(SchedulerJob job, Date startDate) {
Trigger newTrigger;
if (job.getCronExpression() != null) {
newTrigger = scheduleCreator.createBasicCronTrigger(startDate, job.getCronExpression());
} else {
newTrigger = scheduleCreator.createSimpleOnceOnlyTrigger(job.getName(), startDate);
}
try {
schedulerFactoryBean.getScheduler().rescheduleJob(TriggerKey.triggerKey(job.getName()), newTrigger);
schedulerJobManagementServiceBean.save(job);
} catch (SchedulerException e) {
log.error(e.getMessage(), e);
}
}
@Override
public boolean unScheduleJob(String jobName) {
try {
return schedulerFactoryBean.getScheduler().unscheduleJob(new TriggerKey(jobName));
} catch (SchedulerException e) {
log.error("Failed to un-schedule job - {}", jobName, e);
return false;
}
}
@Override
public boolean deleteJob(SchedulerJob job) {
try {
return schedulerFactoryBean.getScheduler().deleteJob(new JobKey(job.getName(), job.getGroupName()));
} catch (SchedulerException e) {
log.error("Failed to delete job - {}", job.getName(), e);
return false;
}
}
@Override
public boolean pauseJob(SchedulerJob job) {
try {
schedulerFactoryBean.getScheduler().pauseJob(new JobKey(job.getName(), job.getGroupName()));
return true;
} catch (SchedulerException e) {
log.error("Failed to pause job - {}", job.getName(), e);
return false;
}
}
@Override
public boolean continueJob(SchedulerJob job) {
try {
schedulerFactoryBean.getScheduler().resumeJob(new JobKey(job.getName(), job.getGroupName()));
return true;
} catch (SchedulerException e) {
log.error("Failed to resume job - {}", job.getName(), e);
return false;
}
}
@Override
public boolean executeJob(SchedulerJob job) {
try {
schedulerFactoryBean.getScheduler().triggerJob(new JobKey(job.getName(), job.getGroupName()));
return true;
} catch (SchedulerException e) {
log.error("Failed to start new job - {}", job.getName(), e);
return false;
}
}
@Override
public boolean executeJobWithParametersOnce(String name, String group, JobDataMap dataMap, Date date) {
Trigger onceOnlyTriggerForJob = scheduleCreator.createOnceOnlyTriggerForJob(name, group, date, dataMap);
try {
schedulerFactoryBean.getScheduler().scheduleJob(onceOnlyTriggerForJob);
return true;
} catch (SchedulerException e) {
log.error("Failed to start new job - {}", name, e);
return false;
}
}
}

View File

@@ -0,0 +1,43 @@
package dev.sheldan.abstracto.scheduling.service;
import dev.sheldan.abstracto.scheduling.config.JobConfigLoader;
import dev.sheldan.abstracto.scheduling.factory.SchedulerJobConverter;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import dev.sheldan.abstracto.scheduling.model.SchedulerService;
import dev.sheldan.abstracto.scheduling.service.management.SchedulerJobManagementServiceBean;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Component
public class SchedulerStartupService {
@Autowired
private SchedulerService schedulerService;
@Autowired
private JobConfigLoader jobConfigLoader;
@Autowired
private SchedulerJobManagementServiceBean schedulerJobManagementServiceBean;
@Autowired
private SchedulerJobConverter schedulerJobConverter;
@EventListener
@Transactional
public void handleContextRefreshEvent(ContextRefreshedEvent ctxStartEvt) {
jobConfigLoader.getJobs().forEach((s, schedulerJob) -> {
SchedulerJob job = schedulerJobConverter.fromJobProperties(schedulerJob);
if(!schedulerJobManagementServiceBean.doesJobExist(job) || !schedulerJobManagementServiceBean.isJobDefinitionTheSame(job)) {
schedulerJobManagementServiceBean.createOrUpdate(job);
}
});
schedulerService.startScheduledJobs();
}
}

View File

@@ -0,0 +1,70 @@
package dev.sheldan.abstracto.scheduling.service.management;
import dev.sheldan.abstracto.scheduling.model.SchedulerJob;
import dev.sheldan.abstracto.scheduling.repository.SchedulerJobRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
@Slf4j
public class SchedulerJobManagementServiceBean {
@Autowired
private SchedulerJobRepository repository;
public SchedulerJob createOrUpdate(SchedulerJob job) {
if(repository.existsByName(job.getName())) {
SchedulerJob byName = repository.findByName(job.getName());
byName.setActive(job.isActive());
byName.setClazz(job.getClazz());
byName.setCronExpression(job.getCronExpression());
byName.setGroupName(job.getGroupName());
return repository.save(byName);
} else {
return this.createJob(job);
}
}
public SchedulerJob createJob(SchedulerJob job) {
log.info("Creating job {}", job.getName());
repository.save(job);
return job;
}
public List<SchedulerJob> findAll() {
return repository.findAll();
}
public SchedulerJob save(SchedulerJob job) {
repository.save(job);
return job;
}
public boolean doesJobExist(SchedulerJob schedulerJob) {
return repository.existsByName(schedulerJob.getName());
}
public boolean isJobDefinitionTheSame(SchedulerJob job) {
SchedulerJob old = repository.findByName(job.getName());
if(old == null) {
return false;
}
boolean cronExp;
if(old.getCronExpression() == null && job.getCronExpression() != null) {
cronExp = false;
} else if(old.getCronExpression() != null && job.getCronExpression() == null) {
cronExp = false;
} else if(old.getCronExpression() == null && job.getCronExpression() == null) {
cronExp = true;
} else {
cronExp = old.getCronExpression().equals(job.getCronExpression());
}
boolean active = old.isActive() == job.isActive();
boolean classEqual = old.getClazz().equals(job.getClazz());
boolean group = old.getGroupName().equals(job.getGroupName());
return cronExp && active && classEqual && group;
}
}

View File

@@ -0,0 +1,15 @@
spring.quartz.job-store-type=jdbc
spring.quartz.jdbc.initialize-schema=never
spring.quartz.properties.org.quartz.scheduler.instanceName=quartz-abstracto-app
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO
spring.quartz.properties.org.quartz.scheduler.instanceIdGenerator.class=dev.sheldan.abstracto.scheduling.service.IdGenerationService
spring.quartz.properties.org.quartz.threadPool.threadCount=20
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
spring.quartz.properties.org.quartz.jobStore.useProperties=true
spring.quartz.properties.org.quartz.jobStore.misfireThreshold=60000
spring.quartz.properties.org.quartz.jobStore.tablePrefix=qrtz_
spring.quartz.properties.org.quartz.jobStore.isClustered=false
spring.quartz.properties.org.quartz.plugin.shutdownHook.class=org.quartz.plugins.management.ShutdownHookPlugin
spring.quartz.properties.org.quartz.plugin.shutdownHook.cleanShutdown=TRUE

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.sheldan.abstracto.scheduling</groupId>
<artifactId>scheduling</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>scheduling-int</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,30 @@
package dev.sheldan.abstracto.scheduling.model;
import lombok.*;
import javax.persistence.*;
@Getter
@Setter
@Entity
@Builder
@Table(name = "scheduler_job")
@NoArgsConstructor
@AllArgsConstructor
public class SchedulerJob {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String groupName;
private String clazz;
private String cronExpression;
private boolean active;
}

View File

@@ -0,0 +1,25 @@
package dev.sheldan.abstracto.scheduling.model;
import org.quartz.JobDataMap;
import java.util.Date;
public interface SchedulerService {
void startScheduledJobs();
void scheduleJob(SchedulerJob job);
void updateJob(SchedulerJob job, Date startDate);
boolean unScheduleJob(String jobName);
boolean deleteJob(SchedulerJob job);
boolean pauseJob(SchedulerJob job);
boolean continueJob(SchedulerJob job);
boolean executeJob(SchedulerJob job);
boolean executeJobWithParametersOnce(String name, String group, JobDataMap dataMap, Date date);
}

View File

@@ -17,4 +17,5 @@ public class EmbedConfiguration {
private List<EmbedField> fields;
private EmbedFooter footer;
private OffsetDateTime timeStamp;
private String additionalMessage;
}

View File

@@ -3,6 +3,7 @@ package dev.sheldan.abstracto.templating.loading;
import com.google.gson.Gson;
import dev.sheldan.abstracto.core.models.ContextAware;
import dev.sheldan.abstracto.core.models.ServerContext;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import dev.sheldan.abstracto.templating.TemplateDto;
import dev.sheldan.abstracto.templating.TemplateService;
import dev.sheldan.abstracto.templating.embed.*;
@@ -11,7 +12,6 @@ import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
@@ -51,7 +51,7 @@ public class TemplateServiceBean implements TemplateService {
}
@Override
public MessageEmbed renderEmbedTemplate(String key, Object model) {
public MessageToSend renderEmbedTemplate(String key, Object model) {
String embedConfig = this.renderTemplate(key + "_embed", model);
EmbedBuilder builder = new EmbedBuilder();
EmbedConfiguration configuration = gson.fromJson(embedConfig, EmbedConfiguration.class);
@@ -91,7 +91,11 @@ public class TemplateServiceBean implements TemplateService {
builder.setColor(new Color(color.getR(), color.getG(), color.getB()).getRGB());
}
return builder.build();
return MessageToSend.builder()
.embed(builder.build())
.message(configuration.getAdditionalMessage())
.build();
}
private String impromptu(String templateStr, Object model) {

View File

@@ -1,7 +1,7 @@
package dev.sheldan.abstracto.templating;
import dev.sheldan.abstracto.core.models.ServerContext;
import net.dv8tion.jda.api.entities.MessageEmbed;
import dev.sheldan.abstracto.core.models.embed.MessageToSend;
import java.util.HashMap;
@@ -9,7 +9,7 @@ public interface TemplateService {
TemplateDto getTemplateByKey(String key);
boolean templateExists(String key);
String renderTemplate(TemplateDto templateDto);
MessageEmbed renderEmbedTemplate(String key, Object model);
MessageToSend renderEmbedTemplate(String key, Object model);
String renderTemplate(String key, HashMap<String, Object> parameters);
String renderTemplate(String key, Object model);
String renderContextAwareTemplate(String key, ServerContext serverContext);

View File

@@ -1,13 +0,0 @@
package dev.sheldan.abstracto.templating.embed;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class EmbedAuthor {
private String name;
private String url;
private String avatar;
}

View File

@@ -1,12 +0,0 @@
package dev.sheldan.abstracto.templating.embed;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class EmbedColor {
private Integer r;
private Integer g;
private Integer b;
}

View File

@@ -1,21 +0,0 @@
package dev.sheldan.abstracto.templating.embed;
import lombok.Getter;
import lombok.Setter;
import java.time.OffsetDateTime;
import java.util.List;
@Getter
@Setter
public class EmbedConfiguration {
private EmbedAuthor author;
private EmbedTitle title;
private EmbedColor color;
private String description;
private String thumbnail;
private String imageUrl;
private List<EmbedField> fields;
private EmbedFooter footer;
private OffsetDateTime timeStamp;
}

View File

@@ -1,12 +0,0 @@
package dev.sheldan.abstracto.templating.embed;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class EmbedField {
private String name;
private String value;
private Boolean inline;
}

View File

@@ -1,11 +0,0 @@
package dev.sheldan.abstracto.templating.embed;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class EmbedFooter {
private String text;
private String icon;
}

View File

@@ -1,11 +0,0 @@
package dev.sheldan.abstracto.templating.embed;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class EmbedTitle {
private String title;
private String url;
}