From 48b22c9938a185484124b23db3e33928a64a5189 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Sat, 26 Jun 2021 11:06:34 -0500 Subject: [PATCH 01/31] Update to Kotlin 1.5.20 --- build.gradle.kts | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a86a2e4..7e62dff 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,20 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.5.10" + kotlin("jvm") version "1.5.20" + //id("com.github.johnrengelman.shadow") version "7.0.0" // Only for fat jars + //application // Only for fat jars java } +/* +// Only for fat jars +application { + mainClass.set("com.hoshikurama.github.ticketmanager.TicketManager") +} + + */ + group = "com.hoshikurama.github" version = "4.1.0" @@ -20,13 +30,36 @@ dependencies { compileOnly("io.papermc.paper:paper-api:1.17-R0.1-SNAPSHOT") compileOnly("com.github.MilkBowl:VaultAPI:1.7") implementation("joda-time:joda-time:2.10.10") - implementation(kotlin("stdlib", version = "1.5.10")) + implementation(kotlin("stdlib", version = "1.5.20")) implementation("com.github.seratch:kotliquery:1.3.1") implementation("com.zaxxer:HikariCP:4.0.3") implementation("mysql:mysql-connector-java:8.0.25") implementation("org.xerial:sqlite-jdbc:3.34.0") } +// Only for fat jars +/* +tasks { + named("shadowJar") { + dependencies { + include(dependency("com.zaxxer:HikariCP:4.0.3")) + include(dependency("mysql:mysql-connector-java:8.0.25")) + include(dependency("org.xerial:sqlite-jdbc:3.34.0")) + include(dependency("org.jetbrains.kotlin:kotlin-stdlib:1.5.20")) + include(dependency("com.github.seratch:kotliquery:1.3.1")) + include(dependency("joda-time:joda-time:2.10.10")) + } + + relocate("com.zaxxer.hikari","com.hoshikurama.github.ticketmanager.shaded.zaxxerHikari") + relocate("com.mysql","com.hoshikurama.github.ticketmanager.shaded.mysqlDrivers") + relocate("org.sqlite","com.hoshikurama.github.ticketmanager.shaded.sqliteDrivers") + relocate("kotlin","com.hoshikurama.github.ticketmanager.shaded.kotlin-stdlib") + relocate("kotliquery","com.hoshikurama.github.ticketmanager.shaded.kotliquery") + relocate("org.joda","com.hoshikurama.github.ticketmanager.shaded.joda-time") + } +} + */ + tasks.withType { kotlinOptions.jvmTarget = "16" } \ No newline at end of file From bfb538bba3c1b2d40ba5c0227e7cb563d655f831 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Sat, 26 Jun 2021 11:06:48 -0500 Subject: [PATCH 02/31] Update to Kotlin 1.5.20 --- src/main/resources/plugin.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 89bf9ec..fc33fee 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -8,7 +8,7 @@ libraries: - com.zaxxer:HikariCP:4.0.3 - mysql:mysql-connector-java:8.0.25 - org.xerial:sqlite-jdbc:3.34.0 - - org.jetbrains.kotlin:kotlin-stdlib:1.5.10 + - org.jetbrains.kotlin:kotlin-stdlib:1.5.20 - com.github.seratch:kotliquery:1.3.1 - joda-time:joda-time:2.10.10 commands: From b891d05e2a155be7a7b3eabca26e29e2dfc7c9f5 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Sun, 27 Jun 2021 12:53:39 -0500 Subject: [PATCH 03/31] Fixed bug where listassigned didn't create one line space between header and last chat --- src/main/resources/languages/Locales/en_CA.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/languages/Locales/en_CA.yml b/src/main/resources/languages/Locales/en_CA.yml index 3ed6951..cfc7aee 100644 --- a/src/main/resources/languages/Locales/en_CA.yml +++ b/src/main/resources/languages/Locales/en_CA.yml @@ -104,7 +104,7 @@ Command_DeepView: 'deepview' # # List Format: ListFormat_Header: '%nl%%CC%[TicketManager]&f Viewing All Open Tickets:' -ListFormat_AssignedHeader: '%CC%[TicketManager]&f Viewing Open Tickets Assigned to You:' +ListFormat_AssignedHeader: '%nl%%CC%[TicketManager]&f Viewing Open Tickets Assigned to You:' ListFormat_Entry: '%priorityCC%[%ID%] &8[%CC%%creator% &8-> %CC%%assign%&8]&f %comment%' # # Clickable Text: From f4caae150f080fe132576da0e4e7b91afa2872c2 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:14:18 -0500 Subject: [PATCH 04/31] Saving 5.0.0 Progress --- .gitignore | 3 +- Paper/build.gradle.kts | 42 + .../github/ticketmanager/paper/Globals.kt | 31 + .../paper/MetricsKotlinBukkit.kt | 19 +- .../paper/TicketManagerPlugin.kt | 229 +++ .../ticketmanager/paper/events/Commands.kt | 17 + .../ticketmanager/paper/events/PlayerJoin.kt | 73 + .../ticketmanager/paper/events/TabComplete.kt | 17 + build.gradle.kts | 62 +- common/build.gradle.kts | 24 + .../ticketmanager/common}/Localization.kt | 64 +- .../ticketmanager/common/Miscellaneous.kt | 33 + .../ticketmanager/common/PluginState.kt | 116 ++ .../common/databases/Database.kt | 47 + .../ticketmanager/common/databases/MySQL.kt | 123 ++ .../ticketmanager/common/databases/SQLite.kt | 117 ++ .../common/ticket/BasicTicket.kt | 37 + .../ticketmanager/common/ticket/Ticket.kt | 50 + .../src/main/resources/locales}/Example.yml | 0 .../src/main/resources/locales}/en_CA.yml | 0 settings.gradle.kts | 7 +- .../github/ticketmanager/Globals.kt | 199 --- .../github/ticketmanager/PluginState.kt | 134 -- .../ticketmanager/TicketManagerPlugin.kt | 111 -- .../ticketmanager/databases/Database.kt | 43 - .../github/ticketmanager/databases/MySQL.kt | 413 ------ .../github/ticketmanager/databases/SQLite.kt | 417 ------ .../ticketmanager/events/CommandPipeline.kt | 1291 ----------------- .../github/ticketmanager/events/PlayerJoin.kt | 54 - .../ticketmanager/events/TabCompletion.kt | 292 ---- .../github/ticketmanager/ticket/Ticket.kt | 76 - .../ticketmanager/ticket/TicketHandler.kt | 46 - src/main/resources/config.yml | 86 -- src/main/resources/plugin.yml | 240 --- 34 files changed, 1012 insertions(+), 3501 deletions(-) create mode 100644 Paper/build.gradle.kts create mode 100644 Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/Globals.kt rename src/main/kotlin/com/hoshikurama/github/ticketmanager/MetricsKotlin.kt => Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/MetricsKotlinBukkit.kt (98%) create mode 100644 Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/TicketManagerPlugin.kt create mode 100644 Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/Commands.kt create mode 100644 Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/PlayerJoin.kt create mode 100644 Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/TabComplete.kt create mode 100644 common/build.gradle.kts rename {src/main/kotlin/com/hoshikurama/github/ticketmanager => common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common}/Localization.kt (91%) create mode 100644 common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Miscellaneous.kt create mode 100644 common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/PluginState.kt create mode 100644 common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/Database.kt create mode 100644 common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/MySQL.kt create mode 100644 common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/SQLite.kt create mode 100644 common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/BasicTicket.kt create mode 100644 common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/Ticket.kt rename {src/main/resources/languages/Locales => common/src/main/resources/locales}/Example.yml (100%) rename {src/main/resources/languages/Locales => common/src/main/resources/locales}/en_CA.yml (100%) delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/Globals.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/PluginState.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/TicketManagerPlugin.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/Database.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/MySQL.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/SQLite.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/events/CommandPipeline.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/events/PlayerJoin.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/events/TabCompletion.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/ticket/Ticket.kt delete mode 100644 src/main/kotlin/com/hoshikurama/github/ticketmanager/ticket/TicketHandler.kt delete mode 100644 src/main/resources/config.yml delete mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore index 12b5f23..1ffc0ef 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ src/main/Stuff.txt gradle.properties gradlew gradlew.bat -settings.gradle.kts \ No newline at end of file +common/settings.gradle.kts +/common/common.iml diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts new file mode 100644 index 0000000..29b54e1 --- /dev/null +++ b/Paper/build.gradle.kts @@ -0,0 +1,42 @@ +plugins { + id("com.github.johnrengelman.shadow") version "7.0.0" + kotlin("jvm") + java + application +} + +application { + mainClass.set("com.hoshikurama.github.mcwandsframework.MCWandsFramework") +} + +repositories { + mavenCentral() + maven { url = uri("https://papermc.io/repo/repository/maven-public/") } + maven { url = uri("https://jitpack.io") } +} + +dependencies { + compileOnly("io.papermc.paper:paper-api:1.17-R0.1-SNAPSHOT") + implementation(kotlin("stdlib", version = "1.5.20")) + implementation("mysql:mysql-connector-java:8.0.25") + implementation("org.xerial:sqlite-jdbc:3.34.0") + implementation("com.github.jasync-sql:jasync-mysql:1.1.6") + implementation("com.github.seratch:kotliquery:1.3.1") + implementation("com.github.HoshiKurama:KyoriComponentDSL:1.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0") + implementation("net.kyori:adventure-api:4.8.1") + implementation("net.kyori:adventure-extra-kotlin:4.8.1") + implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") + implementation("org.yaml:snakeyaml:1.29") + implementation("joda-time:joda-time:2.10.10") + compileOnly("com.github.MilkBowl:VaultAPI:1.7") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0") + implementation(project(":common")) +} + +tasks { + named("shadowJar") { + archiveBaseName.set("TicketManager-Paper-5.0.0.jar") + } +} \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/Globals.kt b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/Globals.kt new file mode 100644 index 0000000..a3192ac --- /dev/null +++ b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/Globals.kt @@ -0,0 +1,31 @@ +package com.hoshikurama.github.ticketmanager.paper + +import com.hoshikurama.github.ticketmanager.common.PluginState +import com.hoshikurama.github.ticketmanager.common.TMLocale +import kotlinx.coroutines.Deferred +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.ChatColor +import org.bukkit.entity.Player +import java.util.logging.Level + +fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) + +internal val mainPlugin: TicketManagerPlugin + get() = TicketManagerPlugin.plugin + +internal val pluginState: Deferred + get() = mainPlugin.configState + + +suspend fun pushMassNotify(permission: String, localeMsg: suspend (TMLocale) -> Component) { + Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.await().localeHandler.consoleLocale)) + + Bukkit.getOnlinePlayers().asSequence() + .filter { it.has(permission) } + .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } +} + +internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) + +internal suspend fun Player.toTMLocale() = pluginState.await().localeHandler.getOrDefault(locale().toString()) \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/MetricsKotlin.kt b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/MetricsKotlinBukkit.kt similarity index 98% rename from src/main/kotlin/com/hoshikurama/github/ticketmanager/MetricsKotlin.kt rename to Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/MetricsKotlinBukkit.kt index 14025d1..6a826a2 100644 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/MetricsKotlin.kt +++ b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/MetricsKotlinBukkit.kt @@ -1,4 +1,4 @@ -package com.hoshikurama.github.ticketmanager +package com.hoshikurama.github.ticketmanager.paper import org.bukkit.Bukkit import org.bukkit.configuration.file.YamlConfiguration @@ -809,20 +809,3 @@ class Metrics(plugin: JavaPlugin, serviceId: Int) { } } -internal class UpdateChecker(private val resourceID: Int) { - internal fun getLatestVersion(): String? { - var inputStream: InputStream? = null - var scanner: Scanner? = null - - return try { - inputStream = URL("https://api.spigotmc.org/legacy/update.php?resource=$resourceID").openStream() - scanner = Scanner(inputStream!!) - if (scanner.hasNext()) scanner.next() else null - } - catch (ignored: Exception) { null } - finally { - inputStream?.close() - scanner?.close() - } - } -} \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/TicketManagerPlugin.kt new file mode 100644 index 0000000..e3a569e --- /dev/null +++ b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/TicketManagerPlugin.kt @@ -0,0 +1,229 @@ +package com.hoshikurama.github.ticketmanager.paper + +import com.github.hoshikurama.componentDSL.formattedContent +import com.github.shynixn.mccoroutine.* +import com.hoshikurama.github.ticketmanager.common.* +import com.hoshikurama.github.ticketmanager.common.databases.Database +import com.hoshikurama.github.ticketmanager.common.databases.MySQL +import com.hoshikurama.github.ticketmanager.common.databases.SQLite +import com.hoshikurama.github.ticketmanager.paper.events.Commands +import com.hoshikurama.github.ticketmanager.paper.events.PlayerJoin +import com.hoshikurama.github.ticketmanager.paper.events.TabComplete +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import net.kyori.adventure.extra.kotlin.text +import net.milkbowl.vault.permission.Permission +import org.bukkit.Bukkit +import java.io.File + +class TicketManagerPlugin : SuspendingJavaPlugin() { + @OptIn(ObsoleteCoroutinesApi::class) + private val singleOffThread = newSingleThreadContext("SingleOffThread") + + internal val pluginLocked = NonBlockingSync(singleOffThread, true) + internal lateinit var perms: Permission private set + internal lateinit var configState: Deferred + + internal val ticketCountMetrics = NonBlockingSync(singleOffThread, 0) + private lateinit var metrics: Metrics + + + companion object { lateinit var plugin: TicketManagerPlugin } + init { plugin = this } + + + override fun onEnable() { + + // Find Vault plugin + server.servicesManager.getRegistration(Permission::class.java)?.provider + ?.let { perms = it } + ?: this.pluginLoader.disablePlugin(this) + + // Launch Metrics + launch { + metrics = Metrics(plugin, metricsKey) + metrics.addCustomChart( + Metrics.SingleLineChart("tickets_made") { + runBlocking { + val ticketCount = ticketCountMetrics.check() + ticketCountMetrics.set(0) + ticketCount + } + } + ) + } + + // Launches PluginState initialisation + launchAsync { loadPlugin() } + + // Register Event + server.pluginManager.registerSuspendingEvents(PlayerJoin(), plugin) + + // Creates task timers + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, Runnable { + launchAsync { + if (pluginLocked.check()) return@launchAsync + + try { + val state = pluginState.await() + + // Mass Unread Notify + if (state.allowUnreadTicketUpdates) { + state.database.getTicketIDsWithUpdates() + .toList() + .groupBy({ it.first }, { it.second }) + .asSequence() + .mapNotNull { Bukkit.getPlayer(it.key)?.run { Pair(this, it.value) } } + .filter { it.first.has("ticketmanager.notify.unreadUpdates.scheduled") } + .forEach { + val template = if (it.second.size > 1) it.first.toTMLocale().notifyUnreadUpdateMulti + else it.first.toTMLocale().notifyUnreadUpdateSingle + val tickets = it.second.joinToString(", ") + + val sentMessage = template.replace("%num%", tickets) + it.first.sendMessage(text { formattedContent(sentMessage) }) + } + } + + // Open and Assigned Notify + val tickets = state.database.getOpenTickets() + .map { it.assignedTo } + .toList() + + Bukkit.getOnlinePlayers().asSequence() + .filter { it.has("ticketmanager.notify.openTickets.scheduled") } + .forEach { p -> + val open = tickets.size.toString() + val assigned = tickets.asSequence() + .filterNotNull() + .filter { s -> + if (s.startsWith("::")) + perms.getPlayerGroups(p) + .asSequence() + .map { "::$it" } + .filter { it == s } + .any() + else s == p.name + }.count().toString() + + val sentMessage = p.toTMLocale().notifyOpenAssigned + .replace("%open%", open) + .replace("%assigned%", assigned) + p.sendMessage(text { formattedContent(sentMessage) }) + + } + + launch { state.cooldowns.filterMapAsync() } + + } catch (e: Exception) { + e.printStackTrace() + //postModifiedStacktrace(e) + } + } + }, 100, 12000) + } + + internal suspend fun loadPlugin() = coroutineScope { + pluginLocked.set(true) + + // Builds instructions for plugin scope + configState = async { + + if (!File(plugin.dataFolder, "config.yml").exists()) { + plugin.saveDefaultConfig() + launch { + while (!(::configState.isInitialized)) + delay(100L) + pushMassNotify("ticketmanager.notify.warning") { text { formattedContent(it.warningsNoConfig) } } + } + } + + plugin.reloadConfig() + val config = plugin.config + + config.run { + val database: () -> Database? = { + val type = getString("Database_Mode", "SQLite")!! + .let { tryOrNull { Database.Type.valueOf(it) } ?: Database.Type.SQLite } + + when (type) { + Database.Type.MySQL -> MySQL( + getString("MySQL_Host")!!, + getString("MySQL_Port")!!, + getString("MySQL_DBName")!!, + getString("MySQL_Username")!!, + getString("MySQL_Password")!! + ) + Database.Type.SQLite -> SQLite() + } + } + + val cooldown: () -> PluginState.Cooldown? = { + PluginState.Cooldown( + getBoolean("Use_Cooldowns", false), + getLong("Cooldown_Time", 0L) + ) + } + + val localeHandler: suspend () -> LocaleHandler? = { + LocaleHandler.buildLocalesAsync( + getString("Colour_Code", "&3")!!, + getString("Preferred_Locale", "en_ca")!!, + getString("Console_Locale", "en_ca")!!, + getBoolean("Force_Locale", false) + ) + } + + val allowUnreadTicketUpdates: () -> Boolean? = { + getBoolean("Allow_Unread_Ticket_Updates", true) + } + + val checkForPluginUpdate: () -> Boolean? = { + getBoolean("Allow_UpdateChecking", false) + } + + val pluginVersion: () -> String = { + mainPlugin.description.version + } + + PluginState.createDeferredPluginState( + database, + cooldown, + localeHandler, + allowUnreadTicketUpdates, + checkForPluginUpdate, + pluginVersion, + ) + } + } + + val pluginState = configState.await() + + withContext(minecraftDispatcher) { + // Register events and commands + pluginState.localeHandler.getCommandBases().forEach { + plugin.getCommand(it)?.setSuspendingExecutor(Commands()) + mainPlugin.getCommand(it)?.setSuspendingTabCompleter(TabComplete()) + // Remember to register any keyword in plugin.yml + } + } + + launch { + if (pluginState.database.updateNeeded().await()) { + pluginState.database.updateDatabase() + } + mainPlugin.pluginLocked.set(false) + } + + + withContext(minecraftDispatcher) { + // Register events and commands + pluginState.localeHandler.getCommandBases().forEach { + plugin.getCommand(it)?.setSuspendingExecutor(Commands()) + mainPlugin.getCommand(it)?.setSuspendingTabCompleter(TabComplete()) + // Remember to register any keyword in plugin.yml + } + } + } +} \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/Commands.kt new file mode 100644 index 0000000..42bc871 --- /dev/null +++ b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/Commands.kt @@ -0,0 +1,17 @@ +package com.hoshikurama.github.ticketmanager.paper.events + +import com.github.shynixn.mccoroutine.SuspendingCommandExecutor +import org.bukkit.command.Command +import org.bukkit.command.CommandSender + +class Commands : SuspendingCommandExecutor { + + override suspend fun onCommand( + sender: CommandSender, + command: Command, + label: String, + args: Array + ): Boolean { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/PlayerJoin.kt b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/PlayerJoin.kt new file mode 100644 index 0000000..e20b42b --- /dev/null +++ b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/PlayerJoin.kt @@ -0,0 +1,73 @@ +package com.hoshikurama.github.ticketmanager.paper.events + +import com.github.hoshikurama.componentDSL.formattedContent +import com.github.shynixn.mccoroutine.asyncDispatcher +import com.hoshikurama.github.ticketmanager.paper.has +import com.hoshikurama.github.ticketmanager.paper.mainPlugin +import com.hoshikurama.github.ticketmanager.paper.pluginState +import com.hoshikurama.github.ticketmanager.paper.toTMLocale +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.kyori.adventure.extra.kotlin.text +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerJoinEvent + +class PlayerJoin : Listener { + + @EventHandler + suspend fun onPlayerJoin(event: PlayerJoinEvent) = coroutineScope { + if (mainPlugin.pluginLocked.check()) return@coroutineScope + + val player = event.player + + withContext(mainPlugin.asyncDispatcher) { + + //Plugin Update Checking + launch { + val pluginUpdateStatus = pluginState.await().pluginUpdateAvailable.await() + if (player.has("ticketmanager.notify.pluginUpdate") && pluginUpdateStatus != null) { + val sentMSG = player.toTMLocale().notifyPluginUpdate + .replace("%current%", pluginUpdateStatus.first) + .replace("%latest%", pluginUpdateStatus.second) + player.sendMessage(text { formattedContent(sentMSG) }) + } + } + + // Unread Updates + launch { + if (player.has("ticketmanager.notify.unreadUpdates.onJoin")) { + pluginState.await().database.getTicketIDsWithUpdates(player.uniqueId) + .toList() + .run { if (size == 0) null else this } + ?.run { + val template = if (size == 1) player.toTMLocale().notifyUnreadUpdateSingle + else player.toTMLocale().notifyUnreadUpdateMulti + val tickets = this.joinToString(", ") + + + val sentMSG = template.replace("%num%", tickets) + player.sendMessage(text { formattedContent(sentMSG) }) + } + } + } + + // View Open-Count and Assigned-Count Tickets + launch { + if (player.has("ticketmanager.notify.openTickets.onJoin")) { + val open = pluginState.await().database.getOpenTickets() + val assigned = pluginState.await().database.getOpenAssigned(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) + + val sentMSG = player.toTMLocale().notifyOpenAssigned + .replace("%open%", "${open.count()}") + .replace("%assigned%", "${assigned.count()}") + + player.sendMessage(text { formattedContent(sentMSG) }) + + } + } + } + } +} \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/TabComplete.kt b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/TabComplete.kt new file mode 100644 index 0000000..6c3697a --- /dev/null +++ b/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/TabComplete.kt @@ -0,0 +1,17 @@ +package com.hoshikurama.github.ticketmanager.paper.events + +import com.github.shynixn.mccoroutine.SuspendingTabCompleter +import org.bukkit.command.Command +import org.bukkit.command.CommandSender + +class TabComplete : SuspendingTabCompleter { + + override suspend fun onTabComplete( + sender: CommandSender, + command: Command, + alias: String, + args: Array + ): List { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 7e62dff..b59ad5d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,65 +1,23 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.5.20" - //id("com.github.johnrengelman.shadow") version "7.0.0" // Only for fat jars - //application // Only for fat jars - java + kotlin("jvm") version "1.5.20" + java } -/* -// Only for fat jars -application { - mainClass.set("com.hoshikurama.github.ticketmanager.TicketManager") -} - - */ - -group = "com.hoshikurama.github" -version = "4.1.0" - repositories { - mavenCentral() - maven { url = uri("https://papermc.io/repo/repository/maven-public/") } - maven { url = uri("https://oss.sonatype.org/content/groups/public/") } - maven { url = uri("https://jitpack.io") } - maven { url = uri("https://oss.sonatype.org/content/repositories/central") } + mavenCentral() } dependencies { - compileOnly("io.papermc.paper:paper-api:1.17-R0.1-SNAPSHOT") - compileOnly("com.github.MilkBowl:VaultAPI:1.7") - implementation("joda-time:joda-time:2.10.10") - implementation(kotlin("stdlib", version = "1.5.20")) - implementation("com.github.seratch:kotliquery:1.3.1") - implementation("com.zaxxer:HikariCP:4.0.3") - implementation("mysql:mysql-connector-java:8.0.25") - implementation("org.xerial:sqlite-jdbc:3.34.0") + implementation(kotlin("stdlib", version = "1.5.20")) } -// Only for fat jars -/* -tasks { - named("shadowJar") { - dependencies { - include(dependency("com.zaxxer:HikariCP:4.0.3")) - include(dependency("mysql:mysql-connector-java:8.0.25")) - include(dependency("org.xerial:sqlite-jdbc:3.34.0")) - include(dependency("org.jetbrains.kotlin:kotlin-stdlib:1.5.20")) - include(dependency("com.github.seratch:kotliquery:1.3.1")) - include(dependency("joda-time:joda-time:2.10.10")) - } - - relocate("com.zaxxer.hikari","com.hoshikurama.github.ticketmanager.shaded.zaxxerHikari") - relocate("com.mysql","com.hoshikurama.github.ticketmanager.shaded.mysqlDrivers") - relocate("org.sqlite","com.hoshikurama.github.ticketmanager.shaded.sqliteDrivers") - relocate("kotlin","com.hoshikurama.github.ticketmanager.shaded.kotlin-stdlib") - relocate("kotliquery","com.hoshikurama.github.ticketmanager.shaded.kotliquery") - relocate("org.joda","com.hoshikurama.github.ticketmanager.shaded.joda-time") - } -} - */ +subprojects { + group = "com.hoshikurama.github" + version = "5.0.0" -tasks.withType { - kotlinOptions.jvmTarget = "16" + tasks.withType { + kotlinOptions.jvmTarget = "16" + } } \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts new file mode 100644 index 0000000..d015eee --- /dev/null +++ b/common/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + kotlin("jvm") + java +} + +repositories { + mavenCentral() + maven { url = uri("https://jitpack.io") } +} + +dependencies { + implementation(kotlin("stdlib", version = "1.5.20")) + implementation("mysql:mysql-connector-java:8.0.25") + implementation("org.xerial:sqlite-jdbc:3.34.0") + implementation("com.github.jasync-sql:jasync-mysql:1.1.6") + implementation("com.github.seratch:kotliquery:1.3.1") + implementation("com.github.HoshiKurama:KyoriComponentDSL:1.0.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0") + implementation("net.kyori:adventure-api:4.8.1") + implementation("net.kyori:adventure-extra-kotlin:4.8.1") + implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") + implementation("org.yaml:snakeyaml:1.29") + implementation("joda-time:joda-time:2.10.10") +} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/Localization.kt b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Localization.kt similarity index 91% rename from src/main/kotlin/com/hoshikurama/github/ticketmanager/Localization.kt rename to common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Localization.kt index c056acc..582a273 100644 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/Localization.kt +++ b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Localization.kt @@ -1,37 +1,52 @@ -package com.hoshikurama.github.ticketmanager +package com.hoshikurama.github.ticketmanager.common -import net.md_5.bungee.api.ChatColor +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import org.yaml.snakeyaml.Yaml const val translationNotFound = " TNF " -class AllLocales(val colourCode: String, - preferredLocale: String, - consoleLocale1: String, - forceLocale: Boolean +class LocaleHandler( + val mainColourCode: String, + private val activeTypes: Map, + private val defaultType: TMLocale, + val consoleLocale: TMLocale, ) { - private val activeTypes: Map - private val defaultType: TMLocale - val consoleLocale: TMLocale - init { - val fallback = EnglishCanada(colourCode) - val supportedTypes = mapOf( - "en_ca" to fallback, - "en_us" to EnglishUS(colourCode), - "en_uk" to EnglishUK(colourCode) - ) + companion object { + suspend fun buildLocalesAsync( + mainColourCode: String, + preferredLocale: String, + console_Locale: String, + forceLocale: Boolean, + ): LocaleHandler = coroutineScope { + val fallback = async { TMLocale(mainColourCode, "en_CA") } + + val activeTypes = + if (forceLocale) mapOf() + else mapOf( + "en_ca" to fallback, + "en_us" to async { TMLocale(mainColourCode, "en_US") }, + "en_uk" to async { TMLocale(mainColourCode, "en_UK") } + ) + .mapValues { it.value.await() } - defaultType = supportedTypes.getOrDefault(preferredLocale.lowercase(), fallback) - consoleLocale = supportedTypes.getOrDefault(consoleLocale1.lowercase(), defaultType) - activeTypes = if (forceLocale) mapOf() else supportedTypes + val defaultType = activeTypes.getOrDefault(preferredLocale.lowercase(), fallback.await()) + val consoleLocale = activeTypes.getOrDefault(console_Locale.lowercase(), defaultType) + + LocaleHandler(mainColourCode, activeTypes, defaultType, consoleLocale) + } } fun getOrDefault(type: String) = activeTypes.getOrDefault(type.lowercase(), defaultType) fun getCommandBases() = if (activeTypes.isEmpty()) setOf(defaultType.commandBase) else activeTypes.map { it.value.commandBase }.toSet() } -sealed class TMLocale(colourCode: String, locale: String) { + +class TMLocale( + colourCode: String, + locale: String, +) { // View and Deep View Format val viewFormatHeader: String val viewFormatSep1: String @@ -225,12 +240,11 @@ sealed class TMLocale(colourCode: String, locale: String) { val helpSep: String init { - val inputStream = mainPlugin.getResource("languages/Locales/$locale.yml") + val inputStream = this::class.java.getResourceAsStream("locales/$locale.yml") val contents: Map = Yaml().load(inputStream) fun matchOrDefault(key: String) = contents[key] ?.replace("%CC%", colourCode) ?.replace("%nl%", "\n") - ?.let { ChatColor.translateAlternateColorCodes('&', it) } ?: translationNotFound viewFormatHeader = matchOrDefault("ViewFormat_Header") @@ -386,8 +400,4 @@ sealed class TMLocale(colourCode: String, locale: String) { searchLastClosedBy = matchOrDefault("Search_LastClosedBy") notifyPluginUpdate = matchOrDefault("Notify_Event_PluginUpdate") } -} - -class EnglishCanada(colourCode: String) : TMLocale(colourCode, "en_CA") -class EnglishUS(colourCode: String) : TMLocale(colourCode, "en_CA") // No words differ from en_CA -class EnglishUK(colourCode: String) : TMLocale(colourCode, "en_CA") // No words differ from en_CA \ No newline at end of file +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Miscellaneous.kt b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Miscellaneous.kt new file mode 100644 index 0000000..9b6e079 --- /dev/null +++ b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Miscellaneous.kt @@ -0,0 +1,33 @@ +package com.hoshikurama.github.ticketmanager.common + +import kotlinx.coroutines.withContext +import java.io.InputStream +import java.net.URL +import java.util.* +import kotlin.coroutines.CoroutineContext + +class UpdateChecker(private val resourceID: Int) { + fun getLatestVersion(): String? { + var inputStream: InputStream? = null + var scanner: Scanner? = null + + return try { + inputStream = URL("https://api.spigotmc.org/legacy/update.php?resource=$resourceID").openStream() + scanner = Scanner(inputStream!!) + if (scanner.hasNext()) scanner.next() else null + } + catch (ignored: Exception) { null } + finally { + inputStream?.close() + scanner?.close() + } + } +} + +class NonBlockingSync( + private val context: CoroutineContext, + private var t: T +) { + suspend fun check() = withContext(context) { t } + suspend fun set(v: T) = withContext(context) { t = v } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/PluginState.kt b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/PluginState.kt new file mode 100644 index 0000000..6bcb052 --- /dev/null +++ b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/PluginState.kt @@ -0,0 +1,116 @@ +package com.hoshikurama.github.ticketmanager.common + +import com.hoshikurama.github.ticketmanager.common.databases.Database +import com.hoshikurama.github.ticketmanager.common.databases.SQLite +import kotlinx.coroutines.* +import net.kyori.adventure.text.Component +import java.time.Instant +import java.util.* + +class PluginState( + val cooldowns: Cooldown, + val database: Database, + val localeHandler: LocaleHandler, + val allowUnreadTicketUpdates: Boolean, + val pluginUpdateAvailable: Deferred?> +) { + + companion object { + suspend inline fun createDeferredPluginState( + crossinline database: () -> Database?, + crossinline cooldown: () -> Cooldown?, + crossinline localeHandler: suspend () -> LocaleHandler?, + crossinline allowUnreadTicketUpdates: () -> Boolean?, + crossinline checkForPluginUpdate: () -> Boolean?, + crossinline pluginVersion: () -> String, + ) = coroutineScope { + + val deferredDatabase = async { tryOrDefault(database, SQLite()) } + val deferredCooldown = async { tryOrDefault(cooldown, Cooldown(false, 0)) } + val deferredAllowUnreadUpdates = async { tryOrDefault(allowUnreadTicketUpdates, true) } + + val deferredPluginUpdate = async { + val shouldCheck = checkForPluginUpdate() + if (shouldCheck == true) { + val curVersion = pluginVersion() + val latestVersion = UpdateChecker(91178).getLatestVersion() + .run { this ?: curVersion } + + val curVersSplit = curVersion.split(".").map(String::toInt) + val latestVersSplit = latestVersion.split(".").map(String::toInt) + + for (i in 0..latestVersSplit.lastIndex) { + if (curVersSplit[i] < latestVersSplit[i]) + return@async Pair(curVersion, latestVersion) + } + } + return@async null + } + + val deferredLocaleHandler = async { + tryOrDefaultSuspend(localeHandler, + LocaleHandler.buildLocalesAsync( + mainColourCode = "&3", + preferredLocale = "en_ca", + console_Locale = "en_ca", + forceLocale = false, + ) + ) + } + + PluginState( + cooldowns = deferredCooldown.await(), + database = deferredDatabase.await(), + localeHandler = deferredLocaleHandler.await(), + allowUnreadTicketUpdates = deferredAllowUnreadUpdates.await(), + pluginUpdateAvailable = deferredPluginUpdate + ) + } + } + + + class Cooldown( + private val enabled: Boolean, + private val duration: Long, + ) { + @OptIn(ObsoleteCoroutinesApi::class) + private val threadContext = newSingleThreadContext("Cooldown") //This WILL be replaced in the future + private val map = mutableMapOf() + + suspend fun checkAndSetAsync(uuid: UUID?): Boolean { + return withContext(threadContext) { + if (!enabled || uuid == null) return@withContext false + + val curTime = Instant.now().epochSecond + val applies = map[uuid]?.let { it <= curTime } ?: false + return@withContext if (applies) true + else map.put(uuid, duration + curTime).run { true } + } + } + + suspend fun filterMapAsync() { + withContext(threadContext) { + map.forEach { + if (it.value > Instant.now().epochSecond) + map.remove(it.key) + } + } + } + } +} + +inline fun tryOrDefault(attempted: () -> T?, default: T): T = + tryOrNull(attempted).run { this ?: default } + +inline fun tryOrNull(function: () -> T): T? = + try { function() } + catch (ignored: Exception) { null } + +suspend inline fun tryOrDefaultSuspend(crossinline attempted: suspend () -> T?, default: T): T = + tryOrNullSuspend(attempted).run { this ?: default } + +suspend inline fun tryOrNullSuspend(crossinline function: suspend () -> T): T? = coroutineScope { + try { function() } + catch (ignored: Exception) { null } +} + diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/Database.kt new file mode 100644 index 0000000..9e555f4 --- /dev/null +++ b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/Database.kt @@ -0,0 +1,47 @@ +package com.hoshikurama.github.ticketmanager.common.databases + +import com.hoshikurama.github.ticketmanager.common.ticket.BasicTicket +import com.hoshikurama.github.ticketmanager.common.ticket.Ticket +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.flow.Flow +import java.util.* + +interface Database { + val type: Type + + enum class Type { + MySQL, SQLite + } + + // Individual things + suspend fun getAssignmentOrNull(ticketID: Int): Deferred + suspend fun getCreatorUUIDOrNull(ticketID: Int): Deferred + suspend fun getTicketLocationOrNull(ticketID: Int): Deferred + suspend fun getPriorityOrNull(ticketID: Int): Deferred + suspend fun getStatusOrNull(ticketID: Int): Deferred + suspend fun getCreatorStatusUpdateOrNull(ticketID: Int): Deferred + suspend fun setAssignment(ticketID: Int, assignment: String?) + suspend fun setPriority(ticketID: Int, priority: Ticket.Priority) + suspend fun setStatus(ticketID: Int, status: Ticket.Status) + suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) + suspend fun buildBasicTicket(id: Int): BasicTicket? + + // More specific Ticket actions + suspend fun addAction(ticketID: Int, action: Ticket.Action) + suspend fun addTicket(ticket: Ticket, action: Ticket.Action): Deferred + suspend fun getOpenTickets(): Flow + suspend fun getOpenAssigned(assignment: String, groupAssignment: List): Flow + suspend fun getTicketOrNull(ID: Int): Deferred + suspend fun getTicketIDsWithUpdates(): Flow> + suspend fun getTicketIDsWithUpdates(uuid: UUID): Flow + suspend fun isValidID(ticketID: Int): Deferred + suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) + suspend fun searchDatabase(searchFunction: (Ticket) -> Boolean): Flow + + // Database Modifications + suspend fun closeDatabase() + suspend fun createDatabasesIfNeeded() + suspend fun migrateDatabase(to: Type) + suspend fun updateNeeded(): Deferred + suspend fun updateDatabase() +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/MySQL.kt b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/MySQL.kt new file mode 100644 index 0000000..dbab502 --- /dev/null +++ b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/MySQL.kt @@ -0,0 +1,123 @@ +package com.hoshikurama.github.ticketmanager.common.databases + +import com.hoshikurama.github.ticketmanager.common.ticket.BasicTicket +import com.hoshikurama.github.ticketmanager.common.ticket.Ticket +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.flow.Flow +import java.util.* + +class MySQL( + host: String, + port: String, + dbName: String, + username: String, + password: String, +) : Database { + override val type: Database.Type + get() = TODO("Not yet implemented") + + override suspend fun getAssignmentOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getCreatorUUIDOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getTicketLocationOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getPriorityOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getStatusOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getCreatorStatusUpdateOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun setAssignment(ticketID: Int, assignment: String?) { + TODO("Not yet implemented") + } + + override suspend fun setPriority(ticketID: Int, priority: Ticket.Priority) { + TODO("Not yet implemented") + } + + override suspend fun setStatus(ticketID: Int, status: Ticket.Status) { + TODO("Not yet implemented") + } + + override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { + TODO("Not yet implemented") + } + + override suspend fun buildBasicTicket(id: Int): BasicTicket? { + TODO("Not yet implemented") + } + + override suspend fun addAction(ticketID: Int, action: Ticket.Action) { + TODO("Not yet implemented") + } + + override suspend fun addTicket(ticket: Ticket, action: Ticket.Action): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getOpenTickets(): Flow { + TODO("Not yet implemented") + } + + override suspend fun getOpenAssigned(assignment: String, groupAssignment: List): Flow { + TODO("Not yet implemented") + } + + override suspend fun getTicketOrNull(ID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getTicketIDsWithUpdates(): Flow> { + TODO("Not yet implemented") + } + + override suspend fun getTicketIDsWithUpdates(uuid: UUID): Flow { + TODO("Not yet implemented") + } + + override suspend fun isValidID(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { + TODO("Not yet implemented") + } + + override suspend fun searchDatabase(searchFunction: (Ticket) -> Boolean): Flow { + TODO("Not yet implemented") + } + + override suspend fun closeDatabase() { + TODO("Not yet implemented") + } + + override suspend fun createDatabasesIfNeeded() { + TODO("Not yet implemented") + } + + override suspend fun migrateDatabase(to: Database.Type) { + TODO("Not yet implemented") + } + + override suspend fun updateNeeded(): Deferred { + TODO("Not yet implemented") + } + + override suspend fun updateDatabase() { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/SQLite.kt new file mode 100644 index 0000000..f3ae730 --- /dev/null +++ b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/SQLite.kt @@ -0,0 +1,117 @@ +package com.hoshikurama.github.ticketmanager.common.databases + +import com.hoshikurama.github.ticketmanager.common.ticket.BasicTicket +import com.hoshikurama.github.ticketmanager.common.ticket.Ticket +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.flow.Flow +import java.util.* + +class SQLite : Database { + override val type: Database.Type + get() = TODO("Not yet implemented") + + override suspend fun getAssignmentOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getCreatorUUIDOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getTicketLocationOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getPriorityOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getStatusOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getCreatorStatusUpdateOrNull(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun setAssignment(ticketID: Int, assignment: String?) { + TODO("Not yet implemented") + } + + override suspend fun setPriority(ticketID: Int, priority: Ticket.Priority) { + TODO("Not yet implemented") + } + + override suspend fun setStatus(ticketID: Int, status: Ticket.Status) { + TODO("Not yet implemented") + } + + override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { + TODO("Not yet implemented") + } + + override suspend fun buildBasicTicket(id: Int): BasicTicket? { + TODO("Not yet implemented") + } + + override suspend fun addAction(ticketID: Int, action: Ticket.Action) { + TODO("Not yet implemented") + } + + override suspend fun addTicket(ticket: Ticket, action: Ticket.Action): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getOpenTickets(): Flow { + TODO("Not yet implemented") + } + + override suspend fun getOpenAssigned(assignment: String, groupAssignment: List): Flow { + TODO("Not yet implemented") + } + + override suspend fun getTicketOrNull(ID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun getTicketIDsWithUpdates(): Flow> { + TODO("Not yet implemented") + } + + override suspend fun getTicketIDsWithUpdates(uuid: UUID): Flow { + TODO("Not yet implemented") + } + + override suspend fun isValidID(ticketID: Int): Deferred { + TODO("Not yet implemented") + } + + override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { + TODO("Not yet implemented") + } + + override suspend fun searchDatabase(searchFunction: (Ticket) -> Boolean): Flow { + TODO("Not yet implemented") + } + + override suspend fun closeDatabase() { + TODO("Not yet implemented") + } + + override suspend fun createDatabasesIfNeeded() { + TODO("Not yet implemented") + } + + override suspend fun migrateDatabase(to: Database.Type) { + TODO("Not yet implemented") + } + + override suspend fun updateNeeded(): Deferred { + TODO("Not yet implemented") + } + + override suspend fun updateDatabase() { + TODO("Not yet implemented") + } + +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/BasicTicket.kt b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/BasicTicket.kt new file mode 100644 index 0000000..4de4b44 --- /dev/null +++ b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/BasicTicket.kt @@ -0,0 +1,37 @@ +package com.hoshikurama.github.ticketmanager.common.ticket + +import com.hoshikurama.github.ticketmanager.common.PluginState +import kotlinx.coroutines.Deferred +import java.util.* + +class BasicTicket( + val id: Int, + val creatorUUID: UUID?, + val location: Ticket.TicketLocation?, + val priority: Ticket.Priority, + val status: Ticket.Status, + val assignedTo: String?, + val statusUpdateForCreator: Boolean, + + val pluginState: PluginState, +) { + companion object { + suspend fun buildOrNull(pluginState: PluginState, id: Int) = + pluginState.database.buildBasicTicket(id) + } + + suspend fun setCreatorStatusUpdate(value: Boolean) = + pluginState.database.setCreatorStatusUpdate(id, value) + + suspend fun setTicketPriority(value: Ticket.Priority) = + pluginState.database.setPriority(id, value) + + suspend fun setTicketStatus(value: Ticket.Status) = + pluginState.database.setStatus(id, value) + + suspend fun setAssignedTo(value: String?) = + pluginState.database.setAssignment(id, value) + + suspend fun uuidMatches(other: UUID?) = + creatorUUID?.equals(other) ?: (other == null) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/Ticket.kt b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/Ticket.kt new file mode 100644 index 0000000..fbc67ee --- /dev/null +++ b/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/Ticket.kt @@ -0,0 +1,50 @@ +package com.hoshikurama.github.ticketmanager.common.ticket + +import com.hoshikurama.github.ticketmanager.common.TMLocale +import java.time.Instant +import java.util.* + +class Ticket( + val creatorUUID: UUID?, // UUID if player, null if Console + val location: TicketLocation?, // TicketLocation if player, null if Console + val actions: List = listOf(), // List of actions + val priority: Priority = Priority.NORMAL, // Priority 1-5 or Lowest to Highest + val status: Status = Status.OPEN, // Status OPEN or CLOSED + val assignedTo: String? = null, // Null if not assigned to anybody + val statusUpdateForCreator: Boolean = false, // Determines whether player should be notified + val id: Int = -1, // Ticket ID 1+... -1 placeholder during ticket creation +) { + + enum class Priority(val level: Byte, val colourCode: String) { + LOWEST(1, "&1"), + LOW(2, "&9"), + NORMAL(3, "&e"), + HIGH(4, "&c"), + HIGHEST(5, "&4") + } + enum class Status(val colourCode: String) { + OPEN("&a"), CLOSED("&c") + } + + data class Action(val type: Type, val user: UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { + enum class Type { + ASSIGN, CLOSE, COMMENT, OPEN, REOPEN, SET_PRIORITY, MASS_CLOSE + } + } + data class TicketLocation(val world: String, val x: Int, val y: Int, val z: Int) { + override fun toString() = "$world $x $y $z" + } +} + +fun Ticket.Priority.toLocaledWord(locale: TMLocale)= when (this) { + Ticket.Priority.LOWEST -> locale.priorityLowest + Ticket.Priority.LOW -> locale.priorityLow + Ticket.Priority.NORMAL -> locale.priorityNormal + Ticket.Priority.HIGH -> locale.priorityHigh + Ticket.Priority.HIGHEST -> locale.priorityHighest +} + +fun Ticket.Status.toLocaledWord(locale: TMLocale) = when (this) { + Ticket.Status.OPEN -> locale.statusOpen + Ticket.Status.CLOSED -> locale.statusClosed +} \ No newline at end of file diff --git a/src/main/resources/languages/Locales/Example.yml b/common/src/main/resources/locales/Example.yml similarity index 100% rename from src/main/resources/languages/Locales/Example.yml rename to common/src/main/resources/locales/Example.yml diff --git a/src/main/resources/languages/Locales/en_CA.yml b/common/src/main/resources/locales/en_CA.yml similarity index 100% rename from src/main/resources/languages/Locales/en_CA.yml rename to common/src/main/resources/locales/en_CA.yml diff --git a/settings.gradle.kts b/settings.gradle.kts index 16499b8..797b104 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,6 @@ -rootProject.name = "TicketManagerKotlin" \ No newline at end of file +rootProject.name = "TicketManager" + +include( + "common", + "Paper" +) \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/Globals.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/Globals.kt deleted file mode 100644 index 4d9da90..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/Globals.kt +++ /dev/null @@ -1,199 +0,0 @@ -package com.hoshikurama.github.ticketmanager - -import com.hoshikurama.github.ticketmanager.ticket.Ticket -import net.md_5.bungee.api.ChatColor -import net.md_5.bungee.api.chat.ClickEvent -import net.md_5.bungee.api.chat.HoverEvent -import net.md_5.bungee.api.chat.TextComponent -import net.md_5.bungee.api.chat.hover.content.Text -import org.bukkit.Bukkit -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player -import java.time.Instant -import java.util.* -import java.util.logging.Level -import kotlin.Comparator - -internal val sortForList: Comparator = Comparator.comparing(Ticket::priority).reversed().thenComparing(Comparator.comparing(Ticket::id).reversed()) - -internal val mainPlugin: TicketManagerPlugin - get() = TicketManagerPlugin.plugin - -internal val pluginState: PluginState - get() = mainPlugin.configState - - -fun CommandSender.sendPlatformMessage( - component: net.md_5.bungee.api.chat.BaseComponent, - serverType: PluginState.ServerType? = pluginState.serverType - ) { - when (serverType) { - PluginState.ServerType.Paper -> sendMessage(component) - PluginState.ServerType.Spigot -> spigot().sendMessage(component) - else -> try { - Class.forName("com.destroystokyo.paper.VersionHistoryManager\$VersionData") - sendPlatformMessage(component, PluginState.ServerType.Paper) - } catch (e: Exception) { - sendPlatformMessage(component, PluginState.ServerType.Spigot) - } - } -} - -fun Player.sendPlatformMessage( - component: net.md_5.bungee.api.chat.BaseComponent, - serverType: PluginState.ServerType? = pluginState.serverType -) { - when (serverType) { - PluginState.ServerType.Paper -> sendMessage(component) - PluginState.ServerType.Spigot -> spigot().sendMessage(component) - else -> try { - Class.forName("com.destroystokyo.paper.VersionHistoryManager\$VersionData") - sendPlatformMessage(component, PluginState.ServerType.Paper) - } catch (e: Exception) { - sendPlatformMessage(component, PluginState.ServerType.Spigot) - } - } -} - -fun getUUUIDStringOrNull(playerName: String): String? { - return when (pluginState.serverType) { - PluginState.ServerType.Paper -> Bukkit.getPlayerUniqueId(playerName)?.toString() - PluginState.ServerType.Spigot -> - Bukkit.getOfflinePlayers() - .asSequence() - .filter { it.name?.equals(playerName) ?: false } - .map { it.uniqueId.toString() } - .firstOrNull() - } -} - -fun byteToPriority(byte: Byte) = when (byte.toInt()) { - 1 -> Ticket.Priority.LOWEST - 2 -> Ticket.Priority.LOW - 3 -> Ticket.Priority.NORMAL - 4 -> Ticket.Priority.HIGH - 5 -> Ticket.Priority.HIGHEST - else -> Ticket.Priority.NORMAL -} - -fun postModifiedStacktrace(e: Exception) { - Bukkit.getOnlinePlayers().asSequence() - .filter { it.has("ticketmanager.notify.warning") } - .map { it to getLocale(it) } - .forEach { p -> - val sentComponent = TextComponent("") - - listOf( - p.second.stacktraceLine1, - p.second.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), - p.second.stacktraceLine3.replace("%message%", e.message ?: "?"), - p.second.stacktraceLine4 - ) - .map(::toColour) - .map(::TextComponent) - .forEach { sentComponent.addExtra(it) } - - // Adds stacktrace entries - e.stackTrace - .filter { it.className.startsWith("com.hoshikurama.github.ticketmanager") } - .map { - p.second.stacktraceEntry - .replace("%method%", it.methodName) - .replace("%file%", it.fileName ?: "?") - .replace("%line%", "${it.lineNumber}") - } - .map(::toColour) - .map(::TextComponent) - .forEach { sentComponent.addExtra(it) } - - p.first.sendPlatformMessage(sentComponent, null) - } -} - -internal fun anyLocksPresent() = mainPlugin.pluginLocked - -internal fun getLocale(player: Player) = when (pluginState.serverType) { - PluginState.ServerType.Paper -> pluginState.enabledLocales.getOrDefault(player.locale().toString()) - PluginState.ServerType.Spigot -> pluginState.enabledLocales.getOrDefault(player.locale) -} - -internal fun getLocale(sender: CommandSender): TMLocale { - return if (sender is Player) getLocale(sender) - else pluginState.enabledLocales.consoleLocale -} - -fun Player.has(permission: String): Boolean = mainPlugin.perms.has(this, permission) - -fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true - -fun consoleLog(level: Level, message: String): Unit = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) - -fun toColour(string: String): String = ChatColor.translateAlternateColorCodes('&', string) - -fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> String, level: Level = Level.INFO) { - consoleLog(level, localeMsg(pluginState.enabledLocales.consoleLocale)) - - Bukkit.getOnlinePlayers().asSequence() - .filter { it.has(permission) } - .forEach { getLocale(it).run(localeMsg).run(::toColour).run { it.sendMessage(this) } } -} - -fun String.sendColouredMessageTo(player: Player) = - toColour(this).run { player.sendMessage(this) } - -fun String.sendColouredMessageTo(sender: CommandSender) = - toColour(this).run { sender.sendMessage(this) } - -fun stripColour(str: String): String { - return ChatColor.stripColor(str).replace("&", "&&") -} - -fun Long.toLargestRelativeTime(locale: TMLocale): String { - val timeAgo = Instant.now().epochSecond - this - - return when { - timeAgo >= 31556952L -> (timeAgo / 31556952L).toString() + locale.timeYears - timeAgo >= 604800L ->(timeAgo / 604800L).toString() + locale.timeWeeks - timeAgo >= 86400L ->(timeAgo / 86400L).toString() + locale.timeDays - timeAgo >= 3600L ->(timeAgo / 3600L).toString() + locale.timeHours - timeAgo >= 60L ->(timeAgo / 60L).toString() + locale.timeMinutes - else -> timeAgo.toString() + locale.timeSeconds - } -} - -fun relTimeToEpochSecond(relTime: String, locale: TMLocale): Long { - var seconds = 0L - var index = 0 - val unprocessed = StringBuilder(relTime) - - while (unprocessed.isNotEmpty() && index != unprocessed.lastIndex + 1) { - unprocessed[index].toString().toByteOrNull() - // If number... - ?.apply { index++ } - // If not a number... - ?: run { - val number = if (index == 0) 0 - else unprocessed.substring(0, index).toLong() - - seconds += number * when (unprocessed[index].toString()) { - locale.searchTimeSecond -> 1L - locale.searchTimeMinute -> 60L - locale.searchTimeHour -> 3600L - locale.searchTimeDay -> 86400L - locale.searchTimeWeek -> 604800L - locale.searchTimeYear -> 31556952L - else -> 0L - } - - unprocessed.delete(0, index+1) - index = 0 - } - } - - return Instant.now().epochSecond - seconds -} - -fun TextComponent.addViewTicketOnClick(id: Int, locale: TMLocale) { - hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickViewTicket)) - clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, locale.run { "/$commandBase $commandWordView $id" } ) -} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/PluginState.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/PluginState.kt deleted file mode 100644 index fdc2b0b..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/PluginState.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.hoshikurama.github.ticketmanager - -import com.hoshikurama.github.ticketmanager.databases.Database -import com.hoshikurama.github.ticketmanager.databases.MySQL -import com.hoshikurama.github.ticketmanager.databases.SQLite -import com.hoshikurama.github.ticketmanager.events.Commands -import com.hoshikurama.github.ticketmanager.events.TabCompletePaper -import com.hoshikurama.github.ticketmanager.events.TabCompleteSpigot -import org.bukkit.Bukkit -import java.io.File -import java.io.InputStream -import java.net.URL -import java.time.Instant -import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicReference -import java.util.logging.Level - -class PluginState { - internal val enabledLocales: AllLocales - internal val cooldowns: Cooldown - internal val database: Database - internal val serverType: ServerType - internal val allowUnreadTicketUpdates: Boolean - internal val updateAvailable: AtomicReference?> = AtomicReference(null) - - init { - mainPlugin.pluginLocked = true - - var announceGeneration = false - if (!File(mainPlugin.dataFolder, "config.yml").exists()) { - mainPlugin.saveDefaultConfig() - announceGeneration = true - } - - mainPlugin.reloadConfig() - val config = mainPlugin.config - - config.run { - enabledLocales = AllLocales( - getString("Colour_Code", "&3")!!, - getString("Preferred_Locale", "en_ca")!!, - getString("Console_Locale", "en_ca")!!, - getBoolean("Force_Locale", false) - ) - - cooldowns = Cooldown( - getBoolean("Use_Cooldowns", false), - getLong("Cooldown_Time", 0L) - ) - - database = tryOrNull { - val type = getString("Database_Mode", "SQLite")!! - .let { tryOrNull { Database.Types.valueOf(it) } ?: Database.Types.SQLite } - - when (type) { - Database.Types.MySQL -> MySQL( - getString("MySQL_Host")!!, - getString("MySQL_Port")!!, - getString("MySQL_DBName")!!, - getString("MySQL_Username")!!, - getString("MySQL_Password")!!) - Database.Types.SQLite -> SQLite() - } - } ?: SQLite() - - allowUnreadTicketUpdates = getBoolean("Allow_Unread_Ticket_Updates", true) - - // Assigns update available - val allowUpdateCheck = getBoolean("Allow_UpdateChecking", false) - if (allowUpdateCheck) { - Bukkit.getScheduler().runTaskAsynchronously(mainPlugin, Runnable { - - val curVersion = mainPlugin.description.version - val latestVersion = UpdateChecker(91178).getLatestVersion() - .run { this ?: curVersion } - updateAvailable.set(curVersion to latestVersion) - }) - } else updateAvailable.set(null) - } - - serverType = tryOrNull { - Class.forName("com.destroystokyo.paper.VersionHistoryManager\$VersionData") - ServerType.Paper - } ?: ServerType.Spigot - - - if (announceGeneration) { - Bukkit.getScheduler().scheduleSyncDelayedTask(mainPlugin, { - pushMassNotify("ticketmanager.notify.warning", { it.warningsNoConfig }, Level.WARNING) - }, 100) - } - - // Register events and commands - enabledLocales.getCommandBases().forEach { - mainPlugin.getCommand(it)?.setExecutor(Commands()) - if (serverType == ServerType.Paper) - mainPlugin.server.pluginManager.registerEvents(TabCompletePaper(), mainPlugin) - else mainPlugin.getCommand(it)?.tabCompleter = TabCompleteSpigot() - // Remember to register any keyword in plugin.yml - } - - // Allows object to initialise while scheduling db update - if (database.updateNeeded()) { - Bukkit.getScheduler().runTaskLaterAsynchronously(mainPlugin, Runnable { - database.updateDatabase() - mainPlugin.pluginLocked = false - }, 20) - } else mainPlugin.pluginLocked = false - } - - class Cooldown(private val enabled: Boolean, private val duration: Long) { - private val map = ConcurrentHashMap() - - fun checkAndSet(uuid: UUID?): Boolean { - if (!enabled || uuid == null) return false - - val curTime = Instant.now().epochSecond - val applies = map[uuid]?.let { it <= curTime } ?: false - return if (applies) true - else map.put(uuid, duration + curTime).run { true } - } - - fun filterMap(): Unit = map.forEach { if (it.value > Instant.now().epochSecond) map.remove(it.key) } - } - - enum class ServerType { - Paper, Spigot - } - - private inline fun tryOrNull(function: () -> T): T? = - try { function() } - catch (ignored: Exception) { null } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/TicketManagerPlugin.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/TicketManagerPlugin.kt deleted file mode 100644 index 230b92b..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/TicketManagerPlugin.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.hoshikurama.github.ticketmanager - -import com.hoshikurama.github.ticketmanager.events.PlayerJoin -import net.milkbowl.vault.permission.Permission -import org.bukkit.Bukkit -import org.bukkit.plugin.java.JavaPlugin -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import java.util.logging.Level - -class TicketManagerPlugin : JavaPlugin() { - private val pluginLockedInternal = AtomicBoolean(true) - private lateinit var metrics: Metrics - internal val ticketCountMetrics = AtomicInteger(0) - - lateinit var perms: Permission - private set - lateinit var configState: PluginState - internal set - - var pluginLocked: Boolean - get() = pluginLockedInternal.get() - set(v) = pluginLockedInternal.set(v) - - companion object { lateinit var plugin: TicketManagerPlugin } - init { plugin = this } - - - override fun onEnable() { - pluginLocked = true - - // Creates Plugin State from config file - configState = PluginState() - - // Find Vault plugin - server.servicesManager.getRegistration(Permission::class.java)?.provider - ?.let { perms = it } - ?: consoleLog(Level.SEVERE, configState.enabledLocales.consoleLocale.warningsVaultNotFound) - .also { this.pluginLoader.disablePlugin(this) } - - // Register Event - server.pluginManager.registerEvents(PlayerJoin(), this) - - // Launch Metrics - metrics = Metrics(this, metricsKey) - metrics.addCustomChart( - Metrics.SingleLineChart("tickets_made") - { ticketCountMetrics.getAndSet(0) } - ) - - - Bukkit.getScheduler().runTaskTimerAsynchronously(mainPlugin, Runnable { - if (anyLocksPresent()) return@Runnable - - // Mass Unread Notify - try { - if (pluginState.allowUnreadTicketUpdates) { - configState.database.getTicketIDsWithUpdates() - .groupBy({ it.first }, { it.second }) - .asSequence() - .mapNotNull { Bukkit.getPlayer(it.key)?.run { Pair(this, it.value) } } - .filter { it.first.has("ticketmanager.notify.unreadUpdates.scheduled") } - .forEach { - val template = if (it.second.size > 1) getLocale(it.first).notifyUnreadUpdateMulti - else getLocale(it.first).notifyUnreadUpdateSingle - val tickets = it.second.joinToString(", ") - - template.replace("%num%", tickets) - .sendColouredMessageTo(it.first) - } - } - - // Open and Assigned Notify - val tickets = pluginState.database.getOpen() - .map { it.assignedTo } - - Bukkit.getOnlinePlayers().asSequence() - .filter { it.has("ticketmanager.notify.openTickets.scheduled") } - .forEach { p -> - val open = tickets.size.toString() - val assigned = tickets.asSequence() - .filterNotNull() - .filter { s -> - if (s.startsWith("::")) - perms.getPlayerGroups(p) - .asSequence() - .map { "::$it" } - .filter { it == s } - .any() - else s == p.name - }.count().toString() - - getLocale(p).notifyOpenAssigned - .replace("%open%", open) - .replace("%assigned%", assigned) - .sendColouredMessageTo(p) - } - - configState.cooldowns.filterMap() - - } catch (e: Exception) { - e.printStackTrace() - postModifiedStacktrace(e) - } - }, 0, 12000) - } - - override fun onDisable() { - configState.database.closeDatabase() - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/Database.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/Database.kt deleted file mode 100644 index feaa5cd..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/Database.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.hoshikurama.github.ticketmanager.databases - -import com.hoshikurama.github.ticketmanager.ticket.Ticket -import java.util.* - -interface Database { - enum class Types { - MySQL, SQLite - } - - val type: Types - - // Individual things - fun getAssignment(ticketID: Int): String? - fun getCreatorUUID(ticketID: Int): UUID? - fun getLocation(ticketID: Int): org.bukkit.Location? - fun getPriority(ticketID: Int): Ticket.Priority - fun getStatus(ticketID: Int): Ticket.Status - fun getStatusUpdateForCreator(ticketID: Int): Boolean - fun setAssignment(ticketID: Int, assignment: String?) - fun setPriority(ticketID: Int, priority: Ticket.Priority) - fun setStatus(ticketID: Int, status: Ticket.Status) - fun setStatusUpdateForCreator(ticketID: Int, status: Boolean) - - // More specific Ticket actions - fun addAction(ticketID: Int, action: Ticket.Action) - fun addTicket(ticket: Ticket, action: Ticket.Action): Int - fun getOpen(): List - fun getOpenAssigned(assignment: String, groupAssignment: List): List - fun getTicket(ID: Int): Ticket? - fun getTicketIDsWithUpdates(): List> - fun getTicketIDsWithUpdates(uuid: UUID): List - fun isValidID(ticketID: Int): Boolean - fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) - fun searchDatabase(searchFunction: (Ticket) -> Boolean): List - - // Database Modifications - fun closeDatabase() - fun createDatabasesIfNeeded() - fun migrateDatabase(targetType: Types) - fun updateNeeded(): Boolean - fun updateDatabase() -} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/MySQL.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/MySQL.kt deleted file mode 100644 index 41cb95d..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/MySQL.kt +++ /dev/null @@ -1,413 +0,0 @@ -package com.hoshikurama.github.ticketmanager.databases - -import com.hoshikurama.github.ticketmanager.* -import com.hoshikurama.github.ticketmanager.ticket.Ticket -import com.hoshikurama.github.ticketmanager.ticket.toBukkitLocationOrNull -import kotliquery.* -import org.bukkit.Bukkit -import java.sql.Statement -import java.time.Instant -import java.util.* - -internal class MySQL( - host: String, - port: String, - dbName: String, - username: String, - password: String, -) : Database { - override val type = Database.Types.MySQL - private val dataSource = HikariCP.default("jdbc:mysql://$host:$port/$dbName?serverTimezone=UTC", username, password) - private val toBukkitLocOrNull: String.() -> org.bukkit.Location? = { - this.split(" ") - .let { Ticket.Location( - world = it[0], - x = it[1].toInt(), - y = it[2].toInt(), - z = it[3].toInt()) - } - .toBukkitLocationOrNull() - } - - init { - createDatabasesIfNeeded() - } - - override fun getAssignment(ticketID: Int): String? { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT ASSIGNED_TO FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.stringOrNull(1) } - .asSingle - ) - } - } - - override fun getCreatorUUID(ticketID: Int): UUID? { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT CREATOR_UUID FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.stringOrNull(1)?.run(UUID::fromString) } - .asSingle - ) - } - } - - override fun getLocation(ticketID: Int): org.bukkit.Location? { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT LOCATION FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.stringOrNull(1)?.toBukkitLocOrNull() } - .asSingle - ) - } - } - - override fun getPriority(ticketID: Int): Ticket.Priority { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT PRIORITY FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { byteToPriority(it.byte(1)) } - .asSingle - )!! - } - } - - override fun getStatus(ticketID: Int): Ticket.Status { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT STATUS FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { Ticket.Status.valueOf(it.string(1)) } - .asSingle - )!! - } - } - - override fun getStatusUpdateForCreator(ticketID: Int): Boolean { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT STATUS_UPDATE_FOR_CREATOR FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.boolean(1) } - .asSingle - )!! - } - } - - override fun setAssignment(ticketID: Int, assignment: String?) { - using(sessionOf(dataSource)) { - it.run(queryOf("UPDATE TicketManager_V4_Tickets SET ASSIGNED_TO = ? WHERE ID = $ticketID;", assignment).asUpdate) - } - } - - override fun setPriority(ticketID: Int, priority: Ticket.Priority) { - using(sessionOf(dataSource)) { - it.run(queryOf("UPDATE TicketManager_V4_Tickets SET PRIORITY = ? WHERE ID = $ticketID;", priority.level).asUpdate) - } - } - - override fun setStatus(ticketID: Int, status: Ticket.Status) { - using(sessionOf(dataSource)) { - it.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID = $ticketID;", status.name).asUpdate) - } - } - - override fun setStatusUpdateForCreator(ticketID: Int, status: Boolean) { - using(sessionOf(dataSource)) { - it.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS_UPDATE_FOR_CREATOR = ? WHERE ID = $ticketID;", status).asUpdate) - } - } - - // More Specific Ticket Actions - - override fun addAction(ticketID: Int, action: Ticket.Action) { - using(sessionOf(dataSource)) { writeAction(action, ticketID, it) } - } - - override fun addTicket(ticket: Ticket, action: Ticket.Action): Int { - return using(sessionOf(dataSource)) { - val id = writeTicket(ticket, it) - writeAction(action, id.toInt(), it) - return@using id.toInt() - } - } - - override fun getOpen(): List { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE STATUS = ?;", Ticket.Status.OPEN.toString()) - .map { it.toTicket(session) } - .asList) - }.sortedWith(sortForList) - } - - override fun getOpenAssigned(assignment: String, groupAssignment: List) = - getOpen().filter { it.assignedTo == assignment || it.assignedTo in groupAssignment } - - override fun getTicket(ID: Int): Ticket? { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE ID = $ID;") - .map { row -> row.toTicket(session) } - .asSingle) - } - } - - override fun getTicketIDsWithUpdates(): List> { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT CREATOR_UUID, ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ?;", true) - .map { Pair( first = UUID.fromString(it.string(1)), second = it.int(2)) } - .asList) - } - } - - - override fun getTicketIDsWithUpdates(uuid: UUID): List { - return using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ? AND CREATOR_UUID = ?;", true, uuid) - .map { it.int(1) } - .asList - ) - } - } - - override fun isValidID(ticketID: Int): Boolean { - val result = using(sessionOf(dataSource)) { session -> - session.run(queryOf("SELECT ID FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { true } - .asSingle - ) - } - return result ?: false - } - - override fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { - using(sessionOf(dataSource)) { session -> - val idStatusPairs = session.run(queryOf("SELECT ID, STATUS FROM TicketManager_V4_Tickets WHERE ID BETWEEN $lowerBound AND $upperBound;") - .map { it.int(1) to Ticket.Status.valueOf(it.string(2)) } - .asList - ) - - idStatusPairs.asSequence() - .filter { it.second == Ticket.Status.OPEN } - .forEach { - writeAction( - Ticket.Action( - type = Ticket.Action.Type.MASS_CLOSE, - user = uuid, - message = null, - timestamp = Instant.now().epochSecond - ), - ticketID = it.first, - session = session - ) - } - - val idString = idStatusPairs.map { it.first }.joinToString(", ") - session.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID IN ($idString);", Ticket.Status.CLOSED.name).asUpdate) - } - } - - override fun searchDatabase(searchFunction: (Ticket) -> Boolean): List { - val matchedTickets = mutableListOf() - - using(sessionOf(dataSource)) { session -> - session.forEach(queryOf("SELECT * FROM TicketManager_V4_Tickets")) { row -> - row.toTicket(session).takeIf(searchFunction)?.apply(matchedTickets::add) - } - } - return matchedTickets - } - - override fun closeDatabase() { - dataSource.close() - } - - override fun createDatabasesIfNeeded() { - using(sessionOf(dataSource)) { - if (!tableExists("TicketManager_V4_Tickets", it)) - it.run(queryOf(""" - CREATE TABLE TicketManager_V4_Tickets ( - ID INT NOT NULL AUTO_INCREMENT, - CREATOR_UUID VARCHAR(36) CHARACTER SET latin1 COLLATE latin1_general_ci, - PRIORITY TINYINT NOT NULL, - STATUS VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, - ASSIGNED_TO VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - STATUS_UPDATE_FOR_CREATOR BOOLEAN NOT NULL, - LOCATION VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - KEY STATUS_V4 (STATUS) USING BTREE, - KEY STATUS_UPDATE_FOR_CREATOR_V4 (STATUS_UPDATE_FOR_CREATOR) USING BTREE, - PRIMARY KEY (ID) - ) ENGINE=InnoDB;""".trimIndent() - ).asExecute) - if (!tableExists("TicketManager_V4_Actions", it)) - it.run(queryOf(""" - CREATE TABLE TicketManager_V4_Actions ( - ACTION_ID INT NOT NULL AUTO_INCREMENT, - TICKET_ID INT NOT NULL, - ACTION_TYPE VARCHAR(20) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL, - CREATOR_UUID VARCHAR(36) CHARACTER SET latin1 COLLATE latin1_general_ci, - MESSAGE TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, - TIMESTAMP BIGINT NOT NULL, - PRIMARY KEY (ACTION_ID) - ) ENGINE=InnoDB;""".trimIndent() - ).asExecute) - } - } - - override fun updateNeeded(): Boolean { - return using(sessionOf(dataSource)) { - tableExists("TicketManagerTicketsV2", it) - } - } - - override fun updateDatabase() { - pushMassNotify("ticketmanager.notify.info", { it.informationDBUpdate } ) - - fun playerNameToUUIDOrNull(name: String) = Bukkit.getOfflinePlayers() - .asSequence() - .filter { it.name == name } - .map { it.uniqueId } - .firstOrNull() - - using(sessionOf(dataSource)) { session -> - session.forEach(queryOf("SELECT * FROM TicketManagerTicketsV2;")) { row -> - val ticket = Ticket( - id = row.int(1), - priority = byteToPriority(row.byte(3)), - statusUpdateForCreator = row.boolean(10), - status = Ticket.Status.valueOf(row.string(2)), - assignedTo = row.stringOrNull(6), - - creatorUUID = row.string(5).let { - if (it.lowercase() == "console") null - else UUID.fromString(it) - }, - location = row.string(7).run { - if (equals("NoLocation")) null - else Ticket.Location(split(" ")) - }, - actions = row.string(9) - .split("/MySQLNewLine/") - .filter { it.isNotBlank() } - .map { it.split("/MySQLSep/") } - .mapIndexed { index, action -> - Ticket.Action( - message = action[1], - type = if (index == 0) Ticket.Action.Type.OPEN else Ticket.Action.Type.COMMENT, - timestamp = row.long(8), - user = - if (action[0].lowercase() == "console") null - else playerNameToUUIDOrNull(action[0]) - ) - } - ) - val id = writeTicket(ticket, session) - ticket.actions.forEach { writeAction(it, id.toInt(), session) } - } - - session.run(queryOf("DROP INDEX STATUS ON TicketManagerTicketsV2;").asExecute) - session.run(queryOf("DROP INDEX UPDATEDBYOTHERUSER ON TicketManagerTicketsV2;").asExecute) - session.run(queryOf("DROP TABLE TicketManagerTicketsV2;").asUpdate) - - pushMassNotify("ticketmanager.notify.info", { it.informationDBUpdateComplete } ) - } - } - - override fun migrateDatabase(targetType: Database.Types) { - mainPlugin.pluginLocked = true - - when (targetType) { - Database.Types.MySQL -> {} // MySQL -> MySQL not permitted! - - Database.Types.SQLite -> { - pushMassNotify("ticketmanager.notify.info", { - it.informationDBConvertInit - .replace("%fromDB%", Database.Types.MySQL.name) - .replace("%toDB%", Database.Types.SQLite.name) - }) - - try { - val sqLite = SQLite() - - // Writes to SQLite - using(sessionOf(dataSource)) { session -> - session.forEach(queryOf("SELECT * FROM TicketManager_V4_Tickets")) { row -> //NOTE: During conversion, ticket ID is not guaranteed to be preserved - row.toTicket(session).apply { - val newID = sqLite.addTicket(this, actions[0]) - if (actions.size > 1) actions.subList(1, actions.size) - .forEach { sqLite.addAction(newID, it) } - } - } - } - - pushMassNotify("ticketmanager.notify.info", { it.informationDBConvertSuccess } ) - - } catch (e: Exception) { - e.printStackTrace() - postModifiedStacktrace(e) - } - } - } - - mainPlugin.pluginLocked = false - } - - private fun writeTicket(ticket: Ticket, session: Session): Long { - val stmt = session.connection.underlying.prepareStatement( - "INSERT INTO TicketManager_V4_Tickets (CREATOR_UUID, PRIORITY, STATUS, ASSIGNED_TO, STATUS_UPDATE_FOR_CREATOR, LOCATION) VALUES (?,?,?,?,?,?);", - Statement.RETURN_GENERATED_KEYS) - stmt.setString(1, ticket.creatorUUID?.toString()) - stmt.setByte(2, ticket.priority.level) - stmt.setString(3, ticket.status.name) - stmt.setString(4, ticket.assignedTo) - stmt.setBoolean(5, ticket.statusUpdateForCreator) - stmt.setString(6, ticket.location?.toString()) - stmt.executeUpdate() - - val rs = stmt.generatedKeys - rs.next() - return rs.getLong(1) - } - - private fun writeAction(action: Ticket.Action, ticketID: Int, session: Session) { - session.run(queryOf("INSERT INTO TicketManager_V4_Actions (TICKET_ID,ACTION_TYPE,CREATOR_UUID,MESSAGE,TIMESTAMP) VALUES (?,?,?,?,?);", - ticketID, - action.type.name, - action.user?.toString(), - action.message, - action.timestamp - ).asExecute) - } - - private fun Row.toTicket(session: Session): Ticket { - val id = int(1) - return Ticket( - id = id, - creatorUUID = stringOrNull(2)?.let(UUID::fromString), - priority = byteToPriority(byte(3)), - status = Ticket.Status.valueOf(string(4)), - assignedTo = stringOrNull(5), - statusUpdateForCreator = boolean(6), - location = stringOrNull(7)?.split(" ")?.let { - Ticket.Location( - world = it[0], - x = it[1].toInt(), - y = it[2].toInt(), - z = it[3].toInt() - ) - }, - actions = session.run(queryOf("SELECT ACTION_ID, ACTION_TYPE, CREATOR_UUID, MESSAGE, TIMESTAMP FROM TicketManager_V4_Actions WHERE TICKET_ID = $id;") - .map { row -> - row.int(1) to Ticket.Action( - type = Ticket.Action.Type.valueOf(row.string(2)), - user = row.stringOrNull(3)?.let { UUID.fromString(it) }, - message = row.stringOrNull(4), - timestamp = row.long(5) - ) - }.asList - ) - .sortedBy { it.first } - .map { it.second } - ) - } - - private fun tableExists(table: String, session: Session): Boolean { - return using(session.connection.underlying.metaData.getTables(null, null, table, null)) { - while (it.next()) - if (it.getString("TABLE_NAME")?.equals(table) == true) return@using true - return@using false - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/SQLite.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/SQLite.kt deleted file mode 100644 index 6a14bfd..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/databases/SQLite.kt +++ /dev/null @@ -1,417 +0,0 @@ -package com.hoshikurama.github.ticketmanager.databases - -import com.hoshikurama.github.ticketmanager.* -import com.hoshikurama.github.ticketmanager.ticket.Ticket -import com.hoshikurama.github.ticketmanager.ticket.toBukkitLocationOrNull -import kotliquery.* -import org.bukkit.Bukkit -import java.sql.DriverManager -import java.time.Instant -import java.util.* - - -internal class SQLite : Database { - override val type = Database.Types.SQLite - private val url: String = "jdbc:sqlite:${mainPlugin.dataFolder.absolutePath}/TicketManager-SQLite.db" - private val toBukkitLocOrNull: String.() -> org.bukkit.Location? = { - this.split(" ") - .let { Ticket.Location( - world = it[0], - x = it[1].toInt(), - y = it[2].toInt(), - z = it[3].toInt()) - } - .toBukkitLocationOrNull() - } - - init { - createDatabasesIfNeeded() - } - - override fun getAssignment(ticketID: Int): String? { - return using(getSession()) { session -> - session.run(queryOf("SELECT ASSIGNED_TO FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.stringOrNull(1) } - .asSingle - ) - } - } - - override fun getCreatorUUID(ticketID: Int): UUID? { - return using(getSession()) { session -> - session.run(queryOf("SELECT CREATOR_UUID FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.stringOrNull(1)?.run(UUID::fromString) } - .asSingle - ) - } - } - - override fun getLocation(ticketID: Int): org.bukkit.Location? { - return using(getSession()) { session -> - session.run(queryOf("SELECT LOCATION FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.stringOrNull(1)?.toBukkitLocOrNull() } - .asSingle - ) - } - } - - override fun getPriority(ticketID: Int): Ticket.Priority { - return using(getSession()) { session -> - session.run(queryOf("SELECT PRIORITY FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { byteToPriority(it.byte(1)) } - .asSingle - )!! - } - } - - override fun getStatus(ticketID: Int): Ticket.Status { - return using(getSession()) { session -> - session.run(queryOf("SELECT STATUS FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { Ticket.Status.valueOf(it.string(1)) } - .asSingle - )!! - } - } - - override fun getStatusUpdateForCreator(ticketID: Int): Boolean { - return using(getSession()) { session -> - session.run(queryOf("SELECT STATUS_UPDATE_FOR_CREATOR FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.boolean(1) } - .asSingle - )!! - } - } - - override fun setAssignment(ticketID: Int, assignment: String?) { - using(getSession()) { - it.run(queryOf("UPDATE TicketManager_V4_Tickets SET ASSIGNED_TO = ? WHERE ID = $ticketID;", assignment).asUpdate) - } - } - - override fun setPriority(ticketID: Int, priority: Ticket.Priority) { - using(getSession()) { - it.run(queryOf("UPDATE TicketManager_V4_Tickets SET PRIORITY = ? WHERE ID = $ticketID;", priority.level).asUpdate) - } - } - - override fun setStatus(ticketID: Int, status: Ticket.Status) { - using(getSession()) { - it.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID = $ticketID;", status.name).asUpdate) - } - } - - override fun setStatusUpdateForCreator(ticketID: Int, status: Boolean) { - using(getSession()) { - it.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS_UPDATE_FOR_CREATOR = ? WHERE ID = $ticketID;", status).asUpdate) - } - } - - override fun addAction(ticketID: Int, action: Ticket.Action) { - using(getSession()) { writeAction(action, ticketID, it) } - } - - override fun addTicket(ticket: Ticket, action: Ticket.Action): Int { - return using(getSession()) { - val id = writeTicket(ticket, it) - writeAction(action, id!!.toInt(), it) - return@using id.toInt() - } - } - - override fun getOpen(): List { - return using(getSession()) { session -> - session.run(queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE STATUS = ?;", Ticket.Status.OPEN.toString()) - .map { it.toTicket(session) } - .asList - ) - }.sortedWith(sortForList) - } - - override fun getOpenAssigned(assignment: String, groupAssignment: List) = - getOpen().filter { it.assignedTo == assignment || it.assignedTo in groupAssignment } - - override fun getTicket(ID: Int): Ticket? { - return using(getSession()) { session -> - session.run(queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE ID = $ID;") - .map { row -> row.toTicket(session) } - .asSingle - ) - } - } - - override fun getTicketIDsWithUpdates(): List> { - return using(getSession()) { session -> - session.run(queryOf("SELECT CREATOR_UUID, ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ?;", true) - .map { Pair( first = UUID.fromString(it.string(1)), second = it.int(2)) } - .asList - ) - } - } - - override fun getTicketIDsWithUpdates(uuid: UUID): List { - return using(getSession()) { session -> - session.run(queryOf("SELECT ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ? AND CREATOR_UUID = ?;", true, uuid) - .map { it.int(1) } - .asList - ) - } - } - - override fun isValidID(ticketID: Int): Boolean { - val result = using(getSession()) { session -> - session.run(queryOf("SELECT ID FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { true } - .asSingle - ) - } - return result ?: false - } - - override fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { - using(getSession()) { session -> - val idStatusPairs = session.run(queryOf("SELECT ID, STATUS FROM TicketManager_V4_Tickets WHERE ID BETWEEN $lowerBound AND $upperBound;") - .map { it.int(1) to Ticket.Status.valueOf(it.string(2)) } - .asList - ) - - idStatusPairs.asSequence() - .filter { it.second == Ticket.Status.OPEN } - .forEach { - writeAction( - Ticket.Action( - type = Ticket.Action.Type.MASS_CLOSE, - user = uuid, - message = null, - timestamp = Instant.now().epochSecond - ), - ticketID = it.first, - session = session - ) - } - - val idString = idStatusPairs.map { it.first }.joinToString(", ") - session.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID IN ($idString);", Ticket.Status.CLOSED.name).asUpdate) - } - } - - override fun searchDatabase(searchFunction: (Ticket) -> Boolean): List { - val matchedTickets = mutableListOf() - - using(getSession()) { session -> - session.forEach(queryOf("SELECT * FROM TicketManager_V4_Tickets")) { row -> - row.toTicket(session).takeIf(searchFunction)?.apply(matchedTickets::add) - } - } - return matchedTickets - } - - override fun closeDatabase() { - // NOT needed as database makes individual connections - } - - override fun createDatabasesIfNeeded() { - using(getSession()) { - if (!tableExists("TicketManager_V4_Tickets", it)) { - it.run( - queryOf(""" - CREATE TABLE TicketManager_V4_Tickets ( - ID INTEGER PRIMARY KEY AUTOINCREMENT, - CREATOR_UUID VARCHAR(36) COLLATE NOCASE, - PRIORITY TINYINT NOT NULL, - STATUS VARCHAR(10) COLLATE NOCASE NOT NULL, - ASSIGNED_TO VARCHAR(255) COLLATE NOCASE, - STATUS_UPDATE_FOR_CREATOR BOOLEAN NOT NULL, - LOCATION VARCHAR(255) COLLATE NOCASE - );""".trimIndent() - ).asExecute - ) - it.run(queryOf("CREATE INDEX STATUS_V4 ON TicketManager_V4_Tickets (STATUS)").asExecute) - it.run(queryOf("CREATE INDEX STATUS_UPDATE_FOR_CREATOR_V4 ON TicketManager_V4_Tickets (STATUS_UPDATE_FOR_CREATOR)").asExecute) - } - - if (!tableExists("TicketManager_V4_Actions", it)) { - it.run( - queryOf(""" - CREATE TABLE TicketManager_V4_Actions ( - ACTION_ID INTEGER PRIMARY KEY AUTOINCREMENT, - TICKET_ID INTEGER NOT NULL, - ACTION_TYPE VARCHAR(20) COLLATE NOCASE NOT NULL, - CREATOR_UUID VARCHAR(36) COLLATE NOCASE, - MESSAGE TEXT COLLATE NOCASE, - TIMESTAMP BIGINT NOT NULL - );""".trimIndent() - ).asExecute - ) - } - } - } - - override fun migrateDatabase(targetType: Database.Types) { - mainPlugin.pluginLocked = true - - when (targetType) { - Database.Types.SQLite -> {} // SQLite -> SQLite is not permitted - - Database.Types.MySQL -> { - pushMassNotify("ticketmanager.notify.info", { - it.informationDBConvertInit - .replace("%fromDB%", Database.Types.SQLite.name) - .replace("%toDB%", Database.Types.MySQL.name) - } ) - - try { - val config = mainPlugin.config - val mySQL = MySQL( - config.getString("MySQL_Host")!!, - config.getString("MySQL_Port")!!, - config.getString("MySQL_DBName")!!, - config.getString("MySQL_Username")!!, - config.getString("MySQL_Password")!! - ) - - // Writes to MySQL - using(getSession()) { session -> - session.forEach(queryOf("SELECT * FROM TicketManager_V4_Tickets")) { row -> //NOTE: During conversion, ticket ID is not guaranteed to be preserved - row.toTicket(session).apply { - val newID = mySQL.addTicket(this, actions[0]) - if (actions.size > 1) actions.subList(1, actions.size) - .forEach { mySQL.addAction(newID, it) } - } - } - } - - pushMassNotify("ticketmanager.notify.info", { it.informationDBConvertSuccess } ) - - } catch (e: Exception) { - e.printStackTrace() - postModifiedStacktrace(e) - } - } - } - - mainPlugin.pluginLocked = false - } - - override fun updateNeeded(): Boolean { - return using(getSession()) { - tableExists("TicketManagerTicketsV2", it) - } - } - - override fun updateDatabase() { - pushMassNotify("ticketmanager.notify.info", { it.informationDBUpdate } ) - - fun playerNameToUUIDOrNull(name: String) = Bukkit.getOfflinePlayers() - .asSequence() - .filter { it.name == name } - .map { it.uniqueId } - .firstOrNull() - - using(getSession()) { session -> - session.forEach(queryOf("SELECT * FROM TicketManagerTicketsV2;")) { row -> - val ticket = Ticket( - id = row.int(1), - priority = byteToPriority(row.byte(3)), - statusUpdateForCreator = row.boolean(10), - status = Ticket.Status.valueOf(row.string(2)), - assignedTo = row.stringOrNull(6), - - creatorUUID = row.string(5).let { - if (it.lowercase() == "console") null - else UUID.fromString(it) - }, - location = row.string(7).run { - if (equals("NoLocation")) null - else Ticket.Location(split(" ")) - }, - actions = row.string(9) - .split("/MySQLNewLine/") - .filter { it.isNotBlank() } - .map { it.split("/MySQLSep/") } - .mapIndexed { index, action -> - Ticket.Action( - message = action[1], - type = if (index == 0) Ticket.Action.Type.OPEN else Ticket.Action.Type.COMMENT, - timestamp = row.long(8), - user = - if (action[0].lowercase() == "console") null - else playerNameToUUIDOrNull(action[0]) - ) - } - ) - val id = writeTicket(ticket, session) - ticket.actions.forEach { writeAction(it, id!!.toInt(), session) } - } - - session.run(queryOf("DROP INDEX STATUS;").asExecute) - session.run(queryOf("DROP INDEX UPDATEDBYOTHERUSER;").asExecute) - session.run(queryOf("DROP TABLE TicketManagerTicketsV2;").asUpdate) - - pushMassNotify("ticketmanager.notify.info", { it.informationDBUpdateComplete } ) - } - } - - private fun tableExists(table: String, session: Session): Boolean { - return using(session.connection.underlying.metaData.getTables(null, null, table, null)) { - while (it.next()) - if (it.getString("TABLE_NAME")?.equals(table) == true) return@using true - return@using false - } - } - - private fun getSession() = Session(Connection(DriverManager.getConnection(url))) - - private fun writeTicket(ticket: Ticket, session: Session): Long? { - return session.run(queryOf("INSERT INTO TicketManager_V4_Tickets (CREATOR_UUID, PRIORITY, STATUS, ASSIGNED_TO, STATUS_UPDATE_FOR_CREATOR, LOCATION) VALUES (?,?,?,?,?,?);", - ticket.creatorUUID, - ticket.priority.level, - ticket.status.name, - ticket.assignedTo, - ticket.statusUpdateForCreator, - ticket.location?.toString() - ).asUpdateAndReturnGeneratedKey) - } - - private fun writeAction(action: Ticket.Action, ticketID: Int, session: Session) { - session.run(queryOf("INSERT INTO TicketManager_V4_Actions (TICKET_ID,ACTION_TYPE,CREATOR_UUID,MESSAGE,TIMESTAMP) VALUES (?,?,?,?,?);", - ticketID, - action.type.name, - action.user?.toString(), - action.message, - action.timestamp - ).asExecute) - } - - private fun Row.toTicket(session: Session): Ticket { - val id = int(1) - return Ticket( - id = id, - creatorUUID = stringOrNull(2)?.let(UUID::fromString), - priority = byteToPriority(byte(3)), - status = Ticket.Status.valueOf(string(4)), - assignedTo = stringOrNull(5), - statusUpdateForCreator = boolean(6), - location = stringOrNull(7)?.split(" ")?.let { - Ticket.Location( - world = it[0], - x = it[1].toInt(), - y = it[2].toInt(), - z = it[3].toInt() - ) - }, - actions = session.run( - queryOf("SELECT ACTION_ID, ACTION_TYPE, CREATOR_UUID, MESSAGE, TIMESTAMP FROM TicketManager_V4_Actions WHERE TICKET_ID = $id;") - .map { row -> - row.int(1) to Ticket.Action( - type = Ticket.Action.Type.valueOf(row.string(2)), - user = row.stringOrNull(3)?.let { UUID.fromString(it) }, - message = row.stringOrNull(4), - timestamp = row.long(5) - ) - }.asList - ) - .sortedBy { it.first } - .map { it.second } - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/CommandPipeline.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/CommandPipeline.kt deleted file mode 100644 index 00b1a3f..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/CommandPipeline.kt +++ /dev/null @@ -1,1291 +0,0 @@ -package com.hoshikurama.github.ticketmanager.events - -import com.hoshikurama.github.ticketmanager.* -import com.hoshikurama.github.ticketmanager.databases.Database -import com.hoshikurama.github.ticketmanager.ticket.* -import net.md_5.bungee.api.ChatColor -import net.md_5.bungee.api.chat.ClickEvent -import net.md_5.bungee.api.chat.HoverEvent -import net.md_5.bungee.api.chat.TextComponent -import net.md_5.bungee.api.chat.hover.content.Text -import org.bukkit.Bukkit -import org.bukkit.command.Command -import org.bukkit.command.CommandExecutor -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player -import java.util.* - -class Commands : CommandExecutor { - override fun onCommand(sender: CommandSender, command: Command, label: String, args1: Array): Boolean { - val senderLocale = getLocale(sender) - val args = args1.toList() - - if (args.isEmpty()) { - sender.sendMessage(senderLocale.warningsInvalidCommand) - return false - } - - if (anyLocksPresent()) { - sender.sendMessage(senderLocale.warningsLocked) - return false - } - - // Attempts to grab ticket and filters out invalid number for ticket - val attemptTicket = getTicketHandler(args, senderLocale) - val pseudoTicket: TicketHandler - if (attemptTicket == null) { - senderLocale.warningsInvalidNumber.sendColouredMessageTo(sender) - return false - } else pseudoTicket = attemptTicket - - if (args.isEmpty()) { - help(sender, senderLocale) - return false - } - - // Shortened Functions - val hasValidPermission = { hasValidPermission(sender, pseudoTicket, args, senderLocale) } - val isValidCommand = { isValidCommand(sender, pseudoTicket, args, senderLocale) } - val notUnderCooldown = { notUnderCooldown(sender, senderLocale, args) } - val executeCommand = { executeCommand(sender, args, senderLocale, pseudoTicket) } - - // Main Bukkit Task - Bukkit.getScheduler().runTaskAsynchronously(mainPlugin, Runnable { - try { - if (notUnderCooldown() && isValidCommand() && hasValidPermission()) - executeCommand()?.let { pushNotifications(sender, it, senderLocale, pseudoTicket) } - } catch (e: Exception) { - e.printStackTrace() - postModifiedStacktrace(e) - sender.sendMessage(senderLocale.warningsUnexpectedError) - } - }) - - return false - } - - private fun getTicketHandler(args: List, senderLocale: TMLocale): TicketHandler? { - val id: Int? = when (args[0]) { - senderLocale.commandWordAssign, - senderLocale.commandWordSilentAssign, - senderLocale.commandWordClaim, - senderLocale.commandWordSilentClaim, - senderLocale.commandWordClose, - senderLocale.commandWordSilentClose, - senderLocale.commandWordComment, - senderLocale.commandWordSilentComment, - senderLocale.commandWordReopen, - senderLocale.commandWordSilentReopen, - senderLocale.commandWordSetPriority, - senderLocale.commandWordSilentSetPriority, - senderLocale.commandWordTeleport, - senderLocale.commandWordUnassign, - senderLocale.commandWordSilentUnassign, - senderLocale.commandWordView, - senderLocale.commandWordDeepView -> args.getOrNull(1)?.toIntOrNull() - else -> 0 - } - - return id?.let(::TicketHandler) - } - - private fun hasValidPermission( - sender: CommandSender, - ticketHandler: TicketHandler, - args: List, - senderLocale: TMLocale - ): Boolean { - if (sender !is Player) return true - - fun has(perm: String) = sender.has(perm) - fun hasSilent() = has("ticketmanager.commandArg.silence") - fun hasDuality(basePerm: String): Boolean { - val senderUUID = sender.toUUIDOrNull() - val ownsTicket = ticketHandler.UUIDMatches(senderUUID) - return has("$basePerm.all") || (sender.has("$basePerm.own") && ownsTicket) - } - - return senderLocale.run { - when (args[0]) { - commandWordAssign, commandWordClaim, commandWordUnassign -> - has("ticketmanager.command.assign") - commandWordSilentAssign, commandWordSilentClaim,commandWordSilentUnassign -> - has("ticketmanager.command.assign") && hasSilent() - commandWordClose -> hasDuality("ticketmanager.command.close") - commandWordSilentClose -> hasDuality("ticketmanager.command.close") && hasSilent() - commandWordCloseAll -> has("ticketmanager.command.closeAll") - commandWordSilentCloseAll -> has("ticketmanager.command.closeAll") && hasSilent() - commandWordComment -> hasDuality("ticketmanager.command.comment") - commandWordSilentComment -> hasDuality("ticketmanager.command.comment") && hasSilent() - commandWordCreate -> has("ticketmanager.command.create") - commandWordHelp -> has("ticketmanager.command.help") - commandWordReload -> has("ticketmanager.command.reload") - commandWordList -> has("ticketmanager.command.list") - commandWordListAssigned -> has("ticketmanager.command.list") - commandWordReopen -> has("ticketmanager.command.reopen") - commandWordSilentReopen -> has("ticketmanager.command.reopen") && hasSilent() - commandWordSearch -> has("ticketmanager.command.search") - commandWordSetPriority -> has("ticketmanager.command.setPriority") - commandWordSilentSetPriority -> has("ticketmanager.command.setPriority") && hasSilent() - commandWordTeleport -> has("ticketmanager.command.teleport") - commandWordView -> hasDuality("ticketmanager.command.view") - commandWordDeepView -> hasDuality("ticketmanager.command.viewdeep") - commandWordConvertDB -> has("ticketmanager.command.convertDatabase") - commandWordHistory -> - sender.has("ticketmanager.command.history.all") || - sender.has("ticketmanager.command.history.own").let { hasPerm -> - if (args.size >= 2) hasPerm && args[1] == sender.name - else hasPerm - } - else -> true - } - } - .also { if (!it) senderLocale.warningsNoPermission.sendColouredMessageTo(sender) } - } - - private fun isValidCommand( - sender: CommandSender, - ticketHandler: TicketHandler, - args: List, - senderLocale: TMLocale - ): Boolean { - val dbRef = pluginState.database - fun invalidCommand() = senderLocale.warningsInvalidCommand.sendColouredMessageTo(sender) - fun notANumber() = senderLocale.warningsInvalidNumber.sendColouredMessageTo(sender) - fun notATicket() = senderLocale.warningsInvalidID.sendColouredMessageTo(sender) - fun outOfBounds() = senderLocale.warningsPriorityOutOfBounds.sendColouredMessageTo(sender) - fun ticketClosed() = senderLocale.warningsTicketAlreadyClosed.sendColouredMessageTo(sender) - fun ticketOpen() = senderLocale.warningsTicketAlreadyOpen.sendColouredMessageTo(sender) - - return senderLocale.run { - when (args[0]) { - commandWordAssign, commandWordSilentAssign -> - check(::invalidCommand) { args.size >= 3 } - .thenCheck(::notATicket) { dbRef.isValidID(args[1].toInt()) } - .thenCheck(::ticketClosed) { ticketHandler.status != Ticket.Status.CLOSED } - - commandWordClaim, commandWordSilentClaim -> - check(::invalidCommand) { args.size == 2 } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - .thenCheck(::ticketClosed) { ticketHandler.status != Ticket.Status.CLOSED } - - commandWordClose, commandWordSilentClose -> - check(::invalidCommand) { args.size >= 2 } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - .thenCheck(::ticketClosed) { ticketHandler.status != Ticket.Status.CLOSED } - - commandWordComment, commandWordSilentComment -> - check(::invalidCommand) { args.size >= 3 } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - .thenCheck(::ticketClosed) { ticketHandler.status != Ticket.Status.CLOSED } - - commandWordCloseAll, commandWordSilentCloseAll -> - check(::invalidCommand) { args.size == 3 } - .thenCheck(::notANumber) { args[1].toIntOrNull() != null } - .thenCheck(::notANumber) { args[2].toIntOrNull() != null } - - commandWordReopen, commandWordSilentReopen -> - check(::invalidCommand) { args.size == 2 } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - .thenCheck(::ticketOpen) { ticketHandler.status != Ticket.Status.OPEN } - - commandWordSetPriority, commandWordSilentSetPriority -> - check(::invalidCommand) { args.size == 3 } - .thenCheck(::outOfBounds) { args[2].toByteOrNull() != null } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - .thenCheck(::ticketClosed) { ticketHandler.status != Ticket.Status.CLOSED } - - commandWordUnassign, commandWordSilentUnassign -> - check(::invalidCommand) { args.size == 2 } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - .thenCheck(::ticketClosed) { ticketHandler.status != Ticket.Status.CLOSED } - - commandWordView -> - check(::invalidCommand) { args.size == 2 } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - - commandWordDeepView -> - check(::invalidCommand) { args.size == 2 } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - - commandWordTeleport -> - check(::invalidCommand) { args.size == 2 } - .thenCheck(::notATicket) { pluginState.database.isValidID(args[1].toInt()) } - - commandWordCreate -> check(::invalidCommand) { args.size >= 2 } - - commandWordHistory -> - check(::invalidCommand) { args.isNotEmpty() } - .thenCheck(::notANumber) { if (args.size >= 3) args[2].toIntOrNull() != null else true} - - commandWordList -> - check(::notANumber) { if (args.size == 2) args[1].toIntOrNull() != null else true } - - commandWordListAssigned -> - check(::notANumber) { if (args.size == 2) args[1].toIntOrNull() != null else true } - - commandWordSearch -> check(::invalidCommand) { args.size >= 2} - - commandWordReload -> true - commandWordVersion -> true - commandWordHelp -> true - - commandWordConvertDB -> - check(::invalidCommand) { args.size == 2 } - .thenCheck( { senderLocale.warningsInvalidDBType.sendColouredMessageTo(sender) }, - { - try { Database.Types.valueOf(args[1]); true } - catch (e: Exception) { false } - } - ) - .thenCheck( { senderLocale.warningsConvertToSameDBType.sendColouredMessageTo(sender) } ) - { pluginState.database.type != Database.Types.valueOf(args[1]) } - - else -> false.also { invalidCommand() } - } - } - } - - private fun notUnderCooldown( - sender: CommandSender, - senderLocale: TMLocale, - args: List - ): Boolean { - val underCooldown = when (args[0]) { - senderLocale.commandWordCreate, - senderLocale.commandWordComment, - senderLocale.commandWordSilentComment -> - pluginState.cooldowns.checkAndSet(sender.toUUIDOrNull()) - else -> false - } - - return !underCooldown.also { if (it) senderLocale.warningsUnderCooldown.sendColouredMessageTo(sender) } - } - - private fun executeCommand( - sender: CommandSender, - args: List, - senderLocale: TMLocale, - ticketHandler: TicketHandler - ): NotifyParams? { - return senderLocale.run { - when (args[0]) { - commandWordAssign -> assign(sender, args, false, senderLocale, ticketHandler) - commandWordSilentAssign -> assign(sender, args, true, senderLocale, ticketHandler) - commandWordClaim -> claim(sender, args, false, senderLocale, ticketHandler) - commandWordSilentClaim -> claim(sender, args, true, senderLocale, ticketHandler) - commandWordClose -> close(sender, args, false, ticketHandler) - commandWordSilentClose -> close(sender, args, true, ticketHandler) - commandWordCloseAll -> closeAll(sender, args, false) - commandWordSilentCloseAll -> closeAll(sender, args, true) - commandWordComment -> comment(sender, args, false, ticketHandler) - commandWordSilentComment -> comment(sender, args, true, ticketHandler) - commandWordCreate -> create(sender, args) - commandWordHelp -> help(sender, senderLocale).let { null } - commandWordHistory -> history(sender, args, senderLocale).let { null } - commandWordList -> list(sender, args, senderLocale).let { null } - commandWordListAssigned -> listAssigned(sender, args, senderLocale).let { null } - commandWordReload -> reload(sender, senderLocale).let { null } - commandWordReopen -> reopen(sender,args, false, ticketHandler) - commandWordSilentReopen -> reopen(sender,args, true, ticketHandler) - commandWordSearch -> search(sender, args, senderLocale).let { null } - commandWordSetPriority -> setPriority(sender, args, false, ticketHandler) - commandWordSilentSetPriority -> setPriority(sender, args, true, ticketHandler) - commandWordTeleport -> teleport(sender, ticketHandler).let { null } - commandWordUnassign -> unAssign(sender, args, false, senderLocale, ticketHandler) - commandWordSilentUnassign -> unAssign(sender, args, true, senderLocale, ticketHandler) - commandWordVersion -> version(sender, senderLocale).let { null } - commandWordView -> view(sender, senderLocale, ticketHandler).let { null } - commandWordDeepView -> viewDeep(sender, senderLocale, ticketHandler).let { null } - commandWordConvertDB -> convertDatabase(args).let { null } - else -> null - } - } - } - - private fun pushNotifications( - sender: CommandSender, - params: NotifyParams, - locale: TMLocale, - ticketHandler: TicketHandler - ) { - params.run { - if (sendSenderMSG) - senderLambda!!.invoke(locale).sendColouredMessageTo(sender) - - if (sendCreatorMSG) - ticketHandler.creatorUUID - ?.run(Bukkit::getPlayer) - ?.let { creatorLambda!!(getLocale(it)) } - ?.run { sendColouredMessageTo(creator!!) } - - if (sendMassNotifyMSG) - pushMassNotify(massNotifyPerm, massNotifyLambda!!) - } - } - - private class NotifyParams( - silent: Boolean, - ticketHandler: TicketHandler, - sender: CommandSender, - creatorAlertPerm: String, - val massNotifyPerm: String, - val senderLambda: ((TMLocale) -> String)?, - val creatorLambda: ((TMLocale) -> String)?, - val massNotifyLambda: ((TMLocale) -> String)?, - ) { - val creator: Player? = ticketHandler.creatorUUID?.let(Bukkit::getPlayer) - val sendSenderMSG: Boolean = (!sender.has(massNotifyPerm) || silent) - && senderLambda != null - val sendCreatorMSG: Boolean = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) - && !silent && (creator?.isOnline ?: false) - && (creator?.has(creatorAlertPerm) ?: false) - && (creator?.has(massNotifyPerm)?.run { !this } ?: false) - && creatorLambda != null - val sendMassNotifyMSG: Boolean = !silent - && massNotifyLambda != null - } - - /*-------------------------*/ - /* Commands */ - /*-------------------------*/ - - // /ticket assign - - private fun allAssignVariations( - sender: CommandSender, - silent: Boolean, - senderLocale: TMLocale, - assignmentID: String, - assignment: String?, - ticketHandler: TicketHandler - ): NotifyParams { - val shownAssignment = assignment ?: senderLocale.miscNobody - - pluginState.database.run { - setAssignment(ticketHandler.id, assignment) - addAction(ticketHandler.id, Ticket.Action(Ticket.Action.Type.ASSIGN, sender.toUUIDOrNull(), assignment)) - } - - return NotifyParams( - silent = silent, - sender = sender, - ticketHandler = ticketHandler, - senderLambda = { - it.notifyTicketAssignSuccess - .replace("%id%", assignmentID) - .replace("%assign%", shownAssignment) - }, - massNotifyLambda = { - it.notifyTicketAssignEvent - .replace("%user%", sender.name) - .replace("%id%", assignmentID) - .replace("%assign%", shownAssignment) - }, - creatorLambda = null, - creatorAlertPerm = "ticketmanager.notify.change.assign", - massNotifyPerm = "ticketmanager.notify.massNotify.assign" - ) - } - - private fun assign( - sender: CommandSender, - args: List, - silent: Boolean, - senderLocale: TMLocale, - ticketHandler: TicketHandler - ): NotifyParams { - val sqlAssignment = args.subList(2, args.size) - .joinToString(" ") - return allAssignVariations(sender, silent, senderLocale, args[1], sqlAssignment, ticketHandler) - } - - // /ticket claim - private fun claim( - sender: CommandSender, - args: List, - silent: Boolean, - senderLocale: TMLocale, - ticketHandler: TicketHandler - ): NotifyParams { - return allAssignVariations(sender, silent, senderLocale, args[1], sender.name, ticketHandler) - } - - // /ticket close [Comment...] - private fun close( - sender: CommandSender, - args: List, - silent: Boolean, - ticketHandler: TicketHandler - ): NotifyParams { - ticketHandler.statusUpdateForCreator = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates - return if (args.size >= 3) - closeWithComment(sender, args, silent, ticketHandler) - else closeWithoutComment(sender, args, silent, ticketHandler) - } - - private fun closeWithComment( - sender: CommandSender, - args: List, - silent: Boolean, - ticketHandler: TicketHandler - ): NotifyParams { - val message = args.subList(2, args.size) - .joinToString(" ") - .run(::stripColour) - - pluginState.database.addAction( - ticketID = ticketHandler.id, - action = Ticket.Action(Ticket.Action.Type.COMMENT, sender.toUUIDOrNull(), message) - ) - - pluginState.database.addAction( - ticketID = ticketHandler.id, - action = Ticket.Action(Ticket.Action.Type.CLOSE, sender.toUUIDOrNull()) - ) - ticketHandler.status = Ticket.Status.CLOSED - - return NotifyParams( - silent = silent, - sender = sender, - ticketHandler = ticketHandler, - senderLambda = { - it.notifyTicketCloseWCommentSuccess - .replace("%id%", args[1]) - }, - massNotifyLambda = { - it.notifyTicketCloseWCommentEvent - .replace("%user%", sender.name) - .replace("%id%", args[1]) - .replace("%message%", message) - }, - creatorLambda = { - it.notifyTicketModificationEvent - .replace("%id%", args[1]) - }, - massNotifyPerm = "ticketmanager.notify.massNotify.close", - creatorAlertPerm = "ticketmanager.notify.change.close" - ) - } - - private fun closeWithoutComment( - sender: CommandSender, - args: List, - silent: Boolean, - ticketHandler: TicketHandler - ): NotifyParams { - pluginState.database.addAction( - ticketID = ticketHandler.id, - action = Ticket.Action(Ticket.Action.Type.CLOSE, sender.toUUIDOrNull()) - ) - ticketHandler.status = Ticket.Status.CLOSED - - return NotifyParams( - silent = silent, - sender = sender, - ticketHandler = ticketHandler, - creatorLambda = { - it.notifyTicketModificationEvent - .replace("%id%", args[1]) - }, - massNotifyLambda = { - it.notifyTicketCloseEvent // %user% %id% - .replace("%user%", sender.name) - .replace("%id%", args[1]) - }, - senderLambda = { - it.notifyTicketCloseSuccess - .replace("%id%", args[1]) - }, - massNotifyPerm = "ticketmanager.notify.massNotify.close", - creatorAlertPerm = "ticketmanager.notify.change.close" - ) - } - - // /ticket closeall - private fun closeAll( - sender: CommandSender, - args: List, - silent: Boolean - ): NotifyParams { - val lowerBound = args[1].toInt() - val upperBound = args[2].toInt() - - pluginState.database.massCloseTickets(lowerBound, upperBound, sender.toUUIDOrNull()) - - return NotifyParams( - silent = silent, - sender = sender, - ticketHandler = TicketHandler(-1), - creatorLambda = null, - senderLambda = { - it.notifyTicketMassCloseSuccess - .replace("%low%", args[1]) - .replace("%high%", args[2]) - }, - massNotifyLambda = { - it.notifyTicketMassCloseEvent - .replace("%user%", sender.name) - .replace("%low%", args[1]) - .replace("%high%", args[2]) - }, - massNotifyPerm = "ticketmanager.notify.massNotify.massClose", - creatorAlertPerm = "ticketmanager.notify.change.massClose" - ) - } - - // /ticket comment - private fun comment( - sender: CommandSender, - args: List, - silent: Boolean, - ticketHandler: TicketHandler - ): NotifyParams { - val message = args.subList(2, args.size) - .joinToString(" ") - .run(::stripColour) - val nonCreatorMadeChange = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) - - pluginState.database.addAction( - ticketID = ticketHandler.id, - action = Ticket.Action(Ticket.Action.Type.COMMENT, sender.toUUIDOrNull(), message) - ) - - ticketHandler.statusUpdateForCreator = nonCreatorMadeChange && pluginState.allowUnreadTicketUpdates - - return NotifyParams( - silent = silent, - sender = sender, - ticketHandler = ticketHandler, - creatorLambda = { - it.notifyTicketModificationEvent - .replace("%id%", args[1]) - }, - senderLambda = { - it.notifyTicketCommentSuccess - .replace("%id%", args[1]) - }, - massNotifyLambda = { - it.notifyTicketCommentEvent - .replace("%user%", sender.name) - .replace("%id%", args[1]) - .replace("%message%", message) - }, - massNotifyPerm = "ticketmanager.notify.massNotify.comment", - creatorAlertPerm = "ticketmanager.notify.change.comment" - ) - } - - // /ticket convertdatabase - private fun convertDatabase( - args: List, - ) { - val type = args[1].run(Database.Types::valueOf) - pluginState.database.migrateDatabase(type) - } - - // /ticket create - private fun create( - sender: CommandSender, - args: List - ): NotifyParams { - val message = args.subList(1, args.size) - .joinToString(" ") - .run(::stripColour) - val action = Ticket.Action(Ticket.Action.Type.OPEN, sender.toUUIDOrNull(), message) - val ticket = Ticket(sender.toUUIDOrNull(), sender.toLocationOrNull()) - val id = pluginState.database.addTicket(ticket, action).toString() - - mainPlugin.ticketCountMetrics.incrementAndGet() - return NotifyParams( - silent = false, - sender = sender, - ticketHandler = TicketHandler(-1), - creatorLambda = null, - senderLambda = { - it.notifyTicketCreationSuccess - .replace("%id%", id) - }, - massNotifyLambda = { - it.notifyTicketCreationEvent - .replace("%user%", sender.name) - .replace("%id%", id) - .replace("%message%", message) - }, - creatorAlertPerm = "ticketmanager.NO NODE", - massNotifyPerm = "ticketmanager.notify.massNotify.create", - ) - } - - // /ticket help - private fun help(sender: CommandSender, locale: TMLocale) { - val hasSilentPerm = sender.has("ticketmanager.commandArg.silence") - val cc = pluginState.enabledLocales.colourCode - - // Builds base header - val sentComponent = TextComponent(locale.helpHeader) - sentComponent.addExtra(locale.helpLine1) - if (hasSilentPerm) { - sentComponent.addExtra(locale.helpLine2) - sentComponent.addExtra(locale.helpLine3) - } - sentComponent.addExtra(locale.helpSep) - - locale.run { - listOf( // Triple(silence-able, format, permissions) - Triple(true, "$commandWordAssign &f<$parameterID> <$parameterAssignment...>", listOf("ticketmanager.command.assign")), - Triple(true, "$commandWordClaim &f<$parameterID>", listOf("ticketmanager.command.claim")), - Triple(true, "$commandWordClose &f<$parameterID> &7[$parameterComment...]", listOf("ticketmanager.command.close.all", "ticketmanager.command.close.own")), - Triple(true, "$commandWordCloseAll &f<$parameterLowerID> <$parameterUpperID>", listOf("ticketmanager.command.closeAll")), - Triple(true, "$commandWordComment &f<$parameterID> <$parameterComment...>", listOf("ticketmanager.command.comment.all", "ticketmanager.command.comment.own")), - Triple(false, "$commandWordConvertDB &f<$parameterTargetDB>", listOf("ticketmanager.command.convertDatabase")), - Triple(false, "$commandWordCreate &f<$parameterComment...>", listOf("ticketmanager.command.create")), - Triple(false, commandWordHelp, listOf("ticketmanager.command.help")), - Triple(false, "$commandWordHistory &7[$parameterUser] [$parameterPage]", listOf("ticketmanager.command.history.all", "ticketmanager.command.history.own")), - Triple(false, "$commandWordList &7[$parameterPage]", listOf("ticketmanager.command.list")), - Triple(false, "$commandWordListAssigned &7[$parameterPage]", listOf("ticketmanager.command.list")), - Triple(false, commandWordReload, listOf("ticketmanager.command.reload")), - Triple(true, "$commandWordReopen &f<$parameterID>", listOf("ticketmanager.command.reopen")), - Triple(false, "$commandWordSearch &f<$parameterConstraints...>", listOf("ticketmanager.command.search")), - Triple(true, "$commandWordSetPriority &f<$parameterID> <$parameterLevel>", listOf("ticketmanager.command.setPriority")), - Triple(false, "$commandWordTeleport &f<$parameterID>", listOf("ticketmanager.command.teleport")), - Triple(true, "$commandWordUnassign &f<$parameterID>", listOf("ticketmanager.command.assign")), - Triple(false, "$commandWordView &f<$parameterID>", listOf("ticketmanager.command.view.all", "ticketmanager.command.view.own")), - Triple(false, "$commandWordDeepView &f<$parameterID>", listOf("ticketmanager.command.viewdeep.all", "ticketmanager.command.viewdeep.own")) - ) - } - .filter { it.third.any(sender::has) } - .run { this + Triple(false, locale.commandWordVersion, "NA") } - .map { - val commandString = "$cc/${locale.commandBase} ${it.second}" - if (hasSilentPerm) - if (it.first) "\n&a[✓] $commandString" - else "\n&c[✕] $commandString" - else "\n$commandString" - } - .map(::toColour) - .map(::TextComponent) - .forEach { sentComponent.addExtra(it) } - - sender.sendPlatformMessage(sentComponent) - } - - // /ticket history [User] [Page] - private fun history( - sender: CommandSender, - args: List, - locale: TMLocale - ) { - val targetName = if (args.size >= 2) args[1].takeIf { it != locale.consoleName } else sender.name.takeIf { sender is Player } - val requestedPage = if (args.size >= 3) args[2].toInt() else 1 - - // Leaves console as null. Otherwise attempts UUID grab or [PLAYERNOTFOUND] - fun String.attemptToUUIDString(): String? = - if (equals(locale.consoleName)) null - else Bukkit.getOfflinePlayers().asSequence() - .firstOrNull { equals(it.name) } - ?.run { uniqueId.toString() } - ?: "[PLAYERNOTFOUND]" - - - val searchedUser = targetName?.attemptToUUIDString() - - val resultSize: Int - val resultsChunked = pluginState.database.searchDatabase { it.creatorUUID?.toString() == searchedUser } - .sortedByDescending(Ticket::id) - .also { resultSize = it.size } - .chunked(6) - - val sentComponent = locale.historyHeader - .replace("%name%", targetName ?: locale.consoleName) - .replace("%count%", "$resultSize") - .run(::toColour) - .run(::TextComponent) - - val actualPage = if (requestedPage >= 1 && requestedPage < resultsChunked.size) requestedPage else 1 - - if (resultsChunked.isNotEmpty()) { - resultsChunked.getOrElse(requestedPage - 1) { resultsChunked[1] } - .forEach { - locale.historyEntry - .run { "\n$this" } - .replace("%id%", "${it.id}") - .replace("%SCC%", it.status.getColourCode()) - .replace("%status%", it.status.toColouredString(locale)) - .replace("%comment%", it.actions[0].message!! - .run { if (length > 80) "${substring(0, 81)}..." else this } ) - .run(::toColour) - .run(::TextComponent) - .apply { addViewTicketOnClick(it.id, locale) } - .apply { sentComponent.addExtra(this) } - } - - if (resultsChunked.size > 1) { - buildPageComponent(actualPage, resultsChunked.size, locale) { "/${it.commandBase} ${it.commandWordHistory} ${targetName ?: it.consoleName} " } - .apply(sentComponent::addExtra) - } - } - - sender.sendPlatformMessage(sentComponent) - } - - // /ticket list [Page] - private fun list( - sender: CommandSender, - args: List, - locale: TMLocale - ) { - val sentMSG = createGeneralList(args, locale, locale.listFormatHeader, - getTickets = { db -> db.getOpen() }, - baseCommand = locale.run{ { "/$commandBase $commandWordList " } } - ) - - sender.sendPlatformMessage(sentMSG) - } - - private fun createGeneralList( - args: List, - locale: TMLocale, - headerFormat: String, - getTickets: (Database) -> List, - baseCommand: (TMLocale) -> String - ): TextComponent { - val chunkedTickets = getTickets(pluginState.database).chunked(8) - val page = if (args.size == 2 && args[1].toInt() in 1..chunkedTickets.size) args[1].toInt() else 1 - - val sentMSG = headerFormat - .run(::toColour) - .run(::TextComponent) - - if (chunkedTickets.isNotEmpty()) { - chunkedTickets[page - 1] - .map { createListEntry(it, locale) } - .forEach { sentMSG.addExtra(it) } - } - - if (chunkedTickets.size > 1) { - sentMSG.addExtra(buildPageComponent(page, chunkedTickets.size, locale, baseCommand)) - } - - return sentMSG - } - - private fun buildPageComponent( - curPage: Int, - pageCount: Int, - locale: TMLocale, - baseCommand: (TMLocale) -> String, - ): TextComponent { - fun TextComponent.addForward() { - this.color = ChatColor.WHITE - this.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand(locale) + "${curPage + 1}") - this.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickNextPage)) - } - fun TextComponent.addBack() { - this.color = ChatColor.WHITE - this.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand(locale) + "${curPage - 1}") - this.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickBackPage)) - } - - val back = TextComponent("[${locale.pageBack}]") - val next = TextComponent("[${locale.pageNext}]") - - val separator = TextComponent("...............") - separator.color = ChatColor.DARK_GRAY - - val cc = pluginState.enabledLocales.colourCode - val ofSection = "$cc($curPage${locale.pageOf}$pageCount)" - .run(::toColour) - .run(::TextComponent) - - when (curPage) { - 1 -> { - back.color = ChatColor.DARK_GRAY - next.addForward() - } - pageCount -> { - back.addBack() - next.color = ChatColor.DARK_GRAY - } - else -> { - back.addBack() - next.addForward() - } - } - - val textComponent = TextComponent("\n") - textComponent.apply { - addExtra(back) - addExtra(separator) - addExtra(ofSection) - addExtra(separator) - addExtra(next) - } - - return textComponent - } - - - private fun createListEntry(ticket: Ticket, locale: TMLocale): TextComponent { - val creator = ticket.creatorUUID.toName(locale) - val fixedAssign = ticket.assignedTo ?: "" - - // Shortens comment preview to fit on one line - val fixedComment = ticket.run { - if (12 + id.toString().length + creator.length + fixedAssign.length + actions[0].message!!.length > 58) - actions[0].message!!.substring(0, 43 - id.toString().length - fixedAssign.length - creator.length) + "..." - else actions[0].message!! - } - - return locale.listFormatEntry - .run { "\n$this" } - .replace("%priorityCC%", ticket.priority.getColourCode()) - .replace("%ID%", "${ticket.id}") - .replace("%creator%", creator) - .replace("%assign%", fixedAssign) - .replace("%comment%", fixedComment) - .run(::toColour) - .run(::TextComponent) - .apply { addViewTicketOnClick(ticket.id, locale) } - } - - // /ticket listassigned [Page] - private fun listAssigned( - sender: CommandSender, - args: List, - locale: TMLocale - ) { - val groups = if (sender is Player) - mainPlugin.perms.getPlayerGroups(sender).map { "::$it" } - else listOf() - - val sentMSG = createGeneralList(args, locale, locale.listFormatAssignedHeader, - getTickets = { db -> db.getOpenAssigned(sender.name, groups ) }, - baseCommand = locale.run { { "/$commandBase $commandWordListAssigned " } } - ) - - sender.sendPlatformMessage(sentMSG) - } - - // /ticket reload - private fun reload(sender: CommandSender, locale: TMLocale) { - mainPlugin.pluginLocked = true - pushMassNotify("ticketmanager.notify.info", { it.informationReloadInitiated.replace("%user%", sender.name) } ) - - // Forces async thread to wait for other TicketManager tasks to complete - while (Bukkit.getScheduler().pendingTasks.filter { it.owner == mainPlugin }.size > 2) - Thread.sleep(1000) - - pushMassNotify("ticketmanager.notify.info", { it.informationReloadTasksDone } ) - pluginState.database.closeDatabase() - mainPlugin.configState = PluginState() - - pushMassNotify("ticketmanager.notify.info", { it.informationReloadSuccess }) - if (!sender.has("ticketmanager.notify.info")) - locale.informationReloadSuccess.sendColouredMessageTo(sender) - - mainPlugin.pluginLocked = false - } - - // /ticket reopen - private fun reopen( - sender: CommandSender, - args: List, - silent: Boolean, - ticketHandler: TicketHandler - ): NotifyParams { - val action = Ticket.Action(Ticket.Action.Type.REOPEN, sender.toUUIDOrNull()) - - ticketHandler.statusUpdateForCreator = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates - pluginState.database.addAction(ticketHandler.id, action) - ticketHandler.status = Ticket.Status.OPEN - - return NotifyParams( - silent = silent, - sender = sender, - ticketHandler = ticketHandler, - creatorLambda = { - it.notifyTicketModificationEvent - .replace("%id%", args[1]) - }, - senderLambda = { - it.notifyTicketReopenSuccess - .replace("%id%", args[1]) - }, - massNotifyLambda = { - it.notifyTicketReopenEvent - .replace("%user%", sender.name) - .replace("%id%", args[1]) - }, - creatorAlertPerm = "ticketmanager.notify.change.reopen", - massNotifyPerm = "ticketmanager.notify.massNotify.reopen", - ) - } - - // /ticket search - private fun search( - sender: CommandSender, - args: List, - locale: TMLocale - ) { - fun String.attemptToUUIDString(): String? = - if (equals(locale.consoleName)) null - else Bukkit.getOfflinePlayers().asSequence() - .firstOrNull { equals(it.name) } - ?.run { uniqueId.toString() } - ?: "[PLAYERNOTFOUND]" - - - // Beginning of code execution - sender.sendMessage(locale.searchFormatQuerying) - val constraintTypes = locale.run { - listOf( - searchAssigned, - searchCreator, - searchKeywords, - searchPriority, - searchStatus, - searchTime, - searchWorld, - searchPage, - searchClosedBy, - searchLastClosedBy, - ) - } - - val localedConstraintMap = args.subList(1, args.size) - .asSequence() - .map { it.split(":", limit = 2) } - .filter { it[0] in constraintTypes } - .filter { it.size >= 2 } - .associate { it[0] to it[1] } - - val searchFunctions = localedConstraintMap - .mapNotNull { entry -> - when (entry.key) { - locale.searchWorld -> { t: Ticket -> t.location?.world?.equals(entry.value) ?: false } - locale.searchAssigned -> { t: Ticket -> t.assignedTo == entry.value } - - locale.searchCreator -> { - val searchedUser = entry.value.attemptToUUIDString(); - { t: Ticket -> t.creatorUUID?.toString() == searchedUser } - } - - locale.searchPriority -> { - val searchedPriority = entry.value.toByteOrNull() ?: 0; - { t: Ticket -> t.priority.level == searchedPriority } - } - - locale.searchTime -> { - val creationTime = relTimeToEpochSecond(entry.value, locale); - { t: Ticket -> t.actions[0].timestamp >= creationTime } - } - - locale.searchStatus -> { - val constraintStatus = when (entry.value) { - locale.statusOpen -> Ticket.Status.OPEN.name - locale.statusClosed -> Ticket.Status.CLOSED.name - else -> entry.value - } - { t: Ticket -> t.status.name == constraintStatus} - } - - locale.searchKeywords -> { - val words = entry.value.split(","); - - { t: Ticket -> - val comments = t.actions - .filter { it.type == Ticket.Action.Type.OPEN || it.type == Ticket.Action.Type.COMMENT } - .map { it.message!! } - words.map { w -> comments.any { it.contains(w) } } - .all { it } - } - } - - locale.searchLastClosedBy -> { - val searchedUser = entry.value.attemptToUUIDString(); - { t: Ticket -> - t.actions.lastOrNull { e -> e.type == Ticket.Action.Type.CLOSE } - ?.run { user?.toString() == searchedUser } - ?: false - } - - } - - locale.searchClosedBy -> { - val searchedUser = entry.value.attemptToUUIDString(); - { t: Ticket -> t.actions.any{ it.type == Ticket.Action.Type.CLOSE && it.user?.toString() == searchedUser } } - } - - else -> null - } - } - .asSequence() - - val composedSearch = { t: Ticket -> searchFunctions.map { it(t) }.all { it } } - - - // Results computation - val resultSize: Int - val chunkedTickets = pluginState.database.searchDatabase(composedSearch) - .sortedByDescending(Ticket::id) - .apply { resultSize = size } - .chunked(8) - - val page = localedConstraintMap[locale.searchPage]?.toIntOrNull() - .let { if (it != null && it >= 1 && it < chunkedTickets.size) it else 1 } - - // Initial header - val sentComponent = TextComponent(locale.searchFormatHeader - .replace("%size%", "$resultSize") - .run(::toColour) - ) - - // Function for fixing messageLength - val fixMSGLength = { t: Ticket -> t.actions[0].message!!.run { if (length > 25) "${substring(0,21)}..." else this } } - - // Adds entries - if (chunkedTickets.isNotEmpty()) { - chunkedTickets[page-1] - .map { - locale.searchFormatEntry - .run { "\n$this" } - .replace("%PCC%", it.priority.getColourCode()) - .replace("%SCC%", it.status.getColourCode()) - .replace("%id%", "${it.id}") - .replace("%status%", it.status.toColouredString(locale)) - .replace("%creator%", it.creatorUUID.toName(locale)) - .replace("%assign%", it.assignedTo ?: "") - .replace("%world%", it.location?.world ?: "") - .replace("%time%", it.actions[0].timestamp.toLargestRelativeTime(locale)) - .run(::toColour) - .replace("%comment%", fixMSGLength(it)) - .run(::TextComponent) - .apply { addViewTicketOnClick(it.id, locale) } - } - .forEach { sentComponent.addExtra(it) } - } - - // Implements pages if needed - if (chunkedTickets.size > 1) { - val pageComponent = buildPageComponent(page, chunkedTickets.size, locale) { - // Removes page constraint and converts rest to key:arg - val constraints = localedConstraintMap - .filter { it.key != locale.searchPage } - .map { (k,v) -> "$k:$v" } - "/${locale.commandBase} ${locale.commandWordSearch} $constraints ${locale.searchPage}:" - } - sentComponent.addExtra(pageComponent) - } - sender.sendPlatformMessage(sentComponent) - } - - // /ticket setpriority - private fun setPriority( - sender: CommandSender, - args: List, - silent: Boolean, - ticketHandler: TicketHandler - ): NotifyParams { - pluginState.database.addAction( - ticketID = ticketHandler.id, - action = Ticket.Action(Ticket.Action.Type.SET_PRIORITY, sender.toUUIDOrNull(), args[2]) - ) - ticketHandler.priority = byteToPriority(args[2].toByte()) - - return NotifyParams( - silent = silent, - sender = sender, - ticketHandler = ticketHandler, - creatorLambda = null, - senderLambda = { - it.notifyTicketSetPrioritySuccess - .replace("%id%", args[1]) - .replace("%priority%", ticketHandler.priority.toColouredString(it)) - }, - massNotifyLambda = { - it.notifyTicketSetPriorityEvent - .replace("%user%", sender.name) - .replace("%id%", args[1]) - .replace("%priority%", ticketHandler.priority.toColouredString(it)) - }, - creatorAlertPerm = "ticketmanager.notify.change.priority", - massNotifyPerm = "ticketmanager.notify.massNotify.priority" - ) - } - - // /ticket teleport - private fun teleport(sender: CommandSender, ticketHandler: TicketHandler) { - if (sender is Player) - ticketHandler.location?.let { - Bukkit.getScheduler().runTask(mainPlugin, - Runnable { sender.teleport(it) } - ) - } - } - - // /ticket unassign - private fun unAssign( - sender: CommandSender, - args: List, - silent: Boolean, - senderLocale: TMLocale, - ticketHandler: TicketHandler - ): NotifyParams { - return allAssignVariations(sender, silent, senderLocale, args[1], null, ticketHandler) - } - - // /ticket version - private fun version( - sender: CommandSender, - locale: TMLocale - ) { - val sentComponent = TextComponent("") - val components = listOf( - "&3===========================&r\n", - "&3&lTicketManager:&r&7 by HoshiKurama&r\n", - " &3GitHub Wiki: ", - "&7&nHERE&r\n", - " &3V4.0.0&r\n", - "&3===========================&r" - ) - .map(::toColour) - .map(::TextComponent) - - components[3].clickEvent = ClickEvent(ClickEvent.Action.OPEN_URL, locale.wikiLink) - components[3].hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickWiki)) - - components.forEach { sentComponent.addExtra(it) } - - sender.sendPlatformMessage(sentComponent) - } - - // /ticket view - private fun view( - sender: CommandSender, - locale: TMLocale, - ticketHandler: TicketHandler - ) { - val ticket = pluginState.database.getTicket(ticketHandler.id)!! - val message = buildTicketInfo(ticket, locale) - - if (!sender.nonCreatorMadeChange(ticket.creatorUUID)) - ticketHandler.statusUpdateForCreator = false - - ticket.actions.asSequence() - .filter { it.type == Ticket.Action.Type.COMMENT || it.type == Ticket.Action.Type.OPEN } - .map { locale.viewFormatComment - .run { "\n$this" } - .run(::toColour) - .replace("%user%", it.user.toName(locale)) - .replace("%comment%", it.message!!) - } - .map(::TextComponent) - .forEach { message.addExtra(it) } - - sender.sendPlatformMessage(message) - } - - private fun buildTicketInfo(ticket: Ticket, locale: TMLocale): TextComponent { - val message = TextComponent("") - - val textComponents = listOf( - locale.viewFormatHeader - .replace("%num%", "${ticket.id}"), - locale.viewFormatSep1, - locale.viewFormatInfo1 - .replace("%creator%", ticket.creatorUUID.toName(locale)) - .replace("%assignment%", ticket.assignedTo ?: ""), - locale.viewFormatInfo2 - .replace("%priority%", ticket.priority.toColouredString(locale)) - .replace("%status%", ticket.status.toColouredString(locale)), - locale.viewFormatInfo3 - .replace("%location%", ticket.location?.toString() ?: ""), - locale.viewFormatSep2 - ) - .map(::toColour) - .map { "\n$it" } - .map(::TextComponent) - - if (ticket.location != null) { - textComponents[4].hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickTeleport)) - textComponents[4].clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, - locale.run { "/$commandBase $commandWordTeleport ${ticket.id}" }) - textComponents[4] - } - - textComponents.forEach { message.addExtra(it) } - return message - } - - // /ticket viewdeep - private fun viewDeep( - sender: CommandSender, - locale: TMLocale, - ticketHandler: TicketHandler - ) - { - val ticket = pluginState.database.getTicket(ticketHandler.id)!! - val sentMessage = buildTicketInfo(ticket, locale) - - if (!sender.nonCreatorMadeChange(ticket.creatorUUID)) - ticketHandler.statusUpdateForCreator = false - - fun Ticket.Action.formatDeepAction(): String { - val result = when (type) { - Ticket.Action.Type.OPEN, Ticket.Action.Type.COMMENT -> - locale.viewFormatDeepComment - .replace("%comment%", message!!) - - Ticket.Action.Type.SET_PRIORITY -> - locale.viewFormatDeepSetPriority - .replace("%priority%", byteToPriority(message!!.toByte()) - .toColouredString(locale)) - - Ticket.Action.Type.ASSIGN -> - locale.viewFormatDeepAssigned - .replace("%assign%", message ?: "") - - Ticket.Action.Type.REOPEN -> locale.viewFormatDeepReopen - Ticket.Action.Type.CLOSE -> locale.viewFormatDeepClose - Ticket.Action.Type.MASS_CLOSE -> locale.viewFormatDeepMassClose - } - return result - .replace("%user%", user.toName(locale)) - .replace("%time%", timestamp.toLargestRelativeTime(locale)) - } - - ticket.actions - .map { it.formatDeepAction() } - .map { "\n$it" } - .map(::toColour) - .map(::TextComponent) - .forEach { sentMessage.addExtra(it) } - - sender.sendPlatformMessage(sentMessage) - } -} - -/*-------------------------*/ -/* Other Functions */ -/*-------------------------*/ - -private inline fun check(error: () -> Unit, predicate: () -> Boolean): Boolean { - return if (predicate()) true else error().run { false } -} - -private inline fun Boolean.thenCheck(error: () -> Unit, predicate: () -> Boolean): Boolean { - return if (!this) false - else if (predicate()) true - else error().run { false } -} - -private fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null -private fun CommandSender.toLocationOrNull() = if (this is Player) Ticket.Location(this.location) else null - -private fun CommandSender.nonCreatorMadeChange(creatorUUID: UUID?): Boolean { - if (creatorUUID == null) return false - return this.toUUIDOrNull()?.notEquals(creatorUUID) ?: true -} - -fun T.notEquals(t: T) = this != t \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/PlayerJoin.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/PlayerJoin.kt deleted file mode 100644 index 8bd600e..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/PlayerJoin.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.hoshikurama.github.ticketmanager.events - -import com.hoshikurama.github.ticketmanager.* -import org.bukkit.event.EventHandler -import org.bukkit.event.Listener -import org.bukkit.event.player.PlayerJoinEvent - -class PlayerJoin : Listener { - - @EventHandler - fun onPlayerJoin(event: PlayerJoinEvent) { - if (anyLocksPresent()) return - val player = event.player - - // Plugin Update checking - val pluginUpdateStatus = pluginState.updateAvailable.get() - if (player.has("ticketmanager.notify.pluginUpdate") && pluginUpdateStatus != null) { - if (pluginUpdateStatus.first.replace(".", "").toInt() < pluginUpdateStatus.second.replace(".", "").toInt()) { - getLocale(player).notifyPluginUpdate - .replace("%current%", pluginUpdateStatus.first) - .replace("%latest%", pluginUpdateStatus.second) - .run { sendColouredMessageTo(player) } - } - } - - // Unread Updates - if (player.has("ticketmanager.notify.unreadUpdates.onJoin")) { - pluginState.database.getTicketIDsWithUpdates(player.uniqueId) - .run { if (size == 0) null else this } - ?.run { - val template = - if (size == 1) getLocale(player).notifyUnreadUpdateSingle - else getLocale(player).notifyUnreadUpdateMulti - val tickets = this.joinToString(", ") - - template.replace("%num%", tickets) - .sendColouredMessageTo(player) - } - } - - // View Open-Count and Assigned-Count Tickets - if (player.has("ticketmanager.notify.openTickets.onJoin")) { - val open = pluginState.database.getOpen().size.toString() - val assigned = - pluginState.database.getOpenAssigned(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) - .count().toString() - - getLocale(player).run { notifyOpenAssigned } - .replace("%open%", open) - .replace("%assigned%", assigned) - .sendColouredMessageTo(player) - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/TabCompletion.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/TabCompletion.kt deleted file mode 100644 index 8441b18..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/events/TabCompletion.kt +++ /dev/null @@ -1,292 +0,0 @@ -package com.hoshikurama.github.ticketmanager.events - -import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent -import com.hoshikurama.github.ticketmanager.TMLocale -import com.hoshikurama.github.ticketmanager.databases.Database -import com.hoshikurama.github.ticketmanager.getLocale -import com.hoshikurama.github.ticketmanager.has -import com.hoshikurama.github.ticketmanager.mainPlugin -import org.bukkit.Bukkit -import org.bukkit.World -import org.bukkit.command.Command -import org.bukkit.command.CommandSender -import org.bukkit.command.TabCompleter -import org.bukkit.entity.Player -import org.bukkit.event.EventHandler -import org.bukkit.event.Listener - -class TabCompletePaper: Listener { - @EventHandler - fun onTabCompleteAsync(event: AsyncTabCompleteEvent) { - if (event.buffer.startsWith("/ticket ")) { - val args = event.buffer - .replace(" +".toRegex(), " ") - .split(" ") - .run { subList(1, this.size) } - - event.completions = tabCompleteEventFunction(event.sender, args)?.toMutableList() ?: mutableListOf("") - } - } -} - -class TabCompleteSpigot : TabCompleter { - override fun onTabComplete(sender: CommandSender, command: Command, alias: String, args: Array - ): MutableList? = tabCompleteEventFunction(sender, args.toList())?.toMutableList() -} - -private fun tabCompleteEventFunction(sender: CommandSender, args: List): List? { - if (!sender.has("ticketmanager.commandArg.autotab")) return null - val locale = getLocale(sender) - val perms = LazyPermissions(locale,sender) - - return locale.run { - if (args.size <= 1) return@run perms.getPermissiveCommands() - .filter { it.startsWith(args[0]) } - - when (args[0]) { - commandWordAssign, commandWordSilentAssign -> when { // /ticket assign - !perms.hasAssignVariation -> listOf("") - args.size == 2 -> listOf("<$parameterID>") - .filter { it.startsWith(args[1]) } - args.size == 3 -> { - val groups = mainPlugin.perms.groups.map { "::$it" } - (listOf("<$parameterAssignment...>") + offlinePlayerNames() + groups + listOf(locale.consoleName)) - .filter { it.startsWith(args[2]) } - } - else -> listOf("") - } - - commandWordClaim, commandWordSilentClaim, commandWordUnassign, commandWordSilentUnassign -> when { // /ticket claim - !perms.hasAssignVariation -> listOf("") - args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } - else -> listOf("") - } - - commandWordClose, commandWordSilentClose -> when { // /ticket close [Comment...] - !perms.hasClose -> listOf("") - args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } - else -> (listOf("[$parameterComment...]") + onlineSeenPlayers(sender)) - .filter { it.startsWith(args[args.lastIndex]) } - } - - commandWordCloseAll, commandWordSilentCloseAll -> when { // /ticket closeall - !perms.hasMassClose -> listOf("") - args.size == 2 -> listOf("<$parameterLowerID>").filter { it.startsWith(args[1]) } - args.size == 3 -> listOf("<$parameterUpperID>").filter { it.startsWith(args[2]) } - else -> listOf("") - } - - commandWordComment, commandWordSilentComment -> when { // /ticket comment - !perms.hasComment -> listOf("") - args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } - else -> (listOf("<$parameterComment...>") + onlineSeenPlayers(sender)) - .filter { it.startsWith(args[args.lastIndex]) } - } - - commandWordCreate -> when { // /ticket create - !perms.hasCreate -> listOf("") - else -> (listOf("<$parameterComment...>") + onlineSeenPlayers(sender)) - .filter { it.startsWith(args[args.lastIndex]) } - } - - commandWordHelp -> listOf("") - - commandWordHistory -> when { // /ticket history [User] [Page] - !perms.hasHistory -> listOf("") - args.size == 2 -> (listOf("[$parameterUser]", locale.consoleName) + offlinePlayerNames()) - .filter { it.startsWith(args[1]) } - args.size == 3 -> listOf("[$parameterPage]").filter { it.startsWith(args[2]) } - else -> listOf("") - } - - commandWordList, commandWordListAssigned -> when { // /ticket list(assigned) [Page] - !perms.hasListVariation -> listOf("") - args.size == 2 -> listOf("[$parameterPage]").filter { it.startsWith(args[1]) } - else -> listOf("") - } - - commandWordReopen, commandWordSilentReopen -> when { // /ticket reopen - !perms.hasReopen -> listOf("") - args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } - else -> listOf("") - } - - commandWordSetPriority, commandWordSilentSetPriority -> when { // /ticket setpriority - !perms.hasPriority -> listOf("") - args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } - args.size == 3 -> listOf("<$parameterLevel>", "1", "2", "3", "4", "5").filter { it.startsWith(args[2]) } - else -> listOf("") - } - - commandWordTeleport -> when { // /ticket teleport - !perms.hasTeleport -> listOf("") - args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } - else -> listOf("") - } - - commandWordVersion -> listOf("") - - commandWordView -> when { // /ticket view - !perms.hasView -> listOf("") - args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } - else -> listOf("") - } - - commandWordDeepView -> when { - !perms.hasDeepView -> listOf("") - args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } - else -> listOf("") - } - - commandWordReload -> listOf("") - - commandWordSearch -> { //ticket search keywords:separated,by,commas status:OPEN/CLOSED time:5w creator:creator priority:value assignedto:player world:world - if (!perms.hasSearch) return listOf("") - - val curArgument = args[args.lastIndex] - val splitArgs = curArgument.split(":", limit = 2) - - if (splitArgs.size < 2) - return locale.run { listOf( - "$searchAssigned:", - "$searchCreator:", - "$searchKeywords:", - "$searchPriority:", - "$searchStatus:", - "$searchWorld:", - "$searchClosedBy:", - "$searchLastClosedBy:", - ) } - .filter { it.startsWith(curArgument) } - - // String now has form "constraint:" - return when (splitArgs[0]) { - searchAssigned -> { - val groups = mainPlugin.perms.groups.map { "::$it" } - (offlinePlayerNames() + groups) - .filter { it.startsWith(splitArgs[1]) } - .map { "${splitArgs[0]}:$it" } - } - - searchCreator, searchLastClosedBy, searchClosedBy -> { - (offlinePlayerNames() + listOf(locale.consoleName)) - .filter { it.startsWith(splitArgs[1]) } - .map { "${splitArgs[0]}:$it" } - } - - searchPriority -> { - listOf("1", "2", "3", "4", "5") - .filter { it.startsWith(splitArgs[1]) } - .map { "${splitArgs[0]}:$it" } - } - - locale.searchStatus -> { - listOf(locale.statusOpen, locale.statusClosed) - .filter { it.startsWith(splitArgs[1]) } - .map { "${splitArgs[0]}:$it" } - } - - searchWorld -> { - Bukkit.getWorlds() - .map(World::getName) - .filter { it.startsWith(splitArgs[1]) } - .map { "${splitArgs[0]}:$it" } - } - - searchTime -> { - locale.run { listOf(searchTimeSecond, searchTimeMinute, searchTimeHour, searchTimeDay, searchTimeWeek, searchTimeYear) } - .filter { curArgument[curArgument.lastIndex].digitToIntOrNull() != null } - .map { "${splitArgs[0]}:$it" } - } - - searchKeywords -> listOf(curArgument) - - else -> listOf("") - } - } - - commandWordConvertDB -> when { // /ticket convertDatabase - !perms.hasConvertDB -> listOf("") - args.size == 2 -> - Database.Types.values() - .map(Database.Types::name) - .filter { it.startsWith(args[1]) } - else -> listOf("") - } - - else -> listOf("") - } - } -} - -fun onlineSeenPlayers(sender: CommandSender): List { - return if (sender is Player) - Bukkit.getOnlinePlayers() - .filter(sender::canSee) - .map { it.name } - else Bukkit.getOnlinePlayers() - .map { it.name } -} - -fun offlinePlayerNames() = Bukkit.getOfflinePlayers().mapNotNull { it.name }.toList() - -class LazyPermissions(private val locale: TMLocale, private val sender: CommandSender) { - val hasAssignVariation by lazy { sender.has("ticketmanager.command.assign") } - val hasCreate by lazy { sender.has("ticketmanager.command.create") } - val hasListVariation by lazy { sender.has("ticketmanager.command.list") } - val hasReopen by lazy { sender.has("ticketmanager.command.reopen") } - val hasSearch by lazy { sender.has("ticketmanager.command.search") } - val hasPriority by lazy { sender.has("ticketmanager.command.setPriority") } - val hasTeleport by lazy { sender.has("ticketmanager.command.teleport") } - val hasMassClose by lazy { sender.has("ticketmanager.command.closeAll") } - val hasConvertDB by lazy { sender.has("ticketmanager.command.convertDatabase") } - private val hasHelp by lazy { sender.has("ticketmanager.command.help") } - private val hasReload by lazy { sender.has("ticketmanager.command.reload") } - private val hasSilent by lazy { sender.has("ticketmanager.commandArg.silence") } - val hasClose by lazy { sender.has("ticketmanager.command.close.all") - || sender.has("ticketmanager.command.close.own") } - val hasComment by lazy { sender.has("ticketmanager.command.comment.all") - || sender.has("ticketmanager.command.comment.own") } - val hasView by lazy { sender.has("ticketmanager.command.view.all") - || sender.has("ticketmanager.command.view.own") } - val hasDeepView by lazy { sender.has("ticketmanager.command.viewdeep.all") - || sender.has("ticketmanager.command.viewdeep.own") } - val hasHistory by lazy { sender.has("ticketmanager.command.history.all") - || sender.has("ticketmanager.command.history.own") } - - - fun getPermissiveCommands(): List { - return mapOf( - locale.commandWordAssign to hasAssignVariation, - locale.commandWordSilentAssign to (hasAssignVariation && hasSilent), - locale.commandWordClaim to hasAssignVariation, - locale.commandWordSilentClaim to (hasAssignVariation && hasSilent), - locale.commandWordClose to hasClose, - locale.commandWordSilentClose to (hasClose && hasSilent), - locale.commandWordCloseAll to hasMassClose, - locale.commandWordSilentCloseAll to (hasMassClose && hasSilent), - locale.commandWordComment to hasComment, - locale.commandWordSilentComment to (hasComment && hasSilent), - locale.commandWordConvertDB to hasConvertDB, - locale.commandWordCreate to hasCreate, - locale.commandWordHelp to hasHelp, - locale.commandWordHistory to hasHistory, - locale.commandWordList to hasListVariation, - locale.commandWordListAssigned to hasListVariation, - locale.commandWordReload to hasReload, - locale.commandWordReopen to hasReopen, - locale.commandWordSilentReopen to (hasReopen && hasSilent), - locale.commandWordSearch to hasSearch, - locale.commandWordSetPriority to hasPriority, - locale.commandWordSilentSetPriority to (hasPriority && hasSilent), - locale.commandWordTeleport to hasTeleport, - locale.commandWordUnassign to hasAssignVariation, - locale.commandWordSilentUnassign to (hasAssignVariation && hasSilent), - locale.commandWordVersion to true, - locale.commandWordView to hasView, - locale.commandWordDeepView to hasDeepView - ) - .filter { it.value } - .map { it.key } - } -} \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/ticket/Ticket.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/ticket/Ticket.kt deleted file mode 100644 index c6654c8..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/ticket/Ticket.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.hoshikurama.github.ticketmanager.ticket - -import com.hoshikurama.github.ticketmanager.TMLocale -import org.bukkit.Bukkit -import org.bukkit.Location -import java.time.Instant -import java.util.* - -class Ticket( - val creatorUUID: UUID?, // UUID if player, null if Console - val location: Location?, // Location if player, null if Console - val actions: List = listOf(), // List of actions - val priority: Priority = Priority.NORMAL, // Priority 1-5 or Lowest to Highest - val status: Status = Status.OPEN, // Status OPEN or CLOSED - val assignedTo: String? = null, // Null if not assigned to anybody - val statusUpdateForCreator: Boolean = false, // Determines whether player should be notified - val id: Int = -1, // Ticket ID 1+... -1 placeholder during ticket creation - ) { - - enum class Priority(val level: Byte) { - LOWEST(1), LOW(2), NORMAL(3), HIGH(4), HIGHEST(5) - } - enum class Status { - OPEN, CLOSED - } - - data class Action(val type: Type, val user: UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { - enum class Type { - ASSIGN, CLOSE, COMMENT, OPEN, REOPEN, SET_PRIORITY, MASS_CLOSE - } - } - data class Location(val world: String, val x: Int, val y: Int, val z: Int) { - constructor(bukkitLoc: org.bukkit.Location) : this(bukkitLoc.world!!.name, bukkitLoc.blockX, bukkitLoc.blockY, bukkitLoc.blockZ) - constructor(split: List) : this(split[0], split[1].toInt(), split[2].toInt(), split[3].toInt()) - - override fun toString() = "$world $x $y $z" - } -} - - -fun UUID?.toName(locale: TMLocale): String { - if (this == null) return locale.consoleName - return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" -} - -fun Ticket.Priority.getColourCode() = when(this) { - Ticket.Priority.LOWEST -> "&1" - Ticket.Priority.LOW -> "&9" - Ticket.Priority.NORMAL -> "&e" - Ticket.Priority.HIGH -> "&c" - Ticket.Priority.HIGHEST -> "&4" -} - -fun Ticket.Priority.toColouredString(locale: TMLocale): String { - val word = when (this) { - Ticket.Priority.LOWEST -> locale.priorityLowest - Ticket.Priority.LOW -> locale.priorityLow - Ticket.Priority.NORMAL -> locale.priorityNormal - Ticket.Priority.HIGH -> locale.priorityHigh - Ticket.Priority.HIGHEST -> locale.priorityHighest - } - return word.let { this.getColourCode() + it } -} - -fun Ticket.Status.toColouredString(locale: TMLocale) = when (this) { - Ticket.Status.OPEN -> "&a${locale.statusOpen}" - Ticket.Status.CLOSED -> "&c${locale.statusClosed}" -} - -fun Ticket.Status.getColourCode() = when (this) { - Ticket.Status.OPEN -> "&a" - Ticket.Status.CLOSED -> "&c" -} - -fun Ticket.Location.toBukkitLocationOrNull() = - Bukkit.getWorld(world)?.let { Location(it, x.toDouble(), y.toDouble(), z.toDouble()) } \ No newline at end of file diff --git a/src/main/kotlin/com/hoshikurama/github/ticketmanager/ticket/TicketHandler.kt b/src/main/kotlin/com/hoshikurama/github/ticketmanager/ticket/TicketHandler.kt deleted file mode 100644 index eff768f..0000000 --- a/src/main/kotlin/com/hoshikurama/github/ticketmanager/ticket/TicketHandler.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.hoshikurama.github.ticketmanager.ticket - -import com.hoshikurama.github.ticketmanager.pluginState -import java.util.* - -class TicketHandler(val id: Int) { - internal val creatorUUID: UUID? by lazy { - pluginState.database.getCreatorUUID(id) - } - - internal val location: org.bukkit.Location? by lazy { - pluginState.database.getLocation(id) - } - - internal var statusUpdateForCreator: Boolean - get() = statusUpdateForCreatorInternal - set(v) = pluginState.database.setStatusUpdateForCreator(id, v) - - internal var priority: Ticket.Priority - get() = priorityInternal - set(v) = pluginState.database.setPriority(id, v) - - internal var status: Ticket.Status - get() = statusInternal - set(v) = pluginState.database.setStatus(id, v) - - internal var assignedTo: String? - get() = assignedToInternal - set(v) = pluginState.database.setAssignment(id, v) - - - private val priorityInternal: Ticket.Priority by lazy { - pluginState.database.getPriority(id) - } - private val statusInternal: Ticket.Status by lazy { - pluginState.database.getStatus(id) - } - private val assignedToInternal by lazy { - pluginState.database.getAssignment(id) - } - private val statusUpdateForCreatorInternal: Boolean by lazy { - pluginState.database.getStatusUpdateForCreator(id) - } - - fun UUIDMatches(uuid: UUID?) = creatorUUID?.equals(uuid) ?: (uuid == null) -} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml deleted file mode 100644 index 4b35e08..0000000 --- a/src/main/resources/config.yml +++ /dev/null @@ -1,86 +0,0 @@ -############################################## -############################################## -# TicketManager Config File -############################################## -############################################## -# -# Plugin Proudly Made by: HoshiKurama -# -# All permissions and commands can -# be found on the GitHub wiki page. -# https://github.com/HoshiKurama/TicketManager -# -# ##################### -# Locale: -# ##################### -# Force Locale: Forces specific language to be used for commands and responses. -# Values: true, false -Force_Locale: false -# -# Preferred Locale: Locale to use when player locale is not found. Using a non-supported -# locale will cause TicketManager to internally default to Canadian English. -# Values: -# 'en_CA' = Canadian English -# 'en_US' = United States English -Preferred_Locale: 'en_CA' -# -# Console Locale: Locale Console will use. Using a non-supported locale will cause -# TicketManager to internally default to Preferred_Locale. -# Values: -# 'en_CA' = Canadian English -# 'en_US' = United States English -Console_Locale: 'en_CA' -# -# ##################### -# Cooldown: -# ##################### -# Use Cool-down: Determine if cool-downs should be applied to ticket commands -# that create or modify a ticket. Cool-downs apply to ALL users without the -# override permission. -# Values: true, false -Use_Cooldowns: false -# -# Ticket Modification Cool-down: Time in seconds before a user is able to create or -# modify a ticket. This applies to ALL users without the override permission if -# cool-downs are enabled. -Cooldown_Time: 0 -# -# ##################### -# Database Settings: -# ##################### -# Database mode: Type of database used. SQLite is default and can run locally -# Do NOT run MySQL unless you have a valid MySQL database! Internally defaults -# to SQLite if invalid value is used. -# Values: 'MySQL','SQLite' -Database_Mode: 'SQLite' -# -# ###################### -# MySQL Database -# ###################### -# This section only applies when using MySQL. -MySQL_Port: '' -MySQL_Host: '' -MySQL_DBName: '' -MySQL_Username: '' -MySQL_Password: '' -# -# ###################### -# Other -# ###################### -# Colour Code: Colour code used to add simple colour customization. -# Must be in the form "&". eg: &3, &6, &b, etc -# DO NOT USE ANY COLOUR CODES OTHER THAN COLOUR! -Colour_Code: '&3' -# -# Allow Unread Ticket Updates: When users other than the creator -# make ticket changes, the creator can optionally be alerted at -# regular intervals or on login which tickets have a status change. -# This is NOT the same alert one immediately receives on ticket -# status change. Setting value to 'false' prevents TicketManager -# from flagging a ticket as unread. -# Values: true, false -Allow_Unread_Ticket_Updates: true -# -# Allow Update Checking: Server can check that the latest version of TicketManager is installed during startup or reloads. -# Values: true, false -Allow_UpdateChecking: true \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml deleted file mode 100644 index fc33fee..0000000 --- a/src/main/resources/plugin.yml +++ /dev/null @@ -1,240 +0,0 @@ -name: TicketManager -version: 4.1.0 -main: com.hoshikurama.github.ticketmanager.TicketManagerPlugin -api-version: 1.17 -authors: [HoshiKurama] -depend: [Vault] -libraries: - - com.zaxxer:HikariCP:4.0.3 - - mysql:mysql-connector-java:8.0.25 - - org.xerial:sqlite-jdbc:3.34.0 - - org.jetbrains.kotlin:kotlin-stdlib:1.5.20 - - com.github.seratch:kotliquery:1.3.1 - - joda-time:joda-time:2.10.10 -commands: - ticket: - description: Base for all TicketManager commands -permissions: - ticketmanager.command.assign: - description: Assign a ticket - default: false - ticketmanager.command.close.all: - description: Close any ticket - default: false - ticketmanager.command.close.own: - description: Close only own tickets - default: false - ticketmanager.command.closeAll: - description: Mass close tickets - default: false - ticketmanager.command.comment.all: - description: Comment on any ticket - default: false - ticketmanager.command.comment.own: - description: Comment on only own tickets - default: false - ticketmanager.command.convertDatabase: - description: Convert database to another supported type - default: false - ticketmanager.command.create: - description: Create a ticket - default: false - ticketmanager.command.help: - description: See list of ticket commands to which user has permissions for - default: false - ticketmanager.command.history.all: - description: View all users' histories of tickets - default: false - ticketmanager.command.history.own: - description: View own history of tickets - default: false - ticketmanager.command.list: - description: List all open tickets - default: false - ticketmanager.command.reload: - description: Restarts plugin - default: false - ticketmanager.command.reopen: - description: Reopen any ticket - default: false - ticketmanager.command.search: - description: Search database for certain tickets - default: false - ticketmanager.command.setPriority: - description: Set priority of any ticket - default: false - ticketmanager.command.teleport: - description: Teleport to any ticket - default: false - ticketmanager.command.view.all: - description: View any ticket - default: false - ticketmanager.command.view.own: - description: View own ticket - default: false - ticketmanager.command.viewdeep.all: - description: View any detailed ticket - default: false - ticketmanager.command.viewdeep.own: - description: View own detailed ticket information - default: false - ticketmanager.commandArg.cooldown.override: - description: Overrides cooldown if enabled - default: false - ticketmanager.commandArg.silence: - description: Ability to perform silent variations - default: false - ticketmanager.commandArg.autotab: - description: Allows autotabbing (must have permission for command too) - default: false - ticketmanager.notify.openTickets.onJoin: - description: See open and assigned ticket count on login - default: false - ticketmanager.notify.openTickets.scheduled: - description: See open and assigned ticket count at scheduled intervals - default: false - ticketmanager.notify.unreadUpdates.onJoin: - description: See own tickets with updates on login - default: false - ticketmanager.notify.unreadUpdates.scheduled: - description: See own tickets with updates at scheduled intervals - default: false - ticketmanager.notify.info: - description: See general TicketManager information - default: false - ticketmanager.notify.pluginUpdate: - description: Get notified of plugin update on login - default: false - ticketmanager.notify.warning: - description: See modified stacktrace on errors - default: false - ticketmanager.notify.change.comment: - description: Creator sees when user comments on their ticket - default: false - ticketmanager.notify.change.close: - description: Creator sees when user closes their ticket - default: false - ticketmanager.notify.change.reopen: - description: Creator sees when user reopens their ticket - default: false - ticketmanager.notify.massNotify.assign: - description: See ticket assignment events - default: false - ticketmanager.notify.massNotify.close: - description: See any ticket close events - default: false - ticketmanager.notify.massNotify.comment: - description: See any ticket comment events - default: false - ticketmanager.notify.massNotify.create: - description: See ticket creation events - default: false - ticketmanager.notify.massNotify.massClose: - description: See ticket mass close events - default: false - ticketmanager.notify.massNotify.priority: - description: See ticket priority change events - default: false - ticketmanager.notify.massNotify.reopen: - description: See ticket reopen events - default: false - ticketmanager.command.*: - description: Wildcard for all commands and commandargs - children: - ticketmanager.command.assign: true - ticketmanager.command.close.all: true - ticketmanager.command.close.own: true - ticketmanager.command.closeAll: true - ticketmanager.command.comment.all: true - ticketmanager.command.comment.own: true - ticketmanager.command.convertDatabase: true - ticketmanager.command.create: true - ticketmanager.command.help: true - ticketmanager.command.history.all: true - ticketmanager.command.history.own: true - ticketmanager.command.list: true - ticketmanager.command.reload: true - ticketmanager.command.reopen: true - ticketmanager.command.search: true - ticketmanager.command.setPriority: true - ticketmanager.command.teleport: true - ticketmanager.command.view.all: true - ticketmanager.command.view.own: true - ticketmanager.command.viewdeep.all: true - ticketmanager.command.viewdeep.own: true - default: false - ticketmanager.notify.change.*: - description: Wildcard for all status changes on own ticket - children: - ticketmanager.notify.change.comment: true - ticketmanager.notify.change.close: true - ticketmanager.notify.change.reopen: true - default: false - ticketmanager.massNotify.*: - description: Wildcard for all mass notifications (except errors and plugin info) - children: - ticketmanager.notify.massNotify.assign: true - ticketmanager.notify.massNotify.close: true - ticketmanager.notify.massNotify.comment: true - ticketmanager.notify.massNotify.create: true - ticketmanager.notify.massNotify.massClose: true - ticketmanager.notify.massNotify.priority: true - ticketmanager.notify.massNotify.reopen: true - default: false - ticketmanager.notify.*: - description: Wildcard for all notification permissions - children: - ticketmanager.notify.openTickets.onJoin: true - ticketmanager.notify.openTickets.scheduled: true - ticketmanager.notify.unreadUpdates.onJoin: true - ticketmanager.notify.unreadUpdates.scheduled: true - ticketmanager.notify.warning: true - ticketmanager.notify.info: true - ticketmanager.massNotify.*: true - ticketmanager.notify.change.*: true - ticketmanager.notify.pluginUpdate: true - default: false - ticketmanager.*: - description: Wildcard for all TicketManager permissions - children: - ticketmanager.notify.*: true - ticketmanager.command.*: true - ticketmanager.commandArg.cooldown.override: true - ticketmanager.commandArg.silence: true - ticketmanager.commandArg.autotab: true - default: op - ticketmanager.basic: - description: Preset for normal users - children: - ticketmanager.command.close.own: true - ticketmanager.command.comment.own: true - ticketmanager.command.create: true - ticketmanager.command.help: true - ticketmanager.command.history.own: true - ticketmanager.command.view.own: true - ticketmanager.commandArg.autotab: true - ticketmanager.notify.unreadUpdates.onJoin: true - ticketmanager.notify.unreadUpdates.scheduled: true - ticketmanager.notify.change.*: true - default: false - ticketmanager.manage: - description: Preset for staff (includes ticketmanager.basic) - children: - ticketmanager.basic: true - ticketmanager.command.assign: true - ticketmanager.command.close.all: true - ticketmanager.command.comment.all: true - ticketmanager.command.list: true - ticketmanager.command.reopen: true - ticketmanager.command.search: true - ticketmanager.command.setPriority: true - ticketmanager.command.view.all: true - ticketmanager.command.viewdeep.all: true - ticketmanager.command.viewdeep.own: true - ticketmanager.command.history.all: true - ticketmanager.commandArg.cooldown.override: true - ticketmanager.notify.openTickets.onJoin: true - ticketmanager.notify.openTickets.scheduled: true - ticketmanager.notify.info: true - ticketmanager.massNotify.*: true - default: false \ No newline at end of file From fcecc163aeb6a97b95e2edf4260e47c255328167 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:15:04 -0500 Subject: [PATCH 05/31] Delete .gitignore --- .gitignore | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 1ffc0ef..0000000 --- a/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -.gradle -.idea -build -gradle -API_KEYS.kt -src/main/Stuff.txt -gradle.properties -gradlew -gradlew.bat -common/settings.gradle.kts -/common/common.iml From 5e967125a798d26eda737ec32276034df5c6a225 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:42:17 -0500 Subject: [PATCH 06/31] Refactored artifact ID Fixed Paper Gradle --- Paper/build.gradle.kts | 9 +++++++-- .../hoshikurama}/ticketmanager/paper/Globals.kt | 6 +++--- .../ticketmanager/paper/MetricsKotlinBukkit.kt | 2 +- .../ticketmanager/paper/TicketManagerPlugin.kt | 15 ++++++++------- .../ticketmanager/paper/events/Commands.kt | 2 +- .../ticketmanager/paper/events/PlayerJoin.kt | 10 +++++----- .../ticketmanager/paper/events/TabComplete.kt | 2 +- .../ticketmanager/common/Localization.kt | 2 +- .../ticketmanager/common/Miscellaneous.kt | 2 +- .../ticketmanager/common/PluginState.kt | 6 +++--- .../ticketmanager/common/databases/Database.kt | 6 +++--- .../ticketmanager/common/databases/MySQL.kt | 6 +++--- .../ticketmanager/common/databases/SQLite.kt | 6 +++--- .../ticketmanager/common/ticket/BasicTicket.kt | 4 ++-- .../ticketmanager/common/ticket/Ticket.kt | 4 ++-- 15 files changed, 44 insertions(+), 38 deletions(-) rename Paper/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/paper/Globals.kt (85%) rename Paper/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/paper/MetricsKotlinBukkit.kt (99%) rename Paper/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/paper/TicketManagerPlugin.kt (94%) rename Paper/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/paper/events/Commands.kt (87%) rename Paper/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/paper/events/PlayerJoin.kt (90%) rename Paper/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/paper/events/TabComplete.kt (87%) rename common/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/common/Localization.kt (99%) rename common/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/common/Miscellaneous.kt (94%) rename common/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/common/PluginState.kt (95%) rename common/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/common/databases/Database.kt (91%) rename common/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/common/databases/MySQL.kt (94%) rename common/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/common/databases/SQLite.kt (94%) rename common/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/common/ticket/BasicTicket.kt (89%) rename common/src/main/kotlin/com/{hoshikurama/github => github/hoshikurama}/ticketmanager/common/ticket/Ticket.kt (94%) diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts index 29b54e1..ae634ea 100644 --- a/Paper/build.gradle.kts +++ b/Paper/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } application { - mainClass.set("com.hoshikurama.github.mcwandsframework.MCWandsFramework") + mainClass.set("com.github.hoshikurama.ticketmanager.paper.TicketManagerPlugin") } repositories { @@ -37,6 +37,11 @@ dependencies { tasks { named("shadowJar") { - archiveBaseName.set("TicketManager-Paper-5.0.0.jar") + archiveBaseName.set("TicketManager-Paper") + + dependencies { + include(dependency("com.github.HoshiKurama:KyoriComponentDSL:1.0.0")) + include(project(":common")) + } } } \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/Globals.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt similarity index 85% rename from Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/Globals.kt rename to Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt index a3192ac..765b30c 100644 --- a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/Globals.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt @@ -1,7 +1,7 @@ -package com.hoshikurama.github.ticketmanager.paper +package com.github.hoshikurama.ticketmanager.paper -import com.hoshikurama.github.ticketmanager.common.PluginState -import com.hoshikurama.github.ticketmanager.common.TMLocale +import com.github.hoshikurama.ticketmanager.common.PluginState +import com.github.hoshikurama.ticketmanager.common.TMLocale import kotlinx.coroutines.Deferred import net.kyori.adventure.text.Component import org.bukkit.Bukkit diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/MetricsKotlinBukkit.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/MetricsKotlinBukkit.kt similarity index 99% rename from Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/MetricsKotlinBukkit.kt rename to Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/MetricsKotlinBukkit.kt index 6a826a2..c1d5caf 100644 --- a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/MetricsKotlinBukkit.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/MetricsKotlinBukkit.kt @@ -1,4 +1,4 @@ -package com.hoshikurama.github.ticketmanager.paper +package com.github.hoshikurama.ticketmanager.paper import org.bukkit.Bukkit import org.bukkit.configuration.file.YamlConfiguration diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt similarity index 94% rename from Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/TicketManagerPlugin.kt rename to Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index e3a569e..8569da0 100644 --- a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -1,14 +1,15 @@ -package com.hoshikurama.github.ticketmanager.paper +package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.formattedContent +import com.github.hoshikurama.ticketmanager.common.* import com.github.shynixn.mccoroutine.* import com.hoshikurama.github.ticketmanager.common.* -import com.hoshikurama.github.ticketmanager.common.databases.Database -import com.hoshikurama.github.ticketmanager.common.databases.MySQL -import com.hoshikurama.github.ticketmanager.common.databases.SQLite -import com.hoshikurama.github.ticketmanager.paper.events.Commands -import com.hoshikurama.github.ticketmanager.paper.events.PlayerJoin -import com.hoshikurama.github.ticketmanager.paper.events.TabComplete +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.databases.MySQL +import com.github.hoshikurama.ticketmanager.common.databases.SQLite +import com.github.hoshikurama.ticketmanager.paper.events.Commands +import com.github.hoshikurama.ticketmanager.paper.events.PlayerJoin +import com.github.hoshikurama.ticketmanager.paper.events.TabComplete import kotlinx.coroutines.* import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt similarity index 87% rename from Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/Commands.kt rename to Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index 42bc871..c685b0a 100644 --- a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -1,4 +1,4 @@ -package com.hoshikurama.github.ticketmanager.paper.events +package com.github.hoshikurama.ticketmanager.paper.events import com.github.shynixn.mccoroutine.SuspendingCommandExecutor import org.bukkit.command.Command diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/PlayerJoin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt similarity index 90% rename from Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/PlayerJoin.kt rename to Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt index e20b42b..c3005cb 100644 --- a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/PlayerJoin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt @@ -1,11 +1,11 @@ -package com.hoshikurama.github.ticketmanager.paper.events +package com.github.hoshikurama.ticketmanager.paper.events import com.github.hoshikurama.componentDSL.formattedContent import com.github.shynixn.mccoroutine.asyncDispatcher -import com.hoshikurama.github.ticketmanager.paper.has -import com.hoshikurama.github.ticketmanager.paper.mainPlugin -import com.hoshikurama.github.ticketmanager.paper.pluginState -import com.hoshikurama.github.ticketmanager.paper.toTMLocale +import com.github.hoshikurama.ticketmanager.paper.has +import com.github.hoshikurama.ticketmanager.paper.mainPlugin +import com.github.hoshikurama.ticketmanager.paper.pluginState +import com.github.hoshikurama.ticketmanager.paper.toTMLocale import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch diff --git a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/TabComplete.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt similarity index 87% rename from Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/TabComplete.kt rename to Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt index 6c3697a..95ed5c3 100644 --- a/Paper/src/main/kotlin/com/hoshikurama/github/ticketmanager/paper/events/TabComplete.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt @@ -1,4 +1,4 @@ -package com.hoshikurama.github.ticketmanager.paper.events +package com.github.hoshikurama.ticketmanager.paper.events import com.github.shynixn.mccoroutine.SuspendingTabCompleter import org.bukkit.command.Command diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Localization.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt similarity index 99% rename from common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Localization.kt rename to common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt index 582a273..8204c97 100644 --- a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Localization.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt @@ -1,4 +1,4 @@ -package com.hoshikurama.github.ticketmanager.common +package com.github.hoshikurama.ticketmanager.common import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Miscellaneous.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt similarity index 94% rename from common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Miscellaneous.kt rename to common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt index 9b6e079..1db14a4 100644 --- a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/Miscellaneous.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt @@ -1,4 +1,4 @@ -package com.hoshikurama.github.ticketmanager.common +package com.github.hoshikurama.ticketmanager.common import kotlinx.coroutines.withContext import java.io.InputStream diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/PluginState.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt similarity index 95% rename from common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/PluginState.kt rename to common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt index 6bcb052..644cfdd 100644 --- a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/PluginState.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt @@ -1,7 +1,7 @@ -package com.hoshikurama.github.ticketmanager.common +package com.github.hoshikurama.ticketmanager.common -import com.hoshikurama.github.ticketmanager.common.databases.Database -import com.hoshikurama.github.ticketmanager.common.databases.SQLite +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.databases.SQLite import kotlinx.coroutines.* import net.kyori.adventure.text.Component import java.time.Instant diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt similarity index 91% rename from common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/Database.kt rename to common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index 9e555f4..a97f865 100644 --- a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -1,7 +1,7 @@ -package com.hoshikurama.github.ticketmanager.common.databases +package com.github.hoshikurama.ticketmanager.common.databases -import com.hoshikurama.github.ticketmanager.common.ticket.BasicTicket -import com.hoshikurama.github.ticketmanager.common.ticket.Ticket +import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket +import com.github.hoshikurama.ticketmanager.common.ticket.Ticket import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow import java.util.* diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/MySQL.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt similarity index 94% rename from common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/MySQL.kt rename to common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt index dbab502..58bcd36 100644 --- a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/MySQL.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt @@ -1,7 +1,7 @@ -package com.hoshikurama.github.ticketmanager.common.databases +package com.github.hoshikurama.ticketmanager.common.databases -import com.hoshikurama.github.ticketmanager.common.ticket.BasicTicket -import com.hoshikurama.github.ticketmanager.common.ticket.Ticket +import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket +import com.github.hoshikurama.ticketmanager.common.ticket.Ticket import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow import java.util.* diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt similarity index 94% rename from common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/SQLite.kt rename to common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index f3ae730..fbd9b9b 100644 --- a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -1,7 +1,7 @@ -package com.hoshikurama.github.ticketmanager.common.databases +package com.github.hoshikurama.ticketmanager.common.databases -import com.hoshikurama.github.ticketmanager.common.ticket.BasicTicket -import com.hoshikurama.github.ticketmanager.common.ticket.Ticket +import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket +import com.github.hoshikurama.ticketmanager.common.ticket.Ticket import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow import java.util.* diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/BasicTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt similarity index 89% rename from common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/BasicTicket.kt rename to common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt index 4de4b44..ebed6d1 100644 --- a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/BasicTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt @@ -1,6 +1,6 @@ -package com.hoshikurama.github.ticketmanager.common.ticket +package com.github.hoshikurama.ticketmanager.common.ticket -import com.hoshikurama.github.ticketmanager.common.PluginState +import com.github.hoshikurama.ticketmanager.common.PluginState import kotlinx.coroutines.Deferred import java.util.* diff --git a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/Ticket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/Ticket.kt similarity index 94% rename from common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/Ticket.kt rename to common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/Ticket.kt index fbc67ee..027cb14 100644 --- a/common/src/main/kotlin/com/hoshikurama/github/ticketmanager/common/ticket/Ticket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/Ticket.kt @@ -1,6 +1,6 @@ -package com.hoshikurama.github.ticketmanager.common.ticket +package com.github.hoshikurama.ticketmanager.common.ticket -import com.hoshikurama.github.ticketmanager.common.TMLocale +import com.github.hoshikurama.ticketmanager.common.TMLocale import java.time.Instant import java.util.* From 586095f6846e913f6313144b6196ee7b3070d37b Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 6 Jul 2021 19:47:02 -0500 Subject: [PATCH 07/31] Updated .gitignore --- .gitignore | 9 + Paper/src/main/resources/config.yml | 86 ++++++++++ Paper/src/main/resources/plugin.yml | 245 ++++++++++++++++++++++++++++ 3 files changed, 340 insertions(+) create mode 100644 .gitignore create mode 100644 Paper/src/main/resources/config.yml create mode 100644 Paper/src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7144c44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/Paper/build/ +/.gradle/ +/.idea/ +/build/ +/gradle/ +/gradlew +/gradlew.bat +/common/build/ +/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/API_KEYS.kt diff --git a/Paper/src/main/resources/config.yml b/Paper/src/main/resources/config.yml new file mode 100644 index 0000000..4b35e08 --- /dev/null +++ b/Paper/src/main/resources/config.yml @@ -0,0 +1,86 @@ +############################################## +############################################## +# TicketManager Config File +############################################## +############################################## +# +# Plugin Proudly Made by: HoshiKurama +# +# All permissions and commands can +# be found on the GitHub wiki page. +# https://github.com/HoshiKurama/TicketManager +# +# ##################### +# Locale: +# ##################### +# Force Locale: Forces specific language to be used for commands and responses. +# Values: true, false +Force_Locale: false +# +# Preferred Locale: Locale to use when player locale is not found. Using a non-supported +# locale will cause TicketManager to internally default to Canadian English. +# Values: +# 'en_CA' = Canadian English +# 'en_US' = United States English +Preferred_Locale: 'en_CA' +# +# Console Locale: Locale Console will use. Using a non-supported locale will cause +# TicketManager to internally default to Preferred_Locale. +# Values: +# 'en_CA' = Canadian English +# 'en_US' = United States English +Console_Locale: 'en_CA' +# +# ##################### +# Cooldown: +# ##################### +# Use Cool-down: Determine if cool-downs should be applied to ticket commands +# that create or modify a ticket. Cool-downs apply to ALL users without the +# override permission. +# Values: true, false +Use_Cooldowns: false +# +# Ticket Modification Cool-down: Time in seconds before a user is able to create or +# modify a ticket. This applies to ALL users without the override permission if +# cool-downs are enabled. +Cooldown_Time: 0 +# +# ##################### +# Database Settings: +# ##################### +# Database mode: Type of database used. SQLite is default and can run locally +# Do NOT run MySQL unless you have a valid MySQL database! Internally defaults +# to SQLite if invalid value is used. +# Values: 'MySQL','SQLite' +Database_Mode: 'SQLite' +# +# ###################### +# MySQL Database +# ###################### +# This section only applies when using MySQL. +MySQL_Port: '' +MySQL_Host: '' +MySQL_DBName: '' +MySQL_Username: '' +MySQL_Password: '' +# +# ###################### +# Other +# ###################### +# Colour Code: Colour code used to add simple colour customization. +# Must be in the form "&". eg: &3, &6, &b, etc +# DO NOT USE ANY COLOUR CODES OTHER THAN COLOUR! +Colour_Code: '&3' +# +# Allow Unread Ticket Updates: When users other than the creator +# make ticket changes, the creator can optionally be alerted at +# regular intervals or on login which tickets have a status change. +# This is NOT the same alert one immediately receives on ticket +# status change. Setting value to 'false' prevents TicketManager +# from flagging a ticket as unread. +# Values: true, false +Allow_Unread_Ticket_Updates: true +# +# Allow Update Checking: Server can check that the latest version of TicketManager is installed during startup or reloads. +# Values: true, false +Allow_UpdateChecking: true \ No newline at end of file diff --git a/Paper/src/main/resources/plugin.yml b/Paper/src/main/resources/plugin.yml new file mode 100644 index 0000000..4f4be50 --- /dev/null +++ b/Paper/src/main/resources/plugin.yml @@ -0,0 +1,245 @@ +name: TicketManager +version: 4.1.0 +main: com.github.hoshikurama.ticketmanager.paper.TicketManagerPlugin +api-version: 1.17 +authors: [HoshiKurama] +depend: [Vault] +libraries: + - org.jetbrains.kotlin:kotlin-stdlib:1.5.20 + - mysql:mysql-connector-java:8.0.25 + - org.xerial:sqlite-jdbc:3.34.0 + - com.github.jasync-sql:jasync-mysql:1.1.6 + - com.github.seratch:kotliquery:1.3.1 + - org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0 + - net.kyori:adventure-extra-kotlin:4.8.1 + - net.kyori:adventure-text-serializer-legacy:4.8.1 + - joda-time:joda-time:2.10.10 + - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0 + - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0 +commands: + ticket: + description: Base for all TicketManager commands +permissions: + ticketmanager.command.assign: + description: Assign a ticket + default: false + ticketmanager.command.close.all: + description: Close any ticket + default: false + ticketmanager.command.close.own: + description: Close only own tickets + default: false + ticketmanager.command.closeAll: + description: Mass close tickets + default: false + ticketmanager.command.comment.all: + description: Comment on any ticket + default: false + ticketmanager.command.comment.own: + description: Comment on only own tickets + default: false + ticketmanager.command.convertDatabase: + description: Convert database to another supported type + default: false + ticketmanager.command.create: + description: Create a ticket + default: false + ticketmanager.command.help: + description: See list of ticket commands to which user has permissions for + default: false + ticketmanager.command.history.all: + description: View all users' histories of tickets + default: false + ticketmanager.command.history.own: + description: View own history of tickets + default: false + ticketmanager.command.list: + description: List all open tickets + default: false + ticketmanager.command.reload: + description: Restarts plugin + default: false + ticketmanager.command.reopen: + description: Reopen any ticket + default: false + ticketmanager.command.search: + description: Search database for certain tickets + default: false + ticketmanager.command.setPriority: + description: Set priority of any ticket + default: false + ticketmanager.command.teleport: + description: Teleport to any ticket + default: false + ticketmanager.command.view.all: + description: View any ticket + default: false + ticketmanager.command.view.own: + description: View own ticket + default: false + ticketmanager.command.viewdeep.all: + description: View any detailed ticket + default: false + ticketmanager.command.viewdeep.own: + description: View own detailed ticket information + default: false + ticketmanager.commandArg.cooldown.override: + description: Overrides cooldown if enabled + default: false + ticketmanager.commandArg.silence: + description: Ability to perform silent variations + default: false + ticketmanager.commandArg.autotab: + description: Allows autotabbing (must have permission for command too) + default: false + ticketmanager.notify.openTickets.onJoin: + description: See open and assigned ticket count on login + default: false + ticketmanager.notify.openTickets.scheduled: + description: See open and assigned ticket count at scheduled intervals + default: false + ticketmanager.notify.unreadUpdates.onJoin: + description: See own tickets with updates on login + default: false + ticketmanager.notify.unreadUpdates.scheduled: + description: See own tickets with updates at scheduled intervals + default: false + ticketmanager.notify.info: + description: See general TicketManager information + default: false + ticketmanager.notify.pluginUpdate: + description: Get notified of plugin update on login + default: false + ticketmanager.notify.warning: + description: See modified stacktrace on errors + default: false + ticketmanager.notify.change.comment: + description: Creator sees when user comments on their ticket + default: false + ticketmanager.notify.change.close: + description: Creator sees when user closes their ticket + default: false + ticketmanager.notify.change.reopen: + description: Creator sees when user reopens their ticket + default: false + ticketmanager.notify.massNotify.assign: + description: See ticket assignment events + default: false + ticketmanager.notify.massNotify.close: + description: See any ticket close events + default: false + ticketmanager.notify.massNotify.comment: + description: See any ticket comment events + default: false + ticketmanager.notify.massNotify.create: + description: See ticket creation events + default: false + ticketmanager.notify.massNotify.massClose: + description: See ticket mass close events + default: false + ticketmanager.notify.massNotify.priority: + description: See ticket priority change events + default: false + ticketmanager.notify.massNotify.reopen: + description: See ticket reopen events + default: false + ticketmanager.command.*: + description: Wildcard for all commands and commandargs + children: + ticketmanager.command.assign: true + ticketmanager.command.close.all: true + ticketmanager.command.close.own: true + ticketmanager.command.closeAll: true + ticketmanager.command.comment.all: true + ticketmanager.command.comment.own: true + ticketmanager.command.convertDatabase: true + ticketmanager.command.create: true + ticketmanager.command.help: true + ticketmanager.command.history.all: true + ticketmanager.command.history.own: true + ticketmanager.command.list: true + ticketmanager.command.reload: true + ticketmanager.command.reopen: true + ticketmanager.command.search: true + ticketmanager.command.setPriority: true + ticketmanager.command.teleport: true + ticketmanager.command.view.all: true + ticketmanager.command.view.own: true + ticketmanager.command.viewdeep.all: true + ticketmanager.command.viewdeep.own: true + default: false + ticketmanager.notify.change.*: + description: Wildcard for all status changes on own ticket + children: + ticketmanager.notify.change.comment: true + ticketmanager.notify.change.close: true + ticketmanager.notify.change.reopen: true + default: false + ticketmanager.massNotify.*: + description: Wildcard for all mass notifications (except errors and plugin info) + children: + ticketmanager.notify.massNotify.assign: true + ticketmanager.notify.massNotify.close: true + ticketmanager.notify.massNotify.comment: true + ticketmanager.notify.massNotify.create: true + ticketmanager.notify.massNotify.massClose: true + ticketmanager.notify.massNotify.priority: true + ticketmanager.notify.massNotify.reopen: true + default: false + ticketmanager.notify.*: + description: Wildcard for all notification permissions + children: + ticketmanager.notify.openTickets.onJoin: true + ticketmanager.notify.openTickets.scheduled: true + ticketmanager.notify.unreadUpdates.onJoin: true + ticketmanager.notify.unreadUpdates.scheduled: true + ticketmanager.notify.warning: true + ticketmanager.notify.info: true + ticketmanager.massNotify.*: true + ticketmanager.notify.change.*: true + ticketmanager.notify.pluginUpdate: true + default: false + ticketmanager.*: + description: Wildcard for all TicketManager permissions + children: + ticketmanager.notify.*: true + ticketmanager.command.*: true + ticketmanager.commandArg.cooldown.override: true + ticketmanager.commandArg.silence: true + ticketmanager.commandArg.autotab: true + default: op + ticketmanager.basic: + description: Preset for normal users + children: + ticketmanager.command.close.own: true + ticketmanager.command.comment.own: true + ticketmanager.command.create: true + ticketmanager.command.help: true + ticketmanager.command.history.own: true + ticketmanager.command.view.own: true + ticketmanager.commandArg.autotab: true + ticketmanager.notify.unreadUpdates.onJoin: true + ticketmanager.notify.unreadUpdates.scheduled: true + ticketmanager.notify.change.*: true + default: false + ticketmanager.manage: + description: Preset for staff (includes ticketmanager.basic) + children: + ticketmanager.basic: true + ticketmanager.command.assign: true + ticketmanager.command.close.all: true + ticketmanager.command.comment.all: true + ticketmanager.command.list: true + ticketmanager.command.reopen: true + ticketmanager.command.search: true + ticketmanager.command.setPriority: true + ticketmanager.command.view.all: true + ticketmanager.command.viewdeep.all: true + ticketmanager.command.viewdeep.own: true + ticketmanager.command.history.all: true + ticketmanager.commandArg.cooldown.override: true + ticketmanager.notify.openTickets.onJoin: true + ticketmanager.notify.openTickets.scheduled: true + ticketmanager.notify.info: true + ticketmanager.massNotify.*: true + default: false \ No newline at end of file From ab0743ca283df7e4fc858f2252e4e0ac2fdd8cf3 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Thu, 8 Jul 2021 17:31:20 -0500 Subject: [PATCH 08/31] Basic operation with SQLite --- .gitignore | 1 + Paper/build.gradle.kts | 6 +- .../ticketmanager/paper/Globals.kt | 20 +- .../paper/TicketManagerPlugin.kt | 76 +- .../ticketmanager/paper/events/Commands.kt | 1405 ++++++++++++++++- .../ticketmanager/paper/events/PlayerJoin.kt | 10 +- .../ticketmanager/paper/events/TabComplete.kt | 315 +++- Paper/src/main/resources/plugin.yml | 2 +- build.gradle.kts | 4 + common/build.gradle.kts | 2 +- .../ticketmanager/common/Localization.kt | 6 +- .../ticketmanager/common/Miscellaneous.kt | 63 +- .../ticketmanager/common/PluginState.kt | 5 +- .../common/databases/Database.kt | 72 +- .../ticketmanager/common/databases/MySQL.kt | 75 +- .../ticketmanager/common/databases/SQLite.kt | 408 ++++- .../common/ticket/BasicTicket.kt | 77 +- .../common/ticket/BasicTicketHandler.kt | 55 + .../ticketmanager/common/ticket/FullTicket.kt | 33 + .../ticketmanager/common/ticket/Ticket.kt | 50 - common/src/main/resources/locales/en_CA.yml | 18 +- 21 files changed, 2429 insertions(+), 274 deletions(-) create mode 100644 common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt create mode 100644 common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt delete mode 100644 common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/Ticket.kt diff --git a/.gitignore b/.gitignore index 7144c44..f442b64 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /gradlew.bat /common/build/ /common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/API_KEYS.kt +/src/ diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts index ae634ea..1f37ecb 100644 --- a/Paper/build.gradle.kts +++ b/Paper/build.gradle.kts @@ -16,13 +16,13 @@ repositories { } dependencies { - compileOnly("io.papermc.paper:paper-api:1.17-R0.1-SNAPSHOT") + compileOnly("io.papermc.paper:paper-api:1.17.1-R0.1-SNAPSHOT") implementation(kotlin("stdlib", version = "1.5.20")) implementation("mysql:mysql-connector-java:8.0.25") implementation("org.xerial:sqlite-jdbc:3.34.0") implementation("com.github.jasync-sql:jasync-mysql:1.1.6") implementation("com.github.seratch:kotliquery:1.3.1") - implementation("com.github.HoshiKurama:KyoriComponentDSL:1.0.0") + implementation("com.github.HoshiKurama:KyoriComponentDSL:1.1.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0") implementation("net.kyori:adventure-api:4.8.1") implementation("net.kyori:adventure-extra-kotlin:4.8.1") @@ -40,7 +40,7 @@ tasks { archiveBaseName.set("TicketManager-Paper") dependencies { - include(dependency("com.github.HoshiKurama:KyoriComponentDSL:1.0.0")) + include(dependency("com.github.HoshiKurama:KyoriComponentDSL:1.1.0")) include(project(":common")) } } diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt index 765b30c..f855590 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt @@ -2,11 +2,12 @@ package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.ticketmanager.common.PluginState import com.github.hoshikurama.ticketmanager.common.TMLocale -import kotlinx.coroutines.Deferred import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor +import org.bukkit.command.CommandSender import org.bukkit.entity.Player +import java.util.* import java.util.logging.Level fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) @@ -14,12 +15,12 @@ fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, Ch internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin -internal val pluginState: Deferred +internal val pluginState: PluginState get() = mainPlugin.configState -suspend fun pushMassNotify(permission: String, localeMsg: suspend (TMLocale) -> Component) { - Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.await().localeHandler.consoleLocale)) +internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { + Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers().asSequence() .filter { it.has(permission) } @@ -27,5 +28,14 @@ suspend fun pushMassNotify(permission: String, localeMsg: suspend (TMLocale) -> } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) +internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true -internal suspend fun Player.toTMLocale() = pluginState.await().localeHandler.getOrDefault(locale().toString()) \ No newline at end of file +internal fun Player.toTMLocale() = pluginState.localeHandler.getOrDefault(locale().toString()) +internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else pluginState.localeHandler.consoleLocale + +internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null + +fun UUID?.toName(locale: TMLocale): String { + if (this == null) return locale.consoleName + return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" +} \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index 8569da0..a03a323 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -3,7 +3,6 @@ package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.* import com.github.shynixn.mccoroutine.* -import com.hoshikurama.github.ticketmanager.common.* import com.github.hoshikurama.ticketmanager.common.databases.Database import com.github.hoshikurama.ticketmanager.common.databases.MySQL import com.github.hoshikurama.ticketmanager.common.databases.SQLite @@ -24,7 +23,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { internal val pluginLocked = NonBlockingSync(singleOffThread, true) internal lateinit var perms: Permission private set - internal lateinit var configState: Deferred + internal lateinit var configState: PluginState internal val ticketCountMetrics = NonBlockingSync(singleOffThread, 0) private lateinit var metrics: Metrics @@ -67,15 +66,16 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { if (pluginLocked.check()) return@launchAsync try { - val state = pluginState.await() + val state = pluginState // Mass Unread Notify if (state.allowUnreadTicketUpdates) { - state.database.getTicketIDsWithUpdates() + state.database.getBasicsWithUpdatesAsFlow() .toList() - .groupBy({ it.first }, { it.second }) + .groupBy ({ it.creatorUUID }, { it.id }) .asSequence() - .mapNotNull { Bukkit.getPlayer(it.key)?.run { Pair(this, it.value) } } + .filter { it.key != null } + .mapNotNull { Bukkit.getPlayer(it.key!!)?.run { Pair(this, it.value) } } .filter { it.first.has("ticketmanager.notify.unreadUpdates.scheduled") } .forEach { val template = if (it.second.size > 1) it.first.toTMLocale().notifyUnreadUpdateMulti @@ -88,7 +88,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } // Open and Assigned Notify - val tickets = state.database.getOpenTickets() + val tickets = state.database.getBasicOpenAsFlow() .map { it.assignedTo } .toList() @@ -128,11 +128,12 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { internal suspend fun loadPlugin() = coroutineScope { pluginLocked.set(true) - // Builds instructions for plugin scope - configState = async { - + configState = run { + // Creates config file if not found if (!File(plugin.dataFolder, "config.yml").exists()) { plugin.saveDefaultConfig() + + // Notifies users config was generated after plugin state init launch { while (!(::configState.isInitialized)) delay(100L) @@ -141,9 +142,8 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } plugin.reloadConfig() - val config = plugin.config - - config.run { + plugin.config.run { + val path = plugin.dataFolder.absolutePath val database: () -> Database? = { val type = getString("Database_Mode", "SQLite")!! .let { tryOrNull { Database.Type.valueOf(it) } ?: Database.Type.SQLite } @@ -156,7 +156,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { getString("MySQL_Username")!!, getString("MySQL_Password")!! ) - Database.Type.SQLite -> SQLite() + Database.Type.SQLite -> SQLite(path) } } @@ -188,41 +188,51 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { mainPlugin.description.version } - PluginState.createDeferredPluginState( + PluginState.createPluginState( database, cooldown, localeHandler, allowUnreadTicketUpdates, checkForPluginUpdate, pluginVersion, + path ) } } - val pluginState = configState.await() - - withContext(minecraftDispatcher) { - // Register events and commands - pluginState.localeHandler.getCommandBases().forEach { - plugin.getCommand(it)?.setSuspendingExecutor(Commands()) - mainPlugin.getCommand(it)?.setSuspendingTabCompleter(TabComplete()) - // Remember to register any keyword in plugin.yml - } - } - launch { - if (pluginState.database.updateNeeded().await()) { - pluginState.database.updateDatabase() - } - mainPlugin.pluginLocked.set(false) + configState.database.initialiseDatabase() + val updateNeeded = configState.database.updateNeededAsync().await() + + if (updateNeeded) { + configState.database.updateDatabase( + onBegin = { + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationDBUpdate) } + } + }, + onComplete = { + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationDBUpdateComplete) } + } + pluginLocked.set(true) + }, + offlinePlayerNameToUuidOrNull = { + Bukkit.getOfflinePlayers() + .filter { it.name == name } + .map { it.uniqueId } + .firstOrNull() + } + ) + } else pluginLocked.set(false) } withContext(minecraftDispatcher) { // Register events and commands - pluginState.localeHandler.getCommandBases().forEach { - plugin.getCommand(it)?.setSuspendingExecutor(Commands()) - mainPlugin.getCommand(it)?.setSuspendingTabCompleter(TabComplete()) + configState.localeHandler.getCommandBases().forEach { + getCommand(it)!!.setSuspendingExecutor(Commands()) + server.pluginManager.registerEvents(TabComplete(), this@TicketManagerPlugin) // Remember to register any keyword in plugin.yml } } diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index c685b0a..08d1887 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -1,8 +1,33 @@ package com.github.hoshikurama.ticketmanager.paper.events +import com.github.hoshikurama.componentDSL.* +import com.github.hoshikurama.ticketmanager.common.* +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.ticket.* +import com.github.hoshikurama.ticketmanager.paper.* +import com.github.hoshikurama.ticketmanager.paper.has +import com.github.hoshikurama.ticketmanager.paper.mainPlugin +import com.github.hoshikurama.ticketmanager.paper.pluginState +import com.github.hoshikurama.ticketmanager.paper.toTMLocale import com.github.shynixn.mccoroutine.SuspendingCommandExecutor +import com.github.shynixn.mccoroutine.asyncDispatcher +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.toList +import net.kyori.adventure.extra.kotlin.text +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.TextComponent +import net.kyori.adventure.text.event.ClickEvent +import net.kyori.adventure.text.event.HoverEvent +import net.kyori.adventure.text.format.NamedTextColor +import net.kyori.adventure.text.format.TextDecoration +import org.bukkit.Bukkit +import org.bukkit.ChatColor +import org.bukkit.Location import org.bukkit.command.Command import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import java.lang.Exception +import java.util.* class Commands : SuspendingCommandExecutor { @@ -10,8 +35,1382 @@ class Commands : SuspendingCommandExecutor { sender: CommandSender, command: Command, label: String, - args: Array + args: Array, + ): Boolean = withContext(mainPlugin.asyncDispatcher) { + + val senderLocale = sender.toTMLocale() + val argList = args.toList() + + if (argList.isEmpty()) { + sender.sendMessage(text { formattedContent(senderLocale.warningsInvalidCommand) }) + return@withContext false + } + + if (mainPlugin.pluginLocked.check()) { + sender.sendMessage(text { formattedContent(senderLocale.warningsLocked)}) + return@withContext false + } + + // Grabs BasicTicket. Only null if ID required but doesn't exist. Filters non-valid tickets + val pseudoTicket = getBasicTicketHandlerAsync(argList, senderLocale).await() + if (pseudoTicket == null) { + sender.sendMessage(text { formattedContent(senderLocale.warningsInvalidID) }) + return@withContext false + } + + // Async Calculations + val hasValidPermission = async { hasValidPermission(sender, pseudoTicket, argList, senderLocale) } + val isValidCommand = async { isValidCommand(sender, pseudoTicket, argList, senderLocale) } + val notUnderCooldown = async { notUnderCooldown(sender, senderLocale, argList) } + // Shortened Commands + val executeCommand = suspend { executeCommand(sender, argList, senderLocale, pseudoTicket) } + + try { + if (notUnderCooldown.await() && isValidCommand.await() && hasValidPermission.await()) { + executeCommand()?.let { pushNotifications(sender, it, senderLocale, pseudoTicket) } + } + } catch (e: Exception) { + e.printStackTrace() + //postModifiedStacktrace(e) + //sender.sendMessage(senderLocale.warningsUnexpectedError) + } + + return@withContext true + } + + private suspend fun getBasicTicketHandlerAsync( + args: List, + senderLocale: TMLocale + ): Deferred { + + suspend fun buildFromIDAsync(id: Int) = BasicTicketHandler.buildHandlerAsync(pluginState.database, id) + + return coroutineScope { + when (args[0]) { + senderLocale.commandWordAssign, + senderLocale.commandWordSilentAssign, + senderLocale.commandWordClaim, + senderLocale.commandWordSilentClaim, + senderLocale.commandWordClose, + senderLocale.commandWordSilentClose, + senderLocale.commandWordComment, + senderLocale.commandWordSilentComment, + senderLocale.commandWordReopen, + senderLocale.commandWordSilentReopen, + senderLocale.commandWordSetPriority, + senderLocale.commandWordSilentSetPriority, + senderLocale.commandWordTeleport, + senderLocale.commandWordUnassign, + senderLocale.commandWordSilentUnassign, + senderLocale.commandWordView, + senderLocale.commandWordDeepView -> + args.getOrNull(1) + ?.toIntOrNull() + ?.let { buildFromIDAsync(it) } ?: async { null } + else -> async { BasicTicket(creatorUUID = null, location = null).run { BasicTicketHandler(pluginState.database, this) } } // Occurs when command does not need valid handler + } + } + } + + private fun hasValidPermission( + sender: CommandSender, + basicTicket: BasicTicket, + args: List, + senderLocale: TMLocale ): Boolean { - TODO("Not yet implemented") + try { + if (sender !is Player) return true + + fun has(perm: String) = sender.has(perm) + fun hasSilent() = has("ticketmanager.commandArg.silence") + fun hasDuality(basePerm: String): Boolean { + val senderUUID = sender.toUUIDOrNull() + val ownsTicket = basicTicket.uuidMatches(senderUUID) + return has("$basePerm.all") || (sender.has("$basePerm.own") && ownsTicket) + } + + return senderLocale.run { + when (args[0]) { + commandWordAssign, commandWordClaim, commandWordUnassign -> + has("ticketmanager.command.assign") + commandWordSilentAssign, commandWordSilentClaim,commandWordSilentUnassign -> + has("ticketmanager.command.assign") && hasSilent() + commandWordClose -> hasDuality("ticketmanager.command.close") + commandWordSilentClose -> hasDuality("ticketmanager.command.close") && hasSilent() + commandWordCloseAll -> has("ticketmanager.command.closeAll") + commandWordSilentCloseAll -> has("ticketmanager.command.closeAll") && hasSilent() + commandWordComment -> hasDuality("ticketmanager.command.comment") + commandWordSilentComment -> hasDuality("ticketmanager.command.comment") && hasSilent() + commandWordCreate -> has("ticketmanager.command.create") + commandWordHelp -> has("ticketmanager.command.help") + commandWordReload -> has("ticketmanager.command.reload") + commandWordList -> has("ticketmanager.command.list") + commandWordListAssigned -> has("ticketmanager.command.list") + commandWordReopen -> has("ticketmanager.command.reopen") + commandWordSilentReopen -> has("ticketmanager.command.reopen") && hasSilent() + commandWordSearch -> has("ticketmanager.command.search") + commandWordSetPriority -> has("ticketmanager.command.setPriority") + commandWordSilentSetPriority -> has("ticketmanager.command.setPriority") && hasSilent() + commandWordTeleport -> has("ticketmanager.command.teleport") + commandWordView -> hasDuality("ticketmanager.command.view") + commandWordDeepView -> hasDuality("ticketmanager.command.viewdeep") + commandWordConvertDB -> has("ticketmanager.command.convertDatabase") + commandWordHistory -> + sender.has("ticketmanager.command.history.all") || + sender.has("ticketmanager.command.history.own").let { hasPerm -> + if (args.size >= 2) hasPerm && args[1] == sender.name + else hasPerm + } + else -> true + } + } + .also { if (!it) sender.sendMessage(text { formattedContent(senderLocale.warningsNoPermission) }) } + } catch (e: Exception) { + sender.sendMessage(text { formattedContent(senderLocale.warningsNoPermission) }) + return false + } + } + + private fun isValidCommand( + sender: CommandSender, + basicTicket: BasicTicket, + args: List, + senderLocale: TMLocale + ): Boolean { + fun sendMessage(formattedString: String) = sender.sendMessage(text { formattedContent(formattedString) }) + fun invalidCommand() = sendMessage(senderLocale.warningsInvalidCommand) + fun notANumber() = sendMessage(senderLocale.warningsInvalidNumber) + fun outOfBounds() = sendMessage(senderLocale.warningsPriorityOutOfBounds) + fun ticketClosed() = sendMessage(senderLocale.warningsTicketAlreadyClosed) + fun ticketOpen() = sendMessage(senderLocale.warningsTicketAlreadyOpen) + + return senderLocale.run { + when (args[0]) { + commandWordAssign, commandWordSilentAssign -> + check(::invalidCommand) { args.size >= 3 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordClaim, commandWordSilentClaim -> + check(::invalidCommand) { args.size == 2 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordClose, commandWordSilentClose -> + check(::invalidCommand) { args.size >= 2 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordComment, commandWordSilentComment -> + check(::invalidCommand) { args.size >= 3 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordCloseAll, commandWordSilentCloseAll -> + check(::invalidCommand) { args.size == 3 } + .thenCheck(::notANumber) { args[1].toIntOrNull() != null } + .thenCheck(::notANumber) { args[2].toIntOrNull() != null } + + commandWordReopen, commandWordSilentReopen -> + check(::invalidCommand) { args.size == 2 } + .thenCheck(::ticketOpen) { basicTicket.status != BasicTicket.Status.OPEN } + + commandWordSetPriority, commandWordSilentSetPriority -> + check(::invalidCommand) { args.size == 3 } + .thenCheck(::outOfBounds) { args[2].toByteOrNull() != null } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordUnassign, commandWordSilentUnassign -> + check(::invalidCommand) { args.size == 2 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordView -> check(::invalidCommand) { args.size == 2 } + + commandWordDeepView -> check(::invalidCommand) { args.size == 2 } + + commandWordTeleport -> check(::invalidCommand) { args.size == 2 } + + commandWordCreate -> check(::invalidCommand) { args.size >= 2 } + + commandWordHistory -> + check(::invalidCommand) { args.isNotEmpty() } + .thenCheck(::notANumber) { if (args.size >= 3) args[2].toIntOrNull() != null else true} + + commandWordList -> + check(::notANumber) { if (args.size == 2) args[1].toIntOrNull() != null else true } + + commandWordListAssigned -> + check(::notANumber) { if (args.size == 2) args[1].toIntOrNull() != null else true } + + commandWordSearch -> check(::invalidCommand) { args.size >= 2} + + commandWordReload -> true + commandWordVersion -> true + commandWordHelp -> true + + commandWordConvertDB -> + check(::invalidCommand) { args.size == 2 } + .thenCheck( { sendMessage(senderLocale.warningsInvalidDBType) }, + { + try { Database.Type.valueOf(args[1]); true } + catch (e: Exception) { false } + } + ) + .thenCheck( { sendMessage(senderLocale.warningsConvertToSameDBType) } ) + { pluginState.database.type != Database.Type.valueOf(args[1]) } + + else -> false.also { invalidCommand() } + } + } + } + + private suspend fun notUnderCooldown( + sender: CommandSender, + senderLocale: TMLocale, + args: List + ): Boolean { + val underCooldown = when (args[0]) { + senderLocale.commandWordCreate, + senderLocale.commandWordComment, + senderLocale.commandWordSilentComment -> + pluginState.cooldowns.checkAndSetAsync(sender.toUUIDOrNull()) + else -> false + } + + if (underCooldown) + sender.sendMessage(text { formattedContent(senderLocale.warningsUnderCooldown) }) + + return !underCooldown + } + + private suspend fun executeCommand( + sender: CommandSender, + args: List, + senderLocale: TMLocale, + ticketHandler: BasicTicketHandler + ): NotifyParams? { + return senderLocale.run { + when (args[0]) { + commandWordAssign -> assign(sender, args, false, senderLocale, ticketHandler) + commandWordAssign -> assign(sender, args, false, senderLocale, ticketHandler) + commandWordSilentAssign -> assign(sender, args, true, senderLocale, ticketHandler) + commandWordClaim -> claim(sender, args, false, senderLocale, ticketHandler) + commandWordSilentClaim -> claim(sender, args, true, senderLocale, ticketHandler) + commandWordClose -> close(sender, args, false, ticketHandler) + commandWordSilentClose -> close(sender, args, true, ticketHandler) + commandWordCloseAll -> closeAll(sender, args, false, ticketHandler) + commandWordSilentCloseAll -> closeAll(sender, args, true, ticketHandler) + commandWordComment -> comment(sender, args, false, ticketHandler) + commandWordSilentComment -> comment(sender, args, true, ticketHandler) + commandWordCreate -> create(sender, args) + commandWordHelp -> help(sender, senderLocale).let { null } + commandWordHistory -> history(sender, args, senderLocale).let { null } + commandWordList -> list(sender, args, senderLocale).let { null } + commandWordListAssigned -> listAssigned(sender, args, senderLocale).let { null } + commandWordReload -> reload(sender, senderLocale).let { null } + commandWordReopen -> reopen(sender,args, false, ticketHandler) + commandWordSilentReopen -> reopen(sender,args, true, ticketHandler) + commandWordSearch -> search(sender, args, senderLocale).let { null } + commandWordSetPriority -> setPriority(sender, args, false, ticketHandler) + commandWordSilentSetPriority -> setPriority(sender, args, true, ticketHandler) + commandWordTeleport -> teleport(sender, ticketHandler).let { null } + commandWordUnassign -> unAssign(sender, args, false, senderLocale, ticketHandler) + commandWordSilentUnassign -> unAssign(sender, args, true, senderLocale, ticketHandler) + commandWordVersion -> version(sender, senderLocale).let { null } + commandWordView -> view(sender, senderLocale, ticketHandler).let { null } + commandWordDeepView -> viewDeep(sender, senderLocale, ticketHandler).let { null } + commandWordConvertDB -> convertDatabase(args).let { null } + else -> null + } + } + } + + private fun pushNotifications( + sender: CommandSender, + params: NotifyParams, + locale: TMLocale, + basicTicket: BasicTicket + ) { + params.run { + if (sendSenderMSG) + sender.sendMessage(senderLambda!!.invoke(locale)) + + if (sendCreatorMSG) + basicTicket.creatorUUID + ?.run(Bukkit::getPlayer) + ?.let { creatorLambda!!(it.toTMLocale()) } + ?.run { creator!!.sendMessage(this) } + + if (sendMassNotifyMSG) + pushMassNotify(massNotifyPerm, massNotifyLambda!!) + } + } + + private class NotifyParams( + silent: Boolean, + basicTicket: BasicTicket, + sender: CommandSender, + creatorAlertPerm: String, + val massNotifyPerm: String, + val senderLambda: ((TMLocale) -> Component)?, + val creatorLambda: ((TMLocale) -> Component)?, + val massNotifyLambda: ((TMLocale) -> Component)?, + ) { + val creator: Player? = basicTicket.creatorUUID?.let(Bukkit::getPlayer) + val sendSenderMSG: Boolean = (!sender.has(massNotifyPerm) || silent) + && senderLambda != null + val sendCreatorMSG: Boolean = sender.nonCreatorMadeChange(basicTicket.creatorUUID) + && !silent && (creator?.isOnline ?: false) + && (creator?.has(creatorAlertPerm) ?: false) + && (creator?.has(massNotifyPerm)?.run { !this } ?: false) + && creatorLambda != null + val sendMassNotifyMSG: Boolean = !silent + && massNotifyLambda != null + } + + /*-------------------------*/ + /* Commands */ + /*-------------------------*/ + + private suspend fun allAssignVariations( + sender: CommandSender, + silent: Boolean, + senderLocale: TMLocale, + assignmentID: String, + dbAssignment: String?, + ticketHandler: BasicTicketHandler, + ): NotifyParams = coroutineScope { + val shownAssignment = dbAssignment ?: senderLocale.miscNobody + + launch { ticketHandler.setAssignedTo(dbAssignment) } + launch { pluginState.database.addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.ASSIGN, sender.toUUIDOrNull(), dbAssignment) + )} + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + senderLambda = { + val content = it.notifyTicketAssignSuccess + .replace("%id%", assignmentID) + .replace("%assign%", shownAssignment) + text { formattedContent(content) } + }, + massNotifyLambda = { + val content = it.notifyTicketAssignEvent + .replace("%user%", sender.name) + .replace("%id%", assignmentID) + .replace("%assign%", shownAssignment) + text { formattedContent(content) } + }, + creatorLambda = null, + creatorAlertPerm = "ticketmanager.notify.change.assign", + massNotifyPerm = "ticketmanager.notify.massNotify.assign" + ) + } + + // /ticket assign + private suspend fun assign( + sender: CommandSender, + args: List, + silent: Boolean, + senderLocale: TMLocale, + ticketHandler: BasicTicketHandler, + ): NotifyParams { + val sqlAssignment = args.subList(2, args.size).joinToString(" ") + return allAssignVariations(sender, silent, senderLocale, args[1], sqlAssignment, ticketHandler) + } + + // /ticket claim + private suspend fun claim( + sender: CommandSender, + args: List, + silent: Boolean, + senderLocale: TMLocale, + ticketHandler: BasicTicketHandler, + ): NotifyParams { + return allAssignVariations(sender, silent, senderLocale, args[1], sender.name, ticketHandler) + } + + // /ticket close [Comment...] + private suspend fun close( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler + ): NotifyParams = coroutineScope { + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates + if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { + launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } + } + + return@coroutineScope if (args.size >= 3) + closeWithComment(sender, args, silent, ticketHandler) + else closeWithoutComment(sender, args, silent, ticketHandler) + } + + private suspend fun closeWithComment( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler, + ): NotifyParams = coroutineScope { + val message = args.subList(2, args.size) + .joinToString(" ") + .run(ChatColor::stripColor)!! + + launch { + pluginState.database.run { + addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.COMMENT, sender.toUUIDOrNull(), message) + ) + addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.CLOSE, sender.toUUIDOrNull()) + ) + ticketHandler.setTicketStatus(BasicTicket.Status.CLOSED) + } + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + senderLambda = { + val content = it.notifyTicketCloseWCommentSuccess + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + massNotifyLambda = { + val content = it.notifyTicketCloseWCommentEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + .replace("%message%", message) + text { formattedContent(content) } + }, + creatorLambda = { + val content = it.notifyTicketModificationEvent + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + massNotifyPerm = "ticketmanager.notify.massNotify.close", + creatorAlertPerm = "ticketmanager.notify.change.close" + ) + } + + private suspend fun closeWithoutComment( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler + ): NotifyParams = coroutineScope { + launch { + pluginState.database.addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.CLOSE, sender.toUUIDOrNull()) + ) + ticketHandler.setTicketStatus(BasicTicket.Status.CLOSED) + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + creatorLambda = { + val content = it.notifyTicketModificationEvent + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + massNotifyLambda = { + val content = it.notifyTicketCloseEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + senderLambda = { + val content = it.notifyTicketCloseSuccess + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + massNotifyPerm = "ticketmanager.notify.massNotify.close", + creatorAlertPerm = "ticketmanager.notify.change.close" + ) + } + + // /ticket closeall + private suspend fun closeAll( + sender: CommandSender, + args: List, + silent: Boolean, + basicTicket: BasicTicket + ): NotifyParams = coroutineScope { + val lowerBound = args[1].toInt() + val upperBound = args[2].toInt() + + launch { pluginState.database.massCloseTickets(lowerBound, upperBound, sender.toUUIDOrNull()) } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = basicTicket, + creatorLambda = null, + senderLambda = { + val content =it.notifyTicketMassCloseSuccess + .replace("%low%", args[1]) + .replace("%high%", args[2]) + text { formattedContent(content) } + }, + massNotifyLambda = { + val content = it.notifyTicketMassCloseEvent + .replace("%user%", sender.name) + .replace("%low%", args[1]) + .replace("%high%", args[2]) + text { formattedContent(content) } + }, + massNotifyPerm = "ticketmanager.notify.massNotify.massClose", + creatorAlertPerm = "ticketmanager.notify.change.massClose" + ) + } + + // /ticket comment + private suspend fun comment( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler, + ): NotifyParams = coroutineScope { + val message = args.subList(2, args.size) + .joinToString(" ") + .run(ChatColor::stripColor)!! + + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates + if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { + launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } + } + + launch { + pluginState.database.addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.COMMENT, sender.toUUIDOrNull(), message) + ) + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + creatorLambda = { + val content = it.notifyTicketModificationEvent + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + senderLambda = { + val content = it.notifyTicketCommentSuccess + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + massNotifyLambda = { + val content = it.notifyTicketCommentEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + .replace("%message%", message) + text { formattedContent(content) } + }, + massNotifyPerm = "ticketmanager.notify.massNotify.comment", + creatorAlertPerm = "ticketmanager.notify.change.comment" + ) + } + + // /ticket convertdatabase + private suspend fun convertDatabase(args: List) { + //TODO + /* + val type = args[1].run(Database.Type::valueOf) + pluginState.database.migrateDatabase(type) + + */ + } + + // /ticket create + private suspend fun create( + sender: CommandSender, + args: List, + ): NotifyParams = coroutineScope { + val message = args.subList(1, args.size) + .joinToString(" ") + .run(ChatColor::stripColor)!! + + val ticket = BasicTicket(creatorUUID = sender.toUUIDOrNull(), location = sender.toTicketLocationOrNull()) + + val deferredID = pluginState.database.addNewTicketAsync(ticket, message) + mainPlugin.ticketCountMetrics.run { set(check() + 1) } + val id = deferredID.await().toString() + + NotifyParams( + silent = false, + sender = sender, + basicTicket = ticket, + creatorLambda = null, + senderLambda = { + val content = it.notifyTicketCreationSuccess + .replace("%id%", id) + text { formattedContent(content) } + }, + massNotifyLambda = { + val content = it.notifyTicketCreationEvent + .replace("%user%", sender.name) + .replace("%id%", id) + .replace("%message%", message) + text { formattedContent(content) } + }, + creatorAlertPerm = "ticketmanager.NO NODE", + massNotifyPerm = "ticketmanager.notify.massNotify.create", + ) + } + + // /ticket help + private fun help( + sender: CommandSender, + locale: TMLocale, + ) { + val hasSilentPerm = sender.has("ticketmanager.commandArg.silence") + val cc = pluginState.localeHandler.mainColourCode + + val component = buildComponent { + text { formattedContent(locale.helpHeader) } + text { formattedContent(locale.helpLine1) } + + if (hasSilentPerm) { + text { formattedContent(locale.helpLine2) } + text { formattedContent(locale.helpLine3) } + } + text { formattedContent(locale.helpSep) } + + locale.run { + listOf( // Triple(silence-able, format, permissions) + Triple(true, "$commandWordAssign &f<$parameterID> <$parameterAssignment...>", listOf("ticketmanager.command.assign")), + Triple(true, "$commandWordClaim &f<$parameterID>", listOf("ticketmanager.command.claim")), + Triple(true, "$commandWordClose &f<$parameterID> &7[$parameterComment...]", listOf("ticketmanager.command.close.all", "ticketmanager.command.close.own")), + Triple(true, "$commandWordCloseAll &f<$parameterLowerID> <$parameterUpperID>", listOf("ticketmanager.command.closeAll")), + Triple(true, "$commandWordComment &f<$parameterID> <$parameterComment...>", listOf("ticketmanager.command.comment.all", "ticketmanager.command.comment.own")), + Triple(false, "$commandWordConvertDB &f<$parameterTargetDB>", listOf("ticketmanager.command.convertDatabase")), + Triple(false, "$commandWordCreate &f<$parameterComment...>", listOf("ticketmanager.command.create")), + Triple(false, commandWordHelp, listOf("ticketmanager.command.help")), + Triple(false, "$commandWordHistory &7[$parameterUser] [$parameterPage]", listOf("ticketmanager.command.history.all", "ticketmanager.command.history.own")), + Triple(false, "$commandWordList &7[$parameterPage]", listOf("ticketmanager.command.list")), + Triple(false, "$commandWordListAssigned &7[$parameterPage]", listOf("ticketmanager.command.list")), + Triple(false, commandWordReload, listOf("ticketmanager.command.reload")), + Triple(true, "$commandWordReopen &f<$parameterID>", listOf("ticketmanager.command.reopen")), + Triple(false, "$commandWordSearch &f<$parameterConstraints...>", listOf("ticketmanager.command.search")), + Triple(true, "$commandWordSetPriority &f<$parameterID> <$parameterLevel>", listOf("ticketmanager.command.setPriority")), + Triple(false, "$commandWordTeleport &f<$parameterID>", listOf("ticketmanager.command.teleport")), + Triple(true, "$commandWordUnassign &f<$parameterID>", listOf("ticketmanager.command.assign")), + Triple(false, "$commandWordView &f<$parameterID>", listOf("ticketmanager.command.view.all", "ticketmanager.command.view.own")), + Triple(false, "$commandWordDeepView &f<$parameterID>", listOf("ticketmanager.command.viewdeep.all", "ticketmanager.command.viewdeep.own")) + ) + } + .filter { it.third.any(sender::has) } + .run { this + Triple(false, locale.commandWordVersion, "NA") } + .map { + val commandString = "$cc/${locale.commandBase} ${it.second}" + if (hasSilentPerm) + if (it.first) "\n&a[✓] $commandString" + else "\n&c[✕] $commandString" + else "\n$commandString" + } + .forEach { text { formattedContent(it) } } + } + + sender.sendMessage(component) + } + + // /ticket history [User] [Page] + private suspend fun history( + sender: CommandSender, + args: List, + locale: TMLocale, + ) { + coroutineScope { + val targetName = + if (args.size >= 2) args[1].takeIf { it != locale.consoleName } else sender.name.takeIf { sender is Player } + val requestedPage = if (args.size >= 3) args[2].toInt() else 1 + + // Leaves console as null. Otherwise attempts UUID grab or [PLAYERNOTFOUND] + fun String.attemptToUUIDString(): String? = + if (equals(locale.consoleName)) null + else Bukkit.getOfflinePlayers().asSequence() + .firstOrNull { equals(it.name) } + ?.run { uniqueId.toString() } + ?: "[PLAYERNOTFOUND]" + + val searchedUser = targetName?.attemptToUUIDString() + + val resultSize: Int + val resultsChunked = pluginState.database.searchDatabase { it.creatorUUID.toString() == searchedUser } + .toList() + .sortedByDescending(BasicTicket::id) + .also { resultSize = it.size } + .chunked(6) + + val sentComponent = buildComponent { + text { + formattedContent( + locale.historyHeader + .replace("%name%", targetName ?: locale.consoleName) + .replace("%count%", "$resultSize") + ) + } + + val actualPage = if (requestedPage >= 1 && requestedPage < resultsChunked.size) requestedPage else 1 + + if (resultsChunked.isNotEmpty()) { + resultsChunked.getOrElse(requestedPage - 1) { resultsChunked[1] }.forEach { t -> + text { + formattedContent( + locale.historyEntry + .let { "\n$it" } + .replace("%id%", "${t.id}") + .replace("%SCC%", t.status.colourCode) + .replace("%status%", t.status.toLocaledWord(locale)) + .replace("%comment%", t.actions[0].message!!) + .let { if (it.length > 80) "${it.substring(0, 81)}..." else it } + ) + onHover { showText(Component.text(locale.clickViewTicket)) } + onClick { + action = ClickEvent.Action.RUN_COMMAND + value = locale.run { "/$commandBase $commandWordView ${t.id}" } + } + } + } + + if (resultsChunked.size > 1) { + append(buildPageComponent(actualPage, resultsChunked.size, locale) { + "/${it.commandBase} ${it.commandWordHistory} ${targetName ?: it.consoleName} " + }) + } + } + } + + sender.sendMessage(sentComponent) + } + } + + private fun buildPageComponent( + curPage: Int, + pageCount: Int, + locale: TMLocale, + baseCommand: (TMLocale) -> String, + ): Component { + + fun Component.addForward() { + color(NamedTextColor.WHITE) + clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage + 1}")) + hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickNextPage))) + } + + fun Component.addBack() { + color(NamedTextColor.WHITE) + clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage - 1}")) + hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickBackPage))) + } + + val back = Component.text("[${locale.pageBack}]") + val next = Component.text("[${locale.pageNext}]") + val separator = text { + content("...............") + color(NamedTextColor.DARK_GRAY) + } + val cc = pluginState.localeHandler.mainColourCode + val ofSection = text { formattedContent("$cc($curPage${locale.pageOf}$pageCount)") } + + when (curPage) { + 1 -> { + back.color(NamedTextColor.DARK_GRAY) + next.addForward() + } + pageCount -> { + back.addBack() + next.color(NamedTextColor.DARK_GRAY) + } + else -> { + back.addBack() + back.addForward() + } + } + + return Component.text("\n") + .append(back) + .append(separator) + .append(ofSection) + .append(separator) + .append(next) + } + + private fun createListEntry( + ticket: FullTicket, + locale: TMLocale + ): Component { + val creator = ticket.creatorUUID.toName(locale) + val fixedAssign = ticket.assignedTo ?: "" + + // Shortens comment preview to fit on one line + val fixedComment = ticket.run { + if (12 + id.toString().length + creator.length + fixedAssign.length + actions[0].message!!.length > 58) + actions[0].message!!.substring( + 0, + 43 - id.toString().length - fixedAssign.length - creator.length + ) + "..." + else actions[0].message!! + } + + return text { + formattedContent( + "\n${locale.listFormatEntry}" + .replace("%priorityCC%", ticket.priority.colourCode) + .replace("%ID%", "${ticket.id}") + .replace("%creator%", creator) + .replace("%assign%", fixedAssign) + .replace("%comment%", fixedComment) + ) + onHover { showText(Component.text(locale.clickViewTicket)) } + onClick { + action = ClickEvent.Action.RUN_COMMAND + value = locale.run { "/$commandBase $commandWordView ${ticket.id}" } + } + } } -} \ No newline at end of file + + private suspend fun createGeneralList( + args: List, + locale: TMLocale, + headerFormat: String, + getTickets: suspend (Database) -> List, + baseCommand: (TMLocale) -> String + ): Component { + val chunkedTickets = getTickets(pluginState.database).chunked(8) + val page = if (args.size == 2 && args[1].toInt() in 1..chunkedTickets.size) args[1].toInt() else 1 + + return buildComponent { + text { formattedContent(headerFormat) } + + if (chunkedTickets.isNotEmpty()) { + chunkedTickets[page - 1].forEach { append(createListEntry(it, locale)) } + + if (chunkedTickets.size > 1) { + append(buildPageComponent(page, chunkedTickets.size, locale, baseCommand)) + } + } + } + } + + // /ticket list [Page] + private suspend fun list( + sender: CommandSender, + args: List, + locale: TMLocale, + ) { + sender.sendMessage( + createGeneralList(args, locale, locale.listFormatHeader, + getTickets = { db -> db.getFullOpenAsFlow().toList() }, + baseCommand = locale.run{ { "/$commandBase $commandWordList " } } + ) + ) + } + + // /ticket listassigned [Page] + private suspend fun listAssigned( + sender: CommandSender, + args: List, + locale: TMLocale, + ) { + val groups = if (sender is Player) + mainPlugin.perms.getPlayerGroups(sender).map { "::$it" } + else listOf() + + sender.sendMessage( + createGeneralList(args, locale, locale.listFormatAssignedHeader, + getTickets = { db -> db.getFullOpenAssignedAsFlow(sender.name, groups).toList() }, + baseCommand = locale.run { { "/$commandBase $commandWordListAssigned " } } + ) + ) + } + + // /ticket reload + private suspend fun reload( + sender: CommandSender, + locale: TMLocale, + ) { + coroutineScope { + mainPlugin.pluginLocked.set(true) + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationReloadInitiated.replace("%user%", sender.name)) } + } + + // Eventually try making it wait for other tasks to finish. Will require keeping track of jobs + //pushMassNotify("ticketmanager.notify.info", { it.informationReloadTasksDone } ) + pluginState.database.closeDatabase() + mainPlugin.loadPlugin() + + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationReloadSuccess) } + } + if (!sender.has("ticketmanager.notify.info")) { + sender.sendMessage(text { formattedContent(locale.informationReloadSuccess) }) + } + } + } + + // /ticket reopen + private suspend fun reopen( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler, + ): NotifyParams = coroutineScope { + val action = FullTicket.Action(FullTicket.Action.Type.REOPEN, sender.toUUIDOrNull()) + + // Updates user status if needed + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates + if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { + launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } + } + + launch { + pluginState.database.addAction(ticketHandler.id, action) + ticketHandler.setTicketStatus(BasicTicket.Status.OPEN) + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + creatorLambda = { + val content = it.notifyTicketModificationEvent + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + senderLambda = { + val content = it.notifyTicketReopenSuccess + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + massNotifyLambda = { + val content = it.notifyTicketReopenEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + text { formattedContent(content) } + }, + creatorAlertPerm = "ticketmanager.notify.change.reopen", + massNotifyPerm = "ticketmanager.notify.massNotify.reopen", + ) + } + + // /ticket search + private suspend fun search( + sender: CommandSender, + args: List, + locale: TMLocale, + ) { + coroutineScope { + fun String.attemptToUUIDString(): String? = + if (equals(locale.consoleName)) null + else Bukkit.getOfflinePlayers().asSequence() + .firstOrNull { equals(it.name) } + ?.run { uniqueId.toString() } + ?: "[PLAYERNOTFOUND]" + + // Beginning of code execution + sender.sendMessage(text { formattedContent(locale.searchFormatQuerying) }) + val constraintTypes = locale.run { + listOf( + searchAssigned, + searchCreator, + searchKeywords, + searchPriority, + searchStatus, + searchTime, + searchWorld, + searchPage, + searchClosedBy, + searchLastClosedBy, + ) + } + + val localedConstraintMap = args.subList(1, args.size) + .asSequence() + .map { it.split(":", limit = 2) } + .filter { it[0] in constraintTypes } + .filter { it.size >= 2 } + .associate { it[0] to it[1] } + + val searchFunctions = localedConstraintMap + .mapNotNull { entry -> + when (entry.key) { + locale.searchWorld -> { t: FullTicket -> t.location?.world?.equals(entry.value) ?: false } + locale.searchAssigned -> { t: FullTicket -> t.assignedTo == entry.value } + + locale.searchCreator -> { + val searchedUser = entry.value.attemptToUUIDString(); + { t: FullTicket -> t.creatorUUID?.toString() == searchedUser } + } + + locale.searchPriority -> { + val searchedPriority = entry.value.toByteOrNull() ?: 0; + { t: FullTicket -> t.priority.level == searchedPriority } + } + + locale.searchTime -> { + val creationTime = relTimeToEpochSecond(entry.value, locale); + { t: FullTicket -> t.actions[0].timestamp >= creationTime } + } + + locale.searchStatus -> { + val constraintStatus = when (entry.value) { + locale.statusOpen -> BasicTicket.Status.OPEN.name + locale.statusClosed -> BasicTicket.Status.CLOSED.name + else -> entry.value + } + { t: FullTicket -> t.status.name == constraintStatus} + } + + locale.searchKeywords -> { + val words = entry.value.split(","); + + { t: FullTicket -> + val comments = t.actions + .filter { it.type == FullTicket.Action.Type.OPEN || it.type == FullTicket.Action.Type.COMMENT } + .map { it.message!! } + words.map { w -> comments.any { it.contains(w) } } + .all { it } + } + } + + locale.searchLastClosedBy -> { + val searchedUser = entry.value.attemptToUUIDString(); + { t: FullTicket -> + t.actions.lastOrNull { e -> e.type == FullTicket.Action.Type.CLOSE } + ?.run { user?.toString() == searchedUser } + ?: false + } + + } + + locale.searchClosedBy -> { + val searchedUser = entry.value.attemptToUUIDString(); + { t: FullTicket -> t.actions.any{ it.type == FullTicket.Action.Type.CLOSE && it.user?.toString() == searchedUser } } + } + + else -> null + } + } + .asSequence() + + val composedSearch = { t: FullTicket -> searchFunctions.map { it(t) }.all { it } } + + // Results Computation + val resultSize: Int + val chunkedTickets = pluginState.database.searchDatabase(composedSearch) + .toList() + .sortedByDescending(BasicTicket::id) + .apply { resultSize = size } + .chunked(8) + + val page = localedConstraintMap[locale.searchPage]?.toIntOrNull() + .let { if (it != null && it >= 1 && it < chunkedTickets.size) it else 1 } + val fixMSGLength = { t: FullTicket -> t.actions[0].message!!.run { if (length > 25) "${substring(0,21)}..." else this } } + + val sentComponent = buildComponent { + + // Initial header + text { + formattedContent( + locale.searchFormatHeader.replace("%size%", "$resultSize") + ) + } + + // Adds entries + if (chunkedTickets.isNotEmpty()) { + chunkedTickets[page-1] + .map { + val content = "\n${locale.searchFormatEntry}" + .replace("%PCC%", it.priority.colourCode) + .replace("%SCC%", it.status.colourCode) + .replace("%id%", "${it.id}") + .replace("%status%", it.status.toLocaledWord(locale)) + .replace("%creator%", it.creatorUUID.toName(locale)) + .replace("%assign%", it.assignedTo ?: "") + .replace("%world%", it.location?.world ?: "") + .replace("%time%", it.actions[0].timestamp.toLargestRelativeTime(locale)) + .replace("%comment%", fixMSGLength(it)) + it.id to content + } + .forEach { + text { + formattedContent(it.second) + onHover { showText(Component.text(locale.clickViewTicket)) } + onClick { + action = ClickEvent.Action.RUN_COMMAND + value = locale.run { "/$commandBase $commandWordView ${it.first}" } + } + } + } + } + + // Implements pages if needed + if (chunkedTickets.size > 1) { + val pageComponent = buildPageComponent(page, chunkedTickets.size, locale) { + // Removes page constraint and converts rest to key:arg + val constraints = localedConstraintMap + .filter { it.key != locale.searchPage } + .map { (k, v) -> "$k:$v" } + "/${locale.commandBase} ${locale.commandWordSearch} $constraints ${locale.searchPage}:" + } + append(pageComponent) + } + } + + sender.sendMessage(sentComponent) + } + } + + // /ticket setpriority + private suspend fun setPriority( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler, + ): NotifyParams = coroutineScope { + launch { + pluginState.database.addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.SET_PRIORITY, sender.toUUIDOrNull(), args[2]) + ) + ticketHandler.setTicketPriority(byteToPriority(args[2].toByte())) + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + creatorLambda = null, + senderLambda = { + val content = it.notifyTicketSetPrioritySuccess + .replace("%id%", args[1]) + .replace("%priority%", ticketHandler.run { priority.colourCode + priority.toLocaledWord(it) }) + text { formattedContent(content) } + }, + massNotifyLambda = { + val content = it.notifyTicketSetPriorityEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + .replace("%priority%", ticketHandler.run { priority.colourCode + priority.toLocaledWord(it) }) + text { formattedContent(content) } + }, + creatorAlertPerm = "ticketmanager.notify.change.priority", + massNotifyPerm = "ticketmanager.notify.massNotify.priority" + ) + } + + // /ticket teleport + private fun teleport( + sender: CommandSender, + basicTicket: BasicTicket, + ) { + if (sender is Player && basicTicket.location != null) { + val loc = basicTicket.location!!.run { Location(Bukkit.getWorld(world), x.toDouble(), y.toDouble(), z.toDouble()) } + sender.teleportAsync(loc) + } + } + + // /ticket unassign + private suspend fun unAssign( + sender: CommandSender, + args: List, + silent: Boolean, + senderLocale: TMLocale, + ticketHandler: BasicTicketHandler, + ): NotifyParams { + return allAssignVariations(sender, silent, senderLocale, args[1], null, ticketHandler) + } + + // /ticket version + private fun version( + sender: CommandSender, + locale: TMLocale, + ) { + val sentComponent = buildComponent { + text { + content("===========================\n") + color(NamedTextColor.DARK_AQUA) + } + text { + content("TicketManager:") + decorate(TextDecoration.BOLD) + color(NamedTextColor.DARK_AQUA) + append(Component.text(" by HoshiKurama\n", NamedTextColor.GRAY)) + } + text { + content(" GitHub Wiki: ") + color(NamedTextColor.DARK_AQUA) + } + text { + content("HERE\n") + color(NamedTextColor.GRAY) + decorate(TextDecoration.UNDERLINED) + clickEvent(ClickEvent.openUrl(locale.wikiLink)) + onHover { showText(Component.text(locale.clickWiki)) } + } + text { + content(" V$pluginVersion\n") + color(NamedTextColor.DARK_AQUA) + } + text { + content("===========================") + color(NamedTextColor.DARK_AQUA) + } + } + + sender.sendMessage(sentComponent) + } + + // /ticket view + private suspend fun view( + sender: CommandSender, + locale: TMLocale, + ticketHandler: BasicTicketHandler, + ) { + coroutineScope { + val fullTicket = ticketHandler.toFullTicketAsync().await() + val baseComponent = buildTicketInfoComponent(fullTicket, locale) + + if (!sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && ticketHandler.creatorStatusUpdate) + launch { ticketHandler.setCreatorStatusUpdate(false) } + + val entries = fullTicket.actions.asSequence() + .filter { it.type == FullTicket.Action.Type.COMMENT || it.type == FullTicket.Action.Type.OPEN } + .map { + "\n${locale.viewFormatComment}" + .replace("%user%", it.user.toName(locale)) + .replace("%comment%", it.message!!) + } + .map { text { formattedContent(it) } } + .reduce(TextComponent::append) + + sender.sendMessage(baseComponent.append(entries)) + } + } + + // /ticket viewdeep + private suspend fun viewDeep( + sender: CommandSender, + locale: TMLocale, + ticketHandler: BasicTicketHandler, + ) { + coroutineScope { + val fullTicket = ticketHandler.toFullTicketAsync().await() + val baseComponent = buildTicketInfoComponent(fullTicket, locale) + + if (!sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && ticketHandler.creatorStatusUpdate) + launch { ticketHandler.setCreatorStatusUpdate(false) } + + fun formatDeepAction(action: FullTicket.Action): String { + val result = when (action.type) { + FullTicket.Action.Type.OPEN, FullTicket.Action.Type.COMMENT -> + "\n${locale.viewFormatDeepComment}" + .replace("%comment%", action.message!!) + + FullTicket.Action.Type.SET_PRIORITY -> + "\n${locale.viewFormatDeepSetPriority}" + .replace("%priority%", + byteToPriority(action.message!!.toByte()).run { colourCode + toLocaledWord(locale) } + ) + + FullTicket.Action.Type.ASSIGN -> + "\n${locale.viewFormatDeepAssigned}" + .replace("%assign%", action.message ?: "") + + FullTicket.Action.Type.REOPEN -> "\n${locale.viewFormatDeepReopen}" + FullTicket.Action.Type.CLOSE -> "\n${locale.viewFormatDeepClose}" + FullTicket.Action.Type.MASS_CLOSE -> "\n${locale.viewFormatDeepMassClose}" + } + return result + .replace("%user%", action.user.toName(locale)) + .replace("%time%", action.timestamp.toLargestRelativeTime(locale)) + } + + val entries = fullTicket.actions.asSequence() + .map(::formatDeepAction) + .map { text { formattedContent(it) } } + .reduce(TextComponent::append) + + sender.sendMessage(baseComponent.append(entries)) + } + } + + + + private fun buildTicketInfoComponent( + ticket: FullTicket, + locale: TMLocale, + ) = buildComponent { + text { + formattedContent( + "\n${locale.viewFormatHeader}" + .replace("%num%", "${ticket.id}") + ) + } + text { formattedContent("\n${locale.viewFormatSep1}") } + text { + formattedContent( + "\n${locale.viewFormatInfo1}" + .replace("%creator%", ticket.creatorUUID.toName(locale)) + .replace("%assignment%", ticket.assignedTo ?: "") + ) + } + text { + formattedContent( + "\n${locale.viewFormatInfo2}" + .replace("%priority%", ticket.priority.run { colourCode + toLocaledWord(locale) }) + .replace("%status%", ticket.status.run { colourCode + toLocaledWord(locale) }) + ) + } + text { + formattedContent( + "\n${locale.viewFormatInfo3}" + .replace("%location%", ticket.location?.toString() ?: "") + ) + + if (ticket.location != null) { + onHover { showText(Component.text(locale.clickTeleport)) } + onClick { + action = ClickEvent.Action.RUN_COMMAND + value = locale.run { "/$commandBase $commandWordTeleport ${ticket.id}" } + } + } + } + text { formattedContent("\n${locale.viewFormatSep2}") } + } + +} + +/*-------------------------*/ +/* Other Functions */ +/*-------------------------*/ + +private inline fun check(error: () -> Unit, predicate: () -> Boolean): Boolean { + return if (predicate()) true else error().run { false } +} + +private inline fun Boolean.thenCheck(error: () -> Unit, predicate: () -> Boolean): Boolean { + return if (!this) false + else if (predicate()) true + else error().run { false } +} + +private fun CommandSender.nonCreatorMadeChange(creatorUUID: UUID?): Boolean { + if (creatorUUID == null) return false + return this.toUUIDOrNull()?.notEquals(creatorUUID) ?: true +} + +private fun CommandSender.toTicketLocationOrNull() = if (this is Player) + location.run { BasicTicket.TicketLocation(world.name, blockX, blockY, blockZ) } + else null \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt index c3005cb..f073c70 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt @@ -20,14 +20,13 @@ class PlayerJoin : Listener { @EventHandler suspend fun onPlayerJoin(event: PlayerJoinEvent) = coroutineScope { if (mainPlugin.pluginLocked.check()) return@coroutineScope - val player = event.player withContext(mainPlugin.asyncDispatcher) { //Plugin Update Checking launch { - val pluginUpdateStatus = pluginState.await().pluginUpdateAvailable.await() + val pluginUpdateStatus = pluginState.pluginUpdateAvailable.await() if (player.has("ticketmanager.notify.pluginUpdate") && pluginUpdateStatus != null) { val sentMSG = player.toTMLocale().notifyPluginUpdate .replace("%current%", pluginUpdateStatus.first) @@ -39,7 +38,7 @@ class PlayerJoin : Listener { // Unread Updates launch { if (player.has("ticketmanager.notify.unreadUpdates.onJoin")) { - pluginState.await().database.getTicketIDsWithUpdates(player.uniqueId) + pluginState.database.getIDsWithUpdatesAsFlowFor(player.uniqueId) .toList() .run { if (size == 0) null else this } ?.run { @@ -57,15 +56,14 @@ class PlayerJoin : Listener { // View Open-Count and Assigned-Count Tickets launch { if (player.has("ticketmanager.notify.openTickets.onJoin")) { - val open = pluginState.await().database.getOpenTickets() - val assigned = pluginState.await().database.getOpenAssigned(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) + val open = pluginState.database.getBasicOpenAsFlow() + val assigned = pluginState.database.getBasicOpenAssignedAsFlow(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) val sentMSG = player.toTMLocale().notifyOpenAssigned .replace("%open%", "${open.count()}") .replace("%assigned%", "${assigned.count()}") player.sendMessage(text { formattedContent(sentMSG) }) - } } } diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt index 95ed5c3..e598e4c 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt @@ -1,17 +1,316 @@ package com.github.hoshikurama.ticketmanager.paper.events -import com.github.shynixn.mccoroutine.SuspendingTabCompleter -import org.bukkit.command.Command +import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent +import com.github.hoshikurama.ticketmanager.common.TMLocale +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.paper.has +import com.github.hoshikurama.ticketmanager.paper.mainPlugin +import com.github.hoshikurama.ticketmanager.paper.toTMLocale +import org.bukkit.Bukkit +import org.bukkit.World import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener -class TabComplete : SuspendingTabCompleter { +class TabComplete: Listener { + @EventHandler + fun onTabCompleteAsync(event: AsyncTabCompleteEvent) { + if (event.buffer.startsWith("/ticket ")) { + val args = event.buffer + .replace(" +".toRegex(), " ") + .split(" ") + .run { subList(1, this.size) } - override suspend fun onTabComplete( + event.completions = tabCompleteFunction(event.sender, args).toMutableList() + } + } + + private fun tabCompleteFunction( sender: CommandSender, - command: Command, - alias: String, - args: Array + args: List ): List { - TODO("Not yet implemented") + val blankList = listOf("") + + if (!sender.has("ticketmanager.commandArg.autotab")) return blankList + val locale = sender.toTMLocale() + val perms = LazyPermissions(locale, sender) + + return locale.run { + if (args.size <= 1) return@run perms.getPermissiveCommands() + .filter { it.startsWith(args[0]) } + + when (args[0]) { + commandWordAssign, commandWordSilentAssign -> when { // /ticket assign + !perms.hasAssignVariation -> listOf("") + args.size == 2 -> listOf("<$parameterID>") + .filter { it.startsWith(args[1]) } + args.size == 3 -> { + val groups = mainPlugin.perms.groups.map { "::$it" } + (listOf("<$parameterAssignment...>") + offlinePlayerNames() + groups + listOf(locale.consoleName)) + .filter { it.startsWith(args[2]) } + } + else -> listOf("") + } + + commandWordClaim, commandWordSilentClaim, commandWordUnassign, commandWordSilentUnassign -> when { // /ticket claim + !perms.hasAssignVariation -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordClose, commandWordSilentClose -> when { // /ticket close [Comment...] + !perms.hasClose -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> (listOf("[$parameterComment...]") + onlineSeenPlayers(sender)) + .filter { it.startsWith(args[args.lastIndex]) } + } + + commandWordCloseAll, commandWordSilentCloseAll -> when { // /ticket closeall + !perms.hasMassClose -> listOf("") + args.size == 2 -> listOf("<$parameterLowerID>").filter { it.startsWith(args[1]) } + args.size == 3 -> listOf("<$parameterUpperID>").filter { it.startsWith(args[2]) } + else -> listOf("") + } + + commandWordComment, commandWordSilentComment -> when { // /ticket comment + !perms.hasComment -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> (listOf("<$parameterComment...>") + onlineSeenPlayers(sender)) + .filter { it.startsWith(args[args.lastIndex]) } + } + + commandWordCreate -> when { // /ticket create + !perms.hasCreate -> listOf("") + else -> (listOf("<$parameterComment...>") + onlineSeenPlayers(sender)) + .filter { it.startsWith(args[args.lastIndex]) } + } + + commandWordHelp -> listOf("") + + commandWordHistory -> when { // /ticket history [User] [Page] + !perms.hasHistory -> listOf("") + args.size == 2 -> (listOf("[$parameterUser]", locale.consoleName) + offlinePlayerNames()) + .filter { it.startsWith(args[1]) } + args.size == 3 -> listOf("[$parameterPage]").filter { it.startsWith(args[2]) } + else -> listOf("") + } + + commandWordList, commandWordListAssigned -> when { // /ticket list(assigned) [Page] + !perms.hasListVariation -> listOf("") + args.size == 2 -> listOf("[$parameterPage]").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordReopen, commandWordSilentReopen -> when { // /ticket reopen + !perms.hasReopen -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordSetPriority, commandWordSilentSetPriority -> when { // /ticket setpriority + !perms.hasPriority -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + args.size == 3 -> listOf("<$parameterLevel>", "1", "2", "3", "4", "5").filter { + it.startsWith( + args[2] + ) + } + else -> listOf("") + } + + commandWordTeleport -> when { // /ticket teleport + !perms.hasTeleport -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordVersion -> listOf("") + + commandWordView -> when { // /ticket view + !perms.hasView -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordDeepView -> when { + !perms.hasDeepView -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordReload -> listOf("") + + commandWordSearch -> { //ticket search keywords:separated,by,commas status:OPEN/CLOSED time:5w creator:creator priority:value assignedto:player world:world + if (!perms.hasSearch) return@run listOf("") + + val curArgument = args[args.lastIndex] + val splitArgs = curArgument.split(":", limit = 2) + + if (splitArgs.size < 2) + return@run locale.run { + listOf( + "$searchAssigned:", + "$searchCreator:", + "$searchKeywords:", + "$searchPriority:", + "$searchStatus:", + "$searchWorld:", + "$searchClosedBy:", + "$searchLastClosedBy:", + ) + } + .filter { it.startsWith(curArgument) } + + // String now has form "constraint:" + return@run when (splitArgs[0]) { + searchAssigned -> { + val groups = mainPlugin.perms.groups.map { "::$it" } + (offlinePlayerNames() + groups) + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + searchCreator, searchLastClosedBy, searchClosedBy -> { + (offlinePlayerNames() + listOf(locale.consoleName)) + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + searchPriority -> { + listOf("1", "2", "3", "4", "5") + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + locale.searchStatus -> { + listOf(locale.statusOpen, locale.statusClosed) + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + searchWorld -> { + Bukkit.getWorlds() + .map(World::getName) + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + searchTime -> { + locale.run { + listOf( + searchTimeSecond, + searchTimeMinute, + searchTimeHour, + searchTimeDay, + searchTimeWeek, + searchTimeYear + ) + } + .filter { curArgument[curArgument.lastIndex].digitToIntOrNull() != null } + .map { "${splitArgs[0]}:$it" } + } + + searchKeywords -> listOf(curArgument) + + else -> listOf("") + } + } + + commandWordConvertDB -> when { // /ticket convertDatabase + !perms.hasConvertDB -> listOf("") + args.size == 2 -> + Database.Type.values() + .map(Database.Type::name) + .filter { it.startsWith(args[1]) } + else -> listOf("") + } + + else -> listOf("") + } + } + } + + private fun onlineSeenPlayers(sender: CommandSender): List { + return if (sender is Player) + Bukkit.getOnlinePlayers() + .filter(sender::canSee) + .map { it.name } + else Bukkit.getOnlinePlayers() + .map { it.name } + } + + private fun offlinePlayerNames() = Bukkit.getOfflinePlayers().mapNotNull { it.name }.toList() + + + class LazyPermissions(private val locale: TMLocale, private val sender: CommandSender) { + val hasAssignVariation by lazy { sender.has("ticketmanager.command.assign") } + val hasCreate by lazy { sender.has("ticketmanager.command.create") } + val hasListVariation by lazy { sender.has("ticketmanager.command.list") } + val hasReopen by lazy { sender.has("ticketmanager.command.reopen") } + val hasSearch by lazy { sender.has("ticketmanager.command.search") } + val hasPriority by lazy { sender.has("ticketmanager.command.setPriority") } + val hasTeleport by lazy { sender.has("ticketmanager.command.teleport") } + val hasMassClose by lazy { sender.has("ticketmanager.command.closeAll") } + val hasConvertDB by lazy { sender.has("ticketmanager.command.convertDatabase") } + private val hasHelp by lazy { sender.has("ticketmanager.command.help") } + private val hasReload by lazy { sender.has("ticketmanager.command.reload") } + private val hasSilent by lazy { sender.has("ticketmanager.commandArg.silence") } + val hasClose by lazy { + sender.has("ticketmanager.command.close.all") + || sender.has("ticketmanager.command.close.own") + } + val hasComment by lazy { + sender.has("ticketmanager.command.comment.all") + || sender.has("ticketmanager.command.comment.own") + } + val hasView by lazy { + sender.has("ticketmanager.command.view.all") + || sender.has("ticketmanager.command.view.own") + } + val hasDeepView by lazy { + sender.has("ticketmanager.command.viewdeep.all") + || sender.has("ticketmanager.command.viewdeep.own") + } + val hasHistory by lazy { + sender.has("ticketmanager.command.history.all") + || sender.has("ticketmanager.command.history.own") + } + + + fun getPermissiveCommands(): List { + return mapOf( + locale.commandWordAssign to hasAssignVariation, + locale.commandWordSilentAssign to (hasAssignVariation && hasSilent), + locale.commandWordClaim to hasAssignVariation, + locale.commandWordSilentClaim to (hasAssignVariation && hasSilent), + locale.commandWordClose to hasClose, + locale.commandWordSilentClose to (hasClose && hasSilent), + locale.commandWordCloseAll to hasMassClose, + locale.commandWordSilentCloseAll to (hasMassClose && hasSilent), + locale.commandWordComment to hasComment, + locale.commandWordSilentComment to (hasComment && hasSilent), + locale.commandWordConvertDB to hasConvertDB, + locale.commandWordCreate to hasCreate, + locale.commandWordHelp to hasHelp, + locale.commandWordHistory to hasHistory, + locale.commandWordList to hasListVariation, + locale.commandWordListAssigned to hasListVariation, + locale.commandWordReload to hasReload, + locale.commandWordReopen to hasReopen, + locale.commandWordSilentReopen to (hasReopen && hasSilent), + locale.commandWordSearch to hasSearch, + locale.commandWordSetPriority to hasPriority, + locale.commandWordSilentSetPriority to (hasPriority && hasSilent), + locale.commandWordTeleport to hasTeleport, + locale.commandWordUnassign to hasAssignVariation, + locale.commandWordSilentUnassign to (hasAssignVariation && hasSilent), + locale.commandWordVersion to true, + locale.commandWordView to hasView, + locale.commandWordDeepView to hasDeepView + ) + .filter { it.value } + .map { it.key } + } } } \ No newline at end of file diff --git a/Paper/src/main/resources/plugin.yml b/Paper/src/main/resources/plugin.yml index 4f4be50..b270071 100644 --- a/Paper/src/main/resources/plugin.yml +++ b/Paper/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: TicketManager -version: 4.1.0 +version: 5.0.0 main: com.github.hoshikurama.ticketmanager.paper.TicketManagerPlugin api-version: 1.17 authors: [HoshiKurama] diff --git a/build.gradle.kts b/build.gradle.kts index b59ad5d..a415e17 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -20,4 +20,8 @@ subprojects { tasks.withType { kotlinOptions.jvmTarget = "16" } + + tasks.withType().configureEach { + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + } } \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts index d015eee..511ee66 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { implementation("org.xerial:sqlite-jdbc:3.34.0") implementation("com.github.jasync-sql:jasync-mysql:1.1.6") implementation("com.github.seratch:kotliquery:1.3.1") - implementation("com.github.HoshiKurama:KyoriComponentDSL:1.0.0") + implementation("com.github.HoshiKurama:KyoriComponentDSL:1.1.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0") implementation("net.kyori:adventure-api:4.8.1") implementation("net.kyori:adventure-extra-kotlin:4.8.1") diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt index 8204c97..c5ea1d7 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt @@ -26,8 +26,8 @@ class LocaleHandler( if (forceLocale) mapOf() else mapOf( "en_ca" to fallback, - "en_us" to async { TMLocale(mainColourCode, "en_US") }, - "en_uk" to async { TMLocale(mainColourCode, "en_UK") } + "en_us" to async { TMLocale(mainColourCode, "en_CA") }, + "en_uk" to async { TMLocale(mainColourCode, "en_CA") } ) .mapValues { it.value.await() } @@ -240,7 +240,7 @@ class TMLocale( val helpSep: String init { - val inputStream = this::class.java.getResourceAsStream("locales/$locale.yml") + val inputStream = this::class.java.classLoader.getResourceAsStream("locales/$locale.yml") val contents: Map = Yaml().load(inputStream) fun matchOrDefault(key: String) = contents[key] ?.replace("%CC%", colourCode) diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt index 1db14a4..910ff9d 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt @@ -1,8 +1,10 @@ package com.github.hoshikurama.ticketmanager.common +import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import kotlinx.coroutines.withContext import java.io.InputStream import java.net.URL +import java.time.Instant import java.util.* import kotlin.coroutines.CoroutineContext @@ -30,4 +32,63 @@ class NonBlockingSync( ) { suspend fun check() = withContext(context) { t } suspend fun set(v: T) = withContext(context) { t = v } -} \ No newline at end of file +} + + +fun byteToPriority(byte: Byte) = when (byte.toInt()) { + 1 -> BasicTicket.Priority.LOWEST + 2 -> BasicTicket.Priority.LOW + 3 -> BasicTicket.Priority.NORMAL + 4 -> BasicTicket.Priority.HIGH + 5 -> BasicTicket.Priority.HIGHEST + else -> BasicTicket.Priority.NORMAL +} + +fun Long.toLargestRelativeTime(locale: TMLocale): String { + val timeAgo = Instant.now().epochSecond - this + + return when { + timeAgo >= 31556952L -> (timeAgo / 31556952L).toString() + locale.timeYears + timeAgo >= 604800L ->(timeAgo / 604800L).toString() + locale.timeWeeks + timeAgo >= 86400L ->(timeAgo / 86400L).toString() + locale.timeDays + timeAgo >= 3600L ->(timeAgo / 3600L).toString() + locale.timeHours + timeAgo >= 60L ->(timeAgo / 60L).toString() + locale.timeMinutes + else -> timeAgo.toString() + locale.timeSeconds + } +} + +fun relTimeToEpochSecond(relTime: String, locale: TMLocale): Long { + var seconds = 0L + var index = 0 + val unprocessed = StringBuilder(relTime) + + while (unprocessed.isNotEmpty() && index != unprocessed.lastIndex + 1) { + unprocessed[index].toString().toByteOrNull() + // If number... + ?.apply { index++ } + // If not a number... + ?: run { + val number = if (index == 0) 0 + else unprocessed.substring(0, index).toLong() + + seconds += number * when (unprocessed[index].toString()) { + locale.searchTimeSecond -> 1L + locale.searchTimeMinute -> 60L + locale.searchTimeHour -> 3600L + locale.searchTimeDay -> 86400L + locale.searchTimeWeek -> 604800L + locale.searchTimeYear -> 31556952L + else -> 0L + } + + unprocessed.delete(0, index+1) + index = 0 + } + } + + return Instant.now().epochSecond - seconds +} + +internal val sortForList: Comparator = Comparator.comparing(BasicTicket::priority).reversed().thenComparing(Comparator.comparing(BasicTicket::id).reversed()) + +fun T.notEquals(t: T) = this != t \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt index 644cfdd..ad5c862 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt @@ -16,16 +16,17 @@ class PluginState( ) { companion object { - suspend inline fun createDeferredPluginState( + suspend inline fun createPluginState( crossinline database: () -> Database?, crossinline cooldown: () -> Cooldown?, crossinline localeHandler: suspend () -> LocaleHandler?, crossinline allowUnreadTicketUpdates: () -> Boolean?, crossinline checkForPluginUpdate: () -> Boolean?, crossinline pluginVersion: () -> String, + absolutePathToPluginFolder: String, ) = coroutineScope { - val deferredDatabase = async { tryOrDefault(database, SQLite()) } + val deferredDatabase = async { tryOrDefault(database, SQLite(absolutePathToPluginFolder)) } val deferredCooldown = async { tryOrDefault(cooldown, Cooldown(false, 0)) } val deferredAllowUnreadUpdates = async { tryOrDefault(allowUnreadTicketUpdates, true) } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index a97f865..f548d29 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -1,7 +1,7 @@ package com.github.hoshikurama.ticketmanager.common.databases import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket -import com.github.hoshikurama.ticketmanager.common.ticket.Ticket +import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow import java.util.* @@ -13,35 +13,49 @@ interface Database { MySQL, SQLite } - // Individual things - suspend fun getAssignmentOrNull(ticketID: Int): Deferred - suspend fun getCreatorUUIDOrNull(ticketID: Int): Deferred - suspend fun getTicketLocationOrNull(ticketID: Int): Deferred - suspend fun getPriorityOrNull(ticketID: Int): Deferred - suspend fun getStatusOrNull(ticketID: Int): Deferred - suspend fun getCreatorStatusUpdateOrNull(ticketID: Int): Deferred - suspend fun setAssignment(ticketID: Int, assignment: String?) - suspend fun setPriority(ticketID: Int, priority: Ticket.Priority) - suspend fun setStatus(ticketID: Int, status: Ticket.Status) - suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) - suspend fun buildBasicTicket(id: Int): BasicTicket? - - // More specific Ticket actions - suspend fun addAction(ticketID: Int, action: Ticket.Action) - suspend fun addTicket(ticket: Ticket, action: Ticket.Action): Deferred - suspend fun getOpenTickets(): Flow - suspend fun getOpenAssigned(assignment: String, groupAssignment: List): Flow - suspend fun getTicketOrNull(ID: Int): Deferred - suspend fun getTicketIDsWithUpdates(): Flow> - suspend fun getTicketIDsWithUpdates(uuid: UUID): Flow - suspend fun isValidID(ticketID: Int): Deferred + // Individual property getters + suspend fun getActionsAsFlow(ticketID: Int): Flow> + + // Individual property setters + suspend fun setAssignmentAsync(ticketID: Int, assignment: String?) + suspend fun setCreatorStatusUpdateAsync(ticketID: Int, status: Boolean) + suspend fun setPriorityAsync(ticketID: Int, priority: BasicTicket.Priority) + suspend fun setStatusAsync(ticketID: Int, status: BasicTicket.Status) + + // Get Specific Ticket Type + suspend fun getBasicTicketAsync(ticketID: Int): Deferred + + // Database additions + suspend fun addAction(ticketID: Int, action: FullTicket.Action) + suspend fun addFullTicket(fullTicket: FullTicket) + suspend fun addNewTicketAsync(basicTicket: BasicTicket, message: String): Deferred + + // Database removals suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) - suspend fun searchDatabase(searchFunction: (Ticket) -> Boolean): Flow - // Database Modifications + // Collections of tickets + suspend fun getBasicOpenAsFlow(): Flow + suspend fun getBasicOpenAssignedAsFlow(assignment: String, groupAssignment: List): Flow + suspend fun getBasicsWithUpdatesAsFlow(): Flow + suspend fun getFullOpenAsFlow(): Flow + suspend fun getFullOpenAssignedAsFlow(assignment: String, groupAssignment: List): Flow + suspend fun getIDsWithUpdatesAsFlowFor(uuid: UUID): Flow + + // Database searching + suspend fun searchDatabase(searchFunction: (FullTicket) -> Boolean): Flow + + // Internal Database Functions suspend fun closeDatabase() - suspend fun createDatabasesIfNeeded() - suspend fun migrateDatabase(to: Type) - suspend fun updateNeeded(): Deferred - suspend fun updateDatabase() + suspend fun initialiseDatabase() + suspend fun updateNeededAsync(): Deferred + suspend fun migrateDatabase( + to: Type, + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit, + ) + suspend fun updateDatabase( + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit, + offlinePlayerNameToUuidOrNull: (String) -> UUID? + ) } \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt index 58bcd36..070b988 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt @@ -1,7 +1,7 @@ package com.github.hoshikurama.ticketmanager.common.databases import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket -import com.github.hoshikurama.ticketmanager.common.ticket.Ticket +import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow import java.util.* @@ -16,87 +16,77 @@ class MySQL( override val type: Database.Type get() = TODO("Not yet implemented") - override suspend fun getAssignmentOrNull(ticketID: Int): Deferred { + override suspend fun getActionsAsFlow(ticketID: Int): Flow> { TODO("Not yet implemented") } - override suspend fun getCreatorUUIDOrNull(ticketID: Int): Deferred { + override suspend fun setAssignmentAsync(ticketID: Int, assignment: String?) { TODO("Not yet implemented") } - override suspend fun getTicketLocationOrNull(ticketID: Int): Deferred { + override suspend fun setCreatorStatusUpdateAsync(ticketID: Int, status: Boolean) { TODO("Not yet implemented") } - override suspend fun getPriorityOrNull(ticketID: Int): Deferred { + override suspend fun setPriorityAsync(ticketID: Int, priority: BasicTicket.Priority) { TODO("Not yet implemented") } - override suspend fun getStatusOrNull(ticketID: Int): Deferred { + override suspend fun setStatusAsync(ticketID: Int, status: BasicTicket.Status) { TODO("Not yet implemented") } - override suspend fun getCreatorStatusUpdateOrNull(ticketID: Int): Deferred { + override suspend fun getBasicTicketAsync(ticketID: Int): Deferred { TODO("Not yet implemented") } - override suspend fun setAssignment(ticketID: Int, assignment: String?) { + override suspend fun addAction(ticketID: Int, action: FullTicket.Action) { TODO("Not yet implemented") } - override suspend fun setPriority(ticketID: Int, priority: Ticket.Priority) { + override suspend fun addFullTicket(fullTicket: FullTicket) { TODO("Not yet implemented") } - override suspend fun setStatus(ticketID: Int, status: Ticket.Status) { + override suspend fun addNewTicketAsync(basicTicket: BasicTicket, message: String): Deferred { TODO("Not yet implemented") } - override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { - TODO("Not yet implemented") - } - - override suspend fun buildBasicTicket(id: Int): BasicTicket? { - TODO("Not yet implemented") - } - - override suspend fun addAction(ticketID: Int, action: Ticket.Action) { - TODO("Not yet implemented") - } - - override suspend fun addTicket(ticket: Ticket, action: Ticket.Action): Deferred { - TODO("Not yet implemented") - } - - override suspend fun getOpenTickets(): Flow { + override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { TODO("Not yet implemented") } - override suspend fun getOpenAssigned(assignment: String, groupAssignment: List): Flow { + override suspend fun getBasicOpenAsFlow(): Flow { TODO("Not yet implemented") } - override suspend fun getTicketOrNull(ID: Int): Deferred { + override suspend fun getBasicOpenAssignedAsFlow( + assignment: String, + groupAssignment: List + ): Flow { TODO("Not yet implemented") } - override suspend fun getTicketIDsWithUpdates(): Flow> { + override suspend fun getBasicsWithUpdatesAsFlow(): Flow { TODO("Not yet implemented") } - override suspend fun getTicketIDsWithUpdates(uuid: UUID): Flow { + override suspend fun getFullOpenAsFlow(): Flow { TODO("Not yet implemented") } - override suspend fun isValidID(ticketID: Int): Deferred { + override suspend fun getFullOpenAssignedAsFlow( + assignment: String, + groupAssignment: List + ): Flow { TODO("Not yet implemented") } - override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { + override suspend fun getIDsWithUpdatesAsFlowFor(uuid: UUID): Flow { TODO("Not yet implemented") } - override suspend fun searchDatabase(searchFunction: (Ticket) -> Boolean): Flow { + override suspend fun searchDatabase(searchFunction: (FullTicket) -> Boolean): Flow { TODO("Not yet implemented") } @@ -104,20 +94,27 @@ class MySQL( TODO("Not yet implemented") } - override suspend fun createDatabasesIfNeeded() { + override suspend fun initialiseDatabase() { TODO("Not yet implemented") } - override suspend fun migrateDatabase(to: Database.Type) { + override suspend fun updateNeededAsync(): Deferred { TODO("Not yet implemented") } - override suspend fun updateNeeded(): Deferred { + override suspend fun migrateDatabase( + to: Database.Type, + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit + ) { TODO("Not yet implemented") } - override suspend fun updateDatabase() { + override suspend fun updateDatabase( + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit, + offlinePlayerNameToUuidOrNull: (String) -> UUID? + ) { TODO("Not yet implemented") } - } \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index fbd9b9b..c3a759f 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -1,117 +1,411 @@ package com.github.hoshikurama.ticketmanager.common.databases +import com.github.hoshikurama.ticketmanager.common.byteToPriority import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket -import com.github.hoshikurama.ticketmanager.common.ticket.Ticket +import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket +import com.github.hoshikurama.ticketmanager.common.ticket.toTicketLocation import kotlinx.coroutines.Deferred -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotliquery.* +import java.sql.DriverManager +import java.time.Instant import java.util.* -class SQLite : Database { - override val type: Database.Type - get() = TODO("Not yet implemented") - override suspend fun getAssignmentOrNull(ticketID: Int): Deferred { - TODO("Not yet implemented") +class SQLite(absoluteDataFolderPath: String) : Database { + override val type = Database.Type.SQLite + private val url: String = "jdbc:sqlite:$absoluteDataFolderPath/TicketManager-SQLite.db" + + + private fun getSession() = Session(Connection(DriverManager.getConnection(url))) + + override suspend fun getActionsAsFlow(ticketID: Int): Flow> { + return using(getSession()) { getActions(ticketID, it) } + .map { ticketID to it } + .asFlow() } - override suspend fun getCreatorUUIDOrNull(ticketID: Int): Deferred { - TODO("Not yet implemented") + override suspend fun setAssignmentAsync(ticketID: Int, assignment: String?) { + using(getSession()) { + it.run(queryOf("UPDATE TicketManager_V4_Tickets SET ASSIGNED_TO = ? WHERE ID = $ticketID;", assignment).asUpdate) + } } - override suspend fun getTicketLocationOrNull(ticketID: Int): Deferred { - TODO("Not yet implemented") + override suspend fun setCreatorStatusUpdateAsync(ticketID: Int, status: Boolean) { + using(getSession()) { + it.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS_UPDATE_FOR_CREATOR = ? WHERE ID = $ticketID;", status).asUpdate) + } } - override suspend fun getPriorityOrNull(ticketID: Int): Deferred { - TODO("Not yet implemented") + override suspend fun setPriorityAsync(ticketID: Int, priority: BasicTicket.Priority) { + using(getSession()) { + it.run(queryOf("UPDATE TicketManager_V4_Tickets SET PRIORITY = ? WHERE ID = $ticketID;", priority.level).asUpdate) + } } - override suspend fun getStatusOrNull(ticketID: Int): Deferred { - TODO("Not yet implemented") + override suspend fun setStatusAsync(ticketID: Int, status: BasicTicket.Status) { + using(getSession()) { + it.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID = $ticketID;", status.name).asUpdate) + } } - override suspend fun getCreatorStatusUpdateOrNull(ticketID: Int): Deferred { - TODO("Not yet implemented") + override suspend fun getBasicTicketAsync(ticketID: Int): Deferred = coroutineScope { + async { + using(getSession()) { getBasicTicket(ticketID, it) } + } } - override suspend fun setAssignment(ticketID: Int, assignment: String?) { - TODO("Not yet implemented") + override suspend fun addAction(ticketID: Int, action: FullTicket.Action) { + using(getSession()) { + writeAction(action, ticketID, it) + } } - override suspend fun setPriority(ticketID: Int, priority: Ticket.Priority) { - TODO("Not yet implemented") + override suspend fun addFullTicket(fullTicket: FullTicket) { + using(getSession()) { session -> + writeTicket(fullTicket, session) + + fullTicket.actions.forEach { + writeAction(it, fullTicket.id, session) + } + } } - override suspend fun setStatus(ticketID: Int, status: Ticket.Status) { - TODO("Not yet implemented") + override suspend fun addNewTicketAsync(basicTicket: BasicTicket, message: String): Deferred = coroutineScope { + async { + using(getSession()) { session -> + val id = writeTicket(basicTicket, session)!!.toInt() + writeAction(FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message), id, session) + id + } + } } - override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { - TODO("Not yet implemented") + override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { + using(getSession()) { session -> + + val idStatusPairs = session.run(queryOf("SELECT ID, STATUS FROM TicketManager_V4_Tickets WHERE ID BETWEEN $lowerBound AND $upperBound;") + .map { it.int(1) to BasicTicket.Status.valueOf(it.string(2)) } + .asList + ) + + idStatusPairs.asSequence() + .filter { it.second == BasicTicket.Status.OPEN } + .forEach { + writeAction( + FullTicket.Action( + type = FullTicket.Action.Type.MASS_CLOSE, + user = uuid, + message = null, + timestamp = Instant.now().epochSecond + ), + ticketID = it.first, + session = session + ) + } + + val idString = idStatusPairs.map { it.first }.joinToString(", ") + session.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID IN ($idString);", BasicTicket.Status.CLOSED.name).asUpdate) + } } - override suspend fun buildBasicTicket(id: Int): BasicTicket? { - TODO("Not yet implemented") + override suspend fun getBasicOpenAsFlow(): Flow { + return using(getSession()) { session -> + session.run( + queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE STATUS = ?;", BasicTicket.Status.OPEN.toString()) + .map { it.toBasicTicket() } + .asList + ) + }.asFlow() } - override suspend fun addAction(ticketID: Int, action: Ticket.Action) { - TODO("Not yet implemented") + override suspend fun getBasicOpenAssignedAsFlow( + assignment: String, + groupAssignment: List + ): Flow { + return getBasicOpenAsFlow().filter { it.assignedTo == assignment || it.assignedTo in groupAssignment } } - override suspend fun addTicket(ticket: Ticket, action: Ticket.Action): Deferred { - TODO("Not yet implemented") + override suspend fun getBasicsWithUpdatesAsFlow(): Flow { + return using(getSession()) { session -> + session.run( + queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ?;", true) + .map { it.toBasicTicket() } + .asList + ) + }.asFlow() } - override suspend fun getOpenTickets(): Flow { - TODO("Not yet implemented") + override suspend fun getFullOpenAsFlow(): Flow { + val basicTickets = getBasicOpenAsFlow().toList() + + return using(getSession()) { session -> + basicTickets.map { it.toFullTicket(session) }.asFlow() + } } - override suspend fun getOpenAssigned(assignment: String, groupAssignment: List): Flow { - TODO("Not yet implemented") + override suspend fun getFullOpenAssignedAsFlow(assignment: String, groupAssignment: List): Flow { + return getFullOpenAsFlow().filter { it.assignedTo == assignment || it.assignedTo in groupAssignment } } - override suspend fun getTicketOrNull(ID: Int): Deferred { - TODO("Not yet implemented") + override suspend fun getIDsWithUpdatesAsFlowFor(uuid: UUID): Flow { + return using(getSession()) { session -> + session.run( + queryOf("SELECT ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ? AND CREATOR_UUID = ?;", true, uuid) + .map { it.int(1) } + .asList + ) + }.asFlow() } - override suspend fun getTicketIDsWithUpdates(): Flow> { - TODO("Not yet implemented") + override suspend fun searchDatabase(searchFunction: (FullTicket) -> Boolean): Flow { + val matchedTickets = mutableListOf() + + using(getSession()) { session -> + session.forEach(queryOf("SELECT * FROM TicketManager_V4_Tickets")) { row -> + row.toBasicTicket().toFullTicket(session).takeIf(searchFunction)?.apply(matchedTickets::add) + } + } + + return matchedTickets.asFlow() } - override suspend fun getTicketIDsWithUpdates(uuid: UUID): Flow { - TODO("Not yet implemented") + override suspend fun closeDatabase() { + // NOT needed as database makes individual connections } - override suspend fun isValidID(ticketID: Int): Deferred { - TODO("Not yet implemented") + override suspend fun initialiseDatabase() { + //Creates table if doesn't exist + using(getSession()) { + if (!tableExists("TicketManager_V4_Tickets", it)) { + it.run( + queryOf(""" + CREATE TABLE TicketManager_V4_Tickets ( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + CREATOR_UUID VARCHAR(36) COLLATE NOCASE, + PRIORITY TINYINT NOT NULL, + STATUS VARCHAR(10) COLLATE NOCASE NOT NULL, + ASSIGNED_TO VARCHAR(255) COLLATE NOCASE, + STATUS_UPDATE_FOR_CREATOR BOOLEAN NOT NULL, + LOCATION VARCHAR(255) COLLATE NOCASE + );""".trimIndent() + ).asExecute + ) + it.run(queryOf("CREATE INDEX STATUS_V4 ON TicketManager_V4_Tickets (STATUS)").asExecute) + it.run(queryOf("CREATE INDEX STATUS_UPDATE_FOR_CREATOR_V4 ON TicketManager_V4_Tickets (STATUS_UPDATE_FOR_CREATOR)").asExecute) + } + + if (!tableExists("TicketManager_V4_Actions", it)) { + it.run( + queryOf(""" + CREATE TABLE TicketManager_V4_Actions ( + ACTION_ID INTEGER PRIMARY KEY AUTOINCREMENT, + TICKET_ID INTEGER NOT NULL, + ACTION_TYPE VARCHAR(20) COLLATE NOCASE NOT NULL, + CREATOR_UUID VARCHAR(36) COLLATE NOCASE, + MESSAGE TEXT COLLATE NOCASE, + TIMESTAMP BIGINT NOT NULL + );""".trimIndent() + ).asExecute + ) + } + } } - override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { - TODO("Not yet implemented") + override suspend fun updateNeededAsync(): Deferred { + return coroutineScope { + async { + using(getSession()) { + tableExists("TicketManagerTicketsV2", it) + } + } + } } - override suspend fun searchDatabase(searchFunction: (Ticket) -> Boolean): Flow { - TODO("Not yet implemented") + override suspend fun migrateDatabase( + to: Database.Type, + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit + ) { + //TODO use more functions like () -> Database + /* + @Suppress("BlockingMethodInNonBlockingContext") + runBlocking { + when (to) { + Database.Type.SQLite -> {} // SQLite -> SQLite is not permitted + + Database.Type.MySQL -> { + pushMassNotify("ticketmanager.notify.info", { + it.informationDBConvertInit + .replace("%fromDB%", Database.Types.SQLite.name) + .replace("%toDB%", Database.Types.MySQL.name) + } ) + + try { + val config = mainPlugin.config + val mySQL = MySQL( + config.getString("MySQL_Host")!!, + config.getString("MySQL_Port")!!, + config.getString("MySQL_DBName")!!, + config.getString("MySQL_Username")!!, + config.getString("MySQL_Password")!! + ) + + // Writes to MySQL + using(getSession()) { session -> + session.forEach(queryOf("SELECT * FROM TicketManager_V4_Tickets")) { row -> //NOTE: During conversion, ticket ID is not guaranteed to be preserved + row.toTicket(session).apply { + val newID = mySQL.addTicket(this, actions[0]) + if (actions.size > 1) actions.subList(1, actions.size) + .forEach { mySQL.addAction(newID, it) } + } + } + } + + pushMassNotify("ticketmanager.notify.info", { it.informationDBConvertSuccess } ) + + } catch (e: Exception) { + e.printStackTrace() + postModifiedStacktrace(e) + } + } + } + } + + */ } - override suspend fun closeDatabase() { - TODO("Not yet implemented") + override suspend fun updateDatabase( + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit, + offlinePlayerNameToUuidOrNull: (String) -> UUID? + ) { + coroutineScope { + onBegin() + + using(getSession()) { session -> + session.forEach(queryOf("SELECT * FROM TicketManagerTicketsV2;")) { row -> + val ticket = FullTicket( + id = row.int(1), + priority = byteToPriority(row.byte(3)), + creatorStatusUpdate = row.boolean(10), + status = BasicTicket.Status.valueOf(row.string(2)), + assignedTo = row.stringOrNull(6), + + creatorUUID = row.string(5).let { + if (it.lowercase() == "console") null + else UUID.fromString(it) + }, + location = row.string(7).run { + if (equals("NoLocation")) null + else toTicketLocation() + }, + actions = row.string(9) + .split("/MySQLNewLine/") + .filter { it.isNotBlank() } + .map { it.split("/MySQLSep/") } + .mapIndexed { index, action -> + FullTicket.Action( + message = action[1], + type = if (index == 0) FullTicket.Action.Type.OPEN else FullTicket.Action.Type.COMMENT, + timestamp = row.long(8), + user = + if (action[0].lowercase() == "console") null + else offlinePlayerNameToUuidOrNull(action[0]) + ) + } + ) + val id = writeTicket(ticket, session) + ticket.actions.forEach { writeAction(it, id!!.toInt(), session) } + } + + session.run(queryOf("DROP INDEX STATUS;").asExecute) + session.run(queryOf("DROP INDEX UPDATEDBYOTHERUSER;").asExecute) + session.run(queryOf("DROP TABLE TicketManagerTicketsV2;").asUpdate) + + } + + onComplete() + } + } - override suspend fun createDatabasesIfNeeded() { - TODO("Not yet implemented") + + private fun getBasicTicket(ticketID: Int, session: Session): BasicTicket? { + return session.run( + queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") + .map { it.toBasicTicket() } + .asSingle + ) + } + + private fun writeTicket(ticket: BasicTicket, session: Session): Long? { + return session.run(queryOf("INSERT INTO TicketManager_V4_Tickets (CREATOR_UUID, PRIORITY, STATUS, ASSIGNED_TO, STATUS_UPDATE_FOR_CREATOR, LOCATION) VALUES (?,?,?,?,?,?);", + ticket.creatorUUID, + ticket.priority.level, + ticket.status.name, + ticket.assignedTo, + ticket.creatorStatusUpdate, + ticket.location?.toString() + ).asUpdateAndReturnGeneratedKey) + } + + private fun writeAction(action: FullTicket.Action, ticketID: Int, session: Session) { + session.run(queryOf("INSERT INTO TicketManager_V4_Actions (TICKET_ID,ACTION_TYPE,CREATOR_UUID,MESSAGE,TIMESTAMP) VALUES (?,?,?,?,?);", + ticketID, + action.type.name, + action.user?.toString(), + action.message, + action.timestamp + ).asExecute) + } + + private fun Row.toBasicTicket(): BasicTicket { + return BasicTicket( + id = int(1), + creatorUUID = stringOrNull(2)?.let(UUID::fromString), + priority = byteToPriority(byte(3)), + status = BasicTicket.Status.valueOf(string(4)), + assignedTo = stringOrNull(5), + creatorStatusUpdate = boolean(6), + location = stringOrNull(7)?.split(" ")?.let { + BasicTicket.TicketLocation( + world = it[0], + x = it[1].toInt(), + y = it[2].toInt(), + z = it[3].toInt() + ) + } + ) } - override suspend fun migrateDatabase(to: Database.Type) { - TODO("Not yet implemented") + private fun Row.toAction(): FullTicket.Action { + return FullTicket.Action( + type = FullTicket.Action.Type.valueOf(string(2)), + user = stringOrNull(3)?.let { UUID.fromString(it) }, + message = stringOrNull(4), + timestamp = long(5) + ) } - override suspend fun updateNeeded(): Deferred { - TODO("Not yet implemented") + private fun getActions(ticketID: Int, session: Session): List { + return session.run(queryOf("SELECT ACTION_ID, ACTION_TYPE, CREATOR_UUID, MESSAGE, TIMESTAMP FROM TicketManager_V4_Actions WHERE TICKET_ID = $ticketID;") + .map { row -> row.toAction() } + .asList + ) } - override suspend fun updateDatabase() { - TODO("Not yet implemented") + private fun Row.toTicketIDActionPair() = int(1) to toAction() + + private fun tableExists(table: String, session: Session): Boolean { + return using(session.connection.underlying.metaData.getTables(null, null, table, null)) { + while (it.next()) + if (it.getString("TABLE_NAME")?.equals(table) == true) return@using true + return@using false + } } + private fun BasicTicket.toFullTicket(session: Session) = FullTicket(this, getActions(id, session)) } \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt index ebed6d1..9b95e77 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt @@ -1,37 +1,66 @@ package com.github.hoshikurama.ticketmanager.common.ticket import com.github.hoshikurama.ticketmanager.common.PluginState +import com.github.hoshikurama.ticketmanager.common.TMLocale +import com.github.hoshikurama.ticketmanager.common.databases.Database import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.toList import java.util.* -class BasicTicket( - val id: Int, - val creatorUUID: UUID?, - val location: Ticket.TicketLocation?, - val priority: Ticket.Priority, - val status: Ticket.Status, - val assignedTo: String?, - val statusUpdateForCreator: Boolean, - - val pluginState: PluginState, +open class BasicTicket( + val id: Int = -1, // Ticket ID 1+... -1 placeholder during ticket creation + val creatorUUID: UUID?, // UUID if player, null if Console + val location: TicketLocation?, // TicketLocation if player, null if Console + val priority: Priority = Priority.NORMAL, // Priority 1-5 or Lowest to Highest + val status: Status = Status.OPEN, // Status OPEN or CLOSED + val assignedTo: String? = null, // Null if not assigned to anybody + val creatorStatusUpdate: Boolean = false, // Determines whether player should be notified ) { - companion object { - suspend fun buildOrNull(pluginState: PluginState, id: Int) = - pluginState.database.buildBasicTicket(id) + enum class Priority(val level: Byte, val colourCode: String) { + LOWEST(1, "&1"), + LOW(2, "&9"), + NORMAL(3, "&e"), + HIGH(4, "&c"), + HIGHEST(5, "&4") + } + + enum class Status(val colourCode: String) { + OPEN("&a"), CLOSED("&c") } - suspend fun setCreatorStatusUpdate(value: Boolean) = - pluginState.database.setCreatorStatusUpdate(id, value) + data class TicketLocation(val world: String, val x: Int, val y: Int, val z: Int) { + override fun toString() = "$world $x $y $z" + } + + suspend fun toFullTicketAsync(database: Database): Deferred = coroutineScope { + async { + val sortedActions = database.getActionsAsFlow(id) + .toList() + .sortedBy { it.first } + .map { it.second } + + FullTicket(this@BasicTicket, sortedActions) + } + } +} - suspend fun setTicketPriority(value: Ticket.Priority) = - pluginState.database.setPriority(id, value) +fun BasicTicket.Priority.toLocaledWord(locale: TMLocale) = when (this) { + BasicTicket.Priority.LOWEST -> locale.priorityLowest + BasicTicket.Priority.LOW -> locale.priorityLow + BasicTicket.Priority.NORMAL -> locale.priorityNormal + BasicTicket.Priority.HIGH -> locale.priorityHigh + BasicTicket.Priority.HIGHEST -> locale.priorityHighest +} - suspend fun setTicketStatus(value: Ticket.Status) = - pluginState.database.setStatus(id, value) +fun BasicTicket.Status.toLocaledWord(locale: TMLocale) = when (this) { + BasicTicket.Status.OPEN -> locale.statusOpen + BasicTicket.Status.CLOSED -> locale.statusClosed +} - suspend fun setAssignedTo(value: String?) = - pluginState.database.setAssignment(id, value) +fun BasicTicket.uuidMatches(other: UUID?) = + creatorUUID?.equals(other) ?: (other == null) - suspend fun uuidMatches(other: UUID?) = - creatorUUID?.equals(other) ?: (other == null) -} \ No newline at end of file +fun String.toTicketLocation() = split(" ") + .let { BasicTicket.TicketLocation(it[0], it[1].toInt(), it[2].toInt(), it[3].toInt()) } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt new file mode 100644 index 0000000..33fc3e6 --- /dev/null +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt @@ -0,0 +1,55 @@ +package com.github.hoshikurama.ticketmanager.common.ticket + +import com.github.hoshikurama.ticketmanager.common.PluginState +import com.github.hoshikurama.ticketmanager.common.databases.Database +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import java.util.* + +class BasicTicketHandler( + id: Int, + creatorUUID: UUID?, + location: TicketLocation?, + priority: Priority, + status: Status, + assignedTo: String?, + creatorStatusUpdate: Boolean, + val database: Database, + +) : BasicTicket(id, creatorUUID, location, priority, status, assignedTo, creatorStatusUpdate) { + + constructor(database: Database, basicTicket: BasicTicket): this( + basicTicket.id, + basicTicket.creatorUUID, + basicTicket.location, + basicTicket.priority, + basicTicket.status, + basicTicket.assignedTo, + basicTicket.creatorStatusUpdate, + database + ) + + companion object { + suspend fun buildHandlerAsync(database: Database, id: Int) = coroutineScope { + async { + val basicTicket = database.getBasicTicketAsync(id) + basicTicket.await()?.run { BasicTicketHandler(database, this) } + } + } + } + + suspend fun setCreatorStatusUpdate(value: Boolean) = + database.setCreatorStatusUpdateAsync(id, value) + + suspend fun setTicketPriority(value: Priority) = + database.setPriorityAsync(id, value) + + suspend fun setTicketStatus(value: Status) = + database.setStatusAsync(id, value) + + suspend fun setAssignedTo(value: String?) = + database.setAssignmentAsync(id, value) + + suspend fun toFullTicketAsync() = super.toFullTicketAsync(database) +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt new file mode 100644 index 0000000..6f55fbe --- /dev/null +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt @@ -0,0 +1,33 @@ +package com.github.hoshikurama.ticketmanager.common.ticket + +import java.time.Instant +import java.util.* + +class FullTicket( + id: Int = -1, // Ticket ID 1+... -1 placeholder during ticket creation + creatorUUID: UUID?, // UUID if player, null if Console + location: TicketLocation?, // TicketLocation if player, null if Console + val actions: List = listOf(), // List of actions + priority: Priority = Priority.NORMAL, // Priority 1-5 or Lowest to Highest + status: Status = Status.OPEN, // Status OPEN or CLOSED + assignedTo: String? = null, // Null if not assigned to anybody + creatorStatusUpdate: Boolean = false, // Determines whether player should be notified +) : BasicTicket(id, creatorUUID, location, priority, status, assignedTo, creatorStatusUpdate) { + + constructor(basicTicket: BasicTicket, actions: List): this( + basicTicket.id, + basicTicket.creatorUUID, + basicTicket.location, + actions, + basicTicket.priority, + basicTicket.status, + basicTicket.assignedTo, + basicTicket.creatorStatusUpdate + ) + + data class Action(val type: Type, val user: UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { + enum class Type { + ASSIGN, CLOSE, COMMENT, OPEN, REOPEN, SET_PRIORITY, MASS_CLOSE + } + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/Ticket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/Ticket.kt deleted file mode 100644 index 027cb14..0000000 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/Ticket.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.github.hoshikurama.ticketmanager.common.ticket - -import com.github.hoshikurama.ticketmanager.common.TMLocale -import java.time.Instant -import java.util.* - -class Ticket( - val creatorUUID: UUID?, // UUID if player, null if Console - val location: TicketLocation?, // TicketLocation if player, null if Console - val actions: List = listOf(), // List of actions - val priority: Priority = Priority.NORMAL, // Priority 1-5 or Lowest to Highest - val status: Status = Status.OPEN, // Status OPEN or CLOSED - val assignedTo: String? = null, // Null if not assigned to anybody - val statusUpdateForCreator: Boolean = false, // Determines whether player should be notified - val id: Int = -1, // Ticket ID 1+... -1 placeholder during ticket creation -) { - - enum class Priority(val level: Byte, val colourCode: String) { - LOWEST(1, "&1"), - LOW(2, "&9"), - NORMAL(3, "&e"), - HIGH(4, "&c"), - HIGHEST(5, "&4") - } - enum class Status(val colourCode: String) { - OPEN("&a"), CLOSED("&c") - } - - data class Action(val type: Type, val user: UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { - enum class Type { - ASSIGN, CLOSE, COMMENT, OPEN, REOPEN, SET_PRIORITY, MASS_CLOSE - } - } - data class TicketLocation(val world: String, val x: Int, val y: Int, val z: Int) { - override fun toString() = "$world $x $y $z" - } -} - -fun Ticket.Priority.toLocaledWord(locale: TMLocale)= when (this) { - Ticket.Priority.LOWEST -> locale.priorityLowest - Ticket.Priority.LOW -> locale.priorityLow - Ticket.Priority.NORMAL -> locale.priorityNormal - Ticket.Priority.HIGH -> locale.priorityHigh - Ticket.Priority.HIGHEST -> locale.priorityHighest -} - -fun Ticket.Status.toLocaledWord(locale: TMLocale) = when (this) { - Ticket.Status.OPEN -> locale.statusOpen - Ticket.Status.CLOSED -> locale.statusClosed -} \ No newline at end of file diff --git a/common/src/main/resources/locales/en_CA.yml b/common/src/main/resources/locales/en_CA.yml index cfc7aee..8eb6b22 100644 --- a/common/src/main/resources/locales/en_CA.yml +++ b/common/src/main/resources/locales/en_CA.yml @@ -167,18 +167,18 @@ Parameters_Constraints: 'Constraints' Notify_UnreadUpdate_Single: '%CC%[TicketManager] Ticket &7%num%%CC% has an update! Type &7/ticket view %CC% to clear notification.' Notify_UnreadUpdate_Multi: '%CC%[TicketManager] Tickets &7%num%%CC% have updates! Type &7/ticket view %CC% for all updated tickets to clear notifications.' Notify_OpenAssigned: '%CC%[TicketManager]&7 %open% %CC%tickets open (&7%assigned%%CC% assigned to you)' -Notify_MassCloseSuccess: '%CC%[TicketManager] Mass close from &7%low%%CC% to &7%high%%CC% successful!' -Notify_TicketAssignSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% successfully assigned to:&7 %assign%%CC%!' -Notify_TicketCloseSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% successfully closed!' -Notify_TicketCloseWithCommentSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% successfully closed with a comment!' -Notify_TicketCreationSuccessful: '%CC%[TicketManager] Ticket &7#%id%%CC% successfully created!' -Notify_TicketCommentSuccessful: '%CC%[TicketManager] Ticket &7#%id%%CC% successfully commented on!' -Notify_TicketReopenSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% successfully re-opened!' -Notify_TicketSetPrioritySuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% successfully' +Notify_MassCloseSuccess: '%CC%[TicketManager] Mass close from &7%low%%CC% to &7%high%%CC% sent!' +Notify_TicketAssignSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% assigned to:&7 %assign%%CC%!' +Notify_TicketCloseSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% closed!' +Notify_TicketCloseWithCommentSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% closed with a comment!' +Notify_TicketCreationSuccessful: '%CC%[TicketManager] Ticket &7#%id%%CC% created!' +Notify_TicketCommentSuccessful: '%CC%[TicketManager] Ticket &7#%id%%CC% commented on!' +Notify_TicketReopenSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% re-opened!' +Notify_TicketSetPrioritySuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% priority changed!' Notify_Event_TicketAssign: '%CC%[TicketManager]&7 %user%%CC% assigned ticket &7#%id%%CC% to &7%assign%%CC%.' Notify_Event_TicketClose: '%CC%[TicketManager] &7%user%%CC% closed ticket &7#%id%%CC%.' Notify_Event_TicketCloseWithComment: '%CC%[TicketManager] &7%user%%CC% closed ticket &7#%id%%CC% with a comment:%nl%&7%message%' -Notify_Event_MassClose: '%CC%[TicketManager]&7 %user%%CC% mass-closed tickets from &7#%low%%CC% to &7#%high%%CC%.' +Notify_Event_MassClose: '%CC%[TicketManager]&7 %user%%CC% is mass-closing tickets from &7#%low%%CC% to &7#%high%%CC%.' Notify_Event_TicketComment: '%CC%[TicketManager]&7 %user%%CC% commented on ticket &7#%id%%CC%:&7%nl%%message%' Notify_Event_TicketCreation: '%CC%[TicketManager] &7%user%%CC% created ticket &7#%id%%CC%:&7%nl%%message%' Notify_Event_TicketModification: '%CC%[TicketManager] Ticket &7#%id%%CC% has been updated! Type &7/ticket view %id%%CC% to view this ticket.' From 720529e74ee379af05f649af41b8636dd3fa9504 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Thu, 8 Jul 2021 18:26:33 -0500 Subject: [PATCH 09/31] Bug fixes --- .../paper/TicketManagerPlugin.kt | 6 ++++ .../ticketmanager/paper/events/Commands.kt | 35 +++++++++---------- .../ticketmanager/paper/events/TabComplete.kt | 2 +- .../ticketmanager/common/Miscellaneous.kt | 2 +- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index a03a323..290636d 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -32,6 +32,12 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { companion object { lateinit var plugin: TicketManagerPlugin } init { plugin = this } + override suspend fun onDisableAsync() { + pluginLocked.set(true) + asyncDispatcher.cancelChildren() + pluginState.database.closeDatabase() + + } override fun onEnable() { diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index 08d1887..c41d352 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -626,7 +626,6 @@ class Commands : SuspendingCommandExecutor { /* val type = args[1].run(Database.Type::valueOf) pluginState.database.migrateDatabase(type) - */ } @@ -801,20 +800,20 @@ class Commands : SuspendingCommandExecutor { baseCommand: (TMLocale) -> String, ): Component { - fun Component.addForward() { - color(NamedTextColor.WHITE) - clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage + 1}")) - hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickNextPage))) + fun Component.addForward(): Component { + return color(NamedTextColor.WHITE) + .clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage + 1}")) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickNextPage))) } - fun Component.addBack() { - color(NamedTextColor.WHITE) - clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage - 1}")) - hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickBackPage))) + fun Component.addBack(): Component { + return color(NamedTextColor.WHITE) + .clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage - 1}")) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickBackPage))) } - val back = Component.text("[${locale.pageBack}]") - val next = Component.text("[${locale.pageNext}]") + var back: Component = Component.text("[${locale.pageBack}]") + var next: Component = Component.text("[${locale.pageNext}]") val separator = text { content("...............") color(NamedTextColor.DARK_GRAY) @@ -824,16 +823,16 @@ class Commands : SuspendingCommandExecutor { when (curPage) { 1 -> { - back.color(NamedTextColor.DARK_GRAY) - next.addForward() + back = back.color(NamedTextColor.DARK_GRAY) + next = next.addForward() } pageCount -> { - back.addBack() - next.color(NamedTextColor.DARK_GRAY) + back = back.addBack() + next = next.color(NamedTextColor.DARK_GRAY) } else -> { - back.addBack() - back.addForward() + back = back.addBack() + next = next.addForward() } } @@ -886,7 +885,7 @@ class Commands : SuspendingCommandExecutor { getTickets: suspend (Database) -> List, baseCommand: (TMLocale) -> String ): Component { - val chunkedTickets = getTickets(pluginState.database).chunked(8) + val chunkedTickets = getTickets(pluginState.database).sortedWith(sortForList).chunked(8) val page = if (args.size == 2 && args[1].toInt() in 1..chunkedTickets.size) args[1].toInt() else 1 return buildComponent { diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt index e598e4c..6623f93 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt @@ -32,7 +32,7 @@ class TabComplete: Listener { ): List { val blankList = listOf("") - if (!sender.has("ticketmanager.commandArg.autotab")) return blankList + if (!sender.has("ticketmanager.commandArg.autotab") && sender is Player) return blankList val locale = sender.toTMLocale() val perms = LazyPermissions(locale, sender) diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt index 910ff9d..a84b529 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt @@ -89,6 +89,6 @@ fun relTimeToEpochSecond(relTime: String, locale: TMLocale): Long { return Instant.now().epochSecond - seconds } -internal val sortForList: Comparator = Comparator.comparing(BasicTicket::priority).reversed().thenComparing(Comparator.comparing(BasicTicket::id).reversed()) +val sortForList: Comparator = Comparator.comparing(BasicTicket::priority).reversed().thenComparing(Comparator.comparing(BasicTicket::id).reversed()) fun T.notEquals(t: T) = this != t \ No newline at end of file From 73f3fb6e5643c63f58a6ca88091cde42aa8101f4 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Sat, 10 Jul 2021 15:28:21 -0500 Subject: [PATCH 10/31] MySQL added Performance and Stability Fixes --- Paper/build.gradle.kts | 15 +- .../ticketmanager/paper/Globals.kt | 5 + .../paper/TicketManagerPlugin.kt | 108 +++-- .../ticketmanager/paper/events/Commands.kt | 95 +++-- .../ticketmanager/paper/events/PlayerJoin.kt | 17 +- common/build.gradle.kts | 2 +- .../ticketmanager/common/Localization.kt | 6 +- .../ticketmanager/common/Miscellaneous.kt | 3 + .../ticketmanager/common/PluginState.kt | 19 +- .../common/databases/Database.kt | 37 +- .../ticketmanager/common/databases/MySQL.kt | 375 +++++++++++++++--- .../ticketmanager/common/databases/SQLite.kt | 126 +++--- .../common/ticket/BasicTicket.kt | 10 +- .../common/ticket/BasicTicketHandler.kt | 21 +- 14 files changed, 584 insertions(+), 255 deletions(-) diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts index 1f37ecb..10d9792 100644 --- a/Paper/build.gradle.kts +++ b/Paper/build.gradle.kts @@ -18,21 +18,22 @@ repositories { dependencies { compileOnly("io.papermc.paper:paper-api:1.17.1-R0.1-SNAPSHOT") implementation(kotlin("stdlib", version = "1.5.20")) - implementation("mysql:mysql-connector-java:8.0.25") - implementation("org.xerial:sqlite-jdbc:3.34.0") - implementation("com.github.jasync-sql:jasync-mysql:1.1.6") - implementation("com.github.seratch:kotliquery:1.3.1") implementation("com.github.HoshiKurama:KyoriComponentDSL:1.1.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0") - implementation("net.kyori:adventure-api:4.8.1") implementation("net.kyori:adventure-extra-kotlin:4.8.1") - implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") - implementation("org.yaml:snakeyaml:1.29") implementation("joda-time:joda-time:2.10.10") compileOnly("com.github.MilkBowl:VaultAPI:1.7") implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0") implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0") implementation(project(":common")) + // Used by :common but still needed in plugin.yml + //implementation("mysql:mysql-connector-java:8.0.25") + //implementation("org.xerial:sqlite-jdbc:3.34.0") + //implementation("com.github.jasync-sql:jasync-mysql:1.2.2") + //implementation("com.github.seratch:kotliquery:1.3.1") + //implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") + //implementation("org.yaml:snakeyaml:1.29") + //implementation("net.kyori:adventure-api:4.8.1") } tasks { diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt index f855590..1a5d7b7 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt @@ -2,6 +2,7 @@ package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.ticketmanager.common.PluginState import com.github.hoshikurama.ticketmanager.common.TMLocale +import com.github.shynixn.mccoroutine.asyncDispatcher import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor @@ -9,6 +10,7 @@ import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level +import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) @@ -18,6 +20,9 @@ internal val mainPlugin: TicketManagerPlugin internal val pluginState: PluginState get() = mainPlugin.configState +internal val asyncContext: CoroutineContext + get() = mainPlugin.asyncDispatcher + internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.localeHandler.consoleLocale)) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index 290636d..0def933 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -2,16 +2,15 @@ package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.* -import com.github.shynixn.mccoroutine.* import com.github.hoshikurama.ticketmanager.common.databases.Database import com.github.hoshikurama.ticketmanager.common.databases.MySQL import com.github.hoshikurama.ticketmanager.common.databases.SQLite import com.github.hoshikurama.ticketmanager.paper.events.Commands import com.github.hoshikurama.ticketmanager.paper.events.PlayerJoin import com.github.hoshikurama.ticketmanager.paper.events.TabComplete +import com.github.shynixn.mccoroutine.* import kotlinx.coroutines.* -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.flow.* import net.kyori.adventure.extra.kotlin.text import net.milkbowl.vault.permission.Permission import org.bukkit.Bukkit @@ -34,10 +33,9 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { override suspend fun onDisableAsync() { pluginLocked.set(true) - asyncDispatcher.cancelChildren() pluginState.database.closeDatabase() - } + //TODO: KEEP TRACK OF CONTEXTS TO WAIT ON RELOADS override fun onEnable() { @@ -68,70 +66,61 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { // Creates task timers Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, Runnable { + launchAsync { configState.cooldowns.filterMapAsync() } + launchAsync { if (pluginLocked.check()) return@launchAsync try { - val state = pluginState - // Mass Unread Notify - if (state.allowUnreadTicketUpdates) { - state.database.getBasicsWithUpdatesAsFlow() - .toList() - .groupBy ({ it.creatorUUID }, { it.id }) - .asSequence() - .filter { it.key != null } - .mapNotNull { Bukkit.getPlayer(it.key!!)?.run { Pair(this, it.value) } } - .filter { it.first.has("ticketmanager.notify.unreadUpdates.scheduled") } - .forEach { - val template = if (it.second.size > 1) it.first.toTMLocale().notifyUnreadUpdateMulti - else it.first.toTMLocale().notifyUnreadUpdateSingle - val tickets = it.second.joinToString(", ") - - val sentMessage = template.replace("%num%", tickets) - it.first.sendMessage(text { formattedContent(sentMessage) }) + if (configState.allowUnreadTicketUpdates) { + Bukkit.getOnlinePlayers().asFlow() + .filter { it.has("ticketmanager.notify.unreadUpdates.scheduled") } + .onEach { + launch { + val ticketIDs = configState.database.getIDsWithUpdatesFor(it.uniqueId).toList() + val tickets = ticketIDs.joinToString(", ") + + if (ticketIDs.isEmpty()) return@launch + + val template = if (ticketIDs.size > 1) it.toTMLocale().notifyUnreadUpdateMulti + else it.toTMLocale().notifyUnreadUpdateSingle + + val sentMessage = template.replace("%num%", tickets) + it.sendMessage(text { formattedContent(sentMessage) }) + } } } - // Open and Assigned Notify - val tickets = state.database.getBasicOpenAsFlow() - .map { it.assignedTo } - .toList() + val openPriority = configState.database.getOpenIDPriorityPairs().map { it.first }.toList() + val openCount = openPriority.count() + val assignments = configState.database.getBasicTickets(openPriority).mapNotNull { it.assignedTo }.toList() - Bukkit.getOnlinePlayers().asSequence() + // Open and Assigned Notify + Bukkit.getOnlinePlayers().asFlow() .filter { it.has("ticketmanager.notify.openTickets.scheduled") } - .forEach { p -> - val open = tickets.size.toString() - val assigned = tickets.asSequence() - .filterNotNull() - .filter { s -> - if (s.startsWith("::")) - perms.getPlayerGroups(p) - .asSequence() - .map { "::$it" } - .filter { it == s } - .any() - else s == p.name - }.count().toString() - - val sentMessage = p.toTMLocale().notifyOpenAssigned - .replace("%open%", open) - .replace("%assigned%", assigned) - p.sendMessage(text { formattedContent(sentMessage) }) - + .onEach { p -> + launch { + val groups = perms.getPlayerGroups(p).map { "::$it" } + val assignedCount = assignments + .filter { it == p.name || it in groups } + .count() + + val sentMessage = p.toTMLocale().notifyOpenAssigned + .replace("%open%", "$openCount") + .replace("%assigned%", "$assignedCount") + p.sendMessage(text { formattedContent(sentMessage) }) + } } - - launch { state.cooldowns.filterMapAsync() } - } catch (e: Exception) { e.printStackTrace() - //postModifiedStacktrace(e) + //postModifiedStacktrace(e) TODO } } }, 100, 12000) } - internal suspend fun loadPlugin() = coroutineScope { + internal suspend fun loadPlugin() = withContext(plugin.asyncDispatcher) { pluginLocked.set(true) configState = run { @@ -160,7 +149,8 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { getString("MySQL_Port")!!, getString("MySQL_DBName")!!, getString("MySQL_Username")!!, - getString("MySQL_Password")!! + getString("MySQL_Password")!!, + asyncDispatcher = (plugin.asyncDispatcher as CoroutineDispatcher), ) Database.Type.SQLite -> SQLite(path) } @@ -178,7 +168,8 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { getString("Colour_Code", "&3")!!, getString("Preferred_Locale", "en_ca")!!, getString("Console_Locale", "en_ca")!!, - getBoolean("Force_Locale", false) + getBoolean("Force_Locale", false), + asyncContext ) } @@ -201,14 +192,14 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { allowUnreadTicketUpdates, checkForPluginUpdate, pluginVersion, - path + path, + asyncContext ) } } launch { - configState.database.initialiseDatabase() - val updateNeeded = configState.database.updateNeededAsync().await() + val updateNeeded = configState.database.updateNeeded() if (updateNeeded) { configState.database.updateDatabase( @@ -222,13 +213,14 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { text { formattedContent(it.informationDBUpdateComplete) } } pluginLocked.set(true) - }, + },//TODO ADD ONERROR offlinePlayerNameToUuidOrNull = { Bukkit.getOfflinePlayers() .filter { it.name == name } .map { it.uniqueId } .firstOrNull() - } + }, + context = asyncContext ) } else pluginLocked.set(false) } diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index c41d352..f62760d 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -1,18 +1,21 @@ package com.github.hoshikurama.ticketmanager.paper.events -import com.github.hoshikurama.componentDSL.* +import com.github.hoshikurama.componentDSL.buildComponent +import com.github.hoshikurama.componentDSL.formattedContent +import com.github.hoshikurama.componentDSL.onClick +import com.github.hoshikurama.componentDSL.onHover import com.github.hoshikurama.ticketmanager.common.* import com.github.hoshikurama.ticketmanager.common.databases.Database import com.github.hoshikurama.ticketmanager.common.ticket.* import com.github.hoshikurama.ticketmanager.paper.* -import com.github.hoshikurama.ticketmanager.paper.has -import com.github.hoshikurama.ticketmanager.paper.mainPlugin -import com.github.hoshikurama.ticketmanager.paper.pluginState -import com.github.hoshikurama.ticketmanager.paper.toTMLocale import com.github.shynixn.mccoroutine.SuspendingCommandExecutor import com.github.shynixn.mccoroutine.asyncDispatcher -import kotlinx.coroutines.* +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import net.kyori.adventure.extra.kotlin.text import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextComponent @@ -26,7 +29,6 @@ import org.bukkit.Location import org.bukkit.command.Command import org.bukkit.command.CommandSender import org.bukkit.entity.Player -import java.lang.Exception import java.util.* class Commands : SuspendingCommandExecutor { @@ -80,12 +82,12 @@ class Commands : SuspendingCommandExecutor { private suspend fun getBasicTicketHandlerAsync( args: List, - senderLocale: TMLocale + senderLocale: TMLocale, ): Deferred { - suspend fun buildFromIDAsync(id: Int) = BasicTicketHandler.buildHandlerAsync(pluginState.database, id) + suspend fun buildFromIDAsync(id: Int) = BasicTicketHandler.buildHandlerAsync(pluginState.database, id, asyncContext) - return coroutineScope { + return withContext(asyncContext) { when (args[0]) { senderLocale.commandWordAssign, senderLocale.commandWordSilentAssign, @@ -375,7 +377,7 @@ class Commands : SuspendingCommandExecutor { assignmentID: String, dbAssignment: String?, ticketHandler: BasicTicketHandler, - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { val shownAssignment = dbAssignment ?: senderLocale.miscNobody launch { ticketHandler.setAssignedTo(dbAssignment) } @@ -436,13 +438,13 @@ class Commands : SuspendingCommandExecutor { args: List, silent: Boolean, ticketHandler: BasicTicketHandler - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } } - return@coroutineScope if (args.size >= 3) + return@withContext if (args.size >= 3) closeWithComment(sender, args, silent, ticketHandler) else closeWithoutComment(sender, args, silent, ticketHandler) } @@ -452,7 +454,7 @@ class Commands : SuspendingCommandExecutor { args: List, silent: Boolean, ticketHandler: BasicTicketHandler, - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { val message = args.subList(2, args.size) .joinToString(" ") .run(ChatColor::stripColor)!! @@ -502,7 +504,7 @@ class Commands : SuspendingCommandExecutor { args: List, silent: Boolean, ticketHandler: BasicTicketHandler - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { launch { pluginState.database.addAction( ticketID = ticketHandler.id, @@ -542,11 +544,11 @@ class Commands : SuspendingCommandExecutor { args: List, silent: Boolean, basicTicket: BasicTicket - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { val lowerBound = args[1].toInt() val upperBound = args[2].toInt() - launch { pluginState.database.massCloseTickets(lowerBound, upperBound, sender.toUUIDOrNull()) } + launch { pluginState.database.massCloseTickets(lowerBound, upperBound, sender.toUUIDOrNull(), asyncContext) } NotifyParams( silent = silent, @@ -577,7 +579,7 @@ class Commands : SuspendingCommandExecutor { args: List, silent: Boolean, ticketHandler: BasicTicketHandler, - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { val message = args.subList(2, args.size) .joinToString(" ") .run(ChatColor::stripColor)!! @@ -633,14 +635,14 @@ class Commands : SuspendingCommandExecutor { private suspend fun create( sender: CommandSender, args: List, - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { val message = args.subList(1, args.size) .joinToString(" ") .run(ChatColor::stripColor)!! val ticket = BasicTicket(creatorUUID = sender.toUUIDOrNull(), location = sender.toTicketLocationOrNull()) - val deferredID = pluginState.database.addNewTicketAsync(ticket, message) + val deferredID = async { pluginState.database.addNewTicket(ticket, asyncContext, message) } mainPlugin.ticketCountMetrics.run { set(check() + 1) } val id = deferredID.await().toString() @@ -728,7 +730,7 @@ class Commands : SuspendingCommandExecutor { args: List, locale: TMLocale, ) { - coroutineScope { + withContext(asyncContext) { val targetName = if (args.size >= 2) args[1].takeIf { it != locale.consoleName } else sender.name.takeIf { sender is Player } val requestedPage = if (args.size >= 3) args[2].toInt() else 1 @@ -744,7 +746,7 @@ class Commands : SuspendingCommandExecutor { val searchedUser = targetName?.attemptToUUIDString() val resultSize: Int - val resultsChunked = pluginState.database.searchDatabase { it.creatorUUID.toString() == searchedUser } + val resultsChunked = pluginState.database.searchDatabase(asyncContext) { it.creatorUUID.toString() == searchedUser } .toList() .sortedByDescending(BasicTicket::id) .also { resultSize = it.size } @@ -882,20 +884,29 @@ class Commands : SuspendingCommandExecutor { args: List, locale: TMLocale, headerFormat: String, - getTickets: suspend (Database) -> List, + getIDPriorityPair: suspend (Database) -> Flow>, baseCommand: (TMLocale) -> String ): Component { - val chunkedTickets = getTickets(pluginState.database).sortedWith(sortForList).chunked(8) - val page = if (args.size == 2 && args[1].toInt() in 1..chunkedTickets.size) args[1].toInt() else 1 + val chunkedIDs = getIDPriorityPair(pluginState.database) + .toList() + .sortedWith(compareByDescending> { it.second }.thenByDescending { it.first } ) + .map { it.first } + .chunked(8) + val page = if (args.size == 2 && args[1].toInt() in 1..chunkedIDs.size) args[1].toInt() else 1 + + val fullTickets = chunkedIDs.getOrNull(page - 1) + ?.run { pluginState.database.getFullTickets(this, asyncContext) } + ?.toList() + ?: emptyList() return buildComponent { text { formattedContent(headerFormat) } - if (chunkedTickets.isNotEmpty()) { - chunkedTickets[page - 1].forEach { append(createListEntry(it, locale)) } + if (fullTickets.isNotEmpty()) { + fullTickets.forEach { append(createListEntry(it, locale)) } - if (chunkedTickets.size > 1) { - append(buildPageComponent(page, chunkedTickets.size, locale, baseCommand)) + if (chunkedIDs.size > 1) { + append(buildPageComponent(page, chunkedIDs.size, locale, baseCommand)) } } } @@ -909,7 +920,7 @@ class Commands : SuspendingCommandExecutor { ) { sender.sendMessage( createGeneralList(args, locale, locale.listFormatHeader, - getTickets = { db -> db.getFullOpenAsFlow().toList() }, + getIDPriorityPair = { it.getOpenIDPriorityPairs() }, baseCommand = locale.run{ { "/$commandBase $commandWordList " } } ) ) @@ -921,13 +932,11 @@ class Commands : SuspendingCommandExecutor { args: List, locale: TMLocale, ) { - val groups = if (sender is Player) - mainPlugin.perms.getPlayerGroups(sender).map { "::$it" } - else listOf() + val groups: List = if (sender is Player) mainPlugin.perms.getPlayerGroups(sender).toList() else listOf() sender.sendMessage( createGeneralList(args, locale, locale.listFormatAssignedHeader, - getTickets = { db -> db.getFullOpenAssignedAsFlow(sender.name, groups).toList() }, + getIDPriorityPair = { it.getAssignedOpenIDPriorityPairs(sender.name, groups) }, baseCommand = locale.run { { "/$commandBase $commandWordListAssigned " } } ) ) @@ -938,7 +947,7 @@ class Commands : SuspendingCommandExecutor { sender: CommandSender, locale: TMLocale, ) { - coroutineScope { + withContext(asyncContext) { mainPlugin.pluginLocked.set(true) pushMassNotify("ticketmanager.notify.info") { text { formattedContent(it.informationReloadInitiated.replace("%user%", sender.name)) } @@ -964,7 +973,7 @@ class Commands : SuspendingCommandExecutor { args: List, silent: Boolean, ticketHandler: BasicTicketHandler, - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { val action = FullTicket.Action(FullTicket.Action.Type.REOPEN, sender.toUUIDOrNull()) // Updates user status if needed @@ -1009,7 +1018,7 @@ class Commands : SuspendingCommandExecutor { args: List, locale: TMLocale, ) { - coroutineScope { + withContext(asyncContext) { fun String.attemptToUUIDString(): String? = if (equals(locale.consoleName)) null else Bukkit.getOfflinePlayers().asSequence() @@ -1107,7 +1116,7 @@ class Commands : SuspendingCommandExecutor { // Results Computation val resultSize: Int - val chunkedTickets = pluginState.database.searchDatabase(composedSearch) + val chunkedTickets = pluginState.database.searchDatabase(asyncContext, composedSearch) .toList() .sortedByDescending(BasicTicket::id) .apply { resultSize = size } @@ -1177,7 +1186,7 @@ class Commands : SuspendingCommandExecutor { args: List, silent: Boolean, ticketHandler: BasicTicketHandler, - ): NotifyParams = coroutineScope { + ): NotifyParams = withContext(asyncContext) { launch { pluginState.database.addAction( ticketID = ticketHandler.id, @@ -1277,8 +1286,8 @@ class Commands : SuspendingCommandExecutor { locale: TMLocale, ticketHandler: BasicTicketHandler, ) { - coroutineScope { - val fullTicket = ticketHandler.toFullTicketAsync().await() + withContext(asyncContext) { + val fullTicket = ticketHandler.toFullTicketAsync(asyncContext).await() val baseComponent = buildTicketInfoComponent(fullTicket, locale) if (!sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && ticketHandler.creatorStatusUpdate) @@ -1304,8 +1313,8 @@ class Commands : SuspendingCommandExecutor { locale: TMLocale, ticketHandler: BasicTicketHandler, ) { - coroutineScope { - val fullTicket = ticketHandler.toFullTicketAsync().await() + withContext(asyncContext) { + val fullTicket = ticketHandler.toFullTicketAsync(asyncContext).await() val baseComponent = buildTicketInfoComponent(fullTicket, locale) if (!sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && ticketHandler.creatorStatusUpdate) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt index f073c70..a67baba 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt @@ -1,13 +1,14 @@ package com.github.hoshikurama.ticketmanager.paper.events import com.github.hoshikurama.componentDSL.formattedContent -import com.github.shynixn.mccoroutine.asyncDispatcher import com.github.hoshikurama.ticketmanager.paper.has import com.github.hoshikurama.ticketmanager.paper.mainPlugin import com.github.hoshikurama.ticketmanager.paper.pluginState import com.github.hoshikurama.ticketmanager.paper.toTMLocale -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.* +import com.github.shynixn.mccoroutine.asyncDispatcher +import com.github.shynixn.mccoroutine.minecraftDispatcher +import kotlinx.coroutines.flow.count +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.kyori.adventure.extra.kotlin.text @@ -18,8 +19,8 @@ import org.bukkit.event.player.PlayerJoinEvent class PlayerJoin : Listener { @EventHandler - suspend fun onPlayerJoin(event: PlayerJoinEvent) = coroutineScope { - if (mainPlugin.pluginLocked.check()) return@coroutineScope + suspend fun onPlayerJoin(event: PlayerJoinEvent) = withContext(mainPlugin.minecraftDispatcher) { + if (mainPlugin.pluginLocked.check()) return@withContext val player = event.player withContext(mainPlugin.asyncDispatcher) { @@ -38,7 +39,7 @@ class PlayerJoin : Listener { // Unread Updates launch { if (player.has("ticketmanager.notify.unreadUpdates.onJoin")) { - pluginState.database.getIDsWithUpdatesAsFlowFor(player.uniqueId) + pluginState.database.getIDsWithUpdatesFor(player.uniqueId) .toList() .run { if (size == 0) null else this } ?.run { @@ -56,8 +57,8 @@ class PlayerJoin : Listener { // View Open-Count and Assigned-Count Tickets launch { if (player.has("ticketmanager.notify.openTickets.onJoin")) { - val open = pluginState.database.getBasicOpenAsFlow() - val assigned = pluginState.database.getBasicOpenAssignedAsFlow(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) + val open = pluginState.database.getOpenIDPriorityPairs() + val assigned = pluginState.database.getAssignedOpenIDPriorityPairs(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) val sentMSG = player.toTMLocale().notifyOpenAssigned .replace("%open%", "${open.count()}") diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 511ee66..b35eccb 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -12,7 +12,7 @@ dependencies { implementation(kotlin("stdlib", version = "1.5.20")) implementation("mysql:mysql-connector-java:8.0.25") implementation("org.xerial:sqlite-jdbc:3.34.0") - implementation("com.github.jasync-sql:jasync-mysql:1.1.6") + implementation("com.github.jasync-sql:jasync-mysql:1.2.2") implementation("com.github.seratch:kotliquery:1.3.1") implementation("com.github.HoshiKurama:KyoriComponentDSL:1.1.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0") diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt index c5ea1d7..255586d 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt @@ -1,8 +1,9 @@ package com.github.hoshikurama.ticketmanager.common import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext import org.yaml.snakeyaml.Yaml +import kotlin.coroutines.CoroutineContext const val translationNotFound = " TNF " @@ -19,7 +20,8 @@ class LocaleHandler( preferredLocale: String, console_Locale: String, forceLocale: Boolean, - ): LocaleHandler = coroutineScope { + context: CoroutineContext + ): LocaleHandler = withContext(context) { val fallback = async { TMLocale(mainColourCode, "en_CA") } val activeTypes = diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt index a84b529..3c6ef12 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt @@ -1,6 +1,7 @@ package com.github.hoshikurama.ticketmanager.common import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket +import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import kotlinx.coroutines.withContext import java.io.InputStream import java.net.URL @@ -91,4 +92,6 @@ fun relTimeToEpochSecond(relTime: String, locale: TMLocale): Long { val sortForList: Comparator = Comparator.comparing(BasicTicket::priority).reversed().thenComparing(Comparator.comparing(BasicTicket::id).reversed()) +val sortActions: Comparator = Comparator.comparing(FullTicket.Action::timestamp) + fun T.notEquals(t: T) = this != t \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt index ad5c862..a14662a 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt @@ -3,9 +3,9 @@ package com.github.hoshikurama.ticketmanager.common import com.github.hoshikurama.ticketmanager.common.databases.Database import com.github.hoshikurama.ticketmanager.common.databases.SQLite import kotlinx.coroutines.* -import net.kyori.adventure.text.Component import java.time.Instant import java.util.* +import kotlin.coroutines.CoroutineContext class PluginState( val cooldowns: Cooldown, @@ -24,11 +24,18 @@ class PluginState( crossinline checkForPluginUpdate: () -> Boolean?, crossinline pluginVersion: () -> String, absolutePathToPluginFolder: String, - ) = coroutineScope { + context: CoroutineContext + ) = withContext(context) { - val deferredDatabase = async { tryOrDefault(database, SQLite(absolutePathToPluginFolder)) } val deferredCooldown = async { tryOrDefault(cooldown, Cooldown(false, 0)) } - val deferredAllowUnreadUpdates = async { tryOrDefault(allowUnreadTicketUpdates, true) } + val deferredAllowUnreadUpdates = async { tryOrDefault(allowUnreadTicketUpdates, true) + } + val deferredDatabase = async { + tryOrDefault( + attempted = { database()?.apply { initialiseDatabase() } }, + default = SQLite(absolutePathToPluginFolder) + ) + } val deferredPluginUpdate = async { val shouldCheck = checkForPluginUpdate() @@ -55,6 +62,7 @@ class PluginState( preferredLocale = "en_ca", console_Locale = "en_ca", forceLocale = false, + context = context ) ) } @@ -110,8 +118,7 @@ inline fun tryOrNull(function: () -> T): T? = suspend inline fun tryOrDefaultSuspend(crossinline attempted: suspend () -> T?, default: T): T = tryOrNullSuspend(attempted).run { this ?: default } -suspend inline fun tryOrNullSuspend(crossinline function: suspend () -> T): T? = coroutineScope { +suspend inline fun tryOrNullSuspend(crossinline function: suspend () -> T): T? = try { function() } catch (ignored: Exception) { null } -} diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index f548d29..6d84835 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -2,9 +2,9 @@ package com.github.hoshikurama.ticketmanager.common.databases import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket -import kotlinx.coroutines.Deferred import kotlinx.coroutines.flow.Flow import java.util.* +import kotlin.coroutines.CoroutineContext interface Database { val type: Type @@ -14,46 +14,49 @@ interface Database { } // Individual property getters - suspend fun getActionsAsFlow(ticketID: Int): Flow> + suspend fun getActionsAsFlow(ticketID: Int): Flow // Individual property setters - suspend fun setAssignmentAsync(ticketID: Int, assignment: String?) - suspend fun setCreatorStatusUpdateAsync(ticketID: Int, status: Boolean) - suspend fun setPriorityAsync(ticketID: Int, priority: BasicTicket.Priority) - suspend fun setStatusAsync(ticketID: Int, status: BasicTicket.Status) + suspend fun setAssignment(ticketID: Int, assignment: String?) + suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) + suspend fun setPriority(ticketID: Int, priority: BasicTicket.Priority) + suspend fun setStatus(ticketID: Int, status: BasicTicket.Status) // Get Specific Ticket Type - suspend fun getBasicTicketAsync(ticketID: Int): Deferred + suspend fun getBasicTicket(ticketID: Int): BasicTicket? // Database additions suspend fun addAction(ticketID: Int, action: FullTicket.Action) suspend fun addFullTicket(fullTicket: FullTicket) - suspend fun addNewTicketAsync(basicTicket: BasicTicket, message: String): Deferred + suspend fun addNewTicket(basicTicket: BasicTicket, context: CoroutineContext, message: String): Int // Database removals - suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) + suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?, context: CoroutineContext) // Collections of tickets - suspend fun getBasicOpenAsFlow(): Flow - suspend fun getBasicOpenAssignedAsFlow(assignment: String, groupAssignment: List): Flow - suspend fun getBasicsWithUpdatesAsFlow(): Flow - suspend fun getFullOpenAsFlow(): Flow - suspend fun getFullOpenAssignedAsFlow(assignment: String, groupAssignment: List): Flow - suspend fun getIDsWithUpdatesAsFlowFor(uuid: UUID): Flow + suspend fun getOpenIDPriorityPairs(): Flow> + suspend fun getAssignedOpenIDPriorityPairs(assignment: String, unfixedGroupAssignment: List): Flow> + suspend fun getIDsWithUpdates(): Flow + suspend fun getIDsWithUpdatesFor(uuid: UUID): Flow + suspend fun getBasicTickets(ids: List): Flow + suspend fun getFullTicketsFromBasics(basicTickets: List, context: CoroutineContext): Flow + suspend fun getFullTickets(ids: List, context: CoroutineContext): Flow // Database searching - suspend fun searchDatabase(searchFunction: (FullTicket) -> Boolean): Flow + suspend fun searchDatabase(context: CoroutineContext, searchFunction: (FullTicket) -> Boolean): Flow // Internal Database Functions suspend fun closeDatabase() suspend fun initialiseDatabase() - suspend fun updateNeededAsync(): Deferred + suspend fun updateNeeded(): Boolean suspend fun migrateDatabase( + context: CoroutineContext, to: Type, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit, ) suspend fun updateDatabase( + context: CoroutineContext, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit, offlinePlayerNameToUuidOrNull: (String) -> UUID? diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt index 070b988..9203ea5 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt @@ -1,10 +1,26 @@ package com.github.hoshikurama.ticketmanager.common.databases +import com.github.hoshikurama.ticketmanager.common.byteToPriority +import com.github.hoshikurama.ticketmanager.common.sortActions import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket -import kotlinx.coroutines.Deferred +import com.github.hoshikurama.ticketmanager.common.ticket.toTicketLocation +import com.github.jasync.sql.db.ConnectionPoolConfiguration +import com.github.jasync.sql.db.RowData +import com.github.jasync.sql.db.SuspendingConnection +import com.github.jasync.sql.db.asSuspending +import com.github.jasync.sql.db.mysql.MySQLConnectionBuilder +import com.github.jasync.sql.db.mysql.MySQLQueryResult +import com.github.jasync.sql.db.util.ExecutorServiceUtils +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.toList +import java.time.Instant import java.util.* +import java.util.concurrent.Executor +import kotlin.coroutines.CoroutineContext class MySQL( host: String, @@ -12,97 +28,249 @@ class MySQL( dbName: String, username: String, password: String, + asyncDispatcher: CoroutineDispatcher = Dispatchers.Default, + asyncExecutor: Executor = ExecutorServiceUtils.CommonPool, ) : Database { - override val type: Database.Type - get() = TODO("Not yet implemented") + private val connectionPool = MySQLConnectionBuilder.createConnectionPool( + ConnectionPoolConfiguration( + host = host, + port = port.toInt(), + database = dbName, + username = username, + password = password, + coroutineDispatcher = asyncDispatcher, + executionContext = asyncExecutor, + ) + ) + private val suspendingCon: SuspendingConnection + get() = connectionPool.asSuspending - override suspend fun getActionsAsFlow(ticketID: Int): Flow> { - TODO("Not yet implemented") + override val type = Database.Type.MySQL + + override suspend fun getActionsAsFlow(ticketID: Int) = flow { + suspendingCon.sendPreparedStatement(query = "SELECT ACTION_ID, ACTION_TYPE, CREATOR_UUID, MESSAGE, TIMESTAMP FROM TicketManager_V4_Actions WHERE TICKET_ID = $ticketID;").rows + .forEach { emit(it.toAction()) } } - override suspend fun setAssignmentAsync(ticketID: Int, assignment: String?) { - TODO("Not yet implemented") + override suspend fun setAssignment(ticketID: Int, assignment: String?) { + suspendingCon.sendPreparedStatement("UPDATE TicketManager_V4_Tickets SET ASSIGNED_TO = ? WHERE ID = $ticketID;", listOf(assignment)) } - override suspend fun setCreatorStatusUpdateAsync(ticketID: Int, status: Boolean) { - TODO("Not yet implemented") + override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { + suspendingCon.sendPreparedStatement("UPDATE TicketManager_V4_Tickets SET STATUS_UPDATE_FOR_CREATOR = ? WHERE ID = $ticketID;", listOf(status)) } - override suspend fun setPriorityAsync(ticketID: Int, priority: BasicTicket.Priority) { - TODO("Not yet implemented") + override suspend fun setPriority(ticketID: Int, priority: BasicTicket.Priority) { + suspendingCon.sendPreparedStatement("UPDATE TicketManager_V4_Tickets SET PRIORITY = ? WHERE ID = $ticketID;", listOf(priority.level)) } - override suspend fun setStatusAsync(ticketID: Int, status: BasicTicket.Status) { - TODO("Not yet implemented") + override suspend fun setStatus(ticketID: Int, status: BasicTicket.Status) { + suspendingCon.sendPreparedStatement("UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID = $ticketID;", listOf(status.name)) } - override suspend fun getBasicTicketAsync(ticketID: Int): Deferred { - TODO("Not yet implemented") + override suspend fun getBasicTicket(ticketID: Int): BasicTicket? { + val query = suspendingCon.sendPreparedStatement("SELECT * FROM TicketManager_V4_Tickets WHERE ID = $ticketID;").rows + return query.firstOrNull()?.toBasicTicket() } override suspend fun addAction(ticketID: Int, action: FullTicket.Action) { - TODO("Not yet implemented") + writeAction(ticketID, action) } override suspend fun addFullTicket(fullTicket: FullTicket) { - TODO("Not yet implemented") + writeBasicTicket(fullTicket) + fullTicket.actions.forEach { + writeAction(fullTicket.id, it) + } } - override suspend fun addNewTicketAsync(basicTicket: BasicTicket, message: String): Deferred { - TODO("Not yet implemented") + override suspend fun addNewTicket(basicTicket: BasicTicket, context: CoroutineContext, message: String): Int { + return withContext(context) { + val id = writeBasicTicket(basicTicket).toInt() + launch { writeAction(id, FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message)) } + id + } } - override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { - TODO("Not yet implemented") + override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?, context: CoroutineContext) { + withContext(context) { + val statusPairs = flow { + suspendingCon.sendPreparedStatement("SELECT ID, STATUS FROM TicketManager_V4_Tickets WHERE ID BETWEEN $lowerBound AND $upperBound;") + .rows + .map { (it.getInt(0)!!) to BasicTicket.Status.valueOf(it.getString(1)!!) } + .filter { it.second == BasicTicket.Status.OPEN } + .forEach { emit(it.first) } + } + + launch { + val idString = statusPairs.toList().joinToString(", ") + suspendingCon.sendPreparedStatement( + query = "UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID IN ($idString);", + values = listOf(BasicTicket.Status.CLOSED.name) + ) + } + + statusPairs.onEach { + launch { + writeAction( + action = FullTicket.Action( + type = FullTicket.Action.Type.MASS_CLOSE, + user = uuid, + message = null, + timestamp = Instant.now().epochSecond + ), + ticketID = it + ) + } + } + } } - override suspend fun getBasicOpenAsFlow(): Flow { - TODO("Not yet implemented") + override suspend fun getOpenIDPriorityPairs(): Flow> = flow { + suspendingCon.sendPreparedStatement( + query = "SELECT ID, PRIORITY FROM TicketManager_V4_Tickets WHERE STATUS = ?;", + values = listOf(BasicTicket.Status.OPEN.name) + ) + .rows + .map { it.getInt(0)!! to it.getByte(1)!! } + .forEach { emit(it) } } - override suspend fun getBasicOpenAssignedAsFlow( + override suspend fun getAssignedOpenIDPriorityPairs( assignment: String, - groupAssignment: List - ): Flow { - TODO("Not yet implemented") + unfixedGroupAssignment: List + ): Flow> = flow { + val groupsSQLStatement = unfixedGroupAssignment.joinToString(" OR ") { "ASSIGNED_TO = ?" } + val groupsFixed = unfixedGroupAssignment.map { "::$it" } + + suspendingCon.sendPreparedStatement( + query = "SELECT ID, PRIORITY FROM TicketManager_V4_Tickets WHERE STATUS = ? AND ($groupsSQLStatement);", + values = listOf(BasicTicket.Status.OPEN.name) + groupsFixed + ) + .rows + .map { it.getInt(0)!! to it.getByte(1)!! } + .forEach { emit(it) } } - override suspend fun getBasicsWithUpdatesAsFlow(): Flow { - TODO("Not yet implemented") + override suspend fun getIDsWithUpdates(): Flow = flow { + suspendingCon.sendPreparedStatement( + query = "SELECT ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ?;", + values = listOf(true) + ) + .rows + .map { it.getInt(0)!! } + .forEach { emit(it) } } - override suspend fun getFullOpenAsFlow(): Flow { - TODO("Not yet implemented") + override suspend fun getIDsWithUpdatesFor(uuid: UUID): Flow = flow { + suspendingCon.sendPreparedStatement( + query = "SELECT ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ? AND CREATOR_UUID = ?;", + values = listOf(true, uuid.toString()) + ) + .rows + .map { it.getInt(0)!! } + .forEach { emit(it) } } - override suspend fun getFullOpenAssignedAsFlow( - assignment: String, - groupAssignment: List - ): Flow { - TODO("Not yet implemented") + override suspend fun getBasicTickets(ids: List): Flow = flow { + val idsSQL = ids.joinToString(", ") { "$it" } + + suspendingCon.sendQuery("SELECT * FROM TicketManager_V4_Tickets WHERE ID IN ($idsSQL);") + .rows + .map { it.toBasicTicket() } + .forEach { emit(it) } } - override suspend fun getIDsWithUpdatesAsFlowFor(uuid: UUID): Flow { - TODO("Not yet implemented") + override suspend fun getFullTicketsFromBasics( + basicTickets: List, + context: CoroutineContext + ): Flow = flow { + basicTickets + .map { + withContext(context) { + async { it.toFullTicket() } + } + } + .map { it.await() } + .forEach { emit(it) } } - override suspend fun searchDatabase(searchFunction: (FullTicket) -> Boolean): Flow { - TODO("Not yet implemented") + override suspend fun getFullTickets(ids: List, context: CoroutineContext): Flow = flow { + val idsSQL = ids.joinToString(", ") { "$it" } + suspendingCon.sendQuery("SELECT * FROM TicketManager_V4_Tickets WHERE ID IN ($idsSQL);") + .rows + .map { it.toBasicTicket() } + .map { + withContext(context) { + async { it.toFullTicket() } + } + } + .map { it.await() } + .forEach { emit(it) } + } + + override suspend fun searchDatabase(context: CoroutineContext, searchFunction: (FullTicket) -> Boolean): Flow = flow { + suspendingCon.sendPreparedStatement("SELECT * FROM TicketManager_V4_Tickets;") + .rows + .map { it.toBasicTicket() } + .map { + withContext(context) { + async { it.toFullTicket() } + } + } + .map { it.await() } + .filter(searchFunction) + .forEach{ emit(it) } } override suspend fun closeDatabase() { - TODO("Not yet implemented") + connectionPool.disconnect() } override suspend fun initialiseDatabase() { - TODO("Not yet implemented") + suspendingCon.connect() + + if (!tableExists("TicketManager_V4_Tickets")) { + suspendingCon.sendQuery( + """ + CREATE TABLE TicketManager_V4_Tickets ( + ID INT NOT NULL AUTO_INCREMENT, + CREATOR_UUID VARCHAR(36) CHARACTER SET latin1 COLLATE latin1_general_ci, + PRIORITY TINYINT NOT NULL, + STATUS VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + ASSIGNED_TO VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + STATUS_UPDATE_FOR_CREATOR BOOLEAN NOT NULL, + LOCATION VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + KEY STATUS_V4 (STATUS) USING BTREE, + KEY STATUS_UPDATE_FOR_CREATOR_V4 (STATUS_UPDATE_FOR_CREATOR) USING BTREE, + PRIMARY KEY (ID) + ) ENGINE=InnoDB; + """.trimIndent() + ) + } + if (!tableExists("TicketManager_V4_Actions")) { + suspendingCon.sendQuery( + """ + CREATE TABLE TicketManager_V4_Actions ( + ACTION_ID INT NOT NULL AUTO_INCREMENT, + TICKET_ID INT NOT NULL, + ACTION_TYPE VARCHAR(20) CHARACTER SET latin1 COLLATE latin1_general_ci NOT NULL, + CREATOR_UUID VARCHAR(36) CHARACTER SET latin1 COLLATE latin1_general_ci, + MESSAGE TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + TIMESTAMP BIGINT NOT NULL, + PRIMARY KEY (ACTION_ID) + ) ENGINE=InnoDB; + """.trimIndent() + ) + } } - override suspend fun updateNeededAsync(): Deferred { - TODO("Not yet implemented") + override suspend fun updateNeeded(): Boolean { + return tableExists("TicketManagerTicketsV2") } override suspend fun migrateDatabase( + context: CoroutineContext, to: Database.Type, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit @@ -111,10 +279,127 @@ class MySQL( } override suspend fun updateDatabase( + context: CoroutineContext, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit, offlinePlayerNameToUuidOrNull: (String) -> UUID? ) { - TODO("Not yet implemented") + withContext(context) { + onBegin() + + suspendingCon.sendPreparedStatement("SELECT * FROM TicketManagerTicketsV2;").rows + .forEach { + val fullTicket = FullTicket( + id = it.getInt(0)!!, + priority = byteToPriority(it.getByte(2)!!), + creatorStatusUpdate = it.getBoolean(9)!!, + status = it.getString(1)!!.let(BasicTicket.Status::valueOf), + assignedTo = it.getString(5), + + creatorUUID = it.getString(4)!!.let { s -> + if (s.lowercase() == "console") null + else UUID.fromString(s) + }, + + location = it.getString(6)!!.run { + if (equals("NoLocation")) null + else toTicketLocation() + }, + + actions = it.getString(8)!! + .split("/MySQLNewLine/") + .filter(String::isNotBlank) + .map { s -> s.split("/MySQLSep/") } + .mapIndexed { index, action -> + FullTicket.Action( + message = action[1], + type = if (index == 0) FullTicket.Action.Type.OPEN else FullTicket.Action.Type.COMMENT, + timestamp = it[7] as Long, + user = + if (action[0].lowercase() == "console") null + else offlinePlayerNameToUuidOrNull(action[0]) + ) + } + , + ) + + launch { + val id = writeBasicTicket(fullTicket).toInt() + fullTicket.actions.forEach { a -> writeAction(id, a) } + } + } + + onComplete() + } } + + // PRIVATE FUNCTIONS + private suspend fun tableExists(table: String): Boolean { + suspendingCon.sendQuery("SHOW TABLES;").rows + .forEach { + if (it.getString(0) == table.lowercase()) + return true + } + return false + } + + private suspend fun writeBasicTicket(ticket: BasicTicket): Long { + val query = suspendingCon.sendPreparedStatement( + query = "INSERT INTO TicketManager_V4_Tickets (CREATOR_UUID, PRIORITY, STATUS, ASSIGNED_TO, STATUS_UPDATE_FOR_CREATOR, LOCATION) VALUES (?,?,?,?,?,?);", + listOf( + ticket.creatorUUID?.toString(), + ticket.priority.level, + ticket.status.name, + ticket.assignedTo, + ticket.creatorStatusUpdate, + ticket.location?.toString() + ) + ) as MySQLQueryResult + + return query.lastInsertId + } + + private suspend fun writeAction(ticketID: Int, action: FullTicket.Action) { + suspendingCon.sendPreparedStatement( + query = "INSERT INTO TicketManager_V4_Actions (TICKET_ID,ACTION_TYPE,CREATOR_UUID,MESSAGE,TIMESTAMP) VALUES (?,?,?,?,?);", + listOf( + ticketID, + action.type.name, + action.user?.toString(), + action.message, + action.timestamp, + ) + ) + } + + private fun RowData.toBasicTicket(): BasicTicket { + return BasicTicket( + id = getInt(0)!!, + assignedTo = getString(4), + creatorStatusUpdate = getBoolean(5)!!, + creatorUUID = getString(1)?.let(UUID::fromString), + priority = getByte(2)!!.let(::byteToPriority), + status = getString(3)!!.let(BasicTicket.Status::valueOf), + location = getString(6)?.split(" ")?.let { + BasicTicket.TicketLocation( + world = it[0], + x = it[1].toInt(), + y = it[2].toInt(), + z = it[3].toInt() + ) + } + ) + } + + private fun RowData.toAction(): FullTicket.Action { + return FullTicket.Action( + type = FullTicket.Action.Type.valueOf(getString(1)!!), + user = getString(2)?.let(UUID::fromString), + message = getString(3), + timestamp = getLong(4)!!, + ) + } + + private suspend fun BasicTicket.toFullTicket() = + FullTicket(this, getActionsAsFlow(id).toList().sortedWith(sortActions)) } \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index c3a759f..d7cc6e4 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -4,14 +4,14 @@ import com.github.hoshikurama.ticketmanager.common.byteToPriority import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import com.github.hoshikurama.ticketmanager.common.ticket.toTicketLocation -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow import kotliquery.* import java.sql.DriverManager import java.time.Instant import java.util.* +import kotlin.coroutines.CoroutineContext class SQLite(absoluteDataFolderPath: String) : Database { @@ -21,40 +21,37 @@ class SQLite(absoluteDataFolderPath: String) : Database { private fun getSession() = Session(Connection(DriverManager.getConnection(url))) - override suspend fun getActionsAsFlow(ticketID: Int): Flow> { + override suspend fun getActionsAsFlow(ticketID: Int): Flow { return using(getSession()) { getActions(ticketID, it) } - .map { ticketID to it } .asFlow() } - override suspend fun setAssignmentAsync(ticketID: Int, assignment: String?) { + override suspend fun setAssignment(ticketID: Int, assignment: String?) { using(getSession()) { it.run(queryOf("UPDATE TicketManager_V4_Tickets SET ASSIGNED_TO = ? WHERE ID = $ticketID;", assignment).asUpdate) } } - override suspend fun setCreatorStatusUpdateAsync(ticketID: Int, status: Boolean) { + override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { using(getSession()) { it.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS_UPDATE_FOR_CREATOR = ? WHERE ID = $ticketID;", status).asUpdate) } } - override suspend fun setPriorityAsync(ticketID: Int, priority: BasicTicket.Priority) { + override suspend fun setPriority(ticketID: Int, priority: BasicTicket.Priority) { using(getSession()) { it.run(queryOf("UPDATE TicketManager_V4_Tickets SET PRIORITY = ? WHERE ID = $ticketID;", priority.level).asUpdate) } } - override suspend fun setStatusAsync(ticketID: Int, status: BasicTicket.Status) { + override suspend fun setStatus(ticketID: Int, status: BasicTicket.Status) { using(getSession()) { it.run(queryOf("UPDATE TicketManager_V4_Tickets SET STATUS = ? WHERE ID = $ticketID;", status.name).asUpdate) } } - override suspend fun getBasicTicketAsync(ticketID: Int): Deferred = coroutineScope { - async { - using(getSession()) { getBasicTicket(ticketID, it) } - } + override suspend fun getBasicTicket(ticketID: Int): BasicTicket? { + return using(getSession()) { getBasicTicket(ticketID, it) } } override suspend fun addAction(ticketID: Int, action: FullTicket.Action) { @@ -73,17 +70,15 @@ class SQLite(absoluteDataFolderPath: String) : Database { } } - override suspend fun addNewTicketAsync(basicTicket: BasicTicket, message: String): Deferred = coroutineScope { - async { - using(getSession()) { session -> - val id = writeTicket(basicTicket, session)!!.toInt() - writeAction(FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message), id, session) - id - } + override suspend fun addNewTicket(basicTicket: BasicTicket, context: CoroutineContext, message: String): Int { + return using(getSession()) { session -> + val id = writeTicket(basicTicket, session)!!.toInt() + writeAction(FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message), id, session) + id } } - override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?) { + override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?, context: CoroutineContext) { using(getSession()) { session -> val idStatusPairs = session.run(queryOf("SELECT ID, STATUS FROM TicketManager_V4_Tickets WHERE ID BETWEEN $lowerBound AND $upperBound;") @@ -111,46 +106,46 @@ class SQLite(absoluteDataFolderPath: String) : Database { } } - override suspend fun getBasicOpenAsFlow(): Flow { + override suspend fun getOpenIDPriorityPairs(): Flow> { return using(getSession()) { session -> session.run( - queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE STATUS = ?;", BasicTicket.Status.OPEN.toString()) - .map { it.toBasicTicket() } + queryOf("SELECT ID, PRIORITY FROM TicketManager_V4_Tickets WHERE STATUS = ?;", BasicTicket.Status.OPEN.name) + .map { it.int(1) to it.byte(2) } .asList ) }.asFlow() } - override suspend fun getBasicOpenAssignedAsFlow( + override suspend fun getAssignedOpenIDPriorityPairs( assignment: String, - groupAssignment: List - ): Flow { - return getBasicOpenAsFlow().filter { it.assignedTo == assignment || it.assignedTo in groupAssignment } - } + unfixedGroupAssignment: List + ): Flow> { + val groupsSQLStatement = unfixedGroupAssignment.joinToString(" OR ") { "ASSIGNED_TO = ?" } + val groupsFixed = unfixedGroupAssignment.map { "::$it" } - override suspend fun getBasicsWithUpdatesAsFlow(): Flow { return using(getSession()) { session -> session.run( - queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ?;", true) - .map { it.toBasicTicket() } + queryOf( + statement = "SELECT ID, PRIORITY FROM TicketManager_V4_Tickets WHERE STATUS = ? AND ($groupsSQLStatement);", + params = (listOf(BasicTicket.Status.OPEN.name) + groupsFixed).toTypedArray() + ) + .map { it.int(1) to it.byte(2) } .asList ) }.asFlow() } - override suspend fun getFullOpenAsFlow(): Flow { - val basicTickets = getBasicOpenAsFlow().toList() - + override suspend fun getIDsWithUpdates(): Flow { return using(getSession()) { session -> - basicTickets.map { it.toFullTicket(session) }.asFlow() - } - } - - override suspend fun getFullOpenAssignedAsFlow(assignment: String, groupAssignment: List): Flow { - return getFullOpenAsFlow().filter { it.assignedTo == assignment || it.assignedTo in groupAssignment } + session.run( + queryOf("SELECT ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ?;", true) + .map { it.int(1) } + .asList + ) + }.asFlow() } - override suspend fun getIDsWithUpdatesAsFlowFor(uuid: UUID): Flow { + override suspend fun getIDsWithUpdatesFor(uuid: UUID): Flow { return using(getSession()) { session -> session.run( queryOf("SELECT ID FROM TicketManager_V4_Tickets WHERE STATUS_UPDATE_FOR_CREATOR = ? AND CREATOR_UUID = ?;", true, uuid) @@ -160,7 +155,38 @@ class SQLite(absoluteDataFolderPath: String) : Database { }.asFlow() } - override suspend fun searchDatabase(searchFunction: (FullTicket) -> Boolean): Flow { + override suspend fun getBasicTickets(ids: List): Flow { + val idsSQL = ids.joinToString(", ") { "$it" } + + return using(getSession()) { session -> + session.run( + queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE ID IN ($idsSQL);") + .map { it.toBasicTicket() } + .asList + ) + }.asFlow() + } + + override suspend fun getFullTicketsFromBasics( + basicTickets: List, + context: CoroutineContext + ): Flow { + return using(getSession()) { session -> + basicTickets + .map { it.toFullTicket(session) } + .asFlow() + } + } + + override suspend fun getFullTickets(ids: List, context: CoroutineContext): Flow { + return using(getSession()) { session -> + ids.asSequence() + .mapNotNull { getBasicTicket(it, session) } + .map { it.toFullTicket(session) } + }.asFlow() + } + + override suspend fun searchDatabase(context: CoroutineContext, searchFunction: (FullTicket) -> Boolean): Flow { val matchedTickets = mutableListOf() using(getSession()) { session -> @@ -214,17 +240,14 @@ class SQLite(absoluteDataFolderPath: String) : Database { } } - override suspend fun updateNeededAsync(): Deferred { - return coroutineScope { - async { - using(getSession()) { - tableExists("TicketManagerTicketsV2", it) - } - } + override suspend fun updateNeeded(): Boolean { + return using(getSession()) { + tableExists("TicketManagerTicketsV2", it) } } override suspend fun migrateDatabase( + context: CoroutineContext, to: Database.Type, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit @@ -278,6 +301,7 @@ class SQLite(absoluteDataFolderPath: String) : Database { } override suspend fun updateDatabase( + context: CoroutineContext, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit, offlinePlayerNameToUuidOrNull: (String) -> UUID? @@ -397,8 +421,6 @@ class SQLite(absoluteDataFolderPath: String) : Database { ) } - private fun Row.toTicketIDActionPair() = int(1) to toAction() - private fun tableExists(table: String, session: Session): Boolean { return using(session.connection.underlying.metaData.getTables(null, null, table, null)) { while (it.next()) diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt index 9b95e77..ff73ef2 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt @@ -1,13 +1,14 @@ package com.github.hoshikurama.ticketmanager.common.ticket -import com.github.hoshikurama.ticketmanager.common.PluginState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.sortActions import kotlinx.coroutines.Deferred import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.withContext import java.util.* +import kotlin.coroutines.CoroutineContext open class BasicTicket( val id: Int = -1, // Ticket ID 1+... -1 placeholder during ticket creation @@ -34,12 +35,11 @@ open class BasicTicket( override fun toString() = "$world $x $y $z" } - suspend fun toFullTicketAsync(database: Database): Deferred = coroutineScope { + suspend fun toFullTicketAsync(database: Database, context: CoroutineContext): Deferred = withContext(context) { async { val sortedActions = database.getActionsAsFlow(id) .toList() - .sortedBy { it.first } - .map { it.second } + .sortedWith(sortActions) FullTicket(this@BasicTicket, sortedActions) } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt index 33fc3e6..b5b47b3 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt @@ -1,11 +1,10 @@ package com.github.hoshikurama.ticketmanager.common.ticket -import com.github.hoshikurama.ticketmanager.common.PluginState import com.github.hoshikurama.ticketmanager.common.databases.Database -import kotlinx.coroutines.Deferred import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext import java.util.* +import kotlin.coroutines.CoroutineContext class BasicTicketHandler( id: Int, @@ -31,25 +30,25 @@ class BasicTicketHandler( ) companion object { - suspend fun buildHandlerAsync(database: Database, id: Int) = coroutineScope { + suspend fun buildHandlerAsync(database: Database, id: Int, context: CoroutineContext) = withContext(context) { async { - val basicTicket = database.getBasicTicketAsync(id) - basicTicket.await()?.run { BasicTicketHandler(database, this) } + val basicTicket = database.getBasicTicket(id) + basicTicket?.run { BasicTicketHandler(database, this) } } } } suspend fun setCreatorStatusUpdate(value: Boolean) = - database.setCreatorStatusUpdateAsync(id, value) + database.setCreatorStatusUpdate(id, value) suspend fun setTicketPriority(value: Priority) = - database.setPriorityAsync(id, value) + database.setPriority(id, value) suspend fun setTicketStatus(value: Status) = - database.setStatusAsync(id, value) + database.setStatus(id, value) suspend fun setAssignedTo(value: String?) = - database.setAssignmentAsync(id, value) + database.setAssignment(id, value) - suspend fun toFullTicketAsync() = super.toFullTicketAsync(database) + suspend fun toFullTicketAsync(context: CoroutineContext) = super.toFullTicketAsync(database, context) } \ No newline at end of file From 5dabc6553d1c5824768242fa5f58d3570f5f9f01 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Sat, 10 Jul 2021 15:36:10 -0500 Subject: [PATCH 11/31] Fixed time not showing for /ticket search --- .../hoshikurama/ticketmanager/paper/events/TabComplete.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt index 6623f93..7ff2463 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/TabComplete.kt @@ -158,6 +158,7 @@ class TabComplete: Listener { "$searchWorld:", "$searchClosedBy:", "$searchLastClosedBy:", + "$searchTime:", ) } .filter { it.startsWith(curArgument) } @@ -207,8 +208,8 @@ class TabComplete: Listener { searchTimeYear ) } - .filter { curArgument[curArgument.lastIndex].digitToIntOrNull() != null } - .map { "${splitArgs[0]}:$it" } + .filter { curArgument.last().digitToIntOrNull() != null } + .map { "${splitArgs[0]}:${splitArgs[1]}$it" } } searchKeywords -> listOf(curArgument) From f531ede2b6ccc464b17455ec5381c600508f0cb8 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Sat, 10 Jul 2021 17:16:17 -0500 Subject: [PATCH 12/31] Added command job count --- .../ticketmanager/paper/Globals.kt | 47 +------------- .../paper/TicketManagerPlugin.kt | 2 +- .../ticketmanager/paper/events/Commands.kt | 63 +++++++++++++------ .../ticketmanager/common/Localization.kt | 4 ++ common/src/main/resources/locales/en_CA.yml | 2 + 5 files changed, 52 insertions(+), 66 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt index 1a5d7b7..cf424a0 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt @@ -1,46 +1 @@ -package com.github.hoshikurama.ticketmanager.paper - -import com.github.hoshikurama.ticketmanager.common.PluginState -import com.github.hoshikurama.ticketmanager.common.TMLocale -import com.github.shynixn.mccoroutine.asyncDispatcher -import net.kyori.adventure.text.Component -import org.bukkit.Bukkit -import org.bukkit.ChatColor -import org.bukkit.command.CommandSender -import org.bukkit.entity.Player -import java.util.* -import java.util.logging.Level -import kotlin.coroutines.CoroutineContext - -fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) - -internal val mainPlugin: TicketManagerPlugin - get() = TicketManagerPlugin.plugin - -internal val pluginState: PluginState - get() = mainPlugin.configState - -internal val asyncContext: CoroutineContext - get() = mainPlugin.asyncDispatcher - - -internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { - Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.localeHandler.consoleLocale)) - - Bukkit.getOnlinePlayers().asSequence() - .filter { it.has(permission) } - .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } -} - -internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) -internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true - -internal fun Player.toTMLocale() = pluginState.localeHandler.getOrDefault(locale().toString()) -internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else pluginState.localeHandler.consoleLocale - -internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null - -fun UUID?.toName(locale: TMLocale): String { - if (this == null) return locale.consoleName - return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" -} \ No newline at end of file +package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.buildComponent import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.PluginState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin internal val pluginState: PluginState get() = mainPlugin.configState internal val asyncContext: CoroutineContext get() = mainPlugin.asyncDispatcher internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers().asSequence() .filter { it.has(permission) } .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true internal fun Player.toTMLocale() = pluginState.localeHandler.getOrDefault(locale().toString()) internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else pluginState.localeHandler.consoleLocale internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null fun UUID?.toName(locale: TMLocale): String { if (this == null) return locale.consoleName return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" } fun postModifiedStacktrace(e: Exception) { val onlinePlayers = Bukkit.getOnlinePlayers() .asSequence() .filter { it.has("ticketmanager.notify.warning") } .map { it as Audience to it.toTMLocale() } val console = Bukkit.getConsoleSender() as Audience to pluginState.localeHandler.consoleLocale val playersAndConsole = onlinePlayers + sequenceOf(console) playersAndConsole.forEach { pair -> val audience = pair.first val locale = pair.second audience.sendMessage( buildComponent { // Builds header listOf( locale.stacktraceLine1, locale.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), locale.stacktraceLine3.replace("%message%", e.message ?: "?"), locale.stacktraceLine4, ) .forEach { text { formattedContent(it) } } // Adds stacktrace entries e.stackTrace .filter { it.className.startsWith("com.hoshikurama.github.ticketmanager") } .map { locale.stacktraceEntry .replace("%method%", it.methodName) .replace("%file%", it.fileName ?: "?") .replace("%line%", "${it.lineNumber}") } .forEach { text { formattedContent(it) } } } ) } } \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index 0def933..6570e9c 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -20,6 +20,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { @OptIn(ObsoleteCoroutinesApi::class) private val singleOffThread = newSingleThreadContext("SingleOffThread") + internal val jobCount = NonBlockingSync(singleOffThread, 0) internal val pluginLocked = NonBlockingSync(singleOffThread, true) internal lateinit var perms: Permission private set internal lateinit var configState: PluginState @@ -35,7 +36,6 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { pluginLocked.set(true) pluginState.database.closeDatabase() } - //TODO: KEEP TRACK OF CONTEXTS TO WAIT ON RELOADS override fun onEnable() { diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index f62760d..293db2b 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -10,12 +10,9 @@ import com.github.hoshikurama.ticketmanager.common.ticket.* import com.github.hoshikurama.ticketmanager.paper.* import com.github.shynixn.mccoroutine.SuspendingCommandExecutor import com.github.shynixn.mccoroutine.asyncDispatcher -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async +import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import net.kyori.adventure.extra.kotlin.text import net.kyori.adventure.text.Component import net.kyori.adventure.text.TextComponent @@ -68,13 +65,16 @@ class Commands : SuspendingCommandExecutor { val executeCommand = suspend { executeCommand(sender, argList, senderLocale, pseudoTicket) } try { + mainPlugin.jobCount.run { set(check() + 1) } if (notUnderCooldown.await() && isValidCommand.await() && hasValidPermission.await()) { executeCommand()?.let { pushNotifications(sender, it, senderLocale, pseudoTicket) } + mainPlugin.jobCount.run { set(check() - 1) } } } catch (e: Exception) { e.printStackTrace() - //postModifiedStacktrace(e) - //sender.sendMessage(senderLocale.warningsUnexpectedError) + postModifiedStacktrace(e) + sender.sendMessage(text { formattedContent(senderLocale.warningsUnexpectedError) }) + mainPlugin.jobCount.run { set(check() - 1) } } return@withContext true @@ -948,21 +948,46 @@ class Commands : SuspendingCommandExecutor { locale: TMLocale, ) { withContext(asyncContext) { - mainPlugin.pluginLocked.set(true) - pushMassNotify("ticketmanager.notify.info") { - text { formattedContent(it.informationReloadInitiated.replace("%user%", sender.name)) } - } + try { + mainPlugin.pluginLocked.set(true) + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationReloadInitiated.replace("%user%", sender.name)) } + } - // Eventually try making it wait for other tasks to finish. Will require keeping track of jobs - //pushMassNotify("ticketmanager.notify.info", { it.informationReloadTasksDone } ) - pluginState.database.closeDatabase() - mainPlugin.loadPlugin() + val forceQuitJob = launch { + delay(30L * 1000L) - pushMassNotify("ticketmanager.notify.info") { - text { formattedContent(it.informationReloadSuccess) } - } - if (!sender.has("ticketmanager.notify.info")) { - sender.sendMessage(text { formattedContent(locale.informationReloadSuccess) }) + // Long standing task has occurred if it reaches this point + launch { + pushMassNotify("ticketmanager.notify.warning") { + text { formattedContent(it.warningsLongTaskDuringReload) } + } + mainPlugin.jobCount.set(1) + mainPlugin.asyncDispatcher.cancelChildren() + } + } + + // Waits for other tasks to complete + while (mainPlugin.jobCount.check() > 1) delay(1000L) + + if (!forceQuitJob.isCancelled) + forceQuitJob.cancel("Tasks closed on time") + + pushMassNotify("ticketmanager.notify.info") { text { formattedContent(it.informationReloadTasksDone) } } + pluginState.database.closeDatabase() + mainPlugin.loadPlugin() + + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationReloadSuccess) } + } + if (!sender.has("ticketmanager.notify.info")) { + sender.sendMessage(text { formattedContent(locale.informationReloadSuccess) }) + } + } catch (e: Exception) { + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationReloadFailure) } + } + throw e } } } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt index 255586d..1e4e316 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Localization.kt @@ -103,6 +103,7 @@ class TMLocale( val warningsInvalidDBType: String val warningsConvertToSameDBType: String val warningsUnexpectedError: String + val warningsLongTaskDuringReload: String // Command Types val commandBase: String @@ -222,6 +223,7 @@ class TMLocale( val informationDBUpdateComplete: String val informationDBConvertInit: String val informationDBConvertSuccess: String + val informationReloadFailure: String // Modified Stacktrace val stacktraceLine1: String @@ -401,5 +403,7 @@ class TMLocale( searchClosedBy = matchOrDefault("Search_ClosedBy") searchLastClosedBy = matchOrDefault("Search_LastClosedBy") notifyPluginUpdate = matchOrDefault("Notify_Event_PluginUpdate") + warningsLongTaskDuringReload = matchOrDefault("Warning_LongTaskDuringReload") + informationReloadFailure = matchOrDefault("Info_ReloadFailure") } } \ No newline at end of file diff --git a/common/src/main/resources/locales/en_CA.yml b/common/src/main/resources/locales/en_CA.yml index 8eb6b22..2232d1a 100644 --- a/common/src/main/resources/locales/en_CA.yml +++ b/common/src/main/resources/locales/en_CA.yml @@ -63,6 +63,7 @@ Warning_TicketAlreadyOpen: '&c[TicketManager] This ticket is already open!' Warning_InvalidDBType: '&c[TicketManager] Invalid database type!' Warning_ConvertToSameDBType: '&c[TicketManager] Unable to convert database to type already in use!' Warning_UnexpectedError: '&c[TicketManager] Unexpected error has occurred!' +Warning_LongTaskDuringReload: '&c[TicketManager] Long-standing task detected during reload attempt. Forcefully ending all other tasks!' # # Modified Stacktrace Stacktrace_Line1: '%nl%%nl%%nl%&4[TicketManager] WARNING! An unexpected error has occurred!' @@ -128,6 +129,7 @@ Page_Next: 'Next' Info_ReloadInitiated: '%CC%[TicketManager]&7 %user%%CC% has initiated a plugin restart! Plugin locked and waiting for ongoing tasks to complete...' Info_Reload_TasksDone: '%CC%[TicketManager] All other tasks complete! Reloading now...' Info_ReloadSuccess: '%CC%[TicketManager] Reload&a successful%CC%!' +Info_ReloadFailure: '%CC%[TicketManager] Reload&c failure%CC%!' Info_DBUpdate: '%CC%[TicketManager] Database updating to latest version! Plugin Locked! DO NOT TURN OFF SERVER!' Info_DBUpdateComplete: '%CC%[TicketManager] Database update complete! Plugin unlocked!' Info_DBConversionInit: '%CC%[TicketManager] Database conversion from &7%fromDB%%CC% to &7%toDB%%CC%! Plugin locked!' From 9bb3cd4a95a13cda4671c7952d8ad2334fca791a Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Sun, 11 Jul 2021 11:36:44 -0500 Subject: [PATCH 13/31] Greatly optimized /ticket search --- .../paper/TicketManagerPlugin.kt | 2 +- .../ticketmanager/paper/events/Commands.kt | 366 +++++++++--------- .../common/databases/Database.kt | 6 + .../ticketmanager/common/databases/MySQL.kt | 36 ++ .../ticketmanager/common/databases/SQLite.kt | 40 ++ 5 files changed, 257 insertions(+), 193 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index 6570e9c..2264ec1 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -114,7 +114,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } } catch (e: Exception) { e.printStackTrace() - //postModifiedStacktrace(e) TODO + postModifiedStacktrace(e) } } }, 100, 12000) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index 293db2b..ec553c5 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -308,7 +308,7 @@ class Commands : SuspendingCommandExecutor { commandWordReload -> reload(sender, senderLocale).let { null } commandWordReopen -> reopen(sender,args, false, ticketHandler) commandWordSilentReopen -> reopen(sender,args, true, ticketHandler) - commandWordSearch -> search(sender, args, senderLocale).let { null } + commandWordSearch -> searchNew(sender, args, senderLocale).let { null } commandWordSetPriority -> setPriority(sender, args, false, ticketHandler) commandWordSilentSetPriority -> setPriority(sender, args, true, ticketHandler) commandWordTeleport -> teleport(sender, ticketHandler).let { null } @@ -725,7 +725,7 @@ class Commands : SuspendingCommandExecutor { } // /ticket history [User] [Page] - private suspend fun history( + private suspend fun history( //TODO UPDATE sender: CommandSender, args: List, locale: TMLocale, @@ -795,123 +795,6 @@ class Commands : SuspendingCommandExecutor { } } - private fun buildPageComponent( - curPage: Int, - pageCount: Int, - locale: TMLocale, - baseCommand: (TMLocale) -> String, - ): Component { - - fun Component.addForward(): Component { - return color(NamedTextColor.WHITE) - .clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage + 1}")) - .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickNextPage))) - } - - fun Component.addBack(): Component { - return color(NamedTextColor.WHITE) - .clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage - 1}")) - .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickBackPage))) - } - - var back: Component = Component.text("[${locale.pageBack}]") - var next: Component = Component.text("[${locale.pageNext}]") - val separator = text { - content("...............") - color(NamedTextColor.DARK_GRAY) - } - val cc = pluginState.localeHandler.mainColourCode - val ofSection = text { formattedContent("$cc($curPage${locale.pageOf}$pageCount)") } - - when (curPage) { - 1 -> { - back = back.color(NamedTextColor.DARK_GRAY) - next = next.addForward() - } - pageCount -> { - back = back.addBack() - next = next.color(NamedTextColor.DARK_GRAY) - } - else -> { - back = back.addBack() - next = next.addForward() - } - } - - return Component.text("\n") - .append(back) - .append(separator) - .append(ofSection) - .append(separator) - .append(next) - } - - private fun createListEntry( - ticket: FullTicket, - locale: TMLocale - ): Component { - val creator = ticket.creatorUUID.toName(locale) - val fixedAssign = ticket.assignedTo ?: "" - - // Shortens comment preview to fit on one line - val fixedComment = ticket.run { - if (12 + id.toString().length + creator.length + fixedAssign.length + actions[0].message!!.length > 58) - actions[0].message!!.substring( - 0, - 43 - id.toString().length - fixedAssign.length - creator.length - ) + "..." - else actions[0].message!! - } - - return text { - formattedContent( - "\n${locale.listFormatEntry}" - .replace("%priorityCC%", ticket.priority.colourCode) - .replace("%ID%", "${ticket.id}") - .replace("%creator%", creator) - .replace("%assign%", fixedAssign) - .replace("%comment%", fixedComment) - ) - onHover { showText(Component.text(locale.clickViewTicket)) } - onClick { - action = ClickEvent.Action.RUN_COMMAND - value = locale.run { "/$commandBase $commandWordView ${ticket.id}" } - } - } - } - - private suspend fun createGeneralList( - args: List, - locale: TMLocale, - headerFormat: String, - getIDPriorityPair: suspend (Database) -> Flow>, - baseCommand: (TMLocale) -> String - ): Component { - val chunkedIDs = getIDPriorityPair(pluginState.database) - .toList() - .sortedWith(compareByDescending> { it.second }.thenByDescending { it.first } ) - .map { it.first } - .chunked(8) - val page = if (args.size == 2 && args[1].toInt() in 1..chunkedIDs.size) args[1].toInt() else 1 - - val fullTickets = chunkedIDs.getOrNull(page - 1) - ?.run { pluginState.database.getFullTickets(this, asyncContext) } - ?.toList() - ?: emptyList() - - return buildComponent { - text { formattedContent(headerFormat) } - - if (fullTickets.isNotEmpty()) { - fullTickets.forEach { append(createListEntry(it, locale)) } - - if (chunkedIDs.size > 1) { - append(buildPageComponent(page, chunkedIDs.size, locale, baseCommand)) - } - } - } - } - // /ticket list [Page] private suspend fun list( sender: CommandSender, @@ -1037,13 +920,12 @@ class Commands : SuspendingCommandExecutor { ) } - // /ticket search - private suspend fun search( + private suspend fun searchNew( sender: CommandSender, args: List, locale: TMLocale, ) { - withContext(asyncContext) { + coroutineScope { fun String.attemptToUUIDString(): String? = if (equals(locale.consoleName)) null else Bukkit.getOfflinePlayers().asSequence() @@ -1051,103 +933,88 @@ class Commands : SuspendingCommandExecutor { ?.run { uniqueId.toString() } ?: "[PLAYERNOTFOUND]" - // Beginning of code execution + // Beginning of execution sender.sendMessage(text { formattedContent(locale.searchFormatQuerying) }) - val constraintTypes = locale.run { - listOf( - searchAssigned, - searchCreator, - searchKeywords, - searchPriority, - searchStatus, - searchTime, - searchWorld, - searchPage, - searchClosedBy, - searchLastClosedBy, - ) - } - val localedConstraintMap = args.subList(1, args.size) + + //todo searchPage, + + // Input args mapped to valid search types + val arguments = args.subList(1, args.size) .asSequence() .map { it.split(":", limit = 2) } - .filter { it[0] in constraintTypes } .filter { it.size >= 2 } .associate { it[0] to it[1] } - val searchFunctions = localedConstraintMap - .mapNotNull { entry -> - when (entry.key) { - locale.searchWorld -> { t: FullTicket -> t.location?.world?.equals(entry.value) ?: false } - locale.searchAssigned -> { t: FullTicket -> t.assignedTo == entry.value } - - locale.searchCreator -> { - val searchedUser = entry.value.attemptToUUIDString(); - { t: FullTicket -> t.creatorUUID?.toString() == searchedUser } + val mainTableConstrains = arguments + .mapNotNull { (key, value) -> + when (key) { + locale.searchAssigned -> key to value + locale.searchCreator -> key to value.attemptToUUIDString() + locale.searchPriority -> value.toByteOrNull()?.run { key to this.toString() } + locale.searchStatus -> { + val constraintStatus = when (value) { + locale.statusOpen -> BasicTicket.Status.OPEN.name + locale.statusClosed -> BasicTicket.Status.CLOSED.name + else -> null + } + constraintStatus?.run { key to this } + } + else -> null } + } - locale.searchPriority -> { - val searchedPriority = entry.value.toByteOrNull() ?: 0; - { t: FullTicket -> t.priority.level == searchedPriority } - } + val functionConstraints = arguments + .mapNotNull { (key, value) -> + when (key) { - locale.searchTime -> { - val creationTime = relTimeToEpochSecond(entry.value, locale); - { t: FullTicket -> t.actions[0].timestamp >= creationTime } - } + locale.searchClosedBy -> { + val searchedUser = value.attemptToUUIDString(); + { t: FullTicket -> t.actions.any{ it.type == FullTicket.Action.Type.CLOSE && it.user?.toString() == searchedUser } } + } - locale.searchStatus -> { - val constraintStatus = when (entry.value) { - locale.statusOpen -> BasicTicket.Status.OPEN.name - locale.statusClosed -> BasicTicket.Status.CLOSED.name - else -> entry.value + locale.searchLastClosedBy -> { + val searchedUser = value.attemptToUUIDString(); + { t: FullTicket -> + t.actions.lastOrNull { e -> e.type == FullTicket.Action.Type.CLOSE } + ?.run { user?.toString() == searchedUser } + ?: false + } } - { t: FullTicket -> t.status.name == constraintStatus} - } - locale.searchKeywords -> { - val words = entry.value.split(","); + locale.searchWorld -> { t: FullTicket -> t.location?.world?.equals(value) ?: false } - { t: FullTicket -> - val comments = t.actions - .filter { it.type == FullTicket.Action.Type.OPEN || it.type == FullTicket.Action.Type.COMMENT } - .map { it.message!! } - words.map { w -> comments.any { it.contains(w) } } - .all { it } + locale.searchTime -> { + val creationTime = relTimeToEpochSecond(value, locale); + { t: FullTicket -> t.actions[0].timestamp >= creationTime } } - } - locale.searchLastClosedBy -> { - val searchedUser = entry.value.attemptToUUIDString(); - { t: FullTicket -> - t.actions.lastOrNull { e -> e.type == FullTicket.Action.Type.CLOSE } - ?.run { user?.toString() == searchedUser } - ?: false - } + locale.searchKeywords -> { + val words = value.split(","); - } + { t: FullTicket -> + val comments = t.actions + .filter { it.type == FullTicket.Action.Type.OPEN || it.type == FullTicket.Action.Type.COMMENT } + .map { it.message!! } + words.map { w -> comments.any { it.lowercase().contains(w.lowercase()) } } + .all { it } + } + } - locale.searchClosedBy -> { - val searchedUser = entry.value.attemptToUUIDString(); - { t: FullTicket -> t.actions.any{ it.type == FullTicket.Action.Type.CLOSE && it.user?.toString() == searchedUser } } + else -> null } - - else -> null } - } - .asSequence() - - val composedSearch = { t: FullTicket -> searchFunctions.map { it(t) }.all { it } } + val composedSearch = { t: FullTicket -> functionConstraints.map { it(t) }.all { it } } // Results Computation val resultSize: Int - val chunkedTickets = pluginState.database.searchDatabase(asyncContext, composedSearch) + val chunkedTickets = pluginState.database.searchDatabaseNew(locale, mainTableConstrains, composedSearch) .toList() .sortedByDescending(BasicTicket::id) .apply { resultSize = size } .chunked(8) - val page = localedConstraintMap[locale.searchPage]?.toIntOrNull() + val page = arguments[locale.searchPage]?.toIntOrNull() .let { if (it != null && it >= 1 && it < chunkedTickets.size) it else 1 } val fixMSGLength = { t: FullTicket -> t.actions[0].message!!.run { if (length > 25) "${substring(0,21)}..." else this } } @@ -1192,7 +1059,7 @@ class Commands : SuspendingCommandExecutor { if (chunkedTickets.size > 1) { val pageComponent = buildPageComponent(page, chunkedTickets.size, locale) { // Removes page constraint and converts rest to key:arg - val constraints = localedConstraintMap + val constraints = arguments .filter { it.key != locale.searchPage } .map { (k, v) -> "$k:$v" } "/${locale.commandBase} ${locale.commandWordSearch} $constraints ${locale.searchPage}:" @@ -1379,7 +1246,122 @@ class Commands : SuspendingCommandExecutor { } } + private fun buildPageComponent( + curPage: Int, + pageCount: Int, + locale: TMLocale, + baseCommand: (TMLocale) -> String, + ): Component { + + fun Component.addForward(): Component { + return color(NamedTextColor.WHITE) + .clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage + 1}")) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickNextPage))) + } + + fun Component.addBack(): Component { + return color(NamedTextColor.WHITE) + .clickEvent(ClickEvent.runCommand(baseCommand(locale) + "${curPage - 1}")) + .hoverEvent(HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.text(locale.clickBackPage))) + } + + var back: Component = Component.text("[${locale.pageBack}]") + var next: Component = Component.text("[${locale.pageNext}]") + val separator = text { + content("...............") + color(NamedTextColor.DARK_GRAY) + } + val cc = pluginState.localeHandler.mainColourCode + val ofSection = text { formattedContent("$cc($curPage${locale.pageOf}$pageCount)") } + + when (curPage) { + 1 -> { + back = back.color(NamedTextColor.DARK_GRAY) + next = next.addForward() + } + pageCount -> { + back = back.addBack() + next = next.color(NamedTextColor.DARK_GRAY) + } + else -> { + back = back.addBack() + next = next.addForward() + } + } + + return Component.text("\n") + .append(back) + .append(separator) + .append(ofSection) + .append(separator) + .append(next) + } + private fun createListEntry( + ticket: FullTicket, + locale: TMLocale + ): Component { + val creator = ticket.creatorUUID.toName(locale) + val fixedAssign = ticket.assignedTo ?: "" + + // Shortens comment preview to fit on one line + val fixedComment = ticket.run { + if (12 + id.toString().length + creator.length + fixedAssign.length + actions[0].message!!.length > 58) + actions[0].message!!.substring( + 0, + 43 - id.toString().length - fixedAssign.length - creator.length + ) + "..." + else actions[0].message!! + } + + return text { + formattedContent( + "\n${locale.listFormatEntry}" + .replace("%priorityCC%", ticket.priority.colourCode) + .replace("%ID%", "${ticket.id}") + .replace("%creator%", creator) + .replace("%assign%", fixedAssign) + .replace("%comment%", fixedComment) + ) + onHover { showText(Component.text(locale.clickViewTicket)) } + onClick { + action = ClickEvent.Action.RUN_COMMAND + value = locale.run { "/$commandBase $commandWordView ${ticket.id}" } + } + } + } + + private suspend fun createGeneralList( + args: List, + locale: TMLocale, + headerFormat: String, + getIDPriorityPair: suspend (Database) -> Flow>, + baseCommand: (TMLocale) -> String + ): Component { + val chunkedIDs = getIDPriorityPair(pluginState.database) + .toList() + .sortedWith(compareByDescending> { it.second }.thenByDescending { it.first } ) + .map { it.first } + .chunked(8) + val page = if (args.size == 2 && args[1].toInt() in 1..chunkedIDs.size) args[1].toInt() else 1 + + val fullTickets = chunkedIDs.getOrNull(page - 1) + ?.run { pluginState.database.getFullTickets(this, asyncContext) } + ?.toList() + ?: emptyList() + + return buildComponent { + text { formattedContent(headerFormat) } + + if (fullTickets.isNotEmpty()) { + fullTickets.forEach { append(createListEntry(it, locale)) } + + if (chunkedIDs.size > 1) { + append(buildPageComponent(page, chunkedIDs.size, locale, baseCommand)) + } + } + } + } private fun buildTicketInfoComponent( ticket: FullTicket, diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index 6d84835..c96c951 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -1,5 +1,6 @@ package com.github.hoshikurama.ticketmanager.common.databases +import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import kotlinx.coroutines.flow.Flow @@ -44,6 +45,11 @@ interface Database { // Database searching suspend fun searchDatabase(context: CoroutineContext, searchFunction: (FullTicket) -> Boolean): Flow + suspend fun searchDatabaseNew( + locale: TMLocale, + mainTableConstraints: List>, + searchFunction: (FullTicket) -> Boolean + ): Flow // Internal Database Functions suspend fun closeDatabase() diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt index 9203ea5..e6df7cb 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt @@ -1,5 +1,6 @@ package com.github.hoshikurama.ticketmanager.common.databases +import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.hoshikurama.ticketmanager.common.byteToPriority import com.github.hoshikurama.ticketmanager.common.sortActions import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket @@ -223,6 +224,41 @@ class MySQL( .forEach{ emit(it) } } + override suspend fun searchDatabaseNew( + locale: TMLocale, + mainTableConstraints: List>, + searchFunction: (FullTicket) -> Boolean + ): Flow = flow { + val mainTableSQL = mainTableConstraints + .mapNotNull { + when (it.first) { + locale.searchAssigned -> "ASSIGNED_TO = ?" + locale.searchCreator -> "CREATOR_UUID = ?" + locale.searchPriority -> "PRIORITY = ?" + locale.searchStatus -> "STATUS = ?" + else -> null //Not relevant + } + } + .joinToString(" AND ") + + var statementSQL = "SELECT * FROM TicketManager_V4_Tickets" + if (mainTableConstraints.isNotEmpty()) + statementSQL += " WHERE $mainTableSQL" + + suspendingCon.sendPreparedStatement("$statementSQL;", mainTableConstraints.map { it.second }) + .rows + .map { it.toBasicTicket() } + .map { + coroutineScope { + async { it.toFullTicket() } + } + } + .map { it.await() } + .filter(searchFunction) + .forEach{ emit(it) } + } + + override suspend fun closeDatabase() { connectionPool.disconnect() } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index d7cc6e4..4900078 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -1,5 +1,6 @@ package com.github.hoshikurama.ticketmanager.common.databases +import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.hoshikurama.ticketmanager.common.byteToPriority import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket @@ -198,6 +199,45 @@ class SQLite(absoluteDataFolderPath: String) : Database { return matchedTickets.asFlow() } + override suspend fun searchDatabaseNew( + locale: TMLocale, + mainTableConstraints: List>, + searchFunction: (FullTicket) -> Boolean + ): Flow { + + val mainTableSQL = mainTableConstraints + .mapNotNull { + when (it.first) { + locale.searchAssigned -> "ASSIGNED_TO = ?" + locale.searchCreator -> "CREATOR_UUID = ?" + locale.searchPriority -> "PRIORITY = ?" + locale.searchStatus -> "STATUS = ?" + else -> null //Not relevant + } + } + .joinToString(" AND ") + + val inputtedArgs = mainTableConstraints + .map { it.second } + .toTypedArray() + + var statementSQL = "SELECT * FROM TicketManager_V4_Tickets" + if (mainTableConstraints.isNotEmpty()) + statementSQL += " WHERE $mainTableSQL" + + return using(getSession()) { session -> + val basicTickets = session.run( + queryOf("$statementSQL;", *inputtedArgs) + .map { it.toBasicTicket() } + .asList + ) + + basicTickets + .map { it.toFullTicket(session) } + .filter(searchFunction) + }.asFlow() + } + override suspend fun closeDatabase() { // NOT needed as database makes individual connections } From 2ba74b7088ecad2c48b5bba1deceb76fb8c249a4 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Sun, 11 Jul 2021 14:19:22 -0500 Subject: [PATCH 14/31] Search and History work with Console target --- .../ticketmanager/paper/Globals.kt | 2 +- .../ticketmanager/paper/events/Commands.kt | 25 ++++++----- .../common/databases/Database.kt | 4 +- .../ticketmanager/common/databases/MySQL.kt | 42 +++++++------------ .../ticketmanager/common/databases/SQLite.kt | 38 ++++++----------- 5 files changed, 42 insertions(+), 69 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt index cf424a0..6901a56 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt @@ -1 +1 @@ -package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.buildComponent import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.PluginState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin internal val pluginState: PluginState get() = mainPlugin.configState internal val asyncContext: CoroutineContext get() = mainPlugin.asyncDispatcher internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers().asSequence() .filter { it.has(permission) } .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true internal fun Player.toTMLocale() = pluginState.localeHandler.getOrDefault(locale().toString()) internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else pluginState.localeHandler.consoleLocale internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null fun UUID?.toName(locale: TMLocale): String { if (this == null) return locale.consoleName return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" } fun postModifiedStacktrace(e: Exception) { val onlinePlayers = Bukkit.getOnlinePlayers() .asSequence() .filter { it.has("ticketmanager.notify.warning") } .map { it as Audience to it.toTMLocale() } val console = Bukkit.getConsoleSender() as Audience to pluginState.localeHandler.consoleLocale val playersAndConsole = onlinePlayers + sequenceOf(console) playersAndConsole.forEach { pair -> val audience = pair.first val locale = pair.second audience.sendMessage( buildComponent { // Builds header listOf( locale.stacktraceLine1, locale.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), locale.stacktraceLine3.replace("%message%", e.message ?: "?"), locale.stacktraceLine4, ) .forEach { text { formattedContent(it) } } // Adds stacktrace entries e.stackTrace .filter { it.className.startsWith("com.hoshikurama.github.ticketmanager") } .map { locale.stacktraceEntry .replace("%method%", it.methodName) .replace("%file%", it.fileName ?: "?") .replace("%line%", "${it.lineNumber}") } .forEach { text { formattedContent(it) } } } ) } } \ No newline at end of file +package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.buildComponent import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.PluginState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin internal val pluginState: PluginState get() = mainPlugin.configState internal val asyncContext: CoroutineContext get() = mainPlugin.asyncDispatcher internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers().asSequence() .filter { it.has(permission) } .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true internal fun Player.toTMLocale() = pluginState.localeHandler.getOrDefault(locale().toString()) internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else pluginState.localeHandler.consoleLocale internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null fun UUID?.toName(locale: TMLocale): String { if (this == null) return locale.consoleName return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" } fun postModifiedStacktrace(e: Exception) { val onlinePlayers = Bukkit.getOnlinePlayers() .asSequence() .filter { it.has("ticketmanager.notify.warning") } .map { it as Audience to it.toTMLocale() } val console = Bukkit.getConsoleSender() as Audience to pluginState.localeHandler.consoleLocale val playersAndConsole = onlinePlayers + sequenceOf(console) playersAndConsole.forEach { pair -> val audience = pair.first val locale = pair.second audience.sendMessage( buildComponent { // Builds header listOf( locale.stacktraceLine1, locale.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), locale.stacktraceLine3.replace("%message%", e.message ?: "?"), locale.stacktraceLine4, ) .forEach { text { formattedContent(it) } } // Adds stacktrace entries e.stackTrace .filter { it.className.startsWith("com.github.hoshikurama.ticketmanager") } .map { locale.stacktraceEntry .replace("%method%", it.methodName) .replace("%file%", it.fileName ?: "?") .replace("%line%", "${it.lineNumber}") } .forEach { text { formattedContent(it) } } } ) } } \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index ec553c5..74b3694 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -308,7 +308,7 @@ class Commands : SuspendingCommandExecutor { commandWordReload -> reload(sender, senderLocale).let { null } commandWordReopen -> reopen(sender,args, false, ticketHandler) commandWordSilentReopen -> reopen(sender,args, true, ticketHandler) - commandWordSearch -> searchNew(sender, args, senderLocale).let { null } + commandWordSearch -> search(sender, args, senderLocale).let { null } commandWordSetPriority -> setPriority(sender, args, false, ticketHandler) commandWordSilentSetPriority -> setPriority(sender, args, true, ticketHandler) commandWordTeleport -> teleport(sender, ticketHandler).let { null } @@ -725,7 +725,7 @@ class Commands : SuspendingCommandExecutor { } // /ticket history [User] [Page] - private suspend fun history( //TODO UPDATE + private suspend fun history( sender: CommandSender, args: List, locale: TMLocale, @@ -736,9 +736,8 @@ class Commands : SuspendingCommandExecutor { val requestedPage = if (args.size >= 3) args[2].toInt() else 1 // Leaves console as null. Otherwise attempts UUID grab or [PLAYERNOTFOUND] - fun String.attemptToUUIDString(): String? = - if (equals(locale.consoleName)) null - else Bukkit.getOfflinePlayers().asSequence() + fun String.attemptToUUIDString(): String = + Bukkit.getOfflinePlayers().asSequence() .firstOrNull { equals(it.name) } ?.run { uniqueId.toString() } ?: "[PLAYERNOTFOUND]" @@ -746,7 +745,7 @@ class Commands : SuspendingCommandExecutor { val searchedUser = targetName?.attemptToUUIDString() val resultSize: Int - val resultsChunked = pluginState.database.searchDatabase(asyncContext) { it.creatorUUID.toString() == searchedUser } + val resultsChunked = pluginState.database.searchDatabase(asyncContext, locale, listOf(locale.searchCreator to searchedUser)) { true } .toList() .sortedByDescending(BasicTicket::id) .also { resultSize = it.size } @@ -920,12 +919,12 @@ class Commands : SuspendingCommandExecutor { ) } - private suspend fun searchNew( + private suspend fun search( sender: CommandSender, args: List, locale: TMLocale, ) { - coroutineScope { + withContext(asyncContext) { fun String.attemptToUUIDString(): String? = if (equals(locale.consoleName)) null else Bukkit.getOfflinePlayers().asSequence() @@ -936,9 +935,6 @@ class Commands : SuspendingCommandExecutor { // Beginning of execution sender.sendMessage(text { formattedContent(locale.searchFormatQuerying) }) - - //todo searchPage, - // Input args mapped to valid search types val arguments = args.subList(1, args.size) .asSequence() @@ -1004,11 +1000,14 @@ class Commands : SuspendingCommandExecutor { else -> null } } - val composedSearch = { t: FullTicket -> functionConstraints.map { it(t) }.all { it } } + val composedSearch = + if (functionConstraints.isNotEmpty()) + { t: FullTicket -> functionConstraints.map { it(t) }.all { it } } + else { _: FullTicket -> true } // Results Computation val resultSize: Int - val chunkedTickets = pluginState.database.searchDatabaseNew(locale, mainTableConstrains, composedSearch) + val chunkedTickets = pluginState.database.searchDatabase(asyncContext, locale, mainTableConstrains, composedSearch) .toList() .sortedByDescending(BasicTicket::id) .apply { resultSize = size } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index c96c951..46ad81c 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -44,8 +44,8 @@ interface Database { suspend fun getFullTickets(ids: List, context: CoroutineContext): Flow // Database searching - suspend fun searchDatabase(context: CoroutineContext, searchFunction: (FullTicket) -> Boolean): Flow - suspend fun searchDatabaseNew( + suspend fun searchDatabase( + context: CoroutineContext, locale: TMLocale, mainTableConstraints: List>, searchFunction: (FullTicket) -> Boolean diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt index e6df7cb..2931a6f 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt @@ -210,46 +210,32 @@ class MySQL( .forEach { emit(it) } } - override suspend fun searchDatabase(context: CoroutineContext, searchFunction: (FullTicket) -> Boolean): Flow = flow { - suspendingCon.sendPreparedStatement("SELECT * FROM TicketManager_V4_Tickets;") - .rows - .map { it.toBasicTicket() } - .map { - withContext(context) { - async { it.toFullTicket() } - } - } - .map { it.await() } - .filter(searchFunction) - .forEach{ emit(it) } - } - - override suspend fun searchDatabaseNew( + override suspend fun searchDatabase( + context: CoroutineContext, locale: TMLocale, mainTableConstraints: List>, searchFunction: (FullTicket) -> Boolean ): Flow = flow { - val mainTableSQL = mainTableConstraints - .mapNotNull { - when (it.first) { - locale.searchAssigned -> "ASSIGNED_TO = ?" - locale.searchCreator -> "CREATOR_UUID = ?" - locale.searchPriority -> "PRIORITY = ?" - locale.searchStatus -> "STATUS = ?" - else -> null //Not relevant - } + fun equalsOrIs(string: String?) = if (string == null) "IS NULL" else "= ?" + + val mainTableSQL = mainTableConstraints.joinToString(" AND ") { + when (it.first) { + locale.searchAssigned -> "ASSIGNED_TO ${equalsOrIs(it.second)}" + locale.searchCreator -> "CREATOR_UUID ${equalsOrIs(it.second)}" + locale.searchPriority -> "PRIORITY = ?" + locale.searchStatus -> "STATUS = ?" + else -> "" } - .joinToString(" AND ") - + } var statementSQL = "SELECT * FROM TicketManager_V4_Tickets" if (mainTableConstraints.isNotEmpty()) statementSQL += " WHERE $mainTableSQL" - suspendingCon.sendPreparedStatement("$statementSQL;", mainTableConstraints.map { it.second }) + suspendingCon.sendPreparedStatement("$statementSQL;", mainTableConstraints.mapNotNull { it.second }) .rows .map { it.toBasicTicket() } .map { - coroutineScope { + withContext(context) { async { it.toFullTicket() } } } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index 4900078..bbe91a9 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -187,38 +187,26 @@ class SQLite(absoluteDataFolderPath: String) : Database { }.asFlow() } - override suspend fun searchDatabase(context: CoroutineContext, searchFunction: (FullTicket) -> Boolean): Flow { - val matchedTickets = mutableListOf() - - using(getSession()) { session -> - session.forEach(queryOf("SELECT * FROM TicketManager_V4_Tickets")) { row -> - row.toBasicTicket().toFullTicket(session).takeIf(searchFunction)?.apply(matchedTickets::add) - } - } - - return matchedTickets.asFlow() - } - - override suspend fun searchDatabaseNew( + override suspend fun searchDatabase( + context: CoroutineContext, locale: TMLocale, mainTableConstraints: List>, searchFunction: (FullTicket) -> Boolean ): Flow { - - val mainTableSQL = mainTableConstraints - .mapNotNull { - when (it.first) { - locale.searchAssigned -> "ASSIGNED_TO = ?" - locale.searchCreator -> "CREATOR_UUID = ?" - locale.searchPriority -> "PRIORITY = ?" - locale.searchStatus -> "STATUS = ?" - else -> null //Not relevant - } + fun equalsOrIs(string: String?) = if (string == null) "IS NULL" else "= ?" + + val mainTableSQL = mainTableConstraints.joinToString(" AND ") { + when (it.first) { + locale.searchCreator -> "CREATOR_UUID ${equalsOrIs(it.second)}" + locale.searchAssigned -> "ASSIGNED_TO ${equalsOrIs(it.second)}" + locale.searchPriority -> "PRIORITY = ?" + locale.searchStatus -> "STATUS = ?" + else -> "" } - .joinToString(" AND ") + } val inputtedArgs = mainTableConstraints - .map { it.second } + .mapNotNull { it.second } .toTypedArray() var statementSQL = "SELECT * FROM TicketManager_V4_Tickets" From 29916e6e0c8828c6aeb363cf5e9336ed00491893 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Mon, 12 Jul 2021 14:50:00 -0500 Subject: [PATCH 15/31] Latest version message only shows if latest published version is higher --- Paper/build.gradle.kts | 3 + .../paper/TicketManagerPlugin.kt | 8 ++- Paper/src/main/resources/config.yml | 62 ++++++++++++++----- Paper/src/main/resources/plugin.yml | 3 + .../ticketmanager/common/PluginState.kt | 5 +- 5 files changed, 64 insertions(+), 17 deletions(-) diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts index 10d9792..1aa9682 100644 --- a/Paper/build.gradle.kts +++ b/Paper/build.gradle.kts @@ -34,6 +34,9 @@ dependencies { //implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") //implementation("org.yaml:snakeyaml:1.29") //implementation("net.kyori:adventure-api:4.8.1") + //implementation("io.lettuce:lettuce-core:6.1.3.RELEASE") + //implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.1") + //implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") } tasks { diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index 2264ec1..d45e180 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -4,6 +4,7 @@ import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.* import com.github.hoshikurama.ticketmanager.common.databases.Database import com.github.hoshikurama.ticketmanager.common.databases.MySQL +import com.github.hoshikurama.ticketmanager.common.databases.Redis import com.github.hoshikurama.ticketmanager.common.databases.SQLite import com.github.hoshikurama.ticketmanager.paper.events.Commands import com.github.hoshikurama.ticketmanager.paper.events.PlayerJoin @@ -152,6 +153,11 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { getString("MySQL_Password")!!, asyncDispatcher = (plugin.asyncDispatcher as CoroutineDispatcher), ) + Database.Type.Redis -> Redis( + absoluteDataFolderPath = path, + password = getString("Redis_Password", "default")!!, + getInt("Redis_Backup_Frequency", 300) + ) Database.Type.SQLite -> SQLite(path) } } @@ -213,7 +219,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { text { formattedContent(it.informationDBUpdateComplete) } } pluginLocked.set(true) - },//TODO ADD ONERROR + }, offlinePlayerNameToUuidOrNull = { Bukkit.getOfflinePlayers() .filter { it.name == name } diff --git a/Paper/src/main/resources/config.yml b/Paper/src/main/resources/config.yml index 4b35e08..8c9090a 100644 --- a/Paper/src/main/resources/config.yml +++ b/Paper/src/main/resources/config.yml @@ -10,9 +10,9 @@ # be found on the GitHub wiki page. # https://github.com/HoshiKurama/TicketManager # -# ##################### +# ########################################## # Locale: -# ##################### +# ########################################## # Force Locale: Forces specific language to be used for commands and responses. # Values: true, false Force_Locale: false @@ -31,9 +31,9 @@ Preferred_Locale: 'en_CA' # 'en_US' = United States English Console_Locale: 'en_CA' # -# ##################### -# Cooldown: -# ##################### +# ########################################## +# Cooldown: +# ########################################## # Use Cool-down: Determine if cool-downs should be applied to ticket commands # that create or modify a ticket. Cool-downs apply to ALL users without the # override permission. @@ -45,19 +45,45 @@ Use_Cooldowns: false # cool-downs are enabled. Cooldown_Time: 0 # -# ##################### -# Database Settings: -# ##################### -# Database mode: Type of database used. SQLite is default and can run locally -# Do NOT run MySQL unless you have a valid MySQL database! Internally defaults -# to SQLite if invalid value is used. -# Values: 'MySQL','SQLite' +# ########################################## +# Database Settings: +# ########################################## +# +# Database mode: Type of database used. Internally defaults +# to SQLite if invalid value is used. Information only needs +# to be filled out for the desired database type. +# +# SQLite: +# Stores information in the SQLite file in the TicketManager folder. +# Pros: + No setup. +# + No external database needed. +# + Faster than MySQL for search and history command. +# Cons: - Stores data in TicketManager folder. +# - Easier to overwhelm than Redis or MySQL. +# +# MySQL: +# Stores information in a database that may or may not be on the server. +# Pros: + Can handle more traffic than SQLite. +# + Can be separate from server storage. +# Cons: - Must have MySQL database. +# - Search and history commands slower. +# +# Redis: +# Stores data in memory during operation, making it incredibly fast. However, +# any server crash or power outage will result in all data being lost outside +# of the last database backup. Please DO NOT use this unless you are willing +# to accept the risks! +# Pros: + Incredibly fast. +# + Minimal setup. +# Cons: - Vulnerable to data-loss +# - Takes up RAM (Not much) +# +# Values: 'MySQL','SQLite', 'Redis' Database_Mode: 'SQLite' # # ###################### # MySQL Database # ###################### -# This section only applies when using MySQL. MySQL_Port: '' MySQL_Host: '' MySQL_DBName: '' @@ -65,8 +91,16 @@ MySQL_Username: '' MySQL_Password: '' # # ###################### -# Other +# Redis Database # ###################### +# Backup frequency in seconds +Redis_Backup_Frequency: 300 +# Password for database. Default password is 'default' +Redis_Password: 'default' +# +# ########################################### +# Other +# ########################################### # Colour Code: Colour code used to add simple colour customization. # Must be in the form "&". eg: &3, &6, &b, etc # DO NOT USE ANY COLOUR CODES OTHER THAN COLOUR! diff --git a/Paper/src/main/resources/plugin.yml b/Paper/src/main/resources/plugin.yml index b270071..c1d6833 100644 --- a/Paper/src/main/resources/plugin.yml +++ b/Paper/src/main/resources/plugin.yml @@ -16,6 +16,9 @@ libraries: - joda-time:joda-time:2.10.10 - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0 - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0 + - io.lettuce:lettuce-core:6.1.3.RELEASE + - org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.1 + - org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2 commands: ticket: description: Base for all TicketManager commands diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt index a14662a..8487aa7 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt @@ -48,9 +48,10 @@ class PluginState( val latestVersSplit = latestVersion.split(".").map(String::toInt) for (i in 0..latestVersSplit.lastIndex) { - if (curVersSplit[i] < latestVersSplit[i]) - return@async Pair(curVersion, latestVersion) + if (curVersSplit[i] > latestVersSplit[i]) + return@async null } + return@async Pair(curVersion, latestVersion) } return@async null } From 8e9237556a20832423fce3a4de487438e2af0de8 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Mon, 12 Jul 2021 14:50:35 -0500 Subject: [PATCH 16/31] Redis support testing --- build.gradle.kts | 1 + common/build.gradle.kts | 4 + .../common/databases/Database.kt | 2 +- .../ticketmanager/common/databases/Redis.kt | 373 ++++++++++++++++++ .../common/ticket/BasicTicket.kt | 2 + .../ticketmanager/common/ticket/FullTicket.kt | 27 +- 6 files changed, 407 insertions(+), 2 deletions(-) create mode 100644 common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Redis.kt diff --git a/build.gradle.kts b/build.gradle.kts index a415e17..164d38a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,5 +23,6 @@ subprojects { tasks.withType().configureEach { kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" + kotlinOptions.freeCompilerArgs += "-Xuse-experimental=kotlin.Experimental" } } \ No newline at end of file diff --git a/common/build.gradle.kts b/common/build.gradle.kts index b35eccb..1da3ccf 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + kotlin("plugin.serialization") version "1.5.20" kotlin("jvm") java } @@ -21,4 +22,7 @@ dependencies { implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") implementation("org.yaml:snakeyaml:1.29") implementation("joda-time:joda-time:2.10.10") + implementation("io.lettuce:lettuce-core:6.1.3.RELEASE") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") } \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index 46ad81c..0b30642 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -11,7 +11,7 @@ interface Database { val type: Type enum class Type { - MySQL, SQLite + MySQL, SQLite, Redis } // Individual property getters diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Redis.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Redis.kt new file mode 100644 index 0000000..b7d604c --- /dev/null +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Redis.kt @@ -0,0 +1,373 @@ +package com.github.hoshikurama.ticketmanager.common.databases + +import com.github.hoshikurama.ticketmanager.common.TMLocale +import com.github.hoshikurama.ticketmanager.common.byteToPriority +import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket +import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket +import io.lettuce.core.ExperimentalLettuceCoroutinesApi +import io.lettuce.core.KeyValue +import io.lettuce.core.RedisClient +import io.lettuce.core.RedisURI +import io.lettuce.core.api.StatefulRedisConnection +import io.lettuce.core.api.coroutines +import io.lettuce.core.api.coroutines.RedisCoroutinesCommands +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.util.* +import kotlin.coroutines.CoroutineContext + +@OptIn(ExperimentalLettuceCoroutinesApi::class) +class Redis( + absoluteDataFolderPath: String, + password: String, + saveFrequency: Int +) : Database { + private val client: RedisClient + private val connection: StatefulRedisConnection + private val redis: RedisCoroutinesCommands + private val idAssigner: RedisIDAssignment + + override val type = Database.Type.Redis + + init { + val redisUri = RedisURI.Builder.redis("localhost") + .withPassword(password.toCharArray()) + .withDatabase(1) + .build() + client = RedisClient.create(redisUri) + connection = client.connect() + redis = connection.coroutines() + + idAssigner = RedisIDAssignment(redis) + + connection.sync().configSet("save", "$saveFrequency") + connection.sync().configSet("dir", "$absoluteDataFolderPath/Redis") + connection.sync().configRewrite() + } + + @OptIn(DelicateCoroutinesApi::class) + class RedisIDAssignment(private val cc: RedisCoroutinesCommands) { + private val channel = Channel() + private var counter = 0 + + init { + GlobalScope.launch { + counter = cc.keys("*") + .map { it.toInt() } + .toList() + .maxOrNull()!! + + + while (true) { + counter++ + channel.send(counter) + } + } + } + + suspend fun getAndIncrement() = channel.receive() + } + + internal object TicketHelper { + private inline fun kotlinDecode(json: String) = Json.decodeFromString(json) + private inline fun kotlinEncode(t: T) = Json.encodeToString(t) + + private inline fun T?.ifNotNullThen(f: T.() -> String) = if (this == null) "~NULL~" else f(this) + private inline fun String.rebuild(f: String.() -> T) = if (this == "~NULL~") null else f(this) + + // CREATOR_UUID + fun creatorUuidAsString(uuid: UUID?) = uuid.ifNotNullThen(UUID::toString) + fun stringToCreatorUUID(s: String) = s.rebuild(UUID::fromString) + + // PRIORITY + fun priorityAsString(priority: BasicTicket.Priority) = priority.level.toString() + fun stringToPriority(s: String) = byteToPriority(s.toByte()) + + // STATUS + fun statusAsString(status: BasicTicket.Status) = status.name + fun stringToStatus(s: String) = BasicTicket.Status.valueOf(s) + + // ASSIGNED_TO + fun assignmentAsString(assignment: String?) = assignment.ifNotNullThen { this } + fun stringAsAssignment(s: String) = s.rebuild { this } + + // STATUS_UPDATE_FOR_CREATOR + fun creatorUpdateAsString(creatorUpdate: Boolean) = if (creatorUpdate) "T" else "F" + fun stringToCreatorUpdate(s: String) = s == "T" + + // LOCATION + fun locationAsString(location: BasicTicket.TicketLocation?) = location.ifNotNullThen { kotlinEncode(this) } + fun stringToLocation(s: String) : BasicTicket.TicketLocation? = s.rebuild { kotlinDecode(s) } + + // ACTIONS + fun actionListAsString(list: List) = kotlinEncode(list) + fun stringToActionList(s: String): List = kotlinDecode(s) + } + + + override suspend fun getActionsAsFlow(ticketID: Int): Flow = flow { + redis.hget("$ticketID", "ACTIONS") + ?.let(TicketHelper::stringToActionList) + ?.forEach { emit(it) } + } + + override suspend fun setAssignment(ticketID: Int, assignment: String?) { + redis.hset("$ticketID", "ASSIGNED_TO", TicketHelper.assignmentAsString(assignment)) + } + + override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { + redis.hset("$ticketID", "STATUS_UPDATE_FOR_CREATOR", TicketHelper.creatorUpdateAsString(status)) + } + + override suspend fun setPriority(ticketID: Int, priority: BasicTicket.Priority) { + redis.hset("$ticketID", "PRIORITY", TicketHelper.priorityAsString(priority)) + } + + override suspend fun setStatus(ticketID: Int, status: BasicTicket.Status) { + redis.hset("$ticketID", "STATUS", TicketHelper.statusAsString(status)) + } + + // NOTE: This function actually returns a FullTicket? + override suspend fun getBasicTicket(ticketID: Int): BasicTicket? { + val response = redis.hgetall("$ticketID") + .toList() + + return if (response.isEmpty()) null else { + BasicTicket( + id = ticketID, + creatorUUID = response.getValueFromKey("CREATOR_UUID").run(TicketHelper::stringToCreatorUUID), + location = response.getValueFromKey("LOCATION").run(TicketHelper::stringToLocation), + priority = response.getValueFromKey("PRIORITY").run(TicketHelper::stringToPriority), + status = response.getValueFromKey("STATUS").run(TicketHelper::stringToStatus), + assignedTo = response.getValueFromKey("ASSIGNED_TO").run(TicketHelper::stringAsAssignment), + creatorStatusUpdate = response.getValueFromKey("STATUS_UPDATE_FOR_CREATOR").run(TicketHelper::stringToCreatorUpdate) + ) + } + } + + override suspend fun addAction(ticketID: Int, action: FullTicket.Action) { + redis.hget("$ticketID", "ACTIONS") + ?.run(TicketHelper::stringToActionList) + ?.let { it + action } + ?.run(TicketHelper::actionListAsString) + ?.let { redis.hset("$ticketID", mapOf("ACTIONS" to it)) } + } + + override suspend fun addFullTicket(fullTicket: FullTicket) { + val ticketID = idAssigner.getAndIncrement() + + redis.hset("$ticketID", + mapOf( + "CREATOR_UUID" to fullTicket.creatorUUID.run(TicketHelper::creatorUuidAsString), + "PRIORITY" to fullTicket.priority.run(TicketHelper::priorityAsString), + "STATUS" to fullTicket.status.run(TicketHelper::statusAsString), + "ASSIGNED_TO" to fullTicket.assignedTo.run(TicketHelper::assignmentAsString), + "STATUS_UPDATE_FOR_CREATOR" to fullTicket.creatorStatusUpdate.run(TicketHelper::creatorUpdateAsString), + "LOCATION" to fullTicket.location.run(TicketHelper::locationAsString), + "ACTIONS" to fullTicket.actions.run(TicketHelper::actionListAsString), + ) + ) + } + + override suspend fun addNewTicket(basicTicket: BasicTicket, context: CoroutineContext, message: String): Int { + val actions = listOf(FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message)) + val ticketID = idAssigner.getAndIncrement() + + withContext(context) { + launch { + redis.hset("$ticketID", + mapOf( + "CREATOR_UUID" to basicTicket.creatorUUID.run(TicketHelper::creatorUuidAsString), + "PRIORITY" to basicTicket.priority.run(TicketHelper::priorityAsString), + "STATUS" to basicTicket.status.run(TicketHelper::statusAsString), + "ASSIGNED_TO" to basicTicket.assignedTo.run(TicketHelper::assignmentAsString), + "STATUS_UPDATE_FOR_CREATOR" to basicTicket.creatorStatusUpdate.run(TicketHelper::creatorUpdateAsString), + "LOCATION" to basicTicket.location.run(TicketHelper::locationAsString), + "ACTIONS" to actions.run(TicketHelper::actionListAsString) + ) + ) + } + } + + return ticketID + } + + override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?, context: CoroutineContext) { + (lowerBound..upperBound).forEach { id -> + withContext(context) { + launch { + val response = redis.hgetall("$id").toList() + if (response.isEmpty()) return@launch + + val status = response.getValueFromKey("STATUS").run(TicketHelper::stringToStatus) + if (status != BasicTicket.Status.OPEN) return@launch + + val actions = response.getValueFromKey("ACTIONS").run(TicketHelper::stringToActionList).toMutableList() + actions += FullTicket.Action(FullTicket.Action.Type.MASS_CLOSE, uuid) + + redis.hset("$id", + mapOf( + "STATUS" to BasicTicket.Status.CLOSED.run(TicketHelper::statusAsString), + "ACTIONS" to actions.run(TicketHelper::actionListAsString), + ) + ) + } + } + } + } + + override suspend fun getOpenIDPriorityPairs(): Flow> = flow { + redis.keys("*") + .buffer(1000) + .collect { + val response = redis.hgetall(it).toList() + val priority = response.getValueFromKey("PRIORITY").run(TicketHelper::stringToPriority) + val status = response.getValueFromKey("STATUS").run(TicketHelper::stringToStatus) + + if (status == BasicTicket.Status.OPEN) + emit(it.toInt() to priority.level) + } + } + + override suspend fun getAssignedOpenIDPriorityPairs( + assignment: String, + unfixedGroupAssignment: List + ): Flow> = flow { + val fixedAssignments = unfixedGroupAssignment.map { "::$it" } + + redis.keys("*") + .buffer(1000) + .collect { + val response = redis.hgetall(it).toList() + val priority = response.getValueFromKey("PRIORITY").run(TicketHelper::stringToPriority) + val status = response.getValueFromKey("STATUS").run(TicketHelper::stringToStatus) + val assigned = response.getValueFromKey("ASSIGNED_TO").run(TicketHelper::stringAsAssignment) + + if (status == BasicTicket.Status.OPEN && assigned?.run { it == assignment || it in fixedAssignments } == true) + emit(it.toInt() to priority.level) + } + } + + override suspend fun getIDsWithUpdates(): Flow = flow { + redis.keys("*") + .buffer(1000) + .collect { + val hasUpdate = redis.hget(it, "STATUS_UPDATE_FOR_CREATOR")!!.run(TicketHelper::stringToCreatorUpdate) + + if (hasUpdate) + emit(it.toInt()) + } + } + + override suspend fun getIDsWithUpdatesFor(uuid: UUID): Flow = flow { + redis.keys("*") + .buffer(1000) + .collect { + val response = redis.hgetall(it).toList() + val hasUpdate = response.getValueFromKey("STATUS_UPDATE_FOR_CREATOR").run(TicketHelper::stringToCreatorUpdate) + val creatorUUID = response.getValueFromKey("CREATOR_UUID").run(TicketHelper::stringToCreatorUUID) + + if (creatorUUID?.run { it.equals(uuid) } == true && hasUpdate) + emit(it.toInt()) + } + } + + override suspend fun getBasicTickets(ids: List): Flow = flow { + ids.forEach { id -> + val ticket = getBasicTicket(id) + if (ticket != null) emit(ticket) + } + } + + // getBasicTicket returns FullTicket? Safe to type cast + override suspend fun getFullTicketsFromBasics( + basicTickets: List, + context: CoroutineContext + ): Flow = flow { + basicTickets.forEach { + (it as? FullTicket)?.apply { emit(this) } + } + } + + override suspend fun getFullTickets(ids: List, context: CoroutineContext): Flow = flow { + ids.forEach { + emit(getBasicTicket(it) as FullTicket) + } + } + + override suspend fun searchDatabase( + context: CoroutineContext, + locale: TMLocale, + mainTableConstraints: List>, + searchFunction: (FullTicket) -> Boolean + ): Flow { + val mainToFunction = mainTableConstraints.mapNotNull { (word, arg) -> + when (word) { + locale.searchAssigned -> { t: FullTicket -> t.assignedTo == arg } + locale.searchCreator -> { + val uuid = arg?.run(UUID::fromString); + { t: FullTicket -> t.creatorUUID == uuid } + } + locale.searchPriority -> { + val priority = arg!!.toByte().run(::byteToPriority); + { t: FullTicket -> t.priority == priority } + } + locale.searchStatus -> { + val status = BasicTicket.Status.valueOf(arg!!); + { t: FullTicket -> t.status == status } + } + else -> null + } + } + val newSearchFunction = { t: FullTicket -> mainToFunction.all { it(t) } && searchFunction(t) } + + return flow { + redis.keys("*").collect { id -> + val fullTicket = getBasicTicket(id.toInt()) as FullTicket + + if (newSearchFunction(fullTicket)) + emit(fullTicket) + } + } + } + + override suspend fun closeDatabase() { + connection.close() + client.shutdown() + } + + override suspend fun initialiseDatabase() { + // Database already initialized from instantiation + } + + override suspend fun updateNeeded(): Boolean { + return false // Introduced as V4 Database in TM5 + } + + override suspend fun migrateDatabase( + context: CoroutineContext, + to: Database.Type, + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit + ) { + TODO("Not yet implemented") + } + + override suspend fun updateDatabase( + context: CoroutineContext, + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit, + offlinePlayerNameToUuidOrNull: (String) -> UUID? + ) { + // N/A (Introduced as V4 Database in TM5) + } +} + +private fun List>.getValueFromKey(key: String) = first { it.key == key }.value!! \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt index ff73ef2..7408776 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable import java.util.* import kotlin.coroutines.CoroutineContext @@ -31,6 +32,7 @@ open class BasicTicket( OPEN("&a"), CLOSED("&c") } + @Serializable data class TicketLocation(val world: String, val x: Int, val y: Int, val z: Int) { override fun toString() = "$world $x $y $z" } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt index 6f55fbe..9e84292 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt @@ -1,5 +1,14 @@ package com.github.hoshikurama.ticketmanager.common.ticket +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import java.time.Instant import java.util.* @@ -25,9 +34,25 @@ class FullTicket( basicTicket.creatorStatusUpdate ) - data class Action(val type: Type, val user: UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { + @Serializable + data class Action(val type: Type, val user: @Serializable(with = UUIDSerializer::class) UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { enum class Type { ASSIGN, CLOSE, COMMENT, OPEN, REOPEN, SET_PRIORITY, MASS_CLOSE } } +} + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = UUID::class) +object UUIDSerializer : KSerializer { + override val descriptor: SerialDescriptor + get() = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: UUID) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): UUID { + return UUID.fromString(decoder.decodeString()) + } } \ No newline at end of file From 97f0e917d396a46781b5db789a70950ac857f749 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Mon, 12 Jul 2021 15:33:28 -0500 Subject: [PATCH 17/31] Fix for SQLite --- .../ticketmanager/common/databases/SQLite.kt | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index bbe91a9..498c818 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -6,8 +6,7 @@ import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import com.github.hoshikurama.ticketmanager.common.ticket.toTicketLocation import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.* import kotliquery.* import java.sql.DriverManager import java.time.Instant @@ -23,7 +22,7 @@ class SQLite(absoluteDataFolderPath: String) : Database { private fun getSession() = Session(Connection(DriverManager.getConnection(url))) override suspend fun getActionsAsFlow(ticketID: Int): Flow { - return using(getSession()) { getActions(ticketID, it) } + return using(getSession()) { getActions(ticketID) } .asFlow() } @@ -52,7 +51,13 @@ class SQLite(absoluteDataFolderPath: String) : Database { } override suspend fun getBasicTicket(ticketID: Int): BasicTicket? { - return using(getSession()) { getBasicTicket(ticketID, it) } + return using(getSession()) { session1 -> + session1.run( + queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") + .map { it.toBasicTicket() } + .asSingle + ) + } } override suspend fun addAction(ticketID: Int, action: FullTicket.Action) { @@ -172,19 +177,17 @@ class SQLite(absoluteDataFolderPath: String) : Database { basicTickets: List, context: CoroutineContext ): Flow { - return using(getSession()) { session -> - basicTickets - .map { it.toFullTicket(session) } + return basicTickets + .map { it.toFullTicket() } .asFlow() - } } override suspend fun getFullTickets(ids: List, context: CoroutineContext): Flow { - return using(getSession()) { session -> - ids.asSequence() - .mapNotNull { getBasicTicket(it, session) } - .map { it.toFullTicket(session) } - }.asFlow() + return ids.asFlow() + .mapNotNull { getBasicTicket(it) } + .map { it.toFullTicket() } + .toList() + .asFlow() } override suspend fun searchDatabase( @@ -221,7 +224,7 @@ class SQLite(absoluteDataFolderPath: String) : Database { ) basicTickets - .map { it.toFullTicket(session) } + .map { it.toFullTicket() } .filter(searchFunction) }.asFlow() } @@ -384,15 +387,6 @@ class SQLite(absoluteDataFolderPath: String) : Database { } - - private fun getBasicTicket(ticketID: Int, session: Session): BasicTicket? { - return session.run( - queryOf("SELECT * FROM TicketManager_V4_Tickets WHERE ID = $ticketID;") - .map { it.toBasicTicket() } - .asSingle - ) - } - private fun writeTicket(ticket: BasicTicket, session: Session): Long? { return session.run(queryOf("INSERT INTO TicketManager_V4_Tickets (CREATOR_UUID, PRIORITY, STATUS, ASSIGNED_TO, STATUS_UPDATE_FOR_CREATOR, LOCATION) VALUES (?,?,?,?,?,?);", ticket.creatorUUID, @@ -442,11 +436,13 @@ class SQLite(absoluteDataFolderPath: String) : Database { ) } - private fun getActions(ticketID: Int, session: Session): List { - return session.run(queryOf("SELECT ACTION_ID, ACTION_TYPE, CREATOR_UUID, MESSAGE, TIMESTAMP FROM TicketManager_V4_Actions WHERE TICKET_ID = $ticketID;") - .map { row -> row.toAction() } - .asList - ) + private fun getActions(ticketID: Int): List { + return using(getSession()) { session -> + session.run(queryOf("SELECT ACTION_ID, ACTION_TYPE, CREATOR_UUID, MESSAGE, TIMESTAMP FROM TicketManager_V4_Actions WHERE TICKET_ID = $ticketID;") + .map { row -> row.toAction() } + .asList + ) + } } private fun tableExists(table: String, session: Session): Boolean { @@ -457,5 +453,5 @@ class SQLite(absoluteDataFolderPath: String) : Database { } } - private fun BasicTicket.toFullTicket(session: Session) = FullTicket(this, getActions(id, session)) + private fun BasicTicket.toFullTicket() = FullTicket(this, getActions(id)) } \ No newline at end of file From 1d0694fc0735b66dac4300193819efbb34789a08 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Mon, 12 Jul 2021 15:33:55 -0500 Subject: [PATCH 18/31] Tweak to modified stack trace --- common/src/main/resources/locales/en_CA.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/resources/locales/en_CA.yml b/common/src/main/resources/locales/en_CA.yml index 2232d1a..fd38c7d 100644 --- a/common/src/main/resources/locales/en_CA.yml +++ b/common/src/main/resources/locales/en_CA.yml @@ -67,8 +67,8 @@ Warning_LongTaskDuringReload: '&c[TicketManager] Long-standing task detected dur # # Modified Stacktrace Stacktrace_Line1: '%nl%%nl%%nl%&4[TicketManager] WARNING! An unexpected error has occurred!' -Stacktrace_Line2: '&c%nl% Exception: &7%exception%' -Stacktrace_Line3: '&c%nl% Information: %message%' +Stacktrace_Line2: '&c%nl% Exception: &7%exception%' +Stacktrace_Line3: '&c%nl% Information: &7%message%' Stacktrace_Line4: '&4%nl%=-=-=-=-=-=-=Modified Stacktrace:=-=-=-=-=-=-=' Stacktrace_Entry: '&c%nl% %method% (%file%:%line%)' # From 9bb681a62f30474cf38e7489ae8a82d6c7d09840 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Mon, 12 Jul 2021 18:26:00 -0500 Subject: [PATCH 19/31] Dropping Redis --- Paper/build.gradle.kts | 2 +- .../paper/TicketManagerPlugin.kt | 6 - Paper/src/main/resources/config.yml | 20 +- Paper/src/main/resources/plugin.yml | 7 +- common/build.gradle.kts | 3 +- .../ticketmanager/common/PluginState.kt | 2 +- .../common/databases/Database.kt | 2 +- .../ticketmanager/common/databases/Redis.kt | 373 ------------------ .../common/ticket/BasicTicket.kt | 2 - .../ticketmanager/common/ticket/FullTicket.kt | 27 +- 10 files changed, 8 insertions(+), 436 deletions(-) delete mode 100644 common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Redis.kt diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts index 1aa9682..5560d14 100644 --- a/Paper/build.gradle.kts +++ b/Paper/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { compileOnly("io.papermc.paper:paper-api:1.17.1-R0.1-SNAPSHOT") implementation(kotlin("stdlib", version = "1.5.20")) implementation("com.github.HoshiKurama:KyoriComponentDSL:1.1.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1") implementation("net.kyori:adventure-extra-kotlin:4.8.1") implementation("joda-time:joda-time:2.10.10") compileOnly("com.github.MilkBowl:VaultAPI:1.7") diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index d45e180..4fc9420 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -4,7 +4,6 @@ import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.* import com.github.hoshikurama.ticketmanager.common.databases.Database import com.github.hoshikurama.ticketmanager.common.databases.MySQL -import com.github.hoshikurama.ticketmanager.common.databases.Redis import com.github.hoshikurama.ticketmanager.common.databases.SQLite import com.github.hoshikurama.ticketmanager.paper.events.Commands import com.github.hoshikurama.ticketmanager.paper.events.PlayerJoin @@ -153,11 +152,6 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { getString("MySQL_Password")!!, asyncDispatcher = (plugin.asyncDispatcher as CoroutineDispatcher), ) - Database.Type.Redis -> Redis( - absoluteDataFolderPath = path, - password = getString("Redis_Password", "default")!!, - getInt("Redis_Backup_Frequency", 300) - ) Database.Type.SQLite -> SQLite(path) } } diff --git a/Paper/src/main/resources/config.yml b/Paper/src/main/resources/config.yml index 8c9090a..fecbc82 100644 --- a/Paper/src/main/resources/config.yml +++ b/Paper/src/main/resources/config.yml @@ -68,17 +68,7 @@ Cooldown_Time: 0 # Cons: - Must have MySQL database. # - Search and history commands slower. # -# Redis: -# Stores data in memory during operation, making it incredibly fast. However, -# any server crash or power outage will result in all data being lost outside -# of the last database backup. Please DO NOT use this unless you are willing -# to accept the risks! -# Pros: + Incredibly fast. -# + Minimal setup. -# Cons: - Vulnerable to data-loss -# - Takes up RAM (Not much) -# -# Values: 'MySQL','SQLite', 'Redis' +# Values: 'MySQL','SQLite' Database_Mode: 'SQLite' # # ###################### @@ -90,14 +80,6 @@ MySQL_DBName: '' MySQL_Username: '' MySQL_Password: '' # -# ###################### -# Redis Database -# ###################### -# Backup frequency in seconds -Redis_Backup_Frequency: 300 -# Password for database. Default password is 'default' -Redis_Password: 'default' -# # ########################################### # Other # ########################################### diff --git a/Paper/src/main/resources/plugin.yml b/Paper/src/main/resources/plugin.yml index c1d6833..df6cb9c 100644 --- a/Paper/src/main/resources/plugin.yml +++ b/Paper/src/main/resources/plugin.yml @@ -8,17 +8,14 @@ libraries: - org.jetbrains.kotlin:kotlin-stdlib:1.5.20 - mysql:mysql-connector-java:8.0.25 - org.xerial:sqlite-jdbc:3.34.0 - - com.github.jasync-sql:jasync-mysql:1.1.6 + - com.github.jasync-sql:jasync-mysql:1.2.2 - com.github.seratch:kotliquery:1.3.1 - - org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0 + - org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 - net.kyori:adventure-extra-kotlin:4.8.1 - net.kyori:adventure-text-serializer-legacy:4.8.1 - joda-time:joda-time:2.10.10 - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0 - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0 - - io.lettuce:lettuce-core:6.1.3.RELEASE - - org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.1 - - org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2 commands: ticket: description: Base for all TicketManager commands diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 1da3ccf..43ec158 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,5 +1,4 @@ plugins { - kotlin("plugin.serialization") version "1.5.20" kotlin("jvm") java } @@ -16,7 +15,7 @@ dependencies { implementation("com.github.jasync-sql:jasync-mysql:1.2.2") implementation("com.github.seratch:kotliquery:1.3.1") implementation("com.github.HoshiKurama:KyoriComponentDSL:1.1.0") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1") implementation("net.kyori:adventure-api:4.8.1") implementation("net.kyori:adventure-extra-kotlin:4.8.1") implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt index 8487aa7..8ac4f0a 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt @@ -114,7 +114,7 @@ inline fun tryOrDefault(attempted: () -> T?, default: T): T = inline fun tryOrNull(function: () -> T): T? = try { function() } - catch (ignored: Exception) { null } + catch (e: Exception) { e.printStackTrace(); null } suspend inline fun tryOrDefaultSuspend(crossinline attempted: suspend () -> T?, default: T): T = tryOrNullSuspend(attempted).run { this ?: default } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index 0b30642..46ad81c 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -11,7 +11,7 @@ interface Database { val type: Type enum class Type { - MySQL, SQLite, Redis + MySQL, SQLite } // Individual property getters diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Redis.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Redis.kt deleted file mode 100644 index b7d604c..0000000 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Redis.kt +++ /dev/null @@ -1,373 +0,0 @@ -package com.github.hoshikurama.ticketmanager.common.databases - -import com.github.hoshikurama.ticketmanager.common.TMLocale -import com.github.hoshikurama.ticketmanager.common.byteToPriority -import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket -import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket -import io.lettuce.core.ExperimentalLettuceCoroutinesApi -import io.lettuce.core.KeyValue -import io.lettuce.core.RedisClient -import io.lettuce.core.RedisURI -import io.lettuce.core.api.StatefulRedisConnection -import io.lettuce.core.api.coroutines -import io.lettuce.core.api.coroutines.RedisCoroutinesCommands -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import java.util.* -import kotlin.coroutines.CoroutineContext - -@OptIn(ExperimentalLettuceCoroutinesApi::class) -class Redis( - absoluteDataFolderPath: String, - password: String, - saveFrequency: Int -) : Database { - private val client: RedisClient - private val connection: StatefulRedisConnection - private val redis: RedisCoroutinesCommands - private val idAssigner: RedisIDAssignment - - override val type = Database.Type.Redis - - init { - val redisUri = RedisURI.Builder.redis("localhost") - .withPassword(password.toCharArray()) - .withDatabase(1) - .build() - client = RedisClient.create(redisUri) - connection = client.connect() - redis = connection.coroutines() - - idAssigner = RedisIDAssignment(redis) - - connection.sync().configSet("save", "$saveFrequency") - connection.sync().configSet("dir", "$absoluteDataFolderPath/Redis") - connection.sync().configRewrite() - } - - @OptIn(DelicateCoroutinesApi::class) - class RedisIDAssignment(private val cc: RedisCoroutinesCommands) { - private val channel = Channel() - private var counter = 0 - - init { - GlobalScope.launch { - counter = cc.keys("*") - .map { it.toInt() } - .toList() - .maxOrNull()!! - - - while (true) { - counter++ - channel.send(counter) - } - } - } - - suspend fun getAndIncrement() = channel.receive() - } - - internal object TicketHelper { - private inline fun kotlinDecode(json: String) = Json.decodeFromString(json) - private inline fun kotlinEncode(t: T) = Json.encodeToString(t) - - private inline fun T?.ifNotNullThen(f: T.() -> String) = if (this == null) "~NULL~" else f(this) - private inline fun String.rebuild(f: String.() -> T) = if (this == "~NULL~") null else f(this) - - // CREATOR_UUID - fun creatorUuidAsString(uuid: UUID?) = uuid.ifNotNullThen(UUID::toString) - fun stringToCreatorUUID(s: String) = s.rebuild(UUID::fromString) - - // PRIORITY - fun priorityAsString(priority: BasicTicket.Priority) = priority.level.toString() - fun stringToPriority(s: String) = byteToPriority(s.toByte()) - - // STATUS - fun statusAsString(status: BasicTicket.Status) = status.name - fun stringToStatus(s: String) = BasicTicket.Status.valueOf(s) - - // ASSIGNED_TO - fun assignmentAsString(assignment: String?) = assignment.ifNotNullThen { this } - fun stringAsAssignment(s: String) = s.rebuild { this } - - // STATUS_UPDATE_FOR_CREATOR - fun creatorUpdateAsString(creatorUpdate: Boolean) = if (creatorUpdate) "T" else "F" - fun stringToCreatorUpdate(s: String) = s == "T" - - // LOCATION - fun locationAsString(location: BasicTicket.TicketLocation?) = location.ifNotNullThen { kotlinEncode(this) } - fun stringToLocation(s: String) : BasicTicket.TicketLocation? = s.rebuild { kotlinDecode(s) } - - // ACTIONS - fun actionListAsString(list: List) = kotlinEncode(list) - fun stringToActionList(s: String): List = kotlinDecode(s) - } - - - override suspend fun getActionsAsFlow(ticketID: Int): Flow = flow { - redis.hget("$ticketID", "ACTIONS") - ?.let(TicketHelper::stringToActionList) - ?.forEach { emit(it) } - } - - override suspend fun setAssignment(ticketID: Int, assignment: String?) { - redis.hset("$ticketID", "ASSIGNED_TO", TicketHelper.assignmentAsString(assignment)) - } - - override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { - redis.hset("$ticketID", "STATUS_UPDATE_FOR_CREATOR", TicketHelper.creatorUpdateAsString(status)) - } - - override suspend fun setPriority(ticketID: Int, priority: BasicTicket.Priority) { - redis.hset("$ticketID", "PRIORITY", TicketHelper.priorityAsString(priority)) - } - - override suspend fun setStatus(ticketID: Int, status: BasicTicket.Status) { - redis.hset("$ticketID", "STATUS", TicketHelper.statusAsString(status)) - } - - // NOTE: This function actually returns a FullTicket? - override suspend fun getBasicTicket(ticketID: Int): BasicTicket? { - val response = redis.hgetall("$ticketID") - .toList() - - return if (response.isEmpty()) null else { - BasicTicket( - id = ticketID, - creatorUUID = response.getValueFromKey("CREATOR_UUID").run(TicketHelper::stringToCreatorUUID), - location = response.getValueFromKey("LOCATION").run(TicketHelper::stringToLocation), - priority = response.getValueFromKey("PRIORITY").run(TicketHelper::stringToPriority), - status = response.getValueFromKey("STATUS").run(TicketHelper::stringToStatus), - assignedTo = response.getValueFromKey("ASSIGNED_TO").run(TicketHelper::stringAsAssignment), - creatorStatusUpdate = response.getValueFromKey("STATUS_UPDATE_FOR_CREATOR").run(TicketHelper::stringToCreatorUpdate) - ) - } - } - - override suspend fun addAction(ticketID: Int, action: FullTicket.Action) { - redis.hget("$ticketID", "ACTIONS") - ?.run(TicketHelper::stringToActionList) - ?.let { it + action } - ?.run(TicketHelper::actionListAsString) - ?.let { redis.hset("$ticketID", mapOf("ACTIONS" to it)) } - } - - override suspend fun addFullTicket(fullTicket: FullTicket) { - val ticketID = idAssigner.getAndIncrement() - - redis.hset("$ticketID", - mapOf( - "CREATOR_UUID" to fullTicket.creatorUUID.run(TicketHelper::creatorUuidAsString), - "PRIORITY" to fullTicket.priority.run(TicketHelper::priorityAsString), - "STATUS" to fullTicket.status.run(TicketHelper::statusAsString), - "ASSIGNED_TO" to fullTicket.assignedTo.run(TicketHelper::assignmentAsString), - "STATUS_UPDATE_FOR_CREATOR" to fullTicket.creatorStatusUpdate.run(TicketHelper::creatorUpdateAsString), - "LOCATION" to fullTicket.location.run(TicketHelper::locationAsString), - "ACTIONS" to fullTicket.actions.run(TicketHelper::actionListAsString), - ) - ) - } - - override suspend fun addNewTicket(basicTicket: BasicTicket, context: CoroutineContext, message: String): Int { - val actions = listOf(FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message)) - val ticketID = idAssigner.getAndIncrement() - - withContext(context) { - launch { - redis.hset("$ticketID", - mapOf( - "CREATOR_UUID" to basicTicket.creatorUUID.run(TicketHelper::creatorUuidAsString), - "PRIORITY" to basicTicket.priority.run(TicketHelper::priorityAsString), - "STATUS" to basicTicket.status.run(TicketHelper::statusAsString), - "ASSIGNED_TO" to basicTicket.assignedTo.run(TicketHelper::assignmentAsString), - "STATUS_UPDATE_FOR_CREATOR" to basicTicket.creatorStatusUpdate.run(TicketHelper::creatorUpdateAsString), - "LOCATION" to basicTicket.location.run(TicketHelper::locationAsString), - "ACTIONS" to actions.run(TicketHelper::actionListAsString) - ) - ) - } - } - - return ticketID - } - - override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?, context: CoroutineContext) { - (lowerBound..upperBound).forEach { id -> - withContext(context) { - launch { - val response = redis.hgetall("$id").toList() - if (response.isEmpty()) return@launch - - val status = response.getValueFromKey("STATUS").run(TicketHelper::stringToStatus) - if (status != BasicTicket.Status.OPEN) return@launch - - val actions = response.getValueFromKey("ACTIONS").run(TicketHelper::stringToActionList).toMutableList() - actions += FullTicket.Action(FullTicket.Action.Type.MASS_CLOSE, uuid) - - redis.hset("$id", - mapOf( - "STATUS" to BasicTicket.Status.CLOSED.run(TicketHelper::statusAsString), - "ACTIONS" to actions.run(TicketHelper::actionListAsString), - ) - ) - } - } - } - } - - override suspend fun getOpenIDPriorityPairs(): Flow> = flow { - redis.keys("*") - .buffer(1000) - .collect { - val response = redis.hgetall(it).toList() - val priority = response.getValueFromKey("PRIORITY").run(TicketHelper::stringToPriority) - val status = response.getValueFromKey("STATUS").run(TicketHelper::stringToStatus) - - if (status == BasicTicket.Status.OPEN) - emit(it.toInt() to priority.level) - } - } - - override suspend fun getAssignedOpenIDPriorityPairs( - assignment: String, - unfixedGroupAssignment: List - ): Flow> = flow { - val fixedAssignments = unfixedGroupAssignment.map { "::$it" } - - redis.keys("*") - .buffer(1000) - .collect { - val response = redis.hgetall(it).toList() - val priority = response.getValueFromKey("PRIORITY").run(TicketHelper::stringToPriority) - val status = response.getValueFromKey("STATUS").run(TicketHelper::stringToStatus) - val assigned = response.getValueFromKey("ASSIGNED_TO").run(TicketHelper::stringAsAssignment) - - if (status == BasicTicket.Status.OPEN && assigned?.run { it == assignment || it in fixedAssignments } == true) - emit(it.toInt() to priority.level) - } - } - - override suspend fun getIDsWithUpdates(): Flow = flow { - redis.keys("*") - .buffer(1000) - .collect { - val hasUpdate = redis.hget(it, "STATUS_UPDATE_FOR_CREATOR")!!.run(TicketHelper::stringToCreatorUpdate) - - if (hasUpdate) - emit(it.toInt()) - } - } - - override suspend fun getIDsWithUpdatesFor(uuid: UUID): Flow = flow { - redis.keys("*") - .buffer(1000) - .collect { - val response = redis.hgetall(it).toList() - val hasUpdate = response.getValueFromKey("STATUS_UPDATE_FOR_CREATOR").run(TicketHelper::stringToCreatorUpdate) - val creatorUUID = response.getValueFromKey("CREATOR_UUID").run(TicketHelper::stringToCreatorUUID) - - if (creatorUUID?.run { it.equals(uuid) } == true && hasUpdate) - emit(it.toInt()) - } - } - - override suspend fun getBasicTickets(ids: List): Flow = flow { - ids.forEach { id -> - val ticket = getBasicTicket(id) - if (ticket != null) emit(ticket) - } - } - - // getBasicTicket returns FullTicket? Safe to type cast - override suspend fun getFullTicketsFromBasics( - basicTickets: List, - context: CoroutineContext - ): Flow = flow { - basicTickets.forEach { - (it as? FullTicket)?.apply { emit(this) } - } - } - - override suspend fun getFullTickets(ids: List, context: CoroutineContext): Flow = flow { - ids.forEach { - emit(getBasicTicket(it) as FullTicket) - } - } - - override suspend fun searchDatabase( - context: CoroutineContext, - locale: TMLocale, - mainTableConstraints: List>, - searchFunction: (FullTicket) -> Boolean - ): Flow { - val mainToFunction = mainTableConstraints.mapNotNull { (word, arg) -> - when (word) { - locale.searchAssigned -> { t: FullTicket -> t.assignedTo == arg } - locale.searchCreator -> { - val uuid = arg?.run(UUID::fromString); - { t: FullTicket -> t.creatorUUID == uuid } - } - locale.searchPriority -> { - val priority = arg!!.toByte().run(::byteToPriority); - { t: FullTicket -> t.priority == priority } - } - locale.searchStatus -> { - val status = BasicTicket.Status.valueOf(arg!!); - { t: FullTicket -> t.status == status } - } - else -> null - } - } - val newSearchFunction = { t: FullTicket -> mainToFunction.all { it(t) } && searchFunction(t) } - - return flow { - redis.keys("*").collect { id -> - val fullTicket = getBasicTicket(id.toInt()) as FullTicket - - if (newSearchFunction(fullTicket)) - emit(fullTicket) - } - } - } - - override suspend fun closeDatabase() { - connection.close() - client.shutdown() - } - - override suspend fun initialiseDatabase() { - // Database already initialized from instantiation - } - - override suspend fun updateNeeded(): Boolean { - return false // Introduced as V4 Database in TM5 - } - - override suspend fun migrateDatabase( - context: CoroutineContext, - to: Database.Type, - onBegin: suspend () -> Unit, - onComplete: suspend () -> Unit - ) { - TODO("Not yet implemented") - } - - override suspend fun updateDatabase( - context: CoroutineContext, - onBegin: suspend () -> Unit, - onComplete: suspend () -> Unit, - offlinePlayerNameToUuidOrNull: (String) -> UUID? - ) { - // N/A (Introduced as V4 Database in TM5) - } -} - -private fun List>.getValueFromKey(key: String) = first { it.key == key }.value!! \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt index 7408776..ff73ef2 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.toList import kotlinx.coroutines.withContext -import kotlinx.serialization.Serializable import java.util.* import kotlin.coroutines.CoroutineContext @@ -32,7 +31,6 @@ open class BasicTicket( OPEN("&a"), CLOSED("&c") } - @Serializable data class TicketLocation(val world: String, val x: Int, val y: Int, val z: Int) { override fun toString() = "$world $x $y $z" } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt index 9e84292..8c20957 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt @@ -1,14 +1,5 @@ package com.github.hoshikurama.ticketmanager.common.ticket -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.Serializer -import kotlinx.serialization.descriptors.PrimitiveKind -import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder import java.time.Instant import java.util.* @@ -34,25 +25,9 @@ class FullTicket( basicTicket.creatorStatusUpdate ) - @Serializable - data class Action(val type: Type, val user: @Serializable(with = UUIDSerializer::class) UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { + data class Action(val type: Type, val user: UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { enum class Type { ASSIGN, CLOSE, COMMENT, OPEN, REOPEN, SET_PRIORITY, MASS_CLOSE } } -} - -@OptIn(ExperimentalSerializationApi::class) -@Serializer(forClass = UUID::class) -object UUIDSerializer : KSerializer { - override val descriptor: SerialDescriptor - get() = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: UUID) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): UUID { - return UUID.fromString(decoder.decodeString()) - } } \ No newline at end of file From 9a0f97f88a8bb67cb77b55d8df513f9c160f568b Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 13 Jul 2021 11:05:13 -0500 Subject: [PATCH 20/31] Moved to Mutexes for critical sections --- .../ticketmanager/paper/Globals.kt | 2 +- .../paper/TicketManagerPlugin.kt | 51 ++++--- .../ticketmanager/paper/events/Commands.kt | 60 ++++---- .../ticketmanager/paper/events/PlayerJoin.kt | 12 +- .../ticketmanager/common/ConfigState.kt | 125 +++++++++++++++++ .../ticketmanager/common/Miscellaneous.kt | 10 -- .../ticketmanager/common/PluginState.kt | 130 ++---------------- 7 files changed, 196 insertions(+), 194 deletions(-) create mode 100644 common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ConfigState.kt diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt index 6901a56..b549dda 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt @@ -1 +1 @@ -package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.buildComponent import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.PluginState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin internal val pluginState: PluginState get() = mainPlugin.configState internal val asyncContext: CoroutineContext get() = mainPlugin.asyncDispatcher internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configState.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers().asSequence() .filter { it.has(permission) } .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true internal fun Player.toTMLocale() = pluginState.localeHandler.getOrDefault(locale().toString()) internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else pluginState.localeHandler.consoleLocale internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null fun UUID?.toName(locale: TMLocale): String { if (this == null) return locale.consoleName return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" } fun postModifiedStacktrace(e: Exception) { val onlinePlayers = Bukkit.getOnlinePlayers() .asSequence() .filter { it.has("ticketmanager.notify.warning") } .map { it as Audience to it.toTMLocale() } val console = Bukkit.getConsoleSender() as Audience to pluginState.localeHandler.consoleLocale val playersAndConsole = onlinePlayers + sequenceOf(console) playersAndConsole.forEach { pair -> val audience = pair.first val locale = pair.second audience.sendMessage( buildComponent { // Builds header listOf( locale.stacktraceLine1, locale.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), locale.stacktraceLine3.replace("%message%", e.message ?: "?"), locale.stacktraceLine4, ) .forEach { text { formattedContent(it) } } // Adds stacktrace entries e.stackTrace .filter { it.className.startsWith("com.github.hoshikurama.ticketmanager") } .map { locale.stacktraceEntry .replace("%method%", it.methodName) .replace("%file%", it.fileName ?: "?") .replace("%line%", "${it.lineNumber}") } .forEach { text { formattedContent(it) } } } ) } } \ No newline at end of file +package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.buildComponent import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.ConfigState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin internal val configState: ConfigState get() = mainPlugin.configStateInternal internal val asyncContext: CoroutineContext get() = mainPlugin.asyncDispatcher internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configStateInternal.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers().asSequence() .filter { it.has(permission) } .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true internal fun Player.toTMLocale() = configState.localeHandler.getOrDefault(locale().toString()) internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else configState.localeHandler.consoleLocale internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null fun UUID?.toName(locale: TMLocale): String { if (this == null) return locale.consoleName return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" } fun postModifiedStacktrace(e: Exception) { val onlinePlayers = Bukkit.getOnlinePlayers() .asSequence() .filter { it.has("ticketmanager.notify.warning") } .map { it as Audience to it.toTMLocale() } val console = Bukkit.getConsoleSender() as Audience to configState.localeHandler.consoleLocale val playersAndConsole = onlinePlayers + sequenceOf(console) playersAndConsole.forEach { pair -> val audience = pair.first val locale = pair.second audience.sendMessage( buildComponent { // Builds header listOf( locale.stacktraceLine1, locale.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), locale.stacktraceLine3.replace("%message%", e.message ?: "?"), locale.stacktraceLine4, ) .forEach { text { formattedContent(it) } } // Adds stacktrace entries e.stackTrace .filter { it.className.startsWith("com.github.hoshikurama.ticketmanager") } .map { locale.stacktraceEntry .replace("%method%", it.methodName) .replace("%file%", it.fileName ?: "?") .replace("%line%", "${it.lineNumber}") } .forEach { text { formattedContent(it) } } } ) } } \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index 4fc9420..23fb2ae 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -17,15 +17,10 @@ import org.bukkit.Bukkit import java.io.File class TicketManagerPlugin : SuspendingJavaPlugin() { - @OptIn(ObsoleteCoroutinesApi::class) - private val singleOffThread = newSingleThreadContext("SingleOffThread") - - internal val jobCount = NonBlockingSync(singleOffThread, 0) - internal val pluginLocked = NonBlockingSync(singleOffThread, true) + internal val pluginState = PluginState() internal lateinit var perms: Permission private set - internal lateinit var configState: PluginState + internal lateinit var configStateInternal: ConfigState - internal val ticketCountMetrics = NonBlockingSync(singleOffThread, 0) private lateinit var metrics: Metrics @@ -33,8 +28,8 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { init { plugin = this } override suspend fun onDisableAsync() { - pluginLocked.set(true) - pluginState.database.closeDatabase() + pluginState.pluginLocked.set(true) + configStateInternal.database.closeDatabase() } override fun onEnable() { @@ -50,8 +45,8 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { metrics.addCustomChart( Metrics.SingleLineChart("tickets_made") { runBlocking { - val ticketCount = ticketCountMetrics.check() - ticketCountMetrics.set(0) + val ticketCount = pluginState.ticketCountMetrics.get() + pluginState.ticketCountMetrics.set(0) ticketCount } } @@ -66,19 +61,19 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { // Creates task timers Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, Runnable { - launchAsync { configState.cooldowns.filterMapAsync() } + launchAsync { configStateInternal.cooldowns.filterMapAsync() } launchAsync { - if (pluginLocked.check()) return@launchAsync + if (pluginState.pluginLocked.get()) return@launchAsync try { // Mass Unread Notify - if (configState.allowUnreadTicketUpdates) { + if (configStateInternal.allowUnreadTicketUpdates) { Bukkit.getOnlinePlayers().asFlow() .filter { it.has("ticketmanager.notify.unreadUpdates.scheduled") } .onEach { launch { - val ticketIDs = configState.database.getIDsWithUpdatesFor(it.uniqueId).toList() + val ticketIDs = configStateInternal.database.getIDsWithUpdatesFor(it.uniqueId).toList() val tickets = ticketIDs.joinToString(", ") if (ticketIDs.isEmpty()) return@launch @@ -92,9 +87,9 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } } - val openPriority = configState.database.getOpenIDPriorityPairs().map { it.first }.toList() + val openPriority = configStateInternal.database.getOpenIDPriorityPairs().map { it.first }.toList() val openCount = openPriority.count() - val assignments = configState.database.getBasicTickets(openPriority).mapNotNull { it.assignedTo }.toList() + val assignments = configStateInternal.database.getBasicTickets(openPriority).mapNotNull { it.assignedTo }.toList() // Open and Assigned Notify Bukkit.getOnlinePlayers().asFlow() @@ -121,16 +116,16 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } internal suspend fun loadPlugin() = withContext(plugin.asyncDispatcher) { - pluginLocked.set(true) + pluginState.pluginLocked.set(true) - configState = run { + configStateInternal = run { // Creates config file if not found if (!File(plugin.dataFolder, "config.yml").exists()) { plugin.saveDefaultConfig() // Notifies users config was generated after plugin state init launch { - while (!(::configState.isInitialized)) + while (!(::configStateInternal.isInitialized)) delay(100L) pushMassNotify("ticketmanager.notify.warning") { text { formattedContent(it.warningsNoConfig) } } } @@ -156,8 +151,8 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } } - val cooldown: () -> PluginState.Cooldown? = { - PluginState.Cooldown( + val cooldown: () -> ConfigState.Cooldown? = { + ConfigState.Cooldown( getBoolean("Use_Cooldowns", false), getLong("Cooldown_Time", 0L) ) @@ -185,7 +180,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { mainPlugin.description.version } - PluginState.createPluginState( + ConfigState.createPluginState( database, cooldown, localeHandler, @@ -199,10 +194,10 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } launch { - val updateNeeded = configState.database.updateNeeded() + val updateNeeded = configStateInternal.database.updateNeeded() if (updateNeeded) { - configState.database.updateDatabase( + configStateInternal.database.updateDatabase( onBegin = { pushMassNotify("ticketmanager.notify.info") { text { formattedContent(it.informationDBUpdate) } @@ -212,7 +207,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { pushMassNotify("ticketmanager.notify.info") { text { formattedContent(it.informationDBUpdateComplete) } } - pluginLocked.set(true) + pluginState.pluginLocked.set(true) }, offlinePlayerNameToUuidOrNull = { Bukkit.getOfflinePlayers() @@ -222,13 +217,13 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { }, context = asyncContext ) - } else pluginLocked.set(false) + } else pluginState.pluginLocked.set(false) } withContext(minecraftDispatcher) { // Register events and commands - configState.localeHandler.getCommandBases().forEach { + configStateInternal.localeHandler.getCommandBases().forEach { getCommand(it)!!.setSuspendingExecutor(Commands()) server.pluginManager.registerEvents(TabComplete(), this@TicketManagerPlugin) // Remember to register any keyword in plugin.yml diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index 74b3694..440d6b3 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -45,7 +45,7 @@ class Commands : SuspendingCommandExecutor { return@withContext false } - if (mainPlugin.pluginLocked.check()) { + if (mainPlugin.pluginState.pluginLocked.get()) { sender.sendMessage(text { formattedContent(senderLocale.warningsLocked)}) return@withContext false } @@ -65,16 +65,16 @@ class Commands : SuspendingCommandExecutor { val executeCommand = suspend { executeCommand(sender, argList, senderLocale, pseudoTicket) } try { - mainPlugin.jobCount.run { set(check() + 1) } + mainPlugin.pluginState.jobCount.run { set(get() + 1) } if (notUnderCooldown.await() && isValidCommand.await() && hasValidPermission.await()) { executeCommand()?.let { pushNotifications(sender, it, senderLocale, pseudoTicket) } - mainPlugin.jobCount.run { set(check() - 1) } } } catch (e: Exception) { e.printStackTrace() postModifiedStacktrace(e) sender.sendMessage(text { formattedContent(senderLocale.warningsUnexpectedError) }) - mainPlugin.jobCount.run { set(check() - 1) } + } finally { + mainPlugin.pluginState.jobCount.run { set(get() - 1) } } return@withContext true @@ -85,7 +85,7 @@ class Commands : SuspendingCommandExecutor { senderLocale: TMLocale, ): Deferred { - suspend fun buildFromIDAsync(id: Int) = BasicTicketHandler.buildHandlerAsync(pluginState.database, id, asyncContext) + suspend fun buildFromIDAsync(id: Int) = BasicTicketHandler.buildHandlerAsync(configState.database, id, asyncContext) return withContext(asyncContext) { when (args[0]) { @@ -109,7 +109,7 @@ class Commands : SuspendingCommandExecutor { args.getOrNull(1) ?.toIntOrNull() ?.let { buildFromIDAsync(it) } ?: async { null } - else -> async { BasicTicket(creatorUUID = null, location = null).run { BasicTicketHandler(pluginState.database, this) } } // Occurs when command does not need valid handler + else -> async { BasicTicket(creatorUUID = null, location = null).run { BasicTicketHandler(configState.database, this) } } // Occurs when command does not need valid handler } } } @@ -255,7 +255,7 @@ class Commands : SuspendingCommandExecutor { } ) .thenCheck( { sendMessage(senderLocale.warningsConvertToSameDBType) } ) - { pluginState.database.type != Database.Type.valueOf(args[1]) } + { configState.database.type != Database.Type.valueOf(args[1]) } else -> false.also { invalidCommand() } } @@ -271,7 +271,7 @@ class Commands : SuspendingCommandExecutor { senderLocale.commandWordCreate, senderLocale.commandWordComment, senderLocale.commandWordSilentComment -> - pluginState.cooldowns.checkAndSetAsync(sender.toUUIDOrNull()) + configState.cooldowns.checkAndSetAsync(sender.toUUIDOrNull()) else -> false } @@ -381,7 +381,7 @@ class Commands : SuspendingCommandExecutor { val shownAssignment = dbAssignment ?: senderLocale.miscNobody launch { ticketHandler.setAssignedTo(dbAssignment) } - launch { pluginState.database.addAction( + launch { configState.database.addAction( ticketID = ticketHandler.id, action = FullTicket.Action(FullTicket.Action.Type.ASSIGN, sender.toUUIDOrNull(), dbAssignment) )} @@ -439,7 +439,7 @@ class Commands : SuspendingCommandExecutor { silent: Boolean, ticketHandler: BasicTicketHandler ): NotifyParams = withContext(asyncContext) { - val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && configState.allowUnreadTicketUpdates if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } } @@ -460,7 +460,7 @@ class Commands : SuspendingCommandExecutor { .run(ChatColor::stripColor)!! launch { - pluginState.database.run { + configState.database.run { addAction( ticketID = ticketHandler.id, action = FullTicket.Action(FullTicket.Action.Type.COMMENT, sender.toUUIDOrNull(), message) @@ -506,7 +506,7 @@ class Commands : SuspendingCommandExecutor { ticketHandler: BasicTicketHandler ): NotifyParams = withContext(asyncContext) { launch { - pluginState.database.addAction( + configState.database.addAction( ticketID = ticketHandler.id, action = FullTicket.Action(FullTicket.Action.Type.CLOSE, sender.toUUIDOrNull()) ) @@ -548,7 +548,7 @@ class Commands : SuspendingCommandExecutor { val lowerBound = args[1].toInt() val upperBound = args[2].toInt() - launch { pluginState.database.massCloseTickets(lowerBound, upperBound, sender.toUUIDOrNull(), asyncContext) } + launch { configState.database.massCloseTickets(lowerBound, upperBound, sender.toUUIDOrNull(), asyncContext) } NotifyParams( silent = silent, @@ -584,13 +584,13 @@ class Commands : SuspendingCommandExecutor { .joinToString(" ") .run(ChatColor::stripColor)!! - val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && configState.allowUnreadTicketUpdates if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } } launch { - pluginState.database.addAction( + configState.database.addAction( ticketID = ticketHandler.id, action = FullTicket.Action(FullTicket.Action.Type.COMMENT, sender.toUUIDOrNull(), message) ) @@ -642,8 +642,8 @@ class Commands : SuspendingCommandExecutor { val ticket = BasicTicket(creatorUUID = sender.toUUIDOrNull(), location = sender.toTicketLocationOrNull()) - val deferredID = async { pluginState.database.addNewTicket(ticket, asyncContext, message) } - mainPlugin.ticketCountMetrics.run { set(check() + 1) } + val deferredID = async { configState.database.addNewTicket(ticket, asyncContext, message) } + mainPlugin.pluginState.ticketCountMetrics.run { set(get() + 1) } val id = deferredID.await().toString() NotifyParams( @@ -674,7 +674,7 @@ class Commands : SuspendingCommandExecutor { locale: TMLocale, ) { val hasSilentPerm = sender.has("ticketmanager.commandArg.silence") - val cc = pluginState.localeHandler.mainColourCode + val cc = configState.localeHandler.mainColourCode val component = buildComponent { text { formattedContent(locale.helpHeader) } @@ -745,7 +745,7 @@ class Commands : SuspendingCommandExecutor { val searchedUser = targetName?.attemptToUUIDString() val resultSize: Int - val resultsChunked = pluginState.database.searchDatabase(asyncContext, locale, listOf(locale.searchCreator to searchedUser)) { true } + val resultsChunked = configState.database.searchDatabase(asyncContext, locale, listOf(locale.searchCreator to searchedUser)) { true } .toList() .sortedByDescending(BasicTicket::id) .also { resultSize = it.size } @@ -831,7 +831,7 @@ class Commands : SuspendingCommandExecutor { ) { withContext(asyncContext) { try { - mainPlugin.pluginLocked.set(true) + mainPlugin.pluginState.pluginLocked.set(true) pushMassNotify("ticketmanager.notify.info") { text { formattedContent(it.informationReloadInitiated.replace("%user%", sender.name)) } } @@ -844,19 +844,19 @@ class Commands : SuspendingCommandExecutor { pushMassNotify("ticketmanager.notify.warning") { text { formattedContent(it.warningsLongTaskDuringReload) } } - mainPlugin.jobCount.set(1) + mainPlugin.pluginState.jobCount.set(1) mainPlugin.asyncDispatcher.cancelChildren() } } // Waits for other tasks to complete - while (mainPlugin.jobCount.check() > 1) delay(1000L) + while (mainPlugin.pluginState.jobCount.get() > 1) delay(1000L) if (!forceQuitJob.isCancelled) forceQuitJob.cancel("Tasks closed on time") pushMassNotify("ticketmanager.notify.info") { text { formattedContent(it.informationReloadTasksDone) } } - pluginState.database.closeDatabase() + configState.database.closeDatabase() mainPlugin.loadPlugin() pushMassNotify("ticketmanager.notify.info") { @@ -884,13 +884,13 @@ class Commands : SuspendingCommandExecutor { val action = FullTicket.Action(FullTicket.Action.Type.REOPEN, sender.toUUIDOrNull()) // Updates user status if needed - val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && pluginState.allowUnreadTicketUpdates + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && configState.allowUnreadTicketUpdates if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } } launch { - pluginState.database.addAction(ticketHandler.id, action) + configState.database.addAction(ticketHandler.id, action) ticketHandler.setTicketStatus(BasicTicket.Status.OPEN) } @@ -1007,7 +1007,7 @@ class Commands : SuspendingCommandExecutor { // Results Computation val resultSize: Int - val chunkedTickets = pluginState.database.searchDatabase(asyncContext, locale, mainTableConstrains, composedSearch) + val chunkedTickets = configState.database.searchDatabase(asyncContext, locale, mainTableConstrains, composedSearch) .toList() .sortedByDescending(BasicTicket::id) .apply { resultSize = size } @@ -1079,7 +1079,7 @@ class Commands : SuspendingCommandExecutor { ticketHandler: BasicTicketHandler, ): NotifyParams = withContext(asyncContext) { launch { - pluginState.database.addAction( + configState.database.addAction( ticketID = ticketHandler.id, action = FullTicket.Action(FullTicket.Action.Type.SET_PRIORITY, sender.toUUIDOrNull(), args[2]) ) @@ -1270,7 +1270,7 @@ class Commands : SuspendingCommandExecutor { content("...............") color(NamedTextColor.DARK_GRAY) } - val cc = pluginState.localeHandler.mainColourCode + val cc = configState.localeHandler.mainColourCode val ofSection = text { formattedContent("$cc($curPage${locale.pageOf}$pageCount)") } when (curPage) { @@ -1337,7 +1337,7 @@ class Commands : SuspendingCommandExecutor { getIDPriorityPair: suspend (Database) -> Flow>, baseCommand: (TMLocale) -> String ): Component { - val chunkedIDs = getIDPriorityPair(pluginState.database) + val chunkedIDs = getIDPriorityPair(configState.database) .toList() .sortedWith(compareByDescending> { it.second }.thenByDescending { it.first } ) .map { it.first } @@ -1345,7 +1345,7 @@ class Commands : SuspendingCommandExecutor { val page = if (args.size == 2 && args[1].toInt() in 1..chunkedIDs.size) args[1].toInt() else 1 val fullTickets = chunkedIDs.getOrNull(page - 1) - ?.run { pluginState.database.getFullTickets(this, asyncContext) } + ?.run { configState.database.getFullTickets(this, asyncContext) } ?.toList() ?: emptyList() diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt index a67baba..794c34d 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/PlayerJoin.kt @@ -1,9 +1,9 @@ package com.github.hoshikurama.ticketmanager.paper.events import com.github.hoshikurama.componentDSL.formattedContent +import com.github.hoshikurama.ticketmanager.paper.configState import com.github.hoshikurama.ticketmanager.paper.has import com.github.hoshikurama.ticketmanager.paper.mainPlugin -import com.github.hoshikurama.ticketmanager.paper.pluginState import com.github.hoshikurama.ticketmanager.paper.toTMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import com.github.shynixn.mccoroutine.minecraftDispatcher @@ -20,14 +20,14 @@ class PlayerJoin : Listener { @EventHandler suspend fun onPlayerJoin(event: PlayerJoinEvent) = withContext(mainPlugin.minecraftDispatcher) { - if (mainPlugin.pluginLocked.check()) return@withContext + if (mainPlugin.pluginState.pluginLocked.get()) return@withContext val player = event.player withContext(mainPlugin.asyncDispatcher) { //Plugin Update Checking launch { - val pluginUpdateStatus = pluginState.pluginUpdateAvailable.await() + val pluginUpdateStatus = configState.pluginUpdateAvailable.await() if (player.has("ticketmanager.notify.pluginUpdate") && pluginUpdateStatus != null) { val sentMSG = player.toTMLocale().notifyPluginUpdate .replace("%current%", pluginUpdateStatus.first) @@ -39,7 +39,7 @@ class PlayerJoin : Listener { // Unread Updates launch { if (player.has("ticketmanager.notify.unreadUpdates.onJoin")) { - pluginState.database.getIDsWithUpdatesFor(player.uniqueId) + configState.database.getIDsWithUpdatesFor(player.uniqueId) .toList() .run { if (size == 0) null else this } ?.run { @@ -57,8 +57,8 @@ class PlayerJoin : Listener { // View Open-Count and Assigned-Count Tickets launch { if (player.has("ticketmanager.notify.openTickets.onJoin")) { - val open = pluginState.database.getOpenIDPriorityPairs() - val assigned = pluginState.database.getAssignedOpenIDPriorityPairs(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) + val open = configState.database.getOpenIDPriorityPairs() + val assigned = configState.database.getAssignedOpenIDPriorityPairs(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) val sentMSG = player.toTMLocale().notifyOpenAssigned .replace("%open%", "${open.count()}") diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ConfigState.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ConfigState.kt new file mode 100644 index 0000000..b11c074 --- /dev/null +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ConfigState.kt @@ -0,0 +1,125 @@ +package com.github.hoshikurama.ticketmanager.common + +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.databases.SQLite +import kotlinx.coroutines.* +import java.time.Instant +import java.util.* +import kotlin.coroutines.CoroutineContext + +class ConfigState( + val cooldowns: Cooldown, + val database: Database, + val localeHandler: LocaleHandler, + val allowUnreadTicketUpdates: Boolean, + val pluginUpdateAvailable: Deferred?> +) { + + companion object { + suspend inline fun createPluginState( + crossinline database: () -> Database?, + crossinline cooldown: () -> Cooldown?, + crossinline localeHandler: suspend () -> LocaleHandler?, + crossinline allowUnreadTicketUpdates: () -> Boolean?, + crossinline checkForPluginUpdate: () -> Boolean?, + crossinline pluginVersion: () -> String, + absolutePathToPluginFolder: String, + context: CoroutineContext + ) = withContext(context) { + + val deferredCooldown = async { tryOrDefault(cooldown, Cooldown(false, 0)) } + val deferredAllowUnreadUpdates = async { tryOrDefault(allowUnreadTicketUpdates, true) + } + val deferredDatabase = async { + tryOrDefault( + attempted = { database()?.apply { initialiseDatabase() } }, + default = SQLite(absolutePathToPluginFolder) + ) + } + + val deferredPluginUpdate = async { + val shouldCheck = checkForPluginUpdate() + if (shouldCheck == true) { + val curVersion = pluginVersion() + val latestVersion = UpdateChecker(91178).getLatestVersion() + .run { this ?: curVersion } + + val curVersSplit = curVersion.split(".").map(String::toInt) + val latestVersSplit = latestVersion.split(".").map(String::toInt) + + for (i in 0..latestVersSplit.lastIndex) { + if (curVersSplit[i] > latestVersSplit[i]) + return@async null + } + return@async Pair(curVersion, latestVersion) + } + return@async null + } + + val deferredLocaleHandler = async { + tryOrDefaultSuspend(localeHandler, + LocaleHandler.buildLocalesAsync( + mainColourCode = "&3", + preferredLocale = "en_ca", + console_Locale = "en_ca", + forceLocale = false, + context = context + ) + ) + } + + ConfigState( + cooldowns = deferredCooldown.await(), + database = deferredDatabase.await(), + localeHandler = deferredLocaleHandler.await(), + allowUnreadTicketUpdates = deferredAllowUnreadUpdates.await(), + pluginUpdateAvailable = deferredPluginUpdate + ) + } + } + + + class Cooldown( + private val enabled: Boolean, + private val duration: Long, + ) { + @OptIn(ObsoleteCoroutinesApi::class) + private val threadContext = newSingleThreadContext("Cooldown") //This WILL be replaced in the future + private val map = mutableMapOf() + + suspend fun checkAndSetAsync(uuid: UUID?): Boolean { + return withContext(threadContext) { + if (!enabled || uuid == null) return@withContext false + + val curTime = Instant.now().epochSecond + val applies = map[uuid]?.let { it <= curTime } ?: false + return@withContext if (applies) true + else map.put(uuid, duration + curTime).run { true } + } + } + + suspend fun filterMapAsync() { + withContext(threadContext) { + map.forEach { + if (it.value > Instant.now().epochSecond) + map.remove(it.key) + } + } + } + } +} + +inline fun tryOrDefault(attempted: () -> T?, default: T): T = + tryOrNull(attempted).run { this ?: default } + +inline fun tryOrNull(function: () -> T): T? = + try { function() } + catch (e: Exception) { e.printStackTrace(); null } + +suspend inline fun tryOrDefaultSuspend(crossinline attempted: suspend () -> T?, default: T): T = + tryOrNullSuspend(attempted).run { this ?: default } + +suspend inline fun tryOrNullSuspend(crossinline function: suspend () -> T): T? = + try { function() } + catch (ignored: Exception) { null } + diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt index 3c6ef12..9e805f7 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt @@ -2,12 +2,10 @@ package com.github.hoshikurama.ticketmanager.common import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket -import kotlinx.coroutines.withContext import java.io.InputStream import java.net.URL import java.time.Instant import java.util.* -import kotlin.coroutines.CoroutineContext class UpdateChecker(private val resourceID: Int) { fun getLatestVersion(): String? { @@ -27,14 +25,6 @@ class UpdateChecker(private val resourceID: Int) { } } -class NonBlockingSync( - private val context: CoroutineContext, - private var t: T -) { - suspend fun check() = withContext(context) { t } - suspend fun set(v: T) = withContext(context) { t = v } -} - fun byteToPriority(byte: Byte) = when (byte.toInt()) { 1 -> BasicTicket.Priority.LOWEST diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt index 8ac4f0a..1d2c92a 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt @@ -1,125 +1,17 @@ package com.github.hoshikurama.ticketmanager.common -import com.github.hoshikurama.ticketmanager.common.databases.Database -import com.github.hoshikurama.ticketmanager.common.databases.SQLite -import kotlinx.coroutines.* -import java.time.Instant -import java.util.* -import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock -class PluginState( - val cooldowns: Cooldown, - val database: Database, - val localeHandler: LocaleHandler, - val allowUnreadTicketUpdates: Boolean, - val pluginUpdateAvailable: Deferred?> -) { - - companion object { - suspend inline fun createPluginState( - crossinline database: () -> Database?, - crossinline cooldown: () -> Cooldown?, - crossinline localeHandler: suspend () -> LocaleHandler?, - crossinline allowUnreadTicketUpdates: () -> Boolean?, - crossinline checkForPluginUpdate: () -> Boolean?, - crossinline pluginVersion: () -> String, - absolutePathToPluginFolder: String, - context: CoroutineContext - ) = withContext(context) { - - val deferredCooldown = async { tryOrDefault(cooldown, Cooldown(false, 0)) } - val deferredAllowUnreadUpdates = async { tryOrDefault(allowUnreadTicketUpdates, true) - } - val deferredDatabase = async { - tryOrDefault( - attempted = { database()?.apply { initialiseDatabase() } }, - default = SQLite(absolutePathToPluginFolder) - ) - } - - val deferredPluginUpdate = async { - val shouldCheck = checkForPluginUpdate() - if (shouldCheck == true) { - val curVersion = pluginVersion() - val latestVersion = UpdateChecker(91178).getLatestVersion() - .run { this ?: curVersion } - - val curVersSplit = curVersion.split(".").map(String::toInt) - val latestVersSplit = latestVersion.split(".").map(String::toInt) - - for (i in 0..latestVersSplit.lastIndex) { - if (curVersSplit[i] > latestVersSplit[i]) - return@async null - } - return@async Pair(curVersion, latestVersion) - } - return@async null - } - - val deferredLocaleHandler = async { - tryOrDefaultSuspend(localeHandler, - LocaleHandler.buildLocalesAsync( - mainColourCode = "&3", - preferredLocale = "en_ca", - console_Locale = "en_ca", - forceLocale = false, - context = context - ) - ) - } - - PluginState( - cooldowns = deferredCooldown.await(), - database = deferredDatabase.await(), - localeHandler = deferredLocaleHandler.await(), - allowUnreadTicketUpdates = deferredAllowUnreadUpdates.await(), - pluginUpdateAvailable = deferredPluginUpdate - ) - } - } - - - class Cooldown( - private val enabled: Boolean, - private val duration: Long, - ) { - @OptIn(ObsoleteCoroutinesApi::class) - private val threadContext = newSingleThreadContext("Cooldown") //This WILL be replaced in the future - private val map = mutableMapOf() - - suspend fun checkAndSetAsync(uuid: UUID?): Boolean { - return withContext(threadContext) { - if (!enabled || uuid == null) return@withContext false - - val curTime = Instant.now().epochSecond - val applies = map[uuid]?.let { it <= curTime } ?: false - return@withContext if (applies) true - else map.put(uuid, duration + curTime).run { true } - } - } - - suspend fun filterMapAsync() { - withContext(threadContext) { - map.forEach { - if (it.value > Instant.now().epochSecond) - map.remove(it.key) - } - } - } - } +class PluginState { + val jobCount = MutexControlled(0) + val pluginLocked = MutexControlled(true) + val ticketCountMetrics = MutexControlled(0) } -inline fun tryOrDefault(attempted: () -> T?, default: T): T = - tryOrNull(attempted).run { this ?: default } - -inline fun tryOrNull(function: () -> T): T? = - try { function() } - catch (e: Exception) { e.printStackTrace(); null } - -suspend inline fun tryOrDefaultSuspend(crossinline attempted: suspend () -> T?, default: T): T = - tryOrNullSuspend(attempted).run { this ?: default } - -suspend inline fun tryOrNullSuspend(crossinline function: suspend () -> T): T? = - try { function() } - catch (ignored: Exception) { null } +class MutexControlled(private var t: T) { + private val mutex = Mutex() + suspend fun get(): T = mutex.withLock { t } + suspend fun set(t: T) = mutex.withLock { this.t = t } +} \ No newline at end of file From 66e3f3f14eac4e9981612abadd01e873c1e2c8b8 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 13 Jul 2021 12:51:24 -0500 Subject: [PATCH 21/31] Restructured Ticket package --- .../ticketmanager/paper/events/Commands.kt | 60 ++++++++-------- .../ticketmanager/common/Miscellaneous.kt | 49 ++++++++++++- .../common/databases/Database.kt | 2 +- .../ticketmanager/common/databases/MySQL.kt | 7 +- .../ticketmanager/common/databases/SQLite.kt | 3 +- .../common/ticket/BasicTicket.kt | 49 ++++++------- .../common/ticket/BasicTicketHandler.kt | 48 ++++--------- .../common/ticket/ConcreteBasicTicket.kt | 25 +++++++ .../ticketmanager/common/ticket/FullTicket.kt | 68 +++++++++++++------ 9 files changed, 189 insertions(+), 122 deletions(-) create mode 100644 common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/ConcreteBasicTicket.kt diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index 440d6b3..cc42325 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -51,7 +51,7 @@ class Commands : SuspendingCommandExecutor { } // Grabs BasicTicket. Only null if ID required but doesn't exist. Filters non-valid tickets - val pseudoTicket = getBasicTicketHandlerAsync(argList, senderLocale).await() + val pseudoTicket = getBasicTicketHandler(argList, senderLocale) if (pseudoTicket == null) { sender.sendMessage(text { formattedContent(senderLocale.warningsInvalidID) }) return@withContext false @@ -80,37 +80,35 @@ class Commands : SuspendingCommandExecutor { return@withContext true } - private suspend fun getBasicTicketHandlerAsync( + private suspend fun getBasicTicketHandler( args: List, senderLocale: TMLocale, - ): Deferred { + ): BasicTicketHandler? { - suspend fun buildFromIDAsync(id: Int) = BasicTicketHandler.buildHandlerAsync(configState.database, id, asyncContext) + suspend fun buildFromID(id: Int) = BasicTicketHandler.buildHandler(configState.database, id) - return withContext(asyncContext) { - when (args[0]) { - senderLocale.commandWordAssign, - senderLocale.commandWordSilentAssign, - senderLocale.commandWordClaim, - senderLocale.commandWordSilentClaim, - senderLocale.commandWordClose, - senderLocale.commandWordSilentClose, - senderLocale.commandWordComment, - senderLocale.commandWordSilentComment, - senderLocale.commandWordReopen, - senderLocale.commandWordSilentReopen, - senderLocale.commandWordSetPriority, - senderLocale.commandWordSilentSetPriority, - senderLocale.commandWordTeleport, - senderLocale.commandWordUnassign, - senderLocale.commandWordSilentUnassign, - senderLocale.commandWordView, - senderLocale.commandWordDeepView -> - args.getOrNull(1) - ?.toIntOrNull() - ?.let { buildFromIDAsync(it) } ?: async { null } - else -> async { BasicTicket(creatorUUID = null, location = null).run { BasicTicketHandler(configState.database, this) } } // Occurs when command does not need valid handler - } + return when (args[0]) { + senderLocale.commandWordAssign, + senderLocale.commandWordSilentAssign, + senderLocale.commandWordClaim, + senderLocale.commandWordSilentClaim, + senderLocale.commandWordClose, + senderLocale.commandWordSilentClose, + senderLocale.commandWordComment, + senderLocale.commandWordSilentComment, + senderLocale.commandWordReopen, + senderLocale.commandWordSilentReopen, + senderLocale.commandWordSetPriority, + senderLocale.commandWordSilentSetPriority, + senderLocale.commandWordTeleport, + senderLocale.commandWordUnassign, + senderLocale.commandWordSilentUnassign, + senderLocale.commandWordView, + senderLocale.commandWordDeepView -> + args.getOrNull(1) + ?.toIntOrNull() + ?.let { buildFromID(it) } + else -> ConcreteBasicTicket(creatorUUID = null, location = null).run { BasicTicketHandler(this, configState.database) } // Occurs when command does not need valid handler } } @@ -640,7 +638,7 @@ class Commands : SuspendingCommandExecutor { .joinToString(" ") .run(ChatColor::stripColor)!! - val ticket = BasicTicket(creatorUUID = sender.toUUIDOrNull(), location = sender.toTicketLocationOrNull()) + val ticket = ConcreteBasicTicket(creatorUUID = sender.toUUIDOrNull(), location = sender.toTicketLocationOrNull()) val deferredID = async { configState.database.addNewTicket(ticket, asyncContext, message) } mainPlugin.pluginState.ticketCountMetrics.run { set(get() + 1) } @@ -1178,7 +1176,7 @@ class Commands : SuspendingCommandExecutor { ticketHandler: BasicTicketHandler, ) { withContext(asyncContext) { - val fullTicket = ticketHandler.toFullTicketAsync(asyncContext).await() + val fullTicket = ticketHandler.toFullTicket() val baseComponent = buildTicketInfoComponent(fullTicket, locale) if (!sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && ticketHandler.creatorStatusUpdate) @@ -1205,7 +1203,7 @@ class Commands : SuspendingCommandExecutor { ticketHandler: BasicTicketHandler, ) { withContext(asyncContext) { - val fullTicket = ticketHandler.toFullTicketAsync(asyncContext).await() + val fullTicket = ticketHandler.toFullTicket() val baseComponent = buildTicketInfoComponent(fullTicket, locale) if (!sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && ticketHandler.creatorStatusUpdate) diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt index 9e805f7..8e2562b 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt @@ -2,6 +2,9 @@ package com.github.hoshikurama.ticketmanager.common import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.io.InputStream import java.net.URL import java.time.Instant @@ -84,4 +87,48 @@ val sortForList: Comparator = Comparator.comparing(BasicTicket::pri val sortActions: Comparator = Comparator.comparing(FullTicket.Action::timestamp) -fun T.notEquals(t: T) = this != t \ No newline at end of file +fun T.notEquals(t: T) = this != t + + +// Code from https://github.com/CruGlobal/android-gto-support/blob/47b44477e94e7d913de15066e3dd3eb8b54c4828/gto-support-kotlin-coroutines/src/main/java/org/ccci/gto/android/common/kotlin/coroutines/ReadWriteMutex.kt +interface ReadWriteMutex { + val write: Mutex + val read: Mutex +} + +fun ReadWriteMutex(): ReadWriteMutex = ReadWriteMutexImpl() + +internal class ReadWriteMutexImpl : ReadWriteMutex { + private val stateMutex = Mutex() + private val readerOwner = Any() + internal var readers = 0L + + override val write = Mutex() + override val read = object : Mutex { + override suspend fun lock(owner: Any?) { + stateMutex.withLock { + check(readers < Long.MAX_VALUE) { + "Attempt to lock the read mutex more than ${Long.MAX_VALUE} times concurrently" + } + // first reader should lock the write mutex + if (readers == 0L) write.lock(readerOwner) + readers++ + } + } + + override fun unlock(owner: Any?) { + runBlocking { + stateMutex.withLock { + check(readers > 0L) { "Attempt to unlock the read mutex when it wasn't locked" } + // release the write mutex lock when this is the last reader + if (--readers == 0L) write.unlock(readerOwner) + } + } + } + + override val isLocked get() = TODO("Not supported") + override val onLock get() = TODO("Not supported") + override fun holdsLock(owner: Any) = TODO("Not supported") + override fun tryLock(owner: Any?) = TODO("Not supported") + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index 46ad81c..56f394d 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -11,7 +11,7 @@ interface Database { val type: Type enum class Type { - MySQL, SQLite + MySQL, SQLite, Memory } // Individual property getters diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt index 2931a6f..d88b55e 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt @@ -4,6 +4,7 @@ import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.hoshikurama.ticketmanager.common.byteToPriority import com.github.hoshikurama.ticketmanager.common.sortActions import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket +import com.github.hoshikurama.ticketmanager.common.ticket.ConcreteBasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import com.github.hoshikurama.ticketmanager.common.ticket.toTicketLocation import com.github.jasync.sql.db.ConnectionPoolConfiguration @@ -15,8 +16,8 @@ import com.github.jasync.sql.db.mysql.MySQLQueryResult import com.github.jasync.sql.db.util.ExecutorServiceUtils import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import java.time.Instant import java.util.* @@ -111,7 +112,7 @@ class MySQL( ) } - statusPairs.onEach { + statusPairs.collect { launch { writeAction( action = FullTicket.Action( @@ -395,7 +396,7 @@ class MySQL( } private fun RowData.toBasicTicket(): BasicTicket { - return BasicTicket( + return ConcreteBasicTicket( id = getInt(0)!!, assignedTo = getString(4), creatorStatusUpdate = getBoolean(5)!!, diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index 498c818..ce1602f 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -3,6 +3,7 @@ package com.github.hoshikurama.ticketmanager.common.databases import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.hoshikurama.ticketmanager.common.byteToPriority import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket +import com.github.hoshikurama.ticketmanager.common.ticket.ConcreteBasicTicket import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import com.github.hoshikurama.ticketmanager.common.ticket.toTicketLocation import kotlinx.coroutines.coroutineScope @@ -409,7 +410,7 @@ class SQLite(absoluteDataFolderPath: String) : Database { } private fun Row.toBasicTicket(): BasicTicket { - return BasicTicket( + return ConcreteBasicTicket( id = int(1), creatorUUID = stringOrNull(2)?.let(UUID::fromString), priority = byteToPriority(byte(3)), diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt index ff73ef2..76e62b9 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicket.kt @@ -2,23 +2,24 @@ package com.github.hoshikurama.ticketmanager.common.ticket import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.hoshikurama.ticketmanager.common.databases.Database -import com.github.hoshikurama.ticketmanager.common.sortActions -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable import java.util.* -import kotlin.coroutines.CoroutineContext -open class BasicTicket( - val id: Int = -1, // Ticket ID 1+... -1 placeholder during ticket creation - val creatorUUID: UUID?, // UUID if player, null if Console - val location: TicketLocation?, // TicketLocation if player, null if Console - val priority: Priority = Priority.NORMAL, // Priority 1-5 or Lowest to Highest - val status: Status = Status.OPEN, // Status OPEN or CLOSED - val assignedTo: String? = null, // Null if not assigned to anybody - val creatorStatusUpdate: Boolean = false, // Determines whether player should be notified -) { +interface BasicTicket { + val id: Int // Ticket ID 1+... -1 placeholder during ticket creation + val creatorUUID: UUID? // UUID if player, null if Console + val location: TicketLocation? // TicketLocation if player, null if Console + val priority: Priority // Priority 1-5 or Lowest to Highest + val status: Status // Status OPEN or CLOSED + val assignedTo: String? // Null if not assigned to anybody + val creatorStatusUpdate: Boolean // Determines whether player should be notified + + @Serializable + data class TicketLocation(val world: String, val x: Int, val y: Int, val z: Int) { + override fun toString() = "$world $x $y $z" + } + + @Serializable enum class Priority(val level: Byte, val colourCode: String) { LOWEST(1, "&1"), LOW(2, "&9"), @@ -27,25 +28,15 @@ open class BasicTicket( HIGHEST(5, "&4") } + @Serializable enum class Status(val colourCode: String) { OPEN("&a"), CLOSED("&c") } - data class TicketLocation(val world: String, val x: Int, val y: Int, val z: Int) { - override fun toString() = "$world $x $y $z" - } - - suspend fun toFullTicketAsync(database: Database, context: CoroutineContext): Deferred = withContext(context) { - async { - val sortedActions = database.getActionsAsFlow(id) - .toList() - .sortedWith(sortActions) - - FullTicket(this@BasicTicket, sortedActions) - } - } + suspend fun toFullTicket(database: Database): FullTicket } + fun BasicTicket.Priority.toLocaledWord(locale: TMLocale) = when (this) { BasicTicket.Priority.LOWEST -> locale.priorityLowest BasicTicket.Priority.LOW -> locale.priorityLow @@ -63,4 +54,4 @@ fun BasicTicket.uuidMatches(other: UUID?) = creatorUUID?.equals(other) ?: (other == null) fun String.toTicketLocation() = split(" ") - .let { BasicTicket.TicketLocation(it[0], it[1].toInt(), it[2].toInt(), it[3].toInt()) } + .let { BasicTicket.TicketLocation(it[0], it[1].toInt(), it[2].toInt(), it[3].toInt()) } \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt index b5b47b3..dfcbea9 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/BasicTicketHandler.kt @@ -1,54 +1,30 @@ package com.github.hoshikurama.ticketmanager.common.ticket import com.github.hoshikurama.ticketmanager.common.databases.Database -import kotlinx.coroutines.async -import kotlinx.coroutines.withContext -import java.util.* -import kotlin.coroutines.CoroutineContext class BasicTicketHandler( - id: Int, - creatorUUID: UUID?, - location: TicketLocation?, - priority: Priority, - status: Status, - assignedTo: String?, - creatorStatusUpdate: Boolean, + private val basicTicket: BasicTicket, val database: Database, - -) : BasicTicket(id, creatorUUID, location, priority, status, assignedTo, creatorStatusUpdate) { - - constructor(database: Database, basicTicket: BasicTicket): this( - basicTicket.id, - basicTicket.creatorUUID, - basicTicket.location, - basicTicket.priority, - basicTicket.status, - basicTicket.assignedTo, - basicTicket.creatorStatusUpdate, - database - ) - - companion object { - suspend fun buildHandlerAsync(database: Database, id: Int, context: CoroutineContext) = withContext(context) { - async { - val basicTicket = database.getBasicTicket(id) - basicTicket?.run { BasicTicketHandler(database, this) } - } - } - } +) : BasicTicket by basicTicket { suspend fun setCreatorStatusUpdate(value: Boolean) = database.setCreatorStatusUpdate(id, value) - suspend fun setTicketPriority(value: Priority) = + suspend fun setTicketPriority(value: BasicTicket.Priority) = database.setPriority(id, value) - suspend fun setTicketStatus(value: Status) = + suspend fun setTicketStatus(value: BasicTicket.Status) = database.setStatus(id, value) suspend fun setAssignedTo(value: String?) = database.setAssignment(id, value) - suspend fun toFullTicketAsync(context: CoroutineContext) = super.toFullTicketAsync(database, context) + suspend fun toFullTicket() = basicTicket.toFullTicket(database) + + companion object { + suspend fun buildHandler(database: Database, id: Int): BasicTicketHandler? { + val basicTicket = database.getBasicTicket(id) + return basicTicket?.let { BasicTicketHandler(it, database) } + } + } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/ConcreteBasicTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/ConcreteBasicTicket.kt new file mode 100644 index 0000000..73a19dc --- /dev/null +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/ConcreteBasicTicket.kt @@ -0,0 +1,25 @@ +package com.github.hoshikurama.ticketmanager.common.ticket + +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.sortActions +import kotlinx.coroutines.flow.toList +import java.util.* + +open class ConcreteBasicTicket( + override val id: Int = -1, + override val creatorUUID: UUID?, + override val location: BasicTicket.TicketLocation?, + override val priority: BasicTicket.Priority = BasicTicket.Priority.NORMAL, + override val status: BasicTicket.Status = BasicTicket.Status.OPEN, + override val assignedTo: String? = null, + override val creatorStatusUpdate: Boolean = false, +) : BasicTicket { + + override suspend fun toFullTicket(database: Database): FullTicket { + val sortedActions = database.getActionsAsFlow(id) + .toList() + .sortedWith(sortActions) + + return FullTicket(this, sortedActions) + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt index 8c20957..7e1041a 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/ticket/FullTicket.kt @@ -1,33 +1,61 @@ package com.github.hoshikurama.ticketmanager.common.ticket +import com.github.hoshikurama.ticketmanager.common.databases.Database +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import java.time.Instant import java.util.* +@Serializable class FullTicket( - id: Int = -1, // Ticket ID 1+... -1 placeholder during ticket creation - creatorUUID: UUID?, // UUID if player, null if Console - location: TicketLocation?, // TicketLocation if player, null if Console - val actions: List = listOf(), // List of actions - priority: Priority = Priority.NORMAL, // Priority 1-5 or Lowest to Highest - status: Status = Status.OPEN, // Status OPEN or CLOSED - assignedTo: String? = null, // Null if not assigned to anybody - creatorStatusUpdate: Boolean = false, // Determines whether player should be notified -) : BasicTicket(id, creatorUUID, location, priority, status, assignedTo, creatorStatusUpdate) { - - constructor(basicTicket: BasicTicket, actions: List): this( - basicTicket.id, - basicTicket.creatorUUID, - basicTicket.location, - actions, - basicTicket.priority, - basicTicket.status, - basicTicket.assignedTo, - basicTicket.creatorStatusUpdate + override val id: Int, + @Serializable(with = UUIDSerializer::class) + override val creatorUUID: UUID?, + override val location: BasicTicket.TicketLocation?, + override val priority: BasicTicket.Priority, + override val status: BasicTicket.Status, + override val assignedTo: String?, + override val creatorStatusUpdate: Boolean, + val actions: List +) : BasicTicket { + + constructor(basicTicket: BasicTicket, actionsList: List) : this( + id = basicTicket.id, + creatorUUID = basicTicket.creatorUUID, + location = basicTicket.location, + priority = basicTicket.priority, + status = basicTicket.status, + assignedTo = basicTicket.assignedTo, + creatorStatusUpdate = basicTicket.creatorStatusUpdate, + actions = actionsList ) - data class Action(val type: Type, val user: UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { + @Serializable + data class Action( + val type: Type, @Serializable(with = UUIDSerializer::class) val user: UUID?, val message: String? = null, val timestamp: Long = Instant.now().epochSecond) { enum class Type { ASSIGN, CLOSE, COMMENT, OPEN, REOPEN, SET_PRIORITY, MASS_CLOSE } } + + override suspend fun toFullTicket(database: Database) = this +} + +object UUIDSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: UUID?) { + val string = value?.toString() ?: "NULL" + encoder.encodeString(string) + } + + override fun deserialize(decoder: Decoder): UUID? { + val string = decoder.decodeString() + return if (string == "NULL") null else UUID.fromString(string) + } } \ No newline at end of file From 0a742b58f912bf318da974add3b5fd1314f37b78 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 13 Jul 2021 16:39:41 -0500 Subject: [PATCH 22/31] Update memory file name --- Paper/build.gradle.kts | 12 +- .../paper/TicketManagerPlugin.kt | 9 +- Paper/src/main/resources/config.yml | 19 +- common/build.gradle.kts | 5 +- .../ticketmanager/common/Miscellaneous.kt | 12 + .../ticketmanager/common/PluginState.kt | 10 - .../ticketmanager/common/databases/Memory.kt | 314 ++++++++++++++++++ 7 files changed, 361 insertions(+), 20 deletions(-) create mode 100644 common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts index 5560d14..0821cbe 100644 --- a/Paper/build.gradle.kts +++ b/Paper/build.gradle.kts @@ -26,17 +26,15 @@ dependencies { implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0") implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0") implementation(project(":common")) + // Used by :common but still needed in plugin.yml //implementation("mysql:mysql-connector-java:8.0.25") //implementation("org.xerial:sqlite-jdbc:3.34.0") //implementation("com.github.jasync-sql:jasync-mysql:1.2.2") //implementation("com.github.seratch:kotliquery:1.3.1") //implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") - //implementation("org.yaml:snakeyaml:1.29") - //implementation("net.kyori:adventure-api:4.8.1") - //implementation("io.lettuce:lettuce-core:6.1.3.RELEASE") - //implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.1") //implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") + //implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.2.2") } tasks { @@ -45,7 +43,13 @@ tasks { dependencies { include(dependency("com.github.HoshiKurama:KyoriComponentDSL:1.1.0")) + include(dependency("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.2.2")) + include(dependency("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.2.2")) //todo test without shading include(project(":common")) + + relocate("com.github.hoshikurama.componentDSL", "com.github.hoshikurama.ticketmanager.componentDSL") + relocate("kotlinx.serialization", "com.github.hoshikurama.ticketmanager.shaded.kotlinx.serialization") } + } } \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index 23fb2ae..e46ad1c 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -3,6 +3,7 @@ package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.* import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.databases.Memory import com.github.hoshikurama.ticketmanager.common.databases.MySQL import com.github.hoshikurama.ticketmanager.common.databases.SQLite import com.github.hoshikurama.ticketmanager.paper.events.Commands @@ -51,9 +52,9 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } } ) - } + } //todo add pie chart for database type being used - // Launches PluginState initialisation + // Launches ConfigState initialisation launchAsync { loadPlugin() } // Register Event @@ -148,6 +149,10 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { asyncDispatcher = (plugin.asyncDispatcher as CoroutineDispatcher), ) Database.Type.SQLite -> SQLite(path) + Database.Type.Memory -> Memory( + filePath = path, + backupFrequency = getLong("Memory_Backup_Frequency", 600) + ) } } diff --git a/Paper/src/main/resources/config.yml b/Paper/src/main/resources/config.yml index fecbc82..47e8998 100644 --- a/Paper/src/main/resources/config.yml +++ b/Paper/src/main/resources/config.yml @@ -68,7 +68,18 @@ Cooldown_Time: 0 # Cons: - Must have MySQL database. # - Search and history commands slower. # -# Values: 'MySQL','SQLite' +# Memory: +# Memory refers to TicketManager's custom solution for running the entire +# database in RAM. Data is backed up throughout normal server operations or +# on server shutdowns. It is then loaded up on startup if selected. DO NOT +# USE THIS DATABASE TYPE UNLESS YOU UNDERSTAND THE RISKS OF RUNNING A +# DATABASE IN RAM!!! +# Pros: + Incredibly responsive +# + Incredibly fast queries +# Cons: - Any power loss or server crash will result in all data beyond +# the last backup being lost. +# +# Values: 'MySQL','SQLite','Memory' Database_Mode: 'SQLite' # # ###################### @@ -80,6 +91,12 @@ MySQL_DBName: '' MySQL_Username: '' MySQL_Password: '' # +# ###################### +# Memory as Database +# ###################### +# Time in seconds between database backups being made +Memory_Backup_Frequency: 600 +# # ########################################### # Other # ########################################### diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 43ec158..d7c4406 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,4 +1,5 @@ plugins { + kotlin("plugin.serialization") version "1.5.20" kotlin("jvm") java } @@ -21,7 +22,5 @@ dependencies { implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") implementation("org.yaml:snakeyaml:1.29") implementation("joda-time:joda-time:2.10.10") - implementation("io.lettuce:lettuce-core:6.1.3.RELEASE") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.5.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") -} \ No newline at end of file +} diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt index 8e2562b..32aa55f 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/Miscellaneous.kt @@ -89,6 +89,18 @@ val sortActions: Comparator = Comparator.comparing(FullTicket fun T.notEquals(t: T) = this != t +class MutexControlled(private var t: T) { + private val mutex = Mutex() + + suspend fun get(): T = mutex.withLock { t } + suspend fun set(t: T) = mutex.withLock { this.t = t } +} + +class IncrementalMutexController(private var n: Int) { + private val mutex = Mutex() + suspend fun getAndIncrement() = mutex.withLock { n.also { n++ } } +} + // Code from https://github.com/CruGlobal/android-gto-support/blob/47b44477e94e7d913de15066e3dd3eb8b54c4828/gto-support-kotlin-coroutines/src/main/java/org/ccci/gto/android/common/kotlin/coroutines/ReadWriteMutex.kt interface ReadWriteMutex { diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt index 1d2c92a..b3630dc 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/PluginState.kt @@ -1,17 +1,7 @@ package com.github.hoshikurama.ticketmanager.common -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - class PluginState { val jobCount = MutexControlled(0) val pluginLocked = MutexControlled(true) val ticketCountMetrics = MutexControlled(0) -} - -class MutexControlled(private var t: T) { - private val mutex = Mutex() - - suspend fun get(): T = mutex.withLock { t } - suspend fun set(t: T) = mutex.withLock { this.t = t } } \ No newline at end of file diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt new file mode 100644 index 0000000..81aa1bc --- /dev/null +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt @@ -0,0 +1,314 @@ +package com.github.hoshikurama.ticketmanager.common.databases + +import com.github.hoshikurama.ticketmanager.common.* +import com.github.hoshikurama.ticketmanager.common.ticket.BasicTicket +import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.BufferedWriter +import java.nio.charset.Charset +import java.nio.file.Files +import java.nio.file.Path +import java.time.Instant +import java.util.* +import kotlin.coroutines.CoroutineContext +import kotlin.io.path.createFile +import kotlin.io.path.exists +import kotlin.io.path.notExists + +@OptIn(DelicateCoroutinesApi::class) +class Memory( + private val filePath: String, + backupFrequency: Long +) : Database { + override val type = Database.Type.Memory + + private val mapMutex = ReadWriteMutex() + private val ticketMap: MutableMap + private val fileIOOccurring = MutexControlled(false) + + private val nextTicketID: IncrementalMutexController + private val backupJob: Job + + init { + val path = Path.of("$filePath/TicketManager-Database4-Memory.ticketmanager") + + if (path.exists()) { + val encodedMap = Files.readString(path) + ticketMap = Json.decodeFromString(encodedMap) + + val highestID = ticketMap.maxByOrNull { it.key }?.key ?: 0 + nextTicketID = IncrementalMutexController(highestID + 1) + } else { + ticketMap = mutableMapOf() + nextTicketID = IncrementalMutexController(1) + } + + // Launches backup system for duration of database + backupJob = GlobalScope.launch(Dispatchers.IO) { + while (true) { + delay(1000L * backupFrequency) + + if (!fileIOOccurring.get()) { + fileIOOccurring.set(true) + writeDatabaseToFileBlocking() + fileIOOccurring.set(false) + } + } + } + } + + override suspend fun getActionsAsFlow(ticketID: Int): Flow { + return ticketMap[ticketID]!!.actions.asFlow() + } + + override suspend fun setAssignment(ticketID: Int, assignment: String?) { + mapMutex.write.withLock { + val t = ticketMap[ticketID]!! + ticketMap[ticketID] = FullTicket(t.id, t.creatorUUID, t.location, t.priority, t.status, assignment, t.creatorStatusUpdate, t.actions) + } + } + + override suspend fun setCreatorStatusUpdate(ticketID: Int, status: Boolean) { + mapMutex.write.withLock { + val t = ticketMap[ticketID]!! + ticketMap[ticketID] = FullTicket(t.id, t.creatorUUID, t.location, t.priority, t.status, t.assignedTo, status, t.actions) + } + } + + override suspend fun setPriority(ticketID: Int, priority: BasicTicket.Priority) { + mapMutex.write.withLock { + val t = ticketMap[ticketID]!! + ticketMap[ticketID] = FullTicket(t.id, t.creatorUUID, t.location, priority, t.status, t.assignedTo, t.creatorStatusUpdate, t.actions) + } + } + + override suspend fun setStatus(ticketID: Int, status: BasicTicket.Status) { + mapMutex.write.withLock { + val t = ticketMap[ticketID]!! + ticketMap[ticketID] = FullTicket(t.id, t.creatorUUID, t.location, t.priority, status, t.assignedTo, t.creatorStatusUpdate, t.actions) + } + } + + override suspend fun getBasicTicket(ticketID: Int): BasicTicket? { + return mapMutex.read.withLock { ticketMap[ticketID] } + } + + override suspend fun addAction(ticketID: Int, action: FullTicket.Action) { + mapMutex.write.withLock { + val t = ticketMap[ticketID]!! + ticketMap[ticketID] = FullTicket(t.id, t.creatorUUID, t.location, t.priority, t.status, t.assignedTo, t.creatorStatusUpdate, t.actions + action) + } + } + + override suspend fun addFullTicket(fullTicket: FullTicket) { + val newID = nextTicketID.getAndIncrement() + val newTicket = FullTicket(newID, fullTicket.creatorUUID, fullTicket.location, fullTicket.priority, fullTicket.status, fullTicket.assignedTo, fullTicket.creatorStatusUpdate, fullTicket.actions) + + mapMutex.write.withLock { + ticketMap[newID] = newTicket + } + } + + override suspend fun addNewTicket(basicTicket: BasicTicket, context: CoroutineContext, message: String): Int { + val id = nextTicketID.getAndIncrement() + val action = FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message) + val fullTicket = FullTicket(basicTicket.id, basicTicket.creatorUUID, basicTicket.location, basicTicket.priority, basicTicket.status, basicTicket.assignedTo, basicTicket.creatorStatusUpdate, listOf(action)) + + withContext(context) { + launch { addFullTicket(fullTicket) } + } + + return id + } + + override suspend fun massCloseTickets(lowerBound: Int, upperBound: Int, uuid: UUID?, context: CoroutineContext) { + val ticketsToChange = mutableListOf() + + (lowerBound..upperBound).forEach { ticketID -> + mapMutex.read.withLock { + val ticket = ticketMap[ticketID] + + if (ticket != null && ticket.status == BasicTicket.Status.OPEN) { + ticketsToChange += ticket + } + } + } + + val curTime = Instant.now().epochSecond + val newTickets = ticketsToChange.map { + val action = FullTicket.Action(FullTicket.Action.Type.MASS_CLOSE, uuid, timestamp = curTime) + FullTicket(it.id, it.creatorUUID, it.location, it.priority, BasicTicket.Status.CLOSED, it.assignedTo, it.creatorStatusUpdate, it.actions + action) + } + + mapMutex.write.withLock { + newTickets.forEach { + ticketMap[it.id] = it + } + } + } + + override suspend fun getOpenIDPriorityPairs(): Flow> { + val openTickets: Sequence + + mapMutex.read.withLock { + openTickets = ticketMap.asSequence() + .map { it.value } + .filter { it.status == BasicTicket.Status.OPEN } + } + + return openTickets.map { it.id to it.priority.level }.asFlow() + } + + override suspend fun getAssignedOpenIDPriorityPairs( + assignment: String, + unfixedGroupAssignment: List + ): Flow> { + val openTickets: Sequence + val assignments = unfixedGroupAssignment.map { "::$it" } + assignment + + mapMutex.read.withLock { + openTickets = ticketMap.asSequence() + .map { it.value } + .filter { it.status == BasicTicket.Status.OPEN } + .filter { it.assignedTo in assignments } + } + + return openTickets.map { it.id to it.priority.level }.asFlow() + } + + override suspend fun getIDsWithUpdates(): Flow { + return mapMutex.read.withLock { + ticketMap.asSequence() + .map { it.value } + .filter { it.creatorStatusUpdate } + .map { it.id } + }.asFlow() + } + + override suspend fun getIDsWithUpdatesFor(uuid: UUID): Flow { + return mapMutex.read.withLock { + ticketMap.asSequence() + .map { it.value } + .filter { it.creatorUUID?.equals(uuid) == true } + .filter { it.creatorStatusUpdate } + .map { it.id } + }.asFlow() + } + + override suspend fun getBasicTickets(ids: List): Flow { + return mapMutex.read.withLock { + ids.mapNotNull { ticketMap[it] } + }.asFlow() + } + + override suspend fun getFullTicketsFromBasics( + basicTickets: List, + context: CoroutineContext + ): Flow { + val ids = basicTickets.map { it.id } + + return getBasicTickets(ids).map { it as FullTicket }.toList().asFlow() + } + + override suspend fun getFullTickets(ids: List, context: CoroutineContext): Flow { + return mapMutex.read.withLock { + ids.mapNotNull { ticketMap[it] } + }.asFlow() + } + + override suspend fun searchDatabase( + context: CoroutineContext, + locale: TMLocale, + mainTableConstraints: List>, + searchFunction: (FullTicket) -> Boolean + ): Flow { + val mainToFunction = mainTableConstraints.mapNotNull { (word, arg) -> + when (word) { + locale.searchAssigned -> { t: FullTicket -> t.assignedTo == arg } + locale.searchCreator -> { + val uuid = arg?.run(UUID::fromString); + { t: FullTicket -> t.creatorUUID == uuid } + } + locale.searchPriority -> { + val priority = arg!!.toByte().run(::byteToPriority); + { t: FullTicket -> t.priority == priority } + } + locale.searchStatus -> { + val status = BasicTicket.Status.valueOf(arg!!); + { t: FullTicket -> t.status == status } + } + else -> null + } + } + val newSearchFunction = { t: FullTicket -> mainToFunction.all { it(t) } && searchFunction(t) } + + return mapMutex.read.withLock { + ticketMap.asSequence() + .filter { newSearchFunction(it.value) } + .map { it.value } + }.asFlow() + } + + override suspend fun closeDatabase() { + while (fileIOOccurring.get()) delay(100) + + // Cancels GlobalScope backup job + backupJob.cancel() + + fileIOOccurring.set(true) + writeDatabaseToFileBlocking() + fileIOOccurring.set(false) + } + + override suspend fun initialiseDatabase() { + // Done on object instantiation + } + + override suspend fun updateNeeded(): Boolean { + return false // Introduced as V4 db with TM5 + } + + override suspend fun migrateDatabase( + context: CoroutineContext, + to: Database.Type, + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit + ) { + TODO("Not yet implemented") + } + + override suspend fun updateDatabase( + context: CoroutineContext, + onBegin: suspend () -> Unit, + onComplete: suspend () -> Unit, + offlinePlayerNameToUuidOrNull: (String) -> UUID? + ) { + // Not Applicable yet + } + + private suspend fun writeDatabaseToFileBlocking() { + val path = Path.of("$filePath/TicketManager-Database4-Memory.ticketmanager") + if (path.notExists()) path.createFile() + + val encodedString: String + mapMutex.read.withLock { + encodedString = Json.encodeToString(ticketMap) + } + + var writer: BufferedWriter? = null + try { + writer = Files.newBufferedWriter(path, Charset.forName("UTF-8")) + writer.write(encodedString) + } finally { + writer?.close() + } + } +} \ No newline at end of file From fa5559b46f0aded6833057920a5904283391bc03 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 13 Jul 2021 18:45:09 -0500 Subject: [PATCH 23/31] Finished database conversion support --- .../ticketmanager/paper/events/Commands.kt | 48 +++++++++++-- Paper/src/main/resources/config.yml | 2 +- .../common/databases/Database.kt | 3 + .../ticketmanager/common/databases/Memory.kt | 25 ++++++- .../ticketmanager/common/databases/MySQL.kt | 41 ++++++++++- .../ticketmanager/common/databases/SQLite.kt | 68 ++++++++----------- 6 files changed, 140 insertions(+), 47 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index cc42325..ef9c2b4 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -6,6 +6,9 @@ import com.github.hoshikurama.componentDSL.onClick import com.github.hoshikurama.componentDSL.onHover import com.github.hoshikurama.ticketmanager.common.* import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.databases.Memory +import com.github.hoshikurama.ticketmanager.common.databases.MySQL +import com.github.hoshikurama.ticketmanager.common.databases.SQLite import com.github.hoshikurama.ticketmanager.common.ticket.* import com.github.hoshikurama.ticketmanager.paper.* import com.github.shynixn.mccoroutine.SuspendingCommandExecutor @@ -622,11 +625,48 @@ class Commands : SuspendingCommandExecutor { // /ticket convertdatabase private suspend fun convertDatabase(args: List) { - //TODO - /* val type = args[1].run(Database.Type::valueOf) - pluginState.database.migrateDatabase(type) - */ + val config = mainPlugin.config + + configState.database.migrateDatabase( + context = asyncContext, + to = configState.database.type, + sqLiteBuilder = { SQLite(mainPlugin.dataFolder.absolutePath) }, + mySQLBuilder = { + MySQL( + config.getString("MySQL_Host")!!, + config.getString("MySQL_Port")!!, + config.getString("MySQL_DBName")!!, + config.getString("MySQL_Username")!!, + config.getString("MySQL_Password")!!, + asyncDispatcher = (mainPlugin.asyncDispatcher as CoroutineDispatcher) + ) + }, + memoryBuilder = { + Memory( + filePath = mainPlugin.dataFolder.absolutePath, + backupFrequency = config.getLong("Memory_Backup_Frequency", 600), + ) + }, + onBegin = { + mainPlugin.pluginState.pluginLocked.set(true) + pushMassNotify("ticketmanager.notify.info") { + text { + formattedContent( + it.informationDBConvertInit + .replace("%fromDB%", configState.database.type.name) + .replace("%toDB%", type.name) + ) + } + } + }, + onComplete = { + mainPlugin.pluginState.pluginLocked.set(false) + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationDBConvertSuccess) } + } + } + ) } // /ticket create diff --git a/Paper/src/main/resources/config.yml b/Paper/src/main/resources/config.yml index 47e8998..5e21e94 100644 --- a/Paper/src/main/resources/config.yml +++ b/Paper/src/main/resources/config.yml @@ -59,7 +59,7 @@ Cooldown_Time: 0 # + No external database needed. # + Faster than MySQL for search and history command. # Cons: - Stores data in TicketManager folder. -# - Easier to overwhelm than Redis or MySQL. +# - Easier to overwhelm than Memory or MySQL. # # MySQL: # Stores information in a database that may or may not be on the server. diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt index 56f394d..de9951a 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Database.kt @@ -58,6 +58,9 @@ interface Database { suspend fun migrateDatabase( context: CoroutineContext, to: Type, + mySQLBuilder: suspend () -> MySQL, + sqLiteBuilder: suspend () -> SQLite, + memoryBuilder: suspend () -> Memory, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit, ) diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt index 81aa1bc..53979ad 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt @@ -279,10 +279,33 @@ class Memory( override suspend fun migrateDatabase( context: CoroutineContext, to: Database.Type, + mySQLBuilder: suspend () -> MySQL, + sqLiteBuilder: suspend () -> SQLite, + memoryBuilder: suspend () -> Memory, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit ) { - TODO("Not yet implemented") + onBegin() + + when (to) { + Database.Type.Memory -> return + + Database.Type.MySQL, + Database.Type.SQLite -> { + val otherDB = if (to == Database.Type.MySQL) mySQLBuilder() else sqLiteBuilder() + + mapMutex.read.withLock { + ticketMap.map { it.value } + } + .forEach { + withContext(context) { + launch { otherDB.addFullTicket(it) } + } + } + } + } + + onComplete() } override suspend fun updateDatabase( diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt index d88b55e..3f6c557 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/MySQL.kt @@ -295,10 +295,49 @@ class MySQL( override suspend fun migrateDatabase( context: CoroutineContext, to: Database.Type, + mySQLBuilder: suspend () -> MySQL, + sqLiteBuilder: suspend () -> SQLite, + memoryBuilder: suspend () -> Memory, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit ) { - TODO("Not yet implemented") + onBegin() + + when (to) { + Database.Type.MySQL -> return + + Database.Type.SQLite -> { + val sqlite = sqLiteBuilder() + sqlite.initialiseDatabase() + + // Gets all tables from MySQL + suspendingCon.sendPreparedStatement("SELECT * FROM TicketManager_V4_Tickets") + .rows + .map { it.toBasicTicket().toFullTicket(this) } + .forEach { + withContext(context) { + launch { sqlite.addFullTicket(it) } + } + } + + sqlite.closeDatabase() + } + + Database.Type.Memory -> { + val memory = sqLiteBuilder() + memory.initialiseDatabase() + + // Gets all tables from MySQL + suspendingCon.sendPreparedStatement("SELECT * FROM TicketManager_V4_Tickets") + .rows + .map { it.toBasicTicket().toFullTicket(this) } + .forEach { memory.addFullTicket(it) } + + memory.closeDatabase() + } + } + + onComplete() } override suspend fun updateDatabase( diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index ce1602f..e42f1d6 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -8,6 +8,8 @@ import com.github.hoshikurama.ticketmanager.common.ticket.FullTicket import com.github.hoshikurama.ticketmanager.common.ticket.toTicketLocation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotliquery.* import java.sql.DriverManager import java.time.Instant @@ -281,55 +283,41 @@ class SQLite(absoluteDataFolderPath: String) : Database { override suspend fun migrateDatabase( context: CoroutineContext, to: Database.Type, + mySQLBuilder: suspend () -> MySQL, + sqLiteBuilder: suspend () -> SQLite, + memoryBuilder: suspend () -> Memory, onBegin: suspend () -> Unit, onComplete: suspend () -> Unit ) { - //TODO use more functions like () -> Database - /* - @Suppress("BlockingMethodInNonBlockingContext") - runBlocking { - when (to) { - Database.Type.SQLite -> {} // SQLite -> SQLite is not permitted - - Database.Type.MySQL -> { - pushMassNotify("ticketmanager.notify.info", { - it.informationDBConvertInit - .replace("%fromDB%", Database.Types.SQLite.name) - .replace("%toDB%", Database.Types.MySQL.name) - } ) - - try { - val config = mainPlugin.config - val mySQL = MySQL( - config.getString("MySQL_Host")!!, - config.getString("MySQL_Port")!!, - config.getString("MySQL_DBName")!!, - config.getString("MySQL_Username")!!, - config.getString("MySQL_Password")!! - ) - - // Writes to MySQL - using(getSession()) { session -> - session.forEach(queryOf("SELECT * FROM TicketManager_V4_Tickets")) { row -> //NOTE: During conversion, ticket ID is not guaranteed to be preserved - row.toTicket(session).apply { - val newID = mySQL.addTicket(this, actions[0]) - if (actions.size > 1) actions.subList(1, actions.size) - .forEach { mySQL.addAction(newID, it) } - } - } - } + onBegin() - pushMassNotify("ticketmanager.notify.info", { it.informationDBConvertSuccess } ) + when (to) { + Database.Type.SQLite -> return - } catch (e: Exception) { - e.printStackTrace() - postModifiedStacktrace(e) - } + Database.Type.MySQL, + Database.Type.Memory -> { + val otherDB = if (to == Database.Type.MySQL) mySQLBuilder() else memoryBuilder() + otherDB.initialiseDatabase() + + using(getSession()) { session -> + session.run( + queryOf("SELECT * FROM TicketManager_V4_Tickets") + .map { it.toBasicTicket() } + .asList + ) } + .forEach { + withContext(context) { + launch { + val fullTicket = it.toFullTicket(this@SQLite) + otherDB.addFullTicket(fullTicket) + } + } + } } } - */ + onComplete() } override suspend fun updateDatabase( From 4883b84f7a45589952ce7c85f517f529d3a12311 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 13 Jul 2021 19:39:15 -0500 Subject: [PATCH 24/31] Fixed db conversion issues with Memory storage --- .../ticketmanager/paper/events/Commands.kt | 79 ++++++++++--------- .../ticketmanager/common/databases/Memory.kt | 10 ++- common/src/main/resources/locales/Example.yml | 6 +- common/src/main/resources/locales/en_CA.yml | 2 +- 4 files changed, 54 insertions(+), 43 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index ef9c2b4..64c8399 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -628,45 +628,50 @@ class Commands : SuspendingCommandExecutor { val type = args[1].run(Database.Type::valueOf) val config = mainPlugin.config - configState.database.migrateDatabase( - context = asyncContext, - to = configState.database.type, - sqLiteBuilder = { SQLite(mainPlugin.dataFolder.absolutePath) }, - mySQLBuilder = { - MySQL( - config.getString("MySQL_Host")!!, - config.getString("MySQL_Port")!!, - config.getString("MySQL_DBName")!!, - config.getString("MySQL_Username")!!, - config.getString("MySQL_Password")!!, - asyncDispatcher = (mainPlugin.asyncDispatcher as CoroutineDispatcher) - ) - }, - memoryBuilder = { - Memory( - filePath = mainPlugin.dataFolder.absolutePath, - backupFrequency = config.getLong("Memory_Backup_Frequency", 600), - ) - }, - onBegin = { - mainPlugin.pluginState.pluginLocked.set(true) - pushMassNotify("ticketmanager.notify.info") { - text { - formattedContent( - it.informationDBConvertInit - .replace("%fromDB%", configState.database.type.name) - .replace("%toDB%", type.name) - ) + try { + configState.database.migrateDatabase( + context = asyncContext, + to = type, + sqLiteBuilder = { SQLite(mainPlugin.dataFolder.absolutePath) }, + mySQLBuilder = { + MySQL( + config.getString("MySQL_Host")!!, + config.getString("MySQL_Port")!!, + config.getString("MySQL_DBName")!!, + config.getString("MySQL_Username")!!, + config.getString("MySQL_Password")!!, + asyncDispatcher = (mainPlugin.asyncDispatcher as CoroutineDispatcher) + ) + }, + memoryBuilder = { + Memory( + filePath = mainPlugin.dataFolder.absolutePath, + backupFrequency = config.getLong("Memory_Backup_Frequency", 600), + ) + }, + onBegin = { + mainPlugin.pluginState.pluginLocked.set(true) + pushMassNotify("ticketmanager.notify.info") { + text { + formattedContent( + it.informationDBConvertInit + .replace("%fromDB%", configState.database.type.name) + .replace("%toDB%", type.name) + ) + } + } + }, + onComplete = { + mainPlugin.pluginState.pluginLocked.set(false) + pushMassNotify("ticketmanager.notify.info") { + text { formattedContent(it.informationDBConvertSuccess) } } } - }, - onComplete = { - mainPlugin.pluginState.pluginLocked.set(false) - pushMassNotify("ticketmanager.notify.info") { - text { formattedContent(it.informationDBConvertSuccess) } - } - } - ) + ) + } catch (e: Exception) { + mainPlugin.pluginState.pluginLocked.set(false) + throw e + } } // /ticket create diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt index 53979ad..1b484d4 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt @@ -285,14 +285,16 @@ class Memory( onBegin: suspend () -> Unit, onComplete: suspend () -> Unit ) { - onBegin() - + withContext(context) { + launch { onBegin() } + } when (to) { Database.Type.Memory -> return Database.Type.MySQL, Database.Type.SQLite -> { val otherDB = if (to == Database.Type.MySQL) mySQLBuilder() else sqLiteBuilder() + otherDB.initialiseDatabase() mapMutex.read.withLock { ticketMap.map { it.value } @@ -305,7 +307,9 @@ class Memory( } } - onComplete() + withContext(context) { + launch { onComplete() } + } } override suspend fun updateDatabase( diff --git a/common/src/main/resources/locales/Example.yml b/common/src/main/resources/locales/Example.yml index 537639c..22b4b12 100644 --- a/common/src/main/resources/locales/Example.yml +++ b/common/src/main/resources/locales/Example.yml @@ -8,7 +8,6 @@ # %CC% - Colour Code Theme # %nl% - New Line # ############################ - # View and Deep View Format: ViewFormat_Header: '%num%' ViewFormat_Sep1: '' @@ -74,6 +73,7 @@ Warning_TicketAlreadyOpen: '' Warning_InvalidDBType: '' Warning_ConvertToSameDBType: '' Warning_UnexpectedError: '' +Warning_LongTaskDuringReload: '' # # Modified Stacktrace Stacktrace_Line1: '' @@ -139,6 +139,7 @@ Page_Next: '' Info_ReloadInitiated: '%user%' Info_Reload_TasksDone: '' Info_ReloadSuccess: '' +Info_ReloadFailure: '' Info_DBUpdate: '' Info_DBUpdateComplete: '' Info_DBConversionInit: '' @@ -195,4 +196,5 @@ Notify_Event_TicketCreation: '%user% %id% %%message%' Notify_Event_TicketModification: '%id%' Notify_Event_TicketReopen: '%user% %id%' Notify_Event_SetPriority: '%user% %id% %priority%' -Notify_Event_PluginUpdate: '%current% %latest%' \ No newline at end of file +Notify_Event_PluginUpdate: '%current% %latest%' + diff --git a/common/src/main/resources/locales/en_CA.yml b/common/src/main/resources/locales/en_CA.yml index fd38c7d..7268391 100644 --- a/common/src/main/resources/locales/en_CA.yml +++ b/common/src/main/resources/locales/en_CA.yml @@ -169,7 +169,7 @@ Parameters_Constraints: 'Constraints' Notify_UnreadUpdate_Single: '%CC%[TicketManager] Ticket &7%num%%CC% has an update! Type &7/ticket view %CC% to clear notification.' Notify_UnreadUpdate_Multi: '%CC%[TicketManager] Tickets &7%num%%CC% have updates! Type &7/ticket view %CC% for all updated tickets to clear notifications.' Notify_OpenAssigned: '%CC%[TicketManager]&7 %open% %CC%tickets open (&7%assigned%%CC% assigned to you)' -Notify_MassCloseSuccess: '%CC%[TicketManager] Mass close from &7%low%%CC% to &7%high%%CC% sent!' +Notify_MassCloseSuccess: '%CC%[TicketManager] Mass-close from &7%low%%CC% to &7%high%%CC% sent!' Notify_TicketAssignSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% assigned to:&7 %assign%%CC%!' Notify_TicketCloseSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% closed!' Notify_TicketCloseWithCommentSuccess: '%CC%[TicketManager] Ticket &7#%id%%CC% closed with a comment!' From 12c0ee3f37cab3547a3781d611d2aa4087b27dad Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 13 Jul 2021 19:50:24 -0500 Subject: [PATCH 25/31] Bug Fix- id incremented twice with /ticket create --- .../hoshikurama/ticketmanager/common/databases/Memory.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt index 1b484d4..ba8d0bf 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt @@ -122,8 +122,8 @@ class Memory( val action = FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message) val fullTicket = FullTicket(basicTicket.id, basicTicket.creatorUUID, basicTicket.location, basicTicket.priority, basicTicket.status, basicTicket.assignedTo, basicTicket.creatorStatusUpdate, listOf(action)) - withContext(context) { - launch { addFullTicket(fullTicket) } + mapMutex.write.withLock { + ticketMap[id] = fullTicket } return id From 264fa452afe5121e4f78ac00f33cf8be9d45d953 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Tue, 13 Jul 2021 20:34:44 -0500 Subject: [PATCH 26/31] Things are fixed --- .../hoshikurama/ticketmanager/paper/events/Commands.kt | 7 ++++--- .../hoshikurama/ticketmanager/common/databases/Memory.kt | 6 ++++-- .../hoshikurama/ticketmanager/common/databases/SQLite.kt | 1 + common/src/main/resources/locales/en_CA.yml | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index 64c8399..6ba2a56 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -1121,12 +1121,13 @@ class Commands : SuspendingCommandExecutor { silent: Boolean, ticketHandler: BasicTicketHandler, ): NotifyParams = withContext(asyncContext) { + val newPriority = byteToPriority(args[2].toByte()) launch { configState.database.addAction( ticketID = ticketHandler.id, action = FullTicket.Action(FullTicket.Action.Type.SET_PRIORITY, sender.toUUIDOrNull(), args[2]) ) - ticketHandler.setTicketPriority(byteToPriority(args[2].toByte())) + ticketHandler.setTicketPriority(newPriority) } NotifyParams( @@ -1137,14 +1138,14 @@ class Commands : SuspendingCommandExecutor { senderLambda = { val content = it.notifyTicketSetPrioritySuccess .replace("%id%", args[1]) - .replace("%priority%", ticketHandler.run { priority.colourCode + priority.toLocaledWord(it) }) + .replace("%priority%", ticketHandler.run { newPriority.colourCode + newPriority.toLocaledWord(it) }) text { formattedContent(content) } }, massNotifyLambda = { val content = it.notifyTicketSetPriorityEvent .replace("%user%", sender.name) .replace("%id%", args[1]) - .replace("%priority%", ticketHandler.run { priority.colourCode + priority.toLocaledWord(it) }) + .replace("%priority%", ticketHandler.run { newPriority.colourCode + newPriority.toLocaledWord(it) }) text { formattedContent(content) } }, creatorAlertPerm = "ticketmanager.notify.change.priority", diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt index ba8d0bf..8873b51 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt @@ -120,7 +120,7 @@ class Memory( override suspend fun addNewTicket(basicTicket: BasicTicket, context: CoroutineContext, message: String): Int { val id = nextTicketID.getAndIncrement() val action = FullTicket.Action(FullTicket.Action.Type.OPEN, basicTicket.creatorUUID, message) - val fullTicket = FullTicket(basicTicket.id, basicTicket.creatorUUID, basicTicket.location, basicTicket.priority, basicTicket.status, basicTicket.assignedTo, basicTicket.creatorStatusUpdate, listOf(action)) + val fullTicket = FullTicket(id, basicTicket.creatorUUID, basicTicket.location, basicTicket.priority, basicTicket.status, basicTicket.assignedTo, basicTicket.creatorStatusUpdate, listOf(action)) mapMutex.write.withLock { ticketMap[id] = fullTicket @@ -299,11 +299,13 @@ class Memory( mapMutex.read.withLock { ticketMap.map { it.value } } - .forEach { + .map { withContext(context) { launch { otherDB.addFullTicket(it) } } } + + otherDB.closeDatabase() } } diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt index e42f1d6..27a8c69 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/SQLite.kt @@ -314,6 +314,7 @@ class SQLite(absoluteDataFolderPath: String) : Database { } } } + otherDB.closeDatabase() } } diff --git a/common/src/main/resources/locales/en_CA.yml b/common/src/main/resources/locales/en_CA.yml index 7268391..638554b 100644 --- a/common/src/main/resources/locales/en_CA.yml +++ b/common/src/main/resources/locales/en_CA.yml @@ -184,6 +184,6 @@ Notify_Event_MassClose: '%CC%[TicketManager]&7 %user%%CC% is mass-closing ticket Notify_Event_TicketComment: '%CC%[TicketManager]&7 %user%%CC% commented on ticket &7#%id%%CC%:&7%nl%%message%' Notify_Event_TicketCreation: '%CC%[TicketManager] &7%user%%CC% created ticket &7#%id%%CC%:&7%nl%%message%' Notify_Event_TicketModification: '%CC%[TicketManager] Ticket &7#%id%%CC% has been updated! Type &7/ticket view %id%%CC% to view this ticket.' -Notify_Event_TicketReopen: '%CC%[TicketManager]&7 %user%%CC% re-opened ticket &7#%id%%CC%.' +Notify_Event_TicketReopen: '%CC%[TicketManager]&7 %user%%CC% reopened ticket &7#%id%%CC%.' Notify_Event_SetPriority: '%CC%[TicketManager] &7%user%%CC% set ticket &7#%id%%CC% to priority %priority%' Notify_Event_PluginUpdate: '%CC%[TicketManager] TicketManager has an update!%nl% %CC%Current Version: &7%current%%nl% %CC%Latest Version: &7%latest%' \ No newline at end of file From 25a62251353b9ae276bf24da5d1c71657cacf3ea Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Wed, 14 Jul 2021 10:26:52 -0500 Subject: [PATCH 27/31] Update to Kotlin 1.5.21 --- Paper/build.gradle.kts | 2 +- Paper/src/main/resources/plugin.yml | 2 +- build.gradle.kts | 4 ++-- common/build.gradle.kts | 2 +- .../hoshikurama/ticketmanager/common/databases/Memory.kt | 1 + 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts index 0821cbe..832f129 100644 --- a/Paper/build.gradle.kts +++ b/Paper/build.gradle.kts @@ -17,7 +17,7 @@ repositories { dependencies { compileOnly("io.papermc.paper:paper-api:1.17.1-R0.1-SNAPSHOT") - implementation(kotlin("stdlib", version = "1.5.20")) + implementation(kotlin("stdlib", version = "1.5.21")) implementation("com.github.HoshiKurama:KyoriComponentDSL:1.1.0") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1") implementation("net.kyori:adventure-extra-kotlin:4.8.1") diff --git a/Paper/src/main/resources/plugin.yml b/Paper/src/main/resources/plugin.yml index df6cb9c..02494d8 100644 --- a/Paper/src/main/resources/plugin.yml +++ b/Paper/src/main/resources/plugin.yml @@ -5,7 +5,7 @@ api-version: 1.17 authors: [HoshiKurama] depend: [Vault] libraries: - - org.jetbrains.kotlin:kotlin-stdlib:1.5.20 + - org.jetbrains.kotlin:kotlin-stdlib:1.5.21 - mysql:mysql-connector-java:8.0.25 - org.xerial:sqlite-jdbc:3.34.0 - com.github.jasync-sql:jasync-mysql:1.2.2 diff --git a/build.gradle.kts b/build.gradle.kts index 164d38a..aa8ae7c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - kotlin("jvm") version "1.5.20" + kotlin("jvm") version "1.5.21" java } @@ -10,7 +10,7 @@ repositories { } dependencies { - implementation(kotlin("stdlib", version = "1.5.20")) + implementation(kotlin("stdlib", version = "1.5.21")) } subprojects { diff --git a/common/build.gradle.kts b/common/build.gradle.kts index d7c4406..3634181 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -10,7 +10,7 @@ repositories { } dependencies { - implementation(kotlin("stdlib", version = "1.5.20")) + implementation(kotlin("stdlib", version = "1.5.21")) implementation("mysql:mysql-connector-java:8.0.25") implementation("org.xerial:sqlite-jdbc:3.34.0") implementation("com.github.jasync-sql:jasync-mysql:1.2.2") diff --git a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt index 8873b51..5a0cbd1 100644 --- a/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt +++ b/common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/databases/Memory.kt @@ -323,6 +323,7 @@ class Memory( // Not Applicable yet } + @Suppress("BlockingMethodInNonBlockingContext") private suspend fun writeDatabaseToFileBlocking() { val path = Path.of("$filePath/TicketManager-Database4-Memory.ticketmanager") if (path.notExists()) path.createFile() From b61cf8e5310ca44342c058c5e0213360fd9425b0 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Wed, 14 Jul 2021 11:18:09 -0500 Subject: [PATCH 28/31] Offload serialization core for Server download --- Paper/build.gradle.kts | 13 +------------ Paper/src/main/resources/plugin.yml | 1 + 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Paper/build.gradle.kts b/Paper/build.gradle.kts index 832f129..7820bdf 100644 --- a/Paper/build.gradle.kts +++ b/Paper/build.gradle.kts @@ -26,15 +26,6 @@ dependencies { implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0") implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0") implementation(project(":common")) - - // Used by :common but still needed in plugin.yml - //implementation("mysql:mysql-connector-java:8.0.25") - //implementation("org.xerial:sqlite-jdbc:3.34.0") - //implementation("com.github.jasync-sql:jasync-mysql:1.2.2") - //implementation("com.github.seratch:kotliquery:1.3.1") - //implementation("net.kyori:adventure-text-serializer-legacy:4.8.1") - //implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.2") - //implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.2.2") } tasks { @@ -44,12 +35,10 @@ tasks { dependencies { include(dependency("com.github.HoshiKurama:KyoriComponentDSL:1.1.0")) include(dependency("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.2.2")) - include(dependency("org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.2.2")) //todo test without shading include(project(":common")) relocate("com.github.hoshikurama.componentDSL", "com.github.hoshikurama.ticketmanager.componentDSL") - relocate("kotlinx.serialization", "com.github.hoshikurama.ticketmanager.shaded.kotlinx.serialization") + relocate("kotlinx.serialization.json", "com.github.hoshikurama.ticketmanager.shaded.kotlinx.serialization.json") } - } } \ No newline at end of file diff --git a/Paper/src/main/resources/plugin.yml b/Paper/src/main/resources/plugin.yml index 02494d8..da3ce62 100644 --- a/Paper/src/main/resources/plugin.yml +++ b/Paper/src/main/resources/plugin.yml @@ -16,6 +16,7 @@ libraries: - joda-time:joda-time:2.10.10 - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0 - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0 + - org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.2.2 commands: ticket: description: Base for all TicketManager commands From 8f7453d5dfd38daf5773ab5b0333473f6e9f1953 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Wed, 14 Jul 2021 12:24:11 -0500 Subject: [PATCH 29/31] Added database type to bStats --- .../ticketmanager/paper/Globals.kt | 2 +- .../paper/TicketManagerPlugin.kt | 29 +++++++++++-------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt index b549dda..c019995 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/Globals.kt @@ -1 +1 @@ -package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.buildComponent import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.ConfigState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin internal val configState: ConfigState get() = mainPlugin.configStateInternal internal val asyncContext: CoroutineContext get() = mainPlugin.asyncDispatcher internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configStateInternal.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers().asSequence() .filter { it.has(permission) } .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true internal fun Player.toTMLocale() = configState.localeHandler.getOrDefault(locale().toString()) internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else configState.localeHandler.consoleLocale internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null fun UUID?.toName(locale: TMLocale): String { if (this == null) return locale.consoleName return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" } fun postModifiedStacktrace(e: Exception) { val onlinePlayers = Bukkit.getOnlinePlayers() .asSequence() .filter { it.has("ticketmanager.notify.warning") } .map { it as Audience to it.toTMLocale() } val console = Bukkit.getConsoleSender() as Audience to configState.localeHandler.consoleLocale val playersAndConsole = onlinePlayers + sequenceOf(console) playersAndConsole.forEach { pair -> val audience = pair.first val locale = pair.second audience.sendMessage( buildComponent { // Builds header listOf( locale.stacktraceLine1, locale.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), locale.stacktraceLine3.replace("%message%", e.message ?: "?"), locale.stacktraceLine4, ) .forEach { text { formattedContent(it) } } // Adds stacktrace entries e.stackTrace .filter { it.className.startsWith("com.github.hoshikurama.ticketmanager") } .map { locale.stacktraceEntry .replace("%method%", it.methodName) .replace("%file%", it.fileName ?: "?") .replace("%line%", "${it.lineNumber}") } .forEach { text { formattedContent(it) } } } ) } } \ No newline at end of file +package com.github.hoshikurama.ticketmanager.paper import com.github.hoshikurama.componentDSL.buildComponent import com.github.hoshikurama.componentDSL.formattedContent import com.github.hoshikurama.ticketmanager.common.ConfigState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import net.kyori.adventure.audience.Audience import net.kyori.adventure.text.Component import org.bukkit.Bukkit import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin internal val configState: ConfigState get() = mainPlugin.configStateI internal val asyncContext: CoroutineContext get() = mainPlugin.asyncDispatcher internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> Component) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configStateI.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers().asSequence() .filter { it.has(permission) } .forEach { localeMsg(it.toTMLocale()).run(it::sendMessage) } } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true internal fun Player.toTMLocale() = configState.localeHandler.getOrDefault(locale().toString()) internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else configState.localeHandler.consoleLocale internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null fun UUID?.toName(locale: TMLocale): String { if (this == null) return locale.consoleName return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" } fun postModifiedStacktrace(e: Exception) { val onlinePlayers = Bukkit.getOnlinePlayers() .asSequence() .filter { it.has("ticketmanager.notify.warning") } .map { it as Audience to it.toTMLocale() } val console = Bukkit.getConsoleSender() as Audience to configState.localeHandler.consoleLocale val playersAndConsole = onlinePlayers + sequenceOf(console) playersAndConsole.forEach { pair -> val audience = pair.first val locale = pair.second audience.sendMessage( buildComponent { // Builds header listOf( locale.stacktraceLine1, locale.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), locale.stacktraceLine3.replace("%message%", e.message ?: "?"), locale.stacktraceLine4, ) .forEach { text { formattedContent(it) } } // Adds stacktrace entries e.stackTrace .filter { it.className.startsWith("com.github.hoshikurama.ticketmanager") } .map { locale.stacktraceEntry .replace("%method%", it.methodName) .replace("%file%", it.fileName ?: "?") .replace("%line%", "${it.lineNumber}") } .forEach { text { formattedContent(it) } } } ) } } \ No newline at end of file diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt index e46ad1c..a057101 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/TicketManagerPlugin.kt @@ -20,7 +20,7 @@ import java.io.File class TicketManagerPlugin : SuspendingJavaPlugin() { internal val pluginState = PluginState() internal lateinit var perms: Permission private set - internal lateinit var configStateInternal: ConfigState + internal lateinit var configStateI: ConfigState private lateinit var metrics: Metrics @@ -30,7 +30,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { override suspend fun onDisableAsync() { pluginState.pluginLocked.set(true) - configStateInternal.database.closeDatabase() + configStateI.database.closeDatabase() } override fun onEnable() { @@ -52,6 +52,11 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } } ) + metrics.addCustomChart( + Metrics.SimplePie("database_type") { + configStateI.database.type.name + } + ) } //todo add pie chart for database type being used // Launches ConfigState initialisation @@ -62,19 +67,19 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { // Creates task timers Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, Runnable { - launchAsync { configStateInternal.cooldowns.filterMapAsync() } + launchAsync { configStateI.cooldowns.filterMapAsync() } launchAsync { if (pluginState.pluginLocked.get()) return@launchAsync try { // Mass Unread Notify - if (configStateInternal.allowUnreadTicketUpdates) { + if (configStateI.allowUnreadTicketUpdates) { Bukkit.getOnlinePlayers().asFlow() .filter { it.has("ticketmanager.notify.unreadUpdates.scheduled") } .onEach { launch { - val ticketIDs = configStateInternal.database.getIDsWithUpdatesFor(it.uniqueId).toList() + val ticketIDs = configStateI.database.getIDsWithUpdatesFor(it.uniqueId).toList() val tickets = ticketIDs.joinToString(", ") if (ticketIDs.isEmpty()) return@launch @@ -88,9 +93,9 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } } - val openPriority = configStateInternal.database.getOpenIDPriorityPairs().map { it.first }.toList() + val openPriority = configStateI.database.getOpenIDPriorityPairs().map { it.first }.toList() val openCount = openPriority.count() - val assignments = configStateInternal.database.getBasicTickets(openPriority).mapNotNull { it.assignedTo }.toList() + val assignments = configStateI.database.getBasicTickets(openPriority).mapNotNull { it.assignedTo }.toList() // Open and Assigned Notify Bukkit.getOnlinePlayers().asFlow() @@ -119,14 +124,14 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { internal suspend fun loadPlugin() = withContext(plugin.asyncDispatcher) { pluginState.pluginLocked.set(true) - configStateInternal = run { + configStateI = run { // Creates config file if not found if (!File(plugin.dataFolder, "config.yml").exists()) { plugin.saveDefaultConfig() // Notifies users config was generated after plugin state init launch { - while (!(::configStateInternal.isInitialized)) + while (!(::configStateI.isInitialized)) delay(100L) pushMassNotify("ticketmanager.notify.warning") { text { formattedContent(it.warningsNoConfig) } } } @@ -199,10 +204,10 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { } launch { - val updateNeeded = configStateInternal.database.updateNeeded() + val updateNeeded = configStateI.database.updateNeeded() if (updateNeeded) { - configStateInternal.database.updateDatabase( + configStateI.database.updateDatabase( onBegin = { pushMassNotify("ticketmanager.notify.info") { text { formattedContent(it.informationDBUpdate) } @@ -228,7 +233,7 @@ class TicketManagerPlugin : SuspendingJavaPlugin() { withContext(minecraftDispatcher) { // Register events and commands - configStateInternal.localeHandler.getCommandBases().forEach { + configStateI.localeHandler.getCommandBases().forEach { getCommand(it)!!.setSuspendingExecutor(Commands()) server.pluginManager.registerEvents(TabComplete(), this@TicketManagerPlugin) // Remember to register any keyword in plugin.yml From ce17590ff238e85f085cef9929a94827bfd71b58 Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Wed, 14 Jul 2021 16:18:50 -0500 Subject: [PATCH 30/31] Changed a few things to references --- .../hoshikurama/ticketmanager/paper/events/Commands.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt index 6ba2a56..dd55176 100644 --- a/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt +++ b/Paper/src/main/kotlin/com/github/hoshikurama/ticketmanager/paper/events/Commands.kt @@ -332,13 +332,14 @@ class Commands : SuspendingCommandExecutor { ) { params.run { if (sendSenderMSG) - sender.sendMessage(senderLambda!!.invoke(locale)) + senderLambda!!(locale) + .run(sender::sendMessage) if (sendCreatorMSG) basicTicket.creatorUUID ?.run(Bukkit::getPlayer) ?.let { creatorLambda!!(it.toTMLocale()) } - ?.run { creator!!.sendMessage(this) } + ?.run(creator!!::sendMessage) if (sendMassNotifyMSG) pushMassNotify(massNotifyPerm, massNotifyLambda!!) From 6511f7ff3af185e62149cc8305451267efb814fe Mon Sep 17 00:00:00 2001 From: HoshiKurama <52633255+HoshiKurama@users.noreply.github.com> Date: Wed, 14 Jul 2021 16:19:42 -0500 Subject: [PATCH 31/31] Added Spigot Support --- .gitignore | 2 + Spigot/build.gradle.kts | 43 + .../ticketmanager/spigot/Globals.kt | 1 + .../spigot/MetricsKotlinBukkit.kt | 811 +++++++++ .../spigot/TicketManagerPlugin.kt | 240 +++ .../ticketmanager/spigot/events/Commands.kt | 1447 +++++++++++++++++ .../ticketmanager/spigot/events/PlayerJoin.kt | 68 + .../spigot/events/TabComplete.kt | 314 ++++ Spigot/src/main/resources/config.yml | 119 ++ Spigot/src/main/resources/plugin.yml | 244 +++ settings.gradle.kts | 3 +- 11 files changed, 3291 insertions(+), 1 deletion(-) create mode 100644 Spigot/build.gradle.kts create mode 100644 Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/Globals.kt create mode 100644 Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/MetricsKotlinBukkit.kt create mode 100644 Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/TicketManagerPlugin.kt create mode 100644 Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/Commands.kt create mode 100644 Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/PlayerJoin.kt create mode 100644 Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/TabComplete.kt create mode 100644 Spigot/src/main/resources/config.yml create mode 100644 Spigot/src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore index f442b64..f2127b8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ /common/build/ /common/src/main/kotlin/com/github/hoshikurama/ticketmanager/common/API_KEYS.kt /src/ +/Spigot/Spigot.iml +/Spigot/build/ diff --git a/Spigot/build.gradle.kts b/Spigot/build.gradle.kts new file mode 100644 index 0000000..ca24c22 --- /dev/null +++ b/Spigot/build.gradle.kts @@ -0,0 +1,43 @@ +plugins { + id("com.github.johnrengelman.shadow") version "7.0.0" + kotlin("jvm") + java + application +} + +application { + mainClass.set("com.github.hoshikurama.ticketmanager.spigot.TicketManagerPlugin") +} + +repositories { + mavenCentral() + maven(url = "https://oss.sonatype.org/content/repositories/snapshots/") { + name = "sonatype-oss-snapshots" + } + maven { url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") } + maven { url = uri("https://jitpack.io") } +} + +dependencies { + compileOnly("org.spigotmc:spigot-api:1.17-R0.1-SNAPSHOT") + implementation(kotlin("stdlib", version = "1.5.21")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1") + implementation("joda-time:joda-time:2.10.10") + compileOnly("com.github.MilkBowl:VaultAPI:1.7") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0") + implementation(project(":common")) +} + +tasks { + named("shadowJar") { + archiveBaseName.set("TicketManager-Spigot") + + dependencies { + include(dependency("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.2.2")) + include(project(":common")) + + relocate("kotlinx.serialization.json", "com.github.hoshikurama.ticketmanager.shaded.kotlinx.serialization.json") + } + } +} \ No newline at end of file diff --git a/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/Globals.kt b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/Globals.kt new file mode 100644 index 0000000..58d3918 --- /dev/null +++ b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/Globals.kt @@ -0,0 +1 @@ +package com.github.hoshikurama.ticketmanager.spigot import com.github.hoshikurama.ticketmanager.common.ConfigState import com.github.hoshikurama.ticketmanager.common.TMLocale import com.github.shynixn.mccoroutine.asyncDispatcher import net.md_5.bungee.api.chat.TextComponent import org.bukkit.Bukkit import org.bukkit.ChatColor import org.bukkit.command.CommandSender import org.bukkit.entity.Player import java.util.* import java.util.logging.Level import kotlin.coroutines.CoroutineContext fun consoleLog(level: Level, message: String) = Bukkit.getLogger().log(level, ChatColor.stripColor(message)) internal val mainPlugin: TicketManagerPlugin get() = TicketManagerPlugin.plugin internal val configState: ConfigState get() = mainPlugin.configStateI internal val asyncContext: CoroutineContext get() = mainPlugin.asyncDispatcher internal fun addColour(s: String) = ChatColor.translateAlternateColorCodes('&', s) internal fun pushMassNotify(permission: String, localeMsg: (TMLocale) -> String) { Bukkit.getConsoleSender().sendMessage(localeMsg(mainPlugin.configStateI.localeHandler.consoleLocale)) Bukkit.getOnlinePlayers() .asSequence() .filter { it.has(permission) } .forEach { val message = it.toTMLocale() .run(localeMsg) .run(::addColour) it.sendMessage(message) } } internal fun Player.has(permission: String) = mainPlugin.perms.has(this, permission) internal fun CommandSender.has(permission: String): Boolean = if (this is Player) has(permission) else true internal fun Player.toTMLocale() = configState.localeHandler.getOrDefault(locale) internal fun CommandSender.toTMLocale() = if (this is Player) toTMLocale() else configState.localeHandler.consoleLocale internal fun CommandSender.toUUIDOrNull() = if (this is Player) this.uniqueId else null fun UUID?.toName(locale: TMLocale): String { if (this == null) return locale.consoleName return this.run(Bukkit::getOfflinePlayer).name ?: "UUID" } fun postModifiedStacktrace(e: Exception) { val players = Bukkit.getOnlinePlayers() .asSequence() .filter { it.has("ticketmanager.notify.warning") } val console = Bukkit.getConsoleSender() val buildMessage: (TMLocale) -> TextComponent = { locale -> val sentComponent = TextComponent("") // Creates header listOf( locale.stacktraceLine1, locale.stacktraceLine2.replace("%exception%", e.javaClass.simpleName), locale.stacktraceLine3.replace("%message%", e.message ?: "?"), locale.stacktraceLine4 ) .map(::addColour) .map(::TextComponent) .forEach { sentComponent.addExtra(it) } // Adds entries e.stackTrace .filter { it.className.startsWith("com.hoshikurama.github.ticketmanager") } .map { locale.stacktraceEntry .replace("%method%", it.methodName) .replace("%file%", it.fileName ?: "?") .replace("%line%", "${it.lineNumber}") } .map(::addColour) .map(::TextComponent) .forEach { sentComponent.addExtra(it) } sentComponent } console.run { buildMessage(this.toTMLocale()) .let{ spigot().sendMessage(it) } } players.forEach { p -> buildMessage(p.toTMLocale()) .let { p.spigot().sendMessage(it) } } } \ No newline at end of file diff --git a/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/MetricsKotlinBukkit.kt b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/MetricsKotlinBukkit.kt new file mode 100644 index 0000000..1917457 --- /dev/null +++ b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/MetricsKotlinBukkit.kt @@ -0,0 +1,811 @@ +package com.github.hoshikurama.ticketmanager.spigot + +import org.bukkit.Bukkit +import org.bukkit.configuration.file.YamlConfiguration +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.java.JavaPlugin +import java.io.* +import java.net.URL +import java.nio.charset.StandardCharsets +import java.util.* +import java.util.concurrent.* +import java.util.function.* +import java.util.logging.Level +import java.util.stream.Collectors +import java.util.zip.GZIPOutputStream +import javax.net.ssl.HttpsURLConnection + +/* Note: This is NOT my code. Converted bStats Java to Kotlin using IntelliJ */ +class Metrics(plugin: JavaPlugin, serviceId: Int) { + private val plugin: Plugin + private val metricsBase: MetricsBase + + /** + * Adds a custom chart. + * + * @param chart The chart to add. + */ + fun addCustomChart(chart: CustomChart) { + metricsBase.addCustomChart(chart) + } + + private fun appendPlatformData(builder: JsonObjectBuilder) { + builder.appendField("playerAmount", playerAmount) + builder.appendField("onlineMode", if (Bukkit.getOnlineMode()) 1 else 0) + builder.appendField("bukkitVersion", Bukkit.getVersion()) + builder.appendField("bukkitName", Bukkit.getName()) + builder.appendField("javaVersion", System.getProperty("java.version")) + builder.appendField("osName", System.getProperty("os.name")) + builder.appendField("osArch", System.getProperty("os.arch")) + builder.appendField("osVersion", System.getProperty("os.version")) + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()) + } + + private fun appendServiceData(builder: JsonObjectBuilder) { + builder.appendField("pluginVersion", plugin.description.version) + }// Just use the new method if the reflection failed + + // Around MC 1.8 the return type was changed from an array to a collection, + // This fixes java.lang.NoSuchMethodError: + // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + private val playerAmount: Int + get() { + return try { + // Around MC 1.8 the return type was changed from an array to a collection, + // This fixes java.lang.NoSuchMethodError: + // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; + val onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers") + if (onlinePlayersMethod.returnType == MutableCollection::class.java) (onlinePlayersMethod.invoke( + Bukkit.getServer() + ) as Collection<*>).size else (onlinePlayersMethod.invoke(Bukkit.getServer()) as Array).size + } catch (e: Exception) { + // Just use the new method if the reflection failed + Bukkit.getOnlinePlayers().size + } + } + + class MetricsBase( + private val platform: String, + private val serverUuid: String, + private val serviceId: Int, + private val enabled: Boolean, + private val appendPlatformDataConsumer: Consumer, + private val appendServiceDataConsumer: Consumer, + private val submitTaskConsumer: Consumer?, + private val checkServiceEnabledSupplier: Supplier, + private val errorLogger: BiConsumer, + private val infoLogger: Consumer, + private val logErrors: Boolean, + private val logSentData: Boolean, + private val logResponseStatusText: Boolean + ) { + private val customCharts: MutableSet = HashSet() + fun addCustomChart(chart: CustomChart) { + customCharts.add(chart) + } + + private fun startSubmitting() { + val submitTask = Runnable { + if (!enabled || !checkServiceEnabledSupplier.get()) { + // Submitting data or service is disabled + scheduler.shutdown() + return@Runnable + } + if (submitTaskConsumer != null) { + submitTaskConsumer.accept(Runnable { submitData() }) + } else { + submitData() + } + } + // Many servers tend to restart at a fixed time at xx:00 which causes an uneven distribution + // of requests on the + // bStats backend. To circumvent this problem, we introduce some randomness into the initial + // and second delay. + // WARNING: You must not modify and part of this Metrics class, including the submit delay or + // frequency! + // WARNING: Modifying this code will get your plugin banned on bStats. Just don't do it! + val initialDelay = (1000 * 60 * (3 + Math.random() * 3)).toLong() + val secondDelay = (1000 * 60 * (Math.random() * 30)).toLong() + scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS) + scheduler.scheduleAtFixedRate( + submitTask, initialDelay + secondDelay, (1000 * 60 * 30).toLong(), TimeUnit.MILLISECONDS + ) + } + + private fun submitData() { + val baseJsonBuilder = JsonObjectBuilder() + appendPlatformDataConsumer.accept(baseJsonBuilder) + val serviceJsonBuilder = JsonObjectBuilder() + appendServiceDataConsumer.accept(serviceJsonBuilder) + val chartData: Array = customCharts + .mapNotNull { it.getRequestJsonObject(errorLogger, logErrors) } + .toTypedArray() + serviceJsonBuilder.appendField("id", serviceId) + serviceJsonBuilder.appendField("customCharts", chartData) + baseJsonBuilder.appendField("service", serviceJsonBuilder.build()) + baseJsonBuilder.appendField("serverUUID", serverUuid) + baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION) + val data = baseJsonBuilder.build() + scheduler.execute { + try { + // Send the data + sendData(data) + } catch (e: Exception) { + // Something went wrong! :( + if (logErrors) { + errorLogger.accept("Could not submit bStats metrics data", e) + } + } + } + } + + @Throws(Exception::class) + private fun sendData(data: JsonObjectBuilder.JsonObject) { + if (logSentData) { + infoLogger.accept("Sent bStats metrics data: $data") + } + val url = String.format(REPORT_URL, platform) + val connection = URL(url).openConnection() as HttpsURLConnection + // Compress the data to save bandwidth + val compressedData = compress(data.toString()) + connection.requestMethod = "POST" + connection.addRequestProperty("Accept", "application/json") + connection.addRequestProperty("Connection", "close") + connection.addRequestProperty("Content-Encoding", "gzip") + connection.addRequestProperty("Content-Length", compressedData!!.size.toString()) + connection.setRequestProperty("Content-Type", "application/json") + connection.setRequestProperty("User-Agent", "Metrics-Service/1") + connection.doOutput = true + DataOutputStream(connection.outputStream).use { outputStream -> outputStream.write(compressedData) } + val builder = StringBuilder() + BufferedReader(InputStreamReader(connection.inputStream)).use { bufferedReader -> + var line: String? + while ((bufferedReader.readLine().also { line = it }) != null) { + builder.append(line) + } + } + if (logResponseStatusText) { + infoLogger.accept("Sent data to bStats and received response: $builder") + } + } + + /** Checks that the class was properly relocated. */ + private fun checkRelocation() { + // You can use the property to disable the check in your test environment + if (System.getProperty("bstats.relocatecheck") == null + || System.getProperty("bstats.relocatecheck") != "false" + ) { + // Maven's Relocate is clever and changes strings, too. So we have to use this little + // "trick" ... :D + val defaultPackage = String( + byteArrayOf( + 'o'.code.toByte(), + 'r'.code.toByte(), + 'g'.code.toByte(), + '.'.code.toByte(), + 'b'.code.toByte(), + 's'.code.toByte(), + 't'.code.toByte(), + 'a'.code.toByte(), + 't'.code.toByte(), + 's'.code.toByte() + ) + ) + val examplePackage = String( + byteArrayOf( + 'y'.code.toByte(), + 'o'.code.toByte(), + 'u'.code.toByte(), + 'r'.code.toByte(), + '.'.code.toByte(), + 'p'.code.toByte(), + 'a'.code.toByte(), + 'c'.code.toByte(), + 'k'.code.toByte(), + 'a'.code.toByte(), + 'g'.code.toByte(), + 'e'.code.toByte() + ) + ) + // We want to make sure no one just copy & pastes the example and uses the wrong package + // names + if (MetricsBase::class.java.getPackage().name.startsWith(defaultPackage) + || MetricsBase::class.java.getPackage().name.startsWith(examplePackage) + ) { + throw IllegalStateException("bStats Metrics class has not been relocated correctly!") + } + } + } + + companion object { + /** The version of the Metrics class. */ + val METRICS_VERSION = "2.2.1" + private val scheduler = Executors.newScheduledThreadPool(1 + ) { task: Runnable? -> Thread(task, "bStats-Metrics") } + private val REPORT_URL = "https://bStats.org/api/v2/data/%s" + + /** + * Gzips the given string. + * + * @param str The string to gzip. + * @return The gzipped string. + */ + @Throws(IOException::class) + private fun compress(str: String?): ByteArray? { + if (str == null) { + return null + } + val outputStream = ByteArrayOutputStream() + GZIPOutputStream(outputStream).use { gzip -> gzip.write(str.toByteArray(StandardCharsets.UTF_8)) } + return outputStream.toByteArray() + } + } + + /** + * Creates a new MetricsBase class instance. + * + * @param platform The platform of the service. + * @param serviceId The id of the service. + * @param serverUuid The server uuid. + * @param enabled Whether or not data sending is enabled. + * @param appendPlatformDataConsumer A consumer that receives a `JsonObjectBuilder` and + * appends all platform-specific data. + * @param appendServiceDataConsumer A consumer that receives a `JsonObjectBuilder` and + * appends all service-specific data. + * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be + * used to delegate the data collection to a another thread to prevent errors caused by + * concurrency. Can be `null`. + * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. + * @param errorLogger A consumer that accepts log message and an error. + * @param infoLogger A consumer that accepts info log messages. + * @param logErrors Whether or not errors should be logged. + * @param logSentData Whether or not the sent data should be logged. + * @param logResponseStatusText Whether or not the response status text should be logged. + */ + init { + checkRelocation() + if (enabled) { + startSubmitting() + } + } + } + + @Suppress("unused") + class AdvancedBarChart + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */( + chartId: String?, // Null = skip the chart// Skip this invalid + private val callable: Callable> + ) : + CustomChart(chartId) { + // Null = skip the chart + @get:Throws(Exception::class) + override val chartData: JsonObjectBuilder.JsonObject? + get() { + val valuesBuilder = JsonObjectBuilder() + val map = callable.call() + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null + } + var allSkipped = true + for (entry: Map.Entry in map.entries) { + if (entry.value.size == 0) { + // Skip this invalid + continue + } + allSkipped = false + valuesBuilder.appendField(entry.key, entry.value) + } + return if (allSkipped) { + // Null = skip the chart + null + } else JsonObjectBuilder().appendField("values", valuesBuilder.build()).build() + } + + } + + @Suppress("unused") + class SimpleBarChart + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */(chartId: String?, private val callable: Callable>) : + CustomChart(chartId) { + // Null = skip the chart + @get:Throws(Exception::class) + override val chartData: JsonObjectBuilder.JsonObject? + get() { + val valuesBuilder = JsonObjectBuilder() + val map = callable.call() + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null + } + for (entry: Map.Entry in map.entries) { + valuesBuilder.appendField(entry.key, intArrayOf(entry.value)) + } + return JsonObjectBuilder().appendField("values", valuesBuilder.build()).build() + } + + } + + @Suppress("unused") + class MultiLineChart + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */( + chartId: String?, // Null = skip the chart// Skip this invalid + private val callable: Callable> + ) : + CustomChart(chartId) { + // Null = skip the chart + @get:Throws(Exception::class) + override val chartData: JsonObjectBuilder.JsonObject? + get() { + val valuesBuilder = JsonObjectBuilder() + val map = callable.call() + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null + } + var allSkipped = true + for (entry: Map.Entry in map.entries) { + if (entry.value == 0) { + // Skip this invalid + continue + } + allSkipped = false + valuesBuilder.appendField(entry.key, entry.value) + } + return if (allSkipped) { + // Null = skip the chart + null + } else JsonObjectBuilder().appendField("values", valuesBuilder.build()).build() + } + + } + + @Suppress("unused") + class AdvancedPie + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */( + chartId: String?, // Null = skip the chart// Skip this invalid + private val callable: Callable> + ) : + CustomChart(chartId) { + // Null = skip the chart + @get:Throws(Exception::class) + override val chartData: JsonObjectBuilder.JsonObject? + get() { + val valuesBuilder = JsonObjectBuilder() + val map = callable.call() + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null + } + var allSkipped = true + for (entry: Map.Entry in map.entries) { + if (entry.value == 0) { + // Skip this invalid + continue + } + allSkipped = false + valuesBuilder.appendField(entry.key, entry.value) + } + return if (allSkipped) { + // Null = skip the chart + null + } else JsonObjectBuilder().appendField("values", valuesBuilder.build()).build() + } + + } + + abstract class CustomChart protected constructor(chartId: String?) { + private val chartId: String + fun getRequestJsonObject( + errorLogger: BiConsumer, logErrors: Boolean + ): JsonObjectBuilder.JsonObject? { + val builder = JsonObjectBuilder() + builder.appendField("chartId", chartId) + try { + val data = chartData + ?: // If the data is null we don't send the chart. + return null + builder.appendField("data", data) + } catch (t: Throwable) { + if (logErrors) { + errorLogger.accept("Failed to get data for custom chart with id $chartId", t) + } + return null + } + return builder.build() + } + + @get:Throws(Exception::class) + protected abstract val chartData: JsonObjectBuilder.JsonObject? + + init { + if (chartId == null) { + throw IllegalArgumentException("chartId must not be null") + } + this.chartId = chartId + } + } + + class SingleLineChart + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */(chartId: String?, private val callable: Callable) : CustomChart(chartId) { + // Null = skip the chart + @get:Throws(Exception::class) + override val chartData: JsonObjectBuilder.JsonObject? + get() { + val value = callable.call() + return if (value == 0) { + // Null = skip the chart + null + } else JsonObjectBuilder().appendField("value", value).build() + } + + } + + @Suppress("unused") + class SimplePie + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */(chartId: String?, private val callable: Callable) : CustomChart(chartId) { + // Null = skip the chart + @get:Throws(Exception::class) + override val chartData: JsonObjectBuilder.JsonObject? + get() { + val value = callable.call() + return if (value == null || value.isEmpty()) { + // Null = skip the chart + null + } else JsonObjectBuilder().appendField("value", value).build() + } + + } + + @Suppress("unused") + class DrilldownPie + /** + * Class constructor. + * + * @param chartId The id of the chart. + * @param callable The callable which is used to request the chart data. + */( + chartId: String?, // Null = skip the chart + private val callable: Callable>> + ) : + CustomChart(chartId) { + // Null = skip the chart + @get:Throws(Exception::class) + override val chartData: JsonObjectBuilder.JsonObject? + get() { + val valuesBuilder = JsonObjectBuilder() + val map = callable.call() + if (map == null || map.isEmpty()) { + // Null = skip the chart + return null + } + var reallyAllSkipped = true + for (entryValues: Map.Entry> in map.entries) { + val valueBuilder = JsonObjectBuilder() + var allSkipped = true + for (valueEntry: Map.Entry in map[entryValues.key]!!.entries) { + valueBuilder.appendField(valueEntry.key, valueEntry.value) + allSkipped = false + } + if (!allSkipped) { + reallyAllSkipped = false + valuesBuilder.appendField(entryValues.key, valueBuilder.build()) + } + } + return if (reallyAllSkipped) { + // Null = skip the chart + null + } else JsonObjectBuilder().appendField("values", valuesBuilder.build()).build() + } + + } + + /** + * An extremely simple JSON builder. + * + * + * While this class is neither feature-rich nor the most performant one, it's sufficient enough + * for its use-case. + */ + class JsonObjectBuilder() { + private var builder: StringBuilder? = StringBuilder() + private var hasAtLeastOneField = false + + /** + * Appends a null field to the JSON. + * + * @param key The key of the field. + * @return A reference to this object. + */ + fun appendNull(key: String?): JsonObjectBuilder { + appendFieldUnescaped(key, "null") + return this + } + + /** + * Appends a string field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + fun appendField(key: String?, value: String?): JsonObjectBuilder { + if (value == null) { + throw IllegalArgumentException("JSON value must not be null") + } + appendFieldUnescaped(key, "\"" + escape(value) + "\"") + return this + } + + /** + * Appends an integer field to the JSON. + * + * @param key The key of the field. + * @param value The value of the field. + * @return A reference to this object. + */ + fun appendField(key: String?, value: Int): JsonObjectBuilder { + appendFieldUnescaped(key, value.toString()) + return this + } + + /** + * Appends an object to the JSON. + * + * @param key The key of the field. + * @param object The object. + * @return A reference to this object. + */ + fun appendField(key: String?, `object`: JsonObject?): JsonObjectBuilder { + if (`object` == null) { + throw IllegalArgumentException("JSON object must not be null") + } + appendFieldUnescaped(key, `object`.toString()) + return this + } + + /** + * Appends a string array to the JSON. + * + * @param key The key of the field. + * @param values The string array. + * @return A reference to this object. + */ + fun appendField(key: String?, values: Array?): JsonObjectBuilder { + if (values == null) { + throw IllegalArgumentException("JSON values must not be null") + } + val escapedValues = Arrays.stream(values) + .map { value: String -> + "\"" + escape( + value + ) + "\"" + } + .collect(Collectors.joining(",")) + appendFieldUnescaped(key, "[$escapedValues]") + return this + } + + /** + * Appends an integer array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + fun appendField(key: String?, values: IntArray?): JsonObjectBuilder { + if (values == null) { + throw IllegalArgumentException("JSON values must not be null") + } + val escapedValues = + Arrays.stream(values).mapToObj { i: Int -> i.toString() }.collect(Collectors.joining(",")) + appendFieldUnescaped(key, "[$escapedValues]") + return this + } + + /** + * Appends an object array to the JSON. + * + * @param key The key of the field. + * @param values The integer array. + * @return A reference to this object. + */ + fun appendField(key: String?, values: Array?): JsonObjectBuilder { + if (values == null) { + throw IllegalArgumentException("JSON values must not be null") + } + val escapedValues = Arrays.stream(values).map { obj: JsonObject? -> obj.toString() } + .collect(Collectors.joining(",")) + appendFieldUnescaped(key, "[$escapedValues]") + return this + } + + /** + * Appends a field to the object. + * + * @param key The key of the field. + * @param escapedValue The escaped value of the field. + */ + private fun appendFieldUnescaped(key: String?, escapedValue: String) { + if (builder == null) { + throw IllegalStateException("JSON has already been built") + } + if (key == null) { + throw IllegalArgumentException("JSON key must not be null") + } + if (hasAtLeastOneField) { + builder!!.append(",") + } + builder!!.append("\"").append(escape(key)).append("\":").append(escapedValue) + hasAtLeastOneField = true + } + + /** + * Builds the JSON string and invalidates this builder. + * + * @return The built JSON string. + */ + fun build(): JsonObject { + if (builder == null) { + throw IllegalStateException("JSON has already been built") + } + val `object` = JsonObject(builder!!.append("}").toString()) + builder = null + return `object` + } + + /** + * A super simple representation of a JSON object. + * + * + * This class only exists to make methods of the [JsonObjectBuilder] type-safe and not + * allow a raw string inputs for methods like [JsonObjectBuilder.appendField]. + */ + class JsonObject(private val value: String) { + override fun toString(): String { + return value + } + } + + companion object { + /** + * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. + * + * + * This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. + * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). + * + * @param value The value to escape. + * @return The escaped value. + */ + private fun escape(value: String): String { + val builder = StringBuilder() + for (i in 0 until value.length) { + val c = value[i] + if (c == '"') { + builder.append("\\\"") + } else if (c == '\\') { + builder.append("\\\\") + } else if (c <= '\u000F') { + builder.append("\\u000").append(Integer.toHexString(c.code)) + } else if (c <= '\u001F') { + builder.append("\\u00").append(Integer.toHexString(c.code)) + } else { + builder.append(c) + } + } + return builder.toString() + } + } + + init { + builder!!.append("{") + } + } + + /** + * Creates a new Metrics instance. + * + * @param plugin Your plugin instance. + * @param serviceId The id of the service. It can be found at [What is my plugin id?](https://bstats.org/what-is-my-plugin-id) + */ + init { + this.plugin = plugin + // Get the config file + val bStatsFolder = File(plugin.dataFolder.parentFile, "bStats") + val configFile = File(bStatsFolder, "config.yml") + val config = YamlConfiguration.loadConfiguration(configFile) + if (!config.isSet("serverUuid")) { + config.addDefault("enabled", true) + config.addDefault("serverUuid", UUID.randomUUID().toString()) + config.addDefault("logFailedRequests", false) + config.addDefault("logSentData", false) + config.addDefault("logResponseStatusText", false) + // Inform the server owners about bStats + config + .options() + .header( + "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" + + "many people use their plugin and their total player count. It's recommended to keep bStats\n" + + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" + + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" + + "anonymous." + ) + .copyDefaults(true) + try { + config.save(configFile) + } catch (ignored: IOException) { + } + } + // Load the data + val enabled = config.getBoolean("enabled", true) + val serverUUID = (config.getString("serverUuid"))!! + val logErrors = config.getBoolean("logFailedRequests", false) + val logSentData = config.getBoolean("logSentData", false) + val logResponseStatusText = config.getBoolean("logResponseStatusText", false) + metricsBase = MetricsBase( + "bukkit", + serverUUID, + serviceId, + enabled, + { builder: JsonObjectBuilder -> + appendPlatformData( + builder + ) + }, + { builder: JsonObjectBuilder -> + appendServiceData( + builder + ) + }, + { submitDataTask: Runnable? -> + Bukkit.getScheduler().runTask( + plugin, + (submitDataTask)!! + ) + }, + { plugin.isEnabled() }, + { message: String?, error: Throwable? -> + this.plugin.getLogger().log(Level.WARNING, message, error) + }, + { message: String? -> + this.plugin.getLogger().log(Level.INFO, message) + }, + logErrors, + logSentData, + logResponseStatusText + ) + } +} + diff --git a/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/TicketManagerPlugin.kt b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/TicketManagerPlugin.kt new file mode 100644 index 0000000..2e3449f --- /dev/null +++ b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/TicketManagerPlugin.kt @@ -0,0 +1,240 @@ +package com.github.hoshikurama.ticketmanager.spigot + +import com.github.hoshikurama.ticketmanager.common.* +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.databases.Memory +import com.github.hoshikurama.ticketmanager.common.databases.MySQL +import com.github.hoshikurama.ticketmanager.common.databases.SQLite +import com.github.hoshikurama.ticketmanager.spigot.events.Commands +import com.github.hoshikurama.ticketmanager.spigot.events.PlayerJoin +import com.github.hoshikurama.ticketmanager.spigot.events.TabComplete +import com.github.shynixn.mccoroutine.* +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import net.md_5.bungee.api.chat.TextComponent +import net.milkbowl.vault.permission.Permission +import org.bukkit.Bukkit +import java.io.File + +class TicketManagerPlugin : SuspendingJavaPlugin() { + internal val pluginState = PluginState() + internal lateinit var perms: Permission private set + internal lateinit var configStateI: ConfigState + + private lateinit var metrics: Metrics + + + companion object { lateinit var plugin: TicketManagerPlugin } + init { plugin = this } + + override suspend fun onDisableAsync() { + pluginState.pluginLocked.set(true) + configStateI.database.closeDatabase() + } + + override fun onEnable() { + + // Find Vault plugin + server.servicesManager.getRegistration(Permission::class.java)?.provider + ?.let { perms = it } + ?: this.pluginLoader.disablePlugin(this) + + // Launch Metrics + launch { + metrics = Metrics(plugin, metricsKey) + metrics.addCustomChart( + Metrics.SingleLineChart("tickets_made") { + runBlocking { + val ticketCount = pluginState.ticketCountMetrics.get() + pluginState.ticketCountMetrics.set(0) + ticketCount + } + } + ) + metrics.addCustomChart( + Metrics.SimplePie("database_type") { + configStateI.database.type.name + } + ) + } + + // Launches ConfigState initialisation + launchAsync { loadPlugin() } + + // Register Event + server.pluginManager.registerSuspendingEvents(PlayerJoin(), plugin) + + // Creates task timers + Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, Runnable { + launchAsync { configStateI.cooldowns.filterMapAsync() } + + launchAsync { + if (pluginState.pluginLocked.get()) return@launchAsync + + try { + // Mass Unread Notify + if (configStateI.allowUnreadTicketUpdates) { + Bukkit.getOnlinePlayers().asFlow() + .filter { it.has("ticketmanager.notify.unreadUpdates.scheduled") } + .onEach { + launch { + val ticketIDs = configStateI.database.getIDsWithUpdatesFor(it.uniqueId).toList() + val tickets = ticketIDs.joinToString(", ") + + if (ticketIDs.isEmpty()) return@launch + + val template = if (ticketIDs.size > 1) it.toTMLocale().notifyUnreadUpdateMulti + else it.toTMLocale().notifyUnreadUpdateSingle + + val sentMessage = template.replace("%num%", tickets) + it.sendMessage(sentMessage.run(::addColour)) + } + } + } + + val openPriority = configStateI.database.getOpenIDPriorityPairs().map { it.first }.toList() + val openCount = openPriority.count() + val assignments = configStateI.database.getBasicTickets(openPriority).mapNotNull { it.assignedTo }.toList() + + // Open and Assigned Notify + Bukkit.getOnlinePlayers().asFlow() + .filter { it.has("ticketmanager.notify.openTickets.scheduled") } + .onEach { p -> + launch { + val groups = perms.getPlayerGroups(p).map { "::$it" } + val assignedCount = assignments + .filter { it == p.name || it in groups } + .count() + + val sentMessage = p.toTMLocale().notifyOpenAssigned + .replace("%open%", "$openCount") + .replace("%assigned%", "$assignedCount") + .run(::addColour) + .run(::TextComponent) + p.spigot().sendMessage(sentMessage) + } + } + } catch (e: Exception) { + e.printStackTrace() + postModifiedStacktrace(e) + } + } + }, 100, 12000) + } + + internal suspend fun loadPlugin() = withContext(plugin.asyncDispatcher) { + pluginState.pluginLocked.set(true) + + configStateI = run { + // Creates config file if not found + if (!File(plugin.dataFolder, "config.yml").exists()) { + plugin.saveDefaultConfig() + + // Notifies users config was generated after plugin state init + launch { + while (!(::configStateI.isInitialized)) + delay(100L) + pushMassNotify("ticketmanager.notify.warning") { it.warningsNoConfig } + } + } + + plugin.reloadConfig() + plugin.config.run { + val path = plugin.dataFolder.absolutePath + val database: () -> Database? = { + val type = getString("Database_Mode", "SQLite")!! + .let { tryOrNull { Database.Type.valueOf(it) } ?: Database.Type.SQLite } + + when (type) { + Database.Type.MySQL -> MySQL( + getString("MySQL_Host")!!, + getString("MySQL_Port")!!, + getString("MySQL_DBName")!!, + getString("MySQL_Username")!!, + getString("MySQL_Password")!!, + asyncDispatcher = (plugin.asyncDispatcher as CoroutineDispatcher), + ) + Database.Type.SQLite -> SQLite(path) + Database.Type.Memory -> Memory( + filePath = path, + backupFrequency = getLong("Memory_Backup_Frequency", 600) + ) + } + } + + val cooldown: () -> ConfigState.Cooldown? = { + ConfigState.Cooldown( + getBoolean("Use_Cooldowns", false), + getLong("Cooldown_Time", 0L) + ) + } + + val localeHandler: suspend () -> LocaleHandler? = { + LocaleHandler.buildLocalesAsync( + getString("Colour_Code", "&3")!!, + getString("Preferred_Locale", "en_ca")!!, + getString("Console_Locale", "en_ca")!!, + getBoolean("Force_Locale", false), + asyncContext + ) + } + + val allowUnreadTicketUpdates: () -> Boolean? = { + getBoolean("Allow_Unread_Ticket_Updates", true) + } + + val checkForPluginUpdate: () -> Boolean? = { + getBoolean("Allow_UpdateChecking", false) + } + + val pluginVersion: () -> String = { + mainPlugin.description.version + } + + ConfigState.createPluginState( + database, + cooldown, + localeHandler, + allowUnreadTicketUpdates, + checkForPluginUpdate, + pluginVersion, + path, + asyncContext + ) + } + } + + launch { + val updateNeeded = configStateI.database.updateNeeded() + + if (updateNeeded) { + configStateI.database.updateDatabase( + onBegin = { + pushMassNotify("ticketmanager.notify.info") { it.informationDBUpdate } + }, + onComplete = { + pushMassNotify("ticketmanager.notify.info") { it.informationDBUpdateComplete } + pluginState.pluginLocked.set(true) + }, + offlinePlayerNameToUuidOrNull = { + Bukkit.getOfflinePlayers() + .filter { it.name == name } + .map { it.uniqueId } + .firstOrNull() + }, + context = asyncContext + ) + } else pluginState.pluginLocked.set(false) + } + + + withContext(minecraftDispatcher) { + // Register events and commands + configStateI.localeHandler.getCommandBases().forEach { + getCommand(it)!!.setSuspendingExecutor(Commands()) + getCommand(it)!!.tabCompleter = TabComplete() + // Remember to register any keyword in plugin.yml + } + } + } +} \ No newline at end of file diff --git a/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/Commands.kt b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/Commands.kt new file mode 100644 index 0000000..4198369 --- /dev/null +++ b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/Commands.kt @@ -0,0 +1,1447 @@ +package com.github.hoshikurama.ticketmanager.spigot.events + +import com.github.hoshikurama.ticketmanager.common.* +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.common.databases.Memory +import com.github.hoshikurama.ticketmanager.common.databases.MySQL +import com.github.hoshikurama.ticketmanager.common.databases.SQLite +import com.github.hoshikurama.ticketmanager.common.ticket.* +import com.github.hoshikurama.ticketmanager.spigot.* +import com.github.shynixn.mccoroutine.SuspendingCommandExecutor +import com.github.shynixn.mccoroutine.asyncDispatcher +import com.github.shynixn.mccoroutine.minecraftDispatcher +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.toList +import net.md_5.bungee.api.ChatColor +import net.md_5.bungee.api.chat.ClickEvent +import net.md_5.bungee.api.chat.ComponentBuilder +import net.md_5.bungee.api.chat.HoverEvent +import net.md_5.bungee.api.chat.TextComponent +import net.md_5.bungee.api.chat.hover.content.Text +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import java.util.* + +class Commands : SuspendingCommandExecutor { + + override suspend fun onCommand( + sender: CommandSender, + command: Command, + label: String, + args: Array, + ): Boolean = withContext(mainPlugin.asyncDispatcher) { + + val senderLocale = sender.toTMLocale() + val argList = args.toList() + + if (argList.isEmpty()) { + senderLocale.warningsInvalidCommand + .run(::addColour) + .run(sender::sendMessage) + return@withContext false + } + + if (mainPlugin.pluginState.pluginLocked.get()) { + senderLocale.warningsLocked + .run(::addColour) + .run(sender::sendMessage) + return@withContext false + } + + // Grabs BasicTicket. Only null if ID required but doesn't exist. Filters non-valid tickets + val pseudoTicket = getBasicTicketHandler(argList, senderLocale) + if (pseudoTicket == null) { + senderLocale.warningsInvalidID + .run(::addColour) + .run(sender::sendMessage) + return@withContext false + } + + // Async Calculations + val hasValidPermission = async { hasValidPermission(sender, pseudoTicket, argList, senderLocale) } + val isValidCommand = async { isValidCommand(sender, pseudoTicket, argList, senderLocale) } + val notUnderCooldown = async { notUnderCooldown(sender, senderLocale, argList) } + // Shortened Commands + val executeCommand = suspend { executeCommand(sender, argList, senderLocale, pseudoTicket) } + + try { + mainPlugin.pluginState.jobCount.run { set(get() + 1) } + if (notUnderCooldown.await() && isValidCommand.await() && hasValidPermission.await()) { + executeCommand()?.let { pushNotifications(sender, it, senderLocale, pseudoTicket) } + } + } catch (e: Exception) { + e.printStackTrace() + postModifiedStacktrace(e) + + senderLocale.warningsUnexpectedError + .run(::addColour) + .run(sender::sendMessage) + } finally { + mainPlugin.pluginState.jobCount.run { set(get() - 1) } + } + + return@withContext true + } + + private suspend fun getBasicTicketHandler( + args: List, + senderLocale: TMLocale, + ): BasicTicketHandler? { + + suspend fun buildFromID(id: Int) = BasicTicketHandler.buildHandler(configState.database, id) + + return when (args[0]) { + senderLocale.commandWordAssign, + senderLocale.commandWordSilentAssign, + senderLocale.commandWordClaim, + senderLocale.commandWordSilentClaim, + senderLocale.commandWordClose, + senderLocale.commandWordSilentClose, + senderLocale.commandWordComment, + senderLocale.commandWordSilentComment, + senderLocale.commandWordReopen, + senderLocale.commandWordSilentReopen, + senderLocale.commandWordSetPriority, + senderLocale.commandWordSilentSetPriority, + senderLocale.commandWordTeleport, + senderLocale.commandWordUnassign, + senderLocale.commandWordSilentUnassign, + senderLocale.commandWordView, + senderLocale.commandWordDeepView -> + args.getOrNull(1) + ?.toIntOrNull() + ?.let { buildFromID(it) } + else -> ConcreteBasicTicket(creatorUUID = null, location = null).run { BasicTicketHandler(this, configState.database) } // Occurs when command does not need valid handler + } + } + + private fun hasValidPermission( + sender: CommandSender, + basicTicket: BasicTicket, + args: List, + senderLocale: TMLocale + ): Boolean { + try { + if (sender !is Player) return true + + fun has(perm: String) = sender.has(perm) + fun hasSilent() = has("ticketmanager.commandArg.silence") + fun hasDuality(basePerm: String): Boolean { + val senderUUID = sender.toUUIDOrNull() + val ownsTicket = basicTicket.uuidMatches(senderUUID) + return has("$basePerm.all") || (sender.has("$basePerm.own") && ownsTicket) + } + + return senderLocale.run { + when (args[0]) { + commandWordAssign, commandWordClaim, commandWordUnassign -> + has("ticketmanager.command.assign") + commandWordSilentAssign, commandWordSilentClaim,commandWordSilentUnassign -> + has("ticketmanager.command.assign") && hasSilent() + commandWordClose -> hasDuality("ticketmanager.command.close") + commandWordSilentClose -> hasDuality("ticketmanager.command.close") && hasSilent() + commandWordCloseAll -> has("ticketmanager.command.closeAll") + commandWordSilentCloseAll -> has("ticketmanager.command.closeAll") && hasSilent() + commandWordComment -> hasDuality("ticketmanager.command.comment") + commandWordSilentComment -> hasDuality("ticketmanager.command.comment") && hasSilent() + commandWordCreate -> has("ticketmanager.command.create") + commandWordHelp -> has("ticketmanager.command.help") + commandWordReload -> has("ticketmanager.command.reload") + commandWordList -> has("ticketmanager.command.list") + commandWordListAssigned -> has("ticketmanager.command.list") + commandWordReopen -> has("ticketmanager.command.reopen") + commandWordSilentReopen -> has("ticketmanager.command.reopen") && hasSilent() + commandWordSearch -> has("ticketmanager.command.search") + commandWordSetPriority -> has("ticketmanager.command.setPriority") + commandWordSilentSetPriority -> has("ticketmanager.command.setPriority") && hasSilent() + commandWordTeleport -> has("ticketmanager.command.teleport") + commandWordView -> hasDuality("ticketmanager.command.view") + commandWordDeepView -> hasDuality("ticketmanager.command.viewdeep") + commandWordConvertDB -> has("ticketmanager.command.convertDatabase") + commandWordHistory -> + sender.has("ticketmanager.command.history.all") || + sender.has("ticketmanager.command.history.own").let { hasPerm -> + if (args.size >= 2) hasPerm && args[1] == sender.name + else hasPerm + } + else -> true + } + } + .also { if (!it) + senderLocale.warningsNoPermission + .run(::addColour) + .run(sender::sendMessage) + } + } catch (e: Exception) { + senderLocale.warningsNoPermission + .run(::addColour) + .run(sender::sendMessage) + return false + } + } + + private fun isValidCommand( + sender: CommandSender, + basicTicket: BasicTicket, + args: List, + senderLocale: TMLocale + ): Boolean { + fun sendMessage(formattedString: String) = formattedString.run(::addColour).run(sender::sendMessage) + fun invalidCommand() = sendMessage(senderLocale.warningsInvalidCommand) + fun notANumber() = sendMessage(senderLocale.warningsInvalidNumber) + fun outOfBounds() = sendMessage(senderLocale.warningsPriorityOutOfBounds) + fun ticketClosed() = sendMessage(senderLocale.warningsTicketAlreadyClosed) + fun ticketOpen() = sendMessage(senderLocale.warningsTicketAlreadyOpen) + + return senderLocale.run { + when (args[0]) { + commandWordAssign, commandWordSilentAssign -> + check(::invalidCommand) { args.size >= 3 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordClaim, commandWordSilentClaim -> + check(::invalidCommand) { args.size == 2 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordClose, commandWordSilentClose -> + check(::invalidCommand) { args.size >= 2 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordComment, commandWordSilentComment -> + check(::invalidCommand) { args.size >= 3 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordCloseAll, commandWordSilentCloseAll -> + check(::invalidCommand) { args.size == 3 } + .thenCheck(::notANumber) { args[1].toIntOrNull() != null } + .thenCheck(::notANumber) { args[2].toIntOrNull() != null } + + commandWordReopen, commandWordSilentReopen -> + check(::invalidCommand) { args.size == 2 } + .thenCheck(::ticketOpen) { basicTicket.status != BasicTicket.Status.OPEN } + + commandWordSetPriority, commandWordSilentSetPriority -> + check(::invalidCommand) { args.size == 3 } + .thenCheck(::outOfBounds) { args[2].toByteOrNull() != null } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordUnassign, commandWordSilentUnassign -> + check(::invalidCommand) { args.size == 2 } + .thenCheck(::ticketClosed) { basicTicket.status != BasicTicket.Status.CLOSED } + + commandWordView -> check(::invalidCommand) { args.size == 2 } + + commandWordDeepView -> check(::invalidCommand) { args.size == 2 } + + commandWordTeleport -> check(::invalidCommand) { args.size == 2 } + + commandWordCreate -> check(::invalidCommand) { args.size >= 2 } + + commandWordHistory -> + check(::invalidCommand) { args.isNotEmpty() } + .thenCheck(::notANumber) { if (args.size >= 3) args[2].toIntOrNull() != null else true} + + commandWordList -> + check(::notANumber) { if (args.size == 2) args[1].toIntOrNull() != null else true } + + commandWordListAssigned -> + check(::notANumber) { if (args.size == 2) args[1].toIntOrNull() != null else true } + + commandWordSearch -> check(::invalidCommand) { args.size >= 2} + + commandWordReload -> true + commandWordVersion -> true + commandWordHelp -> true + + commandWordConvertDB -> + check(::invalidCommand) { args.size == 2 } + .thenCheck( { sendMessage(senderLocale.warningsInvalidDBType) }, + { + try { Database.Type.valueOf(args[1]); true } + catch (e: Exception) { false } + } + ) + .thenCheck( { sendMessage(senderLocale.warningsConvertToSameDBType) } ) + { configState.database.type != Database.Type.valueOf(args[1]) } + + else -> false.also { invalidCommand() } + } + } + } + + private suspend fun notUnderCooldown( + sender: CommandSender, + senderLocale: TMLocale, + args: List + ): Boolean { + val underCooldown = when (args[0]) { + senderLocale.commandWordCreate, + senderLocale.commandWordComment, + senderLocale.commandWordSilentComment -> + configState.cooldowns.checkAndSetAsync(sender.toUUIDOrNull()) + else -> false + } + + if (underCooldown) + senderLocale.warningsUnderCooldown + .run(::addColour) + .run(sender::sendMessage) + return !underCooldown + } + + private suspend fun executeCommand( + sender: CommandSender, + args: List, + senderLocale: TMLocale, + ticketHandler: BasicTicketHandler + ): NotifyParams? { + return senderLocale.run { + when (args[0]) { + commandWordAssign -> assign(sender, args, false, senderLocale, ticketHandler) + commandWordAssign -> assign(sender, args, false, senderLocale, ticketHandler) + commandWordSilentAssign -> assign(sender, args, true, senderLocale, ticketHandler) + commandWordClaim -> claim(sender, args, false, senderLocale, ticketHandler) + commandWordSilentClaim -> claim(sender, args, true, senderLocale, ticketHandler) + commandWordClose -> close(sender, args, false, ticketHandler) + commandWordSilentClose -> close(sender, args, true, ticketHandler) + commandWordCloseAll -> closeAll(sender, args, false, ticketHandler) + commandWordSilentCloseAll -> closeAll(sender, args, true, ticketHandler) + commandWordComment -> comment(sender, args, false, ticketHandler) + commandWordSilentComment -> comment(sender, args, true, ticketHandler) + commandWordCreate -> create(sender, args) + commandWordHelp -> help(sender, senderLocale).let { null } + commandWordHistory -> history(sender, args, senderLocale).let { null } + commandWordList -> list(sender, args, senderLocale).let { null } + commandWordListAssigned -> listAssigned(sender, args, senderLocale).let { null } + commandWordReload -> reload(sender, senderLocale).let { null } + commandWordReopen -> reopen(sender,args, false, ticketHandler) + commandWordSilentReopen -> reopen(sender,args, true, ticketHandler) + commandWordSearch -> search(sender, args, senderLocale).let { null } + commandWordSetPriority -> setPriority(sender, args, false, ticketHandler) + commandWordSilentSetPriority -> setPriority(sender, args, true, ticketHandler) + commandWordTeleport -> teleport(sender, ticketHandler).let { null } + commandWordUnassign -> unAssign(sender, args, false, senderLocale, ticketHandler) + commandWordSilentUnassign -> unAssign(sender, args, true, senderLocale, ticketHandler) + commandWordVersion -> version(sender, senderLocale).let { null } + commandWordView -> view(sender, senderLocale, ticketHandler).let { null } + commandWordDeepView -> viewDeep(sender, senderLocale, ticketHandler).let { null } + commandWordConvertDB -> convertDatabase(args).let { null } + else -> null + } + } + } + + private fun pushNotifications( + sender: CommandSender, + params: NotifyParams, + locale: TMLocale, + basicTicket: BasicTicket + ) { + params.run { + if (sendSenderMSG) + senderLambda!!(locale) + .run(::addColour) + .run(sender::sendMessage) + + if (sendCreatorMSG) + basicTicket.creatorUUID + ?.run(Bukkit::getPlayer) + ?.let { creatorLambda!!(it.toTMLocale()) } + ?.let(::addColour) + ?.run(creator!!::sendMessage) + + if (sendMassNotifyMSG) + pushMassNotify(massNotifyPerm, massNotifyLambda!!) + } + } + + private class NotifyParams( + silent: Boolean, + basicTicket: BasicTicket, + sender: CommandSender, + creatorAlertPerm: String, + val massNotifyPerm: String, + val senderLambda: ((TMLocale) -> String)?, + val creatorLambda: ((TMLocale) -> String)?, + val massNotifyLambda: ((TMLocale) -> String)?, + ) { + val creator: Player? = basicTicket.creatorUUID?.let(Bukkit::getPlayer) + val sendSenderMSG: Boolean = (!sender.has(massNotifyPerm) || silent) + && senderLambda != null + val sendCreatorMSG: Boolean = sender.nonCreatorMadeChange(basicTicket.creatorUUID) + && !silent && (creator?.isOnline ?: false) + && (creator?.has(creatorAlertPerm) ?: false) + && (creator?.has(massNotifyPerm)?.run { !this } ?: false) + && creatorLambda != null + val sendMassNotifyMSG: Boolean = !silent + && massNotifyLambda != null + } + + /*-------------------------*/ + /* Commands */ + /*-------------------------*/ + + private suspend fun allAssignVariations( + sender: CommandSender, + silent: Boolean, + senderLocale: TMLocale, + assignmentID: String, + dbAssignment: String?, + ticketHandler: BasicTicketHandler, + ): NotifyParams = withContext(asyncContext) { + val shownAssignment = dbAssignment ?: senderLocale.miscNobody + + launch { ticketHandler.setAssignedTo(dbAssignment) } + launch { configState.database.addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.ASSIGN, sender.toUUIDOrNull(), dbAssignment) + )} + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + senderLambda = { + it.notifyTicketAssignSuccess + .replace("%id%", assignmentID) + .replace("%assign%", shownAssignment) + }, + massNotifyLambda = { + it.notifyTicketAssignEvent + .replace("%user%", sender.name) + .replace("%id%", assignmentID) + .replace("%assign%", shownAssignment) + }, + creatorLambda = null, + creatorAlertPerm = "ticketmanager.notify.change.assign", + massNotifyPerm = "ticketmanager.notify.massNotify.assign" + ) + } + + // /ticket assign + private suspend fun assign( + sender: CommandSender, + args: List, + silent: Boolean, + senderLocale: TMLocale, + ticketHandler: BasicTicketHandler, + ): NotifyParams { + val sqlAssignment = args.subList(2, args.size).joinToString(" ") + return allAssignVariations(sender, silent, senderLocale, args[1], sqlAssignment, ticketHandler) + } + + // /ticket claim + private suspend fun claim( + sender: CommandSender, + args: List, + silent: Boolean, + senderLocale: TMLocale, + ticketHandler: BasicTicketHandler, + ): NotifyParams { + return allAssignVariations(sender, silent, senderLocale, args[1], sender.name, ticketHandler) + } + + // /ticket close [Comment...] + private suspend fun close( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler + ): NotifyParams = withContext(asyncContext) { + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && configState.allowUnreadTicketUpdates + if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { + launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } + } + + return@withContext if (args.size >= 3) + closeWithComment(sender, args, silent, ticketHandler) + else closeWithoutComment(sender, args, silent, ticketHandler) + } + + private suspend fun closeWithComment( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler, + ): NotifyParams = withContext(asyncContext) { + val message = args.subList(2, args.size) + .joinToString(" ") + .run(ChatColor::stripColor)!! + + launch { + configState.database.run { + addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.COMMENT, sender.toUUIDOrNull(), message) + ) + addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.CLOSE, sender.toUUIDOrNull()) + ) + ticketHandler.setTicketStatus(BasicTicket.Status.CLOSED) + } + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + senderLambda = { + it.notifyTicketCloseWCommentSuccess + .replace("%id%", args[1]) + }, + massNotifyLambda = { + it.notifyTicketCloseWCommentEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + .replace("%message%", message) + }, + creatorLambda = { + it.notifyTicketModificationEvent + .replace("%id%", args[1]) + }, + massNotifyPerm = "ticketmanager.notify.massNotify.close", + creatorAlertPerm = "ticketmanager.notify.change.close" + ) + } + + private suspend fun closeWithoutComment( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler + ): NotifyParams = withContext(asyncContext) { + launch { + configState.database.addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.CLOSE, sender.toUUIDOrNull()) + ) + ticketHandler.setTicketStatus(BasicTicket.Status.CLOSED) + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + creatorLambda = { + it.notifyTicketModificationEvent + .replace("%id%", args[1]) + }, + massNotifyLambda = { + it.notifyTicketCloseEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + }, + senderLambda = { + it.notifyTicketCloseSuccess + .replace("%id%", args[1]) + }, + massNotifyPerm = "ticketmanager.notify.massNotify.close", + creatorAlertPerm = "ticketmanager.notify.change.close" + ) + } + + // /ticket closeall + private suspend fun closeAll( + sender: CommandSender, + args: List, + silent: Boolean, + basicTicket: BasicTicket + ): NotifyParams = withContext(asyncContext) { + val lowerBound = args[1].toInt() + val upperBound = args[2].toInt() + + launch { configState.database.massCloseTickets(lowerBound, upperBound, sender.toUUIDOrNull(), asyncContext) } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = basicTicket, + creatorLambda = null, + senderLambda = { + it.notifyTicketMassCloseSuccess + .replace("%low%", args[1]) + .replace("%high%", args[2]) + }, + massNotifyLambda = { + it.notifyTicketMassCloseEvent + .replace("%user%", sender.name) + .replace("%low%", args[1]) + .replace("%high%", args[2]) + }, + massNotifyPerm = "ticketmanager.notify.massNotify.massClose", + creatorAlertPerm = "ticketmanager.notify.change.massClose" + ) + } + + // /ticket comment + private suspend fun comment( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler, + ): NotifyParams = withContext(asyncContext) { + val message = args.subList(2, args.size) + .joinToString(" ") + .run(ChatColor::stripColor)!! + + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && configState.allowUnreadTicketUpdates + if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { + launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } + } + + launch { + configState.database.addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.COMMENT, sender.toUUIDOrNull(), message) + ) + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + creatorLambda = { + it.notifyTicketModificationEvent + .replace("%id%", args[1]) + }, + senderLambda = { + it.notifyTicketCommentSuccess + .replace("%id%", args[1]) + }, + massNotifyLambda = { + it.notifyTicketCommentEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + .replace("%message%", message) + }, + massNotifyPerm = "ticketmanager.notify.massNotify.comment", + creatorAlertPerm = "ticketmanager.notify.change.comment" + ) + } + + // /ticket convertdatabase + private suspend fun convertDatabase(args: List) { + val type = args[1].run(Database.Type::valueOf) + val config = mainPlugin.config + + try { + configState.database.migrateDatabase( + context = asyncContext, + to = type, + sqLiteBuilder = { SQLite(mainPlugin.dataFolder.absolutePath) }, + mySQLBuilder = { + MySQL( + config.getString("MySQL_Host")!!, + config.getString("MySQL_Port")!!, + config.getString("MySQL_DBName")!!, + config.getString("MySQL_Username")!!, + config.getString("MySQL_Password")!!, + asyncDispatcher = (mainPlugin.asyncDispatcher as CoroutineDispatcher) + ) + }, + memoryBuilder = { + Memory( + filePath = mainPlugin.dataFolder.absolutePath, + backupFrequency = config.getLong("Memory_Backup_Frequency", 600), + ) + }, + onBegin = { + mainPlugin.pluginState.pluginLocked.set(true) + pushMassNotify("ticketmanager.notify.info") { + it.informationDBConvertInit + .replace("%fromDB%", configState.database.type.name) + .replace("%toDB%", type.name) + } + }, + onComplete = { + mainPlugin.pluginState.pluginLocked.set(false) + pushMassNotify("ticketmanager.notify.info") { it.informationDBConvertSuccess } + } + ) + } catch (e: Exception) { + mainPlugin.pluginState.pluginLocked.set(false) + throw e + } + } + + // /ticket create + private suspend fun create( + sender: CommandSender, + args: List, + ): NotifyParams = withContext(asyncContext) { + val message = args.subList(1, args.size) + .joinToString(" ") + .run(ChatColor::stripColor)!! + + val ticket = ConcreteBasicTicket(creatorUUID = sender.toUUIDOrNull(), location = sender.toTicketLocationOrNull()) + + val deferredID = async { configState.database.addNewTicket(ticket, asyncContext, message) } + mainPlugin.pluginState.ticketCountMetrics.run { set(get() + 1) } + val id = deferredID.await().toString() + + NotifyParams( + silent = false, + sender = sender, + basicTicket = ticket, + creatorLambda = null, + senderLambda = { + it.notifyTicketCreationSuccess + .replace("%id%", id) + }, + massNotifyLambda = { + it.notifyTicketCreationEvent + .replace("%user%", sender.name) + .replace("%id%", id) + .replace("%message%", message) + }, + creatorAlertPerm = "ticketmanager.NO NODE", + massNotifyPerm = "ticketmanager.notify.massNotify.create", + ) + } + + // /ticket help + private fun help( + sender: CommandSender, + locale: TMLocale, + ) { + val hasSilentPerm = sender.has("ticketmanager.commandArg.silence") + val cc = configState.localeHandler.mainColourCode + + val component = ComponentBuilder("") + + val header = mutableListOf( + locale.helpHeader, + locale.helpLine1, + ) + if (hasSilentPerm) { + header.add(locale.helpLine2) + header.add(locale.helpLine3) + } + header.add(locale.helpSep) + header.map(::addColour).forEach(component::append) + + locale.run { + listOf( // Triple(silence-able, format, permissions) + Triple(true, "$commandWordAssign &f<$parameterID> <$parameterAssignment...>", listOf("ticketmanager.command.assign")), + Triple(true, "$commandWordClaim &f<$parameterID>", listOf("ticketmanager.command.claim")), + Triple(true, "$commandWordClose &f<$parameterID> &7[$parameterComment...]", listOf("ticketmanager.command.close.all", "ticketmanager.command.close.own")), + Triple(true, "$commandWordCloseAll &f<$parameterLowerID> <$parameterUpperID>", listOf("ticketmanager.command.closeAll")), + Triple(true, "$commandWordComment &f<$parameterID> <$parameterComment...>", listOf("ticketmanager.command.comment.all", "ticketmanager.command.comment.own")), + Triple(false, "$commandWordConvertDB &f<$parameterTargetDB>", listOf("ticketmanager.command.convertDatabase")), + Triple(false, "$commandWordCreate &f<$parameterComment...>", listOf("ticketmanager.command.create")), + Triple(false, commandWordHelp, listOf("ticketmanager.command.help")), + Triple(false, "$commandWordHistory &7[$parameterUser] [$parameterPage]", listOf("ticketmanager.command.history.all", "ticketmanager.command.history.own")), + Triple(false, "$commandWordList &7[$parameterPage]", listOf("ticketmanager.command.list")), + Triple(false, "$commandWordListAssigned &7[$parameterPage]", listOf("ticketmanager.command.list")), + Triple(false, commandWordReload, listOf("ticketmanager.command.reload")), + Triple(true, "$commandWordReopen &f<$parameterID>", listOf("ticketmanager.command.reopen")), + Triple(false, "$commandWordSearch &f<$parameterConstraints...>", listOf("ticketmanager.command.search")), + Triple(true, "$commandWordSetPriority &f<$parameterID> <$parameterLevel>", listOf("ticketmanager.command.setPriority")), + Triple(false, "$commandWordTeleport &f<$parameterID>", listOf("ticketmanager.command.teleport")), + Triple(true, "$commandWordUnassign &f<$parameterID>", listOf("ticketmanager.command.assign")), + Triple(false, "$commandWordView &f<$parameterID>", listOf("ticketmanager.command.view.all", "ticketmanager.command.view.own")), + Triple(false, "$commandWordDeepView &f<$parameterID>", listOf("ticketmanager.command.viewdeep.all", "ticketmanager.command.viewdeep.own")) + ) + .filter { it.third.any(sender::has) } + .run { this + Triple(false, locale.commandWordVersion, "NA") } + .map { + val commandString = "$cc/${locale.commandBase} ${it.second}" + if (hasSilentPerm) + if (it.first) "\n&a[✓] $commandString" + else "\n&c[✕] $commandString" + else "\n$commandString" + } + .map(::addColour) + .forEach(component::append) + + sender.spigot().sendMessage(*component.create()) + } + } + + // /ticket history [User] [Page] + private suspend fun history( + sender: CommandSender, + args: List, + locale: TMLocale, + ) { + withContext(asyncContext) { + val targetName = + if (args.size >= 2) args[1].takeIf { it != locale.consoleName } else sender.name.takeIf { sender is Player } + val requestedPage = if (args.size >= 3) args[2].toInt() else 1 + + // Leaves console as null. Otherwise attempts UUID grab or [PLAYERNOTFOUND] + fun String.attemptToUUIDString(): String = + Bukkit.getOfflinePlayers().asSequence() + .firstOrNull { equals(it.name) } + ?.run { uniqueId.toString() } + ?: "[PLAYERNOTFOUND]" + + val searchedUser = targetName?.attemptToUUIDString() + + val resultSize: Int + val resultsChunked = configState.database.searchDatabase(asyncContext, locale, listOf(locale.searchCreator to searchedUser)) { true } + .toList() + .sortedByDescending(BasicTicket::id) + .also { resultSize = it.size } + .chunked(6) + + val componentBuilder = ComponentBuilder() + + locale.historyHeader + .replace("%name%", targetName ?: locale.consoleName) + .replace("%count%", "$resultSize") + .run(::addColour) + .run(componentBuilder::append) + + val actualPage = if (requestedPage >= 1 && requestedPage < resultsChunked.size) requestedPage else 1 + if (resultsChunked.isNotEmpty()) { + resultsChunked.getOrElse(requestedPage - 1) { resultsChunked[1] }.forEach { t -> + locale.historyEntry + .let { "\n$it" } + .replace("%id%", "${t.id}") + .replace("%SCC%", t.status.colourCode) + .replace("%status%", t.status.toLocaledWord(locale)) + .replace("%comment%", t.actions[0].message!!) + .let { if (it.length > 80) "${it.substring(0, 81)}..." else it } + .run(::addColour) + .run(::TextComponent) + .run { convertToClickToView(t.id, locale) } + .run(componentBuilder::append) + } + + if (resultsChunked.size > 1) { + buildPageComponent(actualPage, resultsChunked.size, locale) { + "/${it.commandBase} ${it.commandWordHistory} ${targetName ?: it.consoleName} " + }.run(componentBuilder::append) + } + } + + sender.spigot().sendMessage(*componentBuilder.create()) + } + } + + // /ticket list [Page] + private suspend fun list( + sender: CommandSender, + args: List, + locale: TMLocale, + ) { + sender.spigot().sendMessage( + createGeneralList(args, locale, locale.listFormatHeader, + getIDPriorityPair = { it.getOpenIDPriorityPairs() }, + baseCommand = locale.run{ { "/$commandBase $commandWordList " } } + ) + ) + } + + // /ticket listassigned [Page] + private suspend fun listAssigned( + sender: CommandSender, + args: List, + locale: TMLocale, + ) { + val groups: List = if (sender is Player) mainPlugin.perms.getPlayerGroups(sender).toList() else listOf() + + sender.spigot().sendMessage( + createGeneralList(args, locale, locale.listFormatAssignedHeader, + getIDPriorityPair = { it.getAssignedOpenIDPriorityPairs(sender.name, groups) }, + baseCommand = locale.run { { "/$commandBase $commandWordListAssigned " } } + ) + ) + } + + // /ticket reload + private suspend fun reload( + sender: CommandSender, + locale: TMLocale, + ) { + withContext(asyncContext) { + try { + mainPlugin.pluginState.pluginLocked.set(true) + pushMassNotify("ticketmanager.notify.info") { + it.informationReloadInitiated + .replace("%user%", sender.name) + } + + val forceQuitJob = launch { + delay(30L * 1000L) + + // Long standing task has occurred if it reaches this point + launch { + pushMassNotify("ticketmanager.notify.warning") { + it.warningsLongTaskDuringReload + } + mainPlugin.pluginState.jobCount.set(1) + mainPlugin.asyncDispatcher.cancelChildren() + } + } + + // Waits for other tasks to complete + while (mainPlugin.pluginState.jobCount.get() > 1) delay(1000L) + + if (!forceQuitJob.isCancelled) + forceQuitJob.cancel("Tasks closed on time") + + pushMassNotify("ticketmanager.notify.info") { it.informationReloadTasksDone } + configState.database.closeDatabase() + mainPlugin.loadPlugin() + + pushMassNotify("ticketmanager.notify.info") { it.informationReloadSuccess } + if (!sender.has("ticketmanager.notify.info")) { + locale.informationReloadSuccess + .run(::addColour) + .run(sender::sendMessage) + } + } catch (e: Exception) { + pushMassNotify("ticketmanager.notify.info") { it.informationReloadFailure } + throw e + } + } + } + + // /ticket reopen + private suspend fun reopen( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler, + ): NotifyParams = withContext(asyncContext) { + val action = FullTicket.Action(FullTicket.Action.Type.REOPEN, sender.toUUIDOrNull()) + + // Updates user status if needed + val newCreatorStatusUpdate = sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && configState.allowUnreadTicketUpdates + if (newCreatorStatusUpdate != ticketHandler.creatorStatusUpdate) { + launch { ticketHandler.setCreatorStatusUpdate(newCreatorStatusUpdate) } + } + + launch { + configState.database.addAction(ticketHandler.id, action) + ticketHandler.setTicketStatus(BasicTicket.Status.OPEN) + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + creatorLambda = { + it.notifyTicketModificationEvent + .replace("%id%", args[1]) + }, + senderLambda = { + it.notifyTicketReopenSuccess + .replace("%id%", args[1]) + }, + massNotifyLambda = { + it.notifyTicketReopenEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + }, + creatorAlertPerm = "ticketmanager.notify.change.reopen", + massNotifyPerm = "ticketmanager.notify.massNotify.reopen", + ) + } + + private suspend fun search( + sender: CommandSender, + args: List, + locale: TMLocale, + ) { + withContext(asyncContext) { + fun String.attemptToUUIDString(): String? = + if (equals(locale.consoleName)) null + else Bukkit.getOfflinePlayers().asSequence() + .firstOrNull { equals(it.name) } + ?.run { uniqueId.toString() } + ?: "[PLAYERNOTFOUND]" + + // Beginning of execution + locale.searchFormatQuerying + .run(::addColour) + .run(sender::sendMessage) + + // Input args mapped to valid search types + val arguments = args.subList(1, args.size) + .asSequence() + .map { it.split(":", limit = 2) } + .filter { it.size >= 2 } + .associate { it[0] to it[1] } + + val mainTableConstrains = arguments + .mapNotNull { (key, value) -> + when (key) { + locale.searchAssigned -> key to value + locale.searchCreator -> key to value.attemptToUUIDString() + locale.searchPriority -> value.toByteOrNull()?.run { key to this.toString() } + locale.searchStatus -> { + val constraintStatus = when (value) { + locale.statusOpen -> BasicTicket.Status.OPEN.name + locale.statusClosed -> BasicTicket.Status.CLOSED.name + else -> null + } + constraintStatus?.run { key to this } + } + else -> null + } + } + + val functionConstraints = arguments + .mapNotNull { (key, value) -> + when (key) { + + locale.searchClosedBy -> { + val searchedUser = value.attemptToUUIDString(); + { t: FullTicket -> t.actions.any { it.type == FullTicket.Action.Type.CLOSE && it.user?.toString() == searchedUser } } + } + + locale.searchLastClosedBy -> { + val searchedUser = value.attemptToUUIDString(); + { t: FullTicket -> + t.actions.lastOrNull { e -> e.type == FullTicket.Action.Type.CLOSE } + ?.run { user?.toString() == searchedUser } + ?: false + } + } + + locale.searchWorld -> { t: FullTicket -> t.location?.world?.equals(value) ?: false } + + locale.searchTime -> { + val creationTime = relTimeToEpochSecond(value, locale); + { t: FullTicket -> t.actions[0].timestamp >= creationTime } + } + + locale.searchKeywords -> { + val words = value.split(","); + + { t: FullTicket -> + val comments = t.actions + .filter { it.type == FullTicket.Action.Type.OPEN || it.type == FullTicket.Action.Type.COMMENT } + .map { it.message!! } + words.map { w -> comments.any { it.lowercase().contains(w.lowercase()) } } + .all { it } + } + } + + else -> null + } + } + val composedSearch = + if (functionConstraints.isNotEmpty()) + { t: FullTicket -> functionConstraints.map { it(t) }.all { it } } + else { _: FullTicket -> true } + + // Results Computation + val resultSize: Int + val chunkedTickets = + configState.database.searchDatabase(asyncContext, locale, mainTableConstrains, composedSearch) + .toList() + .sortedByDescending(BasicTicket::id) + .apply { resultSize = size } + .chunked(8) + + val page = arguments[locale.searchPage]?.toIntOrNull() + .let { if (it != null && it >= 1 && it < chunkedTickets.size) it else 1 } + val fixMSGLength = + { t: FullTicket -> t.actions[0].message!!.run { if (length > 25) "${substring(0, 21)}..." else this } } + + val component = TextComponent("") + + // Adds header + locale.searchFormatHeader + .replace("%size%", "$resultSize") + .run(::addColour) + .run(component::addExtra) + + // Adds entries + if (chunkedTickets.isNotEmpty()) { + chunkedTickets[page - 1] + .map { + "\n${locale.searchFormatEntry}" + .replace("%PCC%", it.priority.colourCode) + .replace("%SCC%", it.status.colourCode) + .replace("%id%", "${it.id}") + .replace("%status%", it.status.toLocaledWord(locale)) + .replace("%creator%", it.creatorUUID.toName(locale)) + .replace("%assign%", it.assignedTo ?: "") + .replace("%world%", it.location?.world ?: "") + .replace("%time%", it.actions[0].timestamp.toLargestRelativeTime(locale)) + .replace("%comment%", fixMSGLength(it)) + .run(::addColour) + .run(::TextComponent) + .run { convertToClickToView(it.id, locale) } + } + .forEach(component::addExtra) + + // Implements pages if needed + if (chunkedTickets.size > 1) { + buildPageComponent(page, chunkedTickets.size, locale) { + // Removes page constraint and converts rest to key:arg + val constraints = arguments + .filter { it.key != locale.searchPage } + .map { (k, v) -> "$k:$v" } + "/${locale.commandBase} ${locale.commandWordSearch} $constraints ${locale.searchPage}:" + }.run(component::addExtra) + } + } + sender.spigot().sendMessage(component) + } + } + + // /ticket setpriority + private suspend fun setPriority( + sender: CommandSender, + args: List, + silent: Boolean, + ticketHandler: BasicTicketHandler, + ): NotifyParams = withContext(asyncContext) { + val newPriority = byteToPriority(args[2].toByte()) + launch { + configState.database.addAction( + ticketID = ticketHandler.id, + action = FullTicket.Action(FullTicket.Action.Type.SET_PRIORITY, sender.toUUIDOrNull(), args[2]) + ) + ticketHandler.setTicketPriority(newPriority) + } + + NotifyParams( + silent = silent, + sender = sender, + basicTicket = ticketHandler, + creatorLambda = null, + senderLambda = { + it.notifyTicketSetPrioritySuccess + .replace("%id%", args[1]) + .replace("%priority%", ticketHandler.run { newPriority.colourCode + newPriority.toLocaledWord(it) }) + }, + massNotifyLambda = { + it.notifyTicketSetPriorityEvent + .replace("%user%", sender.name) + .replace("%id%", args[1]) + .replace("%priority%", ticketHandler.run { newPriority.colourCode + newPriority.toLocaledWord(it) }) + }, + creatorAlertPerm = "ticketmanager.notify.change.priority", + massNotifyPerm = "ticketmanager.notify.massNotify.priority" + ) + } + + // /ticket teleport + private suspend fun teleport( + sender: CommandSender, + basicTicket: BasicTicket, + ) { + if (sender is Player && basicTicket.location != null) { + val loc = basicTicket.location!!.run { Location(Bukkit.getWorld(world), x.toDouble(), y.toDouble(), z.toDouble()) } + withContext(mainPlugin.minecraftDispatcher) { + sender.teleport(loc) + } + } + } + + // /ticket unassign + private suspend fun unAssign( + sender: CommandSender, + args: List, + silent: Boolean, + senderLocale: TMLocale, + ticketHandler: BasicTicketHandler, + ): NotifyParams { + return allAssignVariations(sender, silent, senderLocale, args[1], null, ticketHandler) + } + + // /ticket version + private fun version( + sender: CommandSender, + locale: TMLocale, + ) { + val sentComponent = TextComponent("") + val components = listOf( + "&3===========================&r\n", + "&3&lTicketManager:&r&7 by HoshiKurama&r\n", + " &3GitHub Wiki: ", + "&7&nHERE&r\n", + " &3V$pluginVersion\n", + "&3===========================&r" + ) + .map(::addColour) + .map(::TextComponent) + + components[3].clickEvent = ClickEvent(ClickEvent.Action.OPEN_URL, locale.wikiLink) + components[3].hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickWiki)) + + components.forEach(sentComponent::addExtra) + + sender.spigot().sendMessage(sentComponent) + } + + // /ticket view + private suspend fun view( + sender: CommandSender, + locale: TMLocale, + ticketHandler: BasicTicketHandler, + ) { + + + withContext(asyncContext) { + val fullTicket = ticketHandler.toFullTicket() + val baseComponent = buildTicketInfoComponent(fullTicket, locale) + + if (!sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && ticketHandler.creatorStatusUpdate) + launch { ticketHandler.setCreatorStatusUpdate(false) } + + // Entries + fullTicket.actions.asSequence() + .filter { it.type == FullTicket.Action.Type.COMMENT || it.type == FullTicket.Action.Type.OPEN } + .map { + "\n${locale.viewFormatComment}" + .replace("%user%", it.user.toName(locale)) + .replace("%comment%", it.message!!) + } + .map(::addColour) + .map(::TextComponent) + .forEach(baseComponent::addExtra) + + sender.spigot().sendMessage(baseComponent) + } + } + + // /ticket viewdeep + private suspend fun viewDeep( + sender: CommandSender, + locale: TMLocale, + ticketHandler: BasicTicketHandler, + ) { + withContext(asyncContext) { + val fullTicket = ticketHandler.toFullTicket() + val baseComponent = buildTicketInfoComponent(fullTicket, locale) + + if (!sender.nonCreatorMadeChange(ticketHandler.creatorUUID) && ticketHandler.creatorStatusUpdate) + launch { ticketHandler.setCreatorStatusUpdate(false) } + + fun formatDeepAction(action: FullTicket.Action): String { + val result = when (action.type) { + FullTicket.Action.Type.OPEN, FullTicket.Action.Type.COMMENT -> + "\n${locale.viewFormatDeepComment}" + .replace("%comment%", action.message!!) + + FullTicket.Action.Type.SET_PRIORITY -> + "\n${locale.viewFormatDeepSetPriority}" + .replace("%priority%", + byteToPriority(action.message!!.toByte()).run { colourCode + toLocaledWord(locale) } + ) + + FullTicket.Action.Type.ASSIGN -> + "\n${locale.viewFormatDeepAssigned}" + .replace("%assign%", action.message ?: "") + + FullTicket.Action.Type.REOPEN -> "\n${locale.viewFormatDeepReopen}" + FullTicket.Action.Type.CLOSE -> "\n${locale.viewFormatDeepClose}" + FullTicket.Action.Type.MASS_CLOSE -> "\n${locale.viewFormatDeepMassClose}" + } + return result + .replace("%user%", action.user.toName(locale)) + .replace("%time%", action.timestamp.toLargestRelativeTime(locale)) + } + + fullTicket.actions.asSequence() + .map(::formatDeepAction) + .map(::addColour) + .map(::TextComponent) + .forEach(baseComponent::addExtra) + + sender.spigot().sendMessage(baseComponent) + } + } + + private fun buildPageComponent( + curPage: Int, + pageCount: Int, + locale: TMLocale, + baseCommand: (TMLocale) -> String, + ): TextComponent { + fun TextComponent.addForward() { + this.color = ChatColor.WHITE + this.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand(locale) + "${curPage + 1}") + this.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickNextPage)) + } + + fun TextComponent.addBack() { + this.color = ChatColor.WHITE + this.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, baseCommand(locale) + "${curPage - 1}") + this.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickBackPage)) + } + + val back = TextComponent("[${locale.pageBack}]".run(::addColour)) + val next = TextComponent("[${locale.pageNext}]".run(::addColour)) + + val separator = TextComponent("...............") + separator.color = ChatColor.DARK_GRAY + + val cc = configState.localeHandler.mainColourCode + val ofSection = "$cc($curPage${locale.pageOf}$pageCount)" + .run(::addColour) + .run(::TextComponent) + + when (curPage) { + 1 -> { + back.color = ChatColor.DARK_GRAY + next.addForward() + } + pageCount -> { + back.addBack() + next.color = ChatColor.DARK_GRAY + } + else -> { + back.addBack() + next.addForward() + } + } + + val textComponent = TextComponent("\n") + textComponent.apply { + addExtra(back) + addExtra(separator) + addExtra(ofSection) + addExtra(separator) + addExtra(next) + } + + return textComponent + } + + private fun createListEntry( + ticket: FullTicket, + locale: TMLocale + ): TextComponent { + val creator = ticket.creatorUUID.toName(locale) + val fixedAssign = ticket.assignedTo ?: "" + + // Shortens comment preview to fit on one line + val fixedComment = ticket.run { + if (12 + id.toString().length + creator.length + fixedAssign.length + actions[0].message!!.length > 58) + actions[0].message!!.substring( + 0, + 43 - id.toString().length - fixedAssign.length - creator.length + ) + "..." + else actions[0].message!! + } + + return "\n${locale.listFormatEntry}" + .replace("%priorityCC%", ticket.priority.colourCode) + .replace("%ID%", "${ticket.id}") + .replace("%creator%", creator) + .replace("%assign%", fixedAssign) + .replace("%comment%", fixedComment) + .run(::addColour) + .run(::TextComponent) + .run { convertToClickToView(ticket.id, locale) } + } + + private suspend fun createGeneralList( + args: List, + locale: TMLocale, + headerFormat: String, + getIDPriorityPair: suspend (Database) -> Flow>, + baseCommand: (TMLocale) -> String + ): TextComponent { + val chunkedIDs = getIDPriorityPair(configState.database) + .toList() + .sortedWith(compareByDescending> { it.second }.thenByDescending { it.first } ) + .map { it.first } + .chunked(8) + + val page = if (args.size == 2 && args[1].toInt() in 1..chunkedIDs.size) args[1].toInt() else 1 + + val fullTickets = chunkedIDs.getOrNull(page - 1) + ?.run { configState.database.getFullTickets(this, asyncContext) } + ?.toList() + ?: emptyList() + + val builder = TextComponent("") + headerFormat + .run(::addColour) + .run(builder::addExtra) + + if (fullTickets.isNotEmpty()) { + + fullTickets.map { createListEntry(it, locale) } + .forEach(builder::addExtra) + + if (chunkedIDs.size > 1) + buildPageComponent(page, chunkedIDs.size, locale, baseCommand) + .run(builder::addExtra) + } + + return builder + } + + private fun buildTicketInfoComponent( + ticket: FullTicket, + locale: TMLocale, + ): TextComponent { + val textComponent = TextComponent("") + + val info = listOf( + "\n${locale.viewFormatHeader}" + .replace("%num%", "${ticket.id}"), + "\n${locale.viewFormatSep1}", + "\n${locale.viewFormatInfo1}" + .replace("%creator%", ticket.creatorUUID.toName(locale)) + .replace("%assignment%", ticket.assignedTo ?: ""), + "\n${locale.viewFormatInfo2}" + .replace("%priority%", ticket.priority.run { colourCode + toLocaledWord(locale) }) + .replace("%status%", ticket.status.run { colourCode + toLocaledWord(locale) }), + "\n${locale.viewFormatInfo3}" + .replace("%location%", ticket.location?.toString() ?: ""), + ) + .map(::addColour) + .map(::TextComponent) + + if (ticket.location != null) { + info[4].hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickTeleport)) + info[4].clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, + locale.run { "/$commandBase $commandWordTeleport ${ticket.id}" }) + } + + info.forEach(textComponent::addExtra) + return textComponent + } +} + +/*-------------------------*/ +/* Other Functions */ +/*-------------------------*/ + +private inline fun check(error: () -> Unit, predicate: () -> Boolean): Boolean { + return if (predicate()) true else error().run { false } +} + +private inline fun Boolean.thenCheck(error: () -> Unit, predicate: () -> Boolean): Boolean { + return if (!this) false + else if (predicate()) true + else error().run { false } +} + +private fun CommandSender.nonCreatorMadeChange(creatorUUID: UUID?): Boolean { + if (creatorUUID == null) return false + return this.toUUIDOrNull()?.notEquals(creatorUUID) ?: true +} + +private fun CommandSender.toTicketLocationOrNull() = if (this is Player) + location.run { BasicTicket.TicketLocation(world!!.name, blockX, blockY, blockZ) } + else null + +private fun TextComponent.convertToClickToView(id: Int, locale: TMLocale): TextComponent { + hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, Text(locale.clickViewTicket)) + clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, locale.run { "/$commandBase $commandWordView $id" } ) + + val contentStop = TextComponent("") + contentStop.hoverEvent = null + contentStop.clickEvent = null + + this.addExtra(contentStop) + return this +} \ No newline at end of file diff --git a/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/PlayerJoin.kt b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/PlayerJoin.kt new file mode 100644 index 0000000..0ffa538 --- /dev/null +++ b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/PlayerJoin.kt @@ -0,0 +1,68 @@ +package com.github.hoshikurama.ticketmanager.spigot.events + +import com.github.hoshikurama.ticketmanager.spigot.* +import com.github.shynixn.mccoroutine.asyncDispatcher +import com.github.shynixn.mccoroutine.minecraftDispatcher +import kotlinx.coroutines.flow.count +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerJoinEvent + +class PlayerJoin : Listener { + + @EventHandler + suspend fun onPlayerJoin(event: PlayerJoinEvent) = withContext(mainPlugin.minecraftDispatcher) { + if (mainPlugin.pluginState.pluginLocked.get()) return@withContext + val player = event.player + + withContext(mainPlugin.asyncDispatcher) { + + //Plugin Update Checking + launch { + val pluginUpdateStatus = configState.pluginUpdateAvailable.await() + if (player.has("ticketmanager.notify.pluginUpdate") && pluginUpdateStatus != null) { + player.toTMLocale().notifyPluginUpdate + .replace("%current%", pluginUpdateStatus.first) + .replace("%latest%", pluginUpdateStatus.second) + .run(::addColour) + .run(player::sendMessage) + } + } + + // Unread Updates + launch { + if (player.has("ticketmanager.notify.unreadUpdates.onJoin")) { + configState.database.getIDsWithUpdatesFor(player.uniqueId) + .toList() + .run { if (size == 0) null else this } + ?.run { + val template = if (size == 1) player.toTMLocale().notifyUnreadUpdateSingle + else player.toTMLocale().notifyUnreadUpdateMulti + val tickets = this.joinToString(", ") + + template.replace("%num%", tickets) + .run(::addColour) + .run(player::sendMessage) + } + } + } + + // View Open-Count and Assigned-Count Tickets + launch { + if (player.has("ticketmanager.notify.openTickets.onJoin")) { + val open = configState.database.getOpenIDPriorityPairs() + val assigned = configState.database.getAssignedOpenIDPriorityPairs(player.name, mainPlugin.perms.getPlayerGroups(player).toList()) + + player.toTMLocale().notifyOpenAssigned + .replace("%open%", "${open.count()}") + .replace("%assigned%", "${assigned.count()}") + .run(::addColour) + .run(player::sendMessage) + } + } + } + } +} \ No newline at end of file diff --git a/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/TabComplete.kt b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/TabComplete.kt new file mode 100644 index 0000000..6c4ee36 --- /dev/null +++ b/Spigot/src/main/kotlin/com/github/hoshikurama/ticketmanager/spigot/events/TabComplete.kt @@ -0,0 +1,314 @@ +package com.github.hoshikurama.ticketmanager.spigot.events + +import com.github.hoshikurama.ticketmanager.common.TMLocale +import com.github.hoshikurama.ticketmanager.common.databases.Database +import com.github.hoshikurama.ticketmanager.spigot.has +import com.github.hoshikurama.ticketmanager.spigot.mainPlugin +import com.github.hoshikurama.ticketmanager.spigot.toTMLocale +import org.bukkit.Bukkit +import org.bukkit.World +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.command.TabCompleter +import org.bukkit.entity.Player + +class TabComplete: TabCompleter { + + override fun onTabComplete( + sender: CommandSender, + command: Command, + alias: String, + args: Array + ): MutableList { + return tabCompleteFunction(sender, args.toList()).toMutableList() + } + + private fun tabCompleteFunction( + sender: CommandSender, + args: List + ): List { + val blankList = listOf("") + + if (!sender.has("ticketmanager.commandArg.autotab") && sender is Player) return blankList + val locale = sender.toTMLocale() + val perms = LazyPermissions(locale, sender) + + return locale.run { + if (args.size <= 1) return@run perms.getPermissiveCommands() + .filter { it.startsWith(args[0]) } + + when (args[0]) { + commandWordAssign, commandWordSilentAssign -> when { // /ticket assign + !perms.hasAssignVariation -> listOf("") + args.size == 2 -> listOf("<$parameterID>") + .filter { it.startsWith(args[1]) } + args.size == 3 -> { + val groups = mainPlugin.perms.groups.map { "::$it" } + (listOf("<$parameterAssignment...>") + offlinePlayerNames() + groups + listOf(locale.consoleName)) + .filter { it.startsWith(args[2]) } + } + else -> listOf("") + } + + commandWordClaim, commandWordSilentClaim, commandWordUnassign, commandWordSilentUnassign -> when { // /ticket claim + !perms.hasAssignVariation -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordClose, commandWordSilentClose -> when { // /ticket close [Comment...] + !perms.hasClose -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> (listOf("[$parameterComment...]") + onlineSeenPlayers(sender)) + .filter { it.startsWith(args[args.lastIndex]) } + } + + commandWordCloseAll, commandWordSilentCloseAll -> when { // /ticket closeall + !perms.hasMassClose -> listOf("") + args.size == 2 -> listOf("<$parameterLowerID>").filter { it.startsWith(args[1]) } + args.size == 3 -> listOf("<$parameterUpperID>").filter { it.startsWith(args[2]) } + else -> listOf("") + } + + commandWordComment, commandWordSilentComment -> when { // /ticket comment + !perms.hasComment -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> (listOf("<$parameterComment...>") + onlineSeenPlayers(sender)) + .filter { it.startsWith(args[args.lastIndex]) } + } + + commandWordCreate -> when { // /ticket create + !perms.hasCreate -> listOf("") + else -> (listOf("<$parameterComment...>") + onlineSeenPlayers(sender)) + .filter { it.startsWith(args[args.lastIndex]) } + } + + commandWordHelp -> listOf("") + + commandWordHistory -> when { // /ticket history [User] [Page] + !perms.hasHistory -> listOf("") + args.size == 2 -> (listOf("[$parameterUser]", locale.consoleName) + offlinePlayerNames()) + .filter { it.startsWith(args[1]) } + args.size == 3 -> listOf("[$parameterPage]").filter { it.startsWith(args[2]) } + else -> listOf("") + } + + commandWordList, commandWordListAssigned -> when { // /ticket list(assigned) [Page] + !perms.hasListVariation -> listOf("") + args.size == 2 -> listOf("[$parameterPage]").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordReopen, commandWordSilentReopen -> when { // /ticket reopen + !perms.hasReopen -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordSetPriority, commandWordSilentSetPriority -> when { // /ticket setpriority + !perms.hasPriority -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + args.size == 3 -> listOf("<$parameterLevel>", "1", "2", "3", "4", "5").filter { + it.startsWith( + args[2] + ) + } + else -> listOf("") + } + + commandWordTeleport -> when { // /ticket teleport + !perms.hasTeleport -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordVersion -> listOf("") + + commandWordView -> when { // /ticket view + !perms.hasView -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordDeepView -> when { + !perms.hasDeepView -> listOf("") + args.size == 2 -> listOf("<$parameterID>").filter { it.startsWith(args[1]) } + else -> listOf("") + } + + commandWordReload -> listOf("") + + commandWordSearch -> { //ticket search keywords:separated,by,commas status:OPEN/CLOSED time:5w creator:creator priority:value assignedto:player world:world + if (!perms.hasSearch) return@run listOf("") + + val curArgument = args[args.lastIndex] + val splitArgs = curArgument.split(":", limit = 2) + + if (splitArgs.size < 2) + return@run locale.run { + listOf( + "$searchAssigned:", + "$searchCreator:", + "$searchKeywords:", + "$searchPriority:", + "$searchStatus:", + "$searchWorld:", + "$searchClosedBy:", + "$searchLastClosedBy:", + "$searchTime:", + ) + } + .filter { it.startsWith(curArgument) } + + // String now has form "constraint:" + return@run when (splitArgs[0]) { + searchAssigned -> { + val groups = mainPlugin.perms.groups.map { "::$it" } + (offlinePlayerNames() + groups) + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + searchCreator, searchLastClosedBy, searchClosedBy -> { + (offlinePlayerNames() + listOf(locale.consoleName)) + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + searchPriority -> { + listOf("1", "2", "3", "4", "5") + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + locale.searchStatus -> { + listOf(locale.statusOpen, locale.statusClosed) + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + searchWorld -> { + Bukkit.getWorlds() + .map(World::getName) + .filter { it.startsWith(splitArgs[1]) } + .map { "${splitArgs[0]}:$it" } + } + + searchTime -> { + locale.run { + listOf( + searchTimeSecond, + searchTimeMinute, + searchTimeHour, + searchTimeDay, + searchTimeWeek, + searchTimeYear + ) + } + .filter { curArgument.last().digitToIntOrNull() != null } + .map { "${splitArgs[0]}:${splitArgs[1]}$it" } + } + + searchKeywords -> listOf(curArgument) + + else -> listOf("") + } + } + + commandWordConvertDB -> when { // /ticket convertDatabase + !perms.hasConvertDB -> listOf("") + args.size == 2 -> + Database.Type.values() + .map(Database.Type::name) + .filter { it.startsWith(args[1]) } + else -> listOf("") + } + + else -> listOf("") + } + } + } + + private fun onlineSeenPlayers(sender: CommandSender): List { + return if (sender is Player) + Bukkit.getOnlinePlayers() + .filter(sender::canSee) + .map { it.name } + else Bukkit.getOnlinePlayers() + .map { it.name } + } + + private fun offlinePlayerNames() = Bukkit.getOfflinePlayers().mapNotNull { it.name }.toList() + + + class LazyPermissions(private val locale: TMLocale, private val sender: CommandSender) { + val hasAssignVariation by lazy { sender.has("ticketmanager.command.assign") } + val hasCreate by lazy { sender.has("ticketmanager.command.create") } + val hasListVariation by lazy { sender.has("ticketmanager.command.list") } + val hasReopen by lazy { sender.has("ticketmanager.command.reopen") } + val hasSearch by lazy { sender.has("ticketmanager.command.search") } + val hasPriority by lazy { sender.has("ticketmanager.command.setPriority") } + val hasTeleport by lazy { sender.has("ticketmanager.command.teleport") } + val hasMassClose by lazy { sender.has("ticketmanager.command.closeAll") } + val hasConvertDB by lazy { sender.has("ticketmanager.command.convertDatabase") } + private val hasHelp by lazy { sender.has("ticketmanager.command.help") } + private val hasReload by lazy { sender.has("ticketmanager.command.reload") } + private val hasSilent by lazy { sender.has("ticketmanager.commandArg.silence") } + val hasClose by lazy { + sender.has("ticketmanager.command.close.all") + || sender.has("ticketmanager.command.close.own") + } + val hasComment by lazy { + sender.has("ticketmanager.command.comment.all") + || sender.has("ticketmanager.command.comment.own") + } + val hasView by lazy { + sender.has("ticketmanager.command.view.all") + || sender.has("ticketmanager.command.view.own") + } + val hasDeepView by lazy { + sender.has("ticketmanager.command.viewdeep.all") + || sender.has("ticketmanager.command.viewdeep.own") + } + val hasHistory by lazy { + sender.has("ticketmanager.command.history.all") + || sender.has("ticketmanager.command.history.own") + } + + + fun getPermissiveCommands(): List { + return mapOf( + locale.commandWordAssign to hasAssignVariation, + locale.commandWordSilentAssign to (hasAssignVariation && hasSilent), + locale.commandWordClaim to hasAssignVariation, + locale.commandWordSilentClaim to (hasAssignVariation && hasSilent), + locale.commandWordClose to hasClose, + locale.commandWordSilentClose to (hasClose && hasSilent), + locale.commandWordCloseAll to hasMassClose, + locale.commandWordSilentCloseAll to (hasMassClose && hasSilent), + locale.commandWordComment to hasComment, + locale.commandWordSilentComment to (hasComment && hasSilent), + locale.commandWordConvertDB to hasConvertDB, + locale.commandWordCreate to hasCreate, + locale.commandWordHelp to hasHelp, + locale.commandWordHistory to hasHistory, + locale.commandWordList to hasListVariation, + locale.commandWordListAssigned to hasListVariation, + locale.commandWordReload to hasReload, + locale.commandWordReopen to hasReopen, + locale.commandWordSilentReopen to (hasReopen && hasSilent), + locale.commandWordSearch to hasSearch, + locale.commandWordSetPriority to hasPriority, + locale.commandWordSilentSetPriority to (hasPriority && hasSilent), + locale.commandWordTeleport to hasTeleport, + locale.commandWordUnassign to hasAssignVariation, + locale.commandWordSilentUnassign to (hasAssignVariation && hasSilent), + locale.commandWordVersion to true, + locale.commandWordView to hasView, + locale.commandWordDeepView to hasDeepView + ) + .filter { it.value } + .map { it.key } + } + } +} \ No newline at end of file diff --git a/Spigot/src/main/resources/config.yml b/Spigot/src/main/resources/config.yml new file mode 100644 index 0000000..5e21e94 --- /dev/null +++ b/Spigot/src/main/resources/config.yml @@ -0,0 +1,119 @@ +############################################## +############################################## +# TicketManager Config File +############################################## +############################################## +# +# Plugin Proudly Made by: HoshiKurama +# +# All permissions and commands can +# be found on the GitHub wiki page. +# https://github.com/HoshiKurama/TicketManager +# +# ########################################## +# Locale: +# ########################################## +# Force Locale: Forces specific language to be used for commands and responses. +# Values: true, false +Force_Locale: false +# +# Preferred Locale: Locale to use when player locale is not found. Using a non-supported +# locale will cause TicketManager to internally default to Canadian English. +# Values: +# 'en_CA' = Canadian English +# 'en_US' = United States English +Preferred_Locale: 'en_CA' +# +# Console Locale: Locale Console will use. Using a non-supported locale will cause +# TicketManager to internally default to Preferred_Locale. +# Values: +# 'en_CA' = Canadian English +# 'en_US' = United States English +Console_Locale: 'en_CA' +# +# ########################################## +# Cooldown: +# ########################################## +# Use Cool-down: Determine if cool-downs should be applied to ticket commands +# that create or modify a ticket. Cool-downs apply to ALL users without the +# override permission. +# Values: true, false +Use_Cooldowns: false +# +# Ticket Modification Cool-down: Time in seconds before a user is able to create or +# modify a ticket. This applies to ALL users without the override permission if +# cool-downs are enabled. +Cooldown_Time: 0 +# +# ########################################## +# Database Settings: +# ########################################## +# +# Database mode: Type of database used. Internally defaults +# to SQLite if invalid value is used. Information only needs +# to be filled out for the desired database type. +# +# SQLite: +# Stores information in the SQLite file in the TicketManager folder. +# Pros: + No setup. +# + No external database needed. +# + Faster than MySQL for search and history command. +# Cons: - Stores data in TicketManager folder. +# - Easier to overwhelm than Memory or MySQL. +# +# MySQL: +# Stores information in a database that may or may not be on the server. +# Pros: + Can handle more traffic than SQLite. +# + Can be separate from server storage. +# Cons: - Must have MySQL database. +# - Search and history commands slower. +# +# Memory: +# Memory refers to TicketManager's custom solution for running the entire +# database in RAM. Data is backed up throughout normal server operations or +# on server shutdowns. It is then loaded up on startup if selected. DO NOT +# USE THIS DATABASE TYPE UNLESS YOU UNDERSTAND THE RISKS OF RUNNING A +# DATABASE IN RAM!!! +# Pros: + Incredibly responsive +# + Incredibly fast queries +# Cons: - Any power loss or server crash will result in all data beyond +# the last backup being lost. +# +# Values: 'MySQL','SQLite','Memory' +Database_Mode: 'SQLite' +# +# ###################### +# MySQL Database +# ###################### +MySQL_Port: '' +MySQL_Host: '' +MySQL_DBName: '' +MySQL_Username: '' +MySQL_Password: '' +# +# ###################### +# Memory as Database +# ###################### +# Time in seconds between database backups being made +Memory_Backup_Frequency: 600 +# +# ########################################### +# Other +# ########################################### +# Colour Code: Colour code used to add simple colour customization. +# Must be in the form "&". eg: &3, &6, &b, etc +# DO NOT USE ANY COLOUR CODES OTHER THAN COLOUR! +Colour_Code: '&3' +# +# Allow Unread Ticket Updates: When users other than the creator +# make ticket changes, the creator can optionally be alerted at +# regular intervals or on login which tickets have a status change. +# This is NOT the same alert one immediately receives on ticket +# status change. Setting value to 'false' prevents TicketManager +# from flagging a ticket as unread. +# Values: true, false +Allow_Unread_Ticket_Updates: true +# +# Allow Update Checking: Server can check that the latest version of TicketManager is installed during startup or reloads. +# Values: true, false +Allow_UpdateChecking: true \ No newline at end of file diff --git a/Spigot/src/main/resources/plugin.yml b/Spigot/src/main/resources/plugin.yml new file mode 100644 index 0000000..ebc42bc --- /dev/null +++ b/Spigot/src/main/resources/plugin.yml @@ -0,0 +1,244 @@ +name: TicketManager +version: 5.0.0 +main: com.github.hoshikurama.ticketmanager.spigot.TicketManagerPlugin +api-version: 1.17 +authors: [HoshiKurama] +depend: [Vault] +libraries: + - org.jetbrains.kotlin:kotlin-stdlib:1.5.21 + - mysql:mysql-connector-java:8.0.25 + - org.xerial:sqlite-jdbc:3.34.0 + - com.github.jasync-sql:jasync-mysql:1.2.2 + - com.github.seratch:kotliquery:1.3.1 + - org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.1 + - joda-time:joda-time:2.10.10 + - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:1.5.0 + - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:1.5.0 + - org.jetbrains.kotlinx:kotlinx-serialization-core-jvm:1.2.2 +commands: + ticket: + description: Base for all TicketManager commands +permissions: + ticketmanager.command.assign: + description: Assign a ticket + default: false + ticketmanager.command.close.all: + description: Close any ticket + default: false + ticketmanager.command.close.own: + description: Close only own tickets + default: false + ticketmanager.command.closeAll: + description: Mass close tickets + default: false + ticketmanager.command.comment.all: + description: Comment on any ticket + default: false + ticketmanager.command.comment.own: + description: Comment on only own tickets + default: false + ticketmanager.command.convertDatabase: + description: Convert database to another supported type + default: false + ticketmanager.command.create: + description: Create a ticket + default: false + ticketmanager.command.help: + description: See list of ticket commands to which user has permissions for + default: false + ticketmanager.command.history.all: + description: View all users' histories of tickets + default: false + ticketmanager.command.history.own: + description: View own history of tickets + default: false + ticketmanager.command.list: + description: List all open tickets + default: false + ticketmanager.command.reload: + description: Restarts plugin + default: false + ticketmanager.command.reopen: + description: Reopen any ticket + default: false + ticketmanager.command.search: + description: Search database for certain tickets + default: false + ticketmanager.command.setPriority: + description: Set priority of any ticket + default: false + ticketmanager.command.teleport: + description: Teleport to any ticket + default: false + ticketmanager.command.view.all: + description: View any ticket + default: false + ticketmanager.command.view.own: + description: View own ticket + default: false + ticketmanager.command.viewdeep.all: + description: View any detailed ticket + default: false + ticketmanager.command.viewdeep.own: + description: View own detailed ticket information + default: false + ticketmanager.commandArg.cooldown.override: + description: Overrides cooldown if enabled + default: false + ticketmanager.commandArg.silence: + description: Ability to perform silent variations + default: false + ticketmanager.commandArg.autotab: + description: Allows autotabbing (must have permission for command too) + default: false + ticketmanager.notify.openTickets.onJoin: + description: See open and assigned ticket count on login + default: false + ticketmanager.notify.openTickets.scheduled: + description: See open and assigned ticket count at scheduled intervals + default: false + ticketmanager.notify.unreadUpdates.onJoin: + description: See own tickets with updates on login + default: false + ticketmanager.notify.unreadUpdates.scheduled: + description: See own tickets with updates at scheduled intervals + default: false + ticketmanager.notify.info: + description: See general TicketManager information + default: false + ticketmanager.notify.pluginUpdate: + description: Get notified of plugin update on login + default: false + ticketmanager.notify.warning: + description: See modified stacktrace on errors + default: false + ticketmanager.notify.change.comment: + description: Creator sees when user comments on their ticket + default: false + ticketmanager.notify.change.close: + description: Creator sees when user closes their ticket + default: false + ticketmanager.notify.change.reopen: + description: Creator sees when user reopens their ticket + default: false + ticketmanager.notify.massNotify.assign: + description: See ticket assignment events + default: false + ticketmanager.notify.massNotify.close: + description: See any ticket close events + default: false + ticketmanager.notify.massNotify.comment: + description: See any ticket comment events + default: false + ticketmanager.notify.massNotify.create: + description: See ticket creation events + default: false + ticketmanager.notify.massNotify.massClose: + description: See ticket mass close events + default: false + ticketmanager.notify.massNotify.priority: + description: See ticket priority change events + default: false + ticketmanager.notify.massNotify.reopen: + description: See ticket reopen events + default: false + ticketmanager.command.*: + description: Wildcard for all commands and commandargs + children: + ticketmanager.command.assign: true + ticketmanager.command.close.all: true + ticketmanager.command.close.own: true + ticketmanager.command.closeAll: true + ticketmanager.command.comment.all: true + ticketmanager.command.comment.own: true + ticketmanager.command.convertDatabase: true + ticketmanager.command.create: true + ticketmanager.command.help: true + ticketmanager.command.history.all: true + ticketmanager.command.history.own: true + ticketmanager.command.list: true + ticketmanager.command.reload: true + ticketmanager.command.reopen: true + ticketmanager.command.search: true + ticketmanager.command.setPriority: true + ticketmanager.command.teleport: true + ticketmanager.command.view.all: true + ticketmanager.command.view.own: true + ticketmanager.command.viewdeep.all: true + ticketmanager.command.viewdeep.own: true + default: false + ticketmanager.notify.change.*: + description: Wildcard for all status changes on own ticket + children: + ticketmanager.notify.change.comment: true + ticketmanager.notify.change.close: true + ticketmanager.notify.change.reopen: true + default: false + ticketmanager.massNotify.*: + description: Wildcard for all mass notifications (except errors and plugin info) + children: + ticketmanager.notify.massNotify.assign: true + ticketmanager.notify.massNotify.close: true + ticketmanager.notify.massNotify.comment: true + ticketmanager.notify.massNotify.create: true + ticketmanager.notify.massNotify.massClose: true + ticketmanager.notify.massNotify.priority: true + ticketmanager.notify.massNotify.reopen: true + default: false + ticketmanager.notify.*: + description: Wildcard for all notification permissions + children: + ticketmanager.notify.openTickets.onJoin: true + ticketmanager.notify.openTickets.scheduled: true + ticketmanager.notify.unreadUpdates.onJoin: true + ticketmanager.notify.unreadUpdates.scheduled: true + ticketmanager.notify.warning: true + ticketmanager.notify.info: true + ticketmanager.massNotify.*: true + ticketmanager.notify.change.*: true + ticketmanager.notify.pluginUpdate: true + default: false + ticketmanager.*: + description: Wildcard for all TicketManager permissions + children: + ticketmanager.notify.*: true + ticketmanager.command.*: true + ticketmanager.commandArg.cooldown.override: true + ticketmanager.commandArg.silence: true + ticketmanager.commandArg.autotab: true + default: op + ticketmanager.basic: + description: Preset for normal users + children: + ticketmanager.command.close.own: true + ticketmanager.command.comment.own: true + ticketmanager.command.create: true + ticketmanager.command.help: true + ticketmanager.command.history.own: true + ticketmanager.command.view.own: true + ticketmanager.commandArg.autotab: true + ticketmanager.notify.unreadUpdates.onJoin: true + ticketmanager.notify.unreadUpdates.scheduled: true + ticketmanager.notify.change.*: true + default: false + ticketmanager.manage: + description: Preset for staff (includes ticketmanager.basic) + children: + ticketmanager.basic: true + ticketmanager.command.assign: true + ticketmanager.command.close.all: true + ticketmanager.command.comment.all: true + ticketmanager.command.list: true + ticketmanager.command.reopen: true + ticketmanager.command.search: true + ticketmanager.command.setPriority: true + ticketmanager.command.view.all: true + ticketmanager.command.viewdeep.all: true + ticketmanager.command.viewdeep.own: true + ticketmanager.command.history.all: true + ticketmanager.commandArg.cooldown.override: true + ticketmanager.notify.openTickets.onJoin: true + ticketmanager.notify.openTickets.scheduled: true + ticketmanager.notify.info: true + ticketmanager.massNotify.*: true + default: false \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 797b104..19b2f93 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,5 +2,6 @@ rootProject.name = "TicketManager" include( "common", - "Paper" + "Paper", + "Spigot" ) \ No newline at end of file