diff --git a/README.md b/README.md index 9c20731..fee7564 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,5 @@ Command pre- & post-processors for [Cloud v2](https://github.com/incendo/cloud). ## postprocessors -- [cloud-processors-confirmation](./cloud-processors-confirmation) \ No newline at end of file +- [cloud-processors-confirmation](./cloud-processors-confirmation) +- [cloud-processors-cooldown](./cloud-processors-cooldown) diff --git a/cloud-processors-common/src/main/java/org/incendo/cloud/processors/immutables/StagedImmutableBuilder.java b/cloud-processors-common/src/main/java/org/incendo/cloud/processors/immutables/StagedImmutableBuilder.java index 1a2d561..14c947d 100644 --- a/cloud-processors-common/src/main/java/org/incendo/cloud/processors/immutables/StagedImmutableBuilder.java +++ b/cloud-processors-common/src/main/java/org/incendo/cloud/processors/immutables/StagedImmutableBuilder.java @@ -46,7 +46,9 @@ defaultAsDefault = true, put = "*", putAll = "*", - stagedBuilder = true + stagedBuilder = true, + depluralize = true, + depluralizeDictionary = "creationListeners:creationListener" ) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.PACKAGE}) @Retention(RetentionPolicy.SOURCE) diff --git a/cloud-processors-cooldown/README.md b/cloud-processors-cooldown/README.md new file mode 100644 index 0000000..d010a8a --- /dev/null +++ b/cloud-processors-cooldown/README.md @@ -0,0 +1,90 @@ +# cloud-processors-cooldown + +Postprocessor that adds command cooldowns. + +## Installation + +cloud-processors-cooldown is not yet available on Maven Central. + +## Usage + +The cooldown system is managed by a `CooldownManager`, so the first step is to create an instance of that: +```java +CooldownManager cooldownManager = CooldownManager.of(configuration); +``` +The configuration is an instance of `CooldownConfiguration`. Refer to the JavaDocs for information about specific options, +but an example would be: +```java +CooldownConfiguration configuration = CooldownConfiguration.builder() + .repository(CooldownRepository.forMap(new HashMap<>())) + .addActiveCooldownListener(...) + .addCooldownCreationListener(...) + .cooldownNotifier(notifier) + .build(); +``` +The listeners are invoked when different events take place. The active cooldown listener in particular may be used to +inform the command sender that their command execution got blocked due to an active cooldown. + +The repository stores active cooldowns for a command sender in the form of cooldown profiles. +The cooldowns are grouped by their `CooldownGroup`, by default a unique group will be created per command. +You may create a named group by using `CooldownGroup.named(name)`. Commands that use the same cooldown group +will have their cooldowns shared by the command sender. + +You may create a repository from a map, `CloudCache` or even implement your own. If you want to persist the cooldowns +across multiple temporary sessions then you may use a mapping repository to store the cooldown profiles for a persistent key, +rather than the potentially temporary command sender objects: +```java +CooldownRepository repository = CooldownRepository.mapping( + sender -> sender.uuid(), + CooldownRepository.forMap(new HashMap()) +); +``` + +You may also customize how the cooldown profiles are created by passing a `CooldownProfileFactory` to the `CooldownConfiguration`. + +If you want to have the cooldowns automatically removed from the repository to prevent unused profiles from taking up memory you +may register a `ScheduledCleanupCreationListener`: +```java +CooldownRepository repository = CooldownRepository.forMap(new HashMap<>()); +ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); +CooldownConfiguration configuration = CooldownConfiguration.builder() + // ... + .repository(repository) + .addCreationListener(new ScheduledCleanupCreationListener(executorService, repository)) + .build(); +``` + +You then need to register the postprocessor: +```java +commandManager.registerCommandPostProcessor(cooldownManager.createPostprocessor()); +``` + +### Builders + +The cooldowns are configured using a `Cooldown` instance: +```java +Cooldown cooldown = Cooldown.of(DurationFunction.constant(Duration.ofMinutes(5L))); +Cooldown cooldown = Cooldown.of(DurationFunction.constant(Duration.ofMinutes(5L)), CooldownGroup.named("group-name")); +``` +which can then be applied to the command by either manually setting the meta value: +```java +commandBuilder.meta(CooldownManager.META_COOLDOWN_DURATION, cooldown); +``` +or by applying the cooldown to the builder: +```java +commandBuilder.apply(cooldown); +``` + +### Annotations + +Annotated commands may use the `@Cooldown` annotation: +```java +@Cooldown(duration = 5, timeUnit = ChronoUnit.MINUTES, group = "some-group") +public void yourCommand() { + // ... +} +``` +You need to install the builder modifier for this to work: +```java +CooldownBuilderModifier.install(annotationParser); +``` diff --git a/cloud-processors-cooldown/build.gradle.kts b/cloud-processors-cooldown/build.gradle.kts new file mode 100644 index 0000000..9dfe110 --- /dev/null +++ b/cloud-processors-cooldown/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("cloud-processors.base-conventions") + id("cloud-processors.publishing-conventions") +} + +dependencies { + api(projects.cloudProcessorsCommon) + + compileOnly(libs.cloud.annotations) +} + +// TODO(City): Disable this +// we're getting errors on generated files due to -Werror :( +tasks.withType { + options.compilerArgs.remove("-Werror") +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/Cooldown.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/Cooldown.java new file mode 100644 index 0000000..6f38385 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/Cooldown.java @@ -0,0 +1,102 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown; + +import cloud.commandframework.Command; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.immutables.value.Value; +import org.incendo.cloud.processors.immutables.StagedImmutableBuilder; + +/** + * An instance of a cooldown for a {@link CooldownGroup} belonging to a command sender. + * + * @param command sender type + * @since 1.0.0 + */ +@StagedImmutableBuilder +@Value.Immutable +@API(status = API.Status.STABLE, since = "1.0.0") +public interface Cooldown extends Command.Builder.Applicable { + + /** + * Returns a new cooldown with the given {@code duration} + * using {@link CooldownConfiguration#fallbackGroup()} as the group. + * + * @param command sender type + * @param duration the duration + * @return the cooldown + */ + static @NonNull Cooldown of(final @NonNull DurationFunction duration) { + return Cooldown.builder().duration(duration).build(); + } + + /** + * Returns a new cooldown with the given {@code duration} and {@code group}. + * + * @param command sender type + * @param duration the duration + * @param group cooldown group + * @return the cooldown + */ + static @NonNull Cooldown of( + final @NonNull DurationFunction duration, + final @NonNull CooldownGroup group + ) { + return Cooldown.builder().duration(duration).group(group).build(); + } + + /** + * Returns a new cooldown builder. + * + * @param command sender type + * @return the builder + */ + static ImmutableCooldown.@NonNull DurationBuildStage builder() { + return ImmutableCooldown.builder(); + } + + /** + * Returns the cooldown duration. + * + * @return the duration + */ + @NonNull DurationFunction duration(); + + /** + * Returns the group that this instance belongs to. + * If set to {@code null} then {@link CooldownConfiguration#fallbackGroup()} will be used. + * + * @return the cooldown group + */ + default @Nullable CooldownGroup group() { + return null; + } + + @Override + default Command.@NonNull Builder applyToCommandBuilder(Command.@NonNull Builder builder) { + return builder.meta(CooldownManager.META_COOLDOWN_DURATION, this); + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownConfiguration.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownConfiguration.java new file mode 100644 index 0000000..99fe4e3 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownConfiguration.java @@ -0,0 +1,126 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown; + +import cloud.commandframework.Command; +import cloud.commandframework.context.CommandContext; +import java.time.Clock; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; +import org.incendo.cloud.processors.cooldown.listener.CooldownActiveListener; +import org.incendo.cloud.processors.cooldown.listener.CooldownCreationListener; +import org.incendo.cloud.processors.cooldown.profile.CooldownProfileFactory; +import org.incendo.cloud.processors.cooldown.profile.StandardCooldownProfileFactory; +import org.incendo.cloud.processors.immutables.StagedImmutableBuilder; + +/** + * Configuration for a {@link CooldownManager}. + * + * @param command sender type + * @since 1.0.0 + */ +@Value.Immutable +@StagedImmutableBuilder +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownConfiguration { + + /** + * Returns a new configuration builder. + * + * @param command sender type + * @return the builder + */ + static ImmutableCooldownConfiguration.@NonNull RepositoryBuildStage builder() { + return ImmutableCooldownConfiguration.builder(); + } + + /** + * Returns the repository used to store cooldowns. + * + * @return the repository + */ + @NonNull CooldownRepository repository(); + + /** + * Returns the active cooldown listeners. + * + *

The active cooldown listeners are invoked when a cooldown is preventing a sender from executing a command.

+ * + * @return active cooldown listeners + */ + @NonNull List> activeCooldownListeners(); + + /** + * Returns the creation listeners. + * + *

The creation listeners are invoked when a new cooldown is created.

+ * + * @return creation listeners + */ + @NonNull List> creationListeners(); + + /** + * Returns a predicate that determines whether the {@link CommandContext} + * should bypass the cooldown requirement. + * + *

The default implementation always returns {@code false}

+ * + * @return predicate that determines whether cooldowns should be skipped + */ + default @NonNull Predicate<@NonNull CommandContext> bypassCooldown() { + return context -> false; + } + + /** + * Returns the clock used to calculate the current time. + * + * @return the clock + */ + default @NonNull Clock clock() { + return Clock.systemUTC(); + } + + /** + * Returns the factory that produces cooldown profiles. + * + * @return cooldown profile factory + */ + default @NonNull CooldownProfileFactory profileFactory() { + return new StandardCooldownProfileFactory(this); + } + + /** + * Returns the function that determines the fallback {@link CooldownGroup} in case no group has been provided + * to the {@link Cooldown}. + * + * @return the fallback group function + */ + default @NonNull Function<@NonNull Command, @NonNull CooldownGroup> fallbackGroup() { + return CooldownGroup::command; + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownGroup.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownGroup.java new file mode 100644 index 0000000..b62848d --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownGroup.java @@ -0,0 +1,114 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown; + +import cloud.commandframework.Command; +import java.util.Objects; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Groups cooldown together. + * + *

If two commands belong to the same group, the cooldown will be shared between those commands.

+ * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownGroup { + + /** + * Creates a new cooldown group that is unique to the given {@code command}. + * + *

This means that the cooldown will not be shared with any other commands.

+ * + * @param command the command + * @return the group + */ + static CooldownGroup command(final @NonNull Command command) { + return new CommandCooldownGroup(command); + } + + /** + * Creates a new cooldown group identified by the given {@code name}. + * + * @param name group name + * @return the group + */ + static CooldownGroup named(final @NonNull String name) { + return new NamedCooldownGroup(name); + } + + final class CommandCooldownGroup implements CooldownGroup { + + private final Command command; + + private CommandCooldownGroup(final @NonNull Command command) { + this.command = command; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + final CommandCooldownGroup that = (CommandCooldownGroup) o; + return Objects.equals(this.command, that.command); + } + + @Override + public int hashCode() { + return Objects.hash(this.command); + } + } + + final class NamedCooldownGroup implements CooldownGroup { + + private final String name; + + private NamedCooldownGroup(final @NonNull String name) { + this.name = name; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || this.getClass() != o.getClass()) { + return false; + } + final NamedCooldownGroup that = (NamedCooldownGroup) o; + return Objects.equals(this.name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(this.name); + } + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownInstance.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownInstance.java new file mode 100644 index 0000000..c924e54 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownInstance.java @@ -0,0 +1,80 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown; + +import java.time.Duration; +import java.time.Instant; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.immutables.value.Value; +import org.incendo.cloud.processors.cooldown.profile.CooldownProfile; +import org.incendo.cloud.processors.immutables.StagedImmutableBuilder; + +/** + * An instance of a {@link Cooldown}. + * + * @since 1.0.0 + */ +@StagedImmutableBuilder +@Value.Immutable +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownInstance { + + /** + * Returns a new cooldown instance builder. + * + * @return the builder + */ + static ImmutableCooldownInstance.@NonNull ProfileBuildStage builder() { + return ImmutableCooldownInstance.builder(); + } + + /** + * Returns the profile that owns the cooldown. + * + * @return the owning profile + */ + @NonNull CooldownProfile profile(); + + /** + * Returns the cooldown group. + * + * @return the group + */ + @NonNull CooldownGroup group(); + + /** + * Returns the cooldown duration. + * + * @return the duration + */ + @NonNull Duration duration(); + + /** + * Returns the creation time. + * + * @return the creation time + */ + @NonNull Instant creationTime(); +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownManager.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownManager.java new file mode 100644 index 0000000..3a352c4 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownManager.java @@ -0,0 +1,97 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown; + +import cloud.commandframework.execution.postprocessor.CommandPostprocessor; +import cloud.commandframework.keys.CloudKey; +import io.leangen.geantyref.TypeToken; +import java.util.Objects; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Manager for the cooldown system. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public final class CooldownManager { + + /** + * Meta that adds the given cooldown to the command. + */ + public static final CloudKey> META_COOLDOWN_DURATION = CloudKey.of( + "cloud:cooldown_duration", + new TypeToken>() { + } + ); + + /** + * Creates a new confirmation manager using the given {@code configuration}. + * + * @param command sender type + * @param configuration configuration for the confirmation manager + * @return the created manager + */ + public static @NonNull CooldownManager of(final @NonNull CooldownConfiguration configuration) { + return new CooldownManager<>(Objects.requireNonNull(configuration, "configuration")); + } + + private final CooldownRepository repository; + private final CooldownConfiguration configuration; + + private CooldownManager(final @NonNull CooldownConfiguration configuration) { + this.repository = configuration.repository(); + this.configuration = configuration; + } + + /** + * Returns the configuration for this manager. + * + * @return the configuration + */ + public @NonNull CooldownConfiguration configuration() { + return this.configuration; + } + + /** + * Returns the cooldown repository. + * + * @return the repository + */ + public @NonNull CooldownRepository repository() { + return this.repository; + } + + /** + * Returns a {@link CommandPostprocessor} that will prevent commands from executing if they are marked + * as needing confirmation. + * + * @return confirmation postprocessor + */ + public @NonNull CommandPostprocessor createPostprocessor() { + return new CooldownPostprocessor<>(this); + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownPostprocessor.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownPostprocessor.java new file mode 100644 index 0000000..02af38f --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownPostprocessor.java @@ -0,0 +1,102 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown; + +import cloud.commandframework.execution.postprocessor.CommandPostprocessingContext; +import cloud.commandframework.execution.postprocessor.CommandPostprocessor; +import cloud.commandframework.services.types.ConsumerService; +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.processors.cooldown.profile.CooldownProfile; + +/** + * {@link CommandPostprocessor} for {@link CooldownManager}. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.INTERNAL, since = "1.0.0") +final class CooldownPostprocessor implements CommandPostprocessor { + + private final CooldownManager cooldownManager; + + CooldownPostprocessor(final @NonNull CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + @SuppressWarnings("unchecked") + public void accept(final @NonNull CommandPostprocessingContext context) { + final Cooldown cooldown = context.command().commandMeta().getOrDefault(CooldownManager.META_COOLDOWN_DURATION, null); + if (cooldown == null) { + return; + } + + if (this.cooldownManager.configuration().bypassCooldown().test(context.commandContext())) { + return; + } + + final CooldownProfile profile = this.cooldownManager.repository().getProfile( + context.commandContext().sender(), + this.cooldownManager.configuration().profileFactory() + ); + final CooldownGroup group; + if (cooldown.group() != null) { + group = Objects.requireNonNull(cooldown.group(), "group"); + } else { + group = this.cooldownManager.configuration().fallbackGroup().apply(context.command()); + } + + final CooldownInstance cooldownInstance = profile.getCooldown(group); + if (cooldownInstance != null) { + final Instant endTime = cooldownInstance.creationTime().plus(cooldownInstance.duration()); + final Duration remainingTime = Duration.between(Instant.now(this.cooldownManager.configuration().clock()), endTime); + this.cooldownManager.configuration().activeCooldownListeners().forEach(listener -> listener.cooldownActive( + context.commandContext().sender(), + context.command(), + cooldownInstance, + remainingTime + )); + ConsumerService.interrupt(); + return; + } + + final CooldownInstance instance = CooldownInstance.builder() + .profile(profile) + .group(group) + .duration(((DurationFunction) cooldown.duration()).getDuration(context.commandContext())) + .creationTime(Instant.now(this.cooldownManager.configuration().clock())) + .build(); + profile.setCooldown(group, instance); + + this.cooldownManager.configuration().creationListeners().forEach(listener -> listener.cooldownCreated( + context.commandContext().sender(), + context.command(), + instance + )); + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownRepository.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownRepository.java new file mode 100644 index 0000000..2df7de6 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/CooldownRepository.java @@ -0,0 +1,241 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.processors.cache.CloudCache; +import org.incendo.cloud.processors.cooldown.profile.CooldownProfile; +import org.incendo.cloud.processors.cooldown.profile.CooldownProfileFactory; + +/** + * Repository that stores {@link CooldownProfile cooldown profiles} identifies by keys of type {@link K}. + * + *

If the command sender objects are temporary (such as game entities, etc) the keys map be mapped to a persistent + * identifier by using a {@link #mapping(Function, CooldownRepository)} repository.

+ * + *

The {@link CooldownProfile} creation may be customized by passing a custom {@link CooldownProfileFactory} + * to {@link CooldownConfiguration#profileFactory()}.

+ * + * @param type of the keys that identifies the profiles + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownRepository { + + /** + * Returns a new repository backed by the given {@code otherRepository} that maps requests for keys + * of type {@link C} to type {@link K} using the given {@code mappingFunction}. + * + * @param command sender type + * @param repository key type + * @param mappingFunction function that maps between the types + * @param otherRepository backing repository + * @return the repository + */ + static @NonNull CooldownRepository mapping( + final @NonNull Function mappingFunction, + final @NonNull CooldownRepository otherRepository + ) { + return new MappingCooldownRepository<>( + Objects.requireNonNull(mappingFunction, "mappingFunction"), + Objects.requireNonNull(otherRepository, "otherRepository") + ); + } + + /** + * Returns a new repository backed by the given {@code cache}. + * + * @param key type + * @param cache backing cache + * @return the repository + */ + static @NonNull CooldownRepository forCache(final @NonNull CloudCache cache) { + return new CacheCooldownRepository<>(Objects.requireNonNull(cache, "cache")); + } + + /** + * Returns a new repository backed by the given {@code map}. + * + * @param key type + * @param map backing map + * @return the repository + */ + static @NonNull CooldownRepository forMap(final @NonNull Map map) { + return new MapCooldownRepository<>(Objects.requireNonNull(map, "map")); + } + + /** + * Returns the profile for the given {@code key}. + * + *

If no profile exists in the repository, a new profile will be persisted and returned.

+ * + * @param key key that identifies the profile + * @param profileFactory factory that creates profiles + * @return the profile + */ + @NonNull CooldownProfile getProfile(@NonNull K key, @NonNull CooldownProfileFactory profileFactory); + + /** + * Returns the profile for the given {@code key}, if it exists. + * + * @param key key that identifies the profile + * @return the profile, or {@code null} + */ + @Nullable CooldownProfile getProfileIfExists(@NonNull K key); + + /** + * Deletes the profile identified by the given {@code key}. + * + * @param key the key + */ + void deleteProfile(@NonNull K key); + + /** + * Deletes the cooldown identified by the given {@code key} belonging to the given {@code group}. + * + *

If the profile is empty after the deletion, then the profile is deleted too.

+ * + * @param key key identifying the profile + * @param group group to delete + */ + void deleteCooldown(@NonNull K key, @NonNull CooldownGroup group); + + + final class MappingCooldownRepository implements CooldownRepository { + + private final Function mappingFunction; + private final CooldownRepository otherRepository; + + + private MappingCooldownRepository( + final @NonNull Function mappingFunction, + final @NonNull CooldownRepository otherRepository + ) { + this.mappingFunction = mappingFunction; + this.otherRepository = otherRepository; + } + + @Override + public @NonNull CooldownProfile getProfile( + final @NonNull C key, + final @NonNull CooldownProfileFactory profileFactory + ) { + return this.otherRepository.getProfile(this.mappingFunction.apply(key), profileFactory); + } + + @Override + public @Nullable CooldownProfile getProfileIfExists(final @NonNull C key) { + return this.otherRepository.getProfileIfExists(this.mappingFunction.apply(key)); + } + + @Override + public void deleteProfile(final @NonNull C key) { + this.otherRepository.deleteProfile(this.mappingFunction.apply(key)); + } + + @Override + public void deleteCooldown(final @NonNull C key, final @NonNull CooldownGroup group) { + this.otherRepository.deleteCooldown(this.mappingFunction.apply(key), group); + } + } + + abstract class AbstractCooldownRepository implements CooldownRepository { + + @Override + public synchronized void deleteCooldown(final @NonNull K key, final @NonNull CooldownGroup group) { + final CooldownProfile profile = this.getProfileIfExists(key); + if (profile == null) { + return; + } + profile.deleteCooldown(group); + if (profile.isEmpty()) { + this.deleteProfile(key); + } + } + } + + final class MapCooldownRepository extends AbstractCooldownRepository { + + private final Map map; + + private MapCooldownRepository(final @NonNull Map map) { + this.map = map; + } + + @Override + public synchronized @NonNull CooldownProfile getProfile( + final @NonNull K key, + final @NonNull CooldownProfileFactory profileFactory + ) { + return this.map.computeIfAbsent(key, k -> profileFactory.create()); + } + + @Override + public synchronized @Nullable CooldownProfile getProfileIfExists(final @NonNull K key) { + return this.map.get(key); + } + + @Override + public synchronized void deleteProfile(final @NonNull K key) { + this.map.remove(key); + } + } + + final class CacheCooldownRepository extends AbstractCooldownRepository { + + private final CloudCache cache; + + private CacheCooldownRepository(final @NonNull CloudCache cache) { + this.cache = cache; + } + + @Override + public synchronized @NonNull CooldownProfile getProfile( + final @NonNull K key, + final @NonNull CooldownProfileFactory profileFactory + ) { + CooldownProfile profile = this.cache.getIfPresent(key); + if (profile == null) { + profile = profileFactory.create(); + this.cache.put(key, profile); + } + return profile; + } + + @Override + public synchronized @Nullable CooldownProfile getProfileIfExists(final @NonNull K key) { + return this.cache.getIfPresent(key); + } + + @Override + public synchronized void deleteProfile(final @NonNull K key) { + this.cache.delete(key); + } + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/DurationFunction.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/DurationFunction.java new file mode 100644 index 0000000..6c3eac5 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/DurationFunction.java @@ -0,0 +1,59 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown; + +import cloud.commandframework.context.CommandContext; +import java.time.Duration; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * A function that generates a duration from a command context. + * + * @param command sender type + * @since 1.0.0 + */ +@FunctionalInterface +@API(status = API.Status.STABLE, since = "1.0.0") +public interface DurationFunction { + + /** + * Creates a new duration function that always returns {@code duration}. + * + * @param command sender type + * @param duration constant duration + * @return the duration function + */ + static @NonNull DurationFunction constant(final @NonNull Duration duration) { + return context -> duration; + } + + /** + * Returns the duration for the given {@code context}. + * + * @param context the context to get the duration for + * @return the duration + */ + @NonNull Duration getDuration(@NonNull CommandContext context); +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/Cooldown.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/Cooldown.java new file mode 100644 index 0000000..2b33b92 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/Cooldown.java @@ -0,0 +1,67 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.temporal.ChronoUnit; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.processors.cooldown.CooldownConfiguration; + +/** + * Adds a cooldown to the command. + * + * @since 1.0.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@API(status = API.Status.STABLE, since = "1.0.0") +public @interface Cooldown { + + /** + * Returns the duration. + * + * @return the duration + */ + long duration(); + + /** + * Returns the time unit of the {@link #duration()}}. + * + * @return the time unit + */ + @NonNull ChronoUnit timeUnit(); + + /** + * Returns the cooldown group. + * + *

If this is empty then {@link CooldownConfiguration#fallbackGroup()} will be used.

+ * + * @return the cooldown group + */ + String group() default ""; +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/CoooldownBuilderModifier.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/CoooldownBuilderModifier.java new file mode 100644 index 0000000..0266de4 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/CoooldownBuilderModifier.java @@ -0,0 +1,82 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.annotations; + +import cloud.commandframework.Command; +import cloud.commandframework.annotations.AnnotationParser; +import cloud.commandframework.annotations.BuilderModifier; +import java.time.Duration; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.processors.cooldown.CooldownGroup; +import org.incendo.cloud.processors.cooldown.DurationFunction; + +/** + * Builder modifier that will add the cooldown metadata to the command builder. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public final class CoooldownBuilderModifier implements BuilderModifier { + + /** + * Creates a new builder modifier that will add the cooldown metadata to the command builder. + * + * @param command sender type + * @return the builder modifier + */ + public static @NonNull BuilderModifier of() { + return new CoooldownBuilderModifier<>(); + } + + /** + * Adds the cooldown builder modifier to the given {@code annotationParser}. + * + * @param command sender type + * @param annotationParser annotation parser to add the builder modifier to + */ + public static void install(final @NonNull AnnotationParser annotationParser) { + annotationParser.registerBuilderModifier(Cooldown.class, of()); + } + + private CoooldownBuilderModifier() { + } + + @Override + public Command.@NonNull Builder modifyBuilder( + final @NonNull Cooldown annotation, + final Command.@NonNull Builder builder + ) { + final DurationFunction duration = DurationFunction.constant(Duration.of(annotation.duration(), annotation.timeUnit())); + final org.incendo.cloud.processors.cooldown.Cooldown cooldown; + if (annotation.group().isBlank()) { + cooldown = org.incendo.cloud.processors.cooldown.Cooldown.builder().duration(duration).build(); + } else { + cooldown = org.incendo.cloud.processors.cooldown.Cooldown.builder().duration(duration) + .group(CooldownGroup.named(annotation.group())).build(); + } + return builder.apply(cooldown); + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/package-info.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/package-info.java new file mode 100644 index 0000000..4bf2740 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/annotations/package-info.java @@ -0,0 +1,4 @@ +/** + * Utilities for using the cooldown system together with cloud-annotations. + */ +package org.incendo.cloud.processors.cooldown.annotations; diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/CooldownActiveListener.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/CooldownActiveListener.java new file mode 100644 index 0000000..0a761e6 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/CooldownActiveListener.java @@ -0,0 +1,55 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.listener; + +import cloud.commandframework.Command; +import java.time.Duration; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.processors.cooldown.CooldownInstance; + +/** + * Notifies command senders that they have an active cooldown. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownActiveListener { + + /** + * Invoked when a cooldown is preventing the {@code sender} from executing a command. + * + * @param sender command sender to notify + * @param command command that could not be executed + * @param cooldown cooldown instance + * @param remainingTime remaining time + */ + void cooldownActive( + @NonNull C sender, + @NonNull Command command, + @NonNull CooldownInstance cooldown, + @NonNull Duration remainingTime + ); +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/CooldownCreationListener.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/CooldownCreationListener.java new file mode 100644 index 0000000..92a7182 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/CooldownCreationListener.java @@ -0,0 +1,48 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.listener; + +import cloud.commandframework.Command; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.processors.cooldown.CooldownInstance; + +/** + * Listener that gets invoked when a new cooldown is created. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownCreationListener { + + /** + * Invoked when a new cooldown is created. + * + * @param sender sender that the cooldown is created for + * @param command command that triggered the cooldown creation + * @param instance created instance + */ + void cooldownCreated(@NonNull C sender, @NonNull Command command, @NonNull CooldownInstance instance); +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/ScheduledCleanupCreationListener.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/ScheduledCleanupCreationListener.java new file mode 100644 index 0000000..c1b6f36 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/ScheduledCleanupCreationListener.java @@ -0,0 +1,89 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.listener; + +import cloud.commandframework.Command; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.processors.cooldown.CooldownGroup; +import org.incendo.cloud.processors.cooldown.CooldownInstance; +import org.incendo.cloud.processors.cooldown.CooldownRepository; + +/** + * Cooldown creation listener that schedules a task to clean up the cooldown after it expires. + * + * @param command sender type + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public final class ScheduledCleanupCreationListener implements CooldownCreationListener { + + private final ScheduledExecutorService executorService; + private final CooldownRepository cooldownRepository; + + /** + * Creates a new scheduled cleanup listener. + * + * @param executorService service to schedule the cleanup task with + * @param cooldownRepository repository that the cooldown will be deleted from + */ + public ScheduledCleanupCreationListener( + final @NonNull ScheduledExecutorService executorService, + final @NonNull CooldownRepository cooldownRepository + ) { + this.executorService = executorService; + this.cooldownRepository = cooldownRepository; + } + + @Override + public void cooldownCreated( + final @NonNull C sender, + final @NonNull Command command, + final @NonNull CooldownInstance instance + ) { + this.executorService.schedule( + new CooldownDeletionTask(sender, instance.group()), + instance.duration().toMillis(), + TimeUnit.MILLISECONDS + ); + } + + private final class CooldownDeletionTask implements Runnable { + + private final C sender; + private final CooldownGroup group; + + private CooldownDeletionTask(final @NonNull C sender, final @NonNull CooldownGroup group) { + this.sender = sender; + this.group = group; + } + + @Override + public void run() { + ScheduledCleanupCreationListener.this.cooldownRepository.deleteCooldown(this.sender, this.group); + } + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/package-info.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/package-info.java new file mode 100644 index 0000000..7411829 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/listener/package-info.java @@ -0,0 +1,4 @@ +/** + * Listeners for cooldown events. + */ +package org.incendo.cloud.processors.cooldown.listener; diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/package-info.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/package-info.java new file mode 100644 index 0000000..461e6ed --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/package-info.java @@ -0,0 +1,4 @@ +/** + * {@link cloud.commandframework.execution.postprocessor.CommandPostprocessor} that adds command cooldowns. + */ +package org.incendo.cloud.processors.cooldown; diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfile.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfile.java new file mode 100644 index 0000000..acaca44 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfile.java @@ -0,0 +1,69 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.profile; + +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.processors.cooldown.CooldownGroup; +import org.incendo.cloud.processors.cooldown.CooldownInstance; + +/** + * Profile that stores {@link CooldownInstance cooldowns} identified by {@link CooldownGroup groups}. + * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownProfile { + + /** + * Returns the cooldown for the given {@code group} if it exists and it's active. + * + * @param group group that identifies the cooldown + * @return the active cooldown, or {@code null} + */ + @Nullable CooldownInstance getCooldown(@NonNull CooldownGroup group); + + /** + * Sets the cooldown for the given {@code group}. + * + * @param group group that identifies the cooldown + * @param cooldown cooldown value + */ + void setCooldown(@NonNull CooldownGroup group, @NonNull CooldownInstance cooldown); + + /** + * Deletes the cooldown for the given {@code group}, if it exists. + * + * @param group the group + */ + void deleteCooldown(@NonNull CooldownGroup group); + + /** + * Returns whether the profile is empty. + * + * @return {@code true} if the profile is empty, else {@code false} + */ + boolean isEmpty(); +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileFactory.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileFactory.java new file mode 100644 index 0000000..a21d137 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileFactory.java @@ -0,0 +1,43 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.profile; + +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Factory that creates cooldown profiles. + * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public interface CooldownProfileFactory{ + + /** + * Creates a new cooldown profile. + * + * @return the created profile + */ + @NonNull CooldownProfile create(); +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileImpl.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileImpl.java new file mode 100644 index 0000000..6a51b53 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/CooldownProfileImpl.java @@ -0,0 +1,74 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.profile; + +import java.time.Clock; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.incendo.cloud.processors.cooldown.CooldownGroup; +import org.incendo.cloud.processors.cooldown.CooldownInstance; + +final class CooldownProfileImpl implements CooldownProfile { + + private final Map cooldowns; + private final Clock clock; + + CooldownProfileImpl(final @NonNull Clock clock) { + this.cooldowns = new HashMap<>(); + this.clock = clock; + } + + @Override + public synchronized @Nullable CooldownInstance getCooldown(final @NonNull CooldownGroup group) { + final CooldownInstance cooldown = this.cooldowns.get(group); + if (cooldown == null) { + return null; + } + final Instant expiration = cooldown.creationTime().plus(cooldown.duration()); + final Instant currentTime = Instant.now(this.clock); + if (expiration.isAfter(currentTime)) { + return cooldown; + } + this.cooldowns.remove(group); + return null; + } + + @Override + public synchronized void setCooldown(final @NonNull CooldownGroup group, final @NonNull CooldownInstance cooldown) { + this.cooldowns.put(group, cooldown); + } + + @Override + public synchronized void deleteCooldown(final @NonNull CooldownGroup group) { + this.cooldowns.remove(group); + } + + @Override + public boolean isEmpty() { + return this.cooldowns.isEmpty(); + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/StandardCooldownProfileFactory.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/StandardCooldownProfileFactory.java new file mode 100644 index 0000000..0521394 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/StandardCooldownProfileFactory.java @@ -0,0 +1,53 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.cooldown.profile; + +import org.apiguardian.api.API; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.incendo.cloud.processors.cooldown.CooldownConfiguration; + +/** + * Standard implementation of {@link CooldownProfileFactory}. + * + * @since 1.0.0 + */ +@API(status = API.Status.STABLE, since = "1.0.0") +public final class StandardCooldownProfileFactory implements CooldownProfileFactory { + + private final CooldownConfiguration configuration; + + /** + * Creates a new factory instance. + * + * @param configuration the cooldown configuration + */ + public StandardCooldownProfileFactory(final @NonNull CooldownConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public @NonNull CooldownProfile create() { + return new CooldownProfileImpl(this.configuration.clock()); + } +} diff --git a/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/package-info.java b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/package-info.java new file mode 100644 index 0000000..9f2dac9 --- /dev/null +++ b/cloud-processors-cooldown/src/main/java/org/incendo/cloud/processors/cooldown/profile/package-info.java @@ -0,0 +1,4 @@ +/** + * Cooldown profiles stores per-sender cooldowns. + */ +package org.incendo.cloud.processors.cooldown.profile; diff --git a/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/CooldownManagerTest.java b/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/CooldownManagerTest.java new file mode 100644 index 0000000..3fee60d --- /dev/null +++ b/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/CooldownManagerTest.java @@ -0,0 +1,223 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.confirmation; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.execution.CommandExecutionHandler; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; +import org.incendo.cloud.processors.confirmation.util.TestCommandManager; +import org.incendo.cloud.processors.confirmation.util.TestCommandSender; +import org.incendo.cloud.processors.cooldown.Cooldown; +import org.incendo.cloud.processors.cooldown.CooldownConfiguration; +import org.incendo.cloud.processors.cooldown.CooldownGroup; +import org.incendo.cloud.processors.cooldown.CooldownInstance; +import org.incendo.cloud.processors.cooldown.CooldownManager; +import org.incendo.cloud.processors.cooldown.CooldownRepository; +import org.incendo.cloud.processors.cooldown.DurationFunction; +import org.incendo.cloud.processors.cooldown.listener.CooldownActiveListener; +import org.incendo.cloud.processors.cooldown.listener.CooldownCreationListener; +import org.incendo.cloud.processors.cooldown.profile.CooldownProfile; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CooldownManagerTest { + + @Mock + private TestCommandSender commandSender; + @Mock + private CooldownActiveListener notifier; + @Mock(strictness = Mock.Strictness.LENIENT) + private CommandExecutionHandler commandExecutionHandler; + @Mock + private Clock clock; + @Mock + private CooldownCreationListener listener; + + private CommandManager commandManager; + private CooldownManager cooldownManager; + + @BeforeEach + void setup() { + this.commandManager = new TestCommandManager(); + this.cooldownManager = CooldownManager.of( + CooldownConfiguration.builder() + .repository(CooldownRepository.forMap(new HashMap<>())) + .addActiveCooldownListener(this.notifier) + .clock(this.clock) + .addCreationListener(this.listener) + .build() + ); + this.commandManager.registerCommandPostProcessor(this.cooldownManager.createPostprocessor()); + } + + @Test + void testAppliesCooldown() { + // Arrange + final Cooldown decorator = Cooldown.of(DurationFunction.constant(Duration.ofHours(1L))); + this.commandManager.command( + this.commandManager.commandBuilder("command") + .apply(decorator) + .handler(this.commandExecutionHandler) + ); + + when(this.clock.instant()).thenReturn(Instant.now()); + when(this.commandExecutionHandler.executeFuture(any())).thenReturn(CompletableFuture.completedFuture(null)); + + // Act + this.commandManager.commandExecutor().executeCommand(this.commandSender, "command").join(); + this.commandManager.commandExecutor().executeCommand(this.commandSender, "command").join(); + + // Assert + verify(this.commandExecutionHandler).executeFuture(any()); + verify(this.notifier).cooldownActive(eq(this.commandSender), any(), any(), any()); + verify(this.listener).cooldownCreated(eq(this.commandSender), any(), any()); + } + + @Test + void testCooldownExpired() { + // Arrange + final Cooldown decorator = Cooldown.of(DurationFunction.constant(Duration.ofHours(1L))); + this.commandManager.command( + this.commandManager.commandBuilder("command") + .apply(decorator) + .handler(this.commandExecutionHandler) + ); + + when(this.clock.instant()).thenReturn(Instant.ofEpochSecond(0L)).thenReturn(Instant.now()); + when(this.commandExecutionHandler.executeFuture(any())).thenReturn(CompletableFuture.completedFuture(null)); + + // Act + this.commandManager.commandExecutor().executeCommand(this.commandSender, "command").join(); + this.commandManager.commandExecutor().executeCommand(this.commandSender, "command").join(); + + // Assert + verify(this.commandExecutionHandler, times(2)).executeFuture(any()); + verify(this.notifier, never()).cooldownActive(eq(this.commandSender), any(), any(), any()); + verify(this.listener, times(2)).cooldownCreated(eq(this.commandSender), any(), any()); + } + + @Test + void testCooldownGroupsDifferentGroups() { + // Arrange + final Cooldown decorator1 = Cooldown.of( + DurationFunction.constant(Duration.ofHours(1L)), + CooldownGroup.named("foo") + ); + final Cooldown decorator2 = Cooldown.of( + DurationFunction.constant(Duration.ofHours(1L)), + CooldownGroup.named("bar") + ); + this.commandManager.command( + this.commandManager.commandBuilder("command1") + .apply(decorator1) + .handler(this.commandExecutionHandler) + ); + this.commandManager.command( + this.commandManager.commandBuilder("command2") + .apply(decorator2) + .handler(this.commandExecutionHandler) + ); + + when(this.clock.instant()).thenReturn(Instant.now()); + when(this.commandExecutionHandler.executeFuture(any())).thenReturn(CompletableFuture.completedFuture(null)); + + // Act + this.commandManager.commandExecutor().executeCommand(this.commandSender, "command1").join(); + this.commandManager.commandExecutor().executeCommand(this.commandSender, "command2").join(); + + // Assert + verify(this.commandExecutionHandler, times(2)).executeFuture(any()); + verify(this.notifier, never()).cooldownActive(eq(this.commandSender), any(), any(), any()); + verify(this.listener, times(2)).cooldownCreated(eq(this.commandSender), any(), any()); + } + + @Test + void testCooldownGroupsSameGroup() { + // Arrange + final Cooldown decorator = Cooldown.of( + DurationFunction.constant(Duration.ofHours(1L)), + CooldownGroup.named("foo") + ); + this.commandManager.command( + this.commandManager.commandBuilder("command1") + .apply(decorator) + .handler(this.commandExecutionHandler) + ); + this.commandManager.command( + this.commandManager.commandBuilder("command2") + .apply(decorator) + .handler(this.commandExecutionHandler) + ); + + when(this.clock.instant()).thenReturn(Instant.now()); + when(this.commandExecutionHandler.executeFuture(any())).thenReturn(CompletableFuture.completedFuture(null)); + + // Act + this.commandManager.commandExecutor().executeCommand(this.commandSender, "command1").join(); + this.commandManager.commandExecutor().executeCommand(this.commandSender, "command2").join(); + + // Assert + verify(this.commandExecutionHandler, times(1)).executeFuture(any()); + verify(this.notifier).cooldownActive(eq(this.commandSender), any(), any(), any()); + verify(this.listener).cooldownCreated(eq(this.commandSender), any(), any()); + } + + @Test + void testCooldownDeletion() { + // Arrange + final CooldownGroup group = CooldownGroup.named("foo"); + final CooldownProfile profile = this.cooldownManager.repository() + .getProfile(this.commandSender, this.cooldownManager.configuration().profileFactory()); + final CooldownInstance cooldown = CooldownInstance.builder() + .profile(profile) + .group(group) + .duration(Duration.ofHours(1L)) + .creationTime(Instant.now()) + .build(); + profile.setCooldown(cooldown.group(), cooldown); + + // Act + this.cooldownManager.repository().deleteCooldown(this.commandSender, group); + + // Assert + assertThat(profile.isEmpty()).isTrue(); + assertThat(this.cooldownManager.repository().getProfileIfExists(this.commandSender)).isNull(); + } +} diff --git a/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/util/TestCommandManager.java b/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/util/TestCommandManager.java new file mode 100644 index 0000000..3a0a508 --- /dev/null +++ b/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/util/TestCommandManager.java @@ -0,0 +1,41 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.confirmation.util; + +import cloud.commandframework.CommandManager; +import cloud.commandframework.execution.ExecutionCoordinator; +import cloud.commandframework.internal.CommandRegistrationHandler; +import org.checkerframework.checker.nullness.qual.NonNull; + +public final class TestCommandManager extends CommandManager { + + public TestCommandManager() { + super(ExecutionCoordinator.simpleCoordinator(), CommandRegistrationHandler.nullCommandRegistrationHandler()); + } + + @Override + public boolean hasPermission(final @NonNull TestCommandSender sender, final @NonNull String permission) { + return true; + } +} diff --git a/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/util/TestCommandSender.java b/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/util/TestCommandSender.java new file mode 100644 index 0000000..4bb7c34 --- /dev/null +++ b/cloud-processors-cooldown/src/test/java/org/incendo/cloud/processors/confirmation/util/TestCommandSender.java @@ -0,0 +1,27 @@ +// +// MIT License +// +// Copyright (c) 2024 Incendo +// +// 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 org.incendo.cloud.processors.confirmation.util; + +public interface TestCommandSender { +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d0c0895..718cbba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,4 +29,5 @@ dependencyResolutionManagement { rootProject.name = "cloud-processors-parent" include(":cloud-processors-common") -include(":cloud-processors-confirmation") \ No newline at end of file +include(":cloud-processors-confirmation") +include(":cloud-processors-cooldown")