Balance exchange source points, link account with Solarpass with drasl

This commit is contained in:
2025-10-05 23:46:06 +08:00
parent 3803e27a06
commit a902ecef8d
14 changed files with 576 additions and 118 deletions

View File

@@ -23,6 +23,7 @@ repositories {
dependencies { dependencies {
compileOnly("org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT") compileOnly("org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT")
compileOnly("com.github.MilkBowl:VaultAPI:1.7") 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 // These will be packaged into the shadow JAR
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

View File

@@ -5,6 +5,9 @@ import dev.solsynth.snConnect.commands.BedrockRemoverCompleter
import dev.solsynth.snConnect.commands.SnCommand import dev.solsynth.snConnect.commands.SnCommand
import dev.solsynth.snConnect.commands.SnCommandCompleter import dev.solsynth.snConnect.commands.SnCommandCompleter
import dev.solsynth.snConnect.listeners.SnChatListener 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.SnChatMessage
import dev.solsynth.snConnect.models.WebSocketPacket import dev.solsynth.snConnect.models.WebSocketPacket
import dev.solsynth.snConnect.services.SnMessageService 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 net.milkbowl.vault.economy.Economy
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.Bukkit.getOnlinePlayers import org.bukkit.Bukkit.getOnlinePlayers
import org.bukkit.configuration.file.YamlConfiguration
import org.bukkit.plugin.java.JavaPlugin import org.bukkit.plugin.java.JavaPlugin
import java.io.InputStreamReader
class SolarNetworkConnect : JavaPlugin() { class SolarNetworkConnect : JavaPlugin() {
private var economy: Economy? = null private var economy: Economy? = null
private var sn: SnService? = null private var sn: SnService? = null
private var authService: AuthService? = null
private var authUserService: AuthUserService? = null
private var messageService: SnMessageService? = null private var messageService: SnMessageService? = null
private var syncChatRooms: List<String> = emptyList() private var syncChatRooms: List<String> = emptyList()
private var destinationChatId: String? = null private var destinationChatId: String? = null
private var webSocketJob: Job? = null private var webSocketJob: Job? = null
private var messages: Map<String, String> = mapOf() private var messages: Map<String, String> = mapOf()
private var playerSolarpassMap: MutableMap<String, String?> = mutableMapOf()
private fun handleWebSocketPacket(packet: WebSocketPacket) { private fun handleWebSocketPacket(packet: WebSocketPacket) {
logger.info("Received WebSocket packet: type=${packet.type}") logger.info("Received WebSocket packet: type=${packet.type}")
@@ -82,15 +90,13 @@ class SolarNetworkConnect : JavaPlugin() {
this.saveDefaultConfig() this.saveDefaultConfig()
messages = mapOf( val locale = config.getString("locale") ?: "en"
"join" to "➡️ {player} joined the game.", val messageFile = if (locale == "en") "messages.yml" else "messages_$locale.yml"
"joinFirst" to "➡️ {player} first time joined the game.", val messagesStream = getResource(messageFile) ?: throw RuntimeException("$messageFile not found in JAR")
"quit" to "⬅️ {player} left the game.", val messagesConfig = YamlConfiguration.loadConfiguration(InputStreamReader(messagesStream, "UTF-8"))
"death" to "💀 {player} {message}", messages = messagesConfig.getValues(false)
"advancement" to "🎉 {player} unlocked advancement: {advancement}", .mapValues { it.value as String } + (config.getConfigurationSection("messages")
"serverStart" to "🚀 Server started successfully", ?.getValues(false) as? Map<String, String> ?: emptyMap())
"serverStop" to "⏹️ Server stopped"
) + (config.getConfigurationSection("messages")?.getValues(false) as? Map<String, String> ?: emptyMap())
if (!setupNetwork()) { if (!setupNetwork()) {
logger.severe("Failed to setup Solar Network Network, check your configuration.") 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) 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("solar")!!.tabCompleter = SnCommandCompleter()
Bukkit.getPluginCommand("eatrock")!!.setExecutor(BedrockRemoverCommand(this.economy)) Bukkit.getPluginCommand("eatrock")!!.setExecutor(BedrockRemoverCommand(this.economy))
@@ -120,7 +133,7 @@ class SolarNetworkConnect : JavaPlugin() {
// Send server start message // Send server start message
destinationChatId?.let { chatId -> 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 // Send server stop message
destinationChatId?.let { chatId -> destinationChatId?.let { chatId ->
messageService?.sendMessage(chatId, messages["serverStop"] ?: "⏹️ Server stopped") messageService?.sendMessage(chatId, messages["server_stop"] ?: "⏹️ Server stopped")
} }
sn?.disconnect() sn?.disconnect()
@@ -147,6 +160,12 @@ class SolarNetworkConnect : JavaPlugin() {
destinationChatId = destination destinationChatId = destination
sn = SnService(baseUrl, clientId, clientSecret, botApiKey); sn = SnService(baseUrl, clientId, clientSecret, botApiKey);
messageService = SnMessageService(sn!!) 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 { webSocketJob = GlobalScope.launch {
sn!!.connectWebSocketAsFlow().collect { packet -> sn!!.connectWebSocketAsFlow().collect { packet ->
handleWebSocketPacket(packet) handleWebSocketPacket(packet)

View File

@@ -13,7 +13,8 @@ import org.bukkit.command.CommandSender
import org.bukkit.command.TabCompleter import org.bukkit.command.TabCompleter
import org.bukkit.entity.Player 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<String, String>, private val playerSolarpassMap: MutableMap<String, String?>) :
CommandExecutor {
override fun onCommand(p0: CommandSender, p1: Command, p2: String, p3: Array<out String>): Boolean { override fun onCommand(p0: CommandSender, p1: Command, p2: String, p3: Array<out String>): Boolean {
if (p0 !is Player) { if (p0 !is Player) {
return false; return false;
@@ -24,7 +25,10 @@ class SnCommand(private val sn: SnService, private val eco: Economy?) : CommandE
when (p3[0].lowercase()) { when (p3[0].lowercase()) {
"deposit" -> { "deposit" -> {
if (p3.size < 2) { 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; 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) { if (p3[1].equals("confirm", ignoreCase = true) && p3.size >= 3) {
// Confirming order // Confirming order
val orderNumber = p3[2].toLongOrNull(); val orderNumber = p3[2];
if (orderNumber == null) { p0.sendMessage(
p0.sendMessage(ChatColor.RED.toString() + "Invalid order number, it must be a number."); ChatColor.GRAY.toString() + (messages["command_deposit_confirming"]
return true; ?: "Confirming payment, please stand by...")
} );
p0.sendMessage(ChatColor.GRAY.toString() + "Confirming payment, please stand by...");
val order: SnOrder val order: SnOrder
try { try {
order = orderSrv.getOrder(orderNumber); order = orderSrv.getOrder(orderNumber);
orderSrv.cancelOrder(orderNumber); orderSrv.updateOrderStatus(orderNumber, false);
} catch (_: Exception) { } 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; return true;
} }
if (order.status != 1L) { 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; return true;
} }
val bal = order.amount.toDouble() * 100; val bal = order.amount.toDouble() * 100;
eco?.depositPlayer(p0.player, bal) 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 moneyComponent = TextComponent(ChatColor.GOLD.toString() + ChatColor.BOLD + " $bal$")
val suffixComponent = 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.playSound(p0.player!!, Sound.BLOCK_ANVIL_PLACE, 1.0F, 1.0F)
p0.spigot().sendMessage(doneComponent, moneyComponent, suffixComponent) p0.spigot().sendMessage(doneComponent, moneyComponent, suffixComponent)
@@ -71,32 +83,52 @@ class SnCommand(private val sn: SnService, private val eco: Economy?) : CommandE
// Creating new order // Creating new order
val amount = p3[1].toDoubleOrNull(); val amount = p3[1].toDoubleOrNull();
if (amount == null) { 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; 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 val order: SnOrder
try { try {
order = orderSrv.createOrder("Deposit to Highland MC", amount / 100); order = orderSrv.createOrder("Deposit to Highland MC", amount / 100);
} catch (_: Exception) { } 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; return true;
} }
val linkComponent = 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 = 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 = val orderHintComponent =
TextComponent(ChatColor.GRAY.toString() + "Order created, number " + ChatColor.WHITE + ChatColor.BOLD + "#${order.id}") TextComponent(
val followingComponent = TextComponent(ChatColor.GRAY.toString() + " to the payment page.") 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() p0.spigot()
.sendMessage(orderHintComponent, linkComponent, followingComponent); .sendMessage(orderHintComponent, linkComponent, followingComponent);
val afterPaidComponent = 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 = afterPaidComponent.clickEvent =
ClickEvent(ClickEvent.Action.RUN_COMMAND, "/sn deposit confirm ${order.id}") ClickEvent(ClickEvent.Action.RUN_COMMAND, "/sn deposit confirm ${order.id}")
p0.spigot().sendMessage(afterPaidComponent); p0.spigot().sendMessage(afterPaidComponent);
@@ -104,47 +136,67 @@ class SnCommand(private val sn: SnService, private val eco: Economy?) : CommandE
"withdraw" -> { "withdraw" -> {
if (p3.size < 2) { 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; return true;
} }
val amount = p3[1].toDoubleOrNull(); val amount = p3[1].toDoubleOrNull();
if (amount == null) { 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; return true;
} }
if (p3.size < 3) { val playerUuid = p0.uniqueId.toString()
p0.sendMessage(ChatColor.RED.toString() + "You need to specific a wallet account to deposit.") val accountId = playerSolarpassMap[playerUuid]
if (accountId == null) {
p0.sendMessage(
ChatColor.RED.toString() + "Bind your Solarpass first!"
)
return true; return true;
} }
val walletId = p3[2].toLongOrNull(); p0.sendMessage(
if (walletId == null) { ChatColor.GRAY.toString() + (messages["command_withdraw_making"]
p0.sendMessage(ChatColor.RED.toString() + "You need to specific a wallet account to deposit.") ?: "Making transaction, please stand by...")
return true; );
}
p0.sendMessage(ChatColor.GRAY.toString() + "Making transaction, please stand by...");
val bal = amount / 100; val bal = amount / 100;
val resp = eco?.withdrawPlayer(p0.player, "Withdraw to Source Point - $bal SRC", amount); val resp = eco?.withdrawPlayer(p0.player, "Withdraw to Source Point - $bal SRC", amount);
if (resp?.type != EconomyResponse.ResponseType.SUCCESS) { 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; return true;
} }
try { try {
val transactionSrv = SnTransactionService(sn); val transactionSrv = SnBalanceService(sn);
val transaction = transactionSrv.makeTransaction(bal, "Withdraw from Highland MC", walletId); val transaction = transactionSrv.addBalance(bal, "Withdraw from GoatCraft", accountId);
val transactionHintComponent = 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.playSound(p0.player!!, Sound.BLOCK_ANVIL_PLACE, 1.0F, 1.0F)
p0.spigot().sendMessage(transactionHintComponent) p0.spigot().sendMessage(transactionHintComponent)
} catch (_: Exception) { } catch (_: Exception) {
eco?.depositPlayer(p0.player, "Withdraw to Source Point Failed - Refund", amount) eco?.depositPlayer(
p0.sendMessage(ChatColor.RED.toString() + "An error occurred while making transaction. Make sure your wallet is exists then try again later.") 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 return true
} }
@@ -172,4 +224,4 @@ class SnCommandCompleter : TabCompleter {
else -> mutableListOf(); else -> mutableListOf();
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package dev.solsynth.snConnect.listeners package dev.solsynth.snConnect.listeners
import com.Zrips.CMI.CMI
import dev.solsynth.snConnect.services.SnMessageService import dev.solsynth.snConnect.services.SnMessageService
import org.bukkit.Bukkit import org.bukkit.Bukkit
import org.bukkit.entity.Player import org.bukkit.entity.Player
@@ -26,7 +27,8 @@ class SnChatListener(
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
@EventHandler() @EventHandler()
fun onPlayerJoin(event: PlayerJoinEvent) { 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 templateKey = if (!firstTime) "join" else "joinFirst";
val template = messages[templateKey] val template = messages[templateKey]
?: if (!firstTime) "➡️ {player} joined the game." else "➡️ {player} first time joined the game." ?: if (!firstTime) "➡️ {player} joined the game." else "➡️ {player} first time joined the game."

View File

@@ -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<String, String>,
private val playerSolarpassMap: MutableMap<String, String?>
) : 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
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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<DraslPlayer>,
@SerialName("oidcIdentities")
val oidcIdentities: List<DraslOIDCIdentity>
)
@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<DraslUser>(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<DraslPlayer>(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
}
}
}

View File

@@ -8,54 +8,59 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException import java.io.IOException
import java.math.BigDecimal
@Serializable @Serializable
data class SnTransaction ( data class SnTransaction(
val id: Long, val id: String,
@SerialName("created_at") @SerialName("created_at")
val createdAt: Instant, val createdAt: Instant,
@SerialName("updated_at") @SerialName("updated_at")
val updatedAt: Instant, val updatedAt: Instant,
@SerialName("deleted_at") @SerialName("deleted_at")
val deletedAt: Instant? = null, val deletedAt: Instant? = null,
val remark: String, @SerialName("currency")
val amount: String, val currency: String,
@SerialName("payer_id") @SerialName("amount")
val payerID: Long? = null, val amount: Double,
@SerialName("payee_id") @SerialName("remarks")
val payeeID: Long? = null val remarks: String?,
@SerialName("type")
val type: String,
@SerialName("payer_wallet_id")
val payerWalletId: String?,
@SerialName("payee_wallet_id")
val payeeWalletId: String?
) )
@Serializable @Serializable
data class SnTransactionRequest( data class SnTransactionRequest(
@SerialName("client_id") @SerialName("account_id")
val clientId: String, val accountID: String,
@SerialName("client_secret")
val clientSecret: String,
@SerialName("payee_id")
val payeeID: Long? = null,
@SerialName("payer_id")
val payerID: Long? = null,
val remark: 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 { private val json = Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
fun makeTransaction(amount: Double, remark: String, payeeID: Long): SnTransaction { fun addBalance(amount: Double, remark: String, accountID: String): SnTransaction {
val body = SnTransactionRequest( val body = SnTransactionRequest(
sn.clientId,
sn.clientSecret,
amount = amount, amount = amount,
remark = remark, 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() val request = Request.Builder()
.url(sn.getUrl("wa", "/transactions")) .url(sn.getUrl("id", "/wallets/balance"))
.post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull())) .post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull()))
.apply {
sn.botApiKey?.let { header("Authorization", "Bearer $it") }
}
.build() .build()
sn.client.newCall(request).execute().use { response -> sn.client.newCall(request).execute().use { response ->
@@ -66,18 +71,4 @@ class SnTransactionService(private val sn: SnService) {
return out 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<SnTransaction>(responseBody)
return out
}
}
}

View File

@@ -1,13 +1,12 @@
package dev.solsynth.snConnect.services package dev.solsynth.snConnect.services
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException import java.io.IOException
import java.util.UUID import java.util.*
@Serializable @Serializable
data class SnMessageRequest( data class SnMessageRequest(
@@ -16,7 +15,7 @@ data class SnMessageRequest(
) )
class SnMessageService(private val sn: SnService) { class SnMessageService(private val sn: SnService) {
private val json = Json { private val json: Json = Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }

View File

@@ -4,10 +4,12 @@ import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.io.IOException import java.io.IOException
import java.math.BigDecimal
@Serializable @Serializable
data class SnOrderRequest( data class SnOrderRequest(
@@ -15,21 +17,32 @@ data class SnOrderRequest(
val clientId: String, val clientId: String,
@SerialName("client_secret") @SerialName("client_secret")
val clientSecret: String, val clientSecret: String,
val remark: String, @SerialName("currency")
val amount: Double 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 @Serializable
data class SnOrderCancelRequest( data class SnOrderUpdateStatus(
@SerialName("client_id") @SerialName("client_id")
val clientId: String, val clientId: String,
@SerialName("client_secret") @SerialName("client_secret")
val clientSecret: String, val clientSecret: String,
val status: Int
) )
@Serializable @Serializable
data class SnOrder( data class SnOrder(
val id: Long, val id: String,
@SerialName("created_at") @SerialName("created_at")
val createdAt: Instant, val createdAt: Instant,
@SerialName("updated_at") @SerialName("updated_at")
@@ -56,13 +69,14 @@ class SnOrderService(private val sn: SnService) {
fun createOrder(remark: String, amount: Double): SnOrder { fun createOrder(remark: String, amount: Double): SnOrder {
val body = SnOrderRequest( val body = SnOrderRequest(
sn.clientId, clientId = sn.clientId,
sn.clientSecret, clientSecret = sn.clientSecret,
remark, currency = "points",
amount, amount = amount,
remarks = remark
); );
val request = Request.Builder() val request = Request.Builder()
.url(sn.getUrl("wa", "/orders")) .url(sn.getUrl("id", "/orders"))
.post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull())) .post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull()))
.build() .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() val request = Request.Builder()
.url(sn.getUrl("wa", "/orders/$id")) .url(sn.getUrl("id", "/orders/$id"))
.get() .get()
.build() .build()
sn.client.newCall(request).execute().use { response -> sn.client.newCall(request).execute().use { response ->
@@ -89,14 +103,17 @@ class SnOrderService(private val sn: SnService) {
} }
} }
fun cancelOrder(id: Long): SnOrder { // If the isCancelled is true, set the order status to be canceled, otherwise set it to finished
val body = SnOrderCancelRequest( fun updateOrderStatus(id: String, isCancelled: Boolean = false): SnOrder {
val status = if (isCancelled) 2 else 3;
val body = SnOrderUpdateStatus(
sn.clientId, sn.clientId,
sn.clientSecret, sn.clientSecret,
status
); );
val request = Request.Builder() val request = Request.Builder()
.url(sn.getUrl("wa", "/orders/$id/cancel")) .url(sn.getUrl("id", "/orders/$id/status"))
.post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull())) .patch(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull()))
.build() .build()
sn.client.newCall(request).execute().use { response -> sn.client.newCall(request).execute().use { response ->
@@ -107,4 +124,4 @@ class SnOrderService(private val sn: SnService) {
return out return out
} }
} }
} }

View File

@@ -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<APIPlayer> {
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)
}
}
}

View File

@@ -1,15 +1,12 @@
locale: en
sn: sn:
endpoint: https://api.solian.app endpoint: https://api.solian.app
client_id: goatcraft client_id: goatcraft
client_secret: 12345678 client_secret: 12345678
bot_secret: 114.514.19198 bot_secret: 114.514.19198
auth:
endpoint: https://authmc.solsynth.dev
site_secret: hereissecret
chat: chat:
sync_rooms: [] sync_rooms: []
outgoing_room: 00000000-0000-0000-0000-00000000008b 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"

View File

@@ -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"

View File

@@ -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 失败 - 退款"