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}