Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions BotCommands-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import dev.freya02.botcommands.tasks.GenerateBCInfoTask
import dev.freya02.botcommands.utils.registerBucket4JDocs
import dev.freya02.botcommands.utils.registerJetbrainsAnnotationsDocs
import dev.freya02.botcommands.utils.registerSourceSet
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("repositories-conventions")
Expand Down Expand Up @@ -166,6 +167,14 @@ kotlin {
}
}

tasks.named<KotlinCompile>("compileTestKotlin") {
compilerOptions {
optIn.addAll(
"io.github.freya022.botcommands.api.core.hooks.custom.annotations.ExperimentalCustomEvents"
)
}
}

publishedProjectEnvironment {
configureJarArtifact(
artifactId = "BotCommands-core",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package doc.java.examples.core.hooks.custom;

import io.github.freya022.botcommands.api.core.hooks.custom.CustomEventRequirements;
import io.github.freya022.botcommands.api.core.hooks.custom.CustomEventRequirementsProvider;
import io.github.freya022.botcommands.api.core.service.annotations.BService;
import net.dv8tion.jda.api.events.guild.voice.GuildVoiceUpdateEvent;
import org.jspecify.annotations.NullMarked;

@BService
@NullMarked
public class MyCustomEventRequirementsProvider implements CustomEventRequirementsProvider {
// A custom event indicating a member moved to a different audio channel
static class GuildVoiceChannelMoveEvent {
// ...
}

@Override
public CustomEventRequirements get(Class<?> handledEventType) {
// Any event that is or extends GuildVoiceChannelMoveEvent
if (GuildVoiceChannelMoveEvent.class.isAssignableFrom(handledEventType)) {
// A member moving from a channel to another is signaled by a GuildVoiceUpdateEvent,
// so we give the same requirements
return CustomEventRequirements.fromEvents(GuildVoiceUpdateEvent.class);
}

// For other events than those we directly support.
// It can also be events from a different module,
// in which case that module should have a CustomEventRequirementsProvider too.
return CustomEventRequirements.unknown();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,22 @@ import io.github.freya022.botcommands.api.core.config.BCoroutineScopesConfig
import io.github.freya022.botcommands.api.core.config.BEventManagerConfig
import io.github.freya022.botcommands.api.core.events.BGenericEvent
import io.github.freya022.botcommands.api.core.hooks.EventDispatcher
import io.github.freya022.botcommands.api.core.hooks.custom.CustomEventRequirementsProvider
import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.requests.GatewayIntent
import java.util.concurrent.TimeUnit

/**
* Annotates a function as an event listener for a JDA or BC event.
* Annotates a function as an event listener for a JDA, BC, or custom event.
*
* Remember to always check the requirements of the events you're listening to!
* If an event does not fulfill their requirements, and they are not ignored, then the listener will be disabled!
*
* ### Requirements
* - The declaring class must be a service
* - The function must not be static
* - The first argument must be a subclass of [GenericEvent] or [BGenericEvent]
* - The first argument is typically a subclass of [GenericEvent] or [BGenericEvent], but can be anything other than `Any`/`Object`
* - If the event being listened to is a custom event, then a [CustomEventRequirementsProvider] service must be available.
*/
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.hooks.EventListener

/**
* Dispatches JDA and BC events to [@BEventListener][BEventListener] methods.
* Dispatches events to [@BEventListener][BEventListener] methods.
*/
@InterfacedService(acceptMultiple = false)
abstract class EventDispatcher internal constructor() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package io.github.freya022.botcommands.api.core.hooks.custom

import io.github.freya022.botcommands.api.core.hooks.custom.annotations.ExperimentalCustomEvents
import io.github.freya022.botcommands.api.core.utils.toEnumSet
import io.github.freya022.botcommands.internal.core.hooks.custom.CustomEventRequirementsImpl
import io.github.freya022.botcommands.internal.core.hooks.custom.EmptyCustomEventRequirements
import io.github.freya022.botcommands.internal.core.hooks.custom.UnknownCustomEventRequirements
import net.dv8tion.jda.api.events.GenericEvent
import net.dv8tion.jda.api.requests.GatewayIntent

/**
* Represents the requirements of a custom event.
*/
@ExperimentalCustomEvents
interface CustomEventRequirements {

/**
* Whether this custom event has no requirements.
*/
fun isEmpty(): Boolean

/**
* Whether this custom event has unknown requirements.
*/
fun areUnknown(): Boolean

/**
* The unmodifiable intent set required by this event. Can be empty.
*/
fun getIntents(): Set<GatewayIntent>

companion object {

/**
* Creates an instance from the given intents.
*/
@JvmStatic
fun from(vararg intents: GatewayIntent): CustomEventRequirements {
return from(intents.toEnumSet())
}

/**
* Creates an instance from the given intents.
*/
@JvmStatic
fun from(intents: Collection<GatewayIntent>): CustomEventRequirements {
if (intents.isEmpty())
return none()

return CustomEventRequirementsImpl(intents)
}

/**
* Creates an instance from the intents required by the given events.
*
* This is a shortcut to as `from(GatewayIntent.fromEvents(events))`.
*/
@JvmStatic
@SafeVarargs
fun fromEvents(vararg events: Class<out GenericEvent>): CustomEventRequirements {
return from(GatewayIntent.fromEvents(*events))
}

/**
* Creates an instance from the intents required by the given events.
*
* This is a shortcut to as `from(GatewayIntent.fromEvents(events))`.
*/
@JvmStatic
fun fromEvents(events: Collection<Class<out GenericEvent>>): CustomEventRequirements {
return from(GatewayIntent.fromEvents(events))
}

/**
* Creates an instance from the intents required by the given events.
*
* This is a shortcut to as `from(GatewayIntent.fromEvents(E::class.java))`.
*/
@JvmSynthetic
inline fun <reified E : GenericEvent> fromEvent(): CustomEventRequirements {
return from(GatewayIntent.fromEvents(E::class.java))
}

/**
* Creates an instance indicating the event has no requirements.
*/
@JvmStatic
fun none(): CustomEventRequirements = EmptyCustomEventRequirements

/**
* Creates an instance indicating the event is unknown to the requirement provider.
*/
@JvmStatic
fun unknown(): CustomEventRequirements = UnknownCustomEventRequirements
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.github.freya022.botcommands.api.core.hooks.custom

import io.github.freya022.botcommands.api.core.hooks.custom.annotations.ExperimentalCustomEvents
import io.github.freya022.botcommands.api.core.service.annotations.InterfacedService

/**
* Provides the requirements for custom events.
* At least one service must implement this interface if you have a listener for a custom event.
*
* Exactly one instance must return (known) requirements for a given event type.
*
* ### Example
*
* ```java
* @BService
* @NullMarked
* public class MyCustomEventRequirementsProvider implements CustomEventRequirementsProvider {
* // A custom event indicating a member moved to a different audio channel
* static class GuildVoiceChannelMoveEvent {
* // ...
* }
*
* @Override
* public CustomEventRequirements get(Class<?> handledEventType) {
* // Any event that is or extends GuildVoiceChannelMoveEvent
* if (GuildVoiceChannelMoveEvent.class.isAssignableFrom(handledEventType)) {
* // A member moving from a channel to another is signaled by a GuildVoiceUpdateEvent,
* // so we give the same requirements
* return CustomEventRequirements.fromEvents(GuildVoiceUpdateEvent.class);
* }
*
* // For other events than those we directly support.
* // It can also be events from a different module,
* // in which case that module should have a CustomEventRequirementsProvider too.
* return CustomEventRequirements.unknown();
* }
* }
* ```
*
* @see get
*/
@InterfacedService(acceptMultiple = true)
@ExperimentalCustomEvents
interface CustomEventRequirementsProvider {

/**
* Returns a set of requirements for the provided event type, the type corresponds to the listener's first parameter type.
*
* This function must return [CustomEventRequirements.unknown()][CustomEventRequirements.unknown] if the provided type
* is not known (for example, if it comes from another module). Returning anything else in this case, is an error.
*
* @param handledEventType The type of event the listener can handle
*
* @return A [CustomEventRequirements] with the possible requirements
*/
fun get(handledEventType: Class<*>): CustomEventRequirements
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.github.freya022.botcommands.api.core.hooks.custom.annotations

import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.annotation.AnnotationTarget.CONSTRUCTOR
import kotlin.annotation.AnnotationTarget.FIELD
import kotlin.annotation.AnnotationTarget.FUNCTION
import kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE
import kotlin.annotation.AnnotationTarget.PROPERTY
import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER
import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER
import kotlin.annotation.AnnotationTarget.TYPEALIAS
import kotlin.annotation.AnnotationTarget.VALUE_PARAMETER

/**
* Opt-in marker annotation for APIs of custom events that are considered experimental and are not subject to compatibility guarantees:
* The behavior of such API may be changed or the API may be removed completely in any further release.
*
* Please create an issue or join the Discord server if you encounter a problem or want to submit feedback.
*
* Any usage of a declaration annotated with `@ExperimentalCustomEvents` must be accepted either by
* annotating that usage with the [@OptIn][OptIn] annotation, e.g. `@OptIn(ExperimentalCustomEvents::class)`,
* or by using the compiler argument `-opt-in=io.github.freya022.botcommands.api.core.hooks.custom.annotations.ExperimentalCustomEvents`.
*/
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
@Retention(AnnotationRetention.BINARY)
@Target(
CLASS,
ANNOTATION_CLASS,
PROPERTY,
FIELD,
LOCAL_VARIABLE,
VALUE_PARAMETER,
CONSTRUCTOR,
FUNCTION,
PROPERTY_GETTER,
PROPERTY_SETTER,
TYPEALIAS
)
@MustBeDocumented
annotation class ExperimentalCustomEvents
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ inline fun <reified T : Enum<T>> enumSetOfAll(): EnumSet<T> = EnumSet.allOf(T::c
inline fun <reified T : Enum<T>> enumSetOf(vararg elems: T): EnumSet<T> = enumSetOf<T>().apply { addAll(elems) }
inline fun <reified T : Enum<T>, V> enumMapOf(): EnumMap<T, V> = EnumMap<T, V>(T::class.java)

inline fun <reified T : Enum<T>> Array<out T>.toEnumSet(): EnumSet<T> = enumSetOf<T>().apply { addAll(this@toEnumSet) }
inline fun <reified T : Enum<T>> Collection<T>.toEnumSet(): EnumSet<T> = when (this) {
is EnumSet<T> -> EnumSet.copyOf(this)
else -> enumSetOf<T>().also { it.addAll(this) }
}

fun <T> Collection<T>.unmodifiableView(): Collection<T> {
return Collections.unmodifiableCollection(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ internal sealed class ClassPathFunction {
if (this === other) return true
if (other !is ClassPathFunction) return false

return function == other.function
return instance === other.instance && function == other.function
}

override fun hashCode(): Int {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import io.github.freya022.botcommands.api.core.config.BCoroutineScopesConfig
import io.github.freya022.botcommands.api.core.events.InitializationEvent
import io.github.freya022.botcommands.api.core.hooks.EventDispatcher
import io.github.freya022.botcommands.api.core.objectLogger
import io.github.freya022.botcommands.api.core.service.ServiceContainer
import io.github.freya022.botcommands.api.core.service.annotations.BService
import io.github.freya022.botcommands.api.core.service.lazy
import io.github.freya022.botcommands.api.core.utils.loggerOf
import io.github.freya022.botcommands.api.core.utils.simpleNestedName
import io.github.freya022.botcommands.internal.utils.shortSignature
Expand All @@ -21,15 +23,19 @@ private val logger = KotlinLogging.loggerOf<EventDispatcher>()
@BService
internal class EventDispatcherImpl internal constructor(
coroutineScopesConfig: BCoroutineScopesConfig,
private val eventListenerRegistry: EventListenerRegistry,
serviceContainer: ServiceContainer,
) : EventDispatcher() {

private val eventManagerCoroutineScope: CoroutineScope = coroutineScopesConfig.eventManagerScope
private val asyncCoroutineScope: CoroutineScope = coroutineScopesConfig.eventDispatcherScope

// Registry may fetch classes that also dispatch events, causing a circular dependency
private val eventListenerRegistry: EventListenerRegistry by serviceContainer.lazy()

internal fun onEvent(event: GenericEvent) {
// No need to check for `event` type as if it's in the map, then it's recognized
val handlers = eventListenerRegistry[event.javaClass] ?: return
val handlers = eventListenerRegistry[event.javaClass]
if (handlers.isEmpty) return

// Run blocking handlers first
handlers[RunMode.BLOCKING]?.let { eventHandlers ->
Expand Down Expand Up @@ -63,7 +69,8 @@ internal class EventDispatcherImpl internal constructor(

override suspend fun dispatchEvent(event: Any) {
// No need to check for `event` type as if it's in the map, then it's recognized
val handlers = eventListenerRegistry[event.javaClass] ?: return
val handlers = eventListenerRegistry[event.javaClass]
if (handlers.isEmpty) return

// Run blocking handlers first
handlers[RunMode.BLOCKING]?.forEach { eventHandler ->
Expand All @@ -86,7 +93,8 @@ internal class EventDispatcherImpl internal constructor(
override fun dispatchEventAsync(event: Any): List<Deferred<Unit>> {
// Try not to switch context on non-handled events
// No need to check for `event` type as if it's in the map, then it's recognized
val handlers = eventListenerRegistry[event.javaClass] ?: return emptyList()
val handlers = eventListenerRegistry[event.javaClass]
if (handlers.isEmpty) return emptyList()

return handlers.map { eventHandler ->
asyncCoroutineScope.async { runEventHandler(eventHandler, event) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.github.freya022.botcommands.internal.core.ClassPathFunction
import kotlin.time.Duration

internal class EventHandlerFunction(
val eventType: Class<*>,
val classPathFunction: ClassPathFunction,
val priority: Int,
val runMode: BEventListener.RunMode,
Expand Down
Loading