diff --git a/build.gradle.kts b/build.gradle.kts index 1ecf27f..47c7ee8 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,6 +23,7 @@ repositories { dependencies { compileOnly("org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT") compileOnly("com.github.MilkBowl:VaultAPI:1.7") + implementation("com.github.Zrips:CMI-API:9.7.14.3") // These will be packaged into the shadow JAR implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") diff --git a/src/main/kotlin/dev/solsynth/snConnect/SolarNetworkConnect.kt b/src/main/kotlin/dev/solsynth/snConnect/SolarNetworkConnect.kt index 2c8474a..e377ecf 100644 --- a/src/main/kotlin/dev/solsynth/snConnect/SolarNetworkConnect.kt +++ b/src/main/kotlin/dev/solsynth/snConnect/SolarNetworkConnect.kt @@ -5,6 +5,9 @@ import dev.solsynth.snConnect.commands.BedrockRemoverCompleter import dev.solsynth.snConnect.commands.SnCommand import dev.solsynth.snConnect.commands.SnCommandCompleter import dev.solsynth.snConnect.listeners.SnChatListener +import dev.solsynth.snConnect.listeners.SolarpassCheckListener +import dev.solsynth.snConnect.services.AuthService +import dev.solsynth.snConnect.services.AuthUserService import dev.solsynth.snConnect.models.SnChatMessage import dev.solsynth.snConnect.models.WebSocketPacket import dev.solsynth.snConnect.services.SnMessageService @@ -18,17 +21,22 @@ import net.md_5.bungee.api.chat.TextComponent import net.milkbowl.vault.economy.Economy import org.bukkit.Bukkit import org.bukkit.Bukkit.getOnlinePlayers +import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.plugin.java.JavaPlugin +import java.io.InputStreamReader class SolarNetworkConnect : JavaPlugin() { private var economy: Economy? = null private var sn: SnService? = null + private var authService: AuthService? = null + private var authUserService: AuthUserService? = null private var messageService: SnMessageService? = null private var syncChatRooms: List = emptyList() private var destinationChatId: String? = null private var webSocketJob: Job? = null private var messages: Map = mapOf() + private var playerSolarpassMap: MutableMap = mutableMapOf() private fun handleWebSocketPacket(packet: WebSocketPacket) { logger.info("Received WebSocket packet: type=${packet.type}") @@ -82,15 +90,13 @@ class SolarNetworkConnect : JavaPlugin() { this.saveDefaultConfig() - messages = mapOf( - "join" to "➡️ {player} joined the game.", - "joinFirst" to "➡️ {player} first time joined the game.", - "quit" to "⬅️ {player} left the game.", - "death" to "💀 {player} {message}", - "advancement" to "🎉 {player} unlocked advancement: {advancement}", - "serverStart" to "🚀 Server started successfully", - "serverStop" to "⏹️ Server stopped" - ) + (config.getConfigurationSection("messages")?.getValues(false) as? Map ?: emptyMap()) + val locale = config.getString("locale") ?: "en" + val messageFile = if (locale == "en") "messages.yml" else "messages_$locale.yml" + val messagesStream = getResource(messageFile) ?: throw RuntimeException("$messageFile not found in JAR") + val messagesConfig = YamlConfiguration.loadConfiguration(InputStreamReader(messagesStream, "UTF-8")) + messages = messagesConfig.getValues(false) + .mapValues { it.value as String } + (config.getConfigurationSection("messages") + ?.getValues(false) as? Map ?: emptyMap()) if (!setupNetwork()) { logger.severe("Failed to setup Solar Network Network, check your configuration.") @@ -105,7 +111,14 @@ class SolarNetworkConnect : JavaPlugin() { server.pluginManager.registerEvents(SnChatListener(messageService!!, destinationChatId!!, messages), this) } - Bukkit.getPluginCommand("solar")!!.setExecutor(SnCommand(this.sn!!, this.economy)) + if (authUserService != null) { + server.pluginManager.registerEvents( + SolarpassCheckListener(authUserService!!, messages, playerSolarpassMap), + this + ) + } + + Bukkit.getPluginCommand("solar")!!.setExecutor(SnCommand(this.sn!!, this.economy, messages, playerSolarpassMap)) Bukkit.getPluginCommand("solar")!!.tabCompleter = SnCommandCompleter() Bukkit.getPluginCommand("eatrock")!!.setExecutor(BedrockRemoverCommand(this.economy)) @@ -120,7 +133,7 @@ class SolarNetworkConnect : JavaPlugin() { // Send server start message destinationChatId?.let { chatId -> - messageService?.sendMessage(chatId, messages["serverStart"] ?: "🚀 Server started successfully") + messageService?.sendMessage(chatId, messages["server_start"] ?: "🚀 Server started successfully") } } @@ -129,7 +142,7 @@ class SolarNetworkConnect : JavaPlugin() { // Send server stop message destinationChatId?.let { chatId -> - messageService?.sendMessage(chatId, messages["serverStop"] ?: "⏹️ Server stopped") + messageService?.sendMessage(chatId, messages["server_stop"] ?: "⏹️ Server stopped") } sn?.disconnect() @@ -147,6 +160,12 @@ class SolarNetworkConnect : JavaPlugin() { destinationChatId = destination sn = SnService(baseUrl, clientId, clientSecret, botApiKey); messageService = SnMessageService(sn!!) + + val authUrl = config.getString("auth.endpoint") ?: return false; + val siteSecret = config.getString("auth.site_secret") + authService = AuthService(authUrl, siteSecret); + authUserService = AuthUserService(authService!!) + webSocketJob = GlobalScope.launch { sn!!.connectWebSocketAsFlow().collect { packet -> handleWebSocketPacket(packet) diff --git a/src/main/kotlin/dev/solsynth/snConnect/commands/SnCommand.kt b/src/main/kotlin/dev/solsynth/snConnect/commands/SnCommand.kt index 88aadd5..2564f78 100644 --- a/src/main/kotlin/dev/solsynth/snConnect/commands/SnCommand.kt +++ b/src/main/kotlin/dev/solsynth/snConnect/commands/SnCommand.kt @@ -13,7 +13,8 @@ import org.bukkit.command.CommandSender import org.bukkit.command.TabCompleter import org.bukkit.entity.Player -class SnCommand(private val sn: SnService, private val eco: Economy?) : CommandExecutor { +class SnCommand(private val sn: SnService, private val eco: Economy?, private val messages: Map, private val playerSolarpassMap: MutableMap) : + CommandExecutor { override fun onCommand(p0: CommandSender, p1: Command, p2: String, p3: Array): Boolean { if (p0 !is Player) { return false; @@ -24,7 +25,10 @@ class SnCommand(private val sn: SnService, private val eco: Economy?) : CommandE when (p3[0].lowercase()) { "deposit" -> { if (p3.size < 2) { - p0.sendMessage(ChatColor.RED.toString() + "You need to specific an amount to deposit.") + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_deposit_no_amount"] + ?: "You need to specify an amount to deposit.") + ) return true; } @@ -32,35 +36,43 @@ class SnCommand(private val sn: SnService, private val eco: Economy?) : CommandE if (p3[1].equals("confirm", ignoreCase = true) && p3.size >= 3) { // Confirming order - val orderNumber = p3[2].toLongOrNull(); - if (orderNumber == null) { - p0.sendMessage(ChatColor.RED.toString() + "Invalid order number, it must be a number."); - return true; - } - - p0.sendMessage(ChatColor.GRAY.toString() + "Confirming payment, please stand by..."); + val orderNumber = p3[2]; + p0.sendMessage( + ChatColor.GRAY.toString() + (messages["command_deposit_confirming"] + ?: "Confirming payment, please stand by...") + ); val order: SnOrder try { order = orderSrv.getOrder(orderNumber); - orderSrv.cancelOrder(orderNumber); + orderSrv.updateOrderStatus(orderNumber, false); } catch (_: Exception) { - p0.sendMessage(ChatColor.RED.toString() + "An error occurred while pulling transaction. Make sure the order is exists then try again later.") + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_deposit_pull_error"] + ?: "An error occurred while pulling transaction. Make sure the order exists then try again later.") + ) return true; } if (order.status != 1L) { - p0.sendMessage(ChatColor.RED.toString() + "Order was not paid yet."); + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_deposit_order_not_paid"] + ?: "Order was not paid yet.") + ) return true; } val bal = order.amount.toDouble() * 100; eco?.depositPlayer(p0.player, bal) - val doneComponent = TextComponent(ChatColor.GREEN.toString() + "Done!") + val doneComponent = + TextComponent(ChatColor.GREEN.toString() + (messages["command_deposit_done"] ?: "Done!")) val moneyComponent = TextComponent(ChatColor.GOLD.toString() + ChatColor.BOLD + " $bal$") val suffixComponent = - TextComponent(ChatColor.GREEN.toString() + " has been added to your balance.") + TextComponent( + ChatColor.GREEN.toString() + (messages["command_deposit_added_balance"] + ?: " has been added to your balance.") + ) p0.playSound(p0.player!!, Sound.BLOCK_ANVIL_PLACE, 1.0F, 1.0F) p0.spigot().sendMessage(doneComponent, moneyComponent, suffixComponent) @@ -71,32 +83,52 @@ class SnCommand(private val sn: SnService, private val eco: Economy?) : CommandE // Creating new order val amount = p3[1].toDoubleOrNull(); if (amount == null) { - p0.sendMessage(ChatColor.RED.toString() + "You need to specific an amount of number to deposit.") + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_deposit_invalid_amount"] + ?: "You need to specify an amount as a number to deposit.") + ) return true; } - p0.sendMessage(ChatColor.GRAY.toString() + "Creating order, please stand by..."); + p0.sendMessage( + ChatColor.GRAY.toString() + (messages["command_deposit_creating"] + ?: "Creating order, please stand by...") + ); val order: SnOrder try { order = orderSrv.createOrder("Deposit to Highland MC", amount / 100); } catch (_: Exception) { - p0.sendMessage(ChatColor.RED.toString() + "An error occurred while creating order. Try again later.") + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_deposit_create_error"] + ?: "An error occurred while creating order. Try again later.") + ) return true; } val linkComponent = - TextComponent(ChatColor.GOLD.toString() + ChatColor.UNDERLINE.toString() + ChatColor.BOLD.toString() + "click here") + TextComponent( + ChatColor.GOLD.toString() + ChatColor.UNDERLINE.toString() + ChatColor.BOLD.toString() + (messages["command_deposit_click_here"] + ?: "click here") + ) linkComponent.clickEvent = - ClickEvent(ClickEvent.Action.OPEN_URL, "https://solsynth.dev/orders/${order.id}"); + ClickEvent(ClickEvent.Action.OPEN_URL, "https://solian.app/orders/${order.id}"); val orderHintComponent = - TextComponent(ChatColor.GRAY.toString() + "Order created, number " + ChatColor.WHITE + ChatColor.BOLD + "#${order.id}") - val followingComponent = TextComponent(ChatColor.GRAY.toString() + " to the payment page.") + TextComponent( + ChatColor.GRAY.toString() + (messages["command_deposit_order_created"] + ?: "Order created, number ") + ChatColor.WHITE + ChatColor.BOLD + "#${order.id}" + ) + val followingComponent = TextComponent( + ChatColor.GRAY.toString() + (messages["command_deposit_to_payment_page"] ?: " to the payment page.") + ) p0.spigot() .sendMessage(orderHintComponent, linkComponent, followingComponent); val afterPaidComponent = - TextComponent(ChatColor.UNDERLINE.toString() + ChatColor.YELLOW + "After you paid, click here to confirm payment.") + TextComponent( + ChatColor.UNDERLINE.toString() + ChatColor.YELLOW + (messages["command_deposit_after_paid"] + ?: "After you paid, click here to confirm payment.") + ) afterPaidComponent.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, "/sn deposit confirm ${order.id}") p0.spigot().sendMessage(afterPaidComponent); @@ -104,47 +136,67 @@ class SnCommand(private val sn: SnService, private val eco: Economy?) : CommandE "withdraw" -> { if (p3.size < 2) { - p0.sendMessage(ChatColor.RED.toString() + "You need to specific an amount to deposit.") + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_deposit_no_amount"] + ?: "You need to specify an amount to deposit.") + ) return true; } val amount = p3[1].toDoubleOrNull(); if (amount == null) { - p0.sendMessage(ChatColor.RED.toString() + "You need to specific an amount of number to deposit.") + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_deposit_invalid_amount"] + ?: "You need to specify an amount as a number to deposit.") + ) return true; } - if (p3.size < 3) { - p0.sendMessage(ChatColor.RED.toString() + "You need to specific a wallet account to deposit.") + val playerUuid = p0.uniqueId.toString() + val accountId = playerSolarpassMap[playerUuid] + if (accountId == null) { + p0.sendMessage( + ChatColor.RED.toString() + "Bind your Solarpass first!" + ) return true; } - val walletId = p3[2].toLongOrNull(); - if (walletId == null) { - p0.sendMessage(ChatColor.RED.toString() + "You need to specific a wallet account to deposit.") - return true; - } - - p0.sendMessage(ChatColor.GRAY.toString() + "Making transaction, please stand by..."); + p0.sendMessage( + ChatColor.GRAY.toString() + (messages["command_withdraw_making"] + ?: "Making transaction, please stand by...") + ); val bal = amount / 100; val resp = eco?.withdrawPlayer(p0.player, "Withdraw to Source Point - $bal SRC", amount); if (resp?.type != EconomyResponse.ResponseType.SUCCESS) { - p0.sendMessage(ChatColor.RED.toString() + "Your in game account has no enough money for that.") + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_withdraw_no_money"] + ?: "Your in-game account does not have enough money for that.") + ) return true; } try { - val transactionSrv = SnTransactionService(sn); - val transaction = transactionSrv.makeTransaction(bal, "Withdraw from Highland MC", walletId); + val transactionSrv = SnBalanceService(sn); + val transaction = transactionSrv.addBalance(bal, "Withdraw from GoatCraft", accountId); val transactionHintComponent = - TextComponent(ChatColor.GREEN.toString() + "Done! transaction number " + ChatColor.WHITE + ChatColor.BOLD + "#${transaction.id}") + TextComponent( + ChatColor.GREEN.toString() + (messages["command_withdraw_done"] + ?: "Done! transaction number ") + ChatColor.WHITE + ChatColor.BOLD + "#${transaction.id}" + ) p0.playSound(p0.player!!, Sound.BLOCK_ANVIL_PLACE, 1.0F, 1.0F) p0.spigot().sendMessage(transactionHintComponent) } catch (_: Exception) { - eco?.depositPlayer(p0.player, "Withdraw to Source Point Failed - Refund", amount) - p0.sendMessage(ChatColor.RED.toString() + "An error occurred while making transaction. Make sure your wallet is exists then try again later.") + eco?.depositPlayer( + p0.player, + (messages["command_withdraw_refund_reason"] ?: "Withdraw to Source Point Failed - Refund"), + amount + ) + p0.sendMessage( + ChatColor.RED.toString() + (messages["command_withdraw_error"] + ?: "An error occurred while making transaction. Make sure your wallet exists then try again later.") + ) return true } @@ -172,4 +224,4 @@ class SnCommandCompleter : TabCompleter { else -> mutableListOf(); } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/solsynth/snConnect/listeners/SnChatListener.kt b/src/main/kotlin/dev/solsynth/snConnect/listeners/SnChatListener.kt index 4db6f59..b2e04f0 100644 --- a/src/main/kotlin/dev/solsynth/snConnect/listeners/SnChatListener.kt +++ b/src/main/kotlin/dev/solsynth/snConnect/listeners/SnChatListener.kt @@ -1,5 +1,6 @@ package dev.solsynth.snConnect.listeners +import com.Zrips.CMI.CMI import dev.solsynth.snConnect.services.SnMessageService import org.bukkit.Bukkit import org.bukkit.entity.Player @@ -26,7 +27,8 @@ class SnChatListener( @Suppress("SENSELESS_COMPARISON") @EventHandler() fun onPlayerJoin(event: PlayerJoinEvent) { - val firstTime = Bukkit.getOfflinePlayer(event.player.uniqueId) == null; + val user = CMI.getInstance().playerManager.getUser(event.player); + val firstTime = user.playerTime == 0L; val templateKey = if (!firstTime) "join" else "joinFirst"; val template = messages[templateKey] ?: if (!firstTime) "➡️ {player} joined the game." else "➡️ {player} first time joined the game." diff --git a/src/main/kotlin/dev/solsynth/snConnect/listeners/SolarpassCheckListener.kt b/src/main/kotlin/dev/solsynth/snConnect/listeners/SolarpassCheckListener.kt new file mode 100644 index 0000000..b152c97 --- /dev/null +++ b/src/main/kotlin/dev/solsynth/snConnect/listeners/SolarpassCheckListener.kt @@ -0,0 +1,37 @@ +package dev.solsynth.snConnect.listeners + +import dev.solsynth.snConnect.services.AuthUserService +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import org.bukkit.ChatColor +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.player.PlayerJoinEvent + +class SolarpassCheckListener( + private val authUserService: AuthUserService, + private val messages: Map, + private val playerSolarpassMap: MutableMap +) : Listener { + + @EventHandler() + fun onPlayerJoin(event: PlayerJoinEvent) { + GlobalScope.launch { + try { + val playerUuid = event.player.uniqueId.toString() + var solarpassId = playerSolarpassMap[playerUuid] + if (solarpassId == null) { + solarpassId = authUserService.getSnByPlayer(playerUuid) + playerSolarpassMap[playerUuid] = solarpassId + } + if (solarpassId == null) { + // Send suggestion message + val message = messages["solarpass_bind_suggestion"] ?: "${ChatColor.YELLOW}To get more features, please bind your Solarpass account!" + event.player.sendMessage(message) + } + } catch (e: Exception) { + // Optionally log the error or handle it + } + } + } +} diff --git a/src/main/kotlin/dev/solsynth/snConnect/services/AuthService.kt b/src/main/kotlin/dev/solsynth/snConnect/services/AuthService.kt new file mode 100644 index 0000000..9a5f2bb --- /dev/null +++ b/src/main/kotlin/dev/solsynth/snConnect/services/AuthService.kt @@ -0,0 +1,13 @@ +package dev.solsynth.snConnect.services + +import okhttp3.OkHttpClient +import java.util.logging.Logger + +class AuthService(private val baseUrl: String, val siteSecret: String?) { + val client = OkHttpClient.Builder().build(); + private val logger = Logger.getLogger(AuthService::class.java.name) + + fun getUrl(segment: String): String { + return "$baseUrl$segment" + } +} diff --git a/src/main/kotlin/dev/solsynth/snConnect/services/AuthUserService.kt b/src/main/kotlin/dev/solsynth/snConnect/services/AuthUserService.kt new file mode 100644 index 0000000..4405fac --- /dev/null +++ b/src/main/kotlin/dev/solsynth/snConnect/services/AuthUserService.kt @@ -0,0 +1,132 @@ +package dev.solsynth.snConnect.services + +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.Request +import java.io.IOException + +@Serializable +data class DraslPlayer( + @SerialName("userUuid") + val userUuid: String, + val name: String, + val uuid: String, + @SerialName("offlineUuid") + val offlineUuid: String, + @SerialName("fallbackPlayer") + val fallbackPlayer: String, + @SerialName("skinModel") + val skinModel: String, + @SerialName("skinUrl") + val skinUrl: String?, + @SerialName("capeUrl") + val capeUrl: String?, + @SerialName("createdAt") + val createdAt: String, // ISO string + @SerialName("nameLastChangedAt") + val nameLastChangedAt: String +) + +@Serializable +data class DraslOIDCIdentity( + @SerialName("userUuid") + val userUuid: String, + @SerialName("oidcProviderName") + val oidcProviderName: String, + val issuer: String, + val subject: String +) + +@Serializable +data class DraslUser( + @SerialName("isAdmin") + val isAdmin: Boolean, + @SerialName("isLocked") + val isLocked: Boolean, + val uuid: String, + val username: String, + @SerialName("preferredLanguage") + val preferredLanguage: String, + @SerialName("maxPlayerCount") + val maxPlayerCount: Int, + val players: List, + @SerialName("oidcIdentities") + val oidcIdentities: List +) + +@Serializable +data class APILoginRequest( + val username: String, + val password: String +) + +@Serializable +data class APILoginResponse( + val user: DraslUser, + val token: String, + @SerialName("expires_at") + val expiresAt: Instant +) + +@Serializable +data class APIOIDCTokenRequest( + @SerialName("provider") + val provider: String, + val code: String, + val state: String +) + +@Serializable +data class APIOIDCTokenResponse( + val user: DraslUser, + val token: String, + @SerialName("expires_at") + val expiresAt: Instant +) + +class AuthUserService(private val sn: AuthService) { + private val json = Json { + ignoreUnknownKeys = true + } + + private fun addAuthHeader(builder: Request.Builder): Request.Builder { + sn.siteSecret?.let { builder.header("Authorization", "Bearer $it") } + return builder + } + + fun getUser(id: String): DraslUser { + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/users/$id")) + .get() + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val responseBody = response.body!!.string() + return json.decodeFromString(responseBody) + } + } + + fun getPlayerByUuid(uuid: String): DraslPlayer { + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/players/$uuid")) + .get() + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val responseBody = response.body!!.string() + return json.decodeFromString(responseBody) + } + } + + fun getSnByPlayer(playerUuid: String): String? { + return try { + val player = getPlayerByUuid(playerUuid) + val user = getUser(player.userUuid) + user.oidcIdentities.find { it.oidcProviderName == "Solarpass" }?.subject + } catch (e: IOException) { + null + } + } +} diff --git a/src/main/kotlin/dev/solsynth/snConnect/services/SnTransactionService.kt b/src/main/kotlin/dev/solsynth/snConnect/services/SnBalanceService.kt similarity index 50% rename from src/main/kotlin/dev/solsynth/snConnect/services/SnTransactionService.kt rename to src/main/kotlin/dev/solsynth/snConnect/services/SnBalanceService.kt index eb627ef..055b987 100644 --- a/src/main/kotlin/dev/solsynth/snConnect/services/SnTransactionService.kt +++ b/src/main/kotlin/dev/solsynth/snConnect/services/SnBalanceService.kt @@ -8,54 +8,59 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException +import java.math.BigDecimal @Serializable -data class SnTransaction ( - val id: Long, +data class SnTransaction( + val id: String, @SerialName("created_at") val createdAt: Instant, @SerialName("updated_at") val updatedAt: Instant, @SerialName("deleted_at") val deletedAt: Instant? = null, - val remark: String, - val amount: String, - @SerialName("payer_id") - val payerID: Long? = null, - @SerialName("payee_id") - val payeeID: Long? = null + @SerialName("currency") + val currency: String, + @SerialName("amount") + val amount: Double, + @SerialName("remarks") + val remarks: String?, + @SerialName("type") + val type: String, + @SerialName("payer_wallet_id") + val payerWalletId: String?, + @SerialName("payee_wallet_id") + val payeeWalletId: String? ) @Serializable data class SnTransactionRequest( - @SerialName("client_id") - val clientId: String, - @SerialName("client_secret") - val clientSecret: String, - @SerialName("payee_id") - val payeeID: Long? = null, - @SerialName("payer_id") - val payerID: Long? = null, + @SerialName("account_id") + val accountID: String, val remark: String, - val amount: Double + val amount: Double, + val currency: String ) -class SnTransactionService(private val sn: SnService) { +class SnBalanceService(private val sn: SnService) { private val json = Json { ignoreUnknownKeys = true } - fun makeTransaction(amount: Double, remark: String, payeeID: Long): SnTransaction { + fun addBalance(amount: Double, remark: String, accountID: String): SnTransaction { val body = SnTransactionRequest( - sn.clientId, - sn.clientSecret, amount = amount, remark = remark, - payeeID = payeeID, + accountID = accountID, + currency = "points" ); + // Notice, the balance modification is admin API, the bot need have the admin privilege to do so. val request = Request.Builder() - .url(sn.getUrl("wa", "/transactions")) + .url(sn.getUrl("id", "/wallets/balance")) .post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull())) + .apply { + sn.botApiKey?.let { header("Authorization", "Bearer $it") } + } .build() sn.client.newCall(request).execute().use { response -> @@ -66,18 +71,4 @@ class SnTransactionService(private val sn: SnService) { return out } } - - fun getTransaction(id: Long): SnTransaction { - val request = Request.Builder() - .url(sn.getUrl("wa", "/transactions/$id")) - .get() - .build() - sn.client.newCall(request).execute().use { response -> - if (!response.isSuccessful) throw IOException("Unexpected code $response") - val responseBody = response.body!!.string() - val out = json.decodeFromString(responseBody) - - return out - } - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/solsynth/snConnect/services/SnMessageService.kt b/src/main/kotlin/dev/solsynth/snConnect/services/SnMessageService.kt index 88db2cc..4c057d3 100644 --- a/src/main/kotlin/dev/solsynth/snConnect/services/SnMessageService.kt +++ b/src/main/kotlin/dev/solsynth/snConnect/services/SnMessageService.kt @@ -1,13 +1,12 @@ package dev.solsynth.snConnect.services -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException -import java.util.UUID +import java.util.* @Serializable data class SnMessageRequest( @@ -16,7 +15,7 @@ data class SnMessageRequest( ) class SnMessageService(private val sn: SnService) { - private val json = Json { + private val json: Json = Json { ignoreUnknownKeys = true } diff --git a/src/main/kotlin/dev/solsynth/snConnect/services/SnOrderService.kt b/src/main/kotlin/dev/solsynth/snConnect/services/SnOrderService.kt index c9eba0b..288e4fb 100644 --- a/src/main/kotlin/dev/solsynth/snConnect/services/SnOrderService.kt +++ b/src/main/kotlin/dev/solsynth/snConnect/services/SnOrderService.kt @@ -4,10 +4,12 @@ import kotlinx.datetime.Instant import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException +import java.math.BigDecimal @Serializable data class SnOrderRequest( @@ -15,21 +17,32 @@ data class SnOrderRequest( val clientId: String, @SerialName("client_secret") val clientSecret: String, - val remark: String, - val amount: Double + @SerialName("currency") + val currency: String, + @SerialName("amount") + val amount: Double, + @SerialName("remarks") + val remarks: String? = null, + @SerialName("product_identifier") + val productIdentifier: String? = null, + @SerialName("meta") + val meta: JsonElement? = null, + @SerialName("duration_hours") + val durationHours: Int = 24 ) @Serializable -data class SnOrderCancelRequest( +data class SnOrderUpdateStatus( @SerialName("client_id") val clientId: String, @SerialName("client_secret") val clientSecret: String, + val status: Int ) @Serializable data class SnOrder( - val id: Long, + val id: String, @SerialName("created_at") val createdAt: Instant, @SerialName("updated_at") @@ -56,13 +69,14 @@ class SnOrderService(private val sn: SnService) { fun createOrder(remark: String, amount: Double): SnOrder { val body = SnOrderRequest( - sn.clientId, - sn.clientSecret, - remark, - amount, + clientId = sn.clientId, + clientSecret = sn.clientSecret, + currency = "points", + amount = amount, + remarks = remark ); val request = Request.Builder() - .url(sn.getUrl("wa", "/orders")) + .url(sn.getUrl("id", "/orders")) .post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull())) .build() @@ -75,9 +89,9 @@ class SnOrderService(private val sn: SnService) { } } - fun getOrder(id: Long): SnOrder { + fun getOrder(id: String): SnOrder { val request = Request.Builder() - .url(sn.getUrl("wa", "/orders/$id")) + .url(sn.getUrl("id", "/orders/$id")) .get() .build() sn.client.newCall(request).execute().use { response -> @@ -89,14 +103,17 @@ class SnOrderService(private val sn: SnService) { } } - fun cancelOrder(id: Long): SnOrder { - val body = SnOrderCancelRequest( + // If the isCancelled is true, set the order status to be canceled, otherwise set it to finished + fun updateOrderStatus(id: String, isCancelled: Boolean = false): SnOrder { + val status = if (isCancelled) 2 else 3; + val body = SnOrderUpdateStatus( sn.clientId, sn.clientSecret, + status ); val request = Request.Builder() - .url(sn.getUrl("wa", "/orders/$id/cancel")) - .post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull())) + .url(sn.getUrl("id", "/orders/$id/status")) + .patch(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull())) .build() sn.client.newCall(request).execute().use { response -> @@ -107,4 +124,4 @@ class SnOrderService(private val sn: SnService) { return out } } -} \ No newline at end of file +} diff --git a/src/main/kotlin/dev/solsynth/snConnect/services/SnPlayerService.kt b/src/main/kotlin/dev/solsynth/snConnect/services/SnPlayerService.kt new file mode 100644 index 0000000..d82b9ef --- /dev/null +++ b/src/main/kotlin/dev/solsynth/snConnect/services/SnPlayerService.kt @@ -0,0 +1,140 @@ +package dev.solsynth.snConnect.services + +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.IOException + +@Serializable +data class APIPlayer( + val id: String, + @SerialName("user_id") + val userId: String, + @SerialName("display_name") + val displayName: String?, + @SerialName("uuid") + val uuid: String?, // Minecraft UUID + @SerialName("created_at") + val createdAt: Instant, + @SerialName("updated_at") + val updatedAt: Instant, + @SerialName("last_login") + val lastLogin: Instant? +) + +@Serializable +data class APIPlayerCreate( + @SerialName("user_id") + val userId: String, + @SerialName("display_name") + val displayName: String?, + @SerialName("uuid") + val uuid: String? +) + +@Serializable +data class APIPlayerUpdate( + @SerialName("display_name") + val displayName: String?, + @SerialName("uuid") + val uuid: String? +) + +class SnPlayerService(private val sn: AuthService) { + private val json = Json { + ignoreUnknownKeys = true + } + + private fun addAuthHeader(builder: Request.Builder): Request.Builder { + sn.siteSecret?.let { builder.header("Authorization", "Bearer $it") } + return builder + } + + fun getPlayers(): List { + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/players")) + .get() + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val responseBody = response.body!!.string() + return json.decodeFromString(responseBody) + } + } + + fun getPlayer(id: String): APIPlayer { + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/players/$id")) + .get() + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val responseBody = response.body!!.string() + return json.decodeFromString(responseBody) + } + } + + fun getPlayerByUuid(uuid: String): APIPlayer { + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/players/uuid/$uuid")) + .get() + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val responseBody = response.body!!.string() + return json.decodeFromString(responseBody) + } + } + + fun createPlayer(request: APIPlayerCreate): APIPlayer { + val body = Json.encodeToString(request).toRequestBody("application/json".toMediaTypeOrNull()) + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/players")) + .post(body) + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val responseBody = response.body!!.string() + return json.decodeFromString(responseBody) + } + } + + fun updatePlayer(id: String, request: APIPlayerUpdate): APIPlayer { + val body = Json.encodeToString(request).toRequestBody("application/json".toMediaTypeOrNull()) + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/players/$id")) + .patch(body) + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val responseBody = response.body!!.string() + return json.decodeFromString(responseBody) + } + } + + fun deletePlayer(id: String) { + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/players/$id")) + .delete() + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + } + } + + fun getPlayerByUser(userId: String): APIPlayer { + val requestBuilder = Request.Builder() + .url(sn.getUrl("/drasl/api/v2/players/user/$userId")) + .get() + + sn.client.newCall(addAuthHeader(requestBuilder).build()).execute().use { response -> + if (!response.isSuccessful) throw IOException("Unexpected code $response") + val responseBody = response.body!!.string() + return json.decodeFromString(responseBody) + } + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 807c89a..17f392e 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,15 +1,12 @@ +locale: en sn: endpoint: https://api.solian.app client_id: goatcraft client_secret: 12345678 bot_secret: 114.514.19198 +auth: + endpoint: https://authmc.solsynth.dev + site_secret: hereissecret chat: sync_rooms: [] - outgoing_room: 00000000-0000-0000-0000-00000000008b -messages: - join: "➡️ {player} joined the game." - joinFirst: "➡️ {player} first time joined the game." - quit: "⬅️ {player} left the game." - death: "💀 {player} {message}" - serverStart: "🚀 Server started successfully" - serverStop: "⏹️ Server stopped" + outgoing_room: 00000000-0000-0000-0000-00000000008b \ No newline at end of file diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..0036609 --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,29 @@ +join: "➡️ {player} joined the game." +joinFirst: "➡️ {player} first time joined the game." +quit: "⬅️ {player} left the game." +death: "💀 {player} {message}" +advancement: "🎉 {player} unlocked advancement: {advancement}" +server_start: "🚀 Server started successfully" +server_stop: "⏹️ Server stopped" + +# Command messages +command_deposit_no_amount: "You need to specify an amount to deposit." +command_deposit_invalid_order: "Invalid order number, it must be a uuid." +command_deposit_confirming: "Confirming payment, please stand by..." +command_deposit_pull_error: "An error occurred while pulling transaction. Make sure the order exists then try again later." +command_deposit_order_not_paid: "Order was not paid yet." +command_deposit_done: "Done!" +command_deposit_added_balance: " has been added to your balance." +command_deposit_invalid_amount: "You need to specify an amount as a number to deposit." +command_deposit_creating: "Creating order, please stand by..." +command_deposit_create_error: "An error occurred while creating order. Try again later." +command_deposit_click_here: "click here" +command_deposit_order_created: "Order created, number " +command_deposit_to_payment_page: " to the payment page." +command_deposit_after_paid: "After you paid, click here to confirm payment." +command_withdraw_no_wallet: "You need to specify a wallet account to withdraw to." +command_withdraw_making: "Making transaction, please stand by..." +command_withdraw_no_money: "Your in-game account does not have enough money for that." +command_withdraw_done: "Done! transaction number " +command_withdraw_error: "An error occurred while making transaction. Make sure your wallet exists then try again later." +command_withdraw_refund_reason: "Withdraw to Source Point Failed - Refund" diff --git a/src/main/resources/messages_zh.yml b/src/main/resources/messages_zh.yml new file mode 100644 index 0000000..6593062 --- /dev/null +++ b/src/main/resources/messages_zh.yml @@ -0,0 +1,29 @@ +join: "➡️ {player} 加入了游戏。" +joinFirst: "➡️ {player} 首次加入了游戏。" +quit: "⬅️ {player} 离开了游戏。" +death: "💀 {player} {message}" +advancement: "🎉 {player} 解锁成就:{advancement}" +server_start: "🚀 服务器成功启动" +server_stop: "⏹️ 服务器已停止" + +# Command messages +command_deposit_no_amount: "您需要指定存款金额。" +command_deposit_invalid_order: "无效的订单号,它必须是一个 UUID。" +command_deposit_confirming: "正在确认付款,请稍等..." +command_deposit_pull_error: "拉取交易时发生错误。请确保订单存在,然后稍后再试。" +command_deposit_order_not_paid: "订单尚未付款。" +command_deposit_done: "完成!" +command_deposit_added_balance: " 已添加到您的余额。" +command_deposit_invalid_amount: "您需要将金额指定为数字进行存款。" +command_deposit_creating: "正在创建订单,请稍等..." +command_deposit_create_error: "创建订单时发生错误。请稍后再试。" +command_deposit_click_here: "点击这里" +command_deposit_order_created: "订单已创建,编号 " +command_deposit_to_payment_page: "到付款页面。" +command_deposit_after_paid: "付款后,点击这里确认付款。" +command_withdraw_no_wallet: "您需要指定一个提取钱包账户。" +command_withdraw_making: "正在进行交易,请稍等..." +command_withdraw_no_money: "您的游戏内账户没有足够的钱。" +command_withdraw_done: "完成!交易编号 " +command_withdraw_error: "进行交易时发生错误。请确保您的钱包存在,然后稍后再试。" +command_withdraw_refund_reason: "提取到 Source Point 失败 - 退款"