From db63529aa1191fa107b1c98e2e62d6f3684a9f3e Mon Sep 17 00:00:00 2001 From: Minecraftschurli Date: Fri, 16 Oct 2020 14:56:36 +0200 Subject: [PATCH 1/4] remove block call --- .../k9/commands/api/CommandRegistrar.java | 159 +++++++++--------- 1 file changed, 79 insertions(+), 80 deletions(-) diff --git a/src/main/java/com/tterrag/k9/commands/api/CommandRegistrar.java b/src/main/java/com/tterrag/k9/commands/api/CommandRegistrar.java index abda5e11..058a335f 100644 --- a/src/main/java/com/tterrag/k9/commands/api/CommandRegistrar.java +++ b/src/main/java/com/tterrag/k9/commands/api/CommandRegistrar.java @@ -64,7 +64,7 @@ public CommandRegistrar(K9 k9) { this.k9 = k9; } - public Mono invokeCommand(MessageCreateEvent evt, String name, String argstr) { + public Mono invokeCommand(MessageCreateEvent evt, String name, String argstrIn) { Optional commandReq = findCommand(evt.getGuildId().orElse(null), name); ICommand command = commandReq.filter(c -> !c.admin() || evt.getMessage().getAuthor().map(this::isAdmin).orElse(false)).orElse(null); @@ -73,96 +73,95 @@ public Mono invokeCommand(MessageCreateEvent evt, String name, String } CommandContext ctx = new CommandContext(k9, evt); - - if (!command.requirements().matches(ctx).block()) { - return evt.getMessage().getChannel() - .flatMap(c -> c.createMessage("You do not have permission to use this command!")) - .delayElement(Duration.ofSeconds(5)) - .flatMap(m -> m.delete()) - .thenReturn(command); - } - -// evt.getMessage().getChannel().flatMap(c -> c.type()).subscribe(); - - argstr = Strings.nullToEmpty(argstr); - - Map flags = new HashMap<>(); - Map, String> args = new HashMap<>(); - - Map keyToFlag = command.getFlags().stream().collect(Collectors.toMap(Flag::name, f -> f)); - Map longKeyToFlag = command.getFlags().stream().collect(Collectors.toMap(Flag::longFormName, f -> f)); - Matcher matcher = Patterns.FLAGS.matcher(argstr); - while (matcher.find()) { - String flagname = matcher.group(2); - List foundFlags; - if (matcher.group().startsWith("--")) { - foundFlags = Collections.singletonList(longKeyToFlag.get(flagname)); - } else if (matcher.group().startsWith("-")) { - foundFlags = Lists.newArrayList(flagname.chars().mapToObj(i -> keyToFlag.get((char) i)).toArray(Flag[]::new)); - } else { - continue; + return command.requirements().matches(ctx).flatMap(bool -> { + if (!bool) { + return evt.getMessage().getChannel() + .flatMap(c -> c.createMessage("You do not have permission to use this command!")) + .delayElement(Duration.ofSeconds(5)) + .flatMap(m -> m.delete()) + .thenReturn(command); } - if (foundFlags.contains(null)) { - return ctx.reply("Unknown flag(s) \"" + flagname + "\".").thenReturn(command); - } - - String toreplace = matcher.group(1) + matcher.group(2); + String argstr = Strings.nullToEmpty(argstrIn); + + Map flags = new HashMap<>(); + Map, String> args = new HashMap<>(); + + Map keyToFlag = command.getFlags().stream().collect(Collectors.toMap(Flag::name, f -> f)); + Map longKeyToFlag = command.getFlags().stream().collect(Collectors.toMap(Flag::longFormName, f -> f)); + + Matcher matcher = Patterns.FLAGS.matcher(argstr); + while (matcher.find()) { + String flagname = matcher.group(2); + List foundFlags; + if (matcher.group().startsWith("--")) { + foundFlags = Collections.singletonList(longKeyToFlag.get(flagname)); + } else if (matcher.group().startsWith("-")) { + foundFlags = Lists.newArrayList(flagname.chars().mapToObj(i -> keyToFlag.get((char) i)).toArray(Flag[]::new)); + } else { + continue; + } + if (foundFlags.contains(null)) { + return ctx.reply("Unknown flag(s) \"" + flagname + "\".").thenReturn(command); + } - for (int i = 0; i < foundFlags.size(); i++) { - Flag flag = foundFlags.get(i); - String value = null; - if (i == foundFlags.size() - 1) { - if (flag.canHaveValue()) { - value = matcher.group(3); - if (value == null) { - value = matcher.group(4); + String toreplace = matcher.group(1) + matcher.group(2); + + for (int i = 0; i < foundFlags.size(); i++) { + Flag flag = foundFlags.get(i); + String value = null; + if (i == foundFlags.size() - 1) { + if (flag.canHaveValue()) { + value = matcher.group(3); + if (value == null) { + value = matcher.group(4); + } + toreplace = matcher.group(); } - toreplace = matcher.group(); } + if (value == null && flag.needsValue()) { + return ctx.reply("Flag \"" + flag.longFormName() + "\" requires a value.").thenReturn(command); + } + + flags.put(flag, value == null ? flag.getDefaultValue() : value); } - if (value == null && flag.needsValue()) { - return ctx.reply("Flag \"" + flag.longFormName() + "\" requires a value.").thenReturn(command); + toreplace = Pattern.quote(toreplace) + "\\s*"; + argstr = argstr.replaceFirst(toreplace, "").trim(); + matcher.reset(argstr); + } + + for (Argument arg : command.getArguments()) { + boolean required = arg.required(flags.keySet()); + if (required && argstr.isEmpty()) { + long count = command.getArguments().stream().filter(a -> a.required(flags.keySet())).count(); + return ctx.reply("This command requires at least " + count + " argument" + (count > 1 ? "s" : "") + ".").thenReturn(command); } - flags.put(flag, value == null ? flag.getDefaultValue() : value); - } - toreplace = Pattern.quote(toreplace) + "\\s*"; - argstr = argstr.replaceFirst(toreplace, "").trim(); - matcher.reset(argstr); - } + matcher = arg.pattern().matcher(argstr); - for (Argument arg : command.getArguments()) { - boolean required = arg.required(flags.keySet()); - if (required && argstr.isEmpty()) { - long count = command.getArguments().stream().filter(a -> a.required(flags.keySet())).count(); - return ctx.reply("This command requires at least " + count + " argument" + (count > 1 ? "s" : "") + ".").thenReturn(command); - } - - matcher = arg.pattern().matcher(argstr); - - if (matcher.find()) { - String match = matcher.group(); - argstr = argstr.replaceFirst(Pattern.quote(match) + "\\s*", "").trim(); - args.put(arg, match); - } else if (required) { - return ctx.reply("Argument " + arg.name() + " does not accept input: " + argstr + " (does not match `" + arg.pattern().pattern() + "`)").thenReturn(command); + if (matcher.find()) { + String match = matcher.group(); + argstr = argstr.replaceFirst(Pattern.quote(match) + "\\s*", "").trim(); + args.put(arg, match); + } else if (required) { + return ctx.reply("Argument " + arg.name() + " does not accept input: " + argstr + " (does not match `" + arg.pattern().pattern() + "`)").thenReturn(command); + } } - } - try { - final Mono commandResult = command.process(ctx.withFlags(flags).withArgs(args)) - .doOnError(t -> log.error("Exception invoking command: ", t)) - .onErrorResume(CommandException.class, t -> ctx.reply("Could not process command: " + t).then(Mono.empty())) - .onErrorResume(ClientException.class, t -> ctx.reply("Discord error processing command: " + t.getStatus() + " - " + t.getErrorResponse().map(e -> e.getFields().toString()).orElse("{}")).then(Mono.empty())) - .onErrorResume(t -> ctx.reply("Unexpected error processing command: " + t).then(Mono.empty())); - return evt.getMessage().getChannel() // Automatic typing indicator - .flatMap(c -> c.typeUntil(commandResult).then()) - .thenReturn(command); - } catch (RuntimeException e) { - log.error("Exception invoking command: ", e); - return ctx.reply("Unexpected error processing command: " + e).thenReturn(command); // TODO should this be different? - } + try { + final Mono commandResult = command.process(ctx.withFlags(flags).withArgs(args)) + .doOnError(t -> log.error("Exception invoking command: ", t)) + .onErrorResume(CommandException.class, t -> ctx.reply("Could not process command: " + t).then(Mono.empty())) + .onErrorResume(ClientException.class, t -> ctx.reply("Discord error processing command: " + t.getStatus() + " - " + t.getErrorResponse().map(e -> e.getFields().toString()).orElse("{}")).then(Mono.empty())) + .onErrorResume(t -> ctx.reply("Unexpected error processing command: " + t).then(Mono.empty())); + return evt.getMessage().getChannel() // Automatic typing indicator + .flatMap(c -> c.typeUntil(commandResult).then()) + .thenReturn(command); + } catch (RuntimeException e) { + log.error("Exception invoking command: ", e); + return ctx.reply("Unexpected error processing command: " + e).thenReturn(command); // TODO should this be different? + } + }); } public boolean isAdmin(User user) { From fec2c535857b920be1a134dc1e595c180e8bf250 Mon Sep 17 00:00:00 2001 From: Minecraftschurli Date: Fri, 16 Oct 2020 15:17:32 +0200 Subject: [PATCH 2/4] add possibility to store per channel data in GuildStorage --- .../com/tterrag/k9/util/GuildStorage.java | 75 +++++++++++++++---- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/tterrag/k9/util/GuildStorage.java b/src/main/java/com/tterrag/k9/util/GuildStorage.java index 01ab1f1d..a1aadc27 100644 --- a/src/main/java/com/tterrag/k9/util/GuildStorage.java +++ b/src/main/java/com/tterrag/k9/util/GuildStorage.java @@ -11,24 +11,32 @@ import discord4j.core.object.entity.Guild; import discord4j.core.object.entity.Message; import discord4j.common.util.Snowflake; +import discord4j.core.object.entity.channel.Channel; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; import reactor.core.publisher.Mono; @RequiredArgsConstructor public class GuildStorage { - private final Map data = new HashMap<>(); + private final Map, T> data = new HashMap<>(); - private final Function dataCreator; + private final Function, T> dataCreator; - public T get(Snowflake snowflake) { - long guild = snowflake.asLong(); + public T get(Snowflake guildId) { + return get(guildId, null); + } + + public T get(Snowflake guildId, Snowflake channelId) { + long guild = guildId.asLong(); + long channel = channelId != null ? channelId.asLong() : -1l; + Pair key = Pair.of(guild, channel); synchronized (data) { - if (data.containsKey(guild)) { - return data.get(guild); + if (data.containsKey(key)) { + return data.get(key); } else { - T val = dataCreator.apply(guild); - data.put(guild, val); + T val = dataCreator.apply(key); + data.put(key, val); return val; } } @@ -37,34 +45,75 @@ public T get(Snowflake snowflake) { public T get(Guild guild) { return get(guild.getId()); } + + public T get(Guild guild, Channel channel) { + return get(guild.getId(), channel.getId()); + } public Mono get(Message message) { - return message.getGuild().map(this::get); + return get(message, false); + } + + public Mono get(Message message, boolean useChannel) { + if (useChannel) { + return message.getGuild().flatMap(guild -> message.getChannel().map(channel -> get(guild, channel))); + } else { + return message.getGuild().map(this::get); + } } public Optional get(CommandContext ctx) { - return ctx.getGuildId().map(this::get); + return get(ctx, false); + } + + public Optional get(CommandContext ctx, boolean useChannel) { + if (useChannel) { + return ctx.getGuildId().map(guild -> get(guild, ctx.getChannelId())); + } else { + return ctx.getGuildId().map(this::get); + } } public Optional put(Snowflake guild, T val) { + return put(guild, null, val); + } + + public Optional put(Snowflake guild, Snowflake channel, T val) { + Pair key = Pair.of(guild.asLong(), channel!=null?channel.asLong():-1l); synchronized (data) { - return Optional.ofNullable(data.put(guild.asLong(), val)); + return Optional.ofNullable(data.put(key, val)); } } public Optional put(Guild guild, T val) { return put(guild.getId(), val); } + + public Optional put(Guild guild, Channel channel, T val) { + return put(guild.getId(), channel.getId(), val); + } public Mono put(Message message, T val) { - return message.getGuild().transform(Monos.mapOptional(g -> put(g, val))); + return put(message, val, false); + } + + public Mono put(Message message, T val, boolean useChannel) { + if (useChannel) { + return message.getGuild().flatMap(guild -> message.getChannel().transform(Monos.mapOptional(channel -> put(guild, channel, val)))); + } else { + return message.getGuild().transform(Monos.mapOptional(g -> put(g, val))); + } } public Mono put(CommandContext ctx, T val) { return put(ctx.getMessage(), val); } + + public Mono put(CommandContext ctx, T val, boolean useChannel) { + return put(ctx.getMessage(), val, useChannel); + } - public Map snapshot() { + public Map, T> snapshot() { synchronized (data) { return ImmutableMap.copyOf(data); } From 001900717445b816ebf0545ae9e0133407bc2fce Mon Sep 17 00:00:00 2001 From: Minecraftschurli Date: Fri, 16 Oct 2020 15:31:57 +0200 Subject: [PATCH 3/4] add possibility to use the new per channel data --- .../com/tterrag/k9/commands/CommandTrick.java | 6 ++--- .../k9/commands/api/CommandPersisted.java | 22 +++++++++++++++---- .../k9/commands/api/CommandRegistrar.java | 17 +++++++++++++- .../k9/listeners/IncrementListener.java | 2 +- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/tterrag/k9/commands/CommandTrick.java b/src/main/java/com/tterrag/k9/commands/CommandTrick.java index 0a19e252..129d18e8 100644 --- a/src/main/java/com/tterrag/k9/commands/CommandTrick.java +++ b/src/main/java/com/tterrag/k9/commands/CommandTrick.java @@ -1,6 +1,5 @@ package com.tterrag.k9.commands; -import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -17,13 +16,11 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.TypeAdapter; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; -import com.tterrag.k9.K9; import com.tterrag.k9.commands.CommandTrick.TrickData; import com.tterrag.k9.commands.api.Argument; import com.tterrag.k9.commands.api.Command; @@ -53,6 +50,7 @@ import lombok.RequiredArgsConstructor; import lombok.Value; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.tuple.Pair; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; @@ -169,7 +167,7 @@ protected TrickData handleDelegate(TrickData delegate) { } @Override - protected void onLoad(long guild, Map data) { + protected void onLoad(Pair dataKey, Map data) { for (String key : new HashSet<>(data.keySet())) { if (!Patterns.VALID_TRICK_NAME.matcher(key).matches()) { TrickData removed = data.remove(key); diff --git a/src/main/java/com/tterrag/k9/commands/api/CommandPersisted.java b/src/main/java/com/tterrag/k9/commands/api/CommandPersisted.java index 488c57de..2f55499c 100644 --- a/src/main/java/com/tterrag/k9/commands/api/CommandPersisted.java +++ b/src/main/java/com/tterrag/k9/commands/api/CommandPersisted.java @@ -12,6 +12,8 @@ import discord4j.common.util.Snowflake; import discord4j.core.object.entity.Guild; +import discord4j.core.object.entity.channel.Channel; +import org.apache.commons.lang3.tuple.Pair; import reactor.core.publisher.Mono; import reactor.util.annotation.NonNull; import reactor.util.annotation.Nullable; @@ -32,7 +34,7 @@ public Mono onReady(ReadyContext ctx) { return super.onReady(ctx) .then(Mono.fromRunnable(() -> storage = new GuildStorage<>(id -> { - T ret = newHelper(ctx.getDataFolder(), id, ctx.getGson()).fromJson(getFileName(), getDataType()); + T ret = newHelper(ctx.getDataFolder(), id.getKey(), ctx.getGson()).fromJson(getFileName(), getDataType()); onLoad(id, ret); return ret; }))); @@ -41,14 +43,14 @@ public Mono onReady(ReadyContext ctx) { @Override public void save(File dataFolder, Gson gson) { if (storage != null) { - for (Entry e : storage.snapshot().entrySet()) { - SaveHelper helper = newHelper(dataFolder, e.getKey(), gson); + for (Entry, T> e : storage.snapshot().entrySet()) { + SaveHelper helper = newHelper(dataFolder, e.getKey().getKey(), gson); helper.writeJson(getFileName(), e.getValue(), getDataType()); } } } - protected void onLoad(long guild, T data) { + protected void onLoad(Pair key, T data) { } private SaveHelper newHelper(File root, long guild, Gson gson) { @@ -68,12 +70,24 @@ private String getFileName() { public final Optional getData(CommandContext ctx) { return storage.get(ctx); } + + public final Optional getData(CommandContext ctx, boolean useChannel) { + return storage.get(ctx, useChannel); + } public final T getData(Guild guild) { return storage.get(guild); } + + public final T getData(Guild guild, Channel channel) { + return storage.get(guild, channel); + } public final T getData(Snowflake guild) { return storage.get(guild); } + + public final T getData(Snowflake guild, Snowflake channel) { + return storage.get(guild, channel); + } } diff --git a/src/main/java/com/tterrag/k9/commands/api/CommandRegistrar.java b/src/main/java/com/tterrag/k9/commands/api/CommandRegistrar.java index 058a335f..7a111f21 100644 --- a/src/main/java/com/tterrag/k9/commands/api/CommandRegistrar.java +++ b/src/main/java/com/tterrag/k9/commands/api/CommandRegistrar.java @@ -65,7 +65,7 @@ public CommandRegistrar(K9 k9) { } public Mono invokeCommand(MessageCreateEvent evt, String name, String argstrIn) { - Optional commandReq = findCommand(evt.getGuildId().orElse(null), name); + Optional commandReq = findCommand(evt.getGuildId().orElse(null), evt.getMessage().getChannelId(), name); ICommand command = commandReq.filter(c -> !c.admin() || evt.getMessage().getAuthor().map(this::isAdmin).orElse(false)).orElse(null); if (command == null) { @@ -172,6 +172,14 @@ public Optional findCommand(CommandContext ctx, String name) { return findCommand(ctx.getGuildId().orElse(null), name); } + public Optional findCommand(CommandContext ctx, String name, boolean useChannel) { + if (useChannel) { + return findCommand(ctx.getGuildId().orElse(null), ctx.getChannelId(), name); + } else { + return findCommand(ctx.getGuildId().orElse(null), name); + } + } + public Optional findCommand(@Nullable Snowflake guild, String name) { if (guild != null && ctrl.getData(guild).getCommandBlacklist().contains(name)) { return Optional.empty(); @@ -179,6 +187,13 @@ public Optional findCommand(@Nullable Snowflake guild, String name) { return Optional.ofNullable(commands.get(name)); } + public Optional findCommand(@Nullable Snowflake guild, @Nullable Snowflake channel, String name) { + if (channel != null && ctrl.getData(guild, channel).getCommandBlacklist().contains(name)) { + return Optional.empty(); + } + return findCommand(guild, name); + } + public void slurpCommands() { if (!finishedDefaultSlurp) { slurpCommands("com.tterrag.k9.commands"); diff --git a/src/main/java/com/tterrag/k9/listeners/IncrementListener.java b/src/main/java/com/tterrag/k9/listeners/IncrementListener.java index d1c4673b..62433364 100644 --- a/src/main/java/com/tterrag/k9/listeners/IncrementListener.java +++ b/src/main/java/com/tterrag/k9/listeners/IncrementListener.java @@ -23,7 +23,7 @@ public enum IncrementListener { private static final SaveHelper> saveHelper = new SaveHelper<>(new File("counts"), new Gson(), new HashMap<>()); private static final GuildStorage> counts = new GuildStorage<>( - id -> saveHelper.fromJson(id + ".json", new TypeToken>(){}) + id -> saveHelper.fromJson(id.getLeft() + ".json", new TypeToken>(){}) ); public Mono onMessage(MessageCreateEvent event) { From 3ef3dd119af2ab39660ce9aeaf65e4476e741008 Mon Sep 17 00:00:00 2001 From: Minecraftschurli Date: Fri, 16 Oct 2020 15:45:28 +0200 Subject: [PATCH 4/4] add possibility to use the new per channel data in the command blacklist --- .../tterrag/k9/commands/CommandControl.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/tterrag/k9/commands/CommandControl.java b/src/main/java/com/tterrag/k9/commands/CommandControl.java index 733c8fb4..3ec6c31f 100644 --- a/src/main/java/com/tterrag/k9/commands/CommandControl.java +++ b/src/main/java/com/tterrag/k9/commands/CommandControl.java @@ -3,6 +3,7 @@ import java.util.HashSet; import java.util.Set; +import com.google.common.base.Strings; import com.google.gson.reflect.TypeToken; import com.tterrag.k9.commands.CommandControl.ControlData; import com.tterrag.k9.commands.api.Argument; @@ -12,6 +13,7 @@ import com.tterrag.k9.util.Requirements; import com.tterrag.k9.util.Requirements.RequiredType; +import discord4j.common.util.Snowflake; import discord4j.rest.util.Permission; import lombok.Value; import reactor.core.publisher.Mono; @@ -31,6 +33,16 @@ public static class ControlData { private static final Argument ARG_OBJECT = new WordArgument("object", "The object to configure, be it a command name or otherwise", true); + private static final Argument ARG_CHANNEL = new SimpleArgument("channel", "The channel to configure", false) { + @Override + public Snowflake parse(String input) { + if (Strings.isNullOrEmpty(input)) { + return null; + } + return Snowflake.of(input); + } + }; + public CommandControl() { super("ctrl", false, ControlData::new); } @@ -49,12 +61,14 @@ public Mono process(CommandContext ctx) { if (ctx.hasFlag(FLAG_WHITELIST) && ctx.hasFlag(FLAG_BLACKLIST)) { return ctx.error("Illegal flag combination: Cannot whitelist and blacklist"); } + Snowflake guild = ctx.getGuildId().get(); + Snowflake channel = ctx.getArg(ARG_CHANNEL); if (ctx.hasFlag(FLAG_WHITELIST)) { - return Mono.justOrEmpty(getData(ctx)) + return Mono.justOrEmpty(getData(guild, channel)) .doOnNext(data -> data.getCommandBlacklist().remove(ctx.getArg(ARG_OBJECT))) .then(ctx.reply("Whitelisted command.")); } else if (ctx.hasFlag(FLAG_BLACKLIST)) { - return Mono.justOrEmpty(getData(ctx)) + return Mono.justOrEmpty(getData(guild, channel)) .doOnNext(data -> data.getCommandBlacklist().add(ctx.getArg(ARG_OBJECT))) .then(ctx.reply("Blacklisted command.")); }