diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 0000000..8f6f94a --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,25 @@ +name: PR Validation + +on: + pull_request: + branches: + - master + +jobs: + test: + runs-on: ubuntu-latest + + steps: + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'temurin' + cache: 'maven' + + - name: Run Maven Tests + run: mvn clean test diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000..591228d --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,54 @@ +name: Release & Publish + +on: + pull_request: + types: + - closed + branches: + - master + +jobs: + release: + if: github.event.pull_request.merged == true && github.head_ref == 'develop' + runs-on: ubuntu-latest + + steps: + + - name: Checkout master branch + uses: actions/checkout@v4 + with: + ref: master + token: ${{ secrets.PAT_TOKEN }} + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'temurin' + cache: 'maven' + + - name: Remove SNAPSHOT from POM + id: bump_version + run: | + mvn versions:set -DremoveSnapshot=true -DgenerateBackupPoms=false + NEW_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + echo "VERSION=$NEW_VERSION" >> $GITHUB_ENV + + - name: Commit and Push to Master + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git commit -am "chore: release version ${{ env.VERSION }}" + git push origin master + + - name: Package Application + # Tests are skipped because they already passed in the PR Validation workflow + run: mvn clean package -DskipTests + + - name: Publish GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ env.VERSION }} + name: Release v${{ env.VERSION }} + files: target/*.jar + generate_release_notes: true diff --git a/pom.xml b/pom.xml index 92f5d7b..2f68f52 100644 --- a/pom.xml +++ b/pom.xml @@ -7,24 +7,27 @@ org.springframework.boot spring-boot-starter-parent - 2.5.2 + 4.0.4 ovh.excale vgreeter - 2.3.0 + 26.4.0-SNAPSHOT VGreeter Let the bot greet your friends when they join vocal chat on Discord! https://github.com/devExcale/VGreeter jar + + scm:git:${project.url} + + - 1.8 - 8 - 8 + 25 UTF-8 + 0.1.8 @@ -43,7 +46,27 @@ net.dv8tion JDA - 4.3.0_277 + 6.4.1 + + + club.minnced + jdave-api + ${jdave.version} + + + club.minnced + jdave-native-win-x86-64 + ${jdave.version} + + + club.minnced + jdave-native-linux-x86-64 + ${jdave.version} + + + club.minnced + jdave-native-linux-aarch64 + ${jdave.version} @@ -62,7 +85,12 @@ ch.qos.logback logback-classic - 1.2.5 + + + + + com.fasterxml.jackson.core + jackson-databind @@ -76,18 +104,18 @@ org.postgresql postgresql - 42.2.22 + 42.7.7 - + + + org.projectlombok + lombok + 1.18.44 + provided + - - - dv8tion - m2-dv8tion - https://m2.dv8tion.net/releases - - + ${project.name}-${project.version} @@ -105,10 +133,31 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.14.1 + + ${java.version} + full + + + org.projectlombok + lombok + 1.18.44 + + + org.springframework.boot + spring-boot-configuration-processor + ${project.parent.version} + + + + + org.springframework.boot spring-boot-maven-plugin - 2.5.2 diff --git a/src/main/java/ovh/excale/vgreeter/VGreeterApplication.java b/src/main/java/ovh/excale/vgreeter/VGreeterApplication.java index 05c7c5d..bfa68a2 100644 --- a/src/main/java/ovh/excale/vgreeter/VGreeterApplication.java +++ b/src/main/java/ovh/excale/vgreeter/VGreeterApplication.java @@ -1,8 +1,8 @@ package ovh.excale.vgreeter; +import lombok.extern.log4j.Log4j2; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.jetbrains.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.Banner; @@ -11,11 +11,14 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +@Log4j2 @SpringBootApplication public class VGreeterApplication implements CommandLineRunner, ApplicationContextAware { - private static ApplicationContext ctx; + private static Boolean maintenance; + private static ConfigurableApplicationContext ctx; public static void main(String[] args) { @@ -25,26 +28,64 @@ public static void main(String[] args) { } + // TODO: DON'T CONNECT TO DB DURING MAINTENANCE + + // keep previous maintenance state + public static void restart(@Nullable final Runnable then, @Nullable final Boolean maintenance) { + + log.info("Restarting app"); + + Thread thread = new Thread(() -> { + ctx.close(); + + VGreeterApplication.maintenance = maintenance != null ? maintenance : VGreeterApplication.maintenance; + + SpringApplication app = new SpringApplication(VGreeterApplication.class); + app.setBannerMode(Banner.Mode.OFF); + app.run(); + + if(then != null) + then.run(); + + }); + + thread.setDaemon(false); + thread.start(); + + } + + public static void restart(@NotNull Runnable then) { + restart(then, null); + } + + public static void restart(boolean maintenance) { + restart(null, maintenance); + } + + public static boolean isInMaintenance() { + return maintenance; + } + public static ApplicationContext getApplicationContext() { return ctx; } - public final Logger logger; public final String version; - public VGreeterApplication(@Value("${application.version}") String version) { + public VGreeterApplication(@Value("${application.version}") String version, + @Value("${env.MAINTENANCE:false}") boolean maintenance) { this.version = version; - logger = LoggerFactory.getLogger(VGreeterApplication.class); + VGreeterApplication.maintenance = maintenance; } @Override public void run(String[] args) { - logger.info("Running on version {}", version); + log.info("Running on version {}", version); } @Override public void setApplicationContext(@NotNull ApplicationContext context) throws BeansException { - ctx = context; + ctx = (ConfigurableApplicationContext) context; } } diff --git a/src/main/java/ovh/excale/vgreeter/commands/AbstractCommand.java b/src/main/java/ovh/excale/vgreeter/commands/AbstractCommand.java deleted file mode 100644 index b761b64..0000000 --- a/src/main/java/ovh/excale/vgreeter/commands/AbstractCommand.java +++ /dev/null @@ -1,52 +0,0 @@ -package ovh.excale.vgreeter.commands; - -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -import net.dv8tion.jda.api.hooks.EventListener; -import net.dv8tion.jda.api.interactions.commands.build.CommandData; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; -import org.jetbrains.annotations.Nullable; -import ovh.excale.vgreeter.utilities.CommandBuilder; - -public abstract class AbstractCommand { - - private final String name; - private final String description; - private final CommandBuilder builder; - - protected AbstractCommand(String name, String description) { - this.name = name; - this.description = description; - - builder = CommandBuilder - .create(name) - .setDescription(description); - - } - - public abstract ReplyAction execute(SlashCommandEvent event); - - public String getName() { - return name; - } - - public String getDescription() { - return description; - } - - protected CommandBuilder getBuilder() { - return builder; - } - - public CommandData getData() { - return builder.build(); - } - - public boolean hasListener() { - return false; - } - - public @Nullable EventListener getListener() { - return null; - } - -} diff --git a/src/main/java/ovh/excale/vgreeter/commands/CommandRegister.java b/src/main/java/ovh/excale/vgreeter/commands/CommandRegister.java deleted file mode 100644 index 0604d44..0000000 --- a/src/main/java/ovh/excale/vgreeter/commands/CommandRegister.java +++ /dev/null @@ -1,93 +0,0 @@ -package ovh.excale.vgreeter.commands; - -import net.dv8tion.jda.api.events.GenericEvent; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -import net.dv8tion.jda.api.exceptions.ErrorResponseException; -import net.dv8tion.jda.api.hooks.EventListener; -import net.dv8tion.jda.api.interactions.commands.build.CommandData; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; -import org.jetbrains.annotations.NotNull; -import org.springframework.stereotype.Service; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -@Service -public class CommandRegister { - - private final Map commands; - private final Listener listener; - - private CommandRegister() { - commands = new HashMap<>(); - listener = new Listener(); - } - - public CommandRegister register(AbstractCommand command) { - - commands.put(command.getName(), command); - if(command.hasListener()) - listener.register(command.getListener()); - - return this; - - } - - public CommandData[] getData() { - return commands - .values() - .stream() - .map(AbstractCommand::getData) - .toArray(CommandData[]::new); - } - - public Listener getListener() { - return listener; - } - - private class Listener implements EventListener { - - public Set commandListeners; - - protected Listener() { - commandListeners = new HashSet<>(); - } - - protected void register(EventListener listener) { - commandListeners.add(listener); - } - - @Override - public void onEvent(@NotNull GenericEvent genericEvent) { - - if(genericEvent instanceof SlashCommandEvent) - onSlashCommand((SlashCommandEvent) genericEvent); - else - for(EventListener commandListener : commandListeners) - commandListener.onEvent(genericEvent); - - } - - private void onSlashCommand(SlashCommandEvent event) { - - AbstractCommand command = commands.get(event.getName()); - ReplyAction action; - - if(command == null) - action = event - .reply("No such command") - .setEphemeral(true); - else - action = command.execute(event); - - try { - action.queue(); - } catch(ErrorResponseException ignored) { - } - } - - } - -} diff --git a/src/main/java/ovh/excale/vgreeter/commands/TrackIndexCommand.java b/src/main/java/ovh/excale/vgreeter/commands/TrackIndexCommand.java deleted file mode 100644 index a0bc3ec..0000000 --- a/src/main/java/ovh/excale/vgreeter/commands/TrackIndexCommand.java +++ /dev/null @@ -1,186 +0,0 @@ -package ovh.excale.vgreeter.commands; - -import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.entities.Emoji; -import net.dv8tion.jda.api.events.interaction.ButtonClickEvent; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -import net.dv8tion.jda.api.hooks.EventListener; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import net.dv8tion.jda.api.interactions.commands.OptionMapping; -import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.interactions.components.Button; -import net.dv8tion.jda.api.interactions.components.Component; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import ovh.excale.vgreeter.VGreeterApplication; -import ovh.excale.vgreeter.models.TrackModel; -import ovh.excale.vgreeter.repositories.TrackRepository; - -import java.awt.*; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -public class TrackIndexCommand extends AbstractCommand { - - public static final String BUTTON_COMMAND = "trackindex:"; - - private final TrackRepository trackRepo; - private final ButtonClickListener buttonListener; - - public TrackIndexCommand() { - super("trackindex", "List all the tracks"); - getBuilder().addOption("page", "Page number", OptionType.INTEGER); - - trackRepo = VGreeterApplication - .getApplicationContext() - .getBean(TrackRepository.class); - - buttonListener = new ButtonClickListener(); - - } - - @Override - public ReplyAction execute(SlashCommandEvent event) { - - int humanBasedPage = Optional - .ofNullable(event.getOption("page")) - .map(OptionMapping::getAsLong) - .map(Long::intValue) - .orElse(1); - - if(humanBasedPage < 1) - return event - .reply("Page option must be positive!") - .setEphemeral(true); - - Page trackPage = trackRepo.findAll(PageRequest.of(humanBasedPage - 1, - 15, - Sort.by(Sort.Direction.ASC, "id"))); - - if(trackPage.isEmpty()) - return event - .reply("Empty page") - .setEphemeral(true); - - EmbedBuilder eb = computeEmbed(trackPage); - - // TODO: CHECK PERMS AND MESSAGECHANNEL - event - .getChannel() - .sendMessage(eb.build()) - .setActionRow(computeButtons(trackPage)) - .queueAfter(2, TimeUnit.SECONDS); - - String userMention = event - .getUser() - .getAsMention(); - - return event.reply("Track Index requested by " + userMention); - - } - - @Override - public boolean hasListener() { - return true; - } - - @Override - public @Nullable EventListener getListener() { - return buttonListener; - } - - private @NotNull EmbedBuilder computeEmbed(Page trackPage) { - - return new EmbedBuilder() - .setTitle("Track Index") - .setFooter("Page " + (trackPage.getNumber() + 1) + "/" + trackPage.getTotalPages()) - .setColor(Color.BLUE) - .setDescription(trackPage - .getContent() - .stream() - .map(track -> "**#" + track.getId() + "** *" + track.getName() + "*") - .collect(Collectors.joining("\n"))); - - } - - private Component[] computeButtons(Page trackPage) { - - int zeroBasedPage = trackPage.getNumber(); - - Button prevButton = Button - .secondary(BUTTON_COMMAND + (zeroBasedPage), Emoji.fromUnicode("\u25C0")) - .withDisabled(!trackPage.hasPrevious()); - Button nextButton = Button - .secondary(BUTTON_COMMAND + (zeroBasedPage + 2), Emoji.fromUnicode("\u25B6")) - .withDisabled(!trackPage.hasNext()); - - return new Component[] { prevButton, nextButton }; - - } - - // TODO: CLOSE INDEX BUTTON - public class ButtonClickListener extends ListenerAdapter { - - @Override - public void onButtonClick(@NotNull ButtonClickEvent event) { - - // TODO: COMPONENT_ID COMMAND CHECK - String stringPage = event - .getComponentId() - .replace(BUTTON_COMMAND, ""); - int humanBasedPage; - - try { - - humanBasedPage = Integer.parseInt(stringPage); - - } catch(NumberFormatException e) { - event - .reply("Invalid button") - .setEphemeral(true) - .queue(); - return; - } - - if(humanBasedPage < 1) { - event - .reply("Invalid button") - .setEphemeral(true) - .queue(); - return; - } - - Page trackPage = trackRepo.findAll(PageRequest.of(humanBasedPage - 1, - 15, - Sort.by(Sort.Direction.ASC, "id"))); - - if(trackPage.isEmpty()) { - event - .reply("Empty page") - .setEphemeral(true) - .queue(); - return; - } - - EmbedBuilder eb = computeEmbed(trackPage); - - //noinspection ConstantConditions - event - .getMessage() - .delete() - .and(event - .getChannel() - .sendMessage(eb.build()) - .setActionRow(computeButtons(trackPage))) - .queue(); - - } - - } - -} diff --git a/src/main/java/ovh/excale/vgreeter/commands/button/CloseEmbedCommand.java b/src/main/java/ovh/excale/vgreeter/commands/button/CloseEmbedCommand.java new file mode 100644 index 0000000..e40af6b --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/button/CloseEmbedCommand.java @@ -0,0 +1,25 @@ +package ovh.excale.vgreeter.commands.button; + +import java.util.Collections; + +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; +import ovh.excale.vgreeter.commands.core.AbstractButtonCommand; + +public class CloseEmbedCommand extends AbstractButtonCommand { + + public CloseEmbedCommand() { + super("close", "Close and embed or a message"); + } + + @Override + public @NotNull RestAction execute(@NotNull ButtonInteractionEvent event) { + + return event.editMessage("Closed") + .setEmbeds(Collections.emptyList()) + .setComponents(Collections.emptyList()); + + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/button/TrackIndexButtonCommand.java b/src/main/java/ovh/excale/vgreeter/commands/button/TrackIndexButtonCommand.java new file mode 100644 index 0000000..0c593be --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/button/TrackIndexButtonCommand.java @@ -0,0 +1,55 @@ +package ovh.excale.vgreeter.commands.button; + +import lombok.SneakyThrows; +import lombok.extern.log4j.Log4j2; +import net.dv8tion.jda.api.components.actionrow.ActionRow; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; +import ovh.excale.vgreeter.commands.core.AbstractButtonCommand; +import ovh.excale.vgreeter.commands.core.CommandOptions; +import ovh.excale.vgreeter.track.TrackIndex; + +import java.util.Arrays; + +@Log4j2 +public class TrackIndexButtonCommand extends AbstractButtonCommand { + + public TrackIndexButtonCommand() { + super("trackindex", "List all the tracks"); + } + + @SneakyThrows + @Override + public @NotNull RestAction execute(ButtonInteractionEvent event) { + + CommandOptions command = CommandOptions.fromJson(event.getComponentId()); + //noinspection DuplicatedCode + TrackIndex index = new TrackIndex(command); + + try { + + index.fetch(); + + } catch(IllegalArgumentException e) { + return replyEphemeralWith(e.getMessage(), event); + } catch(Exception e) { + log.error(e.getMessage(), e); + return replyEphemeralWith("There has been an internal error", event); + } + + if(index.isEmpty()) + return replyEphemeralWith("Empty page", event); + + return event.editMessageEmbeds(index.buildEmbed().build()) + .setComponents(ActionRow.of(Arrays.asList(index.buildButtons()))); + + } + + private static RestAction replyEphemeralWith(String message, IReplyCallback event) { + return event.reply(message) + .setEphemeral(true); + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/core/AbstractButtonCommand.java b/src/main/java/ovh/excale/vgreeter/commands/core/AbstractButtonCommand.java new file mode 100644 index 0000000..bea6466 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/core/AbstractButtonCommand.java @@ -0,0 +1,37 @@ +package ovh.excale.vgreeter.commands.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; + +public abstract class AbstractButtonCommand extends AbstractCommand { + + protected AbstractButtonCommand(String name, String description) { + super(name, description, ButtonInteractionEvent.class); + } + + @Override + public abstract @NotNull RestAction execute(@NotNull ButtonInteractionEvent event); + + @Override + public boolean accepts(GenericEvent event) { + + if(!(event instanceof ButtonInteractionEvent)) + return false; + + CommandOptions command; + try { + + command = CommandOptions.fromJson(((ButtonInteractionEvent) event).getComponentId()); + + } catch(JsonProcessingException e) { + return false; + } + + return name.equalsIgnoreCase(command.getCommand()); + + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/core/AbstractCommand.java b/src/main/java/ovh/excale/vgreeter/commands/core/AbstractCommand.java new file mode 100644 index 0000000..ea8918e --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/core/AbstractCommand.java @@ -0,0 +1,37 @@ +package ovh.excale.vgreeter.commands.core; + +import lombok.Getter; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.hooks.EventListener; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.Nullable; + +@Getter +public abstract class AbstractCommand { + + protected final String name; + protected final String description; + + private final Class typeClass; + + protected AbstractCommand(String name, String description, Class typeClass) { + this.name = name; + this.description = description; + this.typeClass = typeClass; + } + + public abstract RestAction execute(EventType event); + + public abstract boolean accepts(GenericEvent eventType); + + // TODO: implement method in individual commands + public /*abstract*/ boolean bypassesMaintenance() { return true; } + + public boolean hasListener() { + return false; + } + + public @Nullable EventListener getListener() { + return null; + } +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/core/AbstractMessageCommand.java b/src/main/java/ovh/excale/vgreeter/commands/core/AbstractMessageCommand.java new file mode 100644 index 0000000..d96ca01 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/core/AbstractMessageCommand.java @@ -0,0 +1,40 @@ +package ovh.excale.vgreeter.commands.core; + +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +public abstract class AbstractMessageCommand extends AbstractCommand { + + public static final String PREFIX = "vg:"; + + protected AbstractMessageCommand(String name, String description) { + super(name, description, MessageReceivedEvent.class); + } + + @Override + public abstract @Nullable RestAction execute(@NotNull MessageReceivedEvent event); + + public boolean accepts(GenericEvent event) { + + if(!(event instanceof MessageReceivedEvent)) + return false; + + MessageReceivedEvent messageEvent = (MessageReceivedEvent) event; + if(messageEvent.isFromGuild()) + return false; + + String msgContent = messageEvent + .getMessage() + .getContentRaw() + .toLowerCase(Locale.ROOT); + + return msgContent.startsWith(PREFIX + name); + + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/core/AbstractSlashCommand.java b/src/main/java/ovh/excale/vgreeter/commands/core/AbstractSlashCommand.java new file mode 100644 index 0000000..76f4515 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/core/AbstractSlashCommand.java @@ -0,0 +1,36 @@ +package ovh.excale.vgreeter.commands.core; + +import lombok.Getter; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; + +public abstract class AbstractSlashCommand extends AbstractCommand { + + @Getter + private final CommandBuilder builder; + + protected AbstractSlashCommand(String name, String description) { + super(name, description, SlashCommandInteractionEvent.class); + + builder = CommandBuilder + .create(name) + .setDescription(description); + + } + + @Override + public abstract @NotNull RestAction execute(SlashCommandInteractionEvent event); + + @Override + public boolean accepts(GenericEvent event) { + return event instanceof SlashCommandInteractionEvent && name.equals(((SlashCommandInteractionEvent) event).getName()); + } + + public CommandData getData() { + return builder.build(); + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/utilities/CommandBuilder.java b/src/main/java/ovh/excale/vgreeter/commands/core/CommandBuilder.java similarity index 86% rename from src/main/java/ovh/excale/vgreeter/utilities/CommandBuilder.java rename to src/main/java/ovh/excale/vgreeter/commands/core/CommandBuilder.java index 7933847..62989ba 100644 --- a/src/main/java/ovh/excale/vgreeter/utilities/CommandBuilder.java +++ b/src/main/java/ovh/excale/vgreeter/commands/core/CommandBuilder.java @@ -1,8 +1,10 @@ -package ovh.excale.vgreeter.utilities; +package ovh.excale.vgreeter.commands.core; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData; import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import net.dv8tion.jda.api.interactions.commands.build.Commands; import java.util.LinkedList; import java.util.List; @@ -14,7 +16,7 @@ public static CommandBuilderPrototype create(String name) { return new CommandBuilderPrototype(name); } - private final CommandData commandData; + private final SlashCommandData commandData; private final List subcommands; private SubcommandData currentSubcommand; @@ -22,7 +24,7 @@ public static CommandBuilderPrototype create(String name) { private CommandBuilder(String name, String description) { - commandData = new CommandData(name, description); + commandData = Commands.slash(name, description); subcommands = new LinkedList<>(); currentSubcommand = null; @@ -68,7 +70,7 @@ public CommandBuilder subcommand(String name, String description) { return this; } - public CommandData build() { + public SlashCommandData build() { if(subcommand != null && subcommand) commandData.addSubcommands(subcommands); diff --git a/src/main/java/ovh/excale/vgreeter/commands/core/CommandKeyword.java b/src/main/java/ovh/excale/vgreeter/commands/core/CommandKeyword.java new file mode 100644 index 0000000..2a57d2d --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/core/CommandKeyword.java @@ -0,0 +1,63 @@ +package ovh.excale.vgreeter.commands.core; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.KeyDeserializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdKeySerializers.StringKeySerializer; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public enum CommandKeyword { + + // TODO: ADD OTHER PARAMETERS (e.g. track_id) + // TODO: CHANGE CASE TO snake_case (track_name) + TRACK_NAME("tn", "trackname"), + USER_ID("u", "user"); + + public static final String COMMAND = "cmd"; + public static final String SUBCOMMAND = "scmd"; + public static final String PAGE = "p"; + + private static final Map encodeRecord; + private static final Map decodeRecord; + + static { + + encodeRecord = new HashMap<>(); + decodeRecord = new HashMap<>(); + + for(CommandKeyword keyword : values()) { + encodeRecord.put(keyword.ext, keyword.key); + decodeRecord.put(keyword.key, keyword.ext); + } + } + + public final String key; + public final String ext; + + CommandKeyword(String key, String ext) { + this.key = key; + this.ext = ext; + } + + public static class KeywordDecoder extends KeyDeserializer { + + @Override + public Object deserializeKey(String key, DeserializationContext deserializationContext) { + return decodeRecord.getOrDefault(key, key); + } + + } + + public static class KeywordEncoder extends StringKeySerializer { + + @Override + public void serialize(Object ext, JsonGenerator g, SerializerProvider provider) throws IOException { + g.writeFieldName(encodeRecord.getOrDefault(ext.toString(), ext.toString())); + } + + } +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/core/CommandOptions.java b/src/main/java/ovh/excale/vgreeter/commands/core/CommandOptions.java new file mode 100644 index 0000000..6eeb6b9 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/core/CommandOptions.java @@ -0,0 +1,95 @@ +package ovh.excale.vgreeter.commands.core; + +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; +import ovh.excale.vgreeter.commands.core.CommandKeyword.KeywordDecoder; +import ovh.excale.vgreeter.commands.core.CommandKeyword.KeywordEncoder; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY; + +@JsonInclude(NON_EMPTY) +@Accessors(chain = true) +@Getter +@Setter +public class CommandOptions { + + private static final ObjectMapper objWriter = new ObjectMapper(); + private static final ObjectReader objReader = objWriter.readerFor(CommandOptions.class); + + public static CommandOptions fromJson(String payload) throws JsonProcessingException { + return objReader.readValue(payload); + } + + @JsonProperty(CommandKeyword.COMMAND) + private final String command; + + @JsonProperty(CommandKeyword.SUBCOMMAND) + private final String subcommand; + + @JsonProperty(CommandKeyword.PAGE) + private Integer page; + + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + @JsonAnySetter + @JsonAnyGetter + @JsonSerialize(keyUsing = KeywordEncoder.class) + @JsonDeserialize(keyUsing = KeywordDecoder.class) + private final Map extendedOptions; + + public CommandOptions(String command) { + this(command, null); + } + + @JsonCreator + public CommandOptions(@JsonProperty(CommandKeyword.COMMAND) String command, + @JsonProperty(CommandKeyword.SUBCOMMAND) String subcommand) { + this.command = Objects.requireNonNull(command); + this.subcommand = subcommand; + extendedOptions = new HashMap<>(); + } + + public boolean hasSubcommand() { + return subcommand != null && !subcommand.isEmpty(); + } + + @JsonIgnore + public int getPageSafe() { + return page != null ? page : 1; + } + + public Optional getOption(String key) { + return Optional.ofNullable(extendedOptions.get(key)); + } + + public CommandOptions putOption(String key, Object value) { + extendedOptions.put(key, value.toString()); + return this; + } + + public String json() throws JsonProcessingException { + return objWriter.writeValueAsString(this); + } + + @Override + public String toString() { + return "CommandOptions{" + "command='" + command + '\'' + ", subcommand='" + subcommand + '\'' + ", page=" + + page + ", extendedOptions=" + extendedOptions + '}'; + } + +} + + diff --git a/src/main/java/ovh/excale/vgreeter/commands/core/CommandRegister.java b/src/main/java/ovh/excale/vgreeter/commands/core/CommandRegister.java new file mode 100644 index 0000000..48c8e55 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/core/CommandRegister.java @@ -0,0 +1,105 @@ +package ovh.excale.vgreeter.commands.core; + +import lombok.SneakyThrows; +import net.dv8tion.jda.api.events.GenericEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.hooks.EventListener; +import net.dv8tion.jda.api.interactions.commands.build.CommandData; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; + +import java.lang.reflect.Method; +import java.util.*; +import java.util.stream.Stream; + +@Service +public class CommandRegister { + + private final ListenerRegister listenerRegister; + private final Map, Set>> masterRecord; + + private CommandRegister() { + listenerRegister = new ListenerRegister(); + masterRecord = new HashMap<>(); + } + + public > CommandRegister register(C command) { + + Class commandType = command.getTypeClass(); + //noinspection unchecked + Set commandSet = (Set) masterRecord.computeIfAbsent(commandType, k -> new HashSet<>()); + + commandSet.add(command); + if(command.hasListener()) + listenerRegister.register(command.getListener()); + + return this; + + } + + public CommandData[] getSlashCommandsData() { + + //noinspection unchecked + return Optional.ofNullable((Set) masterRecord.get(SlashCommandInteractionEvent.class)) + .map(Collection::stream) + .orElseGet(Stream::empty) + .map(AbstractSlashCommand::getData) + .toArray(CommandData[]::new); + + } + + public ListenerRegister getListener() { + return listenerRegister; + } + + private class ListenerRegister implements EventListener { + + public Set commandListeners; + + protected ListenerRegister() { + commandListeners = new HashSet<>(); + } + + protected void register(EventListener listener) { + commandListeners.add(listener); + } + + @SneakyThrows + @Override + public void onEvent(@NotNull GenericEvent event) { + + AbstractCommand abstractCommand = masterRecord.entrySet() + .stream() + .filter(entry -> entry.getKey() + .isInstance(event)) + .map(Map.Entry::getValue) + .flatMap(Collection::stream) + .filter(command -> command.accepts(event)) + .findFirst() + .orElse(null); + + if(abstractCommand != null) { + + //noinspection OptionalGetWithoutIsPresent + Method execMethod = Arrays.stream(abstractCommand.getClass() + .getDeclaredMethods()) + .filter(method -> method.getName() + .equals("execute")) + .findFirst() + .get(); + + Optional.ofNullable(execMethod.invoke(abstractCommand, event)) + .map(result -> ((RestAction) result)) + .ifPresent(RestAction::queue); + + } + + for(EventListener commandListener : commandListeners) + commandListener.onEvent(event); + + } + + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/message/RestartCommand.java b/src/main/java/ovh/excale/vgreeter/commands/message/RestartCommand.java new file mode 100644 index 0000000..32517f0 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/message/RestartCommand.java @@ -0,0 +1,52 @@ +package ovh.excale.vgreeter.commands.message; + +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.AbstractMessageCommand; +import ovh.excale.vgreeter.utilities.ArgumentsParser; + +// TODO: OWNER/MOD ONLY +public class RestartCommand extends AbstractMessageCommand { + + public RestartCommand() { + super("restart", ""); + } + + @Override + public @Nullable RestAction execute(@NotNull final MessageReceivedEvent event) { + + Message message = event.getMessage(); + ArgumentsParser arguments = new ArgumentsParser(message.getContentRaw()); + + boolean maintenance = arguments + .getArgumentString(1) + .map("maintenance"::equalsIgnoreCase) + .orElse(false); + String restartMessage = maintenance ? "*Restarting on maintenance mode...*" : "*Restarting...*"; + + message.reply(restartMessage) + // Wait for message to be sent and then restart + .complete(); + + final long authorId = event + .getAuthor() + .getIdLong(); + + VGreeterApplication.restart(() -> VGreeterApplication + .getApplicationContext() + .getBean(JDA.class) + .retrieveUserById(authorId) + .flatMap(User::openPrivateChannel) + .flatMap(dm -> dm.sendMessage("*Done!*")) + .queue(), maintenance); + + return null; + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/message/TrackUploadCommand.java b/src/main/java/ovh/excale/vgreeter/commands/message/TrackUploadCommand.java new file mode 100644 index 0000000..7e5d2c9 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/message/TrackUploadCommand.java @@ -0,0 +1,155 @@ +package ovh.excale.vgreeter.commands.message; + +import lombok.extern.log4j.Log4j2; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.requests.RestAction; +import org.gagravarr.ogg.OggFile; +import org.gagravarr.opus.OpusFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.AbstractMessageCommand; +import ovh.excale.vgreeter.models.TrackModel; +import ovh.excale.vgreeter.models.UserModel; +import ovh.excale.vgreeter.repositories.TrackRepository; +import ovh.excale.vgreeter.repositories.UserRepository; +import ovh.excale.vgreeter.services.TrackService; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Log4j2 +public class TrackUploadCommand extends AbstractMessageCommand { + + private static final Pattern TRACK_NAME_PATTERN = Pattern.compile("([\\w\\d-_]+)\\.opus"); + + private final UserRepository userRepo; + private final TrackService trackService; + + public TrackUploadCommand() { + super("upload", ""); + + userRepo = VGreeterApplication + .getApplicationContext() + .getBean(UserRepository.class); + + trackService = VGreeterApplication + .getApplicationContext() + .getBean(TrackService.class); + + } + + @Override + public @Nullable RestAction execute(@NotNull MessageReceivedEvent event) { + + // TODO: COMMAND PARAMETERS + + User user = event.getAuthor(); + if(user.isBot()) + return null; + + Message message = event.getMessage(); + Optional opt = userRepo.findById(user.getIdLong()); + + boolean hasNotAltname = !opt.isPresent() || opt + .get() + .getAltname() == null; + + if(hasNotAltname) + return message.reply("You must set an `/altname` first to upload a track"); + + UserModel userModel = opt.get(); + + List attachments = message.getAttachments(); + if(attachments.isEmpty()) + return message.reply("The track must be **opus encoded**"); + + Message.Attachment attachment = attachments.get(0); + String filename = attachment.getFileName(); + int size = attachment.getSize(); + + if(size > TrackService.DEFAULT_MAX_TRACK_SIZE) + return message.reply("The file is too big (Max. " + TrackService.DEFAULT_MAX_TRACK_SIZE + ")"); + + Matcher filenameMatcher = TRACK_NAME_PATTERN.matcher(filename.toLowerCase(Locale.ROOT)); + if(!filenameMatcher.matches()) + return message.reply("Filename or extension invalid (filename must be alphanumeric" + + " and can only contain *dashes* `-` and *underscores* `_`, extension must be `.opus`)"); + + InputStream in; + try { + + in = attachment + .getProxy() + .download() + .join(); + + } catch(Exception e) { + + log.warn("Error while retrieving Track InputStream", e); + return message.reply("There has been an internal error while computing the file, please retry. " + + "If the error persists, contact a developer"); + + } + + byte[] data = new byte[size]; + try { + + int read = 0, c; + do { + + c = in.read(data, read, size - read); + if(c > 0) + read += c; + + } while(c > 0); + + in.close(); + + if(read != size) { + log.warn("Size mismatch while reading InputStream. Expected size: " + size + ", read: " + read); + data = Arrays.copyOfRange(data, 0, read); + } + + new OpusFile(new OggFile(new ByteArrayInputStream(data))); + + // TODO: USE TRACK_SERVICE + TrackRepository trackRepo = trackService.getTrackRepo(); + String trackName = filenameMatcher.group(1); + + if(trackRepo.existsByNameAndUploader(trackName, userModel)) + return message.reply("You've already uploaded a track with the same name"); + + TrackModel track = TrackModel + .builder() + .name(filenameMatcher.group(1)) + .uploader(userModel) + .size((long) data.length) + .data(data) + .build(); + trackRepo.save(track); + + } catch(IOException e) { + + log.warn("Error while reading Track InputStream", e); + return message.reply("There has been an internal error while computing the file, please retry. " + + "If the error persists, contact a developer"); + + } catch(IllegalArgumentException e) { + // Not an opus track + return message.reply("The track is not **opus-encoded**."); + } + + return message.reply("Track successfully inserted!"); + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/AltnameCommand.java b/src/main/java/ovh/excale/vgreeter/commands/slash/AltnameCommand.java similarity index 78% rename from src/main/java/ovh/excale/vgreeter/commands/AltnameCommand.java rename to src/main/java/ovh/excale/vgreeter/commands/slash/AltnameCommand.java index f1120d1..6e7eed7 100644 --- a/src/main/java/ovh/excale/vgreeter/commands/AltnameCommand.java +++ b/src/main/java/ovh/excale/vgreeter/commands/slash/AltnameCommand.java @@ -1,18 +1,19 @@ -package ovh.excale.vgreeter.commands; +package ovh.excale.vgreeter.commands.slash; import net.dv8tion.jda.api.entities.User; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; +import net.dv8tion.jda.api.requests.RestAction; import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.AbstractSlashCommand; import ovh.excale.vgreeter.models.UserModel; import ovh.excale.vgreeter.repositories.UserRepository; import java.util.Optional; import java.util.regex.Pattern; -public class AltnameCommand extends AbstractCommand { +public class AltnameCommand extends AbstractSlashCommand { private static final Pattern ALTNAME_PATTERN = Pattern.compile("[\\w\\d-_]{3,}"); @@ -30,9 +31,9 @@ public AltnameCommand() { } @Override - public ReplyAction execute(SlashCommandEvent event) { + public RestAction execute(SlashCommandInteractionEvent event) { - ReplyAction reply; + RestAction reply; User user = event.getUser(); String name = Optional.ofNullable(event.getOption("username")) @@ -47,7 +48,10 @@ public ReplyAction execute(SlashCommandEvent event) { if(!userRepo.existsByAltname(name)) { UserModel userModel = userRepo.findById(user.getIdLong()) - .orElseGet(() -> new UserModel(user.getIdLong())); + .orElseGet(() -> UserModel + .builder() + .snowflake(user.getIdLong()) + .build()); userModel.setAltname(name); userRepo.save(userModel); diff --git a/src/main/java/ovh/excale/vgreeter/commands/PlaytestCommand.java b/src/main/java/ovh/excale/vgreeter/commands/slash/PlaytestCommand.java similarity index 75% rename from src/main/java/ovh/excale/vgreeter/commands/PlaytestCommand.java rename to src/main/java/ovh/excale/vgreeter/commands/slash/PlaytestCommand.java index 46c996b..3be7ae1 100644 --- a/src/main/java/ovh/excale/vgreeter/commands/PlaytestCommand.java +++ b/src/main/java/ovh/excale/vgreeter/commands/slash/PlaytestCommand.java @@ -1,27 +1,26 @@ -package ovh.excale.vgreeter.commands; +package ovh.excale.vgreeter.commands.slash; +import lombok.extern.log4j.Log4j2; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.entities.VoiceChannel; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; import net.dv8tion.jda.api.interactions.commands.OptionType; import net.dv8tion.jda.api.managers.AudioManager; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import net.dv8tion.jda.api.requests.RestAction; import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.AbstractSlashCommand; import ovh.excale.vgreeter.models.TrackModel; import ovh.excale.vgreeter.repositories.TrackRepository; import ovh.excale.vgreeter.services.DiscordService; -import ovh.excale.vgreeter.utilities.TrackPlayer; +import ovh.excale.vgreeter.track.TrackPlayer; import java.util.Optional; import java.util.Set; -public class PlaytestCommand extends AbstractCommand { - - private static final Logger logger = LoggerFactory.getLogger(PlaytestCommand.class); +@Log4j2 +public class PlaytestCommand extends AbstractSlashCommand { public PlaytestCommand() { super("playtest", "Test a track"); @@ -32,7 +31,7 @@ public PlaytestCommand() { } @Override - public ReplyAction execute(SlashCommandEvent event) { + public RestAction execute(SlashCommandInteractionEvent event) { Guild guild = event.getGuild(); Member member = event.getMember(); @@ -47,7 +46,7 @@ public ReplyAction execute(SlashCommandEvent event) { .setEphemeral(true); //noinspection ConstantConditions - VoiceChannel channel = member.getVoiceState() + AudioChannelUnion channel = member.getVoiceState() .getChannel(); if(channel == null) @@ -70,7 +69,7 @@ public ReplyAction execute(SlashCommandEvent event) { TrackPlayer trackPlayer = new TrackPlayer(opt.get()); if(!trackPlayer.canProvide()) { - logger.error("TrackPlayer cannot provide"); + log.error("TrackPlayer cannot provide"); return event.reply("There has been an internal error, retry or contact a developer.") .setEphemeral(true); } @@ -83,6 +82,8 @@ public ReplyAction execute(SlashCommandEvent event) { audioManager.openAudioConnection(channel); guildLocks.add(guild.getIdLong()); } catch(InsufficientPermissionException ignored) { + return event.reply("The bot is missing permission to connect or speak in that voice channel") + .setEphemeral(true); } return event.reply("Playing track `#" + trackId + "`") diff --git a/src/main/java/ovh/excale/vgreeter/commands/ProbabilityCommand.java b/src/main/java/ovh/excale/vgreeter/commands/slash/ProbabilityCommand.java similarity index 84% rename from src/main/java/ovh/excale/vgreeter/commands/ProbabilityCommand.java rename to src/main/java/ovh/excale/vgreeter/commands/slash/ProbabilityCommand.java index f2bb88c..f143f1b 100644 --- a/src/main/java/ovh/excale/vgreeter/commands/ProbabilityCommand.java +++ b/src/main/java/ovh/excale/vgreeter/commands/slash/ProbabilityCommand.java @@ -1,19 +1,20 @@ -package ovh.excale.vgreeter.commands; +package ovh.excale.vgreeter.commands.slash; import net.dv8tion.jda.api.Permission; import net.dv8tion.jda.api.entities.Guild; import net.dv8tion.jda.api.entities.Member; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionMapping; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; +import net.dv8tion.jda.api.requests.RestAction; import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.AbstractSlashCommand; import ovh.excale.vgreeter.models.GuildModel; import ovh.excale.vgreeter.repositories.GuildRepository; import java.util.Optional; -public class ProbabilityCommand extends AbstractCommand { +public class ProbabilityCommand extends AbstractSlashCommand { private final GuildRepository guildRepo; @@ -32,7 +33,7 @@ public ProbabilityCommand() { } @Override - public ReplyAction execute(SlashCommandEvent event) { + public RestAction execute(SlashCommandInteractionEvent event) { Guild guild = event.getGuild(); @@ -42,10 +43,13 @@ public ReplyAction execute(SlashCommandEvent event) { Member member = event.getMember(); Optional opt = guildRepo.findById(guild.getIdLong()); - GuildModel guildModel = opt.orElseGet(() -> new GuildModel(guild.getIdLong())); + GuildModel guildModel = opt.orElseGet(() -> GuildModel + .builder() + .id(guild.getIdLong()) + .build()); int prevProbab = guildModel.getJoinProbability(); - ReplyAction reply; + RestAction reply; String subcommand = Optional.ofNullable(event.getSubcommandName()) .orElse(""); diff --git a/src/main/java/ovh/excale/vgreeter/commands/slash/TrackDownloadCommand.java b/src/main/java/ovh/excale/vgreeter/commands/slash/TrackDownloadCommand.java new file mode 100644 index 0000000..178a3e2 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/slash/TrackDownloadCommand.java @@ -0,0 +1,58 @@ +package ovh.excale.vgreeter.commands.slash; + +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.requests.RestAction; +import net.dv8tion.jda.api.utils.FileUpload; +import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.AbstractSlashCommand; +import ovh.excale.vgreeter.models.TrackModel; +import ovh.excale.vgreeter.repositories.TrackRepository; + +import java.util.Optional; + +public class TrackDownloadCommand extends AbstractSlashCommand { + + private final TrackRepository trackRepo; + + public TrackDownloadCommand() { + super("trackdownload", "download command placeholder"); + + this.getBuilder() + .addOptionRequired("trackid", "The track to download", OptionType.INTEGER); + + trackRepo = VGreeterApplication.getApplicationContext() + .getBean(TrackRepository.class); + + } + + // TODO: 30sec cooldown (whole-guild scope) on download, probably with stopwatch and queue + + @Override + public RestAction execute(SlashCommandInteractionEvent event) { + + Guild guild = event.getGuild(); + + if(guild == null) + return event.reply("This command can be executed in a guild only") + .setEphemeral(true); + + //noinspection ConstantConditions + long trackId = Long.parseLong(event.getOption("trackid") + .getAsString()); + + Optional opt = trackRepo.findById(trackId); + + if(!opt.isPresent()) + return event.reply("No track with such id") + .setEphemeral(true); + + TrackModel track = opt.get(); + + return event.reply(String.format("Track `#%d`", track.getId())) + .addFiles(FileUpload.fromData(track.getData(), track.getName() + ".opus")); + + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/slash/TrackIndexSlashCommand.java b/src/main/java/ovh/excale/vgreeter/commands/slash/TrackIndexSlashCommand.java new file mode 100644 index 0000000..3ee1c74 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/slash/TrackIndexSlashCommand.java @@ -0,0 +1,84 @@ +package ovh.excale.vgreeter.commands.slash; + +import lombok.extern.log4j.Log4j2; +import net.dv8tion.jda.api.components.actionrow.ActionRow; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.requests.RestAction; +import org.jetbrains.annotations.NotNull; +import ovh.excale.vgreeter.commands.core.AbstractSlashCommand; +import ovh.excale.vgreeter.commands.core.CommandOptions; +import ovh.excale.vgreeter.track.TrackIndex; + +import java.util.Arrays; +import java.util.Optional; + +import static ovh.excale.vgreeter.commands.core.CommandKeyword.TRACK_NAME; +import static ovh.excale.vgreeter.commands.core.CommandKeyword.USER_ID; +import static ovh.excale.vgreeter.track.TrackIndex.*; + +@Log4j2 +public class TrackIndexSlashCommand extends AbstractSlashCommand { + + public TrackIndexSlashCommand() { + super("trackindex", "List all the tracks"); + + getBuilder() + // [SUB] all + .subcommand(FILTER_ALL, "Search for all tracks") + .addOption("page", "Page number", OptionType.INTEGER) + // [SUB] name + .subcommand(FILTER_NAME, "Search for all tracks with something in the name") + .addOptionRequired(TRACK_NAME.ext, "Track name", OptionType.STRING) + .addOption("page", "Page number", OptionType.INTEGER) + // [SUB] user + .subcommand(FILTER_USER, "Search for tracks by a user") + .addOptionRequired(USER_ID.ext, "The user to query for", OptionType.USER) + .addOption("page", "Page number", OptionType.INTEGER); + + } + + @Override + public @NotNull RestAction execute(SlashCommandInteractionEvent event) { + + int page = Optional.ofNullable(event.getOption("page")) + .map(OptionMapping::getAsLong) + .map(Long::intValue) + .orElse(1); + + CommandOptions command = new CommandOptions(event.getName(), event.getSubcommandName()).setPage(page); + event.getOptions() + .stream() + .filter(option -> !"page".equals(option.getName())) + .forEach(option -> command.putOption(option.getName(), option.getAsString())); + + //noinspection DuplicatedCode + TrackIndex index = new TrackIndex(command); + try { + + index.fetch(); + + } catch(IllegalArgumentException e) { + return replyEphemeralWith(e.getMessage(), event); + } catch(Exception e) { + log.error(e.getMessage(), e); + return replyEphemeralWith("There has been an internal error", event); + } + + if(index.isEmpty()) + return replyEphemeralWith("Empty page", event); + + return event.replyEmbeds(index.buildEmbed().build()) + .setEphemeral(true) + .addComponents(ActionRow.of(Arrays.asList(index.buildButtons()))); + + } + + private static RestAction replyEphemeralWith(String message, IReplyCallback event) { + return event.reply(message) + .setEphemeral(true); + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/TracknameCommand.java b/src/main/java/ovh/excale/vgreeter/commands/slash/TrackNameCommand.java similarity index 82% rename from src/main/java/ovh/excale/vgreeter/commands/TracknameCommand.java rename to src/main/java/ovh/excale/vgreeter/commands/slash/TrackNameCommand.java index ef439f0..e437a6f 100644 --- a/src/main/java/ovh/excale/vgreeter/commands/TracknameCommand.java +++ b/src/main/java/ovh/excale/vgreeter/commands/slash/TrackNameCommand.java @@ -1,10 +1,11 @@ -package ovh.excale.vgreeter.commands; +package ovh.excale.vgreeter.commands.slash; import net.dv8tion.jda.api.entities.User; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.interactions.commands.OptionType; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; +import net.dv8tion.jda.api.requests.RestAction; import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.AbstractSlashCommand; import ovh.excale.vgreeter.models.TrackModel; import ovh.excale.vgreeter.models.UserModel; import ovh.excale.vgreeter.repositories.TrackRepository; @@ -12,13 +13,13 @@ import java.util.Optional; import java.util.regex.Pattern; -public class TracknameCommand extends AbstractCommand { +public class TrackNameCommand extends AbstractSlashCommand { private static final Pattern TRACKNAME_PATTERN = Pattern.compile("[\\w\\d-_]+"); private final TrackRepository trackRepo; - public TracknameCommand() { + public TrackNameCommand() { super("trackname", "Edit the name of a track"); this.getBuilder() @@ -32,7 +33,7 @@ public TracknameCommand() { } @Override - public ReplyAction execute(SlashCommandEvent event) { + public RestAction execute(SlashCommandInteractionEvent event) { //noinspection ConstantConditions long trackId = Long.parseLong(event.getOption("trackid") diff --git a/src/main/java/ovh/excale/vgreeter/commands/slash/TrackRemoveCommand.java b/src/main/java/ovh/excale/vgreeter/commands/slash/TrackRemoveCommand.java new file mode 100644 index 0000000..ad00c7a --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/commands/slash/TrackRemoveCommand.java @@ -0,0 +1,69 @@ +package ovh.excale.vgreeter.commands.slash; + +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.requests.RestAction; +import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.AbstractSlashCommand; +import ovh.excale.vgreeter.models.TrackModel; +import ovh.excale.vgreeter.repositories.TrackRepository; + +import java.util.Optional; + +public class TrackRemoveCommand extends AbstractSlashCommand { + + private final TrackRepository trackRepo; + + public TrackRemoveCommand() { + super("trackremove", "Delete a track"); + this + .getBuilder() + .addOptionRequired("trackid", "The track to remove", OptionType.INTEGER); + + trackRepo = VGreeterApplication + .getApplicationContext() + .getBean(TrackRepository.class); + + } + + @Override + public RestAction execute(SlashCommandInteractionEvent event) { + + RestAction reply; + User user = event.getUser(); + + //noinspection ConstantConditions + long trackId = event + .getOption("trackid") + .getAsLong(); + + Optional opt = trackRepo.findById(trackId); + if(!opt.isPresent()) + reply = event + .reply("No track with such id") + .setEphemeral(true); + else { + + TrackModel track = opt.get(); + Long userId = user.getIdLong(); + + if(!userId.equals(track.getUploaderId())) + reply = event + .reply("You're not the uploader of track `" + trackId + "`") + .setEphemeral(true); + else { + + trackRepo.delete(track); + reply = event + .reply("Track `" + trackId + "` deleted successfully") + .setEphemeral(true); + + } + + } + + return reply; + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/commands/UploadCommand.java b/src/main/java/ovh/excale/vgreeter/commands/slash/UploadHelpCommand.java similarity index 68% rename from src/main/java/ovh/excale/vgreeter/commands/UploadCommand.java rename to src/main/java/ovh/excale/vgreeter/commands/slash/UploadHelpCommand.java index 56ffc2d..4948774 100644 --- a/src/main/java/ovh/excale/vgreeter/commands/UploadCommand.java +++ b/src/main/java/ovh/excale/vgreeter/commands/slash/UploadHelpCommand.java @@ -1,16 +1,17 @@ -package ovh.excale.vgreeter.commands; +package ovh.excale.vgreeter.commands.slash; -import net.dv8tion.jda.api.events.interaction.SlashCommandEvent; -import net.dv8tion.jda.api.requests.restaction.interactions.ReplyAction; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.requests.RestAction; +import ovh.excale.vgreeter.commands.core.AbstractSlashCommand; -public class UploadCommand extends AbstractCommand { +public class UploadHelpCommand extends AbstractSlashCommand { - public UploadCommand() { + public UploadHelpCommand() { super("upload", "Show help to upload a track"); } @Override - public ReplyAction execute(SlashCommandEvent event) { + public RestAction execute(SlashCommandInteractionEvent event) { //noinspection StringBufferReplaceableByString StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/ovh/excale/vgreeter/models/GuildModel.java b/src/main/java/ovh/excale/vgreeter/models/GuildModel.java index 08a4d74..4cae578 100644 --- a/src/main/java/ovh/excale/vgreeter/models/GuildModel.java +++ b/src/main/java/ovh/excale/vgreeter/models/GuildModel.java @@ -1,10 +1,18 @@ package ovh.excale.vgreeter.models; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.Id; -import javax.persistence.Table; - +import lombok.*; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Setter +@Getter +@ToString @Entity @Table(name = "guild") public class GuildModel { @@ -15,34 +23,9 @@ public class GuildModel { @Column(name = "id_guild") private Long id; + @Builder.Default @Column(name = "join_probability") - private Integer joinProbability; - - public GuildModel() { - joinProbability = DEFAULT_JOIN_PROBABILITY; - } - - public GuildModel(Long id) { - this.id = id; - joinProbability = DEFAULT_JOIN_PROBABILITY; - } - - public Long getId() { - return id; - } - - public GuildModel setId(Long id) { - this.id = id; - return this; - } - - public Integer getJoinProbability() { - return joinProbability; - } - - public GuildModel setJoinProbability(Integer joinProbability) { - this.joinProbability = joinProbability; - return this; - } + private Integer joinProbability = DEFAULT_JOIN_PROBABILITY; } + diff --git a/src/main/java/ovh/excale/vgreeter/models/TrackModel.java b/src/main/java/ovh/excale/vgreeter/models/TrackModel.java index ab760c4..2164565 100644 --- a/src/main/java/ovh/excale/vgreeter/models/TrackModel.java +++ b/src/main/java/ovh/excale/vgreeter/models/TrackModel.java @@ -1,12 +1,19 @@ package ovh.excale.vgreeter.models; +import lombok.*; import org.gagravarr.ogg.OggPacketReader; -import javax.persistence.*; +import jakarta.persistence.*; import java.io.ByteArrayInputStream; import java.sql.Timestamp; import java.time.Instant; +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Setter +@Getter +@ToString @Entity @Table(name = "track") public class TrackModel { @@ -23,76 +30,25 @@ public class TrackModel { @Basic private Long size; + @Builder.Default @Basic - private Timestamp uploadDate; + private Timestamp uploadDate = Timestamp.from(Instant.now()); + @ToString.Exclude @Basic(fetch = FetchType.LAZY) private byte[] data; + @ToString.Exclude @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "uploader_id") private UserModel uploader; - public TrackModel() { - uploadDate = Timestamp.from(Instant.now()); - } - - public Long getId() { - return id; - } - - public TrackModel setId(Long id) { - this.id = id; - return this; - } - - public String getName() { - return name; - } - - public TrackModel setName(String name) { - this.name = name; - return this; - } - - public Long getSize() { - return size; - } - - public TrackModel setSize(Long size) { - this.size = size; - return this; - } - - public byte[] getData() { - return data; - } - - public TrackModel setData(byte[] data) { - this.data = data; - return this; - } - - public UserModel getUploader() { - return uploader; - } - - public TrackModel setUploader(UserModel uploader) { - this.uploader = uploader; - return this; - } - - public Timestamp getUploadDate() { - return uploadDate; - } - - public TrackModel setUploadDate(Timestamp uploadDate) { - this.uploadDate = uploadDate; - return this; - } + @Column(name = "uploader_id", insertable = false, updatable = false) + private Long uploaderId; public OggPacketReader getPacketReader() { return new OggPacketReader(new ByteArrayInputStream(getData())); } + } diff --git a/src/main/java/ovh/excale/vgreeter/models/UserModel.java b/src/main/java/ovh/excale/vgreeter/models/UserModel.java index 463b273..fe81fdf 100644 --- a/src/main/java/ovh/excale/vgreeter/models/UserModel.java +++ b/src/main/java/ovh/excale/vgreeter/models/UserModel.java @@ -1,7 +1,16 @@ package ovh.excale.vgreeter.models; -import javax.persistence.*; +import lombok.*; +import jakarta.persistence.*; +import java.util.Set; + +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Setter +@Getter +@ToString @Entity @Table(name = "\"user\"") public class UserModel { @@ -12,44 +21,14 @@ public class UserModel { @Basic private String altname; + @Builder.Default @Basic @Column(name = "tracks_max") - private Integer trackMaxSize; - - public UserModel() { - trackMaxSize = 64 * 1024; - } - - public UserModel(long snowflake) { - this.snowflake = snowflake; - trackMaxSize = 64 * 1024; - } - - public Long getSnowflake() { - return snowflake; - } - - public UserModel setSnowflake(Long snowflake) { - this.snowflake = snowflake; - return this; - } - - public String getAltname() { - return altname; - } - - public UserModel setAltname(String altname) { - this.altname = altname; - return this; - } - - public Integer getTrackMaxSize() { - return trackMaxSize; - } - - public UserModel setTrackMaxSize(Integer maxTracks) { - this.trackMaxSize = maxTracks; - return this; - } + private Integer trackMaxSize = 64 * 1024; + + @ToString.Exclude + @OneToMany(fetch = FetchType.LAZY, mappedBy = "uploader") + private Set tracks; + } diff --git a/src/main/java/ovh/excale/vgreeter/repositories/GuildRepository.java b/src/main/java/ovh/excale/vgreeter/repositories/GuildRepository.java index ef3a6e4..2ca7c51 100644 --- a/src/main/java/ovh/excale/vgreeter/repositories/GuildRepository.java +++ b/src/main/java/ovh/excale/vgreeter/repositories/GuildRepository.java @@ -1,10 +1,10 @@ package ovh.excale.vgreeter.repositories; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ovh.excale.vgreeter.models.GuildModel; @Repository -public interface GuildRepository extends CrudRepository { +public interface GuildRepository extends JpaRepository { } diff --git a/src/main/java/ovh/excale/vgreeter/repositories/TrackRepository.java b/src/main/java/ovh/excale/vgreeter/repositories/TrackRepository.java index 874433d..8c978fa 100644 --- a/src/main/java/ovh/excale/vgreeter/repositories/TrackRepository.java +++ b/src/main/java/ovh/excale/vgreeter/repositories/TrackRepository.java @@ -1,13 +1,21 @@ package ovh.excale.vgreeter.repositories; -import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ovh.excale.vgreeter.models.TrackModel; import ovh.excale.vgreeter.models.UserModel; @Repository -public interface TrackRepository extends PagingAndSortingRepository { +public interface TrackRepository extends JpaRepository { boolean existsByNameAndUploader(String name, UserModel uploader); + @Query("select t from TrackModel t where lower(t.name) like lower(?1)") + Page findAllByNameQuery(String name, Pageable pageable); + + Page findAllByUploaderIdIs(long userId, Pageable pageable); + } diff --git a/src/main/java/ovh/excale/vgreeter/repositories/UserRepository.java b/src/main/java/ovh/excale/vgreeter/repositories/UserRepository.java index 6f1c23e..662e412 100644 --- a/src/main/java/ovh/excale/vgreeter/repositories/UserRepository.java +++ b/src/main/java/ovh/excale/vgreeter/repositories/UserRepository.java @@ -1,11 +1,11 @@ package ovh.excale.vgreeter.repositories; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ovh.excale.vgreeter.models.UserModel; @Repository -public interface UserRepository extends CrudRepository { +public interface UserRepository extends JpaRepository { boolean existsByAltname(String altname); diff --git a/src/main/java/ovh/excale/vgreeter/services/DiscordEventHandlerService.java b/src/main/java/ovh/excale/vgreeter/services/DiscordEventHandlerService.java deleted file mode 100644 index 29e8949..0000000 --- a/src/main/java/ovh/excale/vgreeter/services/DiscordEventHandlerService.java +++ /dev/null @@ -1,220 +0,0 @@ -package ovh.excale.vgreeter.services; - -import net.dv8tion.jda.api.entities.*; -import net.dv8tion.jda.api.events.guild.voice.GuildVoiceJoinEvent; -import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent; -import net.dv8tion.jda.api.events.message.priv.PrivateMessageReceivedEvent; -import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; -import net.dv8tion.jda.api.hooks.ListenerAdapter; -import net.dv8tion.jda.api.managers.AudioManager; -import org.gagravarr.ogg.OggFile; -import org.gagravarr.opus.OpusFile; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Service; -import ovh.excale.vgreeter.models.GuildModel; -import ovh.excale.vgreeter.models.TrackModel; -import ovh.excale.vgreeter.models.UserModel; -import ovh.excale.vgreeter.repositories.GuildRepository; -import ovh.excale.vgreeter.repositories.TrackRepository; -import ovh.excale.vgreeter.repositories.UserRepository; -import ovh.excale.vgreeter.utilities.TrackPlayer; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -@Service -public class DiscordEventHandlerService extends ListenerAdapter { - - private final static Logger logger = LoggerFactory.getLogger(DiscordEventHandlerService.class); - private static final Pattern TRACK_NAME_PATTERN = Pattern.compile("([\\w\\d-_]+)\\.opus"); - - private final UserRepository userRepo; - private final GuildRepository guildRepo; - private final TrackService trackService; - - private final Random random; - - public DiscordEventHandlerService(UserRepository userRepo, GuildRepository guildRepo, TrackService trackService) { - this.userRepo = userRepo; - this.guildRepo = guildRepo; - this.trackService = trackService; - random = new Random(); - } - - @Override - public void onGuildVoiceJoin(@NotNull GuildVoiceJoinEvent event) { - - Guild guild = event.getGuild(); - User user = event.getMember() - .getUser(); - - Set guildLocks = DiscordService.getGuildVoiceLocks(); - if(user.isBot() || guildLocks.contains(guild.getIdLong())) - return; - - int joinProbability; - - Optional opt = guildRepo.findById(guild.getIdLong()); - if(opt.isPresent()) - joinProbability = opt.get() - .getJoinProbability(); - else { - GuildModel guildModel = new GuildModel(guild.getIdLong()); - joinProbability = guildModel.getJoinProbability(); - guildRepo.save(guildModel); - } - - if(random.nextInt(100) + 1 > joinProbability) - return; - - TrackPlayer trackPlayer = new TrackPlayer(trackService.randomTrack()); - if(!trackPlayer.canProvide()) { - logger.error("TrackPlayer cannot provide"); - return; - } - - VoiceChannel channel = event.getChannelJoined(); - AudioManager audioManager = event.getGuild() - .getAudioManager(); - - trackPlayer.setTrackEndAction(audioManager::closeAudioConnection); - - try { - audioManager.setSendingHandler(trackPlayer); - audioManager.openAudioConnection(channel); - guildLocks.add(guild.getIdLong()); - } catch(InsufficientPermissionException ignored) { - } - - } - - @Override - public void onGuildVoiceLeave(@NotNull GuildVoiceLeaveEvent event) { - - Guild guild = event.getGuild(); - User user = event.getMember() - .getUser(); - SelfUser selfUser = event.getJDA() - .getSelfUser(); - - Set guildLocks = DiscordService.getGuildVoiceLocks(); - if(user.getIdLong() == selfUser.getIdLong()) - guildLocks.remove(guild.getIdLong()); - - } - - @Override - public void onPrivateMessageReceived(@NotNull PrivateMessageReceivedEvent event) { - - User user = event.getAuthor(); - if(user.isBot()) - return; - - Message message = event.getMessage(); - Optional opt = userRepo.findById(user.getIdLong()); - - if(!opt.isPresent() || opt.get() - .getAltname() == null) { - message.reply("You must set an `/altname` first to upload a track") - .queue(); - return; - } - - UserModel userModel = opt.get(); - - List attachments = message.getAttachments(); - if(attachments.isEmpty()) { - message.reply("To upload a track you must send me an **opus encoded** file") - .queue(); - return; - } - - Message.Attachment attachment = attachments.get(0); - String filename = attachment.getFileName(); - int size = attachment.getSize(); - - if(size > TrackService.DEFAULT_MAX_TRACK_SIZE) { - message.reply("The file is too big") - .queue(); - return; - } - - Matcher filenameMatcher = TRACK_NAME_PATTERN.matcher(filename.toLowerCase(Locale.ROOT)); - if(!filenameMatcher.matches()) { - message.reply( - "Filename or extension invalid (filename must be alphanumeric and can only contain *dashes* `-` and *underscores* `_`, extension must be `.opus`)") - .queue(); - return; - } - - attachment.retrieveInputStream() - .thenAcceptAsync(in -> { - - byte[] data = new byte[size]; - - try { - - int read = 0, c; - - do { - c = in.read(data, read, size - read); - if(c > 0) - read += c; - } while(c > 0); - - in.close(); - - if(read != size) { - logger.warn("Size mismatch while reading InputStream. Expected size: " + size + ", read: " + - read); - data = Arrays.copyOfRange(data, 0, read); - } - - new OpusFile(new OggFile(new ByteArrayInputStream(data))); - - // TODO: USE TRACK_SERVICE - TrackRepository trackRepo = trackService.getTrackRepo(); - String trackname = filenameMatcher.group(1); - - if(trackRepo.existsByNameAndUploader(trackname, userModel)) { - message.reply("You've already uploaded a track with the same name") - .queue(); - return; - } - - TrackModel track = new TrackModel().setName(filenameMatcher.group(1)) - .setUploader(userModel) - .setSize((long) data.length) - .setData(data); - trackRepo.save(track); - - message.reply("Track successfully inserted!") - .queue(); - - } catch(IOException e) { - throw new UncheckedIOException(e); - } catch(IllegalArgumentException e) { - - // Not an opus track - message.reply("The provided file is not an **opus-encoded** track.") - .queue(); - - } - }) - .exceptionally(e -> { - message.reply( - "There has been an internal error while computing the file, please retry or contact a developer") - .queue(); - logger.warn("Error while opening OpusFile", e); - return null; - }); - - } - -} diff --git a/src/main/java/ovh/excale/vgreeter/services/DiscordService.java b/src/main/java/ovh/excale/vgreeter/services/DiscordService.java index a98b429..ac2187a 100644 --- a/src/main/java/ovh/excale/vgreeter/services/DiscordService.java +++ b/src/main/java/ovh/excale/vgreeter/services/DiscordService.java @@ -1,71 +1,101 @@ package ovh.excale.vgreeter.services; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; +import club.minnced.discord.jdave.interop.JDaveSessionFactory; import net.dv8tion.jda.api.JDA; import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.audio.AudioModuleConfig; import net.dv8tion.jda.api.entities.Activity; import net.dv8tion.jda.api.interactions.commands.Command; import net.dv8tion.jda.api.requests.GatewayIntent; import net.dv8tion.jda.api.utils.cache.CacheFlag; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.stereotype.Service; -import ovh.excale.vgreeter.commands.*; +import ovh.excale.vgreeter.commands.button.CloseEmbedCommand; +import ovh.excale.vgreeter.commands.button.TrackIndexButtonCommand; +import ovh.excale.vgreeter.commands.slash.AltnameCommand; +import ovh.excale.vgreeter.commands.slash.ProbabilityCommand; +import ovh.excale.vgreeter.commands.message.RestartCommand; +import ovh.excale.vgreeter.commands.slash.UploadHelpCommand; +import ovh.excale.vgreeter.commands.core.CommandRegister; +import ovh.excale.vgreeter.commands.message.TrackUploadCommand; +import ovh.excale.vgreeter.commands.slash.*; -import javax.security.auth.login.LoginException; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; +@Log4j2 @Service public class DiscordService { - private static final Logger logger = LoggerFactory.getLogger(DiscordService.class); + @Getter private static final Set guildVoiceLocks = Collections.synchronizedSet(new HashSet<>()); private final JDA jda; - public DiscordService(DiscordEventHandlerService eventHandler, CommandRegister commands, - @Value("${env.DISCORD_TOKEN}") String token) throws LoginException, InterruptedException { + public DiscordService( + VoiceChannelHandler eventHandler, + CommandRegister commands, + @Value("${env.DISCORD_TOKEN}") String token + ) throws InterruptedException { - jda = JDABuilder.create(token, + jda = JDABuilder + .create( + token, GatewayIntent.GUILD_VOICE_STATES, GatewayIntent.DIRECT_MESSAGES, - GatewayIntent.GUILD_VOICE_STATES) - .disableCache(CacheFlag.ACTIVITY, - CacheFlag.ONLINE_STATUS, - CacheFlag.CLIENT_STATUS, - CacheFlag.MEMBER_OVERRIDES, - CacheFlag.EMOTE) - .setActivity(Activity.listening("people")) - .addEventListeners(eventHandler, commands.getListener()) - .build() - .awaitReady(); + GatewayIntent.MESSAGE_CONTENT + ) + .disableCache( + CacheFlag.ACTIVITY, + CacheFlag.ONLINE_STATUS, + CacheFlag.CLIENT_STATUS, + CacheFlag.MEMBER_OVERRIDES, + CacheFlag.EMOJI + ) + .setActivity(Activity.listening("people")) + .addEventListeners(eventHandler, commands.getListener()) + .setAudioModuleConfig( + new AudioModuleConfig().withDaveSessionFactory(new JDaveSessionFactory()) + ) + .build() + .awaitReady(); + log.info("JDA connected"); - jda.updateCommands() - .addCommands(commands.register(new ProbabilityCommand()) + String commandListString = jda + .updateCommands() + .addCommands(commands + // SLASH COMMANDS + .register(new ProbabilityCommand()) .register(new AltnameCommand()) - .register(new UploadCommand()) + .register(new UploadHelpCommand()) .register(new PlaytestCommand()) - .register(new TracknameCommand()) - .register(new TrackIndexCommand()) - .getData()) - .queue(commandList -> logger.info("[Registered commands] " + commandList.stream() - .map(Command::getName) - .collect(Collectors.joining(", "))), e -> logger.warn("Couldn't update commands", e)); + .register(new TrackNameCommand()) + .register(new TrackIndexSlashCommand()) + .register(new TrackRemoveCommand()) + .register(new TrackDownloadCommand()) + // MESSAGE COMMANDS + .register(new RestartCommand()) + .register(new TrackUploadCommand()) + // BUTTON COMMANDS + .register(new CloseEmbedCommand()) + .register(new TrackIndexButtonCommand()) + .getSlashCommandsData()) + .complete() + .stream() + .map(Command::getName) + .collect(Collectors.joining(", ")); - logger.info("JDA connected"); + log.info("[Registered SlashCommands] " + commandListString); } - public static Set getGuildVoiceLocks() { - return guildVoiceLocks; - } - - @Bean + @Bean(destroyMethod = "shutdown") public JDA getJda() { return jda; } diff --git a/src/main/java/ovh/excale/vgreeter/services/TrackService.java b/src/main/java/ovh/excale/vgreeter/services/TrackService.java index 7f8848a..08fbce3 100644 --- a/src/main/java/ovh/excale/vgreeter/services/TrackService.java +++ b/src/main/java/ovh/excale/vgreeter/services/TrackService.java @@ -1,5 +1,6 @@ package ovh.excale.vgreeter.services; +import lombok.Getter; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -11,12 +12,14 @@ public class TrackService { public static final int DEFAULT_MAX_TRACK_SIZE = 1024 * 64; + @Getter private final TrackRepository trackRepo; public TrackService(TrackRepository trackRepo) { this.trackRepo = trackRepo; } + // TODO: nullsafe public TrackModel randomTrack() { long qty = trackRepo.count(); @@ -31,8 +34,4 @@ public TrackModel randomTrack() { return track; } - public TrackRepository getTrackRepo() { - return trackRepo; - } - } diff --git a/src/main/java/ovh/excale/vgreeter/services/VoiceChannelHandler.java b/src/main/java/ovh/excale/vgreeter/services/VoiceChannelHandler.java new file mode 100644 index 0000000..c281ba6 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/services/VoiceChannelHandler.java @@ -0,0 +1,93 @@ +package ovh.excale.vgreeter.services; + +import lombok.extern.log4j.Log4j2; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.SelfUser; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.unions.AudioChannelUnion; +import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent; +import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.managers.AudioManager; +import org.jetbrains.annotations.NotNull; +import org.springframework.stereotype.Service; +import ovh.excale.vgreeter.models.GuildModel; +import ovh.excale.vgreeter.repositories.GuildRepository; +import ovh.excale.vgreeter.track.TrackPlayer; + +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +@Log4j2 +@Service +public class VoiceChannelHandler extends ListenerAdapter { + + private final GuildRepository guildRepo; + private final TrackService trackService; + + private final Random random; + + public VoiceChannelHandler(GuildRepository guildRepo, TrackService trackService) { + this.guildRepo = guildRepo; + this.trackService = trackService; + random = new Random(); + } + + // TODO: DISABLE VOICE EVENT HANDLING UNDER MAINTENANCE + + @Override + public void onGuildVoiceUpdate(@NotNull GuildVoiceUpdateEvent event) { + + Guild guild = event.getGuild(); + User user = event.getMember().getUser(); + Set guildLocks = DiscordService.getGuildVoiceLocks(); + + if(event.getChannelLeft() != null) { + SelfUser selfUser = event.getJDA().getSelfUser(); + if(user.getIdLong() == selfUser.getIdLong()) + guildLocks.remove(guild.getIdLong()); + } + + if(event.getChannelJoined() == null) + return; + + if(user.isBot() || guildLocks.contains(guild.getIdLong())) + return; + + int joinProbability; + + Optional opt = guildRepo.findById(guild.getIdLong()); + if(opt.isPresent()) + joinProbability = opt.get().getJoinProbability(); + else { + GuildModel guildModel = GuildModel.builder().id(guild.getIdLong()).build(); + joinProbability = guildModel.getJoinProbability(); + guildRepo.save(guildModel); + } + + if(random.nextInt(100) + 1 > joinProbability) + return; + + TrackPlayer trackPlayer = new TrackPlayer(trackService.randomTrack()); + if(!trackPlayer.canProvide()) { + log.error("TrackPlayer cannot provide"); + return; + } + + AudioChannelUnion channel = event.getChannelJoined(); + AudioManager audioManager = guild.getAudioManager(); + trackPlayer.setTrackEndAction(audioManager::closeAudioConnection); + + try { + audioManager.setSendingHandler(trackPlayer); + audioManager.openAudioConnection(channel); + guildLocks.add(guild.getIdLong()); + } catch(InsufficientPermissionException ignored) { + // The bot doesn't have permissions to connect to the Voice Channel, do nothing + } + + } + + +} diff --git a/src/main/java/ovh/excale/vgreeter/track/TrackIndex.java b/src/main/java/ovh/excale/vgreeter/track/TrackIndex.java new file mode 100644 index 0000000..5e98947 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/track/TrackIndex.java @@ -0,0 +1,161 @@ +package ovh.excale.vgreeter.track; + +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.components.buttons.Button; +import org.jetbrains.annotations.NotNull; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import ovh.excale.vgreeter.VGreeterApplication; +import ovh.excale.vgreeter.commands.core.CommandOptions; +import ovh.excale.vgreeter.models.TrackModel; +import ovh.excale.vgreeter.repositories.TrackRepository; +import ovh.excale.vgreeter.utilities.Emojis; + +import java.awt.*; +import java.util.Objects; +import java.util.stream.Collectors; + +import static ovh.excale.vgreeter.commands.core.CommandKeyword.TRACK_NAME; +import static ovh.excale.vgreeter.commands.core.CommandKeyword.USER_ID; + +public class TrackIndex { + + public static final String FILTER_ALL = "all"; + public static final String FILTER_NAME = "name"; + public static final String FILTER_USER = "user"; + public static final int DEFAULT_PAGE_SIZE = 15; + + private final CommandOptions options; + private final TrackRepository trackRepo; + private Page trackPage; + + @Setter + @Getter + private Color embedColor; + private String filterContent; + + @Setter + @Getter + private int pageSize; + + public TrackIndex(CommandOptions options) { + this.options = Objects.requireNonNull(options); + trackRepo = VGreeterApplication.getApplicationContext() + .getBean(TrackRepository.class); + + trackPage = null; + filterContent = null; + embedColor = Color.BLUE; + pageSize = DEFAULT_PAGE_SIZE; + + } + + private void trackPageCheck() throws IllegalStateException { + if(trackPage == null) + throw new IllegalStateException(); + } + + public boolean isEmpty() { + return trackPage == null || !trackPage.hasContent(); + } + + public void fetch() throws IllegalArgumentException { + + int humanBasedPage = options.getPageSafe(); + if(humanBasedPage < 1) + throw new IllegalArgumentException("Page option must be positive"); + + Sort sorting = Sort.by(Sort.Direction.ASC, "id"); + String filter = options.hasSubcommand() ? options.getSubcommand() : FILTER_ALL; + + switch(filter) { + + case FILTER_ALL: + + trackPage = trackRepo.findAll(PageRequest.of(humanBasedPage - 1, pageSize, sorting)); + + break; + + case FILTER_NAME: + + String trackName = options.getOption(TRACK_NAME.ext) + .orElseThrow(() -> new IllegalArgumentException("Missing parameter " + TRACK_NAME.ext)); + filterContent = trackName; + + String formattedTrackName = "%" + trackName.replaceAll("\\s+", "%") + "%"; + trackPage = trackRepo.findAllByNameQuery(formattedTrackName, + PageRequest.of(humanBasedPage - 1, pageSize, sorting)); + + break; + + case FILTER_USER: + + long userId = options.getOption(USER_ID.ext) + .map(Long::valueOf) + .orElseThrow(() -> new IllegalArgumentException("Missing parameter " + USER_ID.ext)); + + filterContent = String.format("<@%d>", userId); + trackPage = trackRepo.findAllByUploaderIdIs(userId, + PageRequest.of(humanBasedPage - 1, pageSize, sorting)); + + break; + + default: + throw new IllegalArgumentException("Unknown filter " + filter); + + } + + } + + public @NotNull EmbedBuilder buildEmbed() { + + trackPageCheck(); + + String filterOut = (filterContent != null) ? "\n\n| _**Filter:** " + filterContent + "_" : ""; + + return new EmbedBuilder() + .setTitle("TrackIndex") + .setFooter("Page " + (trackPage.getNumber() + 1) + "/" + trackPage.getTotalPages()) + .setColor(embedColor) + .setDescription(trackPage.getContent() + .stream() + .map(track -> "**#" + track.getId() + "** *" + track.getName() + "*") + .collect(Collectors.joining("\n")) + .concat(filterOut)); + + } + + @SneakyThrows + public Button[] buildButtons() { + + trackPageCheck(); + + int zeroBasedPage = trackPage.getNumber(); + boolean enablePageChange = trackPage.getTotalPages() != 1; + + // button + options.setPage(trackPage.hasPrevious() ? zeroBasedPage : trackPage.getTotalPages()); + Button prevButton = Button.secondary(enablePageChange ? options.json() : "{\"_\":0}", Emojis.PREVIOUS) + .withDisabled(!enablePageChange); + + // button + options.setPage(!trackPage.isLast() ? zeroBasedPage + 2 : 0); + Button nextButton = Button.secondary(enablePageChange ? options.json() : "{\"_\":1}", Emojis.NEXT) + .withDisabled(!enablePageChange); + + // button + options.setPage(zeroBasedPage + 1); + Button reloadButton = Button.secondary(options.json(), Emojis.RELOAD); + + CommandOptions closeCommand = new CommandOptions("close"); + Button closeButton = Button.secondary(closeCommand.json(), Emojis.CLOSE); + + return new Button[] { prevButton, nextButton, reloadButton, closeButton }; + + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/utilities/TrackPlayer.java b/src/main/java/ovh/excale/vgreeter/track/TrackPlayer.java similarity index 88% rename from src/main/java/ovh/excale/vgreeter/utilities/TrackPlayer.java rename to src/main/java/ovh/excale/vgreeter/track/TrackPlayer.java index 03ed9e3..ae04a98 100644 --- a/src/main/java/ovh/excale/vgreeter/utilities/TrackPlayer.java +++ b/src/main/java/ovh/excale/vgreeter/track/TrackPlayer.java @@ -1,11 +1,10 @@ -package ovh.excale.vgreeter.utilities; +package ovh.excale.vgreeter.track; +import lombok.extern.log4j.Log4j2; import net.dv8tion.jda.api.audio.AudioSendHandler; import org.gagravarr.ogg.OggPacket; import org.gagravarr.ogg.OggPacketReader; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import ovh.excale.vgreeter.models.TrackModel; import java.io.IOException; @@ -16,10 +15,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +@Log4j2 public class TrackPlayer implements AudioSendHandler { - private final static Logger logger = LoggerFactory.getLogger(TrackPlayer.class); - private final TrackModel track; private final Iterator packetIterator; private Runnable trackEndAction; @@ -37,7 +35,7 @@ public TrackPlayer(TrackModel track) { while((packet = packetReader.getNextPacket()) != null) packetList.add(packet); } catch(IOException e) { - logger.error(e.getMessage(), e); + log.error(e.getMessage(), e); } } diff --git a/src/main/java/ovh/excale/vgreeter/utilities/ArgumentsParser.java b/src/main/java/ovh/excale/vgreeter/utilities/ArgumentsParser.java new file mode 100644 index 0000000..9eba0d6 --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/utilities/ArgumentsParser.java @@ -0,0 +1,95 @@ +package ovh.excale.vgreeter.utilities; + +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; + +// TODO: USER/MEMBER PARSE +public class ArgumentsParser { + + private final String rawArguments; + private final String[] splitArguments; + + public ArgumentsParser(String rawArguments) { + this.rawArguments = rawArguments; + splitArguments = rawArguments + .replaceAll(" {2,}", " ") + .trim() + .split(" "); + } + + public ArgumentsParser(String rawArguments, String splitRegex) { + this.rawArguments = rawArguments; + splitArguments = rawArguments.split(splitRegex); + } + + public String getRawArguments() { + return rawArguments; + } + + public String[] getSplitArguments() { + return splitArguments; + } + + public Optional getArgumentCast(int index, Function parser) { + + Optional opt = Optional.empty(); + if(index < splitArguments.length) + try { + + opt = Optional.of(parser.apply(splitArguments[index])); + + } catch(Exception ignored) { + } + + return opt; + } + + public Optional getArgumentString(int index) { + return getArgumentCast(index, String::toString); + } + + public @NotNull String getArgumentString(int index, @NotNull String defValue) { + return getArgumentString(index).orElse(defValue); + } + + public Optional getArgumentInteger(int index) { + return getArgumentCast(index, Integer::parseInt); + } + + public int getArgumentInteger(int index, int defValue) { + return getArgumentInteger(index).orElse(defValue); + } + + public Optional getArgumentLong(int index) { + return getArgumentCast(index, Long::parseLong); + } + + public long getArgumentLong(int index, long defValue) { + return getArgumentLong(index).orElse(defValue); + } + + public Optional getArgumentBoolean(int index) { + return getArgumentCast(index, Boolean::parseBoolean); + } + + public boolean getArgumentBoolean(int index, boolean defValue) { + return getArgumentBoolean(index).orElse(defValue); + } + + public Optional getArgumentText(int index) { + + Optional opt = Optional.empty(); + if(index < splitArguments.length) + opt = Optional.of(String.join(" ", Arrays.copyOfRange(splitArguments, index, splitArguments.length))); + + return opt; + } + + public @NotNull String getArgumentText(int index, @NotNull String defValue) { + return getArgumentText(index).orElse(defValue); + } + +} diff --git a/src/main/java/ovh/excale/vgreeter/utilities/Emojis.java b/src/main/java/ovh/excale/vgreeter/utilities/Emojis.java new file mode 100644 index 0000000..39348cd --- /dev/null +++ b/src/main/java/ovh/excale/vgreeter/utilities/Emojis.java @@ -0,0 +1,17 @@ +package ovh.excale.vgreeter.utilities; + +import lombok.NoArgsConstructor; +import net.dv8tion.jda.api.entities.emoji.Emoji; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class Emojis { + + public static final Emoji PREVIOUS = Emoji.fromUnicode("◀"); + + public static final Emoji NEXT = Emoji.fromUnicode("▶"); + + public static final Emoji RELOAD = Emoji.fromUnicode("🔄"); + + public static final Emoji CLOSE = Emoji.fromUnicode("❌"); + +} diff --git a/src/main/resources-filtered/application.properties b/src/main/resources-filtered/application.properties index 158743f..4224120 100644 --- a/src/main/resources-filtered/application.properties +++ b/src/main/resources-filtered/application.properties @@ -6,7 +6,6 @@ spring.datasource.username: ${env.DATASOURCE_USERNAME} spring.datasource.password: ${env.DATASOURCE_PASSWORD} spring.datasource.hikari.keepalive-time: 30000 spring.datasource.driver-class-name: org.postgresql.Driver -spring.jpa.database-platform: org.hibernate.dialect.PostgreSQL10Dialect # Custom properties application.name: @project.name@