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;
018
019import ca.stellardrift.permissionsex.subject.SubjectRef;
020import cloud.commandframework.arguments.CommandArgument;
021import cloud.commandframework.arguments.standard.UUIDArgument;
022import cloud.commandframework.meta.CommandMeta;
023import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
024import org.checkerframework.checker.nullness.qual.Nullable;
025
026import java.util.Locale;
027import java.util.Map;
028import java.util.UUID;
029import java.util.concurrent.ConcurrentHashMap;
030import java.util.concurrent.ConcurrentMap;
031import java.util.function.Consumer;
032
033/**
034 * Handler for command callbacks.
035 */
036public final class CallbackController {
037    private static final CommandArgument<Commander, UUID> CALLBACK_ID = UUIDArgument.of("id");
038    private final ConcurrentMap<String, java.util.concurrent.ConcurrentMap<UUID, CallbackInstance>> knownCallbacks = new ConcurrentHashMap<>();
039
040    /**
041     * Register a callback, returning the command string to send to execute the provided function.
042     *
043     * @return the command to execute for the provided function
044     */
045    public String registerCallback(final Commander source, final Consumer<Commander> callback)  {
046        final UUID id = UUID.randomUUID();
047        knownCallbacks.computeIfAbsent(mapKey(source), $ -> new ConcurrentHashMap<>())
048                .put(id, new CallbackInstance(source, callback, false));
049        return "/pex cb " + id;
050    }
051
052    private String mapKey(final Commander cmd) {
053        final @Nullable SubjectRef<?> ident = cmd.subjectIdentifier();
054        if (ident != null) {
055            return ident.serializedIdentifier();
056        } else {
057            return PlainComponentSerializer.plain().serialize(cmd.name());
058        }
059    }
060
061    public void clearOwnedBy(final String name) {
062        this.knownCallbacks.remove(name);
063    }
064
065    public void clearOwnedBy(final UUID name) {
066        knownCallbacks.remove(name.toString().toLowerCase(Locale.ROOT));
067    }
068
069    public void registerCommand(final CommandRegistrationContext registration) {
070        registration.register(builder -> builder.argument(CALLBACK_ID)
071            .meta(CommandMeta.DESCRIPTION, "Trigger a registered command callback")
072            .hidden()
073            .handler(ctx -> {
074                final UUID id = ctx.get(CALLBACK_ID);
075                final Map<UUID, CallbackInstance> userCalllbacks = knownCallbacks.get(mapKey(ctx.getSender()));
076                if (userCalllbacks == null) {
077                    throw new CommandException(Messages.COMMAND_CALLBACK_ERROR_UNKNOWN_ID.tr(id));
078                }
079                final CallbackInstance callback = userCalllbacks.get(id);
080                if (callback == null) {
081                    throw new CommandException(Messages.COMMAND_CALLBACK_ERROR_UNKNOWN_ID.tr(id));
082                }
083
084                if (!mapKey(callback.source).equals(mapKey(ctx.getSender()))) {
085                    throw new CommandException(Messages.COMMAND_CALLBACK_ERROR_ONLY_OWN_ALLOWED.tr());
086                }
087
088                try {
089                    callback.callback.accept(ctx.getSender());
090                } finally {
091                    if (callback.oneUse) {
092                        userCalllbacks.remove(id);
093                    }
094                }
095            }), "callback", "cb").toString();
096    }
097
098
099    static final class CallbackInstance {
100        final Commander source;
101        final Consumer<Commander> callback;
102        final boolean oneUse;
103
104        CallbackInstance(final Commander source, final Consumer<Commander> callback, final boolean oneUse) {
105            this.source = source;
106            this.callback = callback;
107            this.oneUse = oneUse;
108        }
109    }
110}