Исправление багов: избегаем ограничение запросов, корректная получение вывода команды, корректные ошибки при осутствии соединения UserAPI.
Доработки: асинхронность для всех команд.
This commit is contained in:
parent
0ce332f050
commit
df0561fe51
@ -4,8 +4,11 @@ package main.VpcSpigotIntegration
|
|||||||
// Here is implemented capture of output command (for dynamic course)
|
// Here is implemented capture of output command (for dynamic course)
|
||||||
// ----------------------------------------------
|
// ----------------------------------------------
|
||||||
|
|
||||||
|
import main.VpcSpigotIntegration.VpcServerIntegration.Companion.PLUGIN
|
||||||
import org.bukkit.Bukkit
|
import org.bukkit.Bukkit
|
||||||
import org.bukkit.command.CommandSender
|
import org.bukkit.command.CommandSender
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class CommandCapture {
|
class CommandCapture {
|
||||||
companion object {
|
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 output = mutableListOf<String>()
|
||||||
|
|
||||||
val customSender = createCapturingSender { message ->
|
val customSender = createCapturingSender { message ->
|
||||||
@ -45,5 +49,61 @@ class CommandCapture {
|
|||||||
|
|
||||||
return output
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11,8 +11,7 @@ class TotalBalanceModules {
|
|||||||
fun getEssentialsBalance(): Float {
|
fun getEssentialsBalance(): Float {
|
||||||
LOGGER.info("Calculating dynamic rate using baltop")
|
LOGGER.info("Calculating dynamic rate using baltop")
|
||||||
// Force update balance top and delay to avoid empty string
|
// Force update balance top and delay to avoid empty string
|
||||||
CommandCapture.execute("baltop force")
|
val output = CommandCapture.execute("baltop force")
|
||||||
val output = CommandCapture.execute("baltop")
|
|
||||||
LOGGER.info("Balance top command output: $output")
|
LOGGER.info("Balance top command output: $output")
|
||||||
|
|
||||||
var total = 0.0f
|
var total = 0.0f
|
||||||
|
|||||||
@ -23,10 +23,10 @@ import kotlin.math.max
|
|||||||
|
|
||||||
class VpcApi {
|
class VpcApi {
|
||||||
companion object {
|
companion object {
|
||||||
// Rate limiting: track timestamps of requests
|
// Rate limiting: lock and last execution time
|
||||||
private val requestTimestamps = ConcurrentLinkedQueue<Long>()
|
private val rateLimitLock = Object()
|
||||||
private const val MAX_REQUESTS_PER_MINUTE = 60
|
private var lastExecutionTime = 0L
|
||||||
private const val MINUTE_IN_MILLIS = 60 * 1000L
|
private const val MIN_INTERVAL_MILLIS = 1000L // 1 second between requests
|
||||||
|
|
||||||
// Using Gson for JSON serialization - much cleaner than manual string building
|
// Using Gson for JSON serialization - much cleaner than manual string building
|
||||||
fun jsonify(args: List<String>): String {
|
fun jsonify(args: List<String>): String {
|
||||||
@ -49,37 +49,27 @@ class VpcApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces rate limiting by checking if we've exceeded the request limit.
|
* Enforces rate limiting by ensuring requests are executed one at a time
|
||||||
* If so, sleeps until we're within the limit again.
|
* with at least 1 second between them.
|
||||||
*/
|
*/
|
||||||
private fun enforceRateLimit() {
|
private fun enforceRateLimit() {
|
||||||
val now = System.currentTimeMillis()
|
synchronized(rateLimitLock) {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val timeSinceLastExecution = now - lastExecutionTime
|
||||||
|
|
||||||
// Remove timestamps older than 1 minute
|
// If less than 1 second has passed since last execution, wait
|
||||||
while (requestTimestamps.isNotEmpty() && now - requestTimestamps.peek() > MINUTE_IN_MILLIS) {
|
if (timeSinceLastExecution < MIN_INTERVAL_MILLIS) {
|
||||||
requestTimestamps.poll()
|
val sleepTime = MIN_INTERVAL_MILLIS - timeSinceLastExecution
|
||||||
}
|
try {
|
||||||
|
Thread.sleep(sleepTime)
|
||||||
// If we've hit the limit, calculate sleep time
|
} catch (e: InterruptedException) {
|
||||||
if (requestTimestamps.size >= MAX_REQUESTS_PER_MINUTE) {
|
Thread.currentThread().interrupt()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// After sleeping, clean up old timestamps again
|
// Update last execution time to now
|
||||||
val afterSleep = System.currentTimeMillis()
|
lastExecutionTime = System.currentTimeMillis()
|
||||||
while (requestTimestamps.isNotEmpty() && afterSleep - requestTimestamps.peek() > MINUTE_IN_MILLIS) {
|
|
||||||
requestTimestamps.poll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current timestamp
|
|
||||||
requestTimestamps.offer(now)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendPost(urlPoint: String, json: String, customUrl: Boolean = false, baseurl: String = ""): ApiResponse {
|
fun sendPost(urlPoint: String, json: String, customUrl: Boolean = false, baseurl: String = ""): ApiResponse {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import java.io.FileInputStream
|
|||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
import kotlin.collections.mutableMapOf
|
import kotlin.collections.mutableMapOf
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.text.isEmpty
|
||||||
|
|
||||||
class VpcServerIntegration() : JavaPlugin(), CommandExecutor {
|
class VpcServerIntegration() : JavaPlugin(), CommandExecutor {
|
||||||
companion object {
|
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 {
|
override fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
|
||||||
if (command.name.equals("vpi", ignoreCase = true)) {
|
if (!command.name.equals("vpi", ignoreCase = true)) return false
|
||||||
if (sender !is Player) {
|
|
||||||
sender.sendMessage("&cТолько игроки могут выполнять данные команды.")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (sender !is Player) {
|
||||||
|
sender.sendMessage("&cТолько игроки могут выполнять данные команды.")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all logic asynchronously
|
||||||
|
Bukkit.getScheduler().runTaskAsynchronously(PLUGIN, Runnable {
|
||||||
when {
|
when {
|
||||||
// Handle currency conversion command
|
// Currency conversion
|
||||||
(args.size == 3 || args.size == 4) && args[0] == "convert" -> {
|
args.size in 3..4 && args[0] == "convert" -> {
|
||||||
handleCurrencyConversion(sender, args)
|
handleCurrencyConversion(sender, args)
|
||||||
}
|
}
|
||||||
args.isNotEmpty() && args[0] == "convert" -> {
|
args.isNotEmpty() && args[0] == "convert" -> {
|
||||||
Utils.send(sender, "/vpi convert <vpc/lc> <сумма>")
|
Utils.send(sender, "/vpi convert <vpc/lc> <сумма>")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle course command
|
// Course info
|
||||||
args.isNotEmpty() && args[0] == "course" -> {
|
args.isNotEmpty() && args[0] == "course" -> {
|
||||||
// Calculate exchange rate
|
|
||||||
val course = calculateExchangeRate()
|
val course = calculateExchangeRate()
|
||||||
LOGGER.info("Exchange rate calculated: $course")
|
LOGGER.info("Exchange rate calculated: $course")
|
||||||
if (course.isInfinite()) {
|
|
||||||
LOGGER.error("Zero global balance?")
|
|
||||||
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
|
|
||||||
|
|
||||||
} else if (course == 0.0) {
|
if (course.isInfinite() || course == 0.0) {
|
||||||
LOGGER.error("Infinite global balance?")
|
LOGGER.error(if (course.isInfinite()) "Zero global balance?" else "Infinite global balance?")
|
||||||
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
|
Utils.send(sender, "&cПроизошла ошибка при расчёте курса. Обратитесь к администратору или повторите позже.")
|
||||||
} else {
|
} else {
|
||||||
val course2VPC = Utils.round(course * (1 - COURSE_COMMISSION / 100), NUM_AFTER_DOT)
|
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" -> {
|
args.size == 2 && args[0] == "auth" -> {
|
||||||
handleAuthentication(sender, args)
|
handleAuthentication(sender, args)
|
||||||
}
|
}
|
||||||
@ -258,14 +258,14 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
|
|||||||
Utils.send(sender, "/vpi auth <ник>")
|
Utils.send(sender, "/vpi auth <ник>")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show general help
|
// Default help menu
|
||||||
else -> {
|
else -> {
|
||||||
showHelpMenu(sender)
|
showHelpMenu(sender)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
})
|
||||||
}
|
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -320,7 +320,9 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
|
|||||||
return if (COURSE_MODE == "static") {
|
return if (COURSE_MODE == "static") {
|
||||||
COURSE_STATIC_VALUE
|
COURSE_STATIC_VALUE
|
||||||
} else {
|
} 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)
|
val vpcUser = VpcApi.user_in_db(username = USERNAME)
|
||||||
if (vpcUser == null) {
|
if (vpcUser == null) {
|
||||||
throw Exception("VPC user not found")
|
return 0.0
|
||||||
|
// throw Exception("VPC user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
val vpcBalance = vpcUser["balance"].toString().toDouble()
|
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)
|
val vpcUser = VpcApi.user_in_db(username = USERNAME)
|
||||||
|
|
||||||
if (vpcUser == null) {
|
if (vpcUser == null) {
|
||||||
throw Exception("VPC user not found")
|
return 0.0
|
||||||
|
// throw Exception("VPC user not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
val vpcBalance = vpcUser["balance"].toString().toDouble()
|
val vpcBalance = vpcUser["balance"].toString().toDouble()
|
||||||
@ -423,6 +427,11 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
|
|||||||
|
|
||||||
// Create new invoice
|
// Create new invoice
|
||||||
val invoiceId = VpcApi.create_invoice(amount).toString()
|
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
|
TO_PAY_INVOICES[sender.name] = invoiceId
|
||||||
INVOICES_AMOUNT[invoiceId] = amountLC
|
INVOICES_AMOUNT[invoiceId] = amountLC
|
||||||
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
|
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
|
||||||
@ -475,18 +484,23 @@ invoice_timeout_seconds=$DEFAULT_INVOICE_TIMEOUT_SECONDS
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start new authentication process
|
// Start new authentication process
|
||||||
val invoiceId = VpcApi.create_invoice(0.001)
|
val invoiceId = VpcApi.create_invoice(0.001).toString()
|
||||||
val vpcUsernameInput = args[1]
|
val vpcUsernameInput = args[1]
|
||||||
|
LOGGER.info("Invoice id: $invoiceId")
|
||||||
|
if (invoiceId == "null") {
|
||||||
|
Utils.send(sender,"&cПроизошла ошибка при генерации счёта. Обратитесь к администратору или повторите позже.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
LOGGER.info("Adding player to authentication lists")
|
LOGGER.info("Adding player to authentication lists")
|
||||||
synchronized(TO_AUTH_PLAYERS) {
|
synchronized(TO_AUTH_PLAYERS) {
|
||||||
TO_AUTH_PLAYERS[sender.name] = vpcUsernameInput
|
TO_AUTH_PLAYERS[sender.name] = vpcUsernameInput
|
||||||
TO_AUTH_PLAYERS_INVOICES[sender.name] = invoiceId.toString()
|
TO_AUTH_PLAYERS_INVOICES[sender.name] = invoiceId
|
||||||
INVOICE_CREATION_TIMES[invoiceId.toString()] = System.currentTimeMillis()
|
INVOICE_CREATION_TIMES[invoiceId] = System.currentTimeMillis()
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Authentication lists updated")
|
LOGGER.info("Authentication lists updated")
|
||||||
sendAuthPrompt(sender, invoiceId.toString())
|
sendAuthPrompt(sender, invoiceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user