mirror of
https://github.com/YuzuZensai/Minikura.git
synced 2026-01-06 04:32:37 +00:00
Merge pull request #1 from JAJAR94/main
feat: websocket auto detect server info change
This commit is contained in:
@@ -36,6 +36,28 @@ const bootstrap = async () => {
|
||||
console.log("Default user created");
|
||||
};
|
||||
|
||||
|
||||
const connectedClients = new Set<any>();
|
||||
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,
|
||||
};
|
||||
|
||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@@ -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:
|
||||
@@ -41,8 +41,8 @@ class Main @Inject constructor(private val logger: Logger, private val server: P
|
||||
private val servers: MutableMap<String, RegisteredServer> = 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 <origin> and <destination> 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ServerData> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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<CustomEnvironmentVariableData> = emptyList(),
|
||||
val created_at: String,
|
||||
val updated_at: String
|
||||
)
|
||||
@@ -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<CustomEnvironmentVariableData> = emptyList(),
|
||||
val api_key: String,
|
||||
val created_at: String,
|
||||
val updated_at: String
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package cafe.kirameki.minikuraVelocity.models.components
|
||||
|
||||
enum class ReverseProxyServerType {
|
||||
VELOCITY, BUNGEECORD
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package cafe.kirameki.minikuraVelocity.models.components
|
||||
|
||||
enum class ServerType {
|
||||
STATEFUL, STATELESS,
|
||||
}
|
||||
@@ -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."))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user