-
Notifications
You must be signed in to change notification settings - Fork 0
Custom Decorators
As Halpbot is built using Hartshorn, it's very easy to override the built-in decorators or to create entirely new 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.
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 the 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";
}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()
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);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"));
}To create your own decorators, you need 3 things:
- An annotation, annotated with
@Decorator. - A decorator that extends
ActionInvokableDecoratorand has a@Boundconstructor which takes in anActionInvokableand the annotation you just created. - A decorator factory that is used to dynamically instantiate your decorator.
You'll 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
FIRSTmeans 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 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;
}
}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);
}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:
17:35:56.450 [ inWS-ReadThread] @ 4604 -> n.pumbas.halpbot.utilities.LogLevel INFO - Invoked [HalpbotCommands#time(int)] in 2992.30ms
- Built-in Commands
- @Command Parameters
- Arguments
- Annotations
- Custom Objects
- Custom TypeParsers
- Slash Commands - W.I.P.
- Pagination - W.I.P