From 2e9561371deebfc293bcab50f3fa8969a1c77b73 Mon Sep 17 00:00:00 2001 From: Luck Date: Sat, 13 May 2017 20:36:05 +0100 Subject: [PATCH] Implement initial web editor support --- .../common/commands/CommandManager.java | 2 + .../abstraction/SharedSubCommand.java | 2 +- .../impl/generic/other/HolderEditor.java | 191 +++++++++++++++++ .../commands/impl/group/GroupMainCommand.java | 2 + .../commands/impl/misc/ApplyEditsCommand.java | 201 ++++++++++++++++++ .../commands/impl/user/UserMainCommand.java | 2 + .../luckperms/common/constants/Message.java | 12 ++ .../common/constants/Permission.java | 3 + 8 files changed, 414 insertions(+), 1 deletion(-) create mode 100644 common/src/main/java/me/lucko/luckperms/common/commands/impl/generic/other/HolderEditor.java create mode 100644 common/src/main/java/me/lucko/luckperms/common/commands/impl/misc/ApplyEditsCommand.java diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/CommandManager.java b/common/src/main/java/me/lucko/luckperms/common/commands/CommandManager.java index 18c6e005a..409ebba34 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/CommandManager.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/CommandManager.java @@ -36,6 +36,7 @@ import me.lucko.luckperms.common.commands.impl.group.GroupMainCommand; import me.lucko.luckperms.common.commands.impl.group.ListGroups; import me.lucko.luckperms.common.commands.impl.log.LogMainCommand; import me.lucko.luckperms.common.commands.impl.migration.MigrationMainCommand; +import me.lucko.luckperms.common.commands.impl.misc.ApplyEditsCommand; import me.lucko.luckperms.common.commands.impl.misc.BulkUpdateCommand; import me.lucko.luckperms.common.commands.impl.misc.CheckCommand; import me.lucko.luckperms.common.commands.impl.misc.ExportCommand; @@ -105,6 +106,7 @@ public class CommandManager { .add(new ReloadConfigCommand()) .add(new BulkUpdateCommand()) .add(new MigrationMainCommand()) + .add(new ApplyEditsCommand()) .add(new CreateGroup()) .add(new DeleteGroup()) .add(new ListGroups()) diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/abstraction/SharedSubCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/abstraction/SharedSubCommand.java index b7cb10c0d..5cdbad58a 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/abstraction/SharedSubCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/abstraction/SharedSubCommand.java @@ -108,7 +108,7 @@ public abstract class SharedSubCommand { return user ? userPermission.isAuthorized(sender) : groupPermission.isAuthorized(sender); } - protected static void save(PermissionHolder holder, Sender sender, LuckPermsPlugin plugin) { + public static void save(PermissionHolder holder, Sender sender, LuckPermsPlugin plugin) { if (holder instanceof User) { User user = ((User) holder); SubCommand.save(user, sender, plugin); diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/impl/generic/other/HolderEditor.java b/common/src/main/java/me/lucko/luckperms/common/commands/impl/generic/other/HolderEditor.java new file mode 100644 index 000000000..f1cdaa085 --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/commands/impl/generic/other/HolderEditor.java @@ -0,0 +1,191 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.commands.impl.generic.other; + +import com.google.common.base.Splitter; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonWriter; + +import me.lucko.luckperms.common.commands.CommandException; +import me.lucko.luckperms.common.commands.CommandResult; +import me.lucko.luckperms.common.commands.abstraction.SubCommand; +import me.lucko.luckperms.common.commands.sender.Sender; +import me.lucko.luckperms.common.constants.Message; +import me.lucko.luckperms.common.constants.Permission; +import me.lucko.luckperms.common.core.NodeModel; +import me.lucko.luckperms.common.core.PriorityComparator; +import me.lucko.luckperms.common.core.model.Group; +import me.lucko.luckperms.common.core.model.PermissionHolder; +import me.lucko.luckperms.common.core.model.User; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.utils.Predicates; + +import io.github.mkremins.fanciful.ChatColor; +import io.github.mkremins.fanciful.FancyMessage; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.StringWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class HolderEditor extends SubCommand { + public HolderEditor(boolean user) { + super("editor", "Opens the web permission editor", user ? Permission.USER_EDITOR : Permission.GROUP_EDITOR, + Predicates.alwaysFalse(), null + ); + } + + @Override + public CommandResult execute(LuckPermsPlugin plugin, Sender sender, T holder, List args, String label) throws CommandException { + JsonObject data = new JsonObject(); + Set nodes = holder.getNodes().values().stream().map(NodeModel::fromNode).collect(Collectors.toCollection(LinkedHashSet::new)); + data.add("nodes", serializePermissions(nodes)); + data.addProperty("who", id(holder)); + + String dataUrl = paste(new GsonBuilder().setPrettyPrinting().create().toJson(data)); + if (dataUrl == null) { + Message.EDITOR_UPLOAD_FAILURE.send(sender); + return CommandResult.STATE_ERROR; + } + + List parts = Splitter.on('/').splitToList(dataUrl); + String id = parts.get(4) + "/" + parts.get(6); + String url = "https://lpedit.lucko.me/?" + id; + + Message.EDITOR_URL.send(sender); + sender.sendMessage(new FancyMessage(url).color(ChatColor.getByChar('b')).link(url)); + return CommandResult.SUCCESS; + } + + private static String id(PermissionHolder holder) { + if (holder instanceof User) { + User user = ((User) holder); + return "user/" + user.getUuid().toString(); + } else { + Group group = ((Group) holder); + return "group/" + group.getName(); + } + } + + private static String paste(String content) { + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) new URL("https://api.github.com/gists").openConnection(); + connection.setRequestMethod("POST"); + connection.setDoInput(true); + connection.setDoOutput(true); + + try (OutputStream os = connection.getOutputStream()) { + StringWriter sw = new StringWriter(); + new JsonWriter(sw).beginObject() + .name("description").value("LuckPerms Web Permissions Editor Data") + .name("public").value(false) + .name("files") + .beginObject().name("luckperms-data.json") + .beginObject().name("content").value(content) + .endObject() + .endObject() + .endObject(); + + os.write(sw.toString().getBytes(StandardCharsets.UTF_8)); + } + + if (connection.getResponseCode() >= 400) { + throw new RuntimeException("Connection returned response code: " + connection.getResponseCode() + " - " + connection.getResponseMessage()); + } + + try (InputStream inputStream = connection.getInputStream()) { + try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) { + JsonObject response = new Gson().fromJson(reader, JsonObject.class); + return response.get("files").getAsJsonObject().get("luckperms-data.json").getAsJsonObject().get("raw_url").getAsString(); + } + } + + } catch (Exception e) { + e.printStackTrace(); + return null; + } finally { + if (connection != null) { + try { + connection.disconnect(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + private static JsonArray serializePermissions(Set nodes) { + List data = new ArrayList<>(); + + for (NodeModel node : nodes) { + JsonObject attributes = new JsonObject(); + attributes.addProperty("permission", node.getPermission()); + attributes.addProperty("value", node.isValue()); + + if (!node.getServer().equals("global")) { + attributes.addProperty("server", node.getServer()); + } + + if (!node.getWorld().equals("global")) { + attributes.addProperty("world", node.getWorld()); + } + + if (node.getExpiry() != 0L) { + attributes.addProperty("expiry", node.getExpiry()); + } + + if (!node.getContexts().isEmpty()) { + attributes.add("context", node.getContextsAsJson()); + } + + data.add(attributes); + } + + data.sort((o1, o2) -> PriorityComparator.get().compareStrings( + o1.get("permission").getAsString(), + o2.get("permission").getAsString() + )); + + JsonArray arr = new JsonArray(); + for (JsonObject o : data) { + arr.add(o); + } + + return arr; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/impl/group/GroupMainCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/impl/group/GroupMainCommand.java index 7a4ef38de..3a3ff039f 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/impl/group/GroupMainCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/impl/group/GroupMainCommand.java @@ -31,6 +31,7 @@ import me.lucko.luckperms.common.commands.abstraction.Command; import me.lucko.luckperms.common.commands.abstraction.MainCommand; import me.lucko.luckperms.common.commands.impl.generic.meta.CommandMeta; import me.lucko.luckperms.common.commands.impl.generic.other.HolderClear; +import me.lucko.luckperms.common.commands.impl.generic.other.HolderEditor; import me.lucko.luckperms.common.commands.impl.generic.other.HolderShowTracks; import me.lucko.luckperms.common.commands.impl.generic.parent.CommandParent; import me.lucko.luckperms.common.commands.impl.generic.permission.CommandPermission; @@ -49,6 +50,7 @@ public class GroupMainCommand extends MainCommand { .add(new CommandPermission<>(false)) .add(new CommandParent<>(false)) .add(new CommandMeta<>(false)) + .add(new HolderEditor<>(false)) .add(new GroupListMembers()) .add(new GroupSetWeight()) .add(new HolderShowTracks<>(false)) diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/impl/misc/ApplyEditsCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/impl/misc/ApplyEditsCommand.java new file mode 100644 index 000000000..4274ac66a --- /dev/null +++ b/common/src/main/java/me/lucko/luckperms/common/commands/impl/misc/ApplyEditsCommand.java @@ -0,0 +1,201 @@ +/* + * This file is part of LuckPerms, licensed under the MIT License. + * + * Copyright (c) lucko (Luck) + * Copyright (c) contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package me.lucko.luckperms.common.commands.impl.misc; + +import com.google.common.base.Splitter; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import me.lucko.luckperms.api.context.ImmutableContextSet; +import me.lucko.luckperms.common.commands.Arg; +import me.lucko.luckperms.common.commands.CommandException; +import me.lucko.luckperms.common.commands.CommandResult; +import me.lucko.luckperms.common.commands.abstraction.SharedSubCommand; +import me.lucko.luckperms.common.commands.abstraction.SingleCommand; +import me.lucko.luckperms.common.commands.sender.Sender; +import me.lucko.luckperms.common.commands.utils.Util; +import me.lucko.luckperms.common.constants.Message; +import me.lucko.luckperms.common.constants.Permission; +import me.lucko.luckperms.common.core.NodeModel; +import me.lucko.luckperms.common.core.model.PermissionHolder; +import me.lucko.luckperms.common.plugin.LuckPermsPlugin; +import me.lucko.luckperms.common.utils.Predicates; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +public class ApplyEditsCommand extends SingleCommand { + public ApplyEditsCommand() { + super("ApplyEdits", "Applies permission changes made from the web editor", + "/%s applyedits [target]", Permission.APPLY_EDITS, Predicates.notInRange(1, 2), + Arg.list( + Arg.create("code", true, "the unique code for the data"), + Arg.create("target", false, "who to apply the data to") + ) + ); + } + + @Override + public CommandResult execute(LuckPermsPlugin plugin, Sender sender, List args, String label) throws CommandException { + String code = args.get(0); + String who = args.size() == 2 ? args.get(1) : null; + + if (!code.contains("/")) { + Message.APPLY_EDITS_INVALID_CODE.send(sender, code); + return CommandResult.INVALID_ARGS; + } + + Iterator codeParts = Splitter.on('/').limit(2).split(code).iterator(); + String part1 = codeParts.next(); + String part2 = codeParts.next(); + + if (part1.isEmpty() || part2.isEmpty()) { + Message.APPLY_EDITS_INVALID_CODE.send(sender, code); + return CommandResult.INVALID_ARGS; + } + + String url = "https://gist.githubusercontent.com/anonymous/" + part1 + "/raw/" + part2 + "/luckperms-data.json"; + JsonObject data; + + try { + data = read(url); + } catch (Exception e) { + e.printStackTrace(); + Message.APPLY_EDITS_UNABLE_TO_READ.send(sender, code); + return CommandResult.FAILURE; + } + + if (who == null) { + if (!data.has("who") || data.get("who").getAsString().isEmpty()) { + Message.APPLY_EDITS_NO_TARGET.send(sender); + return CommandResult.STATE_ERROR; + } + + who = data.get("who").getAsString(); + } + + PermissionHolder holder; + + if (who.startsWith("group/")) { + String group = who.substring("group/".length()); + holder = plugin.getGroupManager().getIfLoaded(group); + + if (holder == null) { + Message.APPLY_EDITS_TARGET_GROUP_NOT_EXISTS.send(sender, group); + return CommandResult.STATE_ERROR; + } + } else if (who.startsWith("user/")) { + String user = who.substring("user/".length()); + UUID uuid = Util.parseUuid(user); + if (uuid == null) { + Message.APPLY_EDITS_TARGET_USER_NOT_UUID.send(sender, user); + return CommandResult.STATE_ERROR; + } + holder = plugin.getUserManager().getIfLoaded(uuid); + if (holder == null) { + plugin.getStorage().loadUser(uuid, null).join(); + } + holder = plugin.getUserManager().getIfLoaded(uuid); + if (holder == null) { + Message.APPLY_EDITS_TARGET_USER_UNABLE_TO_LOAD.send(sender, uuid.toString()); + return CommandResult.STATE_ERROR; + } + } else { + Message.APPLY_EDITS_TARGET_UNKNOWN.send(sender, who); + return CommandResult.STATE_ERROR; + } + + Set nodes = deserializePermissions(data.getAsJsonArray("nodes")); + holder.setNodes(nodes.stream().map(NodeModel::toNode).collect(Collectors.toSet())); + Message.APPLY_EDITS_SUCCESS.send(sender, nodes.size(), holder.getFriendlyName()); + SharedSubCommand.save(holder, sender, plugin); + return CommandResult.SUCCESS; + } + + @Override + public boolean shouldDisplay() { + return false; + } + + private static JsonObject read(String address) throws IOException { + URL url = new URL(address); + try (InputStream in = url.openStream(); InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) { + return new Gson().fromJson(reader, JsonObject.class); + } + } + + private static Set deserializePermissions(JsonArray permissionsSection) { + Set nodes = new HashSet<>(); + + for (JsonElement ent : permissionsSection) { + if (!ent.isJsonObject()) { + continue; + } + + JsonObject data = ent.getAsJsonObject(); + + String permission = data.get("permission").getAsString(); + boolean value = true; + String server = "global"; + String world = "global"; + long expiry = 0L; + ImmutableContextSet context = ImmutableContextSet.empty(); + + if (data.has("value")) { + value = data.get("value").getAsBoolean(); + } + if (data.has("server")) { + server = data.get("server").getAsString(); + } + if (data.has("world")) { + world = data.get("world").getAsString(); + } + if (data.has("expiry")) { + expiry = data.get("expiry").getAsLong(); + } + + if (data.has("context") && data.get("context").isJsonObject()) { + JsonObject contexts = data.get("context").getAsJsonObject(); + context = NodeModel.deserializeContextSet(contexts).makeImmutable(); + } + + nodes.add(NodeModel.of(permission, value, server, world, expiry, context)); + } + + return nodes; + } +} diff --git a/common/src/main/java/me/lucko/luckperms/common/commands/impl/user/UserMainCommand.java b/common/src/main/java/me/lucko/luckperms/common/commands/impl/user/UserMainCommand.java index 979ffccf5..bcf0fe341 100644 --- a/common/src/main/java/me/lucko/luckperms/common/commands/impl/user/UserMainCommand.java +++ b/common/src/main/java/me/lucko/luckperms/common/commands/impl/user/UserMainCommand.java @@ -31,6 +31,7 @@ import me.lucko.luckperms.common.commands.abstraction.Command; import me.lucko.luckperms.common.commands.abstraction.MainCommand; import me.lucko.luckperms.common.commands.impl.generic.meta.CommandMeta; import me.lucko.luckperms.common.commands.impl.generic.other.HolderClear; +import me.lucko.luckperms.common.commands.impl.generic.other.HolderEditor; import me.lucko.luckperms.common.commands.impl.generic.other.HolderShowTracks; import me.lucko.luckperms.common.commands.impl.generic.parent.CommandParent; import me.lucko.luckperms.common.commands.impl.generic.permission.CommandPermission; @@ -51,6 +52,7 @@ public class UserMainCommand extends MainCommand { .add(new CommandPermission<>(true)) .add(new CommandParent<>(true)) .add(new CommandMeta<>(true)) + .add(new HolderEditor<>(true)) .add(new UserSwitchPrimaryGroup()) .add(new UserPromote()) .add(new UserDemote()) diff --git a/common/src/main/java/me/lucko/luckperms/common/constants/Message.java b/common/src/main/java/me/lucko/luckperms/common/constants/Message.java index c9493e84a..07c1af3b3 100644 --- a/common/src/main/java/me/lucko/luckperms/common/constants/Message.java +++ b/common/src/main/java/me/lucko/luckperms/common/constants/Message.java @@ -120,6 +120,18 @@ public enum Message { SEARCH_SHOWING_USERS_WITH_PAGE("&bShowing user entries: {0}", true), SEARCH_SHOWING_GROUPS_WITH_PAGE("&bShowing group entries: {0}", true), + APPLY_EDITS_INVALID_CODE("&aInvalid code. &7({0})", true), + APPLY_EDITS_UNABLE_TO_READ("&aUnable to read data using the given code. &7({0})", true), + APPLY_EDITS_NO_TARGET("&aUnable to parse the target of the edit. Please supply it as an extra argument.", true), + APPLY_EDITS_TARGET_GROUP_NOT_EXISTS("&aTarget group &b{0}&a does not exist.", true), + APPLY_EDITS_TARGET_USER_NOT_UUID("&aTarget user &b{0}&a is not a valid uuid.", true), + APPLY_EDITS_TARGET_USER_UNABLE_TO_LOAD("&aUnable to load target user &b{0}&a.", true), + APPLY_EDITS_TARGET_UNKNOWN("&aInvalid target. &7({0})", true), + APPLY_EDITS_SUCCESS("&aSuccessfully applied &b{0}&a nodes to &b{1}&a.", true), + + EDITOR_UPLOAD_FAILURE("&cUnable to upload permission data to the editor.", true), + EDITOR_URL("&aEditor URL:", true), + CHECK_RESULT("&aPermission check result on user &b{0}&a for permission &b{1}&a: &f{2}", true), CREATE_SUCCESS("&b{0}&a was successfully created.", true), diff --git a/common/src/main/java/me/lucko/luckperms/common/constants/Permission.java b/common/src/main/java/me/lucko/luckperms/common/constants/Permission.java index 2863dda82..56952cfbe 100644 --- a/common/src/main/java/me/lucko/luckperms/common/constants/Permission.java +++ b/common/src/main/java/me/lucko/luckperms/common/constants/Permission.java @@ -48,6 +48,7 @@ public enum Permission { EXPORT(list("export"), Type.NONE), RELOAD_CONFIG(list("reloadconfig"), Type.NONE), BULK_UPDATE(list("bulkupdate"), Type.NONE), + APPLY_EDITS(list("applyedits"), Type.NONE), MIGRATION(list("migration"), Type.NONE), CREATE_GROUP(list("creategroup"), Type.NONE), @@ -89,6 +90,7 @@ public enum Permission { USER_META_REMOVETEMP_PREFIX(list("meta.removetempprefix", "removetempprefix"), Type.USER), USER_META_REMOVETEMP_SUFFIX(list("meta.removetempsuffix", "removetempsuffix"), Type.USER), USER_META_CLEAR(list("meta.clear", "clearmeta"), Type.USER), + USER_EDITOR(list("editor"), Type.USER), USER_SWITCHPRIMARYGROUP(list("switchprimarygroup", "setprimarygroup"), Type.USER), USER_SHOWTRACKS(list("showtracks"), Type.USER), USER_PROMOTE(list("promote"), Type.USER), @@ -126,6 +128,7 @@ public enum Permission { GROUP_META_REMOVETEMP_PREFIX(list("meta.removetempprefix", "removetempprefix"), Type.GROUP), GROUP_META_REMOVETEMP_SUFFIX(list("meta.removetempsuffix", "removetempsuffix"), Type.GROUP), GROUP_META_CLEAR(list("meta.clear", "clearmeta"), Type.GROUP), + GROUP_EDITOR(list("editor"), Type.GROUP), GROUP_LISTMEMBERS(list("listmembers"), Type.GROUP), GROUP_SHOWTRACKS(list("showtracks"), Type.GROUP), GROUP_SETWEIGHT(list("setweight"), Type.GROUP),