mirror of
https://github.com/jie65535/mirai-console-jcf-plugin.git
synced 2025-06-09 17:59:15 +08:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
1ffd53cab1 | |||
184a6d9228 | |||
e6e03002e0 | |||
33336d574d | |||
![]() |
a6140577cb | ||
![]() |
b2090cc09b | ||
![]() |
21bcedcb17 | ||
![]() |
09974aad9e | ||
![]() |
f1e6e2fbf5 | ||
682cdbeb38 | |||
![]() |
8d70e376df | ||
25c756f307 | |||
3bac45c052 | |||
a9e45470fa | |||
37a70e211e | |||
382b485fa7 | |||
![]() |
31f4358ddb | ||
b3a28ef982 | |||
202a80ce45 | |||
afd06d7319 | |||
75bea8e6ce | |||
0558bbfd07 | |||
be20a22fe9 | |||
917d5f2a68 | |||
7fd31c194b | |||
![]() |
76d3cf2f9c | ||
![]() |
d95b54182b | ||
![]() |
a7d1370c39 | ||
![]() |
05a50b3b44 |
7
.gitignore
vendored
7
.gitignore
vendored
@ -119,3 +119,10 @@ run/
|
||||
|
||||
# Local Test Launch point
|
||||
src/test/kotlin/RunTerminal.kt
|
||||
/api_key.txt
|
||||
/config
|
||||
/data
|
||||
/logs
|
||||
/plugin-libraries
|
||||
/plugin-shared-libraries
|
||||
/plugins
|
22
README.md
22
README.md
@ -1,6 +1,8 @@
|
||||
# mirai-console-jcf-plugin
|
||||
基于Mirai Console的Curseforge插件
|
||||
|
||||
# 请注意:本插件需要申请 [Curseforge Api Key](https://console.curseforge.com/) 才可使用!!
|
||||
|
||||
## Introduction
|
||||
|
||||
允许用户通过`QQ`对[Curseforge](https://www.curseforge.com/)网站进行搜索查询
|
||||
@ -10,11 +12,18 @@
|
||||
支持查看文件列表与其下载地址,单独查看文件的更新日志。
|
||||
|
||||
## Usage
|
||||
- /jcf help # 帮助
|
||||
- /jcf ss \<filter\> # 直接搜索
|
||||
- /jcf sspack \<filter\> # 搜索整合包
|
||||
- /jcf ssmod \<filter\> # 搜索模组
|
||||
- /jcf ssres \<filter\> # 搜索资源包
|
||||
指令
|
||||
- /jcf help # 查看帮助
|
||||
- /jcf setApiKey # 设置Curseforge API Key
|
||||
|
||||
分类搜索命令(可配置)
|
||||
- 搜索模组: cfmod \<filter\>
|
||||
- 搜索整合包: cfpack \<filter\>
|
||||
- 搜索资源包: cfres \<filter\>
|
||||
- 搜索存档: cfword \<filter\>
|
||||
- 搜索水桶服插件: cfbukkit \<filter\>
|
||||
- 搜索附加: cfaddon \<filter\>
|
||||
- 搜索定制: cfcustom \<filter\>
|
||||
|
||||
## Screenshots
|
||||
|
||||
@ -32,6 +41,7 @@
|
||||
- [x] 搜索整合包
|
||||
- [x] 搜索资源包
|
||||
- [x] ~~搜索存档~~
|
||||
- [ ] 根据项目ID搜索
|
||||
---
|
||||
- [x] 分页选择
|
||||
- [ ] 获取介绍
|
||||
@ -48,5 +58,3 @@
|
||||
## 鸣谢
|
||||
- [Mirai](https://github.com/mamoe/mirai) 提供机器人平台
|
||||
- [Mirai Console](https://github.com/mamoe/mirai-console) 开放插件接入
|
||||
- [Curseforge API](https://github.com/Gaz492/CurseforgeAPI) 提供`Curseforge API`
|
||||
- [Curseforge API Kotlin library](https://github.com/pearxteam/cursekt) 提供`Curseforge API`的`kotlin`封装
|
||||
|
@ -1,15 +1,22 @@
|
||||
plugins {
|
||||
val kotlinVersion = "1.5.10"
|
||||
val kotlinVersion = "1.7.10"
|
||||
kotlin("jvm") version kotlinVersion
|
||||
kotlin("plugin.serialization") version kotlinVersion
|
||||
|
||||
id("net.mamoe.mirai-console") version "2.7.0"
|
||||
id("net.mamoe.mirai-console") version "2.13.2"
|
||||
}
|
||||
|
||||
group = "me.jie65535"
|
||||
version = "0.1.0"
|
||||
group = "top.jie65535.jcf"
|
||||
version = "1.1.0"
|
||||
|
||||
repositories {
|
||||
maven("https://maven.aliyun.com/repository/public")
|
||||
mavenCentral()
|
||||
}
|
||||
val ktorVersion = "2.2.2"
|
||||
|
||||
dependencies {
|
||||
// implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-core-jvm:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
|
||||
}
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -1,229 +0,0 @@
|
||||
package me.jie65535.jcf
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.features.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import me.jie65535.jcf.model.addon.*
|
||||
import me.jie65535.jcf.model.addon.file.*
|
||||
import me.jie65535.jcf.model.addon.request.*
|
||||
import me.jie65535.jcf.model.category.*
|
||||
import me.jie65535.jcf.model.fingerprint.*
|
||||
import me.jie65535.jcf.model.fingerprint.request.*
|
||||
import me.jie65535.jcf.model.game.*
|
||||
import me.jie65535.jcf.model.minecraft.*
|
||||
import me.jie65535.jcf.model.minecraft.modloader.*
|
||||
import me.jie65535.jcf.util.Date
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
object CurseClient {
|
||||
private val json = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
serializersModule
|
||||
}
|
||||
private val http = HttpClient(OkHttp) {
|
||||
install(HttpTimeout) {
|
||||
this.requestTimeoutMillis = 30_0000
|
||||
this.connectTimeoutMillis = 30_0000
|
||||
this.socketTimeoutMillis = 30_0000
|
||||
}
|
||||
defaultRequest {
|
||||
url.protocol = URLProtocol.HTTPS
|
||||
url.host = "addons-ecs.forgesvc.net"
|
||||
header("accept", "*/*")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGames(supportsAddons: Boolean = false): List<Game> {
|
||||
return json.decodeFromString(
|
||||
http.get("api/v2/game") {
|
||||
parameter("supportsAddons", supportsAddons.toString())
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getGame(gameId: Int): Game {
|
||||
return json.decodeFromString(http.get("api/v2/game/$gameId"))
|
||||
}
|
||||
|
||||
suspend fun getGameDatabaseTimestamp(): Date {
|
||||
return http.get("api/v2/game/timestamp")
|
||||
}
|
||||
|
||||
suspend fun getAddon(projectId: Int): Addon {
|
||||
return json.decodeFromString(http.get("api/v2/addon/$projectId"))
|
||||
}
|
||||
|
||||
suspend fun getAddons(projectIds: Collection<Int>): List<Addon> {
|
||||
return json.decodeFromString(
|
||||
http.post("api/v2/addon") {
|
||||
contentType(ContentType.Application.Json)
|
||||
body = json.encodeToString(projectIds)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getAddons(vararg projectIds: Int): List<Addon> = getAddons(projectIds.toList())
|
||||
|
||||
suspend fun getAddonDescription(projectId: Int): String {
|
||||
return http.get("api/v2/addon/$projectId/description")
|
||||
}
|
||||
|
||||
suspend fun getAddonFiles(projectId: Int): List<AddonFile> {
|
||||
return json.decodeFromString(http.get("api/v2/addon/$projectId/files"))
|
||||
}
|
||||
|
||||
suspend fun getAddonFiles(keys: Collection<Int>): Map<Int, List<AddonFile>> {
|
||||
return json.decodeFromString(
|
||||
http.post("api/v2/addon/files") {
|
||||
contentType(ContentType.Application.Json)
|
||||
body = json.encodeToString(keys)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getAddonFiles(vararg keys: Int): Map<Int, List<AddonFile>> = getAddonFiles(keys.toList())
|
||||
|
||||
suspend fun getAddonFileDownloadUrl(projectId: Int, fileId: Int): String {
|
||||
return http.get("api/v2/addon/$projectId/file/$fileId/download-url")
|
||||
}
|
||||
|
||||
suspend fun getAddonFile(projectId: Int, fileId: Int): AddonFile {
|
||||
return json.decodeFromString(http.get("api/v2/addon/$projectId/file/$fileId"))
|
||||
}
|
||||
|
||||
suspend fun searchAddons(
|
||||
gameId: Int,
|
||||
sectionId: Int = -1,
|
||||
categoryId: Int = -1,
|
||||
sort: AddonSortMethod = AddonSortMethod.FEATURED,
|
||||
sortDescending: Boolean = true,
|
||||
gameVersion: String? = null,
|
||||
index: Int = 0,
|
||||
pageSize: Int = 10,
|
||||
searchFilter: String? = null
|
||||
): List<Addon> {
|
||||
return json.decodeFromString(
|
||||
http.get("api/v2/addon/search") {
|
||||
parameter("gameId", gameId)
|
||||
parameter("sectionId", sectionId)
|
||||
parameter("categoryId", categoryId)
|
||||
parameter("gameVersion", gameVersion)
|
||||
parameter("index", index)
|
||||
parameter("pageSize", pageSize)
|
||||
parameter("searchFilter", searchFilter)
|
||||
parameter("sort", sort)
|
||||
parameter("sortDescending", sortDescending)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getFeaturedAddons(
|
||||
gameId: Int,
|
||||
featuredCount: Int = 6,
|
||||
popularCount: Int = 14,
|
||||
updatedCount: Int = 14,
|
||||
excludedAddons: Collection<Int> = listOf()
|
||||
): Map<FeaturedAddonType, List<Addon>> {
|
||||
return json.decodeFromString(
|
||||
http.post("api/v2/addon/featured") {
|
||||
contentType(ContentType.Application.Json)
|
||||
body = json.encodeToString(FeaturedAddonsRequest(gameId, featuredCount, popularCount, updatedCount, excludedAddons))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getFeaturedAddons(
|
||||
gameId: Int,
|
||||
featuredCount: Int = 6,
|
||||
popularCount: Int = 14,
|
||||
updatedCount: Int = 14,
|
||||
vararg excludedAddons: Int
|
||||
): Map<FeaturedAddonType, List<Addon>> = getFeaturedAddons(gameId, featuredCount, popularCount, updatedCount, excludedAddons.toList())
|
||||
|
||||
suspend fun getCategory(categoryId: Int): Category {
|
||||
return json.decodeFromString(http.get("api/v2/category/$categoryId"))
|
||||
}
|
||||
|
||||
suspend fun getCategory(slug: String): List<Category> {
|
||||
return json.decodeFromString(
|
||||
http.get("api/v2/category") {
|
||||
parameter("slug", slug)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getCategorySection(sectionId: Int): List<Category> {
|
||||
return json.decodeFromString(http.get("api/v2/category/section/$sectionId"))
|
||||
}
|
||||
|
||||
suspend fun getCategories(): List<Category> {
|
||||
return json.decodeFromString(http.get("api/v2/category"))
|
||||
}
|
||||
|
||||
suspend fun getCategoryDatabaseTimestamp(): Date {
|
||||
return http.get("api/v2/category/timestamp")
|
||||
}
|
||||
|
||||
suspend fun getFingerprintMatches(fingerprints: Collection<Long>): FingerprintMatchResult {
|
||||
return json.decodeFromString(
|
||||
http.post("api/v2/fingerprint") {
|
||||
contentType(ContentType.Application.Json)
|
||||
body = json.encodeToString(fingerprints)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getFingerprintMatches(vararg fingerprints: Long): FingerprintMatchResult = getFingerprintMatches(fingerprints.toList())
|
||||
|
||||
suspend fun getFuzzyFingerprintMatches(gameId: Int, fingerprints: List<FolderFingerprint>): List<FuzzyFingerprintMatch> {
|
||||
return json.decodeFromString(
|
||||
http.post("api/v2/fingerprint/fuzzy") {
|
||||
contentType(ContentType.Application.Json)
|
||||
body = json.encodeToString(FuzzyMatchesRequest(gameId, fingerprints))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getModloader(key: String): ModloaderVersion {
|
||||
return json.decodeFromString(http.get("api/v2/minecraft/modloader/$key"))
|
||||
}
|
||||
|
||||
suspend fun getModloaders(): List<ModloaderIndex> {
|
||||
return json.decodeFromString(http.get("api/v2/minecraft/modloader"))
|
||||
}
|
||||
|
||||
suspend fun getModloaders(gameVersion: String): List<ModloaderIndex> {
|
||||
return json.decodeFromString(
|
||||
http.get("api/v2/minecraft/modloader") {
|
||||
parameter("version", gameVersion)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getModloadersDatabaseTimestamp(): Date {
|
||||
return http.get("api/v2/minecraft/modloader/timestamp")
|
||||
}
|
||||
|
||||
suspend fun getMinecraftVersions(): List<MinecraftVersion> {
|
||||
return json.decodeFromString(http.get("api/v2/minecraft/version"))
|
||||
}
|
||||
|
||||
suspend fun getMinecraftVersion(gameVersion: String): MinecraftVersion {
|
||||
return json.decodeFromString(http.get("api/v2/minecraft/version/$gameVersion"))
|
||||
}
|
||||
|
||||
suspend fun getMinecraftVersionsDatabaseTimestamp(): Date {
|
||||
return http.get("api/v2/minecraft/version/timestamp")
|
||||
}
|
||||
|
||||
suspend fun getAddonFileChangeLog(addonId: Int, fileId: Int): String {
|
||||
return http.get("api/v2/addon/${addonId}/file/${fileId}/changelog")
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package me.jie65535.jcf
|
||||
|
||||
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
|
||||
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.unregister
|
||||
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
|
||||
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
|
||||
import net.mamoe.mirai.utils.info
|
||||
|
||||
object JCurseforge : KotlinPlugin(
|
||||
JvmPluginDescription(
|
||||
id = "me.jie65535.jcf",
|
||||
name = "J Curseforge Util",
|
||||
version = "0.1.0",
|
||||
) {
|
||||
author("jie65535")
|
||||
info("""
|
||||
MC Curseforge Util
|
||||
https://github.com/jie65535/mirai-console-jcf-plugin
|
||||
""".trimIndent())
|
||||
}
|
||||
) {
|
||||
override fun onEnable() {
|
||||
logger.info { "Plugin loaded" }
|
||||
JcfCommand.register()
|
||||
}
|
||||
|
||||
override fun onDisable() {
|
||||
JcfCommand.unregister()
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
package me.jie65535.jcf
|
||||
|
||||
import net.mamoe.mirai.console.command.CommandSender
|
||||
import net.mamoe.mirai.console.command.CompositeCommand
|
||||
import net.mamoe.mirai.console.command.UserCommandSender
|
||||
import net.mamoe.mirai.console.plugin.author
|
||||
import net.mamoe.mirai.console.plugin.info
|
||||
import net.mamoe.mirai.console.plugin.name
|
||||
import net.mamoe.mirai.console.plugin.version
|
||||
|
||||
object JcfCommand : CompositeCommand(
|
||||
JCurseforge, "jcf",
|
||||
description = "Curseforge Util"
|
||||
) {
|
||||
@SubCommand
|
||||
@Description("帮助")
|
||||
suspend fun CommandSender.help() {
|
||||
sendMessage("${JCurseforge.name} by ${JCurseforge.author} ${JCurseforge.version}\n" +
|
||||
"${JCurseforge.info}\n" + usage)
|
||||
}
|
||||
|
||||
@SubCommand("ss")
|
||||
@Description("直接搜索")
|
||||
suspend fun UserCommandSender.search(filter: String) {
|
||||
MinecraftService.search(this, MinecraftService.ALL, filter)
|
||||
}
|
||||
|
||||
@SubCommand("ssmod")
|
||||
@Description("搜索模组")
|
||||
suspend fun UserCommandSender.searchMods(filter: String) {
|
||||
MinecraftService.search(this, MinecraftService.SECTION_ID_MODES, filter)
|
||||
}
|
||||
|
||||
@SubCommand("sspack")
|
||||
@Description("搜索整合包")
|
||||
suspend fun UserCommandSender.searchModPacks(filter: String) {
|
||||
MinecraftService.search(this, MinecraftService.SECTION_ID_MODE_PACKS, filter)
|
||||
}
|
||||
|
||||
@SubCommand("ssres")
|
||||
@Description("搜索资源包")
|
||||
suspend fun UserCommandSender.searchResourcePacks(filter: String) {
|
||||
MinecraftService.search(this, MinecraftService.SECTION_ID_RESOURCE_PACKS, filter)
|
||||
}
|
||||
|
||||
// 可能用不上,先注释掉,有需要再说
|
||||
// @SubCommand("ssworld")
|
||||
// @Description("搜索存档")
|
||||
// suspend fun UserCommandSender.searchWorlds(filter: String) {
|
||||
// MinecraftService.search(this, MinecraftService.SECTION_ID_WORLDS, filter)
|
||||
// }
|
||||
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
package me.jie65535.jcf
|
||||
|
||||
import me.jie65535.jcf.model.addon.Addon
|
||||
import me.jie65535.jcf.model.addon.file.AddonFile
|
||||
import me.jie65535.jcf.util.HttpUtil
|
||||
import net.mamoe.mirai.contact.Contact
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.min
|
||||
|
||||
object MessageHandler {
|
||||
|
||||
fun parseSearchResult(addons: List<Addon>, builder: ForwardMessageBuilder, contact: Contact) {
|
||||
for ((index, addon) in addons.withIndex()) {
|
||||
builder.add(contact.bot.id, index.toString(),
|
||||
PlainText("""
|
||||
$index | [${addon.name}] by ${addon.authors[0].name}
|
||||
${formatCount(addon.downloadCount)} Downloads Updated ${addon.dateModified.toLocalDate()}
|
||||
${addon.summary}
|
||||
${addon.websiteUrl}
|
||||
""".trimIndent()))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun parseAddon(addon: Addon, contact: Contact): ForwardMessageBuilder {
|
||||
val builder = ForwardMessageBuilder(contact)
|
||||
.add(contact.bot, loadImage(addon.attachments.find{ it.isDefault }!!.thumbnailUrl, contact))
|
||||
.add(contact.bot, PlainText("""
|
||||
${addon.name}
|
||||
作者:${addon.authors[0].name}
|
||||
概括:${addon.summary}
|
||||
项目ID:${addon.id}
|
||||
创建时间:${addon.dateCreated.toLocalDate()}
|
||||
最近更新:${addon.dateModified.toLocalDate()}
|
||||
总下载:${addon.downloadCount.toLong()}
|
||||
主页:${addon.websiteUrl}
|
||||
""".trimIndent()))
|
||||
if (addon.latestFiles.isNotEmpty()) {
|
||||
builder.add(contact.bot, PlainText("最新文件列表:"))
|
||||
parseAddonFiles(addon.latestFiles, builder, contact)
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
fun parseAddonFiles(addonFiles: Collection<AddonFile>, builder: ForwardMessageBuilder, contact: Contact) {
|
||||
for ((index, file) in addonFiles.withIndex()) {
|
||||
builder.add(contact.bot.id, index.toString(), PlainText("""
|
||||
$index | ${file.displayName} [${file.releaseType}] [${file.fileDate.toLocalDate()}]
|
||||
${file.downloadUrl}
|
||||
""".trimIndent()))
|
||||
}
|
||||
}
|
||||
|
||||
private const val ONE_GRP_SIZE = 5000
|
||||
private const val ONE_MSG_SIZE = 500
|
||||
suspend fun sendLargeMessage(contact: Contact, message: String) {
|
||||
for (g in message.indices step ONE_GRP_SIZE) {
|
||||
val builder = ForwardMessageBuilder(contact)
|
||||
for (i in g until g + min(ONE_GRP_SIZE, message.length-g) step ONE_MSG_SIZE) {
|
||||
builder.add(contact.bot, PlainText(message.subSequence(i, i+(min(ONE_MSG_SIZE, message.length-i)))))
|
||||
}
|
||||
contact.sendMessage(builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
private val singleDecimalFormat = DecimalFormat("0.#")
|
||||
private fun formatCount(count: Double): String = when {
|
||||
count < 1000000 -> singleDecimalFormat.format(count / 1000) + "K"
|
||||
count < 1000000000 -> singleDecimalFormat.format(count / 1000000) + "M"
|
||||
else -> count.toString()
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
private suspend fun loadImage(url: String, contact: Contact): Image {
|
||||
val imgFileName = url.substringAfterLast("/")
|
||||
val file = JCurseforge.resolveDataFile("cache/$imgFileName")
|
||||
val res = if (file.exists()) {
|
||||
file.readBytes().toExternalResource()
|
||||
} else {
|
||||
HttpUtil.downloadImage(url, file).toExternalResource()
|
||||
}
|
||||
val image = contact.uploadImage(res)
|
||||
res.close()
|
||||
return image
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
package me.jie65535.jcf
|
||||
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import me.jie65535.jcf.model.addon.Addon
|
||||
import me.jie65535.jcf.model.addon.AddonSortMethod
|
||||
import me.jie65535.jcf.model.addon.file.AddonFile
|
||||
import net.mamoe.mirai.console.command.UserCommandSender
|
||||
import net.mamoe.mirai.event.events.MessageEvent
|
||||
import net.mamoe.mirai.event.nextEventAsync
|
||||
import net.mamoe.mirai.message.data.ForwardMessageBuilder
|
||||
import net.mamoe.mirai.message.data.PlainText
|
||||
import net.mamoe.mirai.message.data.content
|
||||
import net.mamoe.mirai.utils.MiraiExperimentalApi
|
||||
import java.lang.Exception
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@OptIn(MiraiExperimentalApi::class)
|
||||
object MinecraftService {
|
||||
private val curseClient = CurseClient
|
||||
|
||||
private const val GAME_ID_MINECRAFT = 432
|
||||
const val SECTION_ID_MODES = 6
|
||||
const val SECTION_ID_RESOURCE_PACKS = 12
|
||||
const val SECTION_ID_WORLDS = 17
|
||||
const val SECTION_ID_MODE_PACKS = 4471
|
||||
const val ALL = -1
|
||||
|
||||
private const val WAIT_REPLY_TIMEOUT_MS = 60000L
|
||||
private const val PAGE_SIZE = 10
|
||||
private const val NEXT_PAGE_KEYWORD = "n"
|
||||
private const val SHOW_FILES_KEYWORD = "files"
|
||||
|
||||
suspend fun search(sender: UserCommandSender, sectionId: Int, filter: String) {
|
||||
val addon: Addon
|
||||
var pageIndex = 0
|
||||
while (true) {
|
||||
val searchResult = doSearch(sectionId, filter, pageIndex++, PAGE_SIZE)
|
||||
val hasNextPage = searchResult.size == PAGE_SIZE
|
||||
|
||||
if (searchResult.isEmpty()) {
|
||||
sender.sendMessage("未搜索到结果,请更换关键字重试。")
|
||||
return
|
||||
} else if (searchResult.size == 1) {
|
||||
addon = searchResult[0]
|
||||
break
|
||||
}
|
||||
|
||||
val builder = ForwardMessageBuilder(sender.subject)
|
||||
builder.add(sender.bot, PlainText("${WAIT_REPLY_TIMEOUT_MS/1000}秒内回复编号查看"))
|
||||
MessageHandler.parseSearchResult(searchResult, builder, sender.subject)
|
||||
if (hasNextPage) builder.add(sender.bot, PlainText("回复[$NEXT_PAGE_KEYWORD]下一页"))
|
||||
sender.sendMessage(builder.build())
|
||||
|
||||
try {
|
||||
val nextEvent = sender.nextEventAsync<MessageEvent>(
|
||||
WAIT_REPLY_TIMEOUT_MS,
|
||||
coroutineContext = sender.coroutineContext
|
||||
) { it.sender == sender.user } .await()
|
||||
if (hasNextPage && nextEvent.message.contentEquals(NEXT_PAGE_KEYWORD, true))
|
||||
continue
|
||||
addon = searchResult[nextEvent.message.content.toInt()]
|
||||
break
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
sender.sendMessage("等待回复超时,请重新查询。")
|
||||
} catch (e: NumberFormatException) {
|
||||
sender.sendMessage("请正确回复序号,此次查询已取消,请重新查询。")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
sender.sendMessage("请回复正确的序号,此次查询已取消,请重新查询。")
|
||||
} catch (e: Exception) {
|
||||
sender.sendMessage("内部发生异常,此次查询已取消,请重新查询。")
|
||||
throw e
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// addon handle
|
||||
showAddon(sender, addon)
|
||||
}
|
||||
|
||||
private suspend fun showAddon(sender: UserCommandSender, addon: Addon) {
|
||||
val builder = MessageHandler.parseAddon(addon, sender.subject)
|
||||
if (addon.latestFiles.isNotEmpty())
|
||||
builder.add(sender.bot, PlainText("${WAIT_REPLY_TIMEOUT_MS/1000}秒内回复[${SHOW_FILES_KEYWORD}]查看所有文件,回复文件序号查看更新日志"))
|
||||
sender.sendMessage(builder.build())
|
||||
|
||||
try {
|
||||
val nextEvent = sender.nextEventAsync<MessageEvent>(
|
||||
WAIT_REPLY_TIMEOUT_MS,
|
||||
coroutineContext = sender.coroutineContext
|
||||
) { it.sender == sender.user } .await()
|
||||
if (nextEvent.message.contentEquals(SHOW_FILES_KEYWORD, true)) {
|
||||
showAllFiles(sender, addon)
|
||||
} else {
|
||||
val file = addon.latestFiles[nextEvent.message.content.toInt()]
|
||||
showChangedLog(sender, addon.id, file)
|
||||
}
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
sender.sendMessage("等待回复超时,请重新查询。")
|
||||
} catch (e: NumberFormatException) {
|
||||
sender.sendMessage("请正确回复序号,此次查询已取消,请重新查询。")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
sender.sendMessage("请回复正确的序号,此次查询已取消,请重新查询。")
|
||||
} catch (e: Exception) {
|
||||
sender.sendMessage("内部发生异常,此次查询已取消,请重新查询。")
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showAllFiles(sender: UserCommandSender, addon: Addon) {
|
||||
val files = CurseClient.getAddonFiles(addon.id).sortedByDescending { f -> f.fileDate }
|
||||
if (files.isEmpty()) {
|
||||
sender.sendMessage("没有任何文件 :(")
|
||||
return
|
||||
}
|
||||
val builder = ForwardMessageBuilder(sender.subject)
|
||||
builder.add(sender.bot, PlainText("${WAIT_REPLY_TIMEOUT_MS/1000}秒内回复文件序号查看更新日志"))
|
||||
MessageHandler.parseAddonFiles(files, builder, sender.subject)
|
||||
sender.sendMessage(builder.build())
|
||||
|
||||
try { // cv大法好,回复功能有功夫再封装,先复制粘贴用着 XD
|
||||
val nextEvent = sender.nextEventAsync<MessageEvent>(
|
||||
WAIT_REPLY_TIMEOUT_MS,
|
||||
coroutineContext = sender.coroutineContext
|
||||
) { it.sender == sender.user } .await()
|
||||
val file = files[nextEvent.message.content.toInt()]
|
||||
showChangedLog(sender, addon.id, file)
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
sender.sendMessage("等待回复超时,请重新查询。")
|
||||
} catch (e: NumberFormatException) {
|
||||
sender.sendMessage("请正确回复序号,此次查询已取消,请重新查询。")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
sender.sendMessage("请回复正确的序号,此次查询已取消,请重新查询。")
|
||||
}
|
||||
}
|
||||
|
||||
private val HTMLPattern = Pattern.compile("<[^>]+>", Pattern.CASE_INSENSITIVE)
|
||||
private suspend fun showChangedLog(sender: UserCommandSender, addonId: Int, addonFile: AddonFile) {
|
||||
val changeLogHTML = curseClient.getAddonFileChangeLog(addonId, addonFile.id)
|
||||
val changeLog = HTMLPattern.matcher(changeLogHTML).replaceAll("")
|
||||
MessageHandler.sendLargeMessage(sender.subject, changeLog)
|
||||
}
|
||||
|
||||
private suspend fun doSearch(sectionId: Int, filter: String, pageIndex: Int, pageSize: Int): List<Addon> {
|
||||
return curseClient.searchAddons(
|
||||
gameId = GAME_ID_MINECRAFT,
|
||||
sectionId = sectionId,
|
||||
categoryId = -1,
|
||||
sort = AddonSortMethod.POPULARITY,
|
||||
sortDescending = true,
|
||||
gameVersion = null,
|
||||
index = pageIndex,
|
||||
pageSize = pageSize,
|
||||
searchFilter = filter,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.addon.file.AddonFile
|
||||
import me.jie65535.jcf.model.addon.file.AddonFileLatestForGameVersion
|
||||
import me.jie65535.jcf.model.category.CategorySection
|
||||
import me.jie65535.jcf.util.Date
|
||||
import me.jie65535.jcf.util.internal.DateSerializer
|
||||
|
||||
@Serializable
|
||||
data class Addon(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val authors: List<AddonAuthor>,
|
||||
val attachments: List<AddonAttachment>,
|
||||
val websiteUrl: String,
|
||||
val gameId: Int,
|
||||
val summary: String,
|
||||
val defaultFileId: Int,
|
||||
val downloadCount: Double,
|
||||
val latestFiles: List<AddonFile>,
|
||||
val categories: List<AddonCategory>,
|
||||
val status: AddonStatus,
|
||||
val primaryCategoryId: Int,
|
||||
val categorySection: CategorySection,
|
||||
val slug: String,
|
||||
val gameVersionLatestFiles: List<AddonFileLatestForGameVersion>,
|
||||
val isFeatured: Boolean,
|
||||
val popularityScore: Double,
|
||||
val gamePopularityRank: Int,
|
||||
val primaryLanguage: String,
|
||||
val gameSlug: String,
|
||||
val gameName: String,
|
||||
val portalName: String,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val dateModified: Date,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val dateCreated: Date,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val dateReleased: Date,
|
||||
val isAvailable: Boolean,
|
||||
@SerialName("isExperiemental")
|
||||
val isExperimental: Boolean
|
||||
)
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AddonAttachment(
|
||||
val id: Int,
|
||||
val projectId: Int,
|
||||
val description: String,
|
||||
val isDefault: Boolean,
|
||||
val thumbnailUrl: String,
|
||||
val title: String,
|
||||
val url: String,
|
||||
val status: AddonAttachmentStatus
|
||||
)
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
|
||||
@Serializable(with = AddonAttachmentStatus.Ser::class)
|
||||
enum class AddonAttachmentStatus {
|
||||
NORMAL,
|
||||
DELETED,
|
||||
UPLOADING,
|
||||
BANNED,
|
||||
PENDING_MODERATION;
|
||||
|
||||
internal object Ser : EnumIntSerializer<AddonAttachmentStatus>("$MODEL_PACKAGE.addon.AttachmentStatus", values())
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AddonAuthor(
|
||||
val name: String,
|
||||
val url: String,
|
||||
val projectId: Int,
|
||||
val id: Int,
|
||||
val projectTitleId: Int?,
|
||||
val projectTitleTitle: String?,
|
||||
val userId: Int,
|
||||
val twitchId: Int?
|
||||
)
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AddonCategory(
|
||||
val categoryId: Int,
|
||||
val name: String,
|
||||
val url: String,
|
||||
val avatarUrl: String?,
|
||||
val parentId: Int?,
|
||||
val rootId: Int?,
|
||||
val projectId: Int,
|
||||
val avatarId: Int?,
|
||||
val gameId: Int
|
||||
)
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = AddonPackageType.Ser::class)
|
||||
enum class AddonPackageType {
|
||||
FOLDER,
|
||||
CTOC,
|
||||
SINGLE_FILE,
|
||||
CMOD2,
|
||||
MOD_PACK,
|
||||
MOD;
|
||||
|
||||
internal object Ser : EnumIntSerializer<AddonPackageType>("$MODEL_PACKAGE.addon.AddonPackageType", values())
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.addon.file.AddonFile
|
||||
|
||||
@Serializable
|
||||
data class AddonRepositoryMatch(
|
||||
val id: Int,
|
||||
val latestFiles: List<AddonFile>
|
||||
)
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = AddonRestrictFileAccess.Ser::class)
|
||||
enum class AddonRestrictFileAccess {
|
||||
NONE,
|
||||
ALPHA,
|
||||
ALPHA_AND_BETA;
|
||||
|
||||
internal object Ser : EnumIntSerializer<AddonRestrictFileAccess>("$MODEL_PACKAGE.addon.AddonRestrictFileAccess", values())
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
/**
|
||||
* 附加排序方法
|
||||
*/
|
||||
enum class AddonSortMethod {
|
||||
/**
|
||||
* 精选
|
||||
*/
|
||||
FEATURED,
|
||||
|
||||
/**
|
||||
* 人气
|
||||
*/
|
||||
POPULARITY,
|
||||
|
||||
/**
|
||||
* 最后更新时间
|
||||
*/
|
||||
LAST_UPDATED,
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
NAME,
|
||||
|
||||
/**
|
||||
* 作者
|
||||
*/
|
||||
AUTHOR,
|
||||
|
||||
/**
|
||||
* 总下载数
|
||||
*/
|
||||
TOTAL_DOWNLOADS,
|
||||
|
||||
/**
|
||||
* 类别
|
||||
*/
|
||||
CATEGORY,
|
||||
|
||||
/**
|
||||
* 游戏版本
|
||||
*/
|
||||
GAME_VERSION
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = AddonStatus.Ser::class)
|
||||
enum class AddonStatus {
|
||||
NEW,
|
||||
CHANGES_REQUIRED,
|
||||
UNDER_SOFT_REVIEW,
|
||||
APPROVED,
|
||||
REJECTED,
|
||||
CHANGES_MADE,
|
||||
INACTIVE,
|
||||
ABANDONED,
|
||||
DELETED,
|
||||
UNDER_REVIEW;
|
||||
|
||||
internal object Ser : EnumIntSerializer<AddonStatus>("$MODEL_PACKAGE.addon.AddonStatus", values())
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class FeaturedAddonType {
|
||||
@SerialName("Featured")
|
||||
FEATURED,
|
||||
@SerialName("Popular")
|
||||
POPULAR,
|
||||
@SerialName("RecentlyUpdated")
|
||||
RECENTLY_UPDATED;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.Date
|
||||
import me.jie65535.jcf.util.internal.DateSerializer
|
||||
|
||||
@Serializable
|
||||
data class SortableGameVersion(
|
||||
val gameVersionPadded: String,
|
||||
val gameVersion: String,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val gameVersionReleaseDate: Date,
|
||||
val gameVersionName: String
|
||||
)
|
@ -1,60 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon.file
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.addon.AddonPackageType
|
||||
import me.jie65535.jcf.model.addon.AddonRestrictFileAccess
|
||||
import me.jie65535.jcf.model.addon.AddonStatus
|
||||
import me.jie65535.jcf.model.addon.SortableGameVersion
|
||||
import me.jie65535.jcf.util.Date
|
||||
import me.jie65535.jcf.util.internal.DateSerializer
|
||||
|
||||
@Serializable
|
||||
data class AddonFile(
|
||||
val id: Int,
|
||||
val displayName: String,
|
||||
val fileName: String,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val fileDate: Date,
|
||||
val fileLength: Long,
|
||||
val releaseType: AddonFileReleaseType,
|
||||
val fileStatus: AddonFileStatus,
|
||||
val downloadUrl: String,
|
||||
val isAlternate: Boolean,
|
||||
val alternateFileId: Int,
|
||||
val dependencies: List<AddonFileDependency>,
|
||||
val isAvailable: Boolean,
|
||||
val modules: List<AddonFileModule>,
|
||||
val packageFingerprint: Long,
|
||||
val gameVersion: List<String>,
|
||||
val sortableGameVersion: List<SortableGameVersion>? = null,
|
||||
val installMetadata: String?,
|
||||
val changelog: String? = null,
|
||||
val hasInstallScript: Boolean,
|
||||
val isCompatibleWithClient: Boolean? = null,
|
||||
val categorySectionPackageType: AddonPackageType? = null,
|
||||
val restrictProjectFileAccess: AddonRestrictFileAccess? = null,
|
||||
val projectStatus: AddonStatus? = null,
|
||||
val renderCacheId: Int? = null,
|
||||
val fileLegacyMappingId: Int? = null,
|
||||
val projectId: Int? = null,
|
||||
val parentProjectFileId: Int? = null,
|
||||
val parentFileLegacyMappingId: Int? = null,
|
||||
val fileTypeId: Int? = null,
|
||||
val exposeAsAlternative: Boolean? = null,
|
||||
val packageFingerprintId: Int? = null,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val gameVersionDateReleased: Date?,
|
||||
val gameVersionMappingId: Int? = null,
|
||||
val gameVersionId: Int? = null,
|
||||
val gameId: Int? = null,
|
||||
val isServerPack: Boolean = false,
|
||||
val serverPackFileId: Int?,
|
||||
val gameVersionFlavor: String?
|
||||
)
|
@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon.file
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AddonFileDependency(
|
||||
val id: Int? = null,
|
||||
val addonId: Int,
|
||||
val type: AddonFileRelationType,
|
||||
val fileId: Int? = null
|
||||
)
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon.file
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = AddonFileFingerprintType.Ser::class)
|
||||
enum class AddonFileFingerprintType {
|
||||
PACKAGE,
|
||||
MODULE,
|
||||
MAIN_MODULE,
|
||||
FILE,
|
||||
REFERRENCED_FILE;
|
||||
|
||||
internal object Ser : EnumIntSerializer<AddonFileFingerprintType>("$MODEL_PACKAGE.addon.file.AddonFileFingerprintType", values())
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon.file
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AddonFileLatestForGameVersion(
|
||||
val gameVersion: String,
|
||||
val projectFileId: Int,
|
||||
val projectFileName: String,
|
||||
val fileType: AddonFileReleaseType,
|
||||
val gameVersionFlavor: String?
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon.file
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AddonFileModule(
|
||||
@SerialName("foldername")
|
||||
val folderName: String,
|
||||
val fingerprint: Long,
|
||||
val type: AddonFileFingerprintType? = null
|
||||
)
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon.file
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = AddonFileRelationType.Ser::class)
|
||||
enum class AddonFileRelationType {
|
||||
EMBEDDED_LIBRARY,
|
||||
OPTIONAL_DEPENDENCY,
|
||||
REQUIRED_DEPENDENCY,
|
||||
TOOL,
|
||||
INCOMPATIBLE,
|
||||
INCLUDE;
|
||||
|
||||
internal object Ser : EnumIntSerializer<AddonFileRelationType>("$MODEL_PACKAGE.addon.file.AddonFileRelationType", values())
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon.file
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = AddonFileReleaseType.Ser::class)
|
||||
enum class AddonFileReleaseType {
|
||||
RELEASE,
|
||||
BETA,
|
||||
ALPHA;
|
||||
|
||||
internal object Ser : EnumIntSerializer<AddonFileReleaseType>("$MODEL_PACKAGE.addon.file.AddonFileReleaseType", values())
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.addon.file
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = AddonFileStatus.Ser::class)
|
||||
enum class AddonFileStatus {
|
||||
PROCESSING,
|
||||
CHANGES_REQUIRED,
|
||||
UNDER_REVIEW,
|
||||
APPROVED,
|
||||
REJECTED,
|
||||
MALWARE_DETECTED,
|
||||
DELETED,
|
||||
ARCHIVED,
|
||||
TESTING,
|
||||
RELEASED,
|
||||
READY_FOR_REVIEW,
|
||||
DEPRECATED,
|
||||
BAKING,
|
||||
AWAITING_FOR_PUBLISHING,
|
||||
FAILED_PUBLISHING;
|
||||
|
||||
internal object Ser : EnumIntSerializer<AddonFileStatus>("$MODEL_PACKAGE.addon.file.AddonFileStatus", values())
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
package me.jie65535.jcf.model.addon.request
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FeaturedAddonsRequest(
|
||||
val gameId: Int,
|
||||
val featuredCount: Int,
|
||||
val popularCount: Int,
|
||||
val updatedCount: Int,
|
||||
val excludedAddons: Collection<Int>
|
||||
)
|
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.category
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.Date
|
||||
import me.jie65535.jcf.util.internal.DateSerializer
|
||||
|
||||
@Serializable
|
||||
data class Category(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val slug: String,
|
||||
val avatarUrl: String?,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val dateModified: Date,
|
||||
val parentGameCategoryId: Int?,
|
||||
val rootGameCategoryId: Int?,
|
||||
val gameId: Int
|
||||
)
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.category
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.addon.AddonPackageType
|
||||
|
||||
@Serializable
|
||||
data class CategorySection(
|
||||
val extraIncludePattern: String?,
|
||||
val gameCategoryId: Int,
|
||||
val gameId: Int,
|
||||
val id: Int,
|
||||
val initialInclusionPattern: String,
|
||||
val name: String,
|
||||
val packageType: AddonPackageType,
|
||||
val path: String
|
||||
)
|
@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.fingerprint
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.addon.file.AddonFile
|
||||
|
||||
@Serializable
|
||||
data class FingerprintMatch(
|
||||
val id: Int,
|
||||
val file: AddonFile,
|
||||
val latestFiles: List<AddonFile>
|
||||
)
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.fingerprint
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FingerprintMatchResult(
|
||||
val isCacheBuilt: Boolean,
|
||||
val exactMatches: List<FingerprintMatch>,
|
||||
val exactFingerprints: List<Long>,
|
||||
val partialMatches: List<FingerprintMatch>,
|
||||
val partialMatchFingerprints: Map<String, List<Long>>,
|
||||
val installedFingerprints: List<Long>,
|
||||
val unmatchedFingerprints: List<Long>
|
||||
)
|
@ -1,18 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.fingerprint
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FolderFingerprint(
|
||||
@SerialName("foldername")
|
||||
val folderName: String,
|
||||
val fingerprints: List<Long>
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.fingerprint
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.addon.file.AddonFile
|
||||
|
||||
@Serializable
|
||||
data class FuzzyFingerprintMatch(
|
||||
val id: Int,
|
||||
val file: AddonFile,
|
||||
val latestFiles: List<AddonFile>,
|
||||
val fingerprints: List<Long>
|
||||
)
|
@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.fingerprint.request
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.fingerprint.FolderFingerprint
|
||||
|
||||
@Serializable
|
||||
data class FuzzyMatchesRequest(
|
||||
val gameId: Int,
|
||||
val fingerprints: List<FolderFingerprint>
|
||||
)
|
@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.category.CategorySection
|
||||
import me.jie65535.jcf.util.Date
|
||||
import me.jie65535.jcf.util.internal.DateSerializer
|
||||
|
||||
@Serializable
|
||||
data class Game(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val slug: String,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val dateModified: Date,
|
||||
val gameFiles: List<GameFile>,
|
||||
val gameDetectionHints: List<GameDetectionHint>,
|
||||
val fileParsingRules: List<GameFileParsingRule>,
|
||||
val categorySections: List<CategorySection>,
|
||||
val maxFreeStorage: Long,
|
||||
val maxPremiumStorage: Long,
|
||||
val maxFileSize: Long,
|
||||
val addonSettingsFolderFilter: String?,
|
||||
val addonSettingsStartingFolder: String?,
|
||||
val addonSettingsFileFilter: String?,
|
||||
val addonSettingsFileRemovalFilter: String?,
|
||||
val supportsAddons: Boolean,
|
||||
val supportsPartnerAddons: Boolean,
|
||||
@Serializable(with = SupportedClientConfiguration.Ser::class)
|
||||
val supportedClientConfiguration: Set<SupportedClientConfiguration>,
|
||||
val supportsNotifications: Boolean,
|
||||
val profilerAddonId: Int,
|
||||
val twitchGameId: Int,
|
||||
val clientGameSettingsId: Int
|
||||
)
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GameDetectionHint(
|
||||
val id: Int,
|
||||
val hintType: GameDetectionHintType,
|
||||
val hintPath: String,
|
||||
val hintKey: String?,
|
||||
@Serializable(with = GameDetectionHintOption.Ser::class)
|
||||
val hintOptions: Set<GameDetectionHintOption>,
|
||||
val gameId: Int
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import me.jie65535.jcf.util.internal.FlagSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
enum class GameDetectionHintOption {
|
||||
NONE,
|
||||
INCLUDE_SUB_FOLDERS,
|
||||
FOLDER_ONLY;
|
||||
|
||||
internal object Ser : FlagSerializer<GameDetectionHintOption>("$MODEL_PACKAGE.game.GameDetectionHintOption", NONE to 0x1, INCLUDE_SUB_FOLDERS to 0x2, FOLDER_ONLY to 0x4)
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = GameDetectionHintType.Ser::class)
|
||||
enum class GameDetectionHintType {
|
||||
REGISTRY,
|
||||
FILE_PATH;
|
||||
|
||||
internal object Ser : EnumIntSerializer<GameDetectionHintType>("$MODEL_PACKAGE.game.GameDetectionHintType", values())
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GameFile(
|
||||
val id: Int,
|
||||
val gameId: Int,
|
||||
val isRequired: Boolean,
|
||||
val fileName: String,
|
||||
val fileType: GameFileType,
|
||||
val platformType: GamePlatformType
|
||||
)
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GameFileParsingRule(
|
||||
val commentStripPattern: String,
|
||||
val fileExtension: String,
|
||||
val inclusionPattern: String,
|
||||
val gameId: Int,
|
||||
val id: Int
|
||||
)
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = GameFileType.Ser::class)
|
||||
enum class GameFileType {
|
||||
GENERIC,
|
||||
GAME,
|
||||
LAUNCHER,
|
||||
PROFILER_LOCK_CHECK;
|
||||
|
||||
internal object Ser : EnumIntSerializer<GameFileType>("$MODEL_PACKAGE.game.GameFileType", values())
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = GamePlatformType.Ser::class)
|
||||
enum class GamePlatformType {
|
||||
GENERIC,
|
||||
WINDOWS_32,
|
||||
WINDOWS_64,
|
||||
WINDOWS,
|
||||
OSX;
|
||||
|
||||
internal object Ser : EnumIntSerializer<GamePlatformType>("$MODEL_PACKAGE.game.GamePlatformType", values())
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.game
|
||||
|
||||
import me.jie65535.jcf.util.internal.FlagSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
enum class SupportedClientConfiguration {
|
||||
DEBUG,
|
||||
BETA,
|
||||
RELEASE;
|
||||
|
||||
internal object Ser : FlagSerializer<SupportedClientConfiguration>("$MODEL_PACKAGE.game.SupportedClientConfiguration", DEBUG to 0x1, BETA to 0x2, RELEASE to 0x4)
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.minecraft
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.Date
|
||||
import me.jie65535.jcf.util.internal.DateSerializer
|
||||
|
||||
@Serializable
|
||||
data class MinecraftVersion(
|
||||
val id: Int,
|
||||
val gameVersionId: Int,
|
||||
val versionString: String,
|
||||
val jarDownloadUrl: String,
|
||||
val jsonDownloadUrl: String,
|
||||
val approved: Boolean,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val dateModified: Date,
|
||||
val gameVersionTypeId: Int,
|
||||
val gameVersionStatus: MinecraftVersionStatus,
|
||||
val gameVersionTypeStatus: MinecraftVersionTypeStatus
|
||||
)
|
@ -1,21 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.minecraft
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = MinecraftVersionStatus.Ser::class)
|
||||
enum class MinecraftVersionStatus {
|
||||
APPROVED,
|
||||
DELETED,
|
||||
NEW;
|
||||
|
||||
internal object Ser : EnumIntSerializer<MinecraftVersionStatus>("$MODEL_PACKAGE.game.MinecraftVersionStatus", values())
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.minecraft
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = MinecraftVersionTypeStatus.Ser::class)
|
||||
enum class MinecraftVersionTypeStatus {
|
||||
NORMAL,
|
||||
DELETED;
|
||||
|
||||
internal object Ser : EnumIntSerializer<MinecraftVersionTypeStatus>("$MODEL_PACKAGE.game.MinecraftVersionTypeStatus", values())
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.minecraft.modloader
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.Date
|
||||
import me.jie65535.jcf.util.internal.DateSerializer
|
||||
|
||||
@Serializable
|
||||
data class ModloaderIndex(
|
||||
val name: String,
|
||||
val gameVersion: String,
|
||||
val latest: Boolean,
|
||||
val recommended: Boolean,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val dateModified: Date
|
||||
)
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.minecraft.modloader
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = ModloaderInstallMethod.Ser::class)
|
||||
enum class ModloaderInstallMethod {
|
||||
ANY,
|
||||
FORGE,
|
||||
CAULDRON,
|
||||
LITE_LOADER;
|
||||
|
||||
internal object Ser : EnumIntSerializer<ModloaderInstallMethod>("$MODEL_PACKAGE.game.ModloaderInstallMethod", values())
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.minecraft.modloader
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.util.internal.EnumIntSerializer
|
||||
import me.jie65535.jcf.util.internal.MODEL_PACKAGE
|
||||
|
||||
@Serializable(with = ModloaderType.Ser::class)
|
||||
enum class ModloaderType {
|
||||
ANY,
|
||||
FORGE,
|
||||
CAULDRON,
|
||||
LITE_LOADER;
|
||||
|
||||
internal object Ser : EnumIntSerializer<ModloaderType>("$MODEL_PACKAGE.game.ModloaderType", values(), 0)
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.model.minecraft.modloader
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import me.jie65535.jcf.model.minecraft.MinecraftVersionStatus
|
||||
import me.jie65535.jcf.model.minecraft.MinecraftVersionTypeStatus
|
||||
import me.jie65535.jcf.util.Date
|
||||
import me.jie65535.jcf.util.internal.DateSerializer
|
||||
|
||||
@Serializable
|
||||
data class ModloaderVersion(
|
||||
val id: Int,
|
||||
val gameVersionId: Int,
|
||||
val minecraftGameVersionId: Int,
|
||||
val forgeVersion: String,
|
||||
val name: String,
|
||||
val type: ModloaderType,
|
||||
val downloadUrl: String,
|
||||
@SerialName("filename")
|
||||
val fileName: String,
|
||||
val installMethod: ModloaderInstallMethod,
|
||||
val latest: Boolean,
|
||||
val recommended: Boolean,
|
||||
val approved: Boolean,
|
||||
@Serializable(with = DateSerializer::class)
|
||||
val dateModified: Date,
|
||||
val mavenVersionString: String,
|
||||
val versionJson: String,
|
||||
val librariesInstallLocation: String,
|
||||
val minecraftVersion: String,
|
||||
val additionalFilesJson: String,
|
||||
val modLoaderGameVersionId: Int,
|
||||
val modLoaderGameVersionTypeId: Int,
|
||||
val modLoaderGameVersionStatus: MinecraftVersionStatus,
|
||||
val modLoaderGameVersionTypeStatus: MinecraftVersionTypeStatus,
|
||||
val mcGameVersionId: Int,
|
||||
val mcGameVersionTypeId: Int,
|
||||
val mcGameVersionStatus: MinecraftVersionStatus,
|
||||
val mcGameVersionTypeStatus: MinecraftVersionTypeStatus,
|
||||
val installProfileJson: String
|
||||
)
|
@ -1,5 +0,0 @@
|
||||
package me.jie65535.jcf.util
|
||||
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
typealias Date = OffsetDateTime
|
@ -1,26 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.util.internal
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
import me.jie65535.jcf.util.Date
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
internal object DateSerializer : KSerializer<Date> {
|
||||
override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): Date {
|
||||
return OffsetDateTime.parse(decoder.decodeString())
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Date) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.util.internal
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
internal open class EnumIntSerializer<T>(private val serialName: String, private val values: Array<T>, private val startFrom: Int = 1) : KSerializer<T> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(serialName, PrimitiveKind.INT)
|
||||
|
||||
override fun deserialize(decoder: Decoder): T {
|
||||
val value = decoder.decodeInt()
|
||||
return values[value - startFrom] ?: throw NoSuchElementException("Can't find $serialName with key $value")
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: T) {
|
||||
encoder.encodeInt(values.indexOf(value) + startFrom)
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/*
|
||||
* Copyright © 2020, PearX Team
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package me.jie65535.jcf.util.internal
|
||||
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.descriptors.*
|
||||
import kotlinx.serialization.encoding.*
|
||||
|
||||
internal open class FlagSerializer<T : Enum<T>>(serialName: String, private val map: Map<T, Int>) : KSerializer<Set<T>> {
|
||||
constructor(serialName: String, vararg pairs: Pair<T, Int>) : this(serialName, mapOf(*pairs))
|
||||
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(serialName, PrimitiveKind.INT)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Set<T>) {
|
||||
var bits = 0
|
||||
for(flag in value) {
|
||||
bits = bits or (map[flag] ?: error(""))
|
||||
}
|
||||
encoder.encodeInt(bits)
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): Set<T> {
|
||||
val set = mutableSetOf<T>()
|
||||
val bits = decoder.decodeInt()
|
||||
for((flag, shift) in map) {
|
||||
if(bits and shift == shift)
|
||||
set += flag
|
||||
}
|
||||
return set
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
package me.jie65535.jcf.util.internal
|
||||
|
||||
internal const val MODEL_PACKAGE = "me.jie65535.jcf.model"
|
284
src/main/kotlin/top/jie65535/jcf/CurseforgeApi.kt
Normal file
284
src/main/kotlin/top/jie65535/jcf/CurseforgeApi.kt
Normal file
@ -0,0 +1,284 @@
|
||||
package top.jie65535.jcf
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.json.Json
|
||||
import top.jie65535.jcf.model.Category
|
||||
import top.jie65535.jcf.model.file.File
|
||||
import top.jie65535.jcf.model.game.Game
|
||||
import top.jie65535.jcf.model.game.GameVersionType
|
||||
import top.jie65535.jcf.model.mod.*
|
||||
import top.jie65535.jcf.model.request.*
|
||||
import top.jie65535.jcf.model.request.SortOrder.*
|
||||
import top.jie65535.jcf.model.response.*
|
||||
|
||||
/**
|
||||
* [Api docs](https://docs.curseforge.com/)
|
||||
* @author jie65535
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
class CurseforgeApi(apiKey: String) {
|
||||
|
||||
private val json = Json {
|
||||
isLenient = true
|
||||
ignoreUnknownKeys = true
|
||||
serializersModule
|
||||
}
|
||||
|
||||
private val http = HttpClient(OkHttp) {
|
||||
install(HttpTimeout) {
|
||||
this.requestTimeoutMillis = 60_000
|
||||
this.connectTimeoutMillis = 60_000
|
||||
this.socketTimeoutMillis = 60_000
|
||||
}
|
||||
defaultRequest {
|
||||
url.protocol = URLProtocol.HTTPS
|
||||
url.host = "api.curseforge.com"
|
||||
header("accept", "application/json")
|
||||
header("x-api-key", apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
//region - Game -
|
||||
|
||||
/**
|
||||
* Get all games that are available to the provided API key.
|
||||
*/
|
||||
suspend fun getGames(index: Int? = null, pageSize: Int? = null): GetGamesResponse {
|
||||
return json.decodeFromString(
|
||||
http.get("/v1/games") {
|
||||
parameter("index", index)
|
||||
parameter("pageSize", pageSize)
|
||||
}.body()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single game. A private game is only accessible by its respective API key.
|
||||
*/
|
||||
suspend fun getGame(gameId: Int): Game {
|
||||
return json.decodeFromString<GetGameResponse>(
|
||||
http.get("/v1/games/$gameId").body()
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available versions for each known version type of the specified game.
|
||||
* A private game is only accessible to its respective API key.
|
||||
*/
|
||||
suspend fun getVersions(gameId: Int): Array<GameVersionsByType> {
|
||||
return json.decodeFromString<GetVersionsResponse>(
|
||||
http.get("/v1/games/$gameId/versions").body()
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available version types of the specified game.
|
||||
*
|
||||
* A private game is only accessible to its respective API key.
|
||||
*
|
||||
* Currently, when creating games via the CurseForge Core Console,
|
||||
* you are limited to a single game version type.
|
||||
* This means that this endpoint is probably not useful in most cases
|
||||
* and is relevant mostly when handling existing games that have
|
||||
* multiple game versions such as World of Warcraft and Minecraft
|
||||
* (e.g. 517 for wow_retail).
|
||||
*/
|
||||
suspend fun getVersionTypes(gameId: Int): Array<GameVersionType> {
|
||||
return json.decodeFromString<GetVersionTypesResponse>(
|
||||
http.get("/v1/games/$gameId/versions").body()
|
||||
).data
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region - Categories -
|
||||
|
||||
/**
|
||||
* Get all available classes and categories of the specified game.
|
||||
* Specify a game id for a list of all game categories,
|
||||
* or a class id for a list of categories under that class.
|
||||
*/
|
||||
suspend fun getCategories(gameId: Int, classId: Int? = null): Array<Category> {
|
||||
return json.decodeFromString<GetCategoriesResponse>(
|
||||
http.get("/v1/categories") {
|
||||
parameter("gameId", gameId)
|
||||
parameter("classId", classId)
|
||||
}.body()
|
||||
).data
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region - Mods -
|
||||
|
||||
/**
|
||||
* Get all mods that match the search criteria.
|
||||
* @param gameId Filter by game id.
|
||||
* @param classId Filter by section id (discoverable via Categories)
|
||||
* @param categoryId Filter by category id
|
||||
* @param gameVersion Filter by game version string
|
||||
* @param searchFilter Filter by free text search in the mod name and author
|
||||
* @param sortField Filter by ModsSearchSortField enumeration
|
||||
* @param sortOrder 'asc' if sort is in ascending order, 'desc' if sort is in descending order
|
||||
* @param modLoaderType Filter only mods associated to a given modloader (Forge, Fabric ...). Must be coupled with gameVersion.
|
||||
* @param gameVersionTypeId Filter only mods that contain files tagged with versions of the given gameVersionTypeId
|
||||
* @param slug Filter by slug (coupled with classId will result in a unique result).
|
||||
* @param index A zero based index of the first item to include in the response,
|
||||
* @param pageSize The number of items to include in the response,
|
||||
*/
|
||||
suspend fun searchMods(
|
||||
gameId: Int,
|
||||
classId: Int? = null,
|
||||
categoryId: Int? = null,
|
||||
gameVersion: String? = null,
|
||||
searchFilter: String? = null,
|
||||
sortField: ModsSearchSortField? = null,
|
||||
sortOrder: SortOrder? = null,
|
||||
modLoaderType: ModLoaderType? = null,
|
||||
gameVersionTypeId: Int? = null,
|
||||
slug: String? = null,
|
||||
index: Int? = null,
|
||||
pageSize: Int? = null
|
||||
): SearchModsResponse {
|
||||
return json.decodeFromString(
|
||||
http.get("/v1/mods/search") {
|
||||
parameter("gameId", gameId)
|
||||
parameter("classId", classId)
|
||||
parameter("categoryId", categoryId)
|
||||
parameter("gameVersion", gameVersion)
|
||||
parameter("searchFilter", searchFilter)
|
||||
parameter("sortField", sortField)
|
||||
parameter("sortOrder", when(sortOrder){
|
||||
ASC -> "asc"
|
||||
DESC -> "desc"
|
||||
null -> null
|
||||
})
|
||||
parameter("modLoaderType", modLoaderType)
|
||||
parameter("gameVersionTypeId", gameVersionTypeId)
|
||||
parameter("slug", slug)
|
||||
parameter("index", index)
|
||||
parameter("pageSize", pageSize)
|
||||
}.body()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single mod.
|
||||
*/
|
||||
suspend fun getMod(modId: Int): Mod {
|
||||
return json.decodeFromString<GetModResponse>(
|
||||
http.get("/v1/mods/$modId").body()
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of mods.
|
||||
*/
|
||||
suspend fun getMods(modIds: IntArray): Array<Mod> {
|
||||
return json.decodeFromString<GetModsResponse>(
|
||||
http.post("/v1/mods") {
|
||||
headers.append("Content-Type", "application/json")
|
||||
setBody(json.encodeToString(GetModsByIdsListRequestBody(modIds)))
|
||||
}.body()
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of featured, popular and recently updated mods.
|
||||
*/
|
||||
suspend fun getFeaturedMods(
|
||||
gameId: Int,
|
||||
excludedModIds: IntArray = intArrayOf(),
|
||||
gameVersionTypeId: Int? = null
|
||||
): FeaturedModsResponse {
|
||||
return json.decodeFromString<GetFeaturedModsResponse>(
|
||||
http.get("/v1/mods/featured") {
|
||||
headers.append("Content-Type", "application/json")
|
||||
setBody(json.encodeToString(GetFeaturedModsRequestBody(gameId, excludedModIds, gameVersionTypeId)))
|
||||
}.body()
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full description of a mod in HTML format.
|
||||
*/
|
||||
suspend fun getModDescription(modId: Int): String {
|
||||
return json.decodeFromString<StringResponse>(
|
||||
http.get("/v1/mods/$modId/description").body()
|
||||
).data
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//region - Files -
|
||||
|
||||
/**
|
||||
* Get a single file of the specified mod.
|
||||
*/
|
||||
suspend fun getModFile(modId: Int, fileId: Int): File {
|
||||
return json.decodeFromString<GetModFileResponse>(
|
||||
http.get("/v1/mods/$modId/files/$fileId").body()
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files of the specified mod.
|
||||
*/
|
||||
suspend fun getModFiles(
|
||||
modId: Int,
|
||||
gameVersion: String? = null,
|
||||
modLoaderType: ModLoaderType? = null,
|
||||
gameVersionTypeId: Int? = null,
|
||||
index: Int? = null,
|
||||
pageSize: Int? = null
|
||||
): GetModFilesResponse {
|
||||
return json.decodeFromString(
|
||||
http.get("/v1/mods/$modId/files") {
|
||||
parameter("gameVersion", gameVersion)
|
||||
parameter("modLoaderType", modLoaderType)
|
||||
parameter("gameVersionTypeId", gameVersionTypeId)
|
||||
parameter("index", index)
|
||||
parameter("pageSize", pageSize)
|
||||
}.body()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of files.
|
||||
*/
|
||||
suspend fun getFiles(fileIds: IntArray): Array<File> {
|
||||
return json.decodeFromString<GetFilesResponse>(
|
||||
http.post("/v1/mods/files") {
|
||||
headers.append("Content-Type", "application/json")
|
||||
setBody(json.encodeToString(GetModFilesRequestBody(fileIds)))
|
||||
}.body()
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the changelog of a file in HTML format
|
||||
*/
|
||||
suspend fun getModFileChangelog(modId: Int, fileId: Int): String {
|
||||
return json.decodeFromString<StringResponse>(
|
||||
http.get("/v1/mods/$modId/files/$fileId/changelog").body()
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a download url for a specific file
|
||||
*/
|
||||
suspend fun getModFileDownloadURL(modId: Int, fileId: Int): String {
|
||||
return json.decodeFromString<StringResponse>(
|
||||
http.get("/v1/mods/$modId/files/$fileId/download-url").body()
|
||||
).data
|
||||
}
|
||||
|
||||
//endregion
|
||||
}
|
271
src/main/kotlin/top/jie65535/jcf/MessageHandler.kt
Normal file
271
src/main/kotlin/top/jie65535/jcf/MessageHandler.kt
Normal file
@ -0,0 +1,271 @@
|
||||
package top.jie65535.jcf
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import net.mamoe.mirai.contact.nameCardOrNick
|
||||
import net.mamoe.mirai.event.*
|
||||
import net.mamoe.mirai.event.events.GroupMessageEvent
|
||||
import net.mamoe.mirai.event.events.MessageEvent
|
||||
import net.mamoe.mirai.message.data.*
|
||||
import net.mamoe.mirai.message.data.MessageSource.Key.quote
|
||||
import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import top.jie65535.jcf.model.file.File
|
||||
import top.jie65535.jcf.model.mod.Mod
|
||||
import top.jie65535.jcf.util.PagedList
|
||||
import top.jie65535.jcf.util.HttpUtil
|
||||
import java.text.DecimalFormat
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.min
|
||||
|
||||
class MessageHandler(
|
||||
private val service: MinecraftService,
|
||||
private val eventChannel: EventChannel<Event>,
|
||||
private val logger: MiraiLogger
|
||||
) {
|
||||
|
||||
fun startListen() {
|
||||
eventChannel.subscribeMessages {
|
||||
for ((modClass, command) in PluginConfig.searchCommands) {
|
||||
if (command.isBlank()) continue
|
||||
startsWith(command) {
|
||||
val filter = it.trim()
|
||||
if (filter.isEmpty()) {
|
||||
subject.sendMessage(message.quote() + "必须输入关键字")
|
||||
} else {
|
||||
try {
|
||||
logger.info("${sender.nameCardOrNick}(${sender.id}) $modClass \"$filter\"")
|
||||
val pagedList = service.search(modClass, filter)
|
||||
with(pagedList.current()) {
|
||||
if (isEmpty()) {
|
||||
subject.sendMessage("未搜索到关键字\"$filter\"相关结果")
|
||||
} else if (size == 1) {
|
||||
handleShowMod(get(0))
|
||||
} else {
|
||||
handleModsSearchResult(pagedList)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
subject.sendMessage(message.quote() + "发生内部错误,请稍后重试")
|
||||
logger.error("消息\"$message\"引发异常", e)
|
||||
}
|
||||
}// if
|
||||
}
|
||||
}// for
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理分页列表选择功能,返回用户选中项,返回null表示未选中任何项
|
||||
* @param pagedList 分页的列表
|
||||
* @param format 格式化方法
|
||||
* @return 用户选中项,null表示未选择任何项
|
||||
*/
|
||||
private suspend fun <T> MessageEvent.handlePagedList(pagedList: PagedList<T>, format: suspend (T)->Message): T? {
|
||||
do {
|
||||
var isContinue = false
|
||||
val list = pagedList.current()
|
||||
val listMessage = subject.sendMessage(buildForwardMessage {
|
||||
for ((i, it) in list.withIndex()) {
|
||||
bot.id named i.toString() says format(it)
|
||||
}
|
||||
var msg = "$WAIT_REPLY_TIMEOUT_S 秒内回复编号查看"
|
||||
if (pagedList.hasPrev)
|
||||
msg += "\n回复 $PAGE_UP_KEYWORD 上一页"
|
||||
if (pagedList.hasNext)
|
||||
msg += "\n回复 $PAGE_DOWN_KEYWORD 下一页"
|
||||
bot says msg
|
||||
})
|
||||
try {
|
||||
// 获取用户回复
|
||||
val next = withTimeout(WAIT_REPLY_TIMEOUT_S * 1000L) {
|
||||
eventChannel.nextEvent<MessageEvent>(EventPriority.MONITOR) { it.sender == sender }
|
||||
}
|
||||
val nextMessage = next.message.content
|
||||
if (nextMessage.equals(PAGE_DOWN_KEYWORD, true)) {
|
||||
pagedList.next()
|
||||
} else if (nextMessage.equals(PAGE_UP_KEYWORD, true)) {
|
||||
pagedList.prev()
|
||||
} else {
|
||||
return list[nextMessage.toInt()]
|
||||
}
|
||||
isContinue = true
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
subject.sendMessage("等待回复超时,请重新查询。")
|
||||
} catch (e: NumberFormatException) {
|
||||
subject.sendMessage("请回复正确的选项,此次查询已取消,请重新查询。")
|
||||
} catch (e: IndexOutOfBoundsException) {
|
||||
subject.sendMessage("请回复正确的序号,此次查询已取消,请重新查询。")
|
||||
} catch (e: Throwable) {
|
||||
subject.sendMessage(message.quote() + "发生内部错误,请稍后重试")
|
||||
logger.warning("回复消息\"$message\"引发意外的异常", e)
|
||||
} finally {
|
||||
listMessage.recall()
|
||||
}
|
||||
} while (isContinue)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理mod搜索结果
|
||||
*/
|
||||
private suspend fun MessageEvent.handleModsSearchResult(pagedList: PagedList<Mod>) {
|
||||
val selectedMod = handlePagedList(pagedList) { mod ->
|
||||
val text = PlainText(
|
||||
"""
|
||||
[${mod.name}] by ${mod.authors.firstOrNull()?.name}
|
||||
${formatCount(mod.downloadCount)} Downloads Updated ${mod.dateModified.toLocalDate()}
|
||||
${mod.summary}
|
||||
${mod.links.websiteUrl}
|
||||
""".trimIndent())
|
||||
mod.logo?.thumbnailUrl?.let { loadImage(it) + text } ?: text
|
||||
}
|
||||
selectedMod?.let { handleShowMod(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理展示单个mod
|
||||
*/
|
||||
private suspend fun MessageEvent.handleShowMod(mod: Mod) {
|
||||
subject.sendMessage(
|
||||
buildForwardMessage {
|
||||
// logo
|
||||
mod.logo?.thumbnailUrl?.let {
|
||||
if (it.isNotBlank()) bot says loadImage(it)
|
||||
}
|
||||
// basic info
|
||||
bot says PlainText(with(mod) {
|
||||
"""
|
||||
$name
|
||||
作者:${authors.joinToString { it.name }}
|
||||
概括:$summary
|
||||
项目ID:$id
|
||||
创建时间:${dateCreated.toLocalDate()}
|
||||
最近更新:${dateModified.toLocalDate()}
|
||||
总下载:$downloadCount
|
||||
主页:${mod.links.websiteUrl}
|
||||
""".trimIndent()
|
||||
})
|
||||
var msg = "$WAIT_REPLY_TIMEOUT_S 秒内回复 $SUBSCRIBE_KEYWORD 订阅模组更新"
|
||||
if (mod.latestFiles.isNotEmpty()) {
|
||||
msg += "\n回复编号查看文件详细信息\n" +
|
||||
"回复 $VIEW_FILES_KEYWORD 查看全部历史文件"
|
||||
}
|
||||
bot says msg
|
||||
for ((i, file) in mod.latestFiles.withIndex()) {
|
||||
bot.id named i.toString() says PlainText(
|
||||
"""
|
||||
${file.displayName} [${file.releaseType}] [${file.fileDate.toLocalDate()}]
|
||||
${file.downloadUrl}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
try {
|
||||
// 获取用户回复
|
||||
val next = withTimeout(WAIT_REPLY_TIMEOUT_S * 1000L) {
|
||||
eventChannel.nextEvent<MessageEvent>(EventPriority.MONITOR) { it.sender == sender }
|
||||
}
|
||||
val nextMessage = next.message.content
|
||||
val subsHandler = PluginMain.subscribeHandler
|
||||
if (nextMessage.equals(SUBSCRIBE_KEYWORD, true)) {
|
||||
if (next is GroupMessageEvent) {
|
||||
subsHandler.sub(mod.id, next.sender.id, next.group.id)
|
||||
} else {
|
||||
subsHandler.sub(mod.id, next.sender.id)
|
||||
}
|
||||
subject.sendMessage(QuoteReply(next.source) + "已添加订阅")
|
||||
} else if (mod.latestFiles.isNotEmpty()) {
|
||||
if (nextMessage.equals(VIEW_FILES_KEYWORD, true)) {
|
||||
// 查看所有文件
|
||||
handleModFileList(service.getModFiles(mod.id))
|
||||
} else {
|
||||
// 查看文件详情
|
||||
handleModFile(mod.latestFiles[nextMessage.toInt()])
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// 忽略因回复引发的异常,无论是超时、越界还是格式不正确,不提示错误
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理模组文件列表
|
||||
*/
|
||||
private suspend fun MessageEvent.handleModFileList(pagedList: PagedList<File>) {
|
||||
val selectedFile = handlePagedList(pagedList) { file ->
|
||||
PlainText(
|
||||
"""
|
||||
${file.displayName} [${file.releaseType}] [${file.fileDate.toLocalDate()}]
|
||||
${file.downloadUrl}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
selectedFile?.let { handleModFile(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理模组文件具体信息展示
|
||||
*/
|
||||
private suspend fun MessageEvent.handleModFile(file: File) {
|
||||
try {
|
||||
// 暂时仅展示文件更改日志,可以添加文件依赖相关信息的显示
|
||||
subject.sendMessage(handleModFileChangelog(service.getModFileChangelog(file.modId, file.id)))
|
||||
} catch (e: Throwable) {
|
||||
logger.warning("获取文件[${file.fileName}]更改日志时异常", e)
|
||||
subject.sendMessage("获取文件更改日志时异常,请稍后重试")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理模组文件更改日志
|
||||
* @param changelog 更改日志(HTML)
|
||||
*/
|
||||
private fun MessageEvent.handleModFileChangelog(changelog: String): Message {
|
||||
val logs = HTMLPattern.matcher(changelog).replaceAll("")
|
||||
return sendLargeMessage(logs)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val WAIT_REPLY_TIMEOUT_S = 60
|
||||
private const val PAGE_UP_KEYWORD = "P"
|
||||
private const val PAGE_DOWN_KEYWORD = "N"
|
||||
private const val VIEW_FILES_KEYWORD = "ALL"
|
||||
private const val SUBSCRIBE_KEYWORD = "订阅"
|
||||
private const val ONE_GRP_SIZE = 5000
|
||||
private const val ONE_MSG_SIZE = 500
|
||||
private val singleDecimalFormat = DecimalFormat("0.#")
|
||||
|
||||
private fun formatCount(count: Long): String = when {
|
||||
count < 1000000 -> singleDecimalFormat.format(count / 1000) + "K"
|
||||
count < 1000000000 -> singleDecimalFormat.format(count / 1000000) + "M"
|
||||
else -> count.toString()
|
||||
}
|
||||
|
||||
private suspend fun MessageEvent.loadImage(url: String): Image {
|
||||
val imgFileName = url.substringAfterLast("/")
|
||||
val file = PluginMain.resolveDataFile("cache/$imgFileName")
|
||||
val res = if (file.exists()) {
|
||||
file.readBytes().toExternalResource()
|
||||
} else {
|
||||
HttpUtil.downloadImage(url, file).toExternalResource()
|
||||
}
|
||||
val image = subject.uploadImage(res)
|
||||
withContext(Dispatchers.IO) {
|
||||
res.close()
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
val HTMLPattern: Pattern = Pattern.compile("<[^>]+>", Pattern.CASE_INSENSITIVE)
|
||||
fun MessageEvent.sendLargeMessage(message: String): Message {
|
||||
return buildForwardMessage {
|
||||
for (g in message.indices step ONE_GRP_SIZE) {
|
||||
for (i in g until g + min(ONE_GRP_SIZE, message.length - g) step ONE_MSG_SIZE) {
|
||||
bot says PlainText(message.subSequence(i, i + (min(ONE_MSG_SIZE, message.length - i))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}// fun
|
||||
}
|
||||
}
|
122
src/main/kotlin/top/jie65535/jcf/MinecraftService.kt
Normal file
122
src/main/kotlin/top/jie65535/jcf/MinecraftService.kt
Normal file
@ -0,0 +1,122 @@
|
||||
package top.jie65535.jcf
|
||||
|
||||
import top.jie65535.jcf.model.file.File
|
||||
import top.jie65535.jcf.model.mod.Mod
|
||||
import top.jie65535.jcf.model.request.ModsSearchSortField
|
||||
import top.jie65535.jcf.model.request.SortOrder
|
||||
import top.jie65535.jcf.util.PagedList
|
||||
|
||||
class MinecraftService(apiKey: String) {
|
||||
companion object {
|
||||
private const val GAME_ID_MINECRAFT = 432
|
||||
private const val DEFAULT_PAGE_SIZE = 10
|
||||
private val DEFAULT_SORT_FIELD = ModsSearchSortField.Popularity
|
||||
}
|
||||
|
||||
/**
|
||||
* mod分类
|
||||
*/
|
||||
enum class ModClass(val className: String, val classId: Int) {
|
||||
/**
|
||||
* 存档
|
||||
*/
|
||||
WORLDS("存档",17),
|
||||
|
||||
/**
|
||||
* 水桶服插件
|
||||
*/
|
||||
BUKKIT_PLUGINS("水桶服插件", 5),
|
||||
|
||||
/**
|
||||
* 自定义
|
||||
*/
|
||||
CUSTOMIZATION("定制", 4546),
|
||||
|
||||
/**
|
||||
* 整合包
|
||||
*/
|
||||
MODPACKS("整合包", 4471),
|
||||
|
||||
/**
|
||||
* 资源包
|
||||
*/
|
||||
RESOURCE_PACKS("资源包", 12),
|
||||
|
||||
/**
|
||||
* 附加
|
||||
*/
|
||||
ADDONS("附加", 4559),
|
||||
|
||||
/**
|
||||
* 模组
|
||||
*/
|
||||
MODS("模组", 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* api客户端实例
|
||||
*/
|
||||
private val api = CurseforgeApi(apiKey)
|
||||
|
||||
/**
|
||||
* 根据分类与过滤器进行搜索,返回分页的列表
|
||||
* @param modClass mod分类
|
||||
* @param filter 过滤器
|
||||
* @return 模组分页列表
|
||||
*/
|
||||
fun search(modClass: ModClass, filter: String): PagedList<Mod> =
|
||||
PagedList(DEFAULT_PAGE_SIZE) { index ->
|
||||
val response = api.searchMods(
|
||||
GAME_ID_MINECRAFT,
|
||||
modClass.classId,
|
||||
searchFilter = filter,
|
||||
sortField = DEFAULT_SORT_FIELD,
|
||||
sortOrder = SortOrder.DESC,
|
||||
index = index,
|
||||
pageSize = DEFAULT_PAGE_SIZE
|
||||
)
|
||||
response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模组ID获取指定模组
|
||||
*/
|
||||
suspend fun getMod(modId: Int) = api.getMod(modId)
|
||||
|
||||
/**
|
||||
* 根据模组Id列表获取指定模组列表
|
||||
*/
|
||||
suspend fun getMods(modIds: IntArray) = api.getMods(modIds)
|
||||
|
||||
/**
|
||||
* 获取指定模组文件
|
||||
*/
|
||||
suspend fun getModFile(modId: Int, fileId: Int) = api.getModFile(modId, fileId)
|
||||
|
||||
/**
|
||||
* 获取模组文件列表,返回分页的列表
|
||||
* @return 分页的列表
|
||||
*/
|
||||
suspend fun getModFiles(modId: Int): PagedList<File> =
|
||||
PagedList(DEFAULT_PAGE_SIZE) { index ->
|
||||
val response = api.getModFiles(
|
||||
modId,
|
||||
index = index,
|
||||
pageSize = DEFAULT_PAGE_SIZE
|
||||
)
|
||||
response.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件更改日志,结果为HTML文本
|
||||
* @return Changelog HTML
|
||||
*/
|
||||
suspend fun getModFileChangelog(modId: Int, fileId: Int) =
|
||||
api.getModFileChangelog(modId, fileId)
|
||||
|
||||
/**
|
||||
* 获取文件下载地址
|
||||
*/
|
||||
suspend fun getModFileDownloadURL(modId: Int, fileId: Int) =
|
||||
api.getModFileDownloadURL(modId, fileId)
|
||||
}
|
62
src/main/kotlin/top/jie65535/jcf/PluginCommands.kt
Normal file
62
src/main/kotlin/top/jie65535/jcf/PluginCommands.kt
Normal file
@ -0,0 +1,62 @@
|
||||
package top.jie65535.jcf
|
||||
|
||||
import net.mamoe.mirai.console.command.CommandSender
|
||||
import net.mamoe.mirai.console.command.CompositeCommand
|
||||
|
||||
object PluginCommands : CompositeCommand(PluginMain, "jcf") {
|
||||
@SubCommand
|
||||
@Description("设置Curseforge API Key")
|
||||
suspend fun CommandSender.setApiKey(apiKey: String) {
|
||||
PluginConfig.apiKey = apiKey
|
||||
sendMessage("OK! 重启插件生效")
|
||||
}
|
||||
|
||||
@SubCommand
|
||||
@Description("查看插件帮助")
|
||||
suspend fun CommandSender.help() {
|
||||
val msg = StringBuilder()
|
||||
for ((modClass, cmd) in PluginConfig.searchCommands) {
|
||||
msg.appendLine("搜索${modClass.className}: $cmd")
|
||||
}
|
||||
sendMessage(msg.toString())
|
||||
}
|
||||
|
||||
@SubCommand
|
||||
@Description("设置订阅信息推送bot(qq id)")
|
||||
suspend fun CommandSender.setSubsSender(sender: Long) {
|
||||
PluginConfig.subscribeSender = sender
|
||||
sendMessage("OK! ")
|
||||
}
|
||||
|
||||
@SubCommand
|
||||
@Description("设置检查间隔(单位:秒)")
|
||||
suspend fun CommandSender.setCheckInterval(second: Long) {
|
||||
PluginConfig.checkInterval = second
|
||||
sendMessage("OK! 将在下次检查结束后应用")
|
||||
}
|
||||
|
||||
@SubCommand
|
||||
@Description("查看订阅处理的状态")
|
||||
suspend fun CommandSender.subStat() {
|
||||
val subs = PluginMain.subscribeHandler
|
||||
if (subs.isIdle) {
|
||||
sendMessage("订阅器闲置中")
|
||||
} else {
|
||||
sendMessage("订阅处理正常运行中")
|
||||
}
|
||||
}
|
||||
|
||||
@SubCommand
|
||||
@Description("使订阅器闲置")
|
||||
suspend fun CommandSender.idleSubs() {
|
||||
PluginMain.subscribeHandler.idle()
|
||||
sendMessage("OK,已闲置")
|
||||
}
|
||||
|
||||
@SubCommand
|
||||
@Description("使订阅器恢复运行")
|
||||
suspend fun CommandSender.runSubs() {
|
||||
PluginMain.subscribeHandler.start()
|
||||
sendMessage("OK,已恢复订阅处理")
|
||||
}
|
||||
}
|
35
src/main/kotlin/top/jie65535/jcf/PluginConfig.kt
Normal file
35
src/main/kotlin/top/jie65535/jcf/PluginConfig.kt
Normal file
@ -0,0 +1,35 @@
|
||||
package top.jie65535.jcf
|
||||
|
||||
import net.mamoe.mirai.console.data.AutoSavePluginConfig
|
||||
import net.mamoe.mirai.console.data.ValueDescription
|
||||
import net.mamoe.mirai.console.data.value
|
||||
|
||||
object PluginConfig : AutoSavePluginConfig("JCurseforgeConfig") {
|
||||
@ValueDescription("Curseforge API KEY")
|
||||
var apiKey: String by value()
|
||||
|
||||
@ValueDescription("搜索命令 (MODS,MODPACKS,RESOURCE_PACKS,WORLDS,BUKKIT_PLUGINS,ADDONS,CUSTOMIZATION)")
|
||||
val searchCommands: MutableMap<MinecraftService.ModClass, String> by value(
|
||||
mutableMapOf(
|
||||
MinecraftService.ModClass.MODS to "cfmod ",
|
||||
MinecraftService.ModClass.MODPACKS to "cfpack ",
|
||||
MinecraftService.ModClass.RESOURCE_PACKS to "cfres ",
|
||||
MinecraftService.ModClass.WORLDS to "cfworld ",
|
||||
MinecraftService.ModClass.BUKKIT_PLUGINS to "cfbukkit ",
|
||||
MinecraftService.ModClass.ADDONS to "cfaddon ",
|
||||
MinecraftService.ModClass.CUSTOMIZATION to "cfcustom ",
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* 订阅信息推送bot
|
||||
*/
|
||||
@ValueDescription("订阅信息推送bot(qq id)")
|
||||
var subscribeSender: Long by value(-1L)
|
||||
|
||||
/**
|
||||
* 检查间隔
|
||||
*/
|
||||
@ValueDescription("检查间隔(单位:秒)")
|
||||
var checkInterval: Long by value(60 * 60 * 4L)
|
||||
}
|
36
src/main/kotlin/top/jie65535/jcf/PluginData.kt
Normal file
36
src/main/kotlin/top/jie65535/jcf/PluginData.kt
Normal file
@ -0,0 +1,36 @@
|
||||
package top.jie65535.jcf
|
||||
|
||||
import net.mamoe.mirai.console.data.AutoSavePluginData
|
||||
import net.mamoe.mirai.console.data.value
|
||||
import net.mamoe.mirai.console.data.ValueDescription
|
||||
|
||||
object PluginData : AutoSavePluginData("JCurseforgeData") {
|
||||
|
||||
/**
|
||||
* 模组最新文件的id集合
|
||||
* ```json
|
||||
* {
|
||||
* mod_id: file_id,
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@ValueDescription("模组最新文件的id集合")
|
||||
var modsLastFile: MutableMap<Int, Int> by value()
|
||||
|
||||
/**
|
||||
* 订阅记录
|
||||
* ```json
|
||||
* {
|
||||
* mod_id: {
|
||||
* group_id: [ qq_id, ... ],
|
||||
* ...
|
||||
* },
|
||||
* ...
|
||||
* }
|
||||
* ```
|
||||
* 个人订阅时,group为0
|
||||
*/
|
||||
@ValueDescription("订阅记录")
|
||||
var subscriptionSet: MutableMap<Int, MutableMap<Long, MutableList<Long>>> by value()
|
||||
}
|
49
src/main/kotlin/top/jie65535/jcf/PluginMain.kt
Normal file
49
src/main/kotlin/top/jie65535/jcf/PluginMain.kt
Normal file
@ -0,0 +1,49 @@
|
||||
package top.jie65535.jcf
|
||||
|
||||
import kotlinx.coroutines.launch
|
||||
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
|
||||
import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription
|
||||
import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin
|
||||
import net.mamoe.mirai.event.GlobalEventChannel
|
||||
import net.mamoe.mirai.utils.info
|
||||
|
||||
object PluginMain: KotlinPlugin(
|
||||
JvmPluginDescription(
|
||||
id = "top.jie65535.jcf",
|
||||
name = "J Curseforge Util",
|
||||
version = "1.1.0",
|
||||
) {
|
||||
author("jie65535")
|
||||
info("MC Curseforge Util\n" +
|
||||
"https://github.com/jie65535/mirai-console-jcf-plugin")
|
||||
}
|
||||
) {
|
||||
/**
|
||||
* 订阅处理类
|
||||
*/
|
||||
lateinit var subscribeHandler: SubscribeHandler private set
|
||||
|
||||
override fun onEnable() {
|
||||
logger.info { "Plugin loaded" }
|
||||
PluginData.reload()
|
||||
PluginConfig.reload()
|
||||
PluginCommands.register()
|
||||
|
||||
if (PluginConfig.apiKey.isBlank()) {
|
||||
logger.error("必须配置 Curseforge Api Key 才可以使用本插件!\n" +
|
||||
"请使用 /jcf setApiKey <apiKey> 命令来设置key\n" +
|
||||
"Api key 可以在开发者控制台生成:https://console.curseforge.com/")
|
||||
return
|
||||
}
|
||||
val service = MinecraftService(PluginConfig.apiKey)
|
||||
val eventChannel = GlobalEventChannel.parentScope(this)
|
||||
val messageHandler = MessageHandler(service, eventChannel, logger)
|
||||
subscribeHandler = SubscribeHandler(service, logger)
|
||||
messageHandler.startListen()
|
||||
launch {
|
||||
subscribeHandler.load(this)
|
||||
}
|
||||
subscribeHandler.start()
|
||||
logger.info { "Plugin Enabled" }
|
||||
}
|
||||
}
|
316
src/main/kotlin/top/jie65535/jcf/SubscribeHandler.kt
Normal file
316
src/main/kotlin/top/jie65535/jcf/SubscribeHandler.kt
Normal file
@ -0,0 +1,316 @@
|
||||
package top.jie65535.jcf
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.launch
|
||||
import net.mamoe.mirai.Bot
|
||||
import net.mamoe.mirai.message.data.At
|
||||
import net.mamoe.mirai.message.data.buildMessageChain
|
||||
import net.mamoe.mirai.utils.MiraiLogger
|
||||
import top.jie65535.jcf.model.mod.Mod
|
||||
|
||||
/**
|
||||
* 处理订阅
|
||||
*
|
||||
* @param service api服务
|
||||
* @param logger 日志
|
||||
*/
|
||||
class SubscribeHandler(
|
||||
private val service: MinecraftService,
|
||||
private val logger: MiraiLogger,
|
||||
) {
|
||||
|
||||
// region -- 参数
|
||||
|
||||
/**
|
||||
* 清理订阅
|
||||
*
|
||||
* @param mod 模组id;为null将清空所有订阅
|
||||
* @param group 群号;为null将移除指定mod下所有订阅
|
||||
*/
|
||||
fun clean(mod: Int? = null, group: Long? = null) {
|
||||
var updInner = false
|
||||
val modSet = HashMap(PluginData.modsLastFile)
|
||||
val subSet = HashMap(PluginData.subscriptionSet)
|
||||
if (mod == null) {
|
||||
subSet.clear()
|
||||
modSet.clear()
|
||||
logger.info("清理所有订阅")
|
||||
} else if (group == null) {
|
||||
modSet -= mod
|
||||
subSet -= mod
|
||||
logger.info("清理mod[${MOD_INFO_CACHE[mod]?.name}]的订阅")
|
||||
} else {
|
||||
subSet[mod]?.let {
|
||||
logger.info("清理群/分组[$group]的订阅")
|
||||
updInner = true
|
||||
it -= group
|
||||
}
|
||||
}
|
||||
|
||||
if (modSet.size != PluginData.modsLastFile.size) {
|
||||
PluginData.modsLastFile = modSet
|
||||
}
|
||||
if (subSet.size != PluginData.subscriptionSet.size || updInner) {
|
||||
PluginData.subscriptionSet = subSet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*
|
||||
* @param mod 模组id
|
||||
* @param qq 个人q号或群成员q号
|
||||
* @param group 群号;为null时(默认)表示个人订阅
|
||||
*/
|
||||
fun unsub(mod: Int, qq: Long, group: Long? = null) {
|
||||
if (mod < 0 || qq < 0) return
|
||||
|
||||
val gid = group ?: GROUP_ID_SINGLE
|
||||
val subSet = HashMap(PluginData.subscriptionSet)
|
||||
|
||||
val groups = subSet[mod] ?: return
|
||||
val members = groups[gid] ?: return
|
||||
members -= qq
|
||||
PluginData.subscriptionSet = subSet
|
||||
logger.info("取消订阅--{$mod:{$gid:[$qq]}}")
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录订阅
|
||||
*
|
||||
* @param mod 模组id
|
||||
* @param qq q号
|
||||
* @param group 群号;为null时(默认)表示个人订阅
|
||||
*/
|
||||
fun sub(mod: Int, qq: Long, group: Long? = null) {
|
||||
if (mod < 0 || qq < 0) return
|
||||
val gid = group ?: GROUP_ID_SINGLE
|
||||
val modSet = HashMap(PluginData.modsLastFile)
|
||||
val subSet = HashMap(PluginData.subscriptionSet)
|
||||
if (mod !in modSet) modSet[mod] = -1
|
||||
|
||||
val groupSet = subSet[mod] ?: mutableMapOf()
|
||||
val qqSet = groupSet[gid] ?: mutableListOf()
|
||||
var changed = gid !in groupSet
|
||||
subSet[mod] = groupSet
|
||||
groupSet[gid] = qqSet
|
||||
if (qq !in qqSet) {
|
||||
qqSet += qq
|
||||
changed = true
|
||||
logger.info("添加订阅--{$mod:{$gid:[$qq]}}")
|
||||
}
|
||||
|
||||
if (mod !in PluginData.modsLastFile) {
|
||||
PluginData.modsLastFile = modSet
|
||||
}
|
||||
if (changed) {
|
||||
PluginData.subscriptionSet = subSet
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region -- 流程
|
||||
|
||||
/**
|
||||
* 检查更新
|
||||
*
|
||||
* @param init 是否初始化
|
||||
* @return 检查到更新的{ mod : newFileId }
|
||||
*/
|
||||
private suspend fun checkUpdate(init: Boolean = false) = flow {
|
||||
val oldSet = PluginData.modsLastFile
|
||||
if (oldSet.isNotEmpty()) {
|
||||
val fetchMods = service.getMods(oldSet.keys.toIntArray())
|
||||
.asSequence()
|
||||
.map { it.id to it }
|
||||
.toMap()
|
||||
for ((mod, old) in oldSet) {
|
||||
try {
|
||||
val new = fetchMods[mod]
|
||||
if (new == null) {
|
||||
emit(mod to -1)
|
||||
continue
|
||||
}
|
||||
MOD_INFO_CACHE[mod] = new
|
||||
val last = new.latestFilesIndexes[0].fileId
|
||||
if (old != last || init) {
|
||||
emit(mod to last)
|
||||
logger.info("模组更新【${new.name}】")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warning("err msg: ${e.message}")
|
||||
emit(mod to -1)
|
||||
continue
|
||||
}
|
||||
}// for
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取更新日志
|
||||
*
|
||||
* @param mod 模组id
|
||||
* @param file 文件id
|
||||
* @return 更新日志
|
||||
*/
|
||||
private suspend fun getChangeLogs(mod: Int, file: Int): String = try {
|
||||
val changelog = service.getModFileChangelog(mod, file)
|
||||
MessageHandler.HTMLPattern.matcher(changelog)
|
||||
.replaceAll("")
|
||||
.replace(Regex("\n+"), "\n")
|
||||
} catch (e: Exception) {
|
||||
logger.warning("err msg: ${e.message}")
|
||||
""
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行发送
|
||||
*
|
||||
* @param sender 发送消息的bot
|
||||
* @param modLogs { mod : changeLog }
|
||||
*/
|
||||
private suspend fun send(sender: Bot, modLogs: Pair<Int, String>) {
|
||||
val (mod, logs) = modLogs
|
||||
if (logs.isBlank()) return
|
||||
|
||||
val subGroups = PluginData.subscriptionSet[mod] ?: return
|
||||
val modName = MOD_INFO_CACHE[mod]?.name ?: return
|
||||
val title = "你订阅的mod【$modName】更新啦!"
|
||||
val context = "更新日志:\n$logs"
|
||||
subGroups.forEach { (group, qqs) ->
|
||||
if (group == GROUP_ID_SINGLE) {
|
||||
qqs.forEach {
|
||||
sender.getFriend(it)?.apply {
|
||||
sendMessage(title)
|
||||
sendMessage(context)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender.getGroup(group)?.apply {
|
||||
val titleChain = buildMessageChain {
|
||||
qqs.forEach { +At(it) }
|
||||
+"\n$title"
|
||||
}
|
||||
sendMessage(titleChain)
|
||||
sendMessage(context)
|
||||
}
|
||||
}// if else
|
||||
}// foreach
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备发送更新日志
|
||||
*
|
||||
* @param senderQQ 指定发送消息的机器人id
|
||||
* @param updMod { mod : file }
|
||||
*/
|
||||
private suspend fun feedback(senderQQ: Long, updMod: Pair<Int, Int>) {
|
||||
val (mod, file) = updMod
|
||||
if (mod < 0) return
|
||||
|
||||
Bot.instances.firstOrNull {
|
||||
it.isOnline && it.id == senderQQ
|
||||
}?.let { sender ->
|
||||
val log = getChangeLogs(mod, file)
|
||||
send(sender, mod to log)
|
||||
}// let
|
||||
}
|
||||
|
||||
/**
|
||||
* 循环执行
|
||||
*/
|
||||
private fun CoroutineScope.loop() = launch {
|
||||
val senderQQ = PluginConfig.subscribeSender
|
||||
if (senderQQ < 0) {
|
||||
logger.warning("必须配置订阅信息推送bot(qq id)才可以进行订阅推送!")
|
||||
logger.warning("插件会持续收集订阅与检查mod更新,但无法进行消息推送。")
|
||||
}
|
||||
logger.info("subscription listening")
|
||||
while (true) {
|
||||
delay(1000 * PluginConfig.checkInterval)
|
||||
if (isIdle) continue
|
||||
|
||||
val subSet = HashMap(PluginData.subscriptionSet)
|
||||
val modFiles = HashMap(PluginData.modsLastFile)
|
||||
checkUpdate()
|
||||
.buffer()
|
||||
.collect {
|
||||
val (mod, file) = it
|
||||
if (file < 0) {
|
||||
modFiles -= mod
|
||||
subSet -= mod
|
||||
} else {
|
||||
modFiles[mod] = file
|
||||
feedback(senderQQ, it)
|
||||
}
|
||||
}
|
||||
PluginData.subscriptionSet = subSet
|
||||
PluginData.modsLastFile = modFiles
|
||||
}// while
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region -- 状态
|
||||
|
||||
/**
|
||||
* 是否闲置
|
||||
*/
|
||||
var isIdle = true
|
||||
private set
|
||||
|
||||
/**
|
||||
* 取消闲置
|
||||
*/
|
||||
fun start() {
|
||||
isIdle = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入闲置
|
||||
*/
|
||||
fun idle() {
|
||||
isIdle = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化,并开始订阅循环
|
||||
*
|
||||
* @param scope 指定协程上下文
|
||||
*/
|
||||
suspend fun load(scope: CoroutineScope) {
|
||||
logger.info("loading plugin data...")
|
||||
val subs = HashMap(PluginData.subscriptionSet)
|
||||
val files = HashMap(PluginData.modsLastFile)
|
||||
checkUpdate(true)
|
||||
.buffer()
|
||||
.collect { (mod, file) ->
|
||||
if (file < 0) {
|
||||
files -= mod
|
||||
subs -= mod
|
||||
} else {
|
||||
files[mod] = file
|
||||
}
|
||||
}
|
||||
PluginData.subscriptionSet = subs
|
||||
PluginData.modsLastFile = files
|
||||
scope.loop()
|
||||
}
|
||||
// endregion
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* 标识个人订阅
|
||||
*/
|
||||
const val GROUP_ID_SINGLE: Long = 0
|
||||
|
||||
/**
|
||||
* 缓存模组信息
|
||||
*/
|
||||
private val MOD_INFO_CACHE: MutableMap<Int, Mod> = mutableMapOf()
|
||||
}
|
||||
}
|
64
src/main/kotlin/top/jie65535/jcf/model/Category.kt
Normal file
64
src/main/kotlin/top/jie65535/jcf/model/Category.kt
Normal file
@ -0,0 +1,64 @@
|
||||
package top.jie65535.jcf.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import top.jie65535.jcf.util.OffsetDateTimeSerializer
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class Category(
|
||||
/**
|
||||
* The category id
|
||||
*/
|
||||
val id: Int,
|
||||
|
||||
/**
|
||||
* The game id related to the category
|
||||
*/
|
||||
val gameId: Int,
|
||||
|
||||
/**
|
||||
* Category name
|
||||
*/
|
||||
val name: String,
|
||||
|
||||
/**
|
||||
* The category slug as it appear in the URL
|
||||
*/
|
||||
val slug: String?,
|
||||
|
||||
/**
|
||||
* The category URL
|
||||
*/
|
||||
val url: String?,
|
||||
|
||||
/**
|
||||
* URL for the category icon
|
||||
*/
|
||||
val iconUrl: String?,
|
||||
|
||||
/**
|
||||
* Last modified date of the category
|
||||
*/
|
||||
@Serializable(OffsetDateTimeSerializer::class)
|
||||
val dateModified: OffsetDateTime,
|
||||
|
||||
/**
|
||||
* A top level category for other categories
|
||||
*/
|
||||
val isClass: Boolean? = null,
|
||||
|
||||
/**
|
||||
* The class id of the category, meaning - the class of which this category is under
|
||||
*/
|
||||
val classId: Int? = null,
|
||||
|
||||
/**
|
||||
* The parent category for this category
|
||||
*/
|
||||
val parentCategoryId: Int? = null,
|
||||
|
||||
/**
|
||||
* The display index for this category
|
||||
*/
|
||||
val displayIndex: Int? = null
|
||||
)
|
24
src/main/kotlin/top/jie65535/jcf/model/Pagination.kt
Normal file
24
src/main/kotlin/top/jie65535/jcf/model/Pagination.kt
Normal file
@ -0,0 +1,24 @@
|
||||
package top.jie65535.jcf.model
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class Pagination(
|
||||
/**
|
||||
* A zero based index of the first item that is included in the response
|
||||
*/
|
||||
val index: Int,
|
||||
|
||||
/**
|
||||
* The requested number of items to be included in the response
|
||||
*/
|
||||
val pageSize: Int,
|
||||
|
||||
/**
|
||||
* The actual number of items that were included in the response
|
||||
*/
|
||||
val resultCount: Int,
|
||||
|
||||
/**
|
||||
* The total number of items available by the request
|
||||
*/
|
||||
val totalCount: Long,
|
||||
)
|
@ -0,0 +1,34 @@
|
||||
package top.jie65535.jcf.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import top.jie65535.jcf.util.OffsetDateTimeSerializer
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class SortableGameVersion(
|
||||
/**
|
||||
* Original version name (e.g. 1.5b)
|
||||
*/
|
||||
val gameVersionName: String,
|
||||
|
||||
/**
|
||||
* Used for sorting (e.g. 0000000001.0000000005)
|
||||
*/
|
||||
val gameVersionPadded: String,
|
||||
|
||||
/**
|
||||
* game version clean name (e.g. 1.5)
|
||||
*/
|
||||
val gameVersion: String,
|
||||
|
||||
/**
|
||||
* Game version release date
|
||||
*/
|
||||
@Serializable(OffsetDateTimeSerializer::class)
|
||||
val gameVersionReleaseDate: OffsetDateTime,
|
||||
|
||||
/**
|
||||
* Game version type id
|
||||
*/
|
||||
val gameVersionTypeId: Int? = null
|
||||
)
|
103
src/main/kotlin/top/jie65535/jcf/model/file/File.kt
Normal file
103
src/main/kotlin/top/jie65535/jcf/model/file/File.kt
Normal file
@ -0,0 +1,103 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import top.jie65535.jcf.model.SortableGameVersion
|
||||
import top.jie65535.jcf.util.OffsetDateTimeSerializer
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class File(
|
||||
/**
|
||||
* The file id
|
||||
*/
|
||||
val id: Int,
|
||||
/**
|
||||
* The game id related to the mod that this file belongs to
|
||||
*/
|
||||
val gameId: Int,
|
||||
/**
|
||||
* The mod id
|
||||
*/
|
||||
val modId: Int,
|
||||
/**
|
||||
* Whether the file is available to download
|
||||
*/
|
||||
val isAvailable: Boolean,
|
||||
/**
|
||||
* Display name of the file
|
||||
*/
|
||||
val displayName: String,
|
||||
/**
|
||||
* Exact file name
|
||||
*/
|
||||
val fileName: String,
|
||||
/**
|
||||
* The file release type
|
||||
*/
|
||||
val releaseType: FileReleaseType,
|
||||
/**
|
||||
* Status of the file
|
||||
*/
|
||||
val fileStatus: FileStatus,
|
||||
/**
|
||||
* The file hash (i.e. md5 or sha1)
|
||||
*/
|
||||
val hashes: Array<FileHash>,
|
||||
/**
|
||||
* The file timestamp
|
||||
*/
|
||||
@Serializable(OffsetDateTimeSerializer::class)
|
||||
val fileDate: OffsetDateTime,
|
||||
/**
|
||||
* The file length in bytes
|
||||
*/
|
||||
val fileLength: Long,
|
||||
/**
|
||||
* The number of downloads for the file
|
||||
*/
|
||||
val downloadCount: Long,
|
||||
/**
|
||||
* The file download URL
|
||||
*/
|
||||
val downloadUrl: String?,
|
||||
/**
|
||||
* List of game versions this file is relevant for
|
||||
*/
|
||||
val gameVersions: Array<String>,
|
||||
/**
|
||||
* Metadata used for sorting by game versions
|
||||
*/
|
||||
val sortableGameVersions: Array<SortableGameVersion>,
|
||||
/**
|
||||
* List of dependencies files
|
||||
*/
|
||||
val dependencies: Array<FileDependency>,
|
||||
/**
|
||||
* none
|
||||
*/
|
||||
val exposeAsAlternative: Boolean? = null,
|
||||
/**
|
||||
* none
|
||||
*/
|
||||
val parentProjectFileId: Int? = null,
|
||||
/**
|
||||
* none
|
||||
*/
|
||||
val alternateFileId: Int? = null,
|
||||
/**
|
||||
* none
|
||||
*/
|
||||
val isServerPack: Boolean? = null,
|
||||
/**
|
||||
* none
|
||||
*/
|
||||
val serverPackFileId: Int? = null,
|
||||
/**
|
||||
* none
|
||||
*/
|
||||
val fileFingerprint: Long,
|
||||
/**
|
||||
* none
|
||||
*/
|
||||
val modules: Array<FileModule>,
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class FileDependency(
|
||||
val modId: Int,
|
||||
val relationType: FileRelationType,
|
||||
)
|
7
src/main/kotlin/top/jie65535/jcf/model/file/FileHash.kt
Normal file
7
src/main/kotlin/top/jie65535/jcf/model/file/FileHash.kt
Normal file
@ -0,0 +1,7 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class FileHash(
|
||||
val value: String,
|
||||
val algo: HashAlgo,
|
||||
)
|
13
src/main/kotlin/top/jie65535/jcf/model/file/FileIndex.kt
Normal file
13
src/main/kotlin/top/jie65535/jcf/model/file/FileIndex.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
import top.jie65535.jcf.model.mod.ModLoaderType
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class FileIndex(
|
||||
val gameVersion: String,
|
||||
val fileId: Int,
|
||||
val filename: String,
|
||||
val releaseType: FileReleaseType,
|
||||
val gameVersionTypeId: Int? = null,
|
||||
val modLoader: ModLoaderType? = null,
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class FileModule(
|
||||
val name: String,
|
||||
val fingerprint: Long,
|
||||
)
|
@ -0,0 +1,16 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = FileRelationType.IndexSerializer::class)
|
||||
enum class FileRelationType {
|
||||
EmbeddedLibrary,
|
||||
OptionalDependency,
|
||||
RequiredDependency,
|
||||
Tool,
|
||||
Incompatible,
|
||||
Include;
|
||||
|
||||
internal object IndexSerializer : KSerializer<FileRelationType> by EnumIndexSerializer(values())
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = FileReleaseType.IndexSerializer::class)
|
||||
enum class FileReleaseType {
|
||||
Release,
|
||||
Beta,
|
||||
Alpha;
|
||||
|
||||
internal object IndexSerializer : KSerializer<FileReleaseType> by EnumIndexSerializer(values())
|
||||
}
|
25
src/main/kotlin/top/jie65535/jcf/model/file/FileStatus.kt
Normal file
25
src/main/kotlin/top/jie65535/jcf/model/file/FileStatus.kt
Normal file
@ -0,0 +1,25 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = FileStatus.IndexSerializer::class)
|
||||
enum class FileStatus{
|
||||
Processing,
|
||||
ChangesRequired,
|
||||
UnderReview,
|
||||
Approved,
|
||||
Rejected,
|
||||
MalwareDetected,
|
||||
Deleted,
|
||||
Archived,
|
||||
Testing,
|
||||
Released,
|
||||
ReadyForReview,
|
||||
Deprecated,
|
||||
Baking,
|
||||
AwaitingPublishing,
|
||||
FailedPublishing;
|
||||
|
||||
internal object IndexSerializer : KSerializer<FileStatus> by EnumIndexSerializer(values())
|
||||
}
|
12
src/main/kotlin/top/jie65535/jcf/model/file/HashAlgo.kt
Normal file
12
src/main/kotlin/top/jie65535/jcf/model/file/HashAlgo.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package top.jie65535.jcf.model.file
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = HashAlgo.IndexSerializer::class)
|
||||
enum class HashAlgo(val value: Int) {
|
||||
Sha1(1),
|
||||
Md5(2);
|
||||
|
||||
internal object IndexSerializer : KSerializer<HashAlgo> by EnumIndexSerializer(values())
|
||||
}
|
12
src/main/kotlin/top/jie65535/jcf/model/game/CoreApiStatus.kt
Normal file
12
src/main/kotlin/top/jie65535/jcf/model/game/CoreApiStatus.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package top.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = CoreApiStatus.IndexSerializer::class)
|
||||
enum class CoreApiStatus {
|
||||
Private,
|
||||
Public;
|
||||
|
||||
internal object IndexSerializer : KSerializer<CoreApiStatus> by EnumIndexSerializer(values())
|
||||
}
|
16
src/main/kotlin/top/jie65535/jcf/model/game/CoreStatus.kt
Normal file
16
src/main/kotlin/top/jie65535/jcf/model/game/CoreStatus.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package top.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = CoreStatus.IndexSerializer::class)
|
||||
enum class CoreStatus {
|
||||
Draft,
|
||||
Test,
|
||||
PendingReview,
|
||||
Rejected,
|
||||
Approved,
|
||||
Live;
|
||||
|
||||
internal object IndexSerializer : KSerializer<CoreStatus> by EnumIndexSerializer(values())
|
||||
}
|
17
src/main/kotlin/top/jie65535/jcf/model/game/Game.kt
Normal file
17
src/main/kotlin/top/jie65535/jcf/model/game/Game.kt
Normal file
@ -0,0 +1,17 @@
|
||||
package top.jie65535.jcf.model.game
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import top.jie65535.jcf.util.OffsetDateTimeSerializer
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class Game(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val slug: String?,
|
||||
@Serializable(OffsetDateTimeSerializer::class)
|
||||
val dateModified: OffsetDateTime,
|
||||
val assets: GameAssets,
|
||||
val status: CoreStatus,
|
||||
val apiStatus: CoreApiStatus,
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
package top.jie65535.jcf.model.game
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GameAssets(
|
||||
val iconUrl: String?,
|
||||
val tileUrl: String?,
|
||||
val coverUrl: String?,
|
||||
)
|
@ -0,0 +1,9 @@
|
||||
package top.jie65535.jcf.model.game
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GameVersionType(
|
||||
val id: Int? = null,
|
||||
val gameId: Int? = null,
|
||||
val name: String? = null,
|
||||
val slug: String? = null,
|
||||
)
|
140
src/main/kotlin/top/jie65535/jcf/model/mod/Mod.kt
Normal file
140
src/main/kotlin/top/jie65535/jcf/model/mod/Mod.kt
Normal file
@ -0,0 +1,140 @@
|
||||
package top.jie65535.jcf.model.mod
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import top.jie65535.jcf.model.Category
|
||||
import top.jie65535.jcf.model.file.File
|
||||
import top.jie65535.jcf.model.file.FileIndex
|
||||
import top.jie65535.jcf.util.OffsetDateTimeSerializer
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Serializable
|
||||
class Mod(
|
||||
/**
|
||||
* The mod id
|
||||
*/
|
||||
val id: Int,
|
||||
|
||||
/**
|
||||
* The game id this mod is for
|
||||
*/
|
||||
val gameId: Int,
|
||||
|
||||
/**
|
||||
* The name of the mod
|
||||
*/
|
||||
val name: String,
|
||||
|
||||
/**
|
||||
* The mod slug that would appear in the URL
|
||||
*/
|
||||
val slug: String?,
|
||||
|
||||
/**
|
||||
* Relevant links for the mod such as Issue tracker and Wiki
|
||||
*/
|
||||
val links: ModLinks,
|
||||
|
||||
/**
|
||||
* Mod summary
|
||||
*/
|
||||
val summary: String,
|
||||
|
||||
/**
|
||||
* Current mod status
|
||||
*/
|
||||
val status: ModStatus,
|
||||
|
||||
/**
|
||||
* Number of downloads for the mod
|
||||
*/
|
||||
val downloadCount: Long,
|
||||
|
||||
/**
|
||||
* Whether the mod is included in the featured mods list
|
||||
*/
|
||||
val isFeatured: Boolean,
|
||||
|
||||
/**
|
||||
* The main category of the mod as it was chosen by the mod author
|
||||
*/
|
||||
val primaryCategoryId: Int,
|
||||
|
||||
/**
|
||||
* List of categories that this mod is related to
|
||||
*/
|
||||
val categories: Array<Category>,
|
||||
|
||||
/**
|
||||
* The class id this mod belongs to
|
||||
*/
|
||||
val classId: Int? = null,
|
||||
|
||||
/**
|
||||
* List of the mod's authors
|
||||
*/
|
||||
val authors: Array<ModAuthor>,
|
||||
|
||||
/**
|
||||
* The mod's logo asset
|
||||
*/
|
||||
val logo: ModAsset?,
|
||||
|
||||
/**
|
||||
* List of screenshots assets
|
||||
*/
|
||||
val screenshots: Array<ModAsset>,
|
||||
|
||||
/**
|
||||
* The id of the main file of the mod
|
||||
*/
|
||||
val mainFileId: Int,
|
||||
|
||||
/**
|
||||
* List of latest files of the mod
|
||||
*/
|
||||
val latestFiles: Array<File>,
|
||||
|
||||
/**
|
||||
* List of file related details for the latest files of the mod
|
||||
*/
|
||||
val latestFilesIndexes: Array<FileIndex>,
|
||||
|
||||
/**
|
||||
* The creation date of the mod
|
||||
*/
|
||||
@Serializable(OffsetDateTimeSerializer::class)
|
||||
val dateCreated: OffsetDateTime,
|
||||
|
||||
/**
|
||||
* The last time the mod was modified
|
||||
*/
|
||||
@Serializable(OffsetDateTimeSerializer::class)
|
||||
val dateModified: OffsetDateTime,
|
||||
|
||||
/**
|
||||
* The release date of the mod
|
||||
*/
|
||||
@Serializable(OffsetDateTimeSerializer::class)
|
||||
val dateReleased: OffsetDateTime,
|
||||
|
||||
/**
|
||||
* Is mod allowed to be distributed
|
||||
*/
|
||||
val allowModDistribution: Boolean? = null,
|
||||
|
||||
/**
|
||||
* The mod popularity rank for the game
|
||||
*/
|
||||
val gamePopularityRank: Int,
|
||||
|
||||
/**
|
||||
* Is the mod available for search. This can be false when a mod is experimental,
|
||||
* in a deleted state or has only alpha files
|
||||
*/
|
||||
val isAvailable: Boolean,
|
||||
|
||||
/**
|
||||
* The mod's thumbs up count
|
||||
*/
|
||||
val thumbsUpCount: Int,
|
||||
)
|
11
src/main/kotlin/top/jie65535/jcf/model/mod/ModAsset.kt
Normal file
11
src/main/kotlin/top/jie65535/jcf/model/mod/ModAsset.kt
Normal file
@ -0,0 +1,11 @@
|
||||
package top.jie65535.jcf.model.mod
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class ModAsset(
|
||||
val id: Int,
|
||||
val modId: Int,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val thumbnailUrl: String?,
|
||||
val url: String?,
|
||||
)
|
8
src/main/kotlin/top/jie65535/jcf/model/mod/ModAuthor.kt
Normal file
8
src/main/kotlin/top/jie65535/jcf/model/mod/ModAuthor.kt
Normal file
@ -0,0 +1,8 @@
|
||||
package top.jie65535.jcf.model.mod
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class ModAuthor(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val url: String?,
|
||||
)
|
9
src/main/kotlin/top/jie65535/jcf/model/mod/ModLinks.kt
Normal file
9
src/main/kotlin/top/jie65535/jcf/model/mod/ModLinks.kt
Normal file
@ -0,0 +1,9 @@
|
||||
package top.jie65535.jcf.model.mod
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class ModLinks(
|
||||
val websiteUrl: String?,
|
||||
val wikiUrl: String?,
|
||||
val issuesUrl: String?,
|
||||
val sourceUrl: String?
|
||||
)
|
16
src/main/kotlin/top/jie65535/jcf/model/mod/ModLoaderType.kt
Normal file
16
src/main/kotlin/top/jie65535/jcf/model/mod/ModLoaderType.kt
Normal file
@ -0,0 +1,16 @@
|
||||
package top.jie65535.jcf.model.mod
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = ModLoaderType.IndexSerializer::class)
|
||||
enum class ModLoaderType {
|
||||
Any,
|
||||
Forge,
|
||||
Cauldron,
|
||||
LiteLoader,
|
||||
Fabric,
|
||||
Quilt;
|
||||
|
||||
internal object IndexSerializer : KSerializer<ModLoaderType> by EnumIndexSerializer(values())
|
||||
}
|
20
src/main/kotlin/top/jie65535/jcf/model/mod/ModStatus.kt
Normal file
20
src/main/kotlin/top/jie65535/jcf/model/mod/ModStatus.kt
Normal file
@ -0,0 +1,20 @@
|
||||
package top.jie65535.jcf.model.mod
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = ModStatus.IndexSerializer::class)
|
||||
enum class ModStatus {
|
||||
New,
|
||||
ChangesRequired,
|
||||
UnderSoftReview,
|
||||
Approved,
|
||||
Rejected,
|
||||
ChangesMade,
|
||||
Inactive,
|
||||
Abandoned,
|
||||
Deleted,
|
||||
UnderReview;
|
||||
|
||||
internal object IndexSerializer : KSerializer<ModStatus> by EnumIndexSerializer(values())
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
package top.jie65535.jcf.model.request
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GetFeaturedModsRequestBody(
|
||||
val gameId: Int,
|
||||
val excludedModIds: IntArray,
|
||||
val gameVersionTypeId: Int? = null,
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package top.jie65535.jcf.model.request
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GetModFilesRequestBody(
|
||||
val fileIds: IntArray
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package top.jie65535.jcf.model.request
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GetModsByIdsListRequestBody(
|
||||
val modIds: IntArray
|
||||
)
|
@ -0,0 +1,18 @@
|
||||
package top.jie65535.jcf.model.request
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import top.jie65535.jcf.util.EnumIndexSerializer
|
||||
|
||||
@kotlinx.serialization.Serializable(with = ModsSearchSortField.IndexSerializer::class)
|
||||
enum class ModsSearchSortField {
|
||||
Featured,
|
||||
Popularity,
|
||||
LastUpdated,
|
||||
Name,
|
||||
Author,
|
||||
TotalDownloads,
|
||||
Category,
|
||||
GameVersion;
|
||||
|
||||
internal object IndexSerializer : KSerializer<ModsSearchSortField> by EnumIndexSerializer(values())
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package top.jie65535.jcf.model.request
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
enum class SortOrder {
|
||||
ASC,
|
||||
DESC,
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
package top.jie65535.jcf.model.response
|
||||
|
||||
import top.jie65535.jcf.model.mod.Mod
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class FeaturedModsResponse(
|
||||
val featured: Array<Mod>,
|
||||
val popular: Array<Mod>,
|
||||
val recentlyUpdated: Array<Mod>,
|
||||
)
|
@ -0,0 +1,7 @@
|
||||
package top.jie65535.jcf.model.response
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GameVersionsByType(
|
||||
val type: Int,
|
||||
val versions: Array<String>
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
package top.jie65535.jcf.model.response
|
||||
|
||||
import top.jie65535.jcf.model.Category
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GetCategoriesResponse(
|
||||
val data: Array<Category>
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
package top.jie65535.jcf.model.response
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GetFeaturedModsResponse(
|
||||
val data: FeaturedModsResponse
|
||||
)
|
@ -0,0 +1,8 @@
|
||||
package top.jie65535.jcf.model.response
|
||||
|
||||
import top.jie65535.jcf.model.file.File
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class GetFilesResponse(
|
||||
val data: Array<File>
|
||||
)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user