From 1c6781a3c508705354b80f4f6e9e829d5b145931 Mon Sep 17 00:00:00 2001 From: xXJanisXx Date: Mon, 20 Jul 2026 02:47:14 +0200 Subject: [PATCH 1/9] chore: bump dependencies --- build.gradle.kts | 41 ++++++++++++------------ gradle/libs.versions.toml | 19 +++++------ gradle/wrapper/gradle-wrapper.properties | 2 +- notify-bungeecord/build.gradle.kts | 1 - notify-shared/build.gradle.kts | 5 ++- notify-velocity/build.gradle.kts | 3 -- settings.gradle.kts | 13 ++------ 7 files changed, 34 insertions(+), 50 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 456dcdd..43b143d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,14 @@ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { alias(libs.plugins.kotlin) alias(libs.plugins.shadow) } -val baseVersion = "0.0.6" -val commitHash = System.getenv("COMMIT_HASH") -val snapshotversion = "${baseVersion}-dev.$commitHash" - allprojects { - group = "app.simplecloud.plugin.proxy" - version = if (commitHash != null) snapshotversion else baseVersion + group = "app.simplecloud.plugin" + version = "0.0.6" repositories { mavenCentral() @@ -22,8 +19,10 @@ allprojects { } subprojects { - apply(plugin = "org.jetbrains.kotlin.jvm") - apply(plugin = "com.gradleup.shadow") + apply { + plugin("kotlin") + plugin("com.gradleup.shadow") + } repositories { mavenCentral() @@ -31,31 +30,33 @@ subprojects { maven("https://repo.papermc.io/repository/maven-public/") maven("https://oss.sonatype.org/content/repositories/snapshots") maven("https://repo.simplecloud.app/snapshots") - } + } dependencies { testImplementation(rootProject.libs.kotlin.test) implementation(rootProject.libs.kotlinx.coroutines.core) - implementation(rootProject.libs.kotlin.jvm) - } - - java { - toolchain.languageVersion.set(JavaLanguageVersion.of(21)) } kotlin { - jvmToolchain(21) + jvmToolchain(25) compilerOptions { - apiVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0) + jvmTarget = JvmTarget.JVM_25 + languageVersion = KotlinVersion.KOTLIN_2_4 + apiVersion = KotlinVersion.KOTLIN_2_4 } } - tasks.named("shadowJar", ShadowJar::class) { - mergeServiceFiles() - archiveFileName.set("${project.name}.jar") + java { + toolchain.languageVersion.set(JavaLanguageVersion.of(25)) } tasks.test { useJUnitPlatform() } + + tasks.shadowJar { + mergeServiceFiles() + archiveFileName.set("${project.name}.jar") + } + } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index eeb14b4..ce6b7e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,17 @@ [versions] -kotlin = "2.2.20" -kotlinx-coroutines = "1.10.2" +kotlin = "2.4.10" +kotlinx-coroutines = "1.11.0" -shadow = "9.3.1" -kapt = "2.1.20" -minotaur = "2.8.10" +shadow = "9.5.1" +minotaur = "2.9.0" -simplecloud-api = "0.1.0-platform.29-dev.1776936118022-c717659" -simplecloud-plugin = "0.0.1-platform.1776615774559-382b07e" +simplecloud-api = "0.1.0-platform.40" +simplecloud-plugin = "0.0.1-platform.1778966459069-b1e45e2" -velocity = "3.5.0-SNAPSHOT" +velocity = "4.1.0-SNAPSHOT" bungeecord-api = "1.21-R0.4" -adventure-api = "4.26.1" +adventure-api = "5.2.0" adventure-platform-bungeecord = "4.4.1" configurate = "4.2.0" @@ -22,7 +21,6 @@ cloud-velocity = "2.0.0-beta.10" cloud-bungeecord = "2.0.0-beta.10" [libraries] -kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } @@ -49,5 +47,4 @@ configurate = ["configurate-kotlin", "configurate-yaml"] [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } -kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kapt" } minotaur = { id = "com.modrinth.minotaur", version.ref = "minotaur" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f78a6..a351597 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/notify-bungeecord/build.gradle.kts b/notify-bungeecord/build.gradle.kts index 9f23c50..0e3e900 100644 --- a/notify-bungeecord/build.gradle.kts +++ b/notify-bungeecord/build.gradle.kts @@ -8,7 +8,6 @@ dependencies { implementation(libs.adventure.platform.bungeecord) implementation(libs.adventure.api) implementation(libs.adventure.text.minimessage) - implementation(libs.cloud.bungeecord) } diff --git a/notify-shared/build.gradle.kts b/notify-shared/build.gradle.kts index 4c90a7e..eb4e25a 100644 --- a/notify-shared/build.gradle.kts +++ b/notify-shared/build.gradle.kts @@ -1,9 +1,8 @@ dependencies { - api(libs.bundles.configurate) + api(libs.cloud.core) api(libs.simplecloud.plugin) + api(libs.bundles.configurate) compileOnly(libs.simplecloud.api) compileOnly(libs.adventure.api) compileOnly(libs.adventure.text.minimessage) - - api(libs.cloud.core) } \ No newline at end of file diff --git a/notify-velocity/build.gradle.kts b/notify-velocity/build.gradle.kts index a6f34c8..5b92c5b 100644 --- a/notify-velocity/build.gradle.kts +++ b/notify-velocity/build.gradle.kts @@ -5,14 +5,11 @@ plugins { dependencies { api(project(":notify-shared")) - compileOnly(libs.velocity.api) kapt(libs.velocity.api) - implementation(libs.cloud.velocity) } - modrinth { token.set(project.findProperty("modrinthToken") as String? ?: System.getenv("MODRINTH_TOKEN")) projectId.set("daugEY3o") diff --git a/settings.gradle.kts b/settings.gradle.kts index d0bfdc2..5a2f300 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,16 +1,7 @@ -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -pluginManagement { - repositories { - mavenCentral() - gradlePluginPortal() - } -} - plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } -rootProject.name = "notify-plugin" - include("notify-shared", "notify-velocity", "notify-bungeecord") + +rootProject.name = "notify-plugin" \ No newline at end of file From 5f6ef9babf51c703c2bedec85da14f0bb3b586a8 Mon Sep 17 00:00:00 2001 From: xXJanisXx Date: Mon, 20 Jul 2026 02:53:29 +0200 Subject: [PATCH 2/9] refactor: remove @Setting annotation from config --- .../plugin/notify/shared/config/Config.kt | 117 +++++------------- 1 file changed, 28 insertions(+), 89 deletions(-) diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/config/Config.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/config/Config.kt index 3d83223..9eddc92 100644 --- a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/config/Config.kt +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/config/Config.kt @@ -2,55 +2,31 @@ package app.simplecloud.plugin.notify.shared.config import app.simplecloud.api.server.ServerState import app.simplecloud.plugin.api.shared.config.AbstractMessageConfig +import app.simplecloud.plugin.api.shared.config.VersionedConfig import org.spongepowered.configurate.objectmapping.ConfigSerializable -import org.spongepowered.configurate.objectmapping.meta.Setting @ConfigSerializable data class MessageConfig( - @Setting("version") - val version: String = "1", - - @Setting("format") + override val version: Int = 1, val format: Format = Format(), - - @Setting("command") val command: Command = Command(), - - @Setting("notifications") val notifications: Notifications = Notifications(), - - public override val variables: Map = mapOf( - "prefix" to "" - ), -) : AbstractMessageConfig() + public override val variables: Map = mapOf("prefix" to ""), +) : VersionedConfig, AbstractMessageConfig() @ConfigSerializable data class Format( - @Setting("date") val date: String = "dd.MM.yyyy HH:mm:ss" ) @ConfigSerializable data class Command( - @Setting("help") val help: Help = Help(), - - @Setting("usage") val usage: Usage = Usage(), - - @Setting("permission") val permission: Permission = Permission(), - - @Setting("notify") val notify: Notify = Notify(), - - @Setting("set") val set: SetNotify = SetNotify(), - - @Setting("reload") val reload: Reload = Reload(), - - @Setting("error") val error: Error = Error() ) @@ -65,49 +41,30 @@ data class Help( data class Usage( val invalid: String = " <#DC2626>Use <#F8FAFC> <#DC2626>instead.", val entry: String = "<#E2E8F0>", - @Setting("invalid-state") - val invalidState: String = - " <#DC2626>Use <#F8FAFC>enable <#DC2626>or <#F8FAFC>disable<#DC2626>.", - @Setting("invalid-toggle-value") - val invalidToggleValue: String = - " <#DC2626>Use <#F8FAFC>true <#DC2626>or <#F8FAFC>false<#DC2626>.", - @Setting("missing-player") - val missingPlayer: String = - " <#DC2626>Missing player <#F8FAFC><#DC2626>." + val invalidState: String = " <#DC2626>Use <#F8FAFC>enable <#DC2626>or <#F8FAFC>disable<#DC2626>.", + val invalidToggleValue: String = " <#DC2626>Use <#F8FAFC>true <#DC2626>or <#F8FAFC>false<#DC2626>.", + val missingPlayer: String = " <#DC2626>Missing player <#F8FAFC><#DC2626>." ) @ConfigSerializable data class Permission( - val denied: String = - " <#DC2626>You do not have permission to use this command." + val denied: String = " <#DC2626>You do not have permission to use this command." ) @ConfigSerializable data class Notify( - val enabled: String = - " <#A3E635>Server notifications are now <#F8FAFC>enabled<#A3E635>.", - val disabled: String = - " <#A3E635>Server notifications are now <#F8FAFC>disabled<#A3E635>.", - @Setting("already-enabled") - val alreadyEnabled: String = - " <#F59E0B>Server notifications are already <#F8FAFC>enabled<#F59E0B>.", - @Setting("already-disabled") - val alreadyDisabled: String = - " <#F59E0B>Server notifications are already <#F8FAFC>disabled<#F59E0B>." + val enabled: String = " <#A3E635>Server notifications are now <#F8FAFC>enabled<#A3E635>.", + val disabled: String = " <#A3E635>Server notifications are now <#F8FAFC>disabled<#A3E635>.", + val alreadyEnabled: String = " <#F59E0B>Server notifications are already <#F8FAFC>enabled<#F59E0B>.", + val alreadyDisabled: String = " <#F59E0B>Server notifications are already <#F8FAFC>disabled<#F59E0B>." ) @ConfigSerializable data class SetNotify( - val enabled: String = - " <#A3E635>Server notifications are now <#F8FAFC>enabled <#A3E635>for :true> <#F8FAFC><#A3E635>.", - val disabled: String = - " <#A3E635>Server notifications are now <#F8FAFC>disabled <#A3E635>for :true> <#F8FAFC><#A3E635>.", - @Setting("already-enabled") - val alreadyEnabled: String = - " <#F59E0B>Server notifications are already <#F8FAFC>enabled <#F59E0B>for :true> <#F8FAFC><#F59E0B>.", - @Setting("already-disabled") - val alreadyDisabled: String = - " <#F59E0B>Server notifications are already <#F8FAFC>disabled <#F59E0B>for :true> <#F8FAFC><#F59E0B>." + val enabled: String = " <#A3E635>Server notifications are now <#F8FAFC>enabled <#A3E635>for :true> <#F8FAFC><#A3E635>.", + val disabled: String = " <#A3E635>Server notifications are now <#F8FAFC>disabled <#A3E635>for :true> <#F8FAFC><#A3E635>.", + val alreadyEnabled: String = " <#F59E0B>Server notifications are already <#F8FAFC>enabled <#F59E0B>for :true> <#F8FAFC><#F59E0B>.", + val alreadyDisabled: String = " <#F59E0B>Server notifications are already <#F8FAFC>disabled <#F59E0B>for :true> <#F8FAFC><#F59E0B>." ) @ConfigSerializable @@ -117,28 +74,16 @@ data class Reload( @ConfigSerializable data class ReloadConfig( - val success: String = - " <#A3E635>Notify configuration was reloaded.", - val failed: String = - " <#DC2626>Notify configuration could not be reloaded." + val success: String = " <#A3E635>Notify configuration was reloaded.", + val failed: String = " <#DC2626>Notify configuration could not be reloaded." ) @ConfigSerializable data class Error( - @Setting("player-not-found") - val playerNotFound: String = - " <#DC2626>Player <#F8FAFC> <#DC2626>was not found.", - - @Setting("player-not-online") - val playerNotOnline: String = - " <#DC2626>Player <#F8FAFC> <#DC2626>is not online.", - - @Setting("storage-unavailable") - val storageUnavailable: String = - " <#DC2626>Notify settings are currently unavailable.", - - val internal: String = - " <#DC2626>An internal error occurred. Try again later." + val playerNotFound: String = " <#DC2626>Player <#F8FAFC> <#DC2626>was not found.", + val playerNotOnline: String = " <#DC2626>Player <#F8FAFC> <#DC2626>is not online.", + val storageUnavailable: String = " <#DC2626>Notify settings are currently unavailable.", + val internal: String = " <#DC2626>An internal error occurred. Try again later." ) @ConfigSerializable @@ -166,14 +111,10 @@ data class Hover( @ConfigSerializable data class ServerNotifications( - val starting: String? = - " <#0EA5E9>Server '><#F8FAFC> <#0EA5E9>is now <#F8FAFC>starting<#0EA5E9>.", - val available: String? = - " <#A3E635>Server '><#F8FAFC> <#A3E635>is now <#F8FAFC>available<#A3E635>.", - val stopping: String? = - " <#F59E0B>Server '><#F8FAFC> <#F59E0B>is now <#F8FAFC>stopping<#F59E0B>.", - val stopped: String? = - " <#DC2626>Server '><#F8FAFC> <#DC2626>is now <#F8FAFC>stopped<#DC2626>." + val starting: String? = " <#0EA5E9>Server '><#F8FAFC> <#0EA5E9>is now <#F8FAFC>starting<#0EA5E9>.", + val available: String? = " <#A3E635>Server '><#F8FAFC> <#A3E635>is now <#F8FAFC>available<#A3E635>.", + val stopping: String? = " <#F59E0B>Server '><#F8FAFC> <#F59E0B>is now <#F8FAFC>stopping<#F59E0B>.", + val stopped: String? = " <#DC2626>Server '><#F8FAFC> <#DC2626>is now <#F8FAFC>stopped<#DC2626>." ) { fun getByState(state: ServerState): String? { return when (state) { @@ -187,8 +128,6 @@ data class ServerNotifications( @ConfigSerializable data class GroupNotifications( - val created: String? = - " <#0EA5E9>Group '><#F8FAFC> <#0EA5E9>was created.", - val deleted: String? = - " <#DC2626>Group '><#F8FAFC> <#DC2626>was deleted." + val created: String? = " <#0EA5E9>Group '><#F8FAFC> <#0EA5E9>was created.", + val deleted: String? = " <#DC2626>Group '><#F8FAFC> <#DC2626>was deleted." ) \ No newline at end of file From 25530458be9cf3d4568fe553cd4231e4a56b083e Mon Sep 17 00:00:00 2001 From: xXJanisXx Date: Wed, 22 Jul 2026 22:54:56 +0200 Subject: [PATCH 3/9] feat: add notification commands and per-player toggle - add scnotify command (help, reload, enable/disable, set ) - add per-player notification toggle stored in player properties - restructure project into command/listener/notification/config/utilities packages --- .../notify/bungeecord/BungeeCloudSender.kt | 20 -- .../bungeecord/BungeeCordNotifyPlugin.kt | 31 ++- .../bungeecord/command/BungeeCloudSender.kt | 28 +++ .../listener/NotifyConnectionListener.kt | 23 ++ .../plugin/notify/shared/NotifyPlugin.kt | 221 +++--------------- .../shared/{ => command}/CloudSender.kt | 7 +- .../notify/shared/command/NotifyCommand.kt | 190 +++++++++++++++ .../plugin/notify/shared/config/Config.kt | 4 +- .../shared/listener/NotificationListener.kt | 119 ++++++++++ .../shared/{ => notification}/Notification.kt | 5 +- .../notification/NotificationRenderer.kt | 81 +++++++ .../notification/NotificationSettings.kt | 38 +++ .../notify/shared/utilities/ConfigVersion.kt | 7 + .../shared/utilities/NotifyPermissions.kt | 25 ++ .../notify/velocity/VelocityCloudSender.kt | 18 -- .../notify/velocity/VelocityNotifyPlugin.kt | 23 +- .../velocity/command/VelocityCloudSender.kt | 26 +++ .../listener/NotifyConnectionListener.kt | 22 ++ 18 files changed, 648 insertions(+), 240 deletions(-) delete mode 100644 notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/BungeeCloudSender.kt create mode 100644 notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/command/BungeeCloudSender.kt create mode 100644 notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/listener/NotifyConnectionListener.kt rename notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/{ => command}/CloudSender.kt (57%) create mode 100644 notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/command/NotifyCommand.kt create mode 100644 notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/listener/NotificationListener.kt rename notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/{ => notification}/Notification.kt (83%) create mode 100644 notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/NotificationRenderer.kt create mode 100644 notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/NotificationSettings.kt create mode 100644 notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/utilities/ConfigVersion.kt create mode 100644 notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/utilities/NotifyPermissions.kt delete mode 100644 notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityCloudSender.kt create mode 100644 notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/command/VelocityCloudSender.kt create mode 100644 notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/listener/NotifyConnectionListener.kt diff --git a/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/BungeeCloudSender.kt b/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/BungeeCloudSender.kt deleted file mode 100644 index 36f3c65..0000000 --- a/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/BungeeCloudSender.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.simplecloud.plugin.notify.bungeecord - -import app.simplecloud.plugin.notify.shared.CloudSender -import net.kyori.adventure.platform.bungeecord.BungeeAudiences -import net.kyori.adventure.text.Component -import net.md_5.bungee.api.connection.ProxiedPlayer - -class BungeeCloudSender( - private val player: ProxiedPlayer, - private val adventure: BungeeAudiences, -) : CloudSender { - - override fun sendMessage(message: Component) { - adventure.player(player).sendMessage(message) - } - - override fun hasPermission(permission: String): Boolean { - return player.hasPermission(permission) - } -} diff --git a/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/BungeeCordNotifyPlugin.kt b/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/BungeeCordNotifyPlugin.kt index c568104..f9069c3 100644 --- a/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/BungeeCordNotifyPlugin.kt +++ b/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/BungeeCordNotifyPlugin.kt @@ -1,26 +1,47 @@ package app.simplecloud.plugin.notify.bungeecord -import app.simplecloud.plugin.notify.shared.CloudSender +import app.simplecloud.plugin.notify.bungeecord.command.BungeeCloudSender +import app.simplecloud.plugin.notify.bungeecord.listener.NotifyConnectionListener import app.simplecloud.plugin.notify.shared.NotifyPlugin +import app.simplecloud.plugin.notify.shared.command.CloudSender import net.kyori.adventure.platform.bungeecord.BungeeAudiences import net.md_5.bungee.api.plugin.Plugin +import org.incendo.cloud.SenderMapper +import org.incendo.cloud.bungee.BungeeCommandManager +import org.incendo.cloud.execution.ExecutionCoordinator class BungeeCordNotifyPlugin : Plugin() { private val adventure = BungeeAudiences.create(this) - private var notify: NotifyPlugin? = null + private var notifyPlugin: NotifyPlugin? = null override fun onEnable() { - notify = NotifyPlugin(dataFolder.toPath(), ::onlinePlayers) + val notify = NotifyPlugin(dataFolder.toPath(), ::onlinePlayers) + notify.start() + notify.registerCommands(createCommandManager()) + + notifyPlugin = notify + proxy.pluginManager.registerListener(this, NotifyConnectionListener(notify)) } override fun onDisable() { - notify?.close() - notify = null + notifyPlugin?.close() + notifyPlugin = null adventure.close() } + private fun createCommandManager(): BungeeCommandManager { + return BungeeCommandManager( + this, + ExecutionCoordinator.simpleCoordinator(), + SenderMapper.create( + { source -> BungeeCloudSender(source, adventure) }, + BungeeCloudSender::source + ) + ) + } + private fun onlinePlayers(): Iterable { return proxy.players.map { BungeeCloudSender(it, adventure) } } diff --git a/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/command/BungeeCloudSender.kt b/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/command/BungeeCloudSender.kt new file mode 100644 index 0000000..8f9b4aa --- /dev/null +++ b/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/command/BungeeCloudSender.kt @@ -0,0 +1,28 @@ +package app.simplecloud.plugin.notify.bungeecord.command + +import app.simplecloud.plugin.notify.shared.command.CloudSender +import net.kyori.adventure.platform.bungeecord.BungeeAudiences +import net.kyori.adventure.text.Component +import net.md_5.bungee.api.CommandSender +import net.md_5.bungee.api.connection.ProxiedPlayer +import java.util.UUID + +class BungeeCloudSender( + val source: CommandSender, + private val adventure: BungeeAudiences, +) : CloudSender { + + override val uniqueId: UUID? + get() = (source as? ProxiedPlayer)?.uniqueId + + override val name: String + get() = source.name + + override fun sendMessage(message: Component) { + adventure.sender(source).sendMessage(message) + } + + override fun hasPermission(permission: String): Boolean { + return source.hasPermission(permission) + } +} \ No newline at end of file diff --git a/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/listener/NotifyConnectionListener.kt b/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/listener/NotifyConnectionListener.kt new file mode 100644 index 0000000..bd8f4e8 --- /dev/null +++ b/notify-bungeecord/src/main/kotlin/app/simplecloud/plugin/notify/bungeecord/listener/NotifyConnectionListener.kt @@ -0,0 +1,23 @@ +package app.simplecloud.plugin.notify.bungeecord.listener + +import app.simplecloud.plugin.notify.shared.NotifyPlugin +import net.md_5.bungee.api.event.PlayerDisconnectEvent +import net.md_5.bungee.api.event.PostLoginEvent +import net.md_5.bungee.api.plugin.Listener +import net.md_5.bungee.event.EventHandler + +class NotifyConnectionListener( + private val notify: NotifyPlugin, +) : Listener { + + @EventHandler + fun onPostLogin(event: PostLoginEvent) { + notify.onPlayerJoin(event.player.uniqueId) + } + + @EventHandler + fun onDisconnect(event: PlayerDisconnectEvent) { + notify.onPlayerQuit(event.player.uniqueId) + } + +} \ No newline at end of file diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/NotifyPlugin.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/NotifyPlugin.kt index 5aed557..82d6db7 100644 --- a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/NotifyPlugin.kt +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/NotifyPlugin.kt @@ -1,219 +1,58 @@ package app.simplecloud.plugin.notify.shared import app.simplecloud.api.CloudApi -import app.simplecloud.api.server.Server -import app.simplecloud.api.server.ServerState import app.simplecloud.plugin.api.shared.config.ConfigurationFactory +import app.simplecloud.plugin.notify.shared.command.CloudSender +import app.simplecloud.plugin.notify.shared.command.NotifyCommand import app.simplecloud.plugin.notify.shared.config.MessageConfig -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import net.kyori.adventure.text.Component -import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder -import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import app.simplecloud.plugin.notify.shared.listener.NotificationListener +import app.simplecloud.plugin.notify.shared.notification.NotificationRenderer +import app.simplecloud.plugin.notify.shared.notification.NotificationSettings +import kotlinx.coroutines.* +import org.incendo.cloud.CommandManager import java.nio.file.Path -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.util.* -import java.util.concurrent.ConcurrentHashMap +import java.util.UUID class NotifyPlugin( dataDirectory: Path, - private val recipients: () -> Iterable, + recipients: () -> Iterable, ) { - private val serverHoverPlaceholder = "notifications.hover.server" - private val groupHoverPlaceholder = "notifications.hover.group" - private val serverStatePermissionPrefix = "notify.state.server" - private val serverStoppedPermission = "$serverStatePermissionPrefix.stopped" - private val legacyServerStoppedPermission = "$serverStatePermissionPrefix.deleted" - private val serverStoppedPermissions = setOf(serverStoppedPermission, legacyServerStoppedPermission) + val config = ConfigurationFactory(dataDirectory.resolve("messages.yml").toFile(), MessageConfig::class.java) - private val groupCreatedPermission = "notify.state.group.created" - private val groupDeletedPermission = "notify.state.group.deleted" + private val api = CloudApi.create() + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val renderer = NotificationRenderer(config::get) + private val settings = NotificationSettings(api.player()) + private val listener = NotificationListener(api, config::get, recipients, renderer, settings) - val config = ConfigurationFactory( - dataDirectory.resolve("messages.yml").toFile(), - MessageConfig::class.java, - ) - - private val lastSentState: MutableMap = ConcurrentHashMap() - - private val cloudApi: CloudApi = CloudApi.create() - - private val zone: ZoneId = ZoneId.systemDefault() - private val formatter: DateTimeFormatter - - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - init { - val newConf = config.loadOrCreate(MessageConfig()) - formatter = DateTimeFormatter.ofPattern(newConf.format.date, Locale.getDefault()) - .withZone(zone) - - cloudApi.event().server().onStateChanged { event -> - if (event.oldState == event.newState) { - return@onStateChanged - } - - val serverState = event.newState - val server = event.server - - scope.launch { - val message = config.get().notifications.server?.getByState(serverState) ?: return@launch - - if (!markStateSent(server, serverState)) return@launch - - sendServerNotification( - server, - message, - setOf(serverStatePermission(serverState)), - Placeholder.parsed("state", serverState.name) - ) - } - } - - cloudApi.event().server().onStopped { event -> - scope.launch { - val message = config.get().notifications.server?.stopped ?: return@launch - - sendServerNotification( - event.server, - message, - serverStoppedPermissions, - Placeholder.parsed("state", "STOPPED") - ) - } - } - - cloudApi.event().group().onCreated { event -> - val group = event.group - - scope.launch { - val message = config.get().notifications.group?.created ?: return@launch - - sendGroupNotification( - message, - group.name, - groupCreatedPermission - ) - } - } - - cloudApi.event().group().onDeleted { event -> - scope.launch { - val message = config.get().notifications.group?.deleted ?: return@launch - - sendGroupNotification( - message, - event.name, - groupDeletedPermission - ) - } - } + fun start() { + config.loadOrCreate(MessageConfig()) + listener.start() } fun close() { + listener.stop() + settings.clear() scope.cancel() } - private fun markStateSent(server: Server, serverState: ServerState): Boolean { - return lastSentState.put(server.serverId, serverState) != serverState - } - - private fun sendServerNotification( - server: Server, - message: String, - permissions: Set, - vararg tagResolver: TagResolver, - ) { - val parsed = parseServerMessage( - server, - replaceServerHover(message), - *tagResolver - ) - - broadcast(Notification(parsed, permissions)) - } - - private fun sendGroupNotification(message: String, groupName: String?, permission: String) { - val parsed = parseMessage( - replaceGroupHover(message), - Placeholder.parsed("group", groupName ?: "N/A"), - Placeholder.parsed("time", formatTimestamp(Instant.now())) - ) - - broadcast(Notification(parsed, permission)) - } - - private fun broadcast(notification: Notification) { - recipients().forEach(notification::sendTo) - } - - private fun replaceServerHover(message: String): String { - return replaceHover( - message, - serverHoverPlaceholder, - config.get().notifications.hover.server - ) - } - - private fun replaceGroupHover(message: String): String { - return replaceHover( - message, - groupHoverPlaceholder, - config.get().notifications.hover.group - ) - } - - private fun replaceHover(message: String, placeholder: String, hoverMessage: String): String { - return message.replace( - "'>", - "" - ) + fun registerCommands(manager: CommandManager) { + NotifyCommand(manager, this, renderer, settings, api.player()).register() } - private fun parseServerMessage(server: Server, message: String, vararg tagResolver: TagResolver): Component { - return parseMessage( - message, - Placeholder.parsed("ip", server.ip ?: "N/A"), - Placeholder.parsed("port", server.port.toString()), - Placeholder.parsed("group", server.serverBase.name ?: "N/A"), - Placeholder.parsed("server", server.serverBase.name ?: "N/A"), - Placeholder.parsed("uuid", server.serverId), - Placeholder.parsed("id", server.numericalId.takeIf { it != -1 }?.let { " $it" } ?: ""), - Placeholder.parsed("create_date", formatTimestamp(runCatching { server.createdAt }.getOrNull())), - Placeholder.parsed("time", formatTimestamp(Instant.now())), - Placeholder.parsed("updated", formatTimestamp(runCatching { server.updatedAt }.getOrNull())), - Placeholder.parsed("players", server.playerCount.toString()), - Placeholder.parsed("max", server.maxPlayers.toString()), - *tagResolver - ) - } - - private fun parseMessage(message: String, vararg tagResolver: TagResolver): Component { - return config.get().msg(message, *tagResolver) + fun onPlayerJoin(id: UUID) { + scope.launch { + settings.load(id) + } } - private fun formatTimestamp(value: Any?): String { - if (value == null) return "N/A" - - val instant = when (value) { - is Instant -> value - is Date -> value.toInstant() - is Long -> Instant.ofEpochMilli(value) - is String -> runCatching { Instant.parse(value) }.getOrNull() - else -> null - } ?: return "N/A" - - return formatter.format(instant) + fun onPlayerQuit(id: UUID) { + settings.unload(id) } - private fun serverStatePermission(state: ServerState): String { - return "$serverStatePermissionPrefix.${state.name.lowercase()}" + fun reload(): Boolean { + return runCatching { config.reload() }.isSuccess } } diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/CloudSender.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/command/CloudSender.kt similarity index 57% rename from notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/CloudSender.kt rename to notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/command/CloudSender.kt index b25e6b8..e6b5d21 100644 --- a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/CloudSender.kt +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/command/CloudSender.kt @@ -1,9 +1,14 @@ -package app.simplecloud.plugin.notify.shared +package app.simplecloud.plugin.notify.shared.command import net.kyori.adventure.text.Component +import java.util.UUID interface CloudSender { + val uniqueId: UUID? + + val name: String + fun sendMessage(message: Component) fun hasPermission(permission: String): Boolean diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/command/NotifyCommand.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/command/NotifyCommand.kt new file mode 100644 index 0000000..b1ce8a8 --- /dev/null +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/command/NotifyCommand.kt @@ -0,0 +1,190 @@ +package app.simplecloud.plugin.notify.shared.command + +import app.simplecloud.api.player.CloudPlayer +import app.simplecloud.api.player.PlayerApi +import app.simplecloud.plugin.notify.shared.NotifyPlugin +import app.simplecloud.plugin.notify.shared.config.MessageConfig +import app.simplecloud.plugin.notify.shared.notification.NotificationRenderer +import app.simplecloud.plugin.notify.shared.notification.NotificationSettings +import app.simplecloud.plugin.notify.shared.utilities.NotifyPermissions +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import org.incendo.cloud.CommandManager +import org.incendo.cloud.parser.standard.BooleanParser +import org.incendo.cloud.parser.standard.StringParser + +class NotifyCommand( + private val manager: CommandManager, + private val notify: NotifyPlugin, + private val renderer: NotificationRenderer, + private val settings: NotificationSettings, + private val api: PlayerApi, +) { + + private val config: MessageConfig + get() = notify.config.get() + + private val commands = linkedMapOf( + "/scnotify help" to NotifyPermissions.COMMAND_HELP, + "/scnotify reload config" to NotifyPermissions.COMMAND_RELOAD_CONFIG, + "/scnotify " to NotifyPermissions.COMMAND_TOGGLE_SELF, + "/scnotify set " to NotifyPermissions.COMMAND_TOGGLE_OTHER, + ) + + fun register() { + registerHelp() + registerReload() + registerToggleSelf() + registerToggleOther() + } + + private fun registerHelp() { + manager.command( + manager.commandBuilder("scnotify") + .permission(NotifyPermissions.COMMAND_HELP) + .handler { context -> sendHelp(context.sender()) } + ) + + manager.command( + manager.commandBuilder("scnotify") + .literal("help") + .permission(NotifyPermissions.COMMAND_HELP) + .handler { context -> sendHelp(context.sender()) } + ) + } + + private fun sendHelp(sender: C) { + val usable = commands.filterValues { sender.hasPermission(it) }.keys + + if (usable.isEmpty()) { + sender.sendMessage(renderer.message(config.command.help.empty)) + return + } + + sender.sendMessage(renderer.message(config.command.help.title)) + usable.forEach { usage -> + sender.sendMessage(renderer.message(config.command.help.entry, Placeholder.unparsed("command", usage))) + } + } + + private fun registerReload() { + manager.command( + manager.commandBuilder("scnotify") + .literal("reload").literal("config") + .permission(NotifyPermissions.COMMAND_RELOAD_CONFIG) + .handler { context -> + val sender = context.sender() + + if (notify.reload()) { + sender.sendMessage(renderer.message(config.command.reload.config.success)) + } else { + sender.sendMessage(renderer.message(config.command.reload.config.failed)) + } + } + ) + } + + private fun registerToggleSelf() { + manager.command( + manager.commandBuilder("scnotify") + .literal("enable") + .permission(NotifyPermissions.COMMAND_TOGGLE_SELF) + .handler { context -> toggleSelf(context.sender(), true) } + ) + + manager.command( + manager.commandBuilder("scnotify") + .literal("disable") + .permission(NotifyPermissions.COMMAND_TOGGLE_SELF) + .handler { context -> toggleSelf(context.sender(), false) } + ) + } + + private fun toggleSelf(sender: C, value: Boolean) { + val id = sender.uniqueId + + if (id == null) { + sender.sendMessage(renderer.message(config.command.error.onlyPlayers)) + return + } + + val messages = config.command.notify + + if (settings.isEnabled(id) == value) { + sender.sendMessage(renderer.message(if (value) messages.alreadyEnabled else messages.alreadyDisabled)) + return + } + + settings.set(id, value).whenComplete { _, error -> + if (error != null) { + sender.sendMessage(renderer.message(config.command.error.storageUnavailable)) + return@whenComplete + } + + sender.sendMessage(renderer.message(if (value) messages.enabled else messages.disabled)) + } + } + + private fun registerToggleOther() { + manager.command( + manager.commandBuilder("scnotify") + .literal("set") + .required("player", StringParser.stringParser()) + .required("value", BooleanParser.booleanParser()) + .permission(NotifyPermissions.COMMAND_TOGGLE_OTHER) + .handler { context -> + val sender = context.sender() + val player = context.get("player") + val value = context.get("value") + + api.get(player).whenComplete { target, error -> + if (error != null) { + sender.sendMessage(renderer.message(config.command.error.storageUnavailable)) + return@whenComplete + } + + if (target == null) { + sender.sendMessage(playerError(config.command.error.playerNotFound, player)) + return@whenComplete + } + + if (!target.isOnline) { + sender.sendMessage(playerError(config.command.error.playerNotOnline, player)) + return@whenComplete + } + + applyToTarget(sender, target, value) + } + } + ) + } + + private fun applyToTarget(sender: C, target: CloudPlayer, value: Boolean) { + if (settings.isEnabled(target.uniqueId) == value) { + sender.sendMessage(targetMessage(if (value) config.command.set.alreadyEnabled else config.command.set.alreadyDisabled, target)) + return + } + + settings.set(target.uniqueId, value).whenComplete { _, error -> + if (error != null) { + sender.sendMessage(renderer.message(config.command.error.storageUnavailable)) + return@whenComplete + } + + sender.sendMessage(targetMessage(if (value) config.command.set.enabled else config.command.set.disabled, target)) + target.sendMessage(renderer.message(if (value) config.command.notify.enabled else config.command.notify.disabled)) + } + } + + private fun targetMessage(message: String, target: CloudPlayer) = renderer.message( + message, + // Must stay parsed: is consumed as an argument of the tag. + Placeholder.parsed("playername", target.name), + Placeholder.unparsed("displayname", target.displayName ?: target.name) + ) + + private fun playerError(message: String, playerName: String) = renderer.message( + message, + Placeholder.unparsed("playername", playerName) + ) + +} diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/config/Config.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/config/Config.kt index 9eddc92..de29e49 100644 --- a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/config/Config.kt +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/config/Config.kt @@ -3,11 +3,12 @@ package app.simplecloud.plugin.notify.shared.config import app.simplecloud.api.server.ServerState import app.simplecloud.plugin.api.shared.config.AbstractMessageConfig import app.simplecloud.plugin.api.shared.config.VersionedConfig +import app.simplecloud.plugin.notify.shared.utilities.ConfigVersion import org.spongepowered.configurate.objectmapping.ConfigSerializable @ConfigSerializable data class MessageConfig( - override val version: Int = 1, + override val version: Int = ConfigVersion.VERSION, val format: Format = Format(), val command: Command = Command(), val notifications: Notifications = Notifications(), @@ -82,6 +83,7 @@ data class ReloadConfig( data class Error( val playerNotFound: String = " <#DC2626>Player <#F8FAFC> <#DC2626>was not found.", val playerNotOnline: String = " <#DC2626>Player <#F8FAFC> <#DC2626>is not online.", + val onlyPlayers: String = " <#DC2626>This command can only be used by players.", val storageUnavailable: String = " <#DC2626>Notify settings are currently unavailable.", val internal: String = " <#DC2626>An internal error occurred. Try again later." ) diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/listener/NotificationListener.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/listener/NotificationListener.kt new file mode 100644 index 0000000..04c7411 --- /dev/null +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/listener/NotificationListener.kt @@ -0,0 +1,119 @@ +package app.simplecloud.plugin.notify.shared.listener + +import app.simplecloud.api.CloudApi +import app.simplecloud.api.event.Subscription +import app.simplecloud.api.server.ServerState +import app.simplecloud.plugin.notify.shared.command.CloudSender +import app.simplecloud.plugin.notify.shared.notification.Notification +import app.simplecloud.plugin.notify.shared.config.MessageConfig +import app.simplecloud.plugin.notify.shared.notification.NotificationRenderer +import app.simplecloud.plugin.notify.shared.notification.NotificationSettings +import app.simplecloud.plugin.notify.shared.utilities.NotifyPermissions +import kotlinx.coroutines.* +import kotlinx.coroutines.future.await +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import java.util.concurrent.ConcurrentHashMap + +class NotificationListener( + private val api: CloudApi, + private val config: () -> MessageConfig, + private val recipients: () -> Iterable, + private val renderer: NotificationRenderer, + private val settings: NotificationSettings +) { + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val subscriptions: MutableList = mutableListOf() + private val lastSentState: MutableMap = ConcurrentHashMap() + + fun start() { + subscriptions.add( + api.event().server().onStateChanged { event -> + if (event.oldState == event.newState) { + return@onStateChanged + } + + val server = event.server + val state = event.newState + + scope.launch { + val message = config().notifications.server?.getByState(state) ?: return@launch + if (lastSentState.put(server.serverId, state) == state) return@launch + + broadcast( + Notification( + renderer.server(server, message, Placeholder.unparsed("state", state.name)), + setOf(NotifyPermissions.serverStatePermission(state)) + ) + ) + } + } + ) + + subscriptions.add( + api.event().server().onStopped { event -> + val server = event.server + lastSentState.remove(server.serverId) + + scope.launch { + val message = config().notifications.server?.stopped ?: return@launch + broadcast( + Notification( + renderer.server(server, message, Placeholder.unparsed("state", "STOPPED")), + NotifyPermissions.SERVER_STOPPED_ALIASES + ) + ) + } + } + ) + + subscriptions.add( + api.event().group().onCreated { event -> + val serverGroupId = event.serverGroupId + + scope.launch { + val message = config().notifications.group?.created ?: return@launch + val name = runCatching { api.group().getGroupById(serverGroupId).await() } + .getOrNull()?.name ?: serverGroupId + + broadcast( + Notification( + renderer.group(message, name), + NotifyPermissions.GROUP_CREATED + ) + ) + } + } + ) + + subscriptions.add( + api.event().group().onDeleted { event -> + val name = event.name + + scope.launch { + val message = config().notifications.group?.deleted ?: return@launch + broadcast( + Notification( + renderer.group(message, name), + NotifyPermissions.GROUP_DELETED + ) + ) + } + } + ) + } + + fun stop() { + subscriptions.forEach { it.close() } + subscriptions.clear() + lastSentState.clear() + scope.cancel() + } + + private fun broadcast(notification: Notification) { + recipients() + .filter { sender -> sender.uniqueId?.let(settings::isEnabled) ?: true } + .forEach(notification::sendTo) + } + +} diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/Notification.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/Notification.kt similarity index 83% rename from notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/Notification.kt rename to notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/Notification.kt index ebdf639..1a23dc6 100644 --- a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/Notification.kt +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/Notification.kt @@ -1,5 +1,6 @@ -package app.simplecloud.plugin.notify.shared +package app.simplecloud.plugin.notify.shared.notification +import app.simplecloud.plugin.notify.shared.command.CloudSender import net.kyori.adventure.text.Component class Notification( @@ -21,4 +22,4 @@ class Notification( private fun canReceive(sender: CloudSender): Boolean { return permissions.isEmpty() || permissions.any { sender.hasPermission(it) } } -} +} \ No newline at end of file diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/NotificationRenderer.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/NotificationRenderer.kt new file mode 100644 index 0000000..c57e8de --- /dev/null +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/NotificationRenderer.kt @@ -0,0 +1,81 @@ +package app.simplecloud.plugin.notify.shared.notification + +import app.simplecloud.api.server.Server +import app.simplecloud.plugin.notify.shared.config.MessageConfig +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Date +import java.util.Locale + +class NotificationRenderer( + private val config: () -> MessageConfig, +) { + + private val serverHoverPlaceholder = "notifications.hover.server" + private val groupHoverPlaceholder = "notifications.hover.group" + + private val zone: ZoneId = ZoneId.systemDefault() + + private val formatter: DateTimeFormatter + get() = DateTimeFormatter.ofPattern(config().format.date, Locale.getDefault()).withZone(zone) + + fun server(server: Server, message: String, vararg tagResolver: TagResolver): Component { + return parse( + replaceHover(message, serverHoverPlaceholder, config().notifications.hover.server), + Placeholder.unparsed("ip", server.ip ?: "N/A"), + Placeholder.unparsed("port", server.port.toString()), + Placeholder.unparsed("group", server.serverBase.name ?: "N/A"), + Placeholder.unparsed("server", server.serverBase.name ?: "N/A"), + Placeholder.unparsed("uuid", server.serverId), + Placeholder.unparsed("id", server.numericalId.takeIf { it != -1 }?.let { " $it" } ?: ""), + Placeholder.unparsed("create_date", formatTimestamp(runCatching { server.createdAt }.getOrNull())), + Placeholder.unparsed("time", formatTimestamp(Instant.now())), + Placeholder.unparsed("updated", formatTimestamp(runCatching { server.updatedAt }.getOrNull())), + Placeholder.unparsed("players", server.playerCount.toString()), + Placeholder.unparsed("max", server.maxPlayers.toString()), + *tagResolver + ) + } + + fun group(message: String, groupName: String?): Component { + return parse( + replaceHover(message, groupHoverPlaceholder, config().notifications.hover.group), + Placeholder.unparsed("group", groupName ?: "N/A"), + Placeholder.unparsed("time", formatTimestamp(Instant.now())) + ) + } + + fun message(message: String, vararg tagResolver: TagResolver): Component { + return parse(message, *tagResolver) + } + + private fun parse(message: String, vararg tagResolver: TagResolver): Component { + return config().msg(message, *tagResolver) + } + + private fun replaceHover(message: String, placeholder: String, hoverMessage: String): String { + return message.replace( + "'>", + "" + ) + } + + private fun formatTimestamp(value: Any?): String { + if (value == null) return "N/A" + + val instant = when (value) { + is Instant -> value + is Date -> value.toInstant() + is Long -> Instant.ofEpochMilli(value) + is String -> runCatching { Instant.parse(value) }.getOrNull() + else -> null + } ?: return "N/A" + + return formatter.format(instant) + } + +} \ No newline at end of file diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/NotificationSettings.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/NotificationSettings.kt new file mode 100644 index 0000000..2bd1cf9 --- /dev/null +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/notification/NotificationSettings.kt @@ -0,0 +1,38 @@ +package app.simplecloud.plugin.notify.shared.notification + +import app.simplecloud.api.player.PlayerApi +import kotlinx.coroutines.future.await +import java.util.UUID +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap + +class NotificationSettings( + private val api: PlayerApi, +) { + + private val property = "notify.enabled" + private val cache: MutableMap = ConcurrentHashMap() + + suspend fun load(id: UUID) { + val player = api.get(id).await() + cache[id] = player?.properties?.get(property)?.toBooleanStrictOrNull() ?: true + } + + fun unload(id: UUID) { + cache.remove(id) + } + + fun isEnabled(id: UUID): Boolean { + return cache[id] ?: true + } + + fun set(id: UUID, value: Boolean): CompletableFuture<*> { + cache[id] = value + return api.updatePlayerProperty(id, property, value.toString()) + } + + fun clear() { + cache.clear() + } + +} \ No newline at end of file diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/utilities/ConfigVersion.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/utilities/ConfigVersion.kt new file mode 100644 index 0000000..b79d5d9 --- /dev/null +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/utilities/ConfigVersion.kt @@ -0,0 +1,7 @@ +package app.simplecloud.plugin.notify.shared.utilities + +object ConfigVersion { + + const val VERSION = 1 + +} \ No newline at end of file diff --git a/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/utilities/NotifyPermissions.kt b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/utilities/NotifyPermissions.kt new file mode 100644 index 0000000..0f787e9 --- /dev/null +++ b/notify-shared/src/main/kotlin/app/simplecloud/plugin/notify/shared/utilities/NotifyPermissions.kt @@ -0,0 +1,25 @@ +package app.simplecloud.plugin.notify.shared.utilities + +import app.simplecloud.api.server.ServerState + +object NotifyPermissions { + + const val SERVER_STATE_PREFIX = "notify.state.server" + const val SERVER_STOPPED = "$SERVER_STATE_PREFIX.stopped" + const val LEGACY_SERVER_STOPPED = "$SERVER_STATE_PREFIX.deleted" + + const val GROUP_CREATED = "notify.state.group.created" + const val GROUP_DELETED = "notify.state.group.deleted" + + const val COMMAND_HELP = "simplecloud.command.notify.help" + const val COMMAND_RELOAD_CONFIG = "simplecloud.command.notify.reload.config" + const val COMMAND_TOGGLE_SELF = "simplecloud.command.notify.toggle.self" + const val COMMAND_TOGGLE_OTHER = "simplecloud.command.notify.toggle.other" + + val SERVER_STOPPED_ALIASES = setOf(SERVER_STOPPED, LEGACY_SERVER_STOPPED) + + fun serverStatePermission(state: ServerState): String { + return "$SERVER_STATE_PREFIX.${state.name.lowercase()}" + } + +} diff --git a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityCloudSender.kt b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityCloudSender.kt deleted file mode 100644 index d212c02..0000000 --- a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityCloudSender.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.simplecloud.plugin.notify.velocity - -import app.simplecloud.plugin.notify.shared.CloudSender -import com.velocitypowered.api.proxy.Player -import net.kyori.adventure.text.Component - -class VelocityCloudSender( - private val player: Player, -) : CloudSender { - - override fun sendMessage(message: Component) { - player.sendMessage(message) - } - - override fun hasPermission(permission: String): Boolean { - return player.hasPermission(permission) - } -} diff --git a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt index 7f1ec24..8f99787 100644 --- a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt +++ b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt @@ -1,7 +1,9 @@ package app.simplecloud.plugin.notify.velocity -import app.simplecloud.plugin.notify.shared.CloudSender import app.simplecloud.plugin.notify.shared.NotifyPlugin +import app.simplecloud.plugin.notify.shared.command.CloudSender +import app.simplecloud.plugin.notify.velocity.command.VelocityCloudSender +import app.simplecloud.plugin.notify.velocity.listener.NotifyConnectionListener import com.google.inject.Inject import com.velocitypowered.api.event.Subscribe import com.velocitypowered.api.event.proxy.ProxyInitializeEvent @@ -10,6 +12,9 @@ import com.velocitypowered.api.plugin.Dependency import com.velocitypowered.api.plugin.Plugin import com.velocitypowered.api.plugin.annotation.DataDirectory import com.velocitypowered.api.proxy.ProxyServer +import org.incendo.cloud.SenderMapper +import org.incendo.cloud.execution.ExecutionCoordinator +import org.incendo.cloud.velocity.VelocityCommandManager import java.nio.file.Path @Plugin( @@ -33,7 +38,12 @@ class VelocityNotifyPlugin @Inject constructor( @Subscribe fun onProxyInitialize(event: ProxyInitializeEvent) { - notifyPlugin = NotifyPlugin(dataDirectory, ::onlinePlayers) + val notify = NotifyPlugin(dataDirectory, ::onlinePlayers) + notify.start() + notify.registerCommands(createCommandManager()) + + notifyPlugin = notify + server.eventManager.register(this, NotifyConnectionListener(notify)) } @Subscribe @@ -42,6 +52,15 @@ class VelocityNotifyPlugin @Inject constructor( notifyPlugin = null } + private fun createCommandManager(): VelocityCommandManager { + return VelocityCommandManager( + server.pluginManager.ensurePluginContainer(this), + server, + ExecutionCoordinator.simpleCoordinator(), + SenderMapper.create(::VelocityCloudSender, VelocityCloudSender::source) + ) + } + private fun onlinePlayers(): Iterable { return server.allPlayers.map(::VelocityCloudSender) } diff --git a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/command/VelocityCloudSender.kt b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/command/VelocityCloudSender.kt new file mode 100644 index 0000000..c342604 --- /dev/null +++ b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/command/VelocityCloudSender.kt @@ -0,0 +1,26 @@ +package app.simplecloud.plugin.notify.velocity.command + +import app.simplecloud.plugin.notify.shared.command.CloudSender +import com.velocitypowered.api.command.CommandSource +import com.velocitypowered.api.proxy.Player +import net.kyori.adventure.text.Component +import java.util.UUID + +class VelocityCloudSender( + val source: CommandSource, +) : CloudSender { + + override val uniqueId: UUID? + get() = (source as? Player)?.uniqueId + + override val name: String + get() = (source as? Player)?.username ?: "CONSOLE" + + override fun sendMessage(message: Component) { + source.sendMessage(message) + } + + override fun hasPermission(permission: String): Boolean { + return source.hasPermission(permission) + } +} \ No newline at end of file diff --git a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/listener/NotifyConnectionListener.kt b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/listener/NotifyConnectionListener.kt new file mode 100644 index 0000000..8c54231 --- /dev/null +++ b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/listener/NotifyConnectionListener.kt @@ -0,0 +1,22 @@ +package app.simplecloud.plugin.notify.velocity.listener + +import app.simplecloud.plugin.notify.shared.NotifyPlugin +import com.velocitypowered.api.event.Subscribe +import com.velocitypowered.api.event.connection.DisconnectEvent +import com.velocitypowered.api.event.connection.PostLoginEvent + +class NotifyConnectionListener( + private val notify: NotifyPlugin +) { + + @Subscribe + fun onPostLogin(event: PostLoginEvent) { + notify.onPlayerJoin(event.player.uniqueId) + } + + @Subscribe + fun onDisconnect(event: DisconnectEvent) { + notify.onPlayerQuit(event.player.uniqueId) + } + +} \ No newline at end of file From 9d7090b3dde08cebbe54f27dfe1cfddb59fcc917 Mon Sep 17 00:00:00 2001 From: xXJanisXx Date: Wed, 22 Jul 2026 23:20:09 +0200 Subject: [PATCH 4/9] chore: use project version instead of a hardcoded one and bump to 1.0.0 --- build.gradle.kts | 10 +++++++++- gradle/libs.versions.toml | 4 +++- notify-bungeecord/src/main/resources/plugin.yml | 3 ++- notify-velocity/build.gradle.kts | 11 +++++++++++ .../plugin/notify/velocity/BuildConstants.kt.peb | 5 +++++ .../plugin/notify/velocity/VelocityNotifyPlugin.kt | 11 +++++------ 6 files changed, 35 insertions(+), 9 deletions(-) create mode 100644 notify-velocity/src/main/kotlin-templates/app/simplecloud/plugin/notify/velocity/BuildConstants.kt.peb diff --git a/build.gradle.kts b/build.gradle.kts index 43b143d..871e2d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { allprojects { group = "app.simplecloud.plugin" - version = "0.0.6" + version = "1.0.0" repositories { mavenCentral() @@ -59,4 +59,12 @@ subprojects { archiveFileName.set("${project.name}.jar") } + tasks.processResources { + filesMatching("plugin.yml") { + expand( + "version" to project.version + ) + } + } + } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce6b7e9..a4513e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,6 +4,7 @@ kotlinx-coroutines = "1.11.0" shadow = "9.5.1" minotaur = "2.9.0" +blossom = "2.2.0" simplecloud-api = "0.1.0-platform.40" simplecloud-plugin = "0.0.1-platform.1778966459069-b1e45e2" @@ -47,4 +48,5 @@ configurate = ["configurate-kotlin", "configurate-yaml"] [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } -minotaur = { id = "com.modrinth.minotaur", version.ref = "minotaur" } \ No newline at end of file +minotaur = { id = "com.modrinth.minotaur", version.ref = "minotaur" } +blossom = { id = "net.kyori.blossom", version.ref = "blossom" } \ No newline at end of file diff --git a/notify-bungeecord/src/main/resources/plugin.yml b/notify-bungeecord/src/main/resources/plugin.yml index ca5916e..1b4f158 100644 --- a/notify-bungeecord/src/main/resources/plugin.yml +++ b/notify-bungeecord/src/main/resources/plugin.yml @@ -1,6 +1,7 @@ name: notify-bungeecord -version: 1.0-SNAPSHOT +version: ${version} author: rlqu main: app.simplecloud.plugin.notify.bungeecord.BungeeCordNotifyPlugin +description: An ingame plugin that notifies about v3 server changes website: 'https://github.com/simplecloudapp/notify-plugin' depends: [simplecloud-api] \ No newline at end of file diff --git a/notify-velocity/build.gradle.kts b/notify-velocity/build.gradle.kts index 5b92c5b..bd2e7e6 100644 --- a/notify-velocity/build.gradle.kts +++ b/notify-velocity/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("kapt") alias(libs.plugins.minotaur) + alias(libs.plugins.blossom) } dependencies { @@ -44,4 +45,14 @@ modrinth { loaders.add("velocity") changelog.set("https://docs.simplecloud.app/changelog") syncBodyFrom.set(rootProject.file("README.md").readText()) +} + +sourceSets { + main { + blossom { + kotlinSources { + property("version", project.version.toString()) + } + } + } } \ No newline at end of file diff --git a/notify-velocity/src/main/kotlin-templates/app/simplecloud/plugin/notify/velocity/BuildConstants.kt.peb b/notify-velocity/src/main/kotlin-templates/app/simplecloud/plugin/notify/velocity/BuildConstants.kt.peb new file mode 100644 index 0000000..ceef50a --- /dev/null +++ b/notify-velocity/src/main/kotlin-templates/app/simplecloud/plugin/notify/velocity/BuildConstants.kt.peb @@ -0,0 +1,5 @@ +package app.simplecloud.plugin.notify.velocity + +object BuildConstants { + const val VERSION = "{{ version }}" +} \ No newline at end of file diff --git a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt index 8f99787..36bb99f 100644 --- a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt +++ b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt @@ -20,14 +20,13 @@ import java.nio.file.Path @Plugin( id = "notify-velocity", name = "notify-velocity", - version = "1.0-SNAPSHOT", + version = BuildConstants.VERSION, authors = ["rlqu"], + description = "An ingame plugin that notifies about v3 server changes", + url = "https://github.com/simplecloudapp/notify-plugin", dependencies = [ - Dependency( - id = "simplecloud-api" - ) - ], - url = "https://github.com/simplecloudapp/notify-plugin" + Dependency("simplecloud-api") + ] ) class VelocityNotifyPlugin @Inject constructor( @param:DataDirectory private val dataDirectory: Path, From 1beb5e88cfc83094bbe0ff12f7d1b073571a1c18 Mon Sep 17 00:00:00 2001 From: xXJanisXx Date: Wed, 22 Jul 2026 23:22:28 +0200 Subject: [PATCH 5/9] refactor: rename plugin from notify-platform to simplecloud-notify --- notify-bungeecord/src/main/resources/plugin.yml | 2 +- .../plugin/notify/velocity/VelocityNotifyPlugin.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/notify-bungeecord/src/main/resources/plugin.yml b/notify-bungeecord/src/main/resources/plugin.yml index 1b4f158..0929ec4 100644 --- a/notify-bungeecord/src/main/resources/plugin.yml +++ b/notify-bungeecord/src/main/resources/plugin.yml @@ -1,4 +1,4 @@ -name: notify-bungeecord +name: simplecloud-notify version: ${version} author: rlqu main: app.simplecloud.plugin.notify.bungeecord.BungeeCordNotifyPlugin diff --git a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt index 36bb99f..0fcf3e1 100644 --- a/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt +++ b/notify-velocity/src/main/kotlin/app/simplecloud/plugin/notify/velocity/VelocityNotifyPlugin.kt @@ -18,8 +18,8 @@ import org.incendo.cloud.velocity.VelocityCommandManager import java.nio.file.Path @Plugin( - id = "notify-velocity", - name = "notify-velocity", + id = "simplecloud-notify", + name = "simplecloud-notify", version = BuildConstants.VERSION, authors = ["rlqu"], description = "An ingame plugin that notifies about v3 server changes", From 8d6d78f0863e7471b735dc608327d5a66798dbbd Mon Sep 17 00:00:00 2001 From: xXJanisXx Date: Thu, 23 Jul 2026 05:53:10 +0200 Subject: [PATCH 6/9] feat: update readme --- LICENSE | 2 +- README.md | 138 +++++++++++------- .../plugin/notify/shared/config/Config.kt | 2 +- 3 files changed, 86 insertions(+), 56 deletions(-) diff --git a/LICENSE b/LICENSE index 261eeb9..6e80513 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright [2024-2026] [SimpleCloud] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 2c977e0..838afd0 100644 --- a/README.md +++ b/README.md @@ -37,60 +37,90 @@ Keep your staff informed about server state changes with real-time notifications Here you can see the configuration file for the plugin. All possible options are explained in the comments. ```yaml -# The date format pattern the notify plugin uses. -# For more information, see the official Java documentation: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/text/SimpleDateFormat.html -date-format: dd.MM.yyyy HH:mm:ss - -# Filter server states to notify about. -server-state-filter: - # server-state obviously sets the server state to filter for -- server-state: STARTING - # The message that is being sent to the permitted players goes here. - # Don't get irritated by the |- format, it's just a multiline string. You can also pass oneliners. - # Possible placeholders you can use here are: - # - : The group name the server belongs to - # - : The numerical id of the server - # - : The UUID of the server - # - : The state of the server after the update - # - : The IP that the server is running on - # - : The port that the server is bound to - # - : The amount of players currently online on the server - # - : The maximum amount of players that can join the server - # - : The date of the last update of the server - # - : The date of the initial update (creation) of the server - message: |- - Information of - Timestamp: - State: - Server-IP: - Port: - Players: /'>Server updated its state to . - # Users will need this permission to receive the message. Leave it empty (just '') to allow everyone to receive it. - permission: notify.receive.state-changed.starting - - # You can add more server states to filter like this: -- server-state: AVAILABLE - # To design your own messages, we recommend using the minimessage web-ui: https://webui.advntr.dev/ - message: |- - Information of - Timestamp: - State: - Server-IP: - Port: - Players: /'>Server updated its state to . - # The default permission is notify.receive.state-changed.[server-state] - permission: notify.receive.state-changed.available - -- server-state: STOPPING - message: |- - Information of - Timestamp: - State: - Server-IP: - Port: - Players: /'>Server updated its state to . - # Here you can see that everyone will receive the message, as the permission is empty. - permission: '' +version: 1 + +# ─────────────────────────────────────────────────────────────────────────────── +# Format Settings +# ─────────────────────────────────────────────────────────────────────────────── +format: + date: "dd.MM.yyyy HH:mm:ss" + +# ─────────────────────────────────────────────────────────────────────────────── +# Variables +# Reusable variables that can be used throughout the messages. +# Usage: will be replaced with the defined value. +# ─────────────────────────────────────────────────────────────────────────────── +variables: + prefix: "" + +# ─────────────────────────────────────────────────────────────────────────────── +# Command Messages +# ─────────────────────────────────────────────────────────────────────────────── +command: + help: + title: " <#0EA5E9>SimpleCloud Notify commands" + entry: "<#E2E8F0>" + empty: " <#F59E0B>No notify commands are available for you." + + usage: + invalid: " <#DC2626>Use <#F8FAFC> <#DC2626>instead." + entry: "<#E2E8F0>" + invalid-state: " <#DC2626>Use <#F8FAFC>enable <#DC2626>or <#F8FAFC>disable<#DC2626>." + invalid-toggle-value: " <#DC2626>Use <#F8FAFC>true <#DC2626>or <#F8FAFC>false<#DC2626>." + missing-player: " <#DC2626>Missing player <#F8FAFC><#DC2626>." + + permission: + denied: " <#DC2626>You do not have permission to use this command." + + notify: + enabled: " <#A3E635>Server notifications are now <#F8FAFC>enabled<#A3E635>." + disabled: " <#A3E635>Server notifications are now <#F8FAFC>disabled<#A3E635>." + already-enabled: " <#F59E0B>Server notifications are already <#F8FAFC>enabled<#F59E0B>." + already-disabled: " <#F59E0B>Server notifications are already <#F8FAFC>disabled<#F59E0B>." + + set: + enabled: " <#A3E635>Server notifications are now <#F8FAFC>enabled <#A3E635>for :true> <#F8FAFC><#A3E635>." + disabled: " <#A3E635>Server notifications are now <#F8FAFC>disabled <#A3E635>for :true> <#F8FAFC><#A3E635>." + already-enabled: " <#F59E0B>Server notifications are already <#F8FAFC>enabled <#F59E0B>for :true> <#F8FAFC><#F59E0B>." + already-disabled: " <#F59E0B>Server notifications are already <#F8FAFC>disabled <#F59E0B>for :true> <#F8FAFC><#F59E0B>." + + reload: + config: + success: " <#A3E635>Notify configuration was reloaded." + failed: " <#DC2626>Notify configuration could not be reloaded." + + error: + player-not-found: " <#DC2626>Player <#F8FAFC> <#DC2626>was not found." + player-not-online: " <#DC2626>Player <#F8FAFC> <#DC2626>is not online." + only-players: " <#DC2626>This command can only be used by players." + storage-unavailable: " <#DC2626>Notify settings are currently unavailable." + internal: " <#DC2626>An internal error occurred. Try again later." + +# ─────────────────────────────────────────────────────────────────────────────── +# Notifications +# Notification messages for different server and group states. +# +# NOTE: If you delete a state entry, the corresponding notification will not be sent. +# ─────────────────────────────────────────────────────────────────────────────── +notifications: + hover: + server: |- + <#E2E8F0>Server information +
<#94A3B8>Time<#475569>: <#E2E8F0>