diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 16ba27c..0fbcd20 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -36,6 +36,28 @@ const bootstrap = async () => { console.log("Default user created"); }; + +const connectedClients = new Set(); +const broadcastServerChange = (action: string, serverType: string, serverId: string) => { + const message = { + type: "SERVER_CHANGE", + action, + serverType, + serverId, + timestamp: new Date().toISOString(), + }; + + connectedClients.forEach(client => { + try { + client.send(JSON.stringify(message)); + } catch (error) { + console.error("Error sending WebSocket message:", error); + connectedClients.delete(client); + } + }); + console.log(`Notified Velocity proxy: ${action} ${serverType} ${serverId}`); +}; + const app = new Elysia() .use(swagger({ path: '/swagger', @@ -46,6 +68,26 @@ const app = new Elysia() } } })) + .ws("/ws", { + open(ws) { + const apiKey = ws.data.query.apiKey; + if (!apiKey) { + console.log("apiKey required"); + ws.close(); + return; + } + + connectedClients.add(ws); + console.log("Velocity proxy connected via WebSocket"); + }, + close(ws) { + connectedClients.delete(ws); + console.log("Velocity proxy disconnected from WebSocket"); + }, + message(ws, message) { + console.log("Received message from Velocity proxy:", message); + }, + }) .group('/api', app => app .derive(async ({ headers, cookie: { session_token }, path }) => { // Skip token validation for login route @@ -150,19 +192,6 @@ const app = new Elysia() }), } ) - .ws("/ws", { - body: t.Object({ - message: t.String(), - }), - message(ws, { message }) { - const { id } = ws.data.query; - ws.send({ - id, - message, - time: Date.now(), - }); - }, - }) .post("/logout", async ({ session, cookie: { session_token } }) => { if (!session) return { success: true }; @@ -175,6 +204,24 @@ const app = new Elysia() }; }) .get("/servers", async ({ session }) => { + // Broadcast to all connected WebSocket clients + const message = { + type: "test", + endpoint: "/servers", + timestamp: new Date().toISOString(), + }; + + connectedClients.forEach(client => { + try { + client.send(JSON.stringify(message)); + } catch (error) { + console.error("Error sending WebSocket message:", error); + connectedClients.delete(client); + } + }); + + console.log(`/servers API called, notified ${connectedClients.size} WebSocket clients`); + return await ServerService.getAllServers(!session); }) .get("/servers/:id", async ({ session, params: { id } }) => { @@ -205,6 +252,8 @@ const app = new Elysia() memory: body.memory, }); + broadcastServerChange("CREATE", "SERVER", server.id,); + return { success: true, data: { @@ -252,6 +301,8 @@ const app = new Elysia() const newServer = await ServerService.getServerById(id, !session); + broadcastServerChange("UPDATE", "SERVER", server.id); + return { success: true, data: { @@ -280,6 +331,8 @@ const app = new Elysia() where: { id }, }); + broadcastServerChange("DELETE", "SERVER", server.id); + return { success: true, }; @@ -314,6 +367,8 @@ const app = new Elysia() memory: body.memory, }); + broadcastServerChange("CREATE", "REVERSE_PROXY_SERVER", server.id); + return { success: true, data: { @@ -371,6 +426,8 @@ const app = new Elysia() !session ); + broadcastServerChange("UPDATE", "REVERSE_PROXY_SERVER", server.id); + return { success: true, data: { @@ -404,6 +461,8 @@ const app = new Elysia() where: { id }, }); + broadcastServerChange("DELETE", "REVERSE_PROXY_SERVER", server.id); + return { success: true, }; diff --git a/bun.lockb b/bun.lockb index df22dcc..265b231 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e1d5235 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + db: + image: postgres:17 + environment: + POSTGRES_PASSWORD: postgres + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + +volumes: + postgres_data: diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/Main.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/Main.kt index b9b64d3..25e6674 100644 --- a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/Main.kt +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/Main.kt @@ -41,8 +41,8 @@ class Main @Inject constructor(private val logger: Logger, private val server: P private val servers: MutableMap = HashMap() private val client = OkHttpClient() private val apiKey: String = System.getenv("MINIKURA_API_KEY") ?: "" - private val apiUrl: String = System.getenv("MINIKURA_API_URL") ?: "http://localhost:3000" - private val websocketUrl: String = System.getenv("MINIKURA_WEBSOCKET_URL") ?: "ws://localhost:3000/ws" + private val apiUrl: String = System.getenv("MINIKURA_API_URL") ?: "http://localhost:3000/api" + private val websocketUrl: String = System.getenv("MINIKURA_WEBSOCKET_URL") ?: "ws://localhost:3000/ws?apiKey=$apiKey" private var acceptingTransfers = AtomicBoolean(false) private val redisBungeeApi = RedisBungeeAPI.getRedisBungeeApi() @@ -87,6 +87,10 @@ class Main @Inject constructor(private val logger: Logger, private val server: P .plugin(this) .build() + val getallServerCommandMeta: CommandMeta = commandManager.metaBuilder("getallserver") + .plugin(this) + .build() + // TODO: Rework this command and support and arguments val migrateCommand = SimpleCommand { p -> val source = p.source() @@ -114,10 +118,15 @@ class Main @Inject constructor(private val logger: Logger, private val server: P source.sendMessage(Component.text("Migrating players to server '$targetServerName'...")) } + val getallServerCommand = SimpleCommand { p -> + getallServer() + } + commandManager.register(serversCommandMeta, ServerCommand.createServerCommand(server)) commandManager.register(endCommandMeta, EndCommand.createEndCommand(server)) commandManager.register(refreshCommandMeta, refreshCommand) commandManager.register(migrateCommandMeta, migrateCommand) + commandManager.register(getallServerCommandMeta, getallServerCommand) val connectionHandler = ProxyTransferHandler(servers, logger, acceptingTransfers) server.eventManager.register(this, connectionHandler) @@ -132,6 +141,22 @@ class Main @Inject constructor(private val logger: Logger, private val server: P logger.info("Minikura-Velocity has been initialized.") } + fun refreshServers() { + fetchServers() + fetchReverseProxyServers() + } + + fun getallServer() { + synchronized(ServerDataStore) { + ServerDataStore.getServers().forEach { serverData -> + logger.info("Server ID: ${serverData.id}, Type: ${serverData.type}, Description: ${serverData.description}") + } + ServerDataStore.getReverseProxyServers().forEach { reverseProxyServerData -> + logger.info("Reverse Proxy Server ID: ${reverseProxyServerData.id}, External Address: ${reverseProxyServerData.external_address}, External Port: ${reverseProxyServerData.external_port}") + } + } + } + private fun fetchReverseProxyServers() { val request = Request.Builder() .url("$apiUrl/reverse_proxy_servers") @@ -207,9 +232,9 @@ class Main @Inject constructor(private val logger: Logger, private val server: P servers.clear() for (data in serversData) { - val serverInfo = ServerInfo(data.name, InetSocketAddress(data.address, data.port)) + val serverInfo = ServerInfo(data.id, InetSocketAddress("localhost", data.listen_port)) val registeredServer = server.createRawRegisteredServer(serverInfo) - servers[data.name] = registeredServer + servers[data.id] = registeredServer this.server.registerServer(registeredServer.serverInfo) } } diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/MinikuraWebSocketClient.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/MinikuraWebSocketClient.kt index 94fa9f6..5c73af2 100644 --- a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/MinikuraWebSocketClient.kt +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/MinikuraWebSocketClient.kt @@ -6,22 +6,105 @@ import org.java_websocket.handshake.ServerHandshake import org.slf4j.Logger import java.net.URI import java.time.Duration +import com.google.gson.JsonParser class MinikuraWebSocketClient(private val plugin: Main, private val logger: Logger, private val server: ProxyServer, serverUri: URI?) : WebSocketClient(serverUri) { + override fun onOpen(handshakedata: ServerHandshake) { - logger.info("Connected to websocket") + logger.info("Connected to WebSocket server at: ${uri}") } override fun onMessage(message: String) { logger.debug("Received: $message") + + try { + val jsonElement = JsonParser.parseString(message) + if (jsonElement.isJsonObject) { + val jsonObject = jsonElement.asJsonObject + + val type = jsonObject.get("type")?.asString + val endpoint = jsonObject.get("endpoint")?.asString + val timestamp = jsonObject.get("timestamp")?.asString + + when (type) { + "test" -> { + logger.info("API Call detected: endpoint=$endpoint, timestamp=$timestamp") + + when (endpoint) { + "/servers" -> { + logger.info("dawdawdawdawd") + } + else -> { + logger.info("API endpoint $endpoint was accessed") + } + } + } + "SERVER_CHANGE" -> { + val action = jsonObject.get("action")?.asString + val serverType = jsonObject.get("serverType")?.asString + val serverId = jsonObject.get("serverId")?.asString + + logger.info("Server change detected: action=$action, serverType=$serverType, serverId=$serverId") + + when (action) { + "CREATE" -> { + logger.info("Server '$serverId' was created") + executeRefreshCommand() + } + "UPDATE" -> { + logger.info("Server '$serverId' was updated") + executeRefreshCommand() + } + "DELETE" -> { + logger.info("Server '$serverId' was deleted") + executeRefreshCommand() + } + else -> { + logger.info("$action") + } + } + } + else -> { + logger.info("Received WebSocket message of type: $type") + } + } + } else { + logger.info("Received non-JSON WebSocket message: $message") + } + } catch (e: Exception) { + logger.warn("Failed to parse WebSocket message: $message", e) + } } override fun onError(ex: Exception) { - ex.printStackTrace() + when (ex) { + is java.net.ConnectException -> { + logger.warn("Failed to connect to WebSocket server at: ${uri}") + } + else -> { + logger.error("WebSocket error occurred", ex) + } + } } override fun onClose(code: Int, reason: String, remote: Boolean) { - logger.info("Connection closed, attempting to reconnect...") - server.scheduler.buildTask(plugin, Runnable { reconnect() }).delay(Duration.ofMillis(5000)).schedule() + logger.info("WebSocket connection closed (code: $code, reason: $reason)") + } + + private fun executeRefreshCommand() { + try { + server.scheduler.buildTask(plugin, Runnable { + try { + logger.info("Executing automatic server refresh...") + plugin.refreshServers() + plugin.getallServer() + logger.info("Server refresh completed successfully") + } catch (e: Exception) { + logger.error("Failed to refresh servers", e) + } + }).schedule() + } catch (e: Exception) { + logger.error("Failed to schedule refresh command", e) + } } } \ No newline at end of file diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/datastore/ServerDataStore.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/datastore/ServerDataStore.kt index e278d05..15a58d4 100644 --- a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/datastore/ServerDataStore.kt +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/datastore/ServerDataStore.kt @@ -29,11 +29,11 @@ object ServerDataStore { } fun getServer(name: String): ServerData? { - return servers.find { it.name == name } + return servers.find { it.id == name } } fun getReverseProxyServer(name: String): ReverseProxyServerData? { - return reverseProxyServers.find { it.name == name } + return reverseProxyServers.find { it.id == name } } fun getServers(): List { diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/listeners/ProxyTransferHandler.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/listeners/ProxyTransferHandler.kt index 7c3e177..17f65f8 100644 --- a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/listeners/ProxyTransferHandler.kt +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/listeners/ProxyTransferHandler.kt @@ -124,9 +124,7 @@ class ProxyTransferHandler( } val sortedServers = ServerDataStore.getServers() - .filter { it.join_priority != null } - .sortedBy { it.join_priority } - .mapNotNull { servers[it.name] } + .mapNotNull { servers[it.id] } for (server in sortedServers) { if (server != currentServer) { diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/CustomEnvironmentVariableData.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/CustomEnvironmentVariableData.kt new file mode 100644 index 0000000..847f664 --- /dev/null +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/CustomEnvironmentVariableData.kt @@ -0,0 +1,15 @@ +package cafe.kirameki.minikuraVelocity.models + +import java.time.LocalDateTime + +data class CustomEnvironmentVariableData( + val id: String, + val key: String, + val value: String, + val created_at: String, + val updated_at: String, + val server_id: String? = null, + val server: ServerData? = null, + val reverse_proxy_id: String? = null, + val reverse_proxy_server: ReverseProxyServerData? = null +) \ No newline at end of file diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ReverseProxyServerData.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ReverseProxyServerData.kt index f3fb034..aa33fac 100644 --- a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ReverseProxyServerData.kt +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ReverseProxyServerData.kt @@ -1,12 +1,18 @@ package cafe.kirameki.minikuraVelocity.models +import cafe.kirameki.minikuraVelocity.models.components.ReverseProxyServerType +import java.time.LocalDateTime + data class ReverseProxyServerData( val id: String, - val name: String, + val type: ReverseProxyServerType, val description: String?, - val address: String, - val port: Int, + val external_address: String, + val external_port: Int, + val listen_port: Int = 25565, + val memory: String = "512M", val api_key: String, + val env_variables: List = emptyList(), val created_at: String, val updated_at: String ) \ No newline at end of file diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ServerData.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ServerData.kt index f0c5481..f7d0608 100644 --- a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ServerData.kt +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/ServerData.kt @@ -1,13 +1,15 @@ package cafe.kirameki.minikuraVelocity.models +import cafe.kirameki.minikuraVelocity.models.components.ServerType +import java.time.LocalDateTime + data class ServerData( val id: String, - val name: String, + val type: ServerType, val description: String?, - val address: String, - val port: Int, - val type: String, - val join_priority: Int?, + val listen_port: Int = 25565, + val memory: String = "1G", + val env_variables: List = emptyList(), val api_key: String, val created_at: String, val updated_at: String diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/components/ReverseProxyServerType.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/components/ReverseProxyServerType.kt new file mode 100644 index 0000000..308d495 --- /dev/null +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/components/ReverseProxyServerType.kt @@ -0,0 +1,5 @@ +package cafe.kirameki.minikuraVelocity.models.components + +enum class ReverseProxyServerType { + VELOCITY, BUNGEECORD +} \ No newline at end of file diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/components/ServerType.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/components/ServerType.kt new file mode 100644 index 0000000..526cef2 --- /dev/null +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/models/components/ServerType.kt @@ -0,0 +1,5 @@ +package cafe.kirameki.minikuraVelocity.models.components + +enum class ServerType { + STATEFUL, STATELESS, +} \ No newline at end of file diff --git a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/utils/ProxyTransferUtils.kt b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/utils/ProxyTransferUtils.kt index 86335d9..e67a336 100644 --- a/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/utils/ProxyTransferUtils.kt +++ b/plugins/MinikuraVelocity/src/main/kotlin/cafe/kirameki/minikuraVelocity/utils/ProxyTransferUtils.kt @@ -28,7 +28,7 @@ object ProxyTransferUtils { lateinit var acceptingTransfers: AtomicBoolean fun migratePlayersToServer(targetServer: ReverseProxyServerData, disconnectFailed : Boolean = false): ScheduledTask { - val targetAddress = InetSocketAddress(targetServer.address, targetServer.port) + val targetAddress = InetSocketAddress(targetServer.external_address, targetServer.external_port) val currentProxyName = redisBungeeApi.proxyId val playerOnThisProxy = redisBungeeApi.getPlayersOnProxy(currentProxyName) @@ -77,7 +77,7 @@ object ProxyTransferUtils { player.disconnect(Component.text("Failed to migrate you to the target server. Please reconnect.")) } } catch (e: Exception) { - logger.error("Error migrating player ${player.username} to server ${targetServer.name}: ${e.message}", e) + logger.error("Error migrating player ${player.username} to server ${targetServer.id}: ${e.message}", e) if (disconnectFailed) { player.disconnect(Component.text("Failed to migrate you to the target server. Please reconnect.")) }