Skip to content

Custom Decorators

pumbas600 edited this page Jan 3, 2022 · 20 revisions

As Halpbot is built using Hartshorn's dependency injection, it's very easy to override the built-in decorators or to create entirely new decorators as the services responsible for decorating actions are independent of the specific implementations.

Overriding Decorators

It's possible to override the built-in decorators by binding a custom implementation with a higher priority than the default implementation. All implementations in Halpbot use a priority of -1, so a priority of 0+ will be used preferentially when constructing the decorator. This will make more sense in the examples below.

Example: Overriding the @Log decorator

The basic format of any decorator can be seen below:

Show Imports

import org.dockbox.hartshorn.core.annotations.inject.Binds;
import org.dockbox.hartshorn.core.annotations.inject.Bound;

import nz.pumbas.halpbot.actions.invokable.ActionInvokable;
import nz.pumbas.halpbot.actions.invokable.InvocationContext;
// @Binds indicates that this implementation defines the LogDecorator and should be used in the factory rather than the default
// LogDecorator due to the higher priority
@Binds(value = LogDecorator.class, priority = 0)

// All Decorators take in a generic parameter 'C extends InvocationContext' as they can be used by any action,
// each of which have their own InvocationContext
public class CustomLogDecorator<C extends InvocationContext> extends LogDecorator<C>
{
    // @Bound specifies that this constructor should be used by the factory to create this decorator
    @Bound
    public CustomLogDecorator(ActionInvokable<C> actionInvokable, Log log) {
        super(actionInvokable, log);
    }
}

In this custom decorator, we can then override any of the ActionInvokable<C> methods. In this case, let's override the Invoke(C invocationContext) method and make it log the message received (Assuming that the action was invoked using a MessageReceivedEvent).

TIP: In IntelliJ it's possible to bring up all the overridable methods using CTRL + O (⌃ O for MacOS).

Show Imports

import net.dv8tion.jda.api.events.message.MessageReceivedEvent;

import org.dockbox.hartshorn.core.domain.Exceptional;

import nz.pumbas.halpbot.events.HalpbotEvent;
@Override
public <R> Exceptional<R> invoke(C invocationContext) {
    HalpbotEvent halpbotEvent = invocationContext.halpbotEvent();
    if (halpbotEvent.rawEvent() instanceof MessageReceivedEvent messageEvent) {
        this.logLevel().log(invocationContext.applicationContext(),
                "[%s] %s".formatted(messageEvent.getClass().getSimpleName(), messageEvent.getMessage().getContentRaw()));
    }

    // Invoke the old decorator like normally
    return super.invoke(invocationContext);
}

With this custom log decorator, when the command below is invoked, you get the following logged in the console:

@Log(LogLevel.INFO)
@Command(description = "Tests the @Log decorator")
public String log() {
    return "This command is logged when it is invoked";
}
20:28:11.307 [ inWS-ReadThread] @ 17272 -> n.p.h.d.log.CustomLogDecorator         INFO  - [MessageReceivedEvent] $log 
20:28:11.308 [ inWS-ReadThread] @ 17272 -> n.p.h.decorators.log.LogDecorator      INFO  - [Bot Testing][general] pumbas600#7051 has invoked the action HalpbotCommands#log()

From the console, you can see that super.invoke(invocationContext) caused the overridden decorator to be called. If you wanted to avoid this and instead just calling the actual action itself - or the next decorator if there are multiple on the action, then you can do this by calling it via the decorated ActionInvokable like so:

this.actionInvokable().invoke(invocationContext);

ExplainedException

Sometimes, like in @Cooldown and @Permissions, you don't want to invoke the action at all if certain conditions aren't met. However, you may still want to display a message to the user informing them of the reason why the action wasn't performed. In these situations, you can return an Exceptional containing an ExplainedException with any object and it will be displayed temporarily.

For example, consider this snippet from the PermissionDecorator:

Show Imports

import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;

import org.dockbox.hartshorn.core.domain.Exceptional;

import nz.pumbas.halpbot.common.ExplainedException;
import nz.pumbas.halpbot.events.HalpbotEvent;
@Override
public <R> Exceptional<R> invoke(C invocationContext) {
    HalpbotEvent event = invocationContext.halpbotEvent();
    Guild guild = event.guild();
    Member member = event.member();

    if (guild == null || member == null || this.hasPermission(guild, member)) {
        return super.invoke(invocationContext);
    }
    return Exceptional.of(new ExplainedException("You do not have permission to use this command"));
}

Creating Decorators

To create your own decorators, you need 3 things:

  1. An annotation, annotated with @Decorator.
  2. A decorator that extends ActionInvokableDecorator and has a @Bound constructor which takes in an ActionInvokable and the annotation you just created.
  3. A decorator factory that is used to dynamically instantiate your decorator.

Example: @Time decorator

1. The annotation

The first thing which needs to be done is to create an annotation that will be used to apply the decorator to actions. You'll notice that you get an error with this as we haven't created the TimeDecoratorFactory yet, but we'll get to that in a second.

Show Imports

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import nz.pumbas.halpbot.decorators.Decorator;
import nz.pumbas.halpbot.decorators.Order;
import nz.pumbas.halpbot.utilities.LogLevel;
@Decorator(value = TimeDecoratorFactory.class, order = Order.FIRST)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Time
{
    LogLevel value() default LogLevel.INFO;
}

Note: The order FIRST means that this decoration will be called first, before any others that may be present on the action. In this situation, this enables it to time both the action and the decorators.

2. The decorator

Decorators can be created by extending the ActionInvokableDecorator class. They must also be annotated with @Bind with the decorator class name, meaning this class will be used to instantiate the decorator in the factory. Finally, you must have a constructor that takes in an ActionInvokable and the decorator annotation in that order, as this is the generic factory constructor defined.

Show Imports

import org.dockbox.hartshorn.core.annotations.inject.Binds;
import org.dockbox.hartshorn.core.annotations.inject.Bound;
import org.dockbox.hartshorn.core.domain.Exceptional;

import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;

import nz.pumbas.halpbot.actions.invokable.ActionInvokable;
import nz.pumbas.halpbot.actions.invokable.ActionInvokableDecorator;
import nz.pumbas.halpbot.actions.invokable.InvocationContext;
import nz.pumbas.halpbot.utilities.LogLevel;
// Bind the TimeDecorator to this instance so that the factory knows to instantiate this
@Binds(TimeDecorator.class)
public class TimeDecorator<C extends InvocationContext> extends ActionInvokableDecorator<C>
{
    private final LogLevel logLevel;

    // Its important that you're @Bound constructor contains the ActionInvokable and the annotation so that
    // it can be used by the constructor. If this doesn't match, an exception will be thrown during startup
    @Bound
    public TimeDecorator(ActionInvokable<C> actionInvokable, Time time) {
        super(actionInvokable);
        this.logLevel = time.value();
    }

    @Override
    public <R> Exceptional<R> invoke(C invocationContext) {
        OffsetDateTime start = OffsetDateTime.now();
        Exceptional<R> result = super.invoke(invocationContext);

        // Measure the time in milliseconds between now and before the action was invoked to see how long it took
        double ms = start.until(OffsetDateTime.now(), ChronoUnit.NANOS) / 1_000_000D;
        this.logLevel.log(invocationContext.applicationContext(), "Invoked [%s] %s in %.5fms"
                .formatted(this.executable().qualifiedName(), result.caught() ? "Unsuccessfully" : "Successfully", ms));

        return result;
    }
}

3. The factory

Finally, you need to create an interface implementing ActionInvokableDecoratorFactory<YourDecoratorClass, YourDecoratorAnnotation>, and override the decorate method. Overriding this method allows you to add the @Factory annotation, enabling it to be registered and bound to your decorator. Halpbot uses the generic ActionInvokableDecoratorFactory interface so that it can use the factory independently of any specific implementation.

Note: Behind the scenes, Hartshorn automatically proxies the interface with an implementation that creates an instance of the return type using the matching constructor. If no matching constructor is found, an exception will be thrown during initialisation. For more information, refer to this documentation.

Show Imports

import org.dockbox.hartshorn.core.annotations.Factory;
import org.dockbox.hartshorn.core.annotations.stereotype.Service;

import nz.pumbas.halpbot.actions.invokable.ActionInvokable;
import nz.pumbas.halpbot.decorators.ActionInvokableDecoratorFactory;
@Service
public interface TimeDecoratorFactory extends ActionInvokableDecoratorFactory<TimeDecorator<?>, Time>
{
    @Factory
    @Override
    TimeDecorator<?> decorate(ActionInvokable<?> element, Time annotation);
}

Usage

With the decorator now completely set up, it can be used anywhere a built-in decorator can. Just to try it out, let's try invoking the following command:

@Time
@Command(description = "Tests the @Time decorator")
public String time(int limit) {
    double sum = 0;
    // Some expensive action
    for (int i = 0; i < limit; i++) {
        sum += Math.sqrt(i);
    }
    return "Action complete!";
}

When invoked, we get the following in the console:

12:28:22.777 [ inWS-ReadThread] @ 3176 -> n.p.h.decorators.time.TimeDecorator    INFO  - Invoked [HalpbotCommands#time(int)] Unsuccessfully in 0.99870ms
12:28:37.477 [ inWS-ReadThread] @ 3176 -> n.p.h.decorators.time.TimeDecorator    INFO  - Invoked [HalpbotCommands#time(int)] Successfully in 4.99450ms 

DecoratorMerge

When the same decorators are found on both an action and its class, then a DecoratorMerge strategy is employed to determine how they should be handled. By default, this is KEEP_ACTION for decorators. The 3 possible strategies are described below:

Strategy Description
KEEP_ACTION Ignores decorators on the class and uses the decorators on the action if present.
KEEP_PARENT Ignores decorators on the action and uses the decorators on the class if present.
KEEP_BOTH Uses both the decorators on the class and the action.

You can adjust the merge strategy for your custom decorators in the @Decorator annotation. For example, the PermissionDecorator employs the KEEP_BOTH strategy, allowing you to define a base permission users must-have for actions in a class, while also allowing you to add further permissions for specific actions.

Show Imports

import nz.pumbas.halpbot.decorators.Decorator;
import nz.pumbas.halpbot.decorators.DecoratorMerge;
import nz.pumbas.halpbot.decorators.Order;
@Decorator(value = PermissionDecoratorFactory.class, order = Order.FIRST, merge = DecoratorMerge.KEEP_BOTH)