Update to use a centralized fetcher

This commit is contained in:
downthecrop 2025-11-14 23:20:00 -08:00
parent 6adc5135bc
commit 03a205f64b
9 changed files with 169 additions and 128 deletions

View file

@ -0,0 +1,60 @@
package KondoKit.network
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
/**
* Lightweight helper for opening GET connections with shared defaults so callers don't repeat header setup.
*/
object HttpFetcher {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
private val defaultHeaders = mapOf(
"User-Agent" to DEFAULT_USER_AGENT,
"Accept" to "*/*"
)
fun openGetConnection(
url: String,
headers: Map<String, String> = emptyMap(),
connectTimeoutMillis: Int? = null,
readTimeoutMillis: Int? = null
): HttpURLConnection {
val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = "GET"
(defaultHeaders + headers).forEach { (key, value) ->
connection.setRequestProperty(key, value)
}
connectTimeoutMillis?.let { connection.connectTimeout = it }
readTimeoutMillis?.let { connection.readTimeout = it }
return connection
}
fun fetchString(
url: String,
headers: Map<String, String> = emptyMap(),
connectTimeoutMillis: Int? = null,
readTimeoutMillis: Int? = null
): String {
val connection = openGetConnection(url, headers, connectTimeoutMillis, readTimeoutMillis)
val responseCode = connection.responseCode
val responseStream = if (responseCode in 200..299) {
connection.inputStream
} else {
connection.errorStream
} ?: throw IOException("No response stream available for $url (HTTP $responseCode)")
val payload = responseStream.bufferedReader().use { it.readText() }
if (responseCode !in 200..299) {
throw HttpStatusException(url, responseCode, payload)
}
return payload
}
}
class HttpStatusException(
val url: String,
val statusCode: Int,
val body: String
) : IOException("Request to $url failed with HTTP $statusCode")

View file

@ -10,9 +10,6 @@ object GitLabConfig {
const val GITLAB_PROJECT_PATH = "2009scape/tools/client-plugins" // Using path from DownloadManager
const val GITLAB_BRANCH = "master"
// HTTP headers and client settings
const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
// Debug settings
const val DEBUG = true // Set to false to disable debug logging
@ -34,4 +31,4 @@ object GitLabConfig {
fun getArchiveUrlWithRefType(pluginPath: String): String =
"${getArchiveBaseUrl()}?path=$pluginPath&ref_type=heads"
}
}

View file

@ -1,16 +1,14 @@
package KondoKit.pluginmanager
import KondoKit.views.ReflectiveEditorView
import com.google.gson.Gson
import com.google.gson.JsonObject
import java.awt.EventQueue
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.*
import javax.swing.SwingUtilities
import KondoKit.network.HttpFetcher
import KondoKit.pluginmanager.GitLabConfig
import KondoKit.util.JsonParser
object GitLabPluginFetcher {
@ -33,12 +31,7 @@ object GitLabPluginFetcher {
val apiUrl = GitLabConfig.getRepositoryTreeUrl()
debugLog("API URL: $apiUrl")
// Create URL connection
val url = URL(apiUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", GitLabConfig.CHROME_USER_AGENT)
debugLog("Set User-Agent header to: ${GitLabConfig.CHROME_USER_AGENT}")
val connection = HttpFetcher.openGetConnection(apiUrl)
// Read response
val responseCode = connection.responseCode
@ -51,8 +44,7 @@ object GitLabPluginFetcher {
debugLog("Response preview: ${response.take(200)}...")
// Parse JSON response
val gson = Gson()
val treeItems = gson.fromJson(response, Array<JsonObject>::class.java)
val treeItems = JsonParser.fromJson(response, Array<JsonObject>::class.java)
debugLog("Parsed ${treeItems.size} items from JSON")
// Filter for directories (trees) with mode "040000"
@ -119,11 +111,7 @@ object GitLabPluginFetcher {
val pluginUrl = GitLabConfig.getRawFileUrl(pluginFilePath)
debugLog("Fetching plugin.properties from: $pluginUrl")
// Create URL connection
val url = URL(pluginUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", GitLabConfig.CHROME_USER_AGENT)
val connection = HttpFetcher.openGetConnection(pluginUrl)
// Read response
val responseCode = connection.responseCode
@ -179,4 +167,4 @@ object GitLabPluginFetcher {
debugLog("Parsed properties - Author: $author, Version: $version, Description: $description")
return PluginProperties(author, version, description)
}
}
}

View file

@ -11,6 +11,7 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import javax.swing.SwingUtilities
import KondoKit.pluginmanager.GitLabConfig
import KondoKit.network.HttpFetcher
/**
* Manages downloading and installing plugins from GitLab with concurrent support
@ -127,11 +128,12 @@ object PluginDownloadManager {
}
// Create URL connection
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", GitLabConfig.CHROME_USER_AGENT)
// Add Accept header to avoid 406 errors
connection.setRequestProperty("Accept", "*/*")
val connection = try {
HttpFetcher.openGetConnection(url.toString())
} catch (e: Exception) {
debugLog("Failed to open connection for plugin: ${plugin.path} with URL: $downloadUrl. Error: ${e.message}")
continue
}
debugLog("Connection opened for plugin: ${plugin.path}")
debugLog("Request headers - User-Agent: ${connection.getRequestProperty("User-Agent")}")
@ -367,4 +369,4 @@ object PluginDownloadManager {
return false
}
}
}
}

View file

@ -0,0 +1,48 @@
package KondoKit.util
import KondoKit.network.HttpFetcher
/**
* Shared loader for GE price JSON from remote or bundled sources.
*/
object GEPriceService {
private const val REMOTE_GE_URL = "https://cdn.2009scape.org/gedata/latest.json"
private data class RemoteGEItem(
val item_id: String?,
val value: String?
)
private data class LocalGEItem(
val id: String?,
val grand_exchange_price: String?
)
fun loadRemotePrices(): Map<String, String> {
return try {
val payload = HttpFetcher.fetchString(REMOTE_GE_URL)
val items = JsonParser.fromJson<Array<RemoteGEItem>>(payload)
items.mapNotNull { item ->
val id = item.item_id
val value = item.value
if (id != null && value != null) id to value else null
}.toMap()
} catch (e: Exception) {
emptyMap()
}
}
fun loadLocalPrices(jsonProvider: () -> String?): Map<String, String> {
return try {
val json = jsonProvider() ?: return emptyMap()
val items = JsonParser.fromJson<Array<LocalGEItem>>(json)
items.mapNotNull { item ->
val id = item.id
val price = item.grand_exchange_price
if (id != null && price != null) id to price else null
}.toMap()
} catch (e: Exception) {
emptyMap()
}
}
}

View file

@ -0,0 +1,20 @@
package KondoKit.util
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
/**
* Centralizes JSON parsing to reuse the same Gson configuration everywhere.
*/
object JsonParser {
val gson: Gson = Gson()
inline fun <reified T> fromJson(json: String): T {
val type = object : TypeToken<T>() {}.type
return gson.fromJson(json, type)
}
fun <T> fromJson(json: String, classOfT: Class<T>): T = gson.fromJson(json, classOfT)
fun toJson(src: Any): String = gson.toJson(src)
}

View file

@ -15,15 +15,13 @@ import KondoKit.plugin.Companion.POPUP_FOREGROUND
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.TOOLTIP_BACKGROUND
import com.google.gson.Gson
import KondoKit.util.JsonParser
import plugin.api.API
import rt4.Sprites
import java.awt.*
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import KondoKit.network.HttpFetcher
import KondoKit.network.HttpStatusException
import java.net.SocketTimeoutException
import java.net.URL
import javax.swing.*
import javax.swing.border.MatteBorder
import kotlin.math.floor
@ -235,37 +233,24 @@ object HiscoresView : View {
Thread {
try {
val url = URL(apiUrl)
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
val response = HttpFetcher.fetchString(
url = apiUrl,
connectTimeoutMillis = 5000,
readTimeoutMillis = 5000
)
// If a request take longer than 5 seconds timeout.
connection.connectTimeout = 5000
connection.readTimeout = 5000
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val reader = BufferedReader(InputStreamReader(connection.inputStream))
val response = reader.use { it.readText() }
reader.close()
SwingUtilities.invokeLater {
updatePlayerDataStandalone(response, username, hiscoresPanel)
// Example of how to use setText now that the search field is accessible:
// customSearchField?.setText(username) // You could set the text to the searched username
}
} else {
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Player not found!", JOptionPane.ERROR_MESSAGE)
}
SwingUtilities.invokeLater {
updatePlayerDataStandalone(response, username, hiscoresPanel)
}
} catch (e: SocketTimeoutException) {
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Request timed out", JOptionPane.ERROR_MESSAGE)
}
} catch (e: HttpStatusException) {
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Player not found!", JOptionPane.ERROR_MESSAGE)
}
} catch (e: Exception) {
// Handle other errors
SwingUtilities.invokeLater {
showToast(hiscoresPanel, "Error fetching data!", JOptionPane.ERROR_MESSAGE)
}
@ -274,8 +259,7 @@ object HiscoresView : View {
}
private fun updatePlayerDataStandalone(jsonResponse: String, username: String, hiscoresPanel: JPanel) {
val gson = Gson()
val hiscoresResponse = gson.fromJson(jsonResponse, HiscoresResponse::class.java)
val hiscoresResponse = JsonParser.fromJson<HiscoresResponse>(jsonResponse)
updateHiscoresViewStandalone(hiscoresPanel, hiscoresResponse, username)
}

View file

@ -28,9 +28,7 @@ import java.awt.Font
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.image.BufferedImage
import java.io.BufferedReader
import java.net.HttpURLConnection
import java.net.URL
import KondoKit.util.GEPriceService
import java.text.DecimalFormat
import javax.swing.*
import kotlin.math.ceil
@ -73,60 +71,13 @@ object LootTrackerView : View, OnPostClientTickCallback, OnKillingBlowNPCCallbac
npcDeathSnapshots[npcID] = GroundSnapshot(preDeathSnapshot, Pair(x, z), 0)
}
fun loadGEPrices(): Map<String, String> {
fun loadGEPrices(): Map<String, String> {
return if (useLiveGEPrices) {
try {
println("LootTracker: Loading Remote GE Prices")
val url = URL("https://cdn.2009scape.org/gedata/latest.json")
val connection = url.openConnection() as HttpURLConnection
connection.requestMethod = "GET"
connection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0")
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
val inputStream = connection.inputStream
val content = inputStream.bufferedReader().use(BufferedReader::readText)
val items = content.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val gePrices = mutableMapOf<String, String>()
for (item in items) {
val pairs = item.removeSurrounding("{", "}").split(",")
val itemId = pairs.find { it.trim().startsWith("\"item_id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
val value = pairs.find { it.trim().startsWith("\"value\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
if (itemId != null && value != null) {
gePrices[itemId] = value
}
}
gePrices
} else {
emptyMap()
}
} catch (e: Exception) {
emptyMap()
}
println("LootTracker: Loading Remote GE Prices")
GEPriceService.loadRemotePrices()
} else {
try {
println("LootTracker: Loading Local GE Prices")
Helpers.readResourceText("res/item_configs.json")?.let { json ->
val items = json.trim().removeSurrounding("[", "]").split("},").map { it.trim() + "}" }
val gePrices = mutableMapOf<String, String>()
for (item in items) {
val pairs = item.removeSurrounding("{", "}").split(",")
val id = pairs.find { it.trim().startsWith("\"id\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
val grandExchangePrice = pairs.find { it.trim().startsWith("\"grand_exchange_price\"") }?.split(":")?.get(1)?.trim()?.trim('\"')
if (id != null && grandExchangePrice != null) {
gePrices[id] = grandExchangePrice
}
}
gePrices
} ?: emptyMap()
} catch (e: Exception) {
emptyMap()
}
println("LootTracker: Loading Local GE Prices")
GEPriceService.loadLocalPrices { Helpers.readResourceText("res/item_configs.json") }
}
}

View file

@ -23,6 +23,7 @@ import KondoKit.plugin.Companion.playerXPMultiplier
import KondoKit.plugin.Companion.primaryColor
import KondoKit.plugin.Companion.secondaryColor
import KondoKit.plugin.StateManager.focusedView
import KondoKit.util.JsonParser
import plugin.api.API
import java.awt.*
import java.awt.image.BufferedImage
@ -43,20 +44,10 @@ object XPTrackerView : View, OnUpdateCallback, OnXPUpdateCallback {
val npcHitpointsMap: Map<Int, Int> = try {
val json = Helpers.readResourceText("res/npc_hitpoints_map.json") ?: "{}"
val pairs = json.trim().removeSurrounding("{", "}").split(",")
val map = mutableMapOf<Int, Int>()
for (pair in pairs) {
val keyValue = pair.split(":")
val id = keyValue[0].trim().trim('\"').toIntOrNull()
val hitpoints = keyValue[1].trim()
if (id != null && hitpoints.isNotEmpty()) {
map[id] = hitpoints.toIntOrNull() ?: 0
}
}
map
val parsed: Map<String, Int> = JsonParser.fromJson(json)
parsed.mapNotNull { (key, value) ->
key.toIntOrNull()?.let { it to value }
}.toMap()
} catch (e: Exception) {
println("XPTracker Error parsing NPC HP: ${e.message}")
emptyMap()