diff --git a/api/src/main/java/net/kyori/adventure/audience/Audiences.java b/api/src/main/java/net/kyori/adventure/audience/Audiences.java index 6ef32682d..ae560eef8 100644 --- a/api/src/main/java/net/kyori/adventure/audience/Audiences.java +++ b/api/src/main/java/net/kyori/adventure/audience/Audiences.java @@ -25,15 +25,34 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.function.Consumer; import java.util.stream.Collector; import java.util.stream.Collectors; +import net.kyori.adventure.text.ComponentLike; +import org.jetbrains.annotations.NotNull; -final class Audiences { - private Audiences() { - } - +/** + * {@link Audience}-related utilities. + * + * @since 4.13.0 + */ +public final class Audiences { static final Collector COLLECTOR = Collectors.collectingAndThen( Collectors.toCollection(ArrayList::new), audiences -> Audience.audience(Collections.unmodifiableCollection(audiences)) ); + + private Audiences() { + } + + /** + * Creates an action to send a message. + * + * @param message the message to send + * @return an action to send a message + * @since 4.13.0 + */ + public static @NotNull Consumer sendingMessage(final @NotNull ComponentLike message) { + return audience -> audience.sendMessage(message); + } } diff --git a/api/src/main/java/net/kyori/adventure/text/event/ClickCallback.java b/api/src/main/java/net/kyori/adventure/text/event/ClickCallback.java new file mode 100644 index 000000000..a11bdd38b --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/event/ClickCallback.java @@ -0,0 +1,273 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2023 KyoriPowered + * + * 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 net.kyori.adventure.text.event; + +import java.time.Duration; +import java.time.temporal.TemporalAmount; +import java.util.function.Consumer; +import java.util.function.Predicate; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.builder.AbstractBuilder; +import net.kyori.adventure.permission.PermissionChecker; +import net.kyori.adventure.util.PlatformAPI; +import net.kyori.examination.Examinable; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.CheckReturnValue; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A handler for callback click events. + * + * @param audience type + * @since 4.13.0 + */ +@FunctionalInterface +public interface ClickCallback { + /** + * The default lifetime of a callback after creating it, 12 hours. + * + * @since 4.13.0 + */ + Duration DEFAULT_LIFETIME = Duration.ofHours(12); + + /** + * Indicate that a callback should have unlimited uses. + * + * @since 4.13.0 + */ + int UNLIMITED_USES = -1; + + /** + * Adjust this callback to accept any audience, and perform the appropriate filtering. + * + * @param the wider type + * @param the narrower type + * @param original the original callback of a narrower audience type + * @param type the audience type to accept + * @param otherwise the action to perform on the audience if it is not of the appropriate type + * @return a new callback + * @since 4.13.0 + */ + @CheckReturnValue + @Contract(pure = true) + static @NotNull ClickCallback widen(final @NotNull ClickCallback original, final @NotNull Class type, final @Nullable Consumer otherwise) { + return audience -> { + if (type.isInstance(audience)) { + original.accept(type.cast(audience)); + } else if (otherwise != null) { + otherwise.accept(audience); + } + }; + } + + /** + * Adjust this callback to accept any audience, and perform the appropriate filtering. + * + *

No message will be sent if the audience is not of the appropriate type.

+ * + * @param the wider type + * @param the narrower type + * @param original the original callback of a narrower audience type + * @param type the audience type to accept + * @return a new callback + * @since 4.13.0 + */ + @CheckReturnValue + @Contract(pure = true) + static @NotNull ClickCallback widen(final @NotNull ClickCallback original, final @NotNull Class type) { + return widen(original, type, null); + } + + /** + * Perform an action for this event. + * + * @param audience the single-user audience who is attempting to execute this callback function. + * @since 4.13.0 + */ + void accept(final @NotNull T audience); + + /** + * Filter audiences that receive this click callback. + * + *

Actions from audiences that do not match this predicate will be silently ignored.

+ * + * @param filter the filter to test audiences with + * @return a filtered callback action + * @since 4.13.0 + */ + @CheckReturnValue + @Contract(pure = true) + default @NotNull ClickCallback filter(final @NotNull Predicate filter) { + return this.filter(filter, null); + } + + /** + * Filter audiences that receive this click callback. + * + * @param filter the filter to test audiences with + * @param otherwise the action to perform on the audience if the conditions are not met + * @return a filtered callback action + * @since 4.13.0 + */ + @CheckReturnValue + @Contract(pure = true) + default @NotNull ClickCallback filter(final @NotNull Predicate filter, final @Nullable Consumer otherwise) { + return audience -> { + if (filter.test(audience)) { + this.accept(audience); + } else if (otherwise != null) { + otherwise.accept(audience); + } + }; + } + + /** + * Require that audiences receiving this callback have a certain permission. + * + *

For audiences without permissions information, this test will always fail.

+ * + *

Actions from audiences that do not match this predicate will be silently ignored.

+ * + * @param permission the permission to check + * @return a modified callback + * @since 4.13.0 + */ + @CheckReturnValue + @Contract(pure = true) + default @NotNull ClickCallback requiringPermission(final @NotNull String permission) { + return this.requiringPermission(permission, null); + } + + /** + * Require that audiences receiving this callback have a certain permission. + * + *

For audiences without permissions information, this test will always fail.

+ * + * @param permission the permission to check + * @param otherwise the action to perform on the audience if the conditions are not met + * @return a modified callback + * @since 4.13.0 + */ + @CheckReturnValue + @Contract(pure = true) + default @NotNull ClickCallback requiringPermission(final @NotNull String permission, final @Nullable Consumer otherwise) { + return this.filter(audience -> audience.getOrDefault(PermissionChecker.POINTER, ClickCallbackInternals.ALWAYS_FALSE).test(permission), otherwise); + } + + /** + * Options to configure how a callback can be executed. + * + * @since 4.13.0 + */ + @ApiStatus.NonExtendable + interface Options extends Examinable { + /** + * Create a new builder. + * + * @return the new builder + * @since 4.13.0 + */ + static @NotNull Builder builder() { + return new ClickCallbackOptionsImpl.BuilderImpl(); + } + + /** + * Create a new builder populating from existing options. + * + * @param existing the existing options to populate this builder with + * @return the new builder + * @since 4.13.0 + */ + static @NotNull Builder builder(final @NotNull Options existing) { + return new ClickCallbackOptionsImpl.BuilderImpl(existing); + } + + /** + * The number of times this callback can be executed. + * + *

By default callbacks are single-use.

+ * + * @return allowable use count, or {@link #UNLIMITED_USES} + * @since 4.13.0 + */ + int uses(); + + /** + * How long this callback will last until it is made invalid. + * + *

By default callbacks last the value of {@link #DEFAULT_LIFETIME}.

+ * + * @return the duration of this callback + * @since 4.13.0 + */ + @NotNull Duration lifetime(); + + /** + * A builder for callback options. + * + * @since 4.13.0 + */ + @ApiStatus.NonExtendable + interface Builder extends AbstractBuilder { + /** + * Set the number of uses allowed for this callback. + * + * @param useCount the number of allowed uses, or {@link ClickCallback#UNLIMITED_USES} + * @return this builder + * @since 4.13.0 + */ + @NotNull Builder uses(int useCount); + + /** + * Set how long the callback should last from sending. + * + * @param duration the duration of this callback, from the time it is sent + * @return this builder + * @since 4.13.0 + */ + @NotNull Builder lifetime(final @NotNull TemporalAmount duration); + } + } + + /** + * A provider for actually producing click callbacks. + * + * @since 4.13.0 + */ + @PlatformAPI + @ApiStatus.Internal + interface Provider { + /** + * Create a real click event based on the provided parameters. + * + * @param callback the callback to execute + * @param options the options to apply to this callback + * @return a created click event that will execute the provided callback with options + * @since 4.13.0 + */ + @NotNull ClickEvent create(final @NotNull ClickCallback callback, final @NotNull Options options); + } +} diff --git a/api/src/main/java/net/kyori/adventure/text/event/ClickCallbackInternals.java b/api/src/main/java/net/kyori/adventure/text/event/ClickCallbackInternals.java new file mode 100644 index 000000000..4a7fb4014 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/event/ClickCallbackInternals.java @@ -0,0 +1,47 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2023 KyoriPowered + * + * 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 net.kyori.adventure.text.event; + +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.permission.PermissionChecker; +import net.kyori.adventure.util.Services; +import net.kyori.adventure.util.TriState; +import org.jetbrains.annotations.NotNull; + +final class ClickCallbackInternals { + private ClickCallbackInternals() { + } + + static final PermissionChecker ALWAYS_FALSE = PermissionChecker.always(TriState.FALSE); + + static final ClickCallback.Provider PROVIDER = Services.service(ClickCallback.Provider.class) + .orElseGet(Fallback::new); + + static final class Fallback implements ClickCallback.Provider { + @Override + public @NotNull ClickEvent create(final @NotNull ClickCallback callback, final ClickCallback.@NotNull Options options) { + return ClickEvent.suggestCommand("Callbacks are not supported on this platform!"); + } + } +} diff --git a/api/src/main/java/net/kyori/adventure/text/event/ClickCallbackOptionsImpl.java b/api/src/main/java/net/kyori/adventure/text/event/ClickCallbackOptionsImpl.java new file mode 100644 index 000000000..55bd330fa --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/event/ClickCallbackOptionsImpl.java @@ -0,0 +1,102 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2023 KyoriPowered + * + * 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 net.kyori.adventure.text.event; + +import java.time.Duration; +import java.time.temporal.TemporalAmount; +import java.util.stream.Stream; +import net.kyori.adventure.internal.Internals; +import net.kyori.examination.ExaminableProperty; +import org.jetbrains.annotations.NotNull; + +import static java.util.Objects.requireNonNull; + +final class ClickCallbackOptionsImpl implements ClickCallback.Options { + static final ClickCallback.Options DEFAULT = new ClickCallbackOptionsImpl.BuilderImpl().build(); + + private final int uses; + private final Duration lifetime; + + ClickCallbackOptionsImpl(final int uses, final Duration lifetime) { + this.uses = uses; + this.lifetime = lifetime; + } + + @Override + public int uses() { + return this.uses; + } + + @Override + public @NotNull Duration lifetime() { + return this.lifetime; + } + + @Override + public @NotNull Stream examinableProperties() { + return Stream.of( + ExaminableProperty.of("uses", this.uses), + ExaminableProperty.of("expiration", this.lifetime) + ); + } + + @Override + public String toString() { + return Internals.toString(this); + } + + static final class BuilderImpl implements Builder { + private static final int DEFAULT_USES = 1; + + private int uses; + private Duration lifetime; + + BuilderImpl() { + this.uses = DEFAULT_USES; + this.lifetime = ClickCallback.DEFAULT_LIFETIME; + } + + BuilderImpl(final ClickCallback.@NotNull Options existing) { + this.uses = existing.uses(); + this.lifetime = existing.lifetime(); + } + + @Override + public ClickCallback.@NotNull Options build() { + return new ClickCallbackOptionsImpl(this.uses, this.lifetime); + } + + @Override + public @NotNull Builder uses(final int uses) { + this.uses = uses; + return this; + } + + @Override + public @NotNull Builder lifetime(final @NotNull TemporalAmount lifetime) { + this.lifetime = lifetime instanceof Duration ? (Duration) lifetime : Duration.from(requireNonNull(lifetime, "lifetime")); + return this; + } + } +} diff --git a/api/src/main/java/net/kyori/adventure/text/event/ClickEvent.java b/api/src/main/java/net/kyori/adventure/text/event/ClickEvent.java index 83cd3da57..1a6bee3a2 100644 --- a/api/src/main/java/net/kyori/adventure/text/event/ClickEvent.java +++ b/api/src/main/java/net/kyori/adventure/text/event/ClickEvent.java @@ -25,7 +25,10 @@ import java.net.URL; import java.util.Objects; +import java.util.function.Consumer; import java.util.stream.Stream; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.builder.AbstractBuilder; import net.kyori.adventure.internal.Internals; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.StyleBuilderApplicable; @@ -136,6 +139,46 @@ public final class ClickEvent implements Examinable, StyleBuilderApplicable { return new ClickEvent(Action.COPY_TO_CLIPBOARD, text); } + /** + * Create a click event that, when clicked, will schedule a callback function to be executed on the server. + * + *

By default, this will be a single-use function that expires after the value of {@link ClickCallback#DEFAULT_LIFETIME}.

+ * + * @param function the function to execute + * @return a callback click event + * @since 4.13.0 + */ + public static @NotNull ClickEvent callback(final @NotNull ClickCallback function) { + return ClickCallbackInternals.PROVIDER.create(requireNonNull(function, "function"), ClickCallbackOptionsImpl.DEFAULT); + } + + /** + * Create a click event that, when clicked, will schedule a callback function to be executed on the server. + * + * @param function the function to execute + * @param options options to control how the callback will be stored on the server. + * @return a callback click event + * @since 4.13.0 + */ + public static @NotNull ClickEvent callback(final @NotNull ClickCallback function, final ClickCallback.@NotNull Options options) { + return ClickCallbackInternals.PROVIDER.create(requireNonNull(function, "function"), requireNonNull(options, "options")); + } + + /** + * Create a click event that, when clicked, will schedule a callback function to be executed on the server. + * + * @param function the function to execute + * @param optionsBuilder function that will be called to configure the click callback options + * @return a callback click event + * @since 4.13.0 + */ + public static @NotNull ClickEvent callback(final @NotNull ClickCallback function, final @NotNull Consumer optionsBuilder) { + return ClickCallbackInternals.PROVIDER.create( + requireNonNull(function, "function"), + AbstractBuilder.configureAndBuild(ClickCallback.Options.builder(), requireNonNull(optionsBuilder, "optionsBuilder")) + ); + } + /** * Creates a click event. *