diff --git a/.gitignore b/.gitignore index 2d513a0..aa90dbb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /.idea/ /target/ +/ext/plugins/ diff --git a/ext/sourcepawn-client/Facti13BackendIntegration.sp b/ext/sourcepawn-client/Facti13BackendIntegration.sp new file mode 100644 index 0000000..4aabdb7 --- /dev/null +++ b/ext/sourcepawn-client/Facti13BackendIntegration.sp @@ -0,0 +1,221 @@ +#include +#include + +#define PLUGIN_VERSION "1.0" + +public Plugin myinfo = { + name = "Facti13 Backend Integration", + author = "gsd", + description = "Update server info directly without RCON", + version = PLUGIN_VERSION, + url = "https://tf2.pblr-nyk.pro" +} + +/* CONVAR */ +Handle g_server_id_convar = INVALID_HANDLE; +Handle g_api_secret_key_convar = INVALID_HANDLE; +Handle g_api_gateway_convar = INVALID_HANDLE; + +char g_server_id[32]; +char g_secretkey[64]; +char g_gateway[128]; + +char g_cookie[128]; +char g_url[128]; + +Handle g_timer = INVALID_HANDLE; +int g_lastupdate = 0; +bool g_warned = true; +bool g_setuped = false; +bool g_updating = false; + +stock SetupConVar() { + g_server_id_convar = CreateConVar("sm_fbi_server_id", "srv8", "fbi server id", FCVAR_PROTECTED); + g_api_secret_key_convar = CreateConVar("sm_fbi_secretkey", "123456789", "fbi secret key", FCVAR_PROTECTED); + g_api_gateway_convar = CreateConVar("sm_fbi_gateway", "http://192.168.3.50:8080/api/server/%s", "fbi gateway", FCVAR_PROTECTED); + // + HookConVarChange(g_server_id_convar, OnServerIdChanged); + HookConVarChange(g_api_secret_key_convar, OnSecretKeyChanged); + HookConVarChange(g_api_gateway_convar, OnGatewayChanged); + // + GetConVarString(g_server_id_convar, g_server_id, sizeof(g_server_id)); + GetConVarString(g_api_secret_key_convar, g_secretkey, sizeof(g_secretkey)); + GetConVarString(g_api_gateway_convar, g_gateway, sizeof(g_gateway)); + // + UpdateUrlData(); +} + +stock UnSetupConvar() { + UnhookConVarChange(g_server_id_convar, OnServerIdChanged); + UnhookConVarChange(g_api_secret_key_convar, OnSecretKeyChanged); + UnhookConVarChange(g_api_gateway_convar, OnGatewayChanged); +} + +public OnServerIdChanged(Handle:cvar, const String:oldVal[], const String:newVal[]) { + strcopy(g_server_id, sizeof(g_server_id), newVal); + LogMessage("[FBI] Server id now: %s", g_server_id); + UpdateUrlData(); +} +public OnSecretKeyChanged(Handle:cvar, const String:oldVal[], const String:newVal[]) { + strcopy(g_secretkey, sizeof(g_secretkey), newVal); + LogMessage("[FBI] Secret key now lenght: %d", strlen(g_secretkey)); + UpdateUrlData(); +} +public OnGatewayChanged(Handle:cvar, const String:oldVal[], const String:newVal[]) { + strcopy(g_gateway, sizeof(g_gateway), newVal); + LogMessage("[FBI] Gateway now: %s", g_gateway); + UpdateUrlData(); +} + +stock UpdateUrlData(){ + g_setuped = false; + if (strlen(g_server_id)>0 && strlen(g_secretkey)>0){ + Format(g_url, sizeof(g_url), g_gateway, g_server_id); + Format(g_cookie, sizeof(g_cookie), "secretkey=%s", g_secretkey); + g_setuped = true; + LogMessage("[FBI] Successful set URL and SecretKey"); + } else + LogError("[FBI] Failed URL and SecretKey"); +} + +stock HTTPRequest createRequest() { + HTTPRequest client = INVALID_HANDLE; + if (strlen(g_url)>0) { + client = new HTTPRequest(g_url); + client.SetHeader("Cookie", g_cookie); + client.Timeout = 3; + } else { + LogMessage("[FBI] Client not builded"); + } + return client; +} + +stock JSONObject createPayload() { + JSONObject payload = new JSONObject(); + payload.SetBool("status", true); + + payload.SetInt("player_count", GetClientCount(true)); + payload.SetInt("max_players", MaxClients); + + char map_name[128]; + GetCurrentMap(map_name, sizeof(map_name)); + payload.SetString("map_name", map_name); + + JSONArray players = new JSONArray(); + + for(int client = 0; client <= MAXPLAYERS; client++) { + if (IsValidClient(client)) { + JSONObject player = new JSONObject(); + /* Name */ + char name[64]; + GetClientName(client, name, sizeof(name)); + player.SetString("name", name); + /* Score */ + player.SetInt("score", GetClientFrags(client)) + /* Duration */ + char duration[16]; + int ct = RoundFloat(GetClientTime(client)); + int h = ct / 3600; + int m = ct % 3600 / 60; + int s = ct % 60; + Format(duration, sizeof(duration), "%02d:%02d:%02d", h, m, s); + player.SetString("duration", duration); + /* Id */ + player.SetInt("id", GetClientUserId(client)); + /* Ip */ + char ip[32]; + GetClientIP(client, ip, sizeof(ip), false); + player.SetString("ip", ip); + /* Loss */ + player.SetInt("loss",RoundFloat(GetClientAvgLoss(client, NetFlow_Both)*1000.0)); + /* Ping */ + player.SetInt("ping",RoundFloat(GetClientLatency(client, NetFlow_Both)*1000.0)); + /* State */ + player.SetString("state", "active") + /* Steam 2 так надо, не надо спрашивать почему*/ + char steam2[32]; + GetClientAuthId(client, AuthId_Steam3, steam2, sizeof(steam2)); + player.SetString("steam2", steam2); + players.Push(player); + } + + } + + payload.Set("players", players); + return payload; +} + +stock UpdateStatus(){ + if (!g_setuped) return; + if (g_updating) return; + if (GetTime() - g_lastupdate < 10) return; + + g_updating = true; + JSONObject payload = createPayload(); + createRequest().Post(payload, Request_Callback); + g_lastupdate = GetTime(); +} + +static void Request_Callback(HTTPResponse response, any value){ + g_updating = false; + if (response.Status == HTTPStatus_OK){ + if(!g_warned) { + LogMessage("Success send payload, after error"); + g_warned = true; + } + return; + } + else{ + if(g_warned) { + LogMessage("Failed response! Code: %i", response.Status); + g_warned = false; + } + return; + } +} + +stock IsValidClient(int client){ + if(client > 4096){ + client = EntRefToEntIndex(client); + } + if(client < 1 || client > MaxClients) return false; + if(!IsClientInGame(client)) return false; + if(IsFakeClient(client)) return false; + return true; +} + +public OnPluginStart() { + SetupConVar(); + RegAdminCmd("fbi_test", TestUpdate, ADMFLAG_ROOT); + g_timer = CreateTimer(15, timerCall, 0, TIMER_REPEAT | TIMER_FLAG_NO_MAPCHANGE); +} + +public Action timerCall(Handle:t, any:d) { + UpdateStatus(); +} + +public OnPluginEnd() { + UnSetupConvar(); + if (g_timer != INVALID_HANDLE) { + KillTimer(g_timer); + } +} + +public Action TestUpdate(int client, int args){ + UpdateStatus(); + ReplyToCommand(client, "OK!") + return Plugin_Handled; +} + +public OnClientDisconnect(int client) { + UpdateStatus(); +} +public OnClientAuthorized(int client) { + UpdateStatus(); +} +public OnMapStart() { + UpdateStatus(); +} +public OnMapEnd() { + UpdateStatus(); +} \ No newline at end of file diff --git a/src/main/java/app/controllers/server/ServerUpdaterController.java b/src/main/java/app/controllers/server/ServerUpdaterController.java new file mode 100644 index 0000000..de16a30 --- /dev/null +++ b/src/main/java/app/controllers/server/ServerUpdaterController.java @@ -0,0 +1,38 @@ +package app.controllers.server; + +import app.annotations.enums.AuthMethod; +import app.annotations.interfaces.CheckWebAccess; +import app.entities.Stats; +import app.entities.server.request.ServerRequestBody; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("api/server") +public class ServerUpdaterController { + private Stats stats; + + @Autowired + ServerUpdaterController(Stats stats) { + this.stats = stats; + } + + @PostMapping(value = "/{srv}") + @CheckWebAccess(auth_method = AuthMethod.SECRET_KEY) + public ResponseEntity updateServer(HttpServletRequest request, @PathVariable String srv, @RequestBody ServerRequestBody serverRequestBody) { + if (!stats.getServers().containsKey(srv)) return new ResponseEntity<>(HttpStatus.NOT_FOUND); + stats.getServers().get(srv).RefreshServerFromRequest(serverRequestBody); + return new ResponseEntity(HttpStatus.OK); + } + + @DeleteMapping(value = "/{srv}") + @CheckWebAccess(auth_method = AuthMethod.SECRET_KEY) + public ResponseEntity downServer(HttpServletRequest request, @PathVariable String srv) { + if (!stats.getServers().containsKey(srv)) return new ResponseEntity<>(HttpStatus.NOT_FOUND); + stats.getServers().get(srv).RefreshServerFromRequest(null); + return new ResponseEntity(HttpStatus.OK); + } +} diff --git a/src/main/java/app/entities/a2s/external/ExternalValveClient.java b/src/main/java/app/entities/a2s/external/ExternalValveClient.java index 0b031bd..166338d 100644 --- a/src/main/java/app/entities/a2s/external/ExternalValveClient.java +++ b/src/main/java/app/entities/a2s/external/ExternalValveClient.java @@ -11,7 +11,7 @@ import org.springframework.web.client.RestTemplate; import java.util.*; -public abstract class ExternalValveClient { +public abstract class ExternalValveClient extends BaseUpdater { @JsonIgnore RestTemplate restTemplate; @JsonIgnore @@ -19,7 +19,8 @@ public abstract class ExternalValveClient { @JsonIgnore public String gateway = System.getenv("A2S_BACKEND_URL"); - private final Logger logger = LoggerFactory.getLogger(ExternalValveClient.class); + @JsonIgnore + public final Logger logger = LoggerFactory.getLogger(ExternalValveClient.class); public ExternalValveClient(){ restTemplate = new RestTemplate(); diff --git a/src/main/java/app/entities/server/Server.java b/src/main/java/app/entities/server/Server.java index 589057f..65229dc 100644 --- a/src/main/java/app/entities/server/Server.java +++ b/src/main/java/app/entities/server/Server.java @@ -5,9 +5,11 @@ import app.entities.other.SteamID; import app.entities.a2s.external.ExternalValveClient; import app.entities.a2s.requests.RCONRequest; import app.entities.server.players.RCONPlayer; +import app.entities.server.request.ServerRequestBody; import com.fasterxml.jackson.annotation.*; import lombok.Data; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -28,6 +30,7 @@ public class Server extends ExternalValveClient { List naming; HashMap uniq = new HashMap<>(); List players = new ArrayList<>(); + long last_update = 0; @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) private String db; @@ -40,6 +43,26 @@ public class Server extends ExternalValveClient { double[] city_pos = {0.0, 0.0}; + public Server() { + if (System.getenv("A2S") != null && System.getenv("A2S").equals("false")) { + logger.warn("Create cache last time updater: {} server", name); + CreateTaskUpdater(() -> { + logger.info("Update players cache from: {} server", name); + RefreshLastCheck(60); + return null; + }, 15000); + } else if (System.getenv("A2S") != null && System.getenv("A2S").equals("true")) { + logger.warn("Create a2s updater: {} server", name); + CreateTaskUpdater(() -> { + logger.info("Update players from: {} server", name); + RefreshServerA2SData(); + return null; + }, 60000); + } else { + logger.warn("{} server not be update", name); + } + } + @Override public String toString() { return "SERVER> %s | %s\nSTATS > %d/%d | %s\nCONFIG> grab limit: %d".formatted( @@ -61,6 +84,27 @@ public class Server extends ExternalValveClient { } /////////////////////////////////////////// UpdatePlayers(); + last_update = Instant.now().getEpochSecond(); + } + + public void RefreshLastCheck(int sec) { + if (Instant.now().getEpochSecond() - last_update > sec) { + SetDownStatus(); + } + } + + public void RefreshServerFromRequest(ServerRequestBody serverRequestBody) { + SetDownStatus(); + if (serverRequestBody != null) { + setMax_players(serverRequestBody.getMax_players()); + setPlayer_count(serverRequestBody.getPlayer_count()); + setMap(serverRequestBody.getMap_name()); + setStatus(serverRequestBody.isStatus()); + // + players.clear(); + players = new ArrayList<>(List.of(serverRequestBody.getPlayers())); + } + last_update = Instant.now().getEpochSecond(); } public void UpdateStatusFromA2S(){ diff --git a/src/main/java/app/entities/server/request/PlayerOnServer.java b/src/main/java/app/entities/server/request/PlayerOnServer.java new file mode 100644 index 0000000..e052211 --- /dev/null +++ b/src/main/java/app/entities/server/request/PlayerOnServer.java @@ -0,0 +1,11 @@ +package app.entities.server.request; + +import app.entities.server.players.RCONPlayer; +import com.fasterxml.jackson.annotation.JsonGetter; +import lombok.Data; + +@Data +public class PlayerOnServer extends RCONPlayer { + float[] pos = {}; + //int duration_seconds = 0; +} \ No newline at end of file diff --git a/src/main/java/app/entities/server/request/ServerRequestBody.java b/src/main/java/app/entities/server/request/ServerRequestBody.java new file mode 100644 index 0000000..cb3b31b --- /dev/null +++ b/src/main/java/app/entities/server/request/ServerRequestBody.java @@ -0,0 +1,12 @@ +package app.entities.server.request; + +import lombok.Data; + +@Data +public class ServerRequestBody { + int max_players = 0; + int player_count = 0; + String map_name = ""; + boolean status = false; + PlayerOnServer[] players = {}; +} diff --git a/src/main/java/app/updates/BaseUpdater.java b/src/main/java/app/updates/BaseUpdater.java index 89e5c13..9719939 100644 --- a/src/main/java/app/updates/BaseUpdater.java +++ b/src/main/java/app/updates/BaseUpdater.java @@ -3,6 +3,8 @@ package app.updates; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Supplier; diff --git a/src/main/java/app/updates/PlayersUpdater.java b/src/main/java/app/updates/PlayersUpdater.java index 2b18801..a658e3b 100644 --- a/src/main/java/app/updates/PlayersUpdater.java +++ b/src/main/java/app/updates/PlayersUpdater.java @@ -7,6 +7,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import java.io.IOException; @@ -31,10 +32,11 @@ public class PlayersUpdater extends BaseUpdater{ this.stats = stats; } - @PostConstruct + /* public void updateValues() { if (update) { logger.warn("Updater enabled"); + if (stats.getServers().size()==0) logger.error("Not found servers to update"); stats.getServers().forEach((server_name, server) -> { CreateTaskUpdater(() -> { logger.info("Update players from: {} server", server_name); @@ -43,10 +45,22 @@ public class PlayersUpdater extends BaseUpdater{ return null; }, timeout); }); + } else { + logger.warn("A2S Refresh disabled! Enable last timecheck"); + if (stats.getServers().size()==0) logger.error("Not found servers to update"); + stats.getServers().forEach((server_name, server) -> { + CreateTaskUpdater(() -> { + logger.info("Update players from: {} server", server_name); + server.RefreshLastCheck(60); + return null; + }, 15000); + }); } - } + }*/ public void burstUpdater() { + if (!update) return; + ExecutorService executor = Executors.newCachedThreadPool(); List tasks = new ArrayList<>(); stats.getServers().forEach((server_name, server) -> { diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 78622d6..5837390 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -68,6 +68,6 @@ backend: logging: level: com.ibasco.agql.core.util.*: OFF - app.updates.PlayersUpdater: ERROR + app.entities.a2s.external.ExternalValveClient: WARN app.utils.SaltedCookie: INFO app.annotations.impl.WebAccessAspect: WARN \ No newline at end of file