Skip to content

Custom Decorators

pumbas600 edited this page Dec 28, 2021 · 20 revisions

As Halpbot is built using Hartshorn, it's very easy to override the built-in decorators or to create entirely new decorators.

Overriding Decorators

It is possible to override the built-in decorators by binding a custom implementation with a higher priority than the default implementation. All Halpbot default implementations use a priority of -1, so any priority of 0+ will be used preferentially when constructing the decorator. It's important to note that all decorators must have a @Bound constructor of an ActionInvokable<C> and the decorator annotation.

Example: Overriding the @Log decorator

The basic format of any decorator is like this:

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 exact message sent (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)

Note: Actions aren't limited to message commands, such as buttons, so it's important to check the instance of the event rather than just calling HalpbotEvent#event(Class<?>) unless you're entirely certain.

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 the invocation context is created internally - Rather than via an event listener,
    // the halpbot event may be null
    if (halpbotEvent != null && 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 new decorator, when this command 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";
}
16:08:51.008 [ inWS-ReadThread] @ 13236 -> n.p.halpbot.decorators.log.LogLevel    INFO  - [MessageReceivedEvent] $log 
16:08:51.009 [ inWS-ReadThread] @ 13236 -> n.p.halpbot.decorators.log.LogLevel    INFO  - [Bot Testing][general] pumbas600#7051 has invoked the action HalpbotCommands#log() 

If you wanted to avoid invoking the overridden decorator (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<C> like so:

return this.actionInvokable().invoke(invocationContext);

ExplainedException

Sometimes, like in @Cooldown and @Permission, you don't want to invoke the action at all if certain conditions aren't met, however, you still want to display a message to the user informing them 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:

@Override
public <R> Exceptional<R> invoke(C invocationContext) {
    HalpbotEvent event = invocationContext.halpbotEvent();

    if (event == null || this.permissionService.hasPermissions(event.user().getIdLong(), this.permissions)) {
        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. Creating the annotation

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.utilities.LogLevel;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Time
{
    LogLevel value() default LogLevel.INFO;
}

Note: We'll have to come back and add the @Decorator annotation after we've created the factory

2. Creating the decorator

Decorators can be created by extending the ActionInvokableDecorator class. They must also be annotated with @Bind with the decorator class name, meaning it 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 implementation 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 factory. 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);

        double ms = start.until(OffsetDateTime.now(), ChronoUnit.NANOS) / 1000D;
        this.logLevel.log(invocationContext.applicationContext(),
                "Invoked [%s] in %.2fms".formatted(this.executable().qualifiedName(), ms));

        return result;
    }
}

3. Creating the factory

Finally, you need to create an interface implementing ActionInvokableDecoratorFactory<YourDecoratorClass, YourDecoratorAnnotation>, and override the decorate method. By explicitly typing the factory, this allows it to be properly 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. 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);
}

Finishing touches

We're nearly done setting up the decorator, we just need to go back and add the @Decorator annotation we skipped in step 1. With that, the annotation should look like this:

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.

Clone this wiki locally