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.minecraft.MinecraftPermissionsEx;
020import ca.stellardrift.permissionsex.subject.SubjectRef;
021import net.kyori.adventure.audience.ForwardingAudience;
022import net.kyori.adventure.audience.MessageType;
023import net.kyori.adventure.identity.Identified;
024import net.kyori.adventure.identity.Identity;
025import net.kyori.adventure.text.BuildableComponent;
026import net.kyori.adventure.text.Component;
027import net.kyori.adventure.text.ComponentBuilder;
028import net.kyori.adventure.text.ComponentLike;
029import net.kyori.adventure.text.TextComponent;
030import net.kyori.adventure.text.event.ClickEvent;
031import net.kyori.adventure.text.format.NamedTextColor;
032import net.kyori.adventure.text.format.TextDecoration;
033import org.checkerframework.checker.nullness.qual.NonNull;
034import org.checkerframework.checker.nullness.qual.Nullable;
035
036import java.util.Arrays;
037import java.util.Collection;
038import java.util.Objects;
039import java.util.function.Consumer;
040import java.util.stream.Stream;
041
042import static net.kyori.adventure.text.Component.text;
043
044/**
045 * An actor that can perform commands and receive messages.
046 */
047public interface Commander extends ForwardingAudience.Single {
048
049    /**
050     * The display name for the actor, to be used in any potential action logging.
051     *
052     * @return the name
053     */
054    Component name();
055
056    /**
057     * A reference to the subject used
058     * @return a subject identifier
059     */
060    @Nullable SubjectRef<?> subjectIdentifier();
061
062    /**
063     * A formatter providing formatting options for messages sent to this commander.
064     *
065     * <p>This is usually the same message formatter exposed in {@link MinecraftPermissionsEx#messageFormatter()}.</p>
066     *
067     * @return the formatter
068     */
069    MessageFormatter formatter();
070
071    /**
072     * Get whether this commander has a certain permission.
073     *
074     * <p>This result might not match checking the subject identified by {@link #subjectIdentifier()}
075     * due to additional context that may be present in command execution.</p>
076     *
077     * @param permission the permission to test
078     * @return whether this subject has a certain permission
079     */
080    boolean hasPermission(final String permission);
081
082    default boolean hasPermission(final Permission permission) {
083        return this.hasPermission(permission.value());
084    }
085
086    default void checkPermission(final String permission) throws CommandPermissionException {
087        if (!hasPermission(permission)) {
088            throw new CommandPermissionException(permission);
089        }
090    }
091
092    default void checkPermission(final Permission permission) throws CommandPermissionException {
093        if (!hasPermission(permission)) {
094            throw new CommandPermissionException(permission.value());
095        }
096    }
097
098    /**
099     * Check a permission specialized for a certain subject type.
100     *
101     * @param basePermission the base permission to use
102     * @param subject the specific subject to validate
103     * @throws CommandException thrown if the permission check fails
104     */
105    default void checkSubjectPermission(final String basePermission, final SubjectRef<?> subject) throws CommandException {
106        if (!hasPermission(basePermission + '.' + subject.type().name() + '.' + subject.serializedIdentifier()) // permission to act on others
107                && (!Objects.equals(subject, this.subjectIdentifier()) || !hasPermission(basePermission + ".own"))) { // specialized permission when acting on self
108            throw new CommandException(Messages.EXECUTOR_ERROR_NO_PERMISSION.tr());
109        }
110    }
111
112
113    /**
114     * {@inheritDoc}
115     *
116     * <p>The message should be colored the appropriate output colour if it does not yet have a colour</p>
117     */
118    @Override
119    default void sendMessage(@NonNull Identified source, @NonNull Component message, @NonNull MessageType type) {
120        this.audience().sendMessage(source, message.colorIfAbsent(this.formatter().responseColor()), type);
121    }
122
123    /**
124     * {@inheritDoc}
125     *
126     * <p>The message should be colored the appropriate output colour if it does not yet have a colour</p>
127     */
128    @Override
129    default void sendMessage(@NonNull Identity source, @NonNull Component message, @NonNull MessageType type) {
130        this.audience().sendMessage(source, message.colorIfAbsent(this.formatter().responseColor()), type);
131    }
132
133    /**
134     * Send debug text.
135     *
136     * @param text the text that will be sent
137     */
138    default void debug(final ComponentLike text) {
139        sendMessage(text.asComponent().colorIfAbsent(NamedTextColor.GRAY));
140    }
141
142    /**
143     * Send an error message.
144     *
145     * @param text the error message
146     */
147    default void error(final ComponentLike text) {
148        this.error(text, null);
149    }
150
151    /**
152     * Send an error message to the client.
153     *
154     * @param text the message to send
155     * @param error an exception to optionally expose as a hover event on the message.
156     */
157    default void error(final ComponentLike text, final @Nullable Throwable error) {
158        if (error != null && hasPermission("permissionsex.show-stacktrace-on-hover")) {
159            // We can do a hover stacktrace
160            final TextComponent.Builder base = text().content("The error that occurred was:");
161            for (final StackTraceElement line : error.getStackTrace()) {
162                base.append(Component.newline())
163                        .append(text(line.toString().replace("\t", "    ")));
164            }
165             this.sendMessage(Component.text().append(text).color(NamedTextColor.RED).hoverEvent(base.build()));
166        } else {
167           this.sendMessage(text.asComponent().colorIfAbsent(NamedTextColor.RED));
168        }
169    }
170
171    /**
172     * Send a paginated list to the user.
173     *
174     * @param title a title
175     * @param lines the lines to send
176     */
177    default void sendPaginated(
178        final ComponentLike title,
179        final Collection<? extends ComponentLike> lines
180    ) {
181        this.sendPaginated(title, null, lines.stream());
182    }
183
184    /**
185     * Send a paginated list to the user.
186     *
187     * @param title a title
188     * @param header a header/subtitle
189     * @param lines the lines to send
190     */
191    default void sendPaginated(
192        final ComponentLike title,
193        final @Nullable ComponentLike header,
194        final Collection<? extends ComponentLike> lines
195    ) {
196        this.sendPaginated(title, header, lines.stream());
197    }
198
199    /**
200     * Send a paginated list to the user.
201     *
202     * @param title a title
203     * @param lines the lines to send
204     */
205    default void sendPaginated(
206            final ComponentLike title,
207            final Stream<? extends ComponentLike> lines
208    ) {
209        this.sendPaginated(title, null, lines);
210    }
211
212    /**
213     * Send a paginated list to the user.
214     *
215     * @param title a title
216     * @param header a header/subtitle
217     * @param lines the lines to send
218     */
219    default void sendPaginated(
220            final ComponentLike title,
221            final @Nullable ComponentLike header,
222            final Stream<? extends ComponentLike> lines
223    ) {
224        final Component marker = Component.text("#");
225        this.sendMessage(Component.join(Component.space(), Arrays.asList(marker, title, marker)));
226        if (header != null) {
227            this.sendMessage(header);
228        }
229        lines.forEach(this::sendMessage);
230        this.sendMessage(Component.text("#############################"));
231    }
232
233    /**
234     * Adds a click event to the provided component builder
235     *
236     * @param callback The function to call
237     * @return The updated text
238     */
239    default  <C extends BuildableComponent<C, B>, B extends ComponentBuilder<C, B>> B callback(final B builder, final Consumer<Commander> callback) {
240        final String command = this.formatter().manager().callbackController().registerCallback(this, callback);
241        return builder.decoration(TextDecoration.UNDERLINED, true)
242                .color(this.formatter().highlightColor())
243                .clickEvent(ClickEvent.runCommand(this.formatter().transformCommand(command)));
244    }
245
246}