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}