From 4273fa9566b8abdbd5f0b66377430ce78dd9dfec Mon Sep 17 00:00:00 2001 From: shedaniel Date: Mon, 15 Feb 2021 17:23:53 +0800 Subject: [PATCH] Improve !fabric and add !forge Signed-off-by: shedaniel --- build.gradle | 2 +- .../shedaniel/linkie/discord/CommandBase.kt | 6 +- .../me/shedaniel/linkie/discord/LinkieBot.kt | 1 + .../shedaniel/linkie/discord/ValueKeeper.kt | 33 ++++- .../AbstractPlatformVersionCommand.kt | 112 +++++++++++++++++ .../linkie/discord/commands/FabricCommand.kt | 75 +++++------ .../linkie/discord/commands/ForgeCommand.kt | 117 ++++++++++++++++++ 7 files changed, 292 insertions(+), 54 deletions(-) create mode 100644 src/main/kotlin/me/shedaniel/linkie/discord/commands/AbstractPlatformVersionCommand.kt create mode 100644 src/main/kotlin/me/shedaniel/linkie/discord/commands/ForgeCommand.kt diff --git a/build.gradle b/build.gradle index b2d2429..9b1410f 100644 --- a/build.gradle +++ b/build.gradle @@ -34,7 +34,7 @@ repositories { } dependencies { - compile("me.shedaniel:linkie-core:1.0.59") + compile("me.shedaniel:linkie-core:1.0.60") compile("com.discord4j:discord4j-core:3.1.3") { force = true } diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/CommandBase.kt b/src/main/kotlin/me/shedaniel/linkie/discord/CommandBase.kt index 4cddaf3..4368d55 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/CommandBase.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/CommandBase.kt @@ -48,21 +48,21 @@ fun embedCreator(creator: EmbedCreator) = creator fun MessageCreator.sendPages( initialPage: Int, maxPages: Int, - creator: suspend EmbedCreateSpec.(Int) -> Unit, + creator: suspend EmbedCreateSpec.(page: Int) -> Unit, ) = sendPages(initialPage, maxPages, previous.author.get().id, creator) fun MessageCreator.sendPages( initialPage: Int, maxPages: Int, user: User, - creator: suspend EmbedCreateSpec.(Int) -> Unit, + creator: suspend EmbedCreateSpec.(page: Int) -> Unit, ) = sendPages(initialPage, maxPages, user.id, creator) fun MessageCreator.sendPages( initialPage: Int, maxPages: Int, userId: Snowflake, - creator: suspend EmbedCreateSpec.(Int) -> Unit, + creator: suspend EmbedCreateSpec.(page: Int) -> Unit, ) { var page = initialPage val builder = embedCreator { creator(this, page) } diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/LinkieBot.kt b/src/main/kotlin/me/shedaniel/linkie/discord/LinkieBot.kt index 5cba5dd..be51bc1 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/LinkieBot.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/LinkieBot.kt @@ -168,5 +168,6 @@ fun registerCommands(commands: CommandHandler) { commands.registerCommand(TricksCommand, "trick") commands.registerCommand(ValueCommand, "value") commands.registerCommand(FabricCommand, "fabric") + commands.registerCommand(ForgeCommand, "forge") commands.registerCommand(GoogleCommand, "google") } \ No newline at end of file diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/ValueKeeper.kt b/src/main/kotlin/me/shedaniel/linkie/discord/ValueKeeper.kt index 38fdca5..8965480 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/ValueKeeper.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/ValueKeeper.kt @@ -22,9 +22,10 @@ import java.time.Duration import java.util.* import kotlin.concurrent.timerTask import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty @Suppress("MemberVisibilityCanBePrivate", "unused") -class ValueKeeper constructor(val timeToKeep: Duration, var value: Optional, val getter: suspend () -> T) { +class ValueKeeper constructor(val timeToKeep: Duration, var valueBackend: Optional, val getter: suspend () -> T) : Lazy { companion object { private val timer = Timer() } @@ -40,10 +41,10 @@ class ValueKeeper constructor(val timeToKeep: Duration, var value: Optional constructor(val timeToKeep: Duration, var value: Optional valueKeeper(timeToKeep: Duration = Duration.ofMinutes(2), getter: suspend () -> T): ReadOnlyProperty { - val keeper = ValueKeeper(timeToKeep, getter) - return ReadOnlyProperty { _, _ -> runBlockingNoJs { keeper.get() } } +fun valueKeeper(timeToKeep: Duration = Duration.ofMinutes(2), getter: suspend () -> T): ValueKeeperProperty = + ValueKeeperProperty(timeToKeep, getter) + +class ValueKeeperProperty( + timeToKeep: Duration, + getter: suspend () -> T, +) : ReadOnlyProperty, Lazy { + val keeperLazy = lazy { ValueKeeper(timeToKeep, getter) } + val keeper by keeperLazy + val property = ReadOnlyProperty { _, _ -> runBlockingNoJs { keeper.get() } } + + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return this.property.getValue(thisRef, property) + } + + override fun isInitialized(): Boolean = keeperLazy.isInitialized() && keeper.isInitialized() + override val value: T + get() = runBlockingNoJs { keeper.get() } } \ No newline at end of file diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/commands/AbstractPlatformVersionCommand.kt b/src/main/kotlin/me/shedaniel/linkie/discord/commands/AbstractPlatformVersionCommand.kt new file mode 100644 index 0000000..02496c6 --- /dev/null +++ b/src/main/kotlin/me/shedaniel/linkie/discord/commands/AbstractPlatformVersionCommand.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2019, 2020 shedaniel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.shedaniel.linkie.discord.commands + +import discord4j.core.`object`.entity.User +import discord4j.core.`object`.entity.channel.MessageChannel +import discord4j.core.event.domain.message.MessageCreateEvent +import discord4j.core.spec.EmbedCreateSpec +import me.shedaniel.linkie.discord.CommandBase +import me.shedaniel.linkie.discord.MessageCreator +import me.shedaniel.linkie.discord.sendPages +import me.shedaniel.linkie.discord.utils.addInlineField +import me.shedaniel.linkie.discord.utils.buildSafeDescription +import me.shedaniel.linkie.discord.utils.discriminatedName +import me.shedaniel.linkie.discord.utils.setSafeDescription +import me.shedaniel.linkie.discord.utils.setTimestampToNow +import me.shedaniel.linkie.discord.validateUsage +import me.shedaniel.linkie.discord.valueKeeper +import java.time.Duration +import kotlin.math.ceil + +abstract class AbstractPlatformVersionCommand> : CommandBase { + private val dataKeeper = valueKeeper(Duration.ofMinutes(10)) { updateData() } + protected val data by dataKeeper + + override suspend fun execute(event: MessageCreateEvent, message: MessageCreator, prefix: String, user: User, cmd: String, args: MutableList, channel: MessageChannel) { + args.validateUsage(prefix, 0..1, "$cmd [version|list|first]") + if (!dataKeeper.isInitialized()) { + message.sendEmbed { + setFooter("Requested by " + user.discriminatedName, user.avatarUrl) + setTimestampToNow() + buildSafeDescription { + append("Searching up version data.\n\nIf you are stuck with this message, please do the command again and report the issue on the issue tracker.") + } + }.subscribe() + } + if (args.isNotEmpty() && args[0] == "list") { + val maxPage = ceil(data.versions.size / 20.0).toInt() + message.sendPages(0, maxPage, user) { page -> + setTitle("Available Versions (Page ${page + 1}/$maxPage)") + buildSafeDescription { + data.versions.asSequence().drop(page * 20).take(20).forEach { versionString -> + val version = data[versionString] + appendLine("• $versionString" + when { + version.unstable -> " (Unstable)" + version.version == latestVersion -> " **(Latest)**" + else -> "" + }) + } + } + } + return + } + val latestVersion = this.latestVersion + val gameVersion = when { + args.isEmpty() -> latestVersion + args[0] == "first" -> data.versions.first() + else -> args[0] + } + require(data.versions.contains(gameVersion)) { "Invalid Version Specified: $gameVersion\nYou may list the versions available by using `$prefix$cmd list`" } + val version = data[gameVersion] + message.sendEmbed { + setTitle(getTitle(version.version)) + buildString { + if (data.versions.first() != latestVersion) { + appendLine("Tip: You can use `$prefix$cmd list` to view the available versions, or use `$prefix$cmd first` to view the first version, even if it is unstable.") + } else { + appendLine("Tip: You can use `$prefix$cmd list` to view the available versions.") + } + when { + version.unstable -> addInlineField("Type", "Unstable") + version.version == latestVersion -> addInlineField("Type", "Release (Latest)") + else -> addInlineField("Type", "Release") + } + version.appendData()(this@sendEmbed, this) + }.takeIf { it.isNotBlank() }?.also { + setSafeDescription(it) + } + }.subscribe() + } + + abstract val latestVersion: String + abstract fun getTitle(version: String): String + abstract fun updateData(): T +} + +interface PlatformData { + val versions: Set + + operator fun get(version: String): R +} + +interface PlatformVersion { + val version: String + val unstable: Boolean + + fun appendData(): EmbedCreateSpec.(descriptionBuilder: StringBuilder) -> Unit +} diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/commands/FabricCommand.kt b/src/main/kotlin/me/shedaniel/linkie/discord/commands/FabricCommand.kt index 8ec3e7e..ceb8a95 100644 --- a/src/main/kotlin/me/shedaniel/linkie/discord/commands/FabricCommand.kt +++ b/src/main/kotlin/me/shedaniel/linkie/discord/commands/FabricCommand.kt @@ -16,67 +16,37 @@ package me.shedaniel.linkie.discord.commands -import discord4j.core.`object`.entity.User -import discord4j.core.`object`.entity.channel.MessageChannel -import discord4j.core.event.domain.message.MessageCreateEvent +import discord4j.core.spec.EmbedCreateSpec import kotlinx.serialization.json.* import me.shedaniel.cursemetaapi.CurseMetaAPI -import me.shedaniel.linkie.discord.CommandBase -import me.shedaniel.linkie.discord.MessageCreator import me.shedaniel.linkie.discord.utils.addInlineField -import me.shedaniel.linkie.discord.validateUsage -import me.shedaniel.linkie.discord.valueKeeper import me.shedaniel.linkie.utils.tryToVersion import java.net.URL -import java.time.Duration -object FabricCommand : CommandBase { - private val json = Json { - - } - private val fabricData by valueKeeper(Duration.ofMinutes(10)) { updateData() } - private val latestVersion: String - get() = fabricData.versions.keys.asSequence().filter { +object FabricCommand : AbstractPlatformVersionCommand() { + private val json = Json {} + override val latestVersion: String + get() = data.versions.asSequence().filter { val version = it.tryToVersion() version != null && version.snapshot == null }.maxWithOrNull(compareBy { it.tryToVersion() })!! - override suspend fun execute(event: MessageCreateEvent, message: MessageCreator, prefix: String, user: User, cmd: String, args: MutableList, channel: MessageChannel) { - args.validateUsage(prefix, 0..1, "$cmd [version]") - val latestVersion = this.latestVersion - val gameVersion = if (args.isEmpty()) latestVersion else args[0] - require(fabricData.versions.containsKey(gameVersion)) { "Invalid Version Specified: $gameVersion" } - val version = fabricData.versions[gameVersion]!! - message.sendEmbed { - setTitle("Fabric Version for Minecraft $gameVersion") - addInlineField("Type", when (version.release) { - true -> when (version.version) { - latestVersion -> "Release (Latest)" - else -> "Release" - } - false -> "Unstable" - }) - addInlineField("Loader Version", version.loaderVersion) - addInlineField("Yarn Version", version.yarnVersion) - if (version.apiVersion != null) { - addInlineField("Api Version", version.apiVersion!!.version) - } - }.subscribe() - } + override fun getTitle(version: String): String = "Fabric Version for Minecraft $version" - private fun updateData(): FabricData { + override fun updateData(): FabricData { val data = FabricData() val meta = json.parseToJsonElement(URL("https://meta.fabricmc.net/v2/versions").readText()).jsonObject val loaderVersion = meta["loader"]!!.jsonArray[0].jsonObject["version"]!!.jsonPrimitive.content + val installerVersion = meta["installer"]!!.jsonArray[0].jsonObject["version"]!!.jsonPrimitive.content val mappings = meta["mappings"]!!.jsonArray meta["game"]!!.jsonArray.asSequence().map(JsonElement::jsonObject).forEach { obj -> val version = obj["version"]!!.jsonPrimitive.content val release = version.tryToVersion().let { it != null && it.snapshot == null } val mappingsObj = mappings.firstOrNull { it.jsonObject["gameVersion"]!!.jsonPrimitive.content == version }?.jsonObject ?: return@forEach val yarnVersion = mappingsObj["version"]!!.jsonPrimitive.content - data.versions[version] = FabricVersion(version, release, loaderVersion, yarnVersion) + data.versionsMap[version] = FabricVersion(version, release, loaderVersion, installerVersion, yarnVersion) } - fillFabricApi(data.versions) + fillFabricApi(data.versionsMap) return data } @@ -112,16 +82,33 @@ object FabricCommand : CommandBase { } data class FabricData( - val versions: MutableMap = mutableMapOf(), - ) + val versionsMap: MutableMap = mutableMapOf(), + ) : PlatformData { + override fun get(version: String): FabricVersion = versionsMap[version]!! + override val versions: Set + get() = versionsMap.keys + } data class FabricVersion( - val version: String, + override val version: String, val release: Boolean, val loaderVersion: String, + val installerVersion: String, val yarnVersion: String, var apiVersion: FabricApiVersion? = null, - ) + ) : PlatformVersion { + override val unstable: Boolean + get() = !release + + override fun appendData(): EmbedCreateSpec.(StringBuilder) -> Unit = { + addInlineField("Loader Version", loaderVersion) + addInlineField("Installer Version", installerVersion) + addInlineField("Yarn Version", yarnVersion) + if (apiVersion != null) { + addInlineField("Api Version", apiVersion!!.version) + } + } + } data class FabricApiVersion( val version: String, diff --git a/src/main/kotlin/me/shedaniel/linkie/discord/commands/ForgeCommand.kt b/src/main/kotlin/me/shedaniel/linkie/discord/commands/ForgeCommand.kt new file mode 100644 index 0000000..583db4d --- /dev/null +++ b/src/main/kotlin/me/shedaniel/linkie/discord/commands/ForgeCommand.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2019, 2020 shedaniel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.shedaniel.linkie.discord.commands + +import discord4j.core.spec.EmbedCreateSpec +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import me.shedaniel.linkie.discord.utils.addInlineField +import me.shedaniel.linkie.utils.toVersion +import me.shedaniel.linkie.utils.tryToVersion +import org.dom4j.io.SAXReader +import java.net.URL +import java.util.* + +object ForgeCommand : AbstractPlatformVersionCommand() { + private val json = Json {} + override fun getTitle(version: String): String = "Forge Version for Minecraft $version" + + override val latestVersion: String + get() = data.versions.first() + + override fun updateData(): ForgeData { + val data = ForgeData() + SAXReader().read(URL("https://files.minecraftforge.net/maven/net/minecraftforge/forge/maven-metadata.xml")).rootElement + .element("versioning") + .element("versions") + .elementIterator("version") + .asSequence() + .map { it.text } + .forEach { + val mcVersion = it.substringBefore('-') + val mcVersionSemVer = mcVersion.tryToVersion() ?: return@forEach + val forgeVersion = it.substring(mcVersion.length + 1).substringBeforeLast('-') + data.versionsMap.getOrPut(mcVersion) { ForgeVersion(mcVersion, forgeVersion) }.also { version -> + version.forgeVersion = forgeVersion + } + } + json.parseToJsonElement(URL("http://export.mcpbot.bspk.rs/versions.json").readText()).jsonObject.forEach { mcVersion, mcpVersionsObj -> + mcpVersionsObj.jsonObject["snapshot"]?.jsonArray + ?.map { it.jsonPrimitive.content } + ?.maxByOrNull { it.toInt() } + ?.also { snapshotVersion -> + data.versionsMap[mcVersion]?.also { + it.mcpSnapshot = "$snapshotVersion-$mcVersion" + } + } + } + json.parseToJsonElement(URL("https://gist.githubusercontent.com/shedaniel/afc2748c6d5dd827d4cde161a49687ec/raw/mcp_versions.json").readText()).jsonObject.forEach { mcVersion, versionObj -> + if (versionObj.jsonObject["mcp"]?.jsonPrimitive?.content?.contains(mcVersion) == true) { + versionObj.jsonObject["name"]?.jsonPrimitive?.content?.substringAfterLast('-') + ?.also { snapshotVersion -> + data.versionsMap[mcVersion]?.also { + it.mcpSnapshot = "$snapshotVersion-$mcVersion" + it.tmp = true + } + } + } + } + return data + } + + data class ForgeData( + val versionsMap: SortedMap = TreeMap(compareByDescending { it.toVersion() }), + ) : PlatformData { + override val versions: Set + get() = versionsMap.keys + + override fun get(version: String): ForgeVersion = versionsMap[version]!! + } + + data class ForgeVersion( + override val version: String, + var forgeVersion: String, + var mcpSnapshot: String? = null, + var tmp: Boolean = false, + ) : PlatformVersion { + override val unstable: Boolean + get() = false + + override fun appendData(): EmbedCreateSpec.(StringBuilder) -> Unit = { + addInlineField("Forge Version", forgeVersion) + if (mcpSnapshot != null) { + addInlineField("MCP Version", mcpSnapshot!!) + if (tmp) { + it.insert(0, "The MCP version displayed here is manually managed, as 1.16+ MCP versions are not handled by MCP bot.\n" + + "If the following data is outdated, please report it on our issue tracker!\n\n") + } + } else { + val versions = data.versions.toMutableList() + val ourIndex = versions.indexOf(version) + versions.asSequence().drop(ourIndex + 1).map { data[it] }.firstOrNull { it.mcpSnapshot != null }?.also { usableVersion -> + addInlineField("MCP Version", usableVersion.mcpSnapshot!!) + if (usableVersion.tmp) { + it.insert(0, "The MCP version displayed here is manually managed, as 1.16+ MCP versions are not handled by MCP bot.\n" + + "If the following data is outdated, please report it on our issue tracker!\n\n") + } + } + } + } + } +}