Compare commits

...

7 Commits

Author SHA1 Message Date
80d58ea4e2 🐛 Fix join first 2025-10-06 12:06:57 +08:00
770a58fb1d 🐛 Fix bugs 2025-10-06 03:22:38 +08:00
1862ce4f65 🔇 Toggable websocket packet 2025-10-06 01:00:53 +08:00
1c304a5108 🐛 Bug fixes 2025-10-06 00:49:15 +08:00
a902ecef8d Balance exchange source points, link account with Solarpass with drasl 2025-10-05 23:46:06 +08:00
3803e27a06 Bedrock remover 2025-10-05 19:06:56 +08:00
339944d13a First time join 2025-10-05 18:26:39 +08:00
17 changed files with 887 additions and 188 deletions

View File

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

View File

@@ -1,8 +1,13 @@
package dev.solsynth.snConnect
import dev.solsynth.snConnect.commands.BedrockRemoverCommand
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
@@ -16,19 +21,25 @@ 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<String> = emptyList()
private var destinationChatId: String? = null
private var webSocketJob: Job? = null
private var messages: Map<String, String> = mapOf()
private var playerSolarpassMap: MutableMap<String, String?> = mutableMapOf()
private fun handleWebSocketPacket(packet: WebSocketPacket) {
if (config.getBoolean("debug"))
logger.info("Received WebSocket packet: type=${packet.type}")
if (packet.type.startsWith("messages") && packet.data != null) {
try {
@@ -80,15 +91,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<String, String> ?: 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<String, String> ?: emptyMap())
if (!setupNetwork()) {
logger.severe("Failed to setup Solar Network Network, check your configuration.")
@@ -103,9 +112,19 @@ 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))
Bukkit.getPluginCommand("eatrock")!!.tabCompleter = BedrockRemoverCompleter()
logger.info(
String.format(
"Successfully loaded Solar Network Connect connected with %s",
@@ -114,8 +133,12 @@ class SolarNetworkConnect : JavaPlugin() {
);
// Send server start message
try {
destinationChatId?.let { chatId ->
messageService?.sendMessage(chatId, messages["serverStart"] ?: "🚀 Server started successfully")
messageService?.sendMessage(chatId, messages["server_start"] ?: "🚀 Server started successfully")
}
} catch (e: Exception) {
logger.warning("Failed to send server start message: ${e.message}")
}
}
@@ -123,8 +146,12 @@ class SolarNetworkConnect : JavaPlugin() {
logger.info(String.format("Disabled Version %s", description.version));
// Send server stop message
try {
destinationChatId?.let { chatId ->
messageService?.sendMessage(chatId, messages["serverStop"] ?: "⏹️ Server stopped")
messageService?.sendMessage(chatId, messages["server_stop"] ?: "⏹️ Server stopped")
}
} catch (e: Exception) {
logger.warning("Failed to send server stop message: ${e.message}")
}
sn?.disconnect()
@@ -142,6 +169,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)

View File

@@ -0,0 +1,220 @@
package dev.solsynth.snConnect.commands
import net.milkbowl.vault.economy.Economy
import net.milkbowl.vault.economy.EconomyResponse
import org.bukkit.ChatColor
import org.bukkit.Material
import org.bukkit.Sound
import org.bukkit.command.Command
import org.bukkit.command.CommandExecutor
import org.bukkit.command.CommandSender
import org.bukkit.command.TabCompleter
import org.bukkit.entity.Player
import org.bukkit.inventory.ItemStack
class BedrockRemoverCommand(private val economy: Economy?) : CommandExecutor {
private val paymentMethods = listOf("money", "materials")
private val requiredMaterials = listOf(
Material.PISTON,
Material.TNT,
Material.LEVER,
Material.OAK_TRAPDOOR // Representative for trapdoor group
)
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (sender !is Player) {
sender.sendMessage(ChatColor.RED.toString() + "This command can only be used by players.")
return true
}
if (args.isEmpty() || args.size > 1) {
sender.sendMessage(ChatColor.RED.toString() + "Usage: /bedrockremove <money/materials>")
return true
}
val paymentMethod = args[0].lowercase()
if (paymentMethod !in paymentMethods) {
sender.sendMessage(ChatColor.RED.toString() + "Invalid payment method. Use: money or materials.")
return true
}
// Get the block the player is looking at
val targetBlock = sender.getTargetBlock(null, 10)
if (targetBlock == null || targetBlock.type != Material.BEDROCK) {
sender.sendMessage(ChatColor.RED.toString() + "You must be looking at a bedrock block within 10 blocks.")
return true
}
// Attempt payment
val success = when (paymentMethod) {
"money" -> payWithMoney(sender, 100.0)
"materials" -> payWithAllMaterials(sender)
else -> return false
}
if (!success) {
// Error messages are handled in the pay methods
return true
}
// Remove the bedrock
targetBlock.type = Material.AIR
sender.sendMessage(ChatColor.GREEN.toString() + "Bedrock removed successfully!")
sender.playSound(sender.location, Sound.BLOCK_STONE_BREAK, 1.0f, 1.0f)
return true
}
private fun payWithMoney(player: Player, amount: Double): Boolean {
if (economy == null) {
player.sendMessage(ChatColor.RED.toString() + "Economy not available.")
return false
}
val response = economy.withdrawPlayer(player, "Bedrock removal", amount)
if (response.type != EconomyResponse.ResponseType.SUCCESS) {
player.sendMessage(ChatColor.RED.toString() + "Insufficient funds. You need ${amount}.")
return false
}
player.sendMessage(ChatColor.GREEN.toString() + "Withdrew $amount from your account.")
return true
}
private fun payWithMaterial(player: Player, material: Material): Boolean {
val inventory = player.inventory
// Special handling for materials with multiple variants
val hasItem = when (material) {
Material.OAK_TRAPDOOR -> {
// Check for any trapdoor type
inventory.contents.any { item -> item != null && item.type.name.endsWith("_TRAPDOOR") }
}
Material.PISTON -> {
// Check for any piston type
inventory.contents.any { item -> item != null && (item.type == Material.PISTON || item.type == Material.STICKY_PISTON) }
}
else -> inventory.contains(material, 1)
}
if (!hasItem) {
player.sendMessage(ChatColor.RED.toString() + "You don't have a ${material.name.lowercase()} in your inventory.")
return false
}
// Remove the item
when (material) {
Material.OAK_TRAPDOOR -> {
// Find and remove any trapdoor
for (item in inventory.contents) {
if (item != null && item.type.name.endsWith("_TRAPDOOR")) {
val amount = item.amount
if (amount > 1) {
item.amount = amount - 1
} else {
inventory.remove(item)
}
break
}
}
}
Material.PISTON -> {
// Find and remove any piston
for (item in inventory.contents) {
if (item != null && (item.type == Material.PISTON || item.type == Material.STICKY_PISTON)) {
val amount = item.amount
if (amount > 1) {
item.amount = amount - 1
} else {
inventory.remove(item)
}
break
}
}
}
else -> {
inventory.removeItem(ItemStack(material, 1))
}
}
player.sendMessage(ChatColor.GREEN.toString() + "Consumed 1 ${material.name.lowercase()}.")
return true
}
private fun payWithAllMaterials(player: Player): Boolean {
val inventory = player.inventory
// Check if player has all required materials
for (material in requiredMaterials) {
val hasItem = when (material) {
Material.OAK_TRAPDOOR -> {
// Check for any trapdoor type
inventory.contents.any { item -> item != null && item.type.name.endsWith("_TRAPDOOR") }
}
Material.PISTON -> {
// Check for any piston type
inventory.contents.any { item -> item != null && (item.type == Material.PISTON || item.type == Material.STICKY_PISTON) }
}
else -> inventory.contains(material, 1)
}
if (!hasItem) {
player.sendMessage(ChatColor.RED.toString() + "You don't have all required materials: piston, tnt, lever, and trapdoor.")
return false
}
}
// Remove one of each material
for (material in requiredMaterials) {
when (material) {
Material.OAK_TRAPDOOR -> {
// Find and remove any trapdoor
for (item in inventory.contents) {
if (item != null && item.type.name.endsWith("_TRAPDOOR")) {
val amount = item.amount
if (amount > 1) {
item.amount = amount - 1
} else {
inventory.remove(item)
}
break
}
}
}
Material.PISTON -> {
// Find and remove any piston
for (item in inventory.contents) {
if (item != null && (item.type == Material.PISTON || item.type == Material.STICKY_PISTON)) {
val amount = item.amount
if (amount > 1) {
item.amount = amount - 1
} else {
inventory.remove(item)
}
break
}
}
}
else -> {
inventory.removeItem(ItemStack(material, 1))
}
}
}
player.sendMessage(ChatColor.GREEN.toString() + "Consumed materials: piston, tnt, lever, trapdoor.")
return true
}
}
class BedrockRemoverCompleter : TabCompleter {
private val paymentMethods = listOf("money", "materials")
override fun onTabComplete(
sender: CommandSender,
command: Command,
label: String,
args: Array<out String>
): MutableList<String> {
return when (args.size) {
1 -> paymentMethods.filter { it.startsWith(args[0], ignoreCase = true) }.toMutableList()
else -> mutableListOf()
}
}
}

View File

@@ -5,6 +5,7 @@ import net.md_5.bungee.api.chat.ClickEvent
import net.md_5.bungee.api.chat.TextComponent
import net.milkbowl.vault.economy.Economy
import net.milkbowl.vault.economy.EconomyResponse
import org.bukkit.Bukkit.getLogger
import org.bukkit.ChatColor
import org.bukkit.Sound
import org.bukkit.command.Command
@@ -13,7 +14,13 @@ 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<String, String>,
private val playerSolarpassMap: MutableMap<String, String?>
) :
CommandExecutor {
override fun onCommand(p0: CommandSender, p1: Command, p2: String, p3: Array<out String>): Boolean {
if (p0 !is Player) {
return false;
@@ -24,7 +31,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 +42,44 @@ 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);
// The update validated the order is created by us
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;
val bal = order.amount;
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 +90,53 @@ 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.")
order = orderSrv.createOrder("Deposit to GoatCraft", amount);
} catch (e: Exception) {
p0.sendMessage(
ChatColor.RED.toString() + (messages["command_deposit_create_error"]
?: "An error occurred while creating order. Try again later.")
)
e.printStackTrace();
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.take(6)}" + " "
)
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,51 +144,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() + (messages["command_withdraw_making"]
?: "Making transaction, please stand by...")
);
p0.sendMessage(ChatColor.GRAY.toString() + "Making transaction, please stand by...");
val bal = amount / 100;
val resp = eco?.withdrawPlayer(p0.player, "Withdraw to Source Point - $bal SRC", amount);
// Takes extra 20% as fee
val fee = amount * 0.2;
val resp = eco?.withdrawPlayer(p0.player, amount + fee);
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);
transactionSrv.addBalance(amount, "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! ")
)
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.")
} catch (e: Exception) {
eco.depositPlayer(p0.player, amount + fee)
p0.sendMessage(
ChatColor.RED.toString() + (messages["command_withdraw_error"]
?: "An error occurred while making transaction. Make sure your wallet exists then try again later.")
)
e.printStackTrace()
return true
}
}
else -> {

View File

@@ -1,6 +1,8 @@
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
import org.bukkit.event.EventHandler
import org.bukkit.event.EventPriority
@@ -16,31 +18,31 @@ class SnChatListener(
private val destinationChatId: String,
private val messages: Map<String, String>
) : Listener {
@EventHandler(priority = EventPriority.MONITOR)
fun onPlayerChat(event: AsyncPlayerChatEvent) {
val message = "${event.player.name}: ${event.message}"
messageService.sendMessage(destinationChatId, message)
}
@EventHandler(priority = EventPriority.MONITOR)
@Suppress("SENSELESS_COMPARISON")
@EventHandler()
fun onPlayerJoin(event: PlayerJoinEvent) {
val firstTime = event.player.hasPlayedBefore();
val templateKey = if (!firstTime) "join" else "joinFirst";
val firstTime = Bukkit.getOfflinePlayer(event.player.uniqueId) == null;
val templateKey = if (!firstTime) "join" else "join_first";
val template = messages[templateKey]
?: if (!firstTime) "➡️ {player} joined the game." else "➡️ {player} first time joined the game."
val message = template.replace("{player}", event.player.name)
messageService.sendMessage(destinationChatId, message)
}
@EventHandler(priority = EventPriority.MONITOR)
@EventHandler()
fun onPlayerQuit(event: PlayerQuitEvent) {
val template = messages["quit"] ?: "⬅️ {player} left the game."
val message = template.replace("{player}", event.player.name)
messageService.sendMessage(destinationChatId, message)
}
@EventHandler(priority = EventPriority.MONITOR)
@EventHandler()
fun onPlayerDeath(event: PlayerDeathEvent) {
val template = messages["death"] ?: "💀 {player} {message}"
val message = template.replace("{player}", event.entity.name).replace("{message}", event.deathMessage ?: "")

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

@@ -0,0 +1,70 @@
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
import java.math.BigDecimal
@Serializable
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,
@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("account_id")
val accountID: String,
val remark: String,
val amount: Double,
val currency: String
)
class SnBalanceService(private val sn: SnService) {
private val json = Json {
ignoreUnknownKeys = true
}
fun addBalance(amount: Double, remark: String, accountID: String) {
val body = SnTransactionRequest(
amount = amount,
remark = remark,
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("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 ->
if (!response.isSuccessful) throw IOException("Unexpected code $response")
}
}
}

View File

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

View File

@@ -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")
@@ -37,16 +50,14 @@ data class SnOrder(
@SerialName("deleted_at")
val deletedAt: Instant? = null,
val status: Long,
val remark: String,
val amount: String,
val remarks: String,
val amount: Double,
@SerialName("payer_id")
val payerID: Int? = null,
val payerID: String? = null,
@SerialName("payee_id")
val payeeID: Int? = null,
val payeeID: String? = null,
@SerialName("transaction_id")
val transactionID: Int? = null,
@SerialName("client_id")
val clientID: Long
val transactionID: String? = null,
)
class SnOrderService(private val sn: SnService) {
@@ -56,13 +67,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 +87,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 +101,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 ->

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,83 +0,0 @@
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 SnTransaction (
val id: Long,
@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
)
@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,
val remark: String,
val amount: Double
)
class SnTransactionService(private val sn: SnService) {
private val json = Json {
ignoreUnknownKeys = true
}
fun makeTransaction(amount: Double, remark: String, payeeID: Long): SnTransaction {
val body = SnTransactionRequest(
sn.clientId,
sn.clientSecret,
amount = amount,
remark = remark,
payeeID = payeeID,
);
val request = Request.Builder()
.url(sn.getUrl("wa", "/transactions"))
.post(Json.encodeToString(body).toRequestBody("application/json".toMediaTypeOrNull()))
.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
}
}
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,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"

View File

@@ -0,0 +1,29 @@
join: "➡️ {player} joined the game."
join_first: "➡️ {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} 加入了游戏。"
join_first: "➡️ {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 失败 - 退款"

View File

@@ -4,7 +4,7 @@ main: dev.solsynth.snConnect.SolarNetworkConnect
api-version: '1.21'
authors: [ LittleSheepOvO ]
description: Solar Network Connect provide features connect with Solar Network
loadbefore:
softdepend:
- CMI
- CMIEInjector
- CMILib
@@ -17,7 +17,16 @@ commands:
aliases: ['sn']
permission: solar-network.command.sn
permission-message: §cYou don't have the permission -> §6[solar-network.command.sn]
eatrock:
description: Remove bedrock with payment
usage: §e/eatrock §r<money/materials>
aliases: ['eatrock']
permission: solar-network.command.eatrock
permission-message: §cYou don't have the permission -> §6[solar-network.command.eatrock]
permissions:
solar-network.command.sn:
description: Permission of uses Solar Network Command
default: true
solar-network.command.eatrock:
description: Permission to remove bedrock with payment
default: op