From 16149a7e37f11d5b25ca78d5acc174027088f09d Mon Sep 17 00:00:00 2001 From: Sky Date: Sun, 27 Oct 2024 13:30:59 +0100 Subject: [PATCH] :sparkles: Added reply section --- .../disky/api/skript/AsyncEffectSection.java | 63 +++++++++ .../api/skript/BetterExpressionEntryData.java | 103 ++++++++++++++ .../itsthesky/disky/core/SkriptUtils.java | 7 +- .../components/core/ComponentRow.java | 22 +++ .../disky/elements/effects/ReplyWith.java | 133 ++++++++++++++---- 5 files changed, 297 insertions(+), 31 deletions(-) create mode 100644 src/main/java/info/itsthesky/disky/api/skript/AsyncEffectSection.java create mode 100644 src/main/java/info/itsthesky/disky/api/skript/BetterExpressionEntryData.java diff --git a/src/main/java/info/itsthesky/disky/api/skript/AsyncEffectSection.java b/src/main/java/info/itsthesky/disky/api/skript/AsyncEffectSection.java new file mode 100644 index 00000000..ec296e1a --- /dev/null +++ b/src/main/java/info/itsthesky/disky/api/skript/AsyncEffectSection.java @@ -0,0 +1,63 @@ +package info.itsthesky.disky.api.skript; + +import ch.njol.skript.Skript; +import ch.njol.skript.effects.Delay; +import ch.njol.skript.lang.EffectSection; +import ch.njol.skript.lang.Trigger; +import ch.njol.skript.lang.TriggerItem; +import ch.njol.skript.timings.SkriptTimings; +import ch.njol.skript.variables.Variables; +import org.bukkit.Bukkit; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Represents an effect section that runs asynchronously. + * Made by Sky, inspired by Skript's {@link ch.njol.skript.util.AsyncEffect} & {@link ch.njol.skript.lang.EffectSection} + */ +public abstract class AsyncEffectSection extends EffectSection { + + @Override + @Nullable + protected TriggerItem walk(@NotNull Event e) { + debug(e, true); + + Delay.addDelayedEvent(e); // Mark this event as delayed + Object localVars = Variables.removeLocals(e); // Back up local variables + + if (!Skript.getInstance().isEnabled()) // See https://github.com/SkriptLang/Skript/issues/3702 + return null; + + Bukkit.getScheduler().runTaskAsynchronously(Skript.getInstance(), () -> { + // Re-set local variables + if (localVars != null) + Variables.setLocalVariables(e, localVars); + + execute(e); // Execute this effect + + if (getNext() != null) { + Bukkit.getScheduler().runTask(Skript.getInstance(), () -> { // Walk to next item synchronously + Object timing = null; + if (SkriptTimings.enabled()) { // getTrigger call is not free, do it only if we must + Trigger trigger = getTrigger(); + if (trigger != null) { + timing = SkriptTimings.start(trigger.getDebugLabel()); + } + } + + TriggerItem.walk(getNext(), e); + + Variables.removeLocals(e); // Clean up local vars, we may be exiting now + + SkriptTimings.stop(timing); // Stop timing if it was even started + }); + } else { + Variables.removeLocals(e); + } + }); + return null; + } + + protected abstract void execute(Event e); +} diff --git a/src/main/java/info/itsthesky/disky/api/skript/BetterExpressionEntryData.java b/src/main/java/info/itsthesky/disky/api/skript/BetterExpressionEntryData.java new file mode 100644 index 00000000..530d9d23 --- /dev/null +++ b/src/main/java/info/itsthesky/disky/api/skript/BetterExpressionEntryData.java @@ -0,0 +1,103 @@ +package info.itsthesky.disky.api.skript; + +import ch.njol.skript.ScriptLoader; +import ch.njol.skript.config.Node; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.config.SimpleNode; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.localization.Message; +import ch.njol.skript.log.ErrorQuality; +import ch.njol.skript.log.ParseLogHandler; +import info.itsthesky.disky.DiSky; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.entry.EntryData; +import org.skriptlang.skript.lang.entry.EntryValidator; + +import java.util.ArrayList; +import java.util.List; + +/** + * An {@link org.skriptlang.skript.lang.entry.util.ExpressionEntryData} that allows multiple expressions to be defined in a single entry. + * It also let the developer write the different expressions in a section. + * @param the return type of the expressions + * @author Sky + */ +public class BetterExpressionEntryData extends EntryData>> { + + private static final Message M_IS = new Message("is"); + private final Class returnType; + private final int flags; + + public BetterExpressionEntryData(String key, @Nullable List> defaultValue, boolean optional, + Class returnType, int flags) { + super(key, defaultValue, optional); + + this.returnType = returnType; + this.flags = flags; + } + + public BetterExpressionEntryData(String key, @Nullable List> defaultValue, + boolean optional, Class returnType) { + this(key, defaultValue, optional, returnType, SkriptParser.ALL_FLAGS); + } + + @Override + @Nullable + public List> getValue(@NotNull Node node) { + if (node instanceof final SectionNode sectionNode) { + final List> expressions = new ArrayList<>(); + for (Node subNode : sectionNode) { + final String value = subNode.getKey(); + final Expression expression = parseExpression(value); + if (expression != null) + expressions.add(expression); + } + return expressions; + } else if (node instanceof final SimpleNode simpleNode) { + final String key = simpleNode.getKey(); + if (key == null) + return null; + + final String value = ScriptLoader.replaceOptions(key).substring(getKey().length() + EntryValidator.EntryValidatorBuilder.DEFAULT_ENTRY_SEPARATOR.length()); + final Expression expression = parseExpression(value); + if (expression != null) + return List.of(expression); + + return null; + } else { + return null; + } + } + + private Expression parseExpression(String value) { + Expression expression; + try (ParseLogHandler log = new ParseLogHandler().start()) { + expression = new SkriptParser(value, flags, ParseContext.DEFAULT) + .parseExpression(returnType); + if (expression == null) // print an error if it couldn't parse + log.printError( + "'" + value + "' " + M_IS + " " + SkriptParser.notOfType(returnType), + ErrorQuality.NOT_AN_EXPRESSION + ); + } + return expression; + } + + @Override + public boolean canCreateWith(@NotNull Node node) { + if (node instanceof SectionNode) { + return true; + } else if (node instanceof SimpleNode) { + String key = node.getKey(); + if (key == null) + return false; + key = ScriptLoader.replaceOptions(key); + return key.startsWith(getKey() + EntryValidator.EntryValidatorBuilder.DEFAULT_ENTRY_SEPARATOR); + } + + return false; + } +} diff --git a/src/main/java/info/itsthesky/disky/core/SkriptUtils.java b/src/main/java/info/itsthesky/disky/core/SkriptUtils.java index e4a1f790..b9cf223f 100644 --- a/src/main/java/info/itsthesky/disky/core/SkriptUtils.java +++ b/src/main/java/info/itsthesky/disky/core/SkriptUtils.java @@ -7,14 +7,13 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.TriggerItem; +import ch.njol.skript.lang.VariableString; import ch.njol.skript.lang.parser.ParserInstance; import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.lang.util.SimpleLiteral; import ch.njol.skript.log.*; import ch.njol.skript.registrations.EventValues; -import ch.njol.skript.util.Color; -import ch.njol.skript.util.ColorRGB; -import ch.njol.skript.util.Date; -import ch.njol.skript.util.Getter; +import ch.njol.skript.util.*; import ch.njol.util.Kleenean; import info.itsthesky.disky.DiSky; import info.itsthesky.disky.api.ReflectionUtils; diff --git a/src/main/java/info/itsthesky/disky/elements/components/core/ComponentRow.java b/src/main/java/info/itsthesky/disky/elements/components/core/ComponentRow.java index 89df398f..ecc5939f 100644 --- a/src/main/java/info/itsthesky/disky/elements/components/core/ComponentRow.java +++ b/src/main/java/info/itsthesky/disky/elements/components/core/ComponentRow.java @@ -29,6 +29,24 @@ public ComponentRow() { buttons = new ArrayList<>(); } + public ComponentRow(List components) { + this(); + + for (Object component : components) { + if (component instanceof final SelectMenu menu) + setMenu(menu); + else if (component instanceof final Button button) + add(button); + else if (component instanceof final TextInput input) + setInput(input); + + else if (component instanceof final SelectMenu.Builder menuBuilder) + setMenu(menuBuilder.build()); + else if (component instanceof final TextInput.Builder inputBuilder) + setInput(inputBuilder.build()); + } + } + public TextInput getInput() { return input; } @@ -75,4 +93,8 @@ else if (component instanceof TextInput) setInput((TextInput) component); } } + + public boolean isEmpty() { + return menu == null && input == null && buttons.isEmpty(); + } } diff --git a/src/main/java/info/itsthesky/disky/elements/effects/ReplyWith.java b/src/main/java/info/itsthesky/disky/elements/effects/ReplyWith.java index 8a5850af..5c9e8f2f 100644 --- a/src/main/java/info/itsthesky/disky/elements/effects/ReplyWith.java +++ b/src/main/java/info/itsthesky/disky/elements/effects/ReplyWith.java @@ -1,23 +1,26 @@ package info.itsthesky.disky.elements.effects; +import ch.njol.skript.ScriptLoader; import ch.njol.skript.Skript; import ch.njol.skript.classes.Changer; import ch.njol.skript.config.Node; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.config.validate.SectionValidator; import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Examples; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.lang.Variable; -import ch.njol.skript.util.AsyncEffect; +import ch.njol.skript.lang.TriggerItem; import ch.njol.util.Kleenean; import info.itsthesky.disky.DiSky; import info.itsthesky.disky.api.events.specific.InteractionEvent; import info.itsthesky.disky.api.events.specific.MessageEvent; -import info.itsthesky.disky.api.skript.SpecificBotEffect; -import info.itsthesky.disky.core.Bot; +import info.itsthesky.disky.api.skript.AsyncEffectSection; +import info.itsthesky.disky.api.skript.BetterExpressionEntryData; import info.itsthesky.disky.core.SkriptUtils; +import info.itsthesky.disky.elements.components.core.ComponentRow; import net.dv8tion.jda.api.EmbedBuilder; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; @@ -26,19 +29,25 @@ import net.dv8tion.jda.api.interactions.InteractionHook; import net.dv8tion.jda.api.interactions.callbacks.IPremiumRequiredReplyCallback; import net.dv8tion.jda.api.interactions.callbacks.IReplyCallback; +import net.dv8tion.jda.api.interactions.components.ActionRow; +import net.dv8tion.jda.api.interactions.components.buttons.Button; +import net.dv8tion.jda.api.interactions.components.selections.SelectMenu; +import net.dv8tion.jda.api.interactions.components.text.TextInput; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; -import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder; -import net.dv8tion.jda.api.utils.messages.MessageEditData; -import net.dv8tion.jda.api.utils.messages.MessagePollBuilder; -import net.dv8tion.jda.api.utils.messages.MessagePollData; -import net.dv8tion.jda.internal.requests.RestActionImpl; +import net.dv8tion.jda.api.utils.messages.*; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.entry.EntryContainer; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.lang.entry.util.ExpressionEntryData; -import static info.itsthesky.disky.api.skript.EasyElement.containsInterfaces; -import static info.itsthesky.disky.api.skript.EasyElement.parseSingle; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static info.itsthesky.disky.api.skript.EasyElement.*; @Name("Reply With") @Description({"Reply with a specific message to the channel where a message-event was triggered.", @@ -52,43 +61,116 @@ "wait a second", "edit {_msg} to show \"... world!\""}) @Since("4.4.0") -public class ReplyWith extends AsyncEffect { +public class ReplyWith extends AsyncEffectSection { static { - Skript.registerEffect( + Skript.registerSection( ReplyWith.class, "reply with [hidden] %string/messagecreatebuilder/sticker/embedbuilder/messagepollbuilder% [with [the] reference[d] [message] %-message%] [and store (it|the message) in %-objects%]", "reply with premium [required] message" ); } + private static final EntryValidator ReplySectionValidator = EntryValidator.builder() + .addEntryData(new ExpressionEntryData<>("content", null, true, Object.class)) + .addEntryData(new ExpressionEntryData<>("embed", null, true, Object.class)) + .addEntryData(new ExpressionEntryData<>("poll", null, true, Object.class)) + .addEntryData(new BetterExpressionEntryData<>("components", null, true, Object.class)) + .build(); + private Node node; private Expression exprMessage; private Expression exprReference; private Expression exprResult; private boolean hidden; private boolean premium; + private @Nullable SectionNode sectionNode; + + //region Section Values + private @Nullable Expression secExprContent; + private @Nullable Expression secExprEmbed; + private @Nullable Expression secExprPoll; + private @Nullable List> secExprComponents; + //endregion @Override - public boolean init(Expression[] expressions, int i, Kleenean kleenean, SkriptParser.ParseResult parseResult) { + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult, SectionNode sectionNode, List triggerItems) { if (!containsInterfaces(MessageEvent.class)) { Skript.error("The effect reply effect can only be used in a message event."); return false; } - node = getParser().getNode(); - premium = i == 1; + this.node = getParser().getNode(); + this.premium = matchedPattern == 1; if (!premium) { - hidden = parseResult.expr.startsWith("reply with hidden"); - exprMessage = (Expression) expressions[0]; - exprReference = (Expression) expressions[1]; - exprResult = (Expression) expressions[2]; + this.hidden = parseResult.expr.startsWith("reply with hidden"); + this.exprMessage = (Expression) expressions[0]; + this.exprReference = (Expression) expressions[1]; + this.exprResult = (Expression) expressions[2]; + } + + if (sectionNode != null) { + this.sectionNode = sectionNode; + + EntryContainer validatedEntries = ReplySectionValidator.validate(sectionNode); + if (validatedEntries == null) + return false; + + secExprContent = validatedEntries.getOptional("content", Expression.class, true); + secExprEmbed = validatedEntries.getOptional("embed", Expression.class, true); + secExprPoll = validatedEntries.getOptional("poll", Expression.class, true); + secExprComponents = validatedEntries.getOptional("components", List.class, true); + } return exprResult == null || Changer.ChangerUtils.acceptsChange(exprResult, Changer.ChangeMode.SET, Message.class); } + private MessageCreateBuilder parseAdditionalData(Event event) { + if (sectionNode == null) + return new MessageCreateBuilder(); + + final MessageCreateBuilder builder = new MessageCreateBuilder(); + + if (secExprContent != null) { + final Object content = parseSingle(secExprContent, event); + if (content instanceof final String string) + builder.setContent(string); + } + + if (secExprEmbed != null) { + final Object[] embeds = parseList((Expression) secExprEmbed, event, new Object[0]); + for (Object embed : embeds) { + if (embed instanceof final EmbedBuilder embedBuilder) + builder.addEmbeds(embedBuilder.build()); + } + } + + if (secExprPoll != null) { + final Object poll = parseSingle(secExprPoll, event); + if (poll instanceof final MessagePollBuilder pollBuilder) + builder.setPoll(pollBuilder.build()); + } + + if (secExprComponents != null) { + final List rows = new ArrayList<>(); + + for (Expression secExprComponent : secExprComponents) { + final Object[] rawComponents = parseList((Expression) secExprComponent, event, new Object[0]); + final ComponentRow row = new ComponentRow(Arrays.asList(rawComponents)); + if (row.isEmpty()) + continue; + + rows.add(row); + } + + builder.setComponents(rows.stream().map(ComponentRow::asActionRow).toList()); + } + + return builder; + } + @Override public void execute(@NotNull Event e) { if (premium) { @@ -134,12 +216,11 @@ public void execute(@NotNull Event e) { if (message instanceof MessageCreateBuilder) builder = (MessageCreateBuilder) message; else if (message instanceof EmbedBuilder) - builder = new MessageCreateBuilder().addEmbeds(((EmbedBuilder) message).build()); + builder = parseAdditionalData(e).addEmbeds(((EmbedBuilder) message).build()); else if (message instanceof MessagePollBuilder) - builder = new MessageCreateBuilder().setPoll(((MessagePollBuilder) message).build()); + builder = parseAdditionalData(e).setPoll(((MessagePollBuilder) message).build()); else - builder = new MessageCreateBuilder().setContent((String) message); - final @Nullable MessagePollData poll = builder.getPoll(); + builder = parseAdditionalData(e).setContent((String) message); if (!builder.isValid()) { SkriptUtils.error(node, "The provided message is not valid!"); @@ -165,13 +246,11 @@ else if (message instanceof MessagePollBuilder) } otherRestAction = callback.reply(builder.build()) - .setPoll(poll) .setEphemeral(hidden); } } else { final MessageEvent event = (MessageEvent) e; - messageRestAction = event.getMessageChannel().sendMessage(builder.build()) - .setPoll(poll); + messageRestAction = event.getMessageChannel().sendMessage(builder.build()); if (reference != null) ((MessageCreateAction) messageRestAction).setMessageReference(reference); }