Исправление багов: избегаем ограничение запросов, корректная получение вывода команды, корректные ошибки при осутствии соединения UserAPI.

Доработки: асинхронность для всех команд.
This commit is contained in:
justuser-31 2025-11-29 17:11:57 +03:00
parent 0ce332f050
commit df0561fe51
4 changed files with 121 additions and 58 deletions

View File

@ -4,8 +4,11 @@ package main.VpcSpigotIntegration
// Here is implemented capture of output command (for dynamic course)
// ----------------------------------------------
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PLUGIN
import org.bukkit.Bukkit
import org.bukkit.command.CommandSender
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class CommandCapture {
companion object {
@ -34,7 +37,8 @@ class CommandCapture {
}
}
fun execute(command: String): List<String> {
// Deprecated stupid version for sync version which catch output not every time
fun executeOld(command: String): List<String> {
val output = mutableListOf<String>()
val customSender = createCapturingSender { message ->
@ -45,5 +49,61 @@ class CommandCapture {
return output
}
const val MAX_WAIT_TIME_MS = 5000
const val CHECK_INTERVAL_MS: Long = 300
fun execute(command: String): List<String> {
val output = mutableListOf<String>()
var isCommandFinished = false
val customSender = createCapturingSender { message ->
output.add(message)
}
Bukkit.getScheduler().runTask(PLUGIN) {
Bukkit.dispatchCommand(customSender, command)
}
// Wait for command completion with timeout
val startTime = System.currentTimeMillis()
while (!isCommandFinished && (System.currentTimeMillis() - startTime) < MAX_WAIT_TIME_MS) {
try {
Thread.sleep(CHECK_INTERVAL_MS)
if (output.isNotEmpty()) {
isCommandFinished = true
}
} catch (e: InterruptedException) {
break
}
}
return output
}
// Untested another version
fun executeV3(command: String): List<String> {
val output = mutableListOf<String>()
val latch = CountDownLatch(1)
val customSender = createCapturingSender { message ->
synchronized(output) {
output.add(message)
}
}
Bukkit.getScheduler().runTask(PLUGIN) {
try {
Bukkit.dispatchCommand(customSender, command)
} finally {
latch.countDown()
}
}
// Wait for command completion with timeout
latch.await(MAX_WAIT_TIME_MS/1000.toLong(), TimeUnit.SECONDS)
return output.toList()
}
}
}

View File

@ -11,8 +11,7 @@ class TotalBalanceModules {
fun getEssentialsBalance(): Float {
LOGGER.info("Calculating dynamic rate using baltop")
// Force update balance top and delay to avoid empty string
CommandCapture.execute("baltop force")
val output = CommandCapture.execute("baltop")
val output = CommandCapture.execute("baltop force")
LOGGER.info("Balance top command output: $output")
var total = 0.0f

View File

@ -23,10 +23,10 @@ import kotlin.math.max
class VpcApi {
companion object {
// Rate limiting: track timestamps of requests
private val requestTimestamps = ConcurrentLinkedQueue<Long>()
private const val MAX_REQUESTS_PER_MINUTE = 60
private const val MINUTE_IN_MILLIS = 60 * 1000L
// Rate limiting: lock and last execution time
private val rateLimitLock = Object()
private var lastExecutionTime = 0L
private const val MIN_INTERVAL_MILLIS = 1000L // 1 second between requests
// Using Gson for JSON serialization - much cleaner than manual string building
fun jsonify(args: List<String>): String {
@ -49,37 +49,27 @@ class VpcApi {
}
/**
* Enforces rate limiting by checking if we've exceeded the request limit.
* If so, sleeps until we're within the limit again.
* Enforces rate limiting by ensuring requests are executed one at a time
* with at least 1 second between them.
*/
private fun enforceRateLimit() {
val now = System.currentTimeMillis()
synchronized(rateLimitLock) {
val now = System.currentTimeMillis()
val timeSinceLastExecution = now - lastExecutionTime
// Remove timestamps older than 1 minute
while (requestTimestamps.isNotEmpty() && now - requestTimestamps.peek() > MINUTE_IN_MILLIS) {
requestTimestamps.poll()
}
// If we've hit the limit, calculate sleep time
if (requestTimestamps.size >= MAX_REQUESTS_PER_MINUTE) {
val oldestRequestTime = requestTimestamps.peek()
val sleepTime = max(0, MINUTE_IN_MILLIS - (now - oldestRequestTime)) + 100 // Add small buffer
try {
Thread.sleep(sleepTime)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
// If less than 1 second has passed since last execution, wait
if (timeSinceLastExecution < MIN_INTERVAL_MILLIS) {
val sleepTime = MIN_INTERVAL_MILLIS - timeSinceLastExecution
try {
Thread.sleep(sleepTime)
} catch (e: InterruptedException) {
Thread.currentThread().interrupt()
}
}
// After sleeping, clean up old timestamps again
val afterSleep = System.currentTimeMillis()
while (requestTimestamps.isNotEmpty() && afterSleep - requestTimestamps.peek() > MINUTE_IN_MILLIS) {
requestTimestamps.poll()
}
// Update last execution time to now
lastExecutionTime = System.currentTimeMillis()
}
// Add current timestamp
requestTimestamps.offer(now)
}
fun sendPost(urlPoint: String, json: String, customUrl: Boolean = false, baseurl: String = ""): ApiResponse {

View File

@ -21,6 +21,7 @@ import java.io.FileInputStream
import java.util.Properties
import kotlin.collections.mutableMapOf
import kotlin.math.abs
import kotlin.text.isEmpty
class VpcServerIntegration() : JavaPlugin(), CommandExecutor {
companion object {
@ -216,32 +217,31 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
}
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
if (command.name.equals("vpi", ignoreCase = true)) {
if (sender !is Player) {
sender.sendMessage("&cТолько игроки могут выполнять данные команды.")
return true
}
if (!command.name.equals("vpi", ignoreCase = true)) return false
if (sender !is Player) {
sender.sendMessage("&cТолько игроки могут выполнять данные команды.")
return true
}
// Run all logic asynchronously
Bukkit.getScheduler().runTaskAsynchronously(PLUGIN, Runnable {
when {
// Handle currency conversion command
(args.size == 3 || args.size == 4) && args[0] == "convert" -> {
// Currency conversion
args.size in 3..4 && args[0] == "convert" -> {
handleCurrencyConversion(sender, args)
}
args.isNotEmpty() && args[0] == "convert" -> {
Utils.send(sender, "/vpi convert <vpc/lc> <сумма>")
}
// Handle course command
// Course info
args.isNotEmpty() && args[0] == "course" -> {
// Calculate exchange rate
val course = calculateExchangeRate()
LOGGER.info("Exchange rate calculated: $course")
if (course.isInfinite()) {
LOGGER.error("Zero global balance?")
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
} else if (course == 0.0) {
LOGGER.error("Infinite global balance?")
if (course.isInfinite() || course == 0.0) {
LOGGER.error(if (course.isInfinite()) "Zero global balance?" else "Infinite global balance?")
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
} else {
val course2VPC = Utils.round(course * (1 - COURSE_COMMISSION / 100), NUM_AFTER_DOT)
@ -250,7 +250,7 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
}
}
// Handle authentication command
// Authentication
args.size == 2 && args[0] == "auth" -> {
handleAuthentication(sender, args)
}
@ -258,14 +258,14 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
Utils.send(sender, "/vpi auth <ник>")
}
// Show general help
// Default help menu
else -> {
showHelpMenu(sender)
}
}
return true
}
return false
})
return true
}
/**
@ -320,7 +320,9 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
return if (COURSE_MODE == "static") {
COURSE_STATIC_VALUE
} else {
Utils.round(calculateDynamicRate(), NUM_AFTER_DOT)
val dynamicRate: Any = calculateDynamicRate()
if (dynamicRate != 0.0) Utils.round(dynamicRate, NUM_AFTER_DOT)
else return 0.0
}
}
@ -334,7 +336,8 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
val vpcUser = VpcApi.user_in_db(username = USERNAME)
if (vpcUser == null) {
throw Exception("VPC user not found")
return 0.0
// throw Exception("VPC user not found")
}
val vpcBalance = vpcUser["balance"].toString().toDouble()
@ -349,7 +352,8 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
val vpcUser = VpcApi.user_in_db(username = USERNAME)
if (vpcUser == null) {
throw Exception("VPC user not found")
return 0.0
// throw Exception("VPC user not found")
}
val vpcBalance = vpcUser["balance"].toString().toDouble()
@ -423,6 +427,11 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
// Create new invoice
val invoiceId = VpcApi.create_invoice(amount).toString()
LOGGER.info("Invoice id: $invoiceId")
if (invoiceId == "null") {
Utils.send(sender,"&cПроизошла ошибка при генерации счёта. Обратитесь к администратору или повторите позже.")
return
}
TO_PAY_INVOICES[sender.name] = invoiceId
INVOICES_AMOUNT[invoiceId] = amountLC
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
@ -475,18 +484,23 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
}
// Start new authentication process
val invoiceId = VpcApi.create_invoice(0.001)
val invoiceId = VpcApi.create_invoice(0.001).toString()
val vpcUsernameInput = args[1]
LOGGER.info("Invoice id: $invoiceId")
if (invoiceId == "null") {
Utils.send(sender,"&cПроизошла ошибка при генерации счёта. Обратитесь к администратору или повторите позже.")
return
}
LOGGER.info("Adding player to authentication lists")
synchronized(TO_AUTH_PLAYERS) {
TO_AUTH_PLAYERS[sender.name] = vpcUsernameInput
TO_AUTH_PLAYERS_INVOICES[sender.name] = invoiceId.toString()
INVOICE_CREATION_TIMES[invoiceId.toString()] = System.currentTimeMillis()
TO_AUTH_PLAYERS_INVOICES[sender.name] = invoiceId
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
}
LOGGER.info("Authentication lists updated")
sendAuthPrompt(sender, invoiceId.toString())
sendAuthPrompt(sender, invoiceId)
}
/**