001/*
002 * PermissionsEx
003 * Copyright (C) zml and PermissionsEx contributors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package ca.stellardrift.permissionsex.minecraft.command.definition;
018
019import ca.stellardrift.permissionsex.datastore.ConversionResult;
020import ca.stellardrift.permissionsex.impl.PermissionsEx;
021import ca.stellardrift.permissionsex.impl.util.PCollections;
022import ca.stellardrift.permissionsex.minecraft.MinecraftPermissionsEx;
023import ca.stellardrift.permissionsex.minecraft.command.CommandException;
024import ca.stellardrift.permissionsex.minecraft.command.CommandRegistrationContext;
025import ca.stellardrift.permissionsex.minecraft.command.Commander;
026import ca.stellardrift.permissionsex.minecraft.command.Elements;
027import ca.stellardrift.permissionsex.minecraft.command.Formats;
028import ca.stellardrift.permissionsex.minecraft.command.MessageFormatter;
029import ca.stellardrift.permissionsex.minecraft.command.PEXCommandPreprocessor;
030import ca.stellardrift.permissionsex.minecraft.command.Permission;
031import ca.stellardrift.permissionsex.minecraft.command.argument.Parsers;
032import ca.stellardrift.permissionsex.subject.SubjectDataCache;
033import ca.stellardrift.permissionsex.subject.SubjectRef;
034import ca.stellardrift.permissionsex.subject.SubjectType;
035import ca.stellardrift.permissionsex.subject.SubjectTypeCollection;
036import cloud.commandframework.Command;
037import cloud.commandframework.CommandManager;
038import cloud.commandframework.arguments.CommandArgument;
039import cloud.commandframework.arguments.flags.CommandFlag;
040import cloud.commandframework.arguments.standard.StringArgument;
041import cloud.commandframework.minecraft.extras.MinecraftExtrasMetaKeys;
042import cloud.commandframework.minecraft.extras.MinecraftHelp;
043import io.leangen.geantyref.TypeToken;
044import net.kyori.adventure.text.Component;
045import net.kyori.adventure.text.format.NamedTextColor;
046import org.checkerframework.checker.nullness.qual.Nullable;
047
048import java.util.Locale;
049import java.util.regex.Pattern;
050import java.util.stream.Stream;
051
052import static ca.stellardrift.permissionsex.minecraft.command.Elements.*;
053import static net.kyori.adventure.text.Component.text;
054
055/**
056 * Provider for PermissionsEx commands.
057 */
058public final class PermissionsExCommand {
059
060    private PermissionsExCommand() {
061    }
062
063    // PEX base command
064    public static void register(final CommandRegistrationContext regCtx) {
065        final Command<Commander> helpCommand = regCtx.register(base -> help(base, regCtx.commandManager(), regCtx.manager().messageFormatter()), "help", "?");
066
067        regCtx.register(regCtx.head().handler(ctx -> {
068            final Commander sender = ctx.getSender();
069            final Component version = text(
070                "v" + regCtx.manager().engine().version(),
071                sender.formatter().highlightColor()
072            );
073
074            sender.sendMessage(text("PermissionsEx ").append(version));
075            sender.sendMessage(text("Run " + Formats.formatCommand(helpCommand, PCollections.map()) + "for more information"));
076        }));
077
078        // Subcommands without arguments
079        regCtx.register(PermissionsExCommand::debug, "debug", "d");
080        regCtx.push(RankingCommands::register, "ranking", "rank");
081        regCtx.register(PermissionsExCommand::commandImport, "import");
082        regCtx.register(PermissionsExCommand::reload, "reload", "rel");
083        regCtx.register(PermissionsExCommand::version, "version");
084
085        // legacy commands
086        regCtx.register(debug(regCtx.head().literal("toggle").literal("debug")));
087
088        // <type> list
089        final CommandArgument<Commander, SubjectType<?>> typeArg = CommandArgument.<Commander, SubjectType<?>>ofType(new TypeToken<SubjectType<?>>() {}, "type")
090            .withParser(Parsers.subjectType())
091            .build();
092
093        final Command.Builder<Commander> typeBuilder = regCtx.head()
094            .argument(typeArg);
095
096        regCtx.register(list(typeBuilder.literal("list"), typeArg));
097
098        // <type> <name>...
099        // this is where things start to get hairy
100        final CommandArgument<Commander, ?> identifierArg = CommandArgument.<Commander, Object>ofType(Object.class, "subject")
101            .withParser(Parsers.subjectIdentifier(data -> data.get(typeArg)))
102            .build();
103
104            /* final CommandArgument<Commander, SubjectRef<?>> subjectRef = new ArgumentPair<Commander, SubjectType, Object, SubjectRef<?>>(
105                true,
106                "subject",
107                Pair.of("type", "identifier"),
108                Pair.of(SubjectType.class, Object.class),
109                Pair.of(typeArg.getParser(), Parsers.subjectIdentifier(data -> (SubjectType) data.get(typeArg.getName()))),
110                (cmd, pair) -> {
111                    return SubjectRef.subject(pair.getFirst(), pair.getSecond());
112                },
113                new TypeToken<SubjectRef<?>>() {}
114            ) {}; */
115
116        regCtx.push(typeBuilder.argument(identifierArg), child -> {
117            final SubjectRefProvider refArg = SubjectRefProvider.of(typeArg, identifierArg);
118            child.register(build -> InfoSubcommand.register(build, refArg), "info", "i");
119
120            child.push(child.head().flag(FLAG_TRANSIENT).flag(FLAG_CONTEXT), grandchild -> {
121                grandchild.register(build -> DeleteSubcommand.register(build, refArg), "delete", "del", "remove", "rem", "rm");
122                grandchild.push(build -> ParentSubcommand.register(build, refArg), "parents", "parent", "par");
123                grandchild.register(build -> OptionSubcommand.register(build, refArg), "option", "options", "opt", "o", "meta");
124                grandchild.register(build -> PermissionsSubcommands.permission(build, refArg), "permission", "permissions", "perm", "perms", "p");
125                grandchild.register(build -> PermissionsSubcommands.permissionDefault(build, refArg), "permission-default", "perms-def", "permsdef", "pd", "default", "def");
126            });
127        });
128    }
129
130    // Direct literal subcommands
131
132    private static Command.Builder<Commander> help(final Command.Builder<Commander> base, final CommandManager<Commander> mgr, final MessageFormatter formatter) {
133        final CommandArgument<Commander, String> query = StringArgument.optional("query", StringArgument.StringMode.GREEDY);
134        final Command.Builder<Commander> helpCommand = base
135            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.PEX_HELP_DESCRIPTION.tr())
136            .argument(query)
137            .permission(Permission.pex("help"));
138
139        final MinecraftHelp<Commander> help = new MinecraftHelp<>(
140                Formats.formatCommand(helpCommand, PCollections.map()),
141                cmd -> cmd,
142                mgr
143        );
144
145        help.setHelpColors(MinecraftHelp.HelpColors.of(
146            formatter.responseColor(),
147            formatter.highlightColor(),
148            Formats.lerp(0.2f, formatter.highlightColor(), NamedTextColor.BLACK),
149            NamedTextColor.GRAY,
150            NamedTextColor.DARK_GRAY
151        ));
152
153        return helpCommand
154                .handler(ctx -> help.queryCommands(ctx.getOrDefault(query, ""), ctx.getSender()));
155    }
156
157    private static Command.Builder<Commander> debug(final Command.Builder<Commander> base) {
158        final CommandArgument<Commander, Pattern> filterArg = CommandArgument.<Commander, Pattern>ofType(Pattern.class, "pattern")
159            .asOptional()
160            .withParser(Parsers.greedyPattern())
161            .build();
162
163        return base
164            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.DEBUG_DESCRIPTION.tr())
165            .permission(Permission.pex("debug"))
166            .argument(filterArg)
167            .handler(handler((source, engine, ctx) -> {
168
169                final boolean debugEnabled = !engine.debugMode();
170                final @Nullable Pattern filter = ctx.contains(filterArg.getName()) ? ctx.get(filterArg) : null;
171
172                if (filter != null) {
173                    engine.debugMode(debugEnabled, filter);
174                    source.sendMessage(
175                        Messages.DEBUG_SUCCESS_FILTER.tr(
176                            Formats.bool(debugEnabled),
177                            source.formatter().hl(text().content(filter.pattern()))
178                        )
179                    );
180                } else {
181                    engine.debugMode(debugEnabled);
182                    source.sendMessage(Messages.DEBUG_SUCCESS.tr(Formats.bool(debugEnabled)));
183                }
184            }));
185    }
186
187    private static Command.Builder<Commander> commandImport(final Command.Builder<Commander> base) {
188        final CommandArgument<Commander, String> dataStoreArg = StringArgument.<Commander>newBuilder("data store")
189            .withSuggestionsProvider((ctx, input) -> {
190                final PermissionsEx<?> engine = ctx.get(PEXCommandPreprocessor.PEX_MANAGER).engine();
191                return PCollections.asVector(engine.getAvailableConversions(), conv -> conv.store().identifier());
192                // TODO: include data store names here
193            })
194            .asOptional()
195            .build();
196
197        return base
198            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.PEX_IMPORT_DESCRIPTION.tr())
199            .argument(dataStoreArg)
200            .permission(Permission.pex("import"))
201            .handler(ctx -> {
202                final Commander source = ctx.getSender();
203                final PermissionsEx<?> engine = ctx.get(PEXCommandPreprocessor.PEX_MANAGER).engine();
204                final @Nullable String requestedName = ctx.contains(dataStoreArg.getName()) ? ctx.get(dataStoreArg) : null;
205                if (requestedName == null) {
206                    /* list available conversion actions */
207                    source.sendPaginated(
208                        Messages.IMPORT_LISTING_HEADER.tr(),
209                        Messages.IMPORT_LISTING_SUBTITLE.tr(source.formatter().hl(text().content("/pex import [id]"))),
210                        engine.getAvailableConversions().stream().map(conv ->
211                            source.callback(text()
212                                .append(conv.description())
213                                .append(text(" - /pex import "))
214                                .append(text(conv.store().identifier())), src -> {
215                                src.sendMessage(Messages.IMPORT_ACTION_BEGINNING.tr(conv.description()));
216                                engine.importDataFrom(conv)
217                                    .whenComplete(messageSender(src, Messages.IMPORT_ACTION_SUCCESS.tr(conv.description())));
218                            })
219                        )
220                    );
221                } else {
222                    /* execute a specific import action */
223                    for (final ConversionResult result : engine.getAvailableConversions()) {
224                        if (result.store().identifier().equalsIgnoreCase(requestedName)) {
225                            source.sendMessage(Messages.IMPORT_ACTION_BEGINNING.tr(result.description()));
226                            engine.importDataFrom(result)
227                                .whenComplete(messageSender(source, Messages.IMPORT_ACTION_SUCCESS.tr(result.description())));
228                            return;
229                        }
230                    }
231
232                    if (engine.config().getDataStore(requestedName) == null) {
233                        throw new CommandException(Messages.IMPORT_ERROR_UNKNOWN_STORE.tr(requestedName));
234                    }
235                    source.sendMessage(Messages.IMPORT_ACTION_BEGINNING.tr(requestedName));
236                    engine.importDataFrom(requestedName)
237                        .whenComplete(messageSender(source, Messages.IMPORT_ACTION_SUCCESS.tr(requestedName)));
238                }
239            });
240    }
241
242    private static Command.Builder<Commander> reload(final Command.Builder<Commander> base) {
243        return base
244            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.RELOAD_DESCRIPTION.tr())
245            .permission(Permission.pex("reload"))
246            .handler(ctx -> {
247                ctx.getSender().sendMessage(Messages.RELOAD_ACTION_BEGIN.tr());
248                ctx.get(PEXCommandPreprocessor.PEX_MANAGER).engine().reload()
249                    .whenComplete(messageSender(ctx.getSender(), Messages.RELOAD_ACTION_SUCCESS.tr()));
250            });
251    }
252
253    private static Command.Builder<Commander> version(final Command.Builder<Commander> base) {
254        final CommandFlag<Void> verboseFlag = CommandFlag.newBuilder("verbose").withAliases("v").build();
255        return base
256            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.VERSION_DESCRIPTION.tr())
257            .permission(Permission.pex("version"))
258            .flag(verboseFlag)
259            .handler(ctx -> {
260                final boolean verbose = ctx.flags().isPresent(verboseFlag.getName());
261                ctx.<MinecraftPermissionsEx<?>>get(PEXCommandPreprocessor.PEX_MANAGER)
262                    .describe(ctx.getSender(), verbose);
263            });
264    }
265
266    private static Command.Builder<Commander> list(final Command.Builder<Commander> base, final CommandArgument<Commander, SubjectType<?>> subjectTypeArg) {
267        final CommandArgument<Commander, String> filterArg = StringArgument.optional("filter");
268        final Permission basePerm = Permission.pex("list");
269        return base
270            .meta(MinecraftExtrasMetaKeys.DESCRIPTION, Messages.PEX_LIST_DESCRIPTION.tr())
271            // .permission(basePerm) // TODO: Allow prefix permissions
272            .argument(filterArg)
273            .flag(Elements.FLAG_TRANSIENT)
274            .handler(handler((source, engine, ctx) -> {
275                final SubjectType<?> type = ctx.get(subjectTypeArg);
276                source.checkPermission(basePerm.then(type.name()));
277                printList(
278                    source,
279                    engine.subjects(type),
280                    ctx.flags().isPresent(Elements.FLAG_TRANSIENT.getName()),
281                    ctx.contains(filterArg.getName()) ? ctx.get(filterArg) : null
282                );
283            }));
284    }
285
286    private static <I> void printList(
287        final Commander source,
288        final SubjectTypeCollection<I> collection,
289        final boolean transientData,
290        final @Nullable String filter
291    ) {
292        final SubjectDataCache<I> data;
293        if (transientData) {
294            data = collection.transientData();
295        } else {
296            data = collection.persistentData();
297        }
298
299        Stream<SubjectRef<I>> identifiers = data.getAllIdentifiers()
300            .map(id -> SubjectRef.subject(collection.type(), id));
301
302        if (filter != null && !filter.isEmpty()) {
303            final String lowerFilter = filter.toLowerCase(Locale.ROOT);
304            identifiers = identifiers.filter(it -> it.serializedIdentifier()
305                .toLowerCase(Locale.ROOT)
306                .startsWith(lowerFilter));
307        }
308
309
310        source.sendPaginated(
311            Messages.PEX_LIST_HEADER.tr(collection.type().name()),
312            Messages.PEX_LIST_SUBTITLE.tr(collection.type().name()),
313            identifiers.map(source.formatter()::subject)
314        );
315    }
316}