-
Notifications
You must be signed in to change notification settings - Fork 0
Custom Decorators
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.
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.
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);
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"));
}
To create your own decorators, you need 3 things:
- An annotation, annotated with
@Decorator
. - A decorator that extends
ActionInvokableDecorator
and has a@Bound
constructor which takes in anActionInvokable
and the annotation you just created. - A decorator factory that is used to dynamically instantiate your decorator.
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.
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;
}
}
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);
}
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
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)
- Built-in Commands
- @Command Parameters
- Arguments
- Annotations
- Custom Objects
- Custom TypeParsers
- Slash Commands - W.I.P.
- Pagination - W.I.P