From 9b9ee374f0d1eba6ed3795bc87f818d3cf9baa71 Mon Sep 17 00:00:00 2001 From: gsd Date: Mon, 6 Feb 2023 21:35:02 +0300 Subject: [PATCH] first init --- .gitignore | 2 + pom.xml | 85 ++++++++++++++ src/main/java/app/MainApi.java | 11 ++ src/main/java/app/configs/JobRunrConfig.java | 17 +++ src/main/java/app/configs/ProtocolA2S.java | 41 +++++++ .../java/app/controllers/StatsController.java | 25 ++++ src/main/java/app/entities/other/SteamID.java | 27 +++++ src/main/java/app/entities/server/Server.java | 96 ++++++++++++++++ .../server/players/DefaultPlayer.java | 9 ++ .../entities/server/players/RCONPlayer.java | 34 ++++++ .../entities/server/players/SourcePlayer.java | 15 +++ src/main/java/app/services/GeoIP.java | 27 +++++ src/main/java/app/services/ServersReader.java | 38 +++++++ src/main/java/app/services/Stats.java | 31 +++++ .../java/app/updates/BanCountUpdater.java | 43 +++++++ .../java/app/updates/CountriesUpdater.java | 68 +++++++++++ src/main/java/app/updates/PlayersUpdater.java | 91 +++++++++++++++ src/main/java/app/updates/UniqueUpdater.java | 107 ++++++++++++++++++ src/main/java/app/utils/SteamIDConverter.java | 85 ++++++++++++++ src/main/resources/application.yaml | 35 ++++++ 20 files changed, 887 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/app/MainApi.java create mode 100644 src/main/java/app/configs/JobRunrConfig.java create mode 100644 src/main/java/app/configs/ProtocolA2S.java create mode 100644 src/main/java/app/controllers/StatsController.java create mode 100644 src/main/java/app/entities/other/SteamID.java create mode 100644 src/main/java/app/entities/server/Server.java create mode 100644 src/main/java/app/entities/server/players/DefaultPlayer.java create mode 100644 src/main/java/app/entities/server/players/RCONPlayer.java create mode 100644 src/main/java/app/entities/server/players/SourcePlayer.java create mode 100644 src/main/java/app/services/GeoIP.java create mode 100644 src/main/java/app/services/ServersReader.java create mode 100644 src/main/java/app/services/Stats.java create mode 100644 src/main/java/app/updates/BanCountUpdater.java create mode 100644 src/main/java/app/updates/CountriesUpdater.java create mode 100644 src/main/java/app/updates/PlayersUpdater.java create mode 100644 src/main/java/app/updates/UniqueUpdater.java create mode 100644 src/main/java/app/utils/SteamIDConverter.java create mode 100644 src/main/resources/application.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d513a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea/ +/target/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d3f8bdd --- /dev/null +++ b/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + org.example + MainApi + 1.0-SNAPSHOT + + + 18 + 18 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + 3.0.2 + + + org.springframework.boot + spring-boot-starter + 3.0.2 + + + com.fasterxml.jackson.core + jackson-core + 2.14.2 + + + org.projectlombok + lombok + 1.18.26 + provided + + + org.slf4j + slf4j-api + 2.0.6 + + + org.springframework.boot + spring-boot-starter-data-jpa + 3.0.2 + + + com.mysql + mysql-connector-j + 8.0.32 + + + org.jobrunr + jobrunr + 6.0.0 + + + org.jobrunr + jobrunr-spring-boot-starter + 5.3.3 + + + org.springframework.boot + spring-boot-starter-actuator + 3.0.2 + + + com.ibasco.agql + agql-source-query + 1.0.7 + + + com.ibasco.agql + agql-source-rcon + 1.0.7 + + + com.maxmind.geoip2 + geoip2 + 3.0.2 + + + \ No newline at end of file diff --git a/src/main/java/app/MainApi.java b/src/main/java/app/MainApi.java new file mode 100644 index 0000000..4945267 --- /dev/null +++ b/src/main/java/app/MainApi.java @@ -0,0 +1,11 @@ +package app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MainApi { + public static void main(String[] args) { + SpringApplication.run(MainApi.class); + } +} diff --git a/src/main/java/app/configs/JobRunrConfig.java b/src/main/java/app/configs/JobRunrConfig.java new file mode 100644 index 0000000..a9d15b0 --- /dev/null +++ b/src/main/java/app/configs/JobRunrConfig.java @@ -0,0 +1,17 @@ +package app.configs; + +import org.jobrunr.jobs.mappers.JobMapper; +import org.jobrunr.storage.InMemoryStorageProvider; +import org.jobrunr.storage.StorageProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JobRunrConfig { + @Bean + public StorageProvider storageProvider(JobMapper jobMapper) { + InMemoryStorageProvider storageProvider = new InMemoryStorageProvider(); + storageProvider.setJobMapper(jobMapper); + return storageProvider; + } +} diff --git a/src/main/java/app/configs/ProtocolA2S.java b/src/main/java/app/configs/ProtocolA2S.java new file mode 100644 index 0000000..d1e0fbe --- /dev/null +++ b/src/main/java/app/configs/ProtocolA2S.java @@ -0,0 +1,41 @@ +package app.configs; + +import com.ibasco.agql.core.enums.RateLimitType; +import com.ibasco.agql.core.util.FailsafeOptions; +import com.ibasco.agql.core.util.GeneralOptions; +import com.ibasco.agql.protocols.valve.source.query.SourceQueryClient; +import com.ibasco.agql.protocols.valve.source.query.SourceQueryOptions; +import com.ibasco.agql.protocols.valve.source.query.rcon.SourceRconClient; +import com.ibasco.agql.protocols.valve.source.query.rcon.SourceRconOptions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Configuration +public class ProtocolA2S { + //https://ribasco.github.io/async-gamequery-lib/examples/source_query_example.html#blocking-example + @Scope("prototype") + @Bean + public SourceQueryClient GetSourceQueryClient() { + ExecutorService customExecutor = Executors.newCachedThreadPool(); + SourceQueryOptions options = SourceQueryOptions.builder() + .option(FailsafeOptions.FAILSAFE_RATELIMIT_TYPE, RateLimitType.BURST) + .option(GeneralOptions.THREAD_EXECUTOR_SERVICE, customExecutor) + .build(); + return new SourceQueryClient(options); + } + + @Scope("prototype") + @Bean + public SourceRconClient GetSourceRconClient() { + ExecutorService customExecutor = Executors.newCachedThreadPool(); + SourceRconOptions options = SourceRconOptions.builder() + //.option(FailsafeOptions.FAILSAFE_RATELIMIT_TYPE, RateLimitType.BURST) + .option(GeneralOptions.THREAD_EXECUTOR_SERVICE, customExecutor) + .build(); + return new SourceRconClient(options); + } +} \ No newline at end of file diff --git a/src/main/java/app/controllers/StatsController.java b/src/main/java/app/controllers/StatsController.java new file mode 100644 index 0000000..452770d --- /dev/null +++ b/src/main/java/app/controllers/StatsController.java @@ -0,0 +1,25 @@ +package app.controllers; + +import app.services.Stats; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("api/stats") +public class StatsController { + private Stats stats; + + @Autowired + public StatsController(Stats stats){ + this.stats = stats; + } + + @GetMapping + public ResponseEntity GetStats(){ + return new ResponseEntity<>(stats, HttpStatus.OK); + } +} diff --git a/src/main/java/app/entities/other/SteamID.java b/src/main/java/app/entities/other/SteamID.java new file mode 100644 index 0000000..c94b51e --- /dev/null +++ b/src/main/java/app/entities/other/SteamID.java @@ -0,0 +1,27 @@ +package app.entities.other; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@JsonSerialize +public +class SteamID { + public String steam3; + public String steam2; + public long steam64; + public String steam_url; + + public long account_id; + + public SteamID(String steam3, String steam2, String steam64, long account_id) { + this.steam3 = steam3; + this.steam2 = steam2; + this.steam64 = Long.parseLong(steam64); + this.steam_url = String.format("https://steamcommunity.com/profiles/%s", steam64); + this.account_id = account_id; + } + + @Override + public String toString(){ + return steam_url; + } +} \ No newline at end of file diff --git a/src/main/java/app/entities/server/Server.java b/src/main/java/app/entities/server/Server.java new file mode 100644 index 0000000..0445864 --- /dev/null +++ b/src/main/java/app/entities/server/Server.java @@ -0,0 +1,96 @@ +package app.entities.server; + +import app.entities.server.players.DefaultPlayer; +import app.entities.server.players.RCONPlayer; +import com.fasterxml.jackson.annotation.*; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.ibasco.agql.protocols.valve.source.query.info.SourceQueryInfoResponse; +import com.ibasco.agql.protocols.valve.source.query.players.SourcePlayer; +import com.ibasco.agql.protocols.valve.source.query.players.SourceQueryPlayerResponse; +import jakarta.persistence.criteria.CriteriaBuilder; +import lombok.Data; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +@Data +public class Server { + String name; + String description; + String address; + String preview; + int player_count; + int max_players; + boolean status; + String color; + String workshop; + List naming; + HashMap uniq = new HashMap<>(); + List players = new ArrayList<>(); + + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String db; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String dc; + @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) + private String rcon_password; + + public void UpdateUniq(String key, Long count) { + uniq.merge(key, count, (x,y) -> y); + } + + public InetSocketAddress getInetAddress() { + String[] splitted_address = address.split(":", 2); + return new InetSocketAddress(splitted_address[0], Integer.parseInt(splitted_address[1])); + } + + public void UpdateStatusFromA2S(SourceQueryInfoResponse response) { + SetDownStatus(); + if (response == null) return; + + setMax_players(response.getResult().getMaxPlayers()); + setPlayer_count(response.getResult().getNumOfPlayers()); + setStatus(true); + } + + public void UpdatePlayersFromA2S(SourceQueryPlayerResponse response) { + players.clear(); + if (response != null) { + for (SourcePlayer player: response.getResult()) { + players.add(new app.entities.server.players.SourcePlayer(player)); + } + } + } + + public void UpdatePlayersFromRCON(String response) { + List players_list = Arrays.stream(response.substring(response.indexOf("# userid"), response.length()).split("\n")).toList(); + boolean skip_table_header = true; + for(String player_text: players_list) { + if (skip_table_header || player_text.length() < 1) { + skip_table_header = false; + continue; + } + ///////////////////////////////////////////////////// + List player_line = Arrays.stream(player_text.split("\\s+")).toList(); + RCONPlayer player = new RCONPlayer(player_line); + for (DefaultPlayer sourcePlayer: players) { + if (sourcePlayer.getName().equals(player.getName())) { + player.setScore(sourcePlayer.getScore()); + players.remove(sourcePlayer); + players.add(player); + break; + } + } + } + } + + public void SetDownStatus() { + setStatus(false); + setMax_players(0); + setPlayer_count(0); + players.clear(); + } +} diff --git a/src/main/java/app/entities/server/players/DefaultPlayer.java b/src/main/java/app/entities/server/players/DefaultPlayer.java new file mode 100644 index 0000000..f010e14 --- /dev/null +++ b/src/main/java/app/entities/server/players/DefaultPlayer.java @@ -0,0 +1,9 @@ +package app.entities.server.players; + +import lombok.Data; + +@Data +public class DefaultPlayer { + String name; + int score; +} diff --git a/src/main/java/app/entities/server/players/RCONPlayer.java b/src/main/java/app/entities/server/players/RCONPlayer.java new file mode 100644 index 0000000..eb0d2d8 --- /dev/null +++ b/src/main/java/app/entities/server/players/RCONPlayer.java @@ -0,0 +1,34 @@ +package app.entities.server.players; + +import app.entities.other.SteamID; +import app.entities.server.players.DefaultPlayer; +import app.utils.SteamIDConverter; +import lombok.Data; + +import java.util.List; + +@Data +public class RCONPlayer extends DefaultPlayer { + String duration; + int id; + String ip; + int loss; + int ping; + String state; + String steam2; + SteamID steam; + + public RCONPlayer(List status_line) { + id = Integer.parseInt(status_line.get(1)); + ip = status_line.get(status_line.size() - 1); + state = status_line.get(status_line.size() - 2); + loss = Integer.parseInt(status_line.get(status_line.size() - 3)); + ping = Integer.parseInt(status_line.get(status_line.size() - 4)); + duration = status_line.get(status_line.size() - 5); + steam2 = status_line.get(status_line.size() - 6); + name = String.join(" ", status_line.subList(2, status_line.size() - 6)); + name = name.substring(1, name.length()-1); + //////////////////////////////////////////////////////////////////////////////// + steam = SteamIDConverter.getSteamID(steam2); + } +} diff --git a/src/main/java/app/entities/server/players/SourcePlayer.java b/src/main/java/app/entities/server/players/SourcePlayer.java new file mode 100644 index 0000000..731aba7 --- /dev/null +++ b/src/main/java/app/entities/server/players/SourcePlayer.java @@ -0,0 +1,15 @@ +package app.entities.server.players; + +import app.entities.server.players.DefaultPlayer; +import lombok.Data; + +@Data +public class SourcePlayer extends DefaultPlayer { + float duration; + + public SourcePlayer(com.ibasco.agql.protocols.valve.source.query.players.SourcePlayer player) { + name = player.getName(); + duration = player.getDuration(); + score = player.getScore(); + } +} diff --git a/src/main/java/app/services/GeoIP.java b/src/main/java/app/services/GeoIP.java new file mode 100644 index 0000000..c6708b3 --- /dev/null +++ b/src/main/java/app/services/GeoIP.java @@ -0,0 +1,27 @@ +package app.services; + +import com.maxmind.geoip2.DatabaseReader; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; + +@Component +public class GeoIP { + private DatabaseReader databaseReader; + + @Autowired + public GeoIP(@Value("${backend.geoip_file}") String data) throws IOException { + databaseReader = new DatabaseReader.Builder(new File(data)).build(); + } + + public String GetCountry(String ip) throws UnknownHostException, GeoIp2Exception, IOException { + return databaseReader.country(InetAddress.getByName(ip)).getCountry().getName(); + } +} diff --git a/src/main/java/app/services/ServersReader.java b/src/main/java/app/services/ServersReader.java new file mode 100644 index 0000000..b2f0e33 --- /dev/null +++ b/src/main/java/app/services/ServersReader.java @@ -0,0 +1,38 @@ +package app.services; + +import app.entities.server.Server; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; + +@Component +public class ServersReader { + Stats stats; + ObjectMapper objectMapper; + + @Autowired + public ServersReader(Stats stats, @Value("${backend.servers_file}") String servers_path) { + this.stats = stats; + this.objectMapper = new ObjectMapper(); + + try { + System.out.printf("Read from: %s\n", servers_path); + JsonNode node = this.objectMapper.readTree(new File(servers_path)); + Iterator> iterator = node.fields(); + while (iterator.hasNext()) { + Map.Entry server = iterator.next(); + stats.servers.put(server.getKey(), this.objectMapper.treeToValue(server.getValue(), Server.class)); + } + } catch (IOException err) { + System.out.printf("Cannot read servers file: %s\n", servers_path); + } + } +} diff --git a/src/main/java/app/services/Stats.java b/src/main/java/app/services/Stats.java new file mode 100644 index 0000000..a5435c0 --- /dev/null +++ b/src/main/java/app/services/Stats.java @@ -0,0 +1,31 @@ +package app.services; + +import app.entities.server.Server; +import lombok.Data; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@Data +@Component +@Scope(value = "singleton") +public class Stats { + long ban_count = 0; + int discord_users = 0; + int freevip_players = 0; + int server_uptime = 0; + int vip_players = 0; + int vk_users = 0; + HashMap countries = new HashMap<>(); + HashMap servers = new HashMap<>(); + HashMap uniq = new HashMap<>(); + HashMap updates = new HashMap<>(); + + public void UpdateUniq(String key, Long value) { + uniq.merge(key, value, (x,y) -> y); + } +} diff --git a/src/main/java/app/updates/BanCountUpdater.java b/src/main/java/app/updates/BanCountUpdater.java new file mode 100644 index 0000000..31c135b --- /dev/null +++ b/src/main/java/app/updates/BanCountUpdater.java @@ -0,0 +1,43 @@ +package app.updates; + +import app.services.Stats; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.jobrunr.scheduling.JobScheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; + +import java.time.Instant; + +@Component +public class BanCountUpdater { + private Stats stats; + private JobScheduler jobScheduler; + @Value("${backend.updates.ban_count}") + boolean update = false; + @PersistenceContext + EntityManager entityManager; + + @Autowired + public BanCountUpdater(Stats stats, + JobScheduler jobScheduler) { + this.stats = stats; + this.jobScheduler = jobScheduler; + } + + @PostConstruct + public void SetUpdater(){ + if(update) { + jobScheduler.enqueue(() -> UpdateBanCount()); + jobScheduler.scheduleRecurrently("*/5 * * * *", () -> UpdateBanCount()); + } + } + + public void UpdateBanCount(){ + stats.setBan_count((Long) entityManager.createNativeQuery("SELECT COUNT(*) as count FROM `light_bans`").getSingleResult()); + stats.getUpdates().merge("ban_count", Instant.now().getEpochSecond(), (x, y) -> y); + } +} diff --git a/src/main/java/app/updates/CountriesUpdater.java b/src/main/java/app/updates/CountriesUpdater.java new file mode 100644 index 0000000..b3d570f --- /dev/null +++ b/src/main/java/app/updates/CountriesUpdater.java @@ -0,0 +1,68 @@ +package app.updates; + +import app.entities.server.Server; +import app.services.GeoIP; +import app.services.Stats; +import com.maxmind.geoip2.exception.GeoIp2Exception; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.jobrunr.jobs.annotations.Job; +import org.jobrunr.scheduling.JobScheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.UnknownHostException; +import java.util.Map; + +@Component +public class CountriesUpdater { + Stats stats; + GeoIP geoIP; + JobScheduler jobScheduler; + @PersistenceContext + EntityManager entityManager; + + @Value("${backend.updates.countries}") + private boolean update; + + private final String countries_in_current_year = "SELECT DISTINCT `connect_ip` FROM `%s`.`user_connections` WHERE connection_type LIKE 'connect' AND `timestamp` > CAST(DATE_FORMAT(NOW() ,'%%Y-01-01') as DATE) UNION\n"; + + @Autowired + public CountriesUpdater(Stats stats, + GeoIP geoIP, + JobScheduler jobScheduler) { + this.stats = stats; + this.geoIP = geoIP; + this.jobScheduler = jobScheduler; + } + + @PostConstruct + public void UpdateCountries(){ + if (update) { + jobScheduler.enqueue(() -> UpdateCountriesStatistic()); + jobScheduler.scheduleRecurrently("backend.stats.countries.update", "*/15 * * * *", () -> UpdateCountriesStatistic()); + } + } + + @Job(name = "Update countries statistic") + public void UpdateCountriesStatistic() { + stats.getCountries().clear(); + + String query = ""; + for (Map.Entry stringServerEntry : stats.getServers().entrySet()) { + query += countries_in_current_year.formatted(stringServerEntry.getValue().getDb()); + } + query = query.substring(0, query.length()-7) + ";"; + for(Object ip:entityManager.createNativeQuery(query).getResultList()) { + try { + stats.getCountries().merge(geoIP.GetCountry(String.valueOf(ip)), 1, (x, y) -> x+y); + } catch (UnknownHostException e) { + } catch (IOException e) { + } catch (GeoIp2Exception e) { + } + } + } +} diff --git a/src/main/java/app/updates/PlayersUpdater.java b/src/main/java/app/updates/PlayersUpdater.java new file mode 100644 index 0000000..c59f80e --- /dev/null +++ b/src/main/java/app/updates/PlayersUpdater.java @@ -0,0 +1,91 @@ +package app.updates; + +import app.services.Stats; +import com.ibasco.agql.protocols.valve.source.query.SourceQueryClient; +import com.ibasco.agql.protocols.valve.source.query.info.SourceQueryInfoResponse; +import com.ibasco.agql.protocols.valve.source.query.players.SourceQueryPlayerResponse; +import com.ibasco.agql.protocols.valve.source.query.rcon.SourceRconClient; +import com.ibasco.agql.protocols.valve.source.query.rcon.exceptions.RconException; +import com.ibasco.agql.protocols.valve.source.query.rcon.message.SourceRconAuthResponse; +import com.ibasco.agql.protocols.valve.source.query.rcon.message.SourceRconCmdResponse; +import jakarta.annotation.PostConstruct; +import org.jobrunr.jobs.annotations.Job; +import org.jobrunr.scheduling.JobScheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.concurrent.CompletionException; + +@Component +public class PlayersUpdater { + Stats stats; + ApplicationContext context; + JobScheduler jobScheduler; + + @Value("${backend.updates.a2s}") + private boolean update = false; + + @Autowired + public PlayersUpdater(Stats stats, + ApplicationContext context, + JobScheduler jobScheduler) { + this.stats = stats; + this.context = context; + this.jobScheduler = jobScheduler; + } + + @PostConstruct + public void updateValues() { + if (update) { + stats.getServers().forEach((server_name, server) -> { + jobScheduler.enqueue(() -> UpdatePlayersOnServer(server_name)); + jobScheduler.scheduleRecurrently("backend.stats.info.update." + server_name, "* * * * *", () -> UpdatePlayersOnServer(server_name)); + }); + } + } + + @Job(name = "Update A2S data on: %0") + public void UpdatePlayersOnServer(String server_name) { + SourceQueryClient sourceQueryClient = context.getBean(SourceQueryClient.class); + try { + SourceQueryInfoResponse info = sourceQueryClient.getInfo(stats.getServers().get(server_name).getInetAddress()).join(); + stats.getServers().get(server_name).UpdateStatusFromA2S(info); + sourceQueryClient.close(); + } catch (IOException err) { + stats.getServers().get(server_name).SetDownStatus(); + return; + } + if (!stats.getServers().get(server_name).isStatus() || stats.getServers().get(server_name).getPlayer_count() < 1) { + return; + } + //////////////////////////////////////////////////////////////////////// + //If player count > 0 make base player request + //////////////////////////////////////////////////////////////////////// + try { + sourceQueryClient = context.getBean(SourceQueryClient.class); + SourceQueryPlayerResponse players = sourceQueryClient.getPlayers(stats.getServers().get(server_name).getInetAddress()).join(); + stats.getServers().get(server_name).UpdatePlayersFromA2S(players); + sourceQueryClient.close(); + } catch (IOException err) { + return; + } + /////////////////////////////////////////////////////////////////////// + //Extend current players of rcon result + ////////////////////////////////////////////////////////////////////// + try { + SourceRconClient rcon_client = context.getBean(SourceRconClient.class); + SourceRconAuthResponse response = rcon_client.authenticate(stats.getServers().get(server_name).getInetAddress(), stats.getServers().get(server_name).getRcon_password().getBytes()).join(); + if(!response.isAuthenticated()) { + return; + } + SourceRconCmdResponse rcon_response = rcon_client.execute(stats.getServers().get(server_name).getInetAddress(), "status").join(); + rcon_client.cleanup(); + stats.getServers().get(server_name).UpdatePlayersFromRCON(rcon_response.getResult()); + } catch (RconException | CompletionException err) { + return; + } + } +} diff --git a/src/main/java/app/updates/UniqueUpdater.java b/src/main/java/app/updates/UniqueUpdater.java new file mode 100644 index 0000000..a373c0b --- /dev/null +++ b/src/main/java/app/updates/UniqueUpdater.java @@ -0,0 +1,107 @@ +package app.updates; + +import app.entities.server.Server; +import app.services.Stats; +import jakarta.annotation.PostConstruct; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.jobrunr.jobs.annotations.Job; +import org.jobrunr.scheduling.JobScheduler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import static java.time.Instant.now; + +@Component +public class UniqueUpdater { + Stats stats; + JobScheduler jobScheduler; + + @PersistenceContext + EntityManager entityManager; + + private final String query_total = "SELECT DISTINCT `account_id` FROM `%s`.`user_connections` WHERE `connection_type` LIKE \"disconnect\" AND `connect_duration` > 300 UNION\n"; + private final String query_day = "SELECT DISTINCT `account_id` FROM `%s`.`user_connections` WHERE `connection_type` LIKE \"disconnect\" AND `connect_duration` > 300 AND `timestamp` > CAST(DATE_FORMAT(NOW() ,'%%Y-%%m-%%d') as DATE) UNION\n"; + private final String query_month = "SELECT DISTINCT `account_id` FROM `%s`.`user_connections` WHERE `connection_type` LIKE \"disconnect\" AND `connect_duration` > 300 AND `timestamp` > CAST(DATE_FORMAT(NOW() ,'%%Y-%%m-01') as DATE) UNION\n"; + private final String query_year = "SELECT DISTINCT `account_id` FROM `%s`.`user_connections` WHERE `connection_type` LIKE \"disconnect\" AND `connect_duration` > 300 AND `timestamp` > CAST(DATE_FORMAT(NOW() ,'%%Y-01-01') as DATE) UNION\n"; + + @Value("${backend.updates.unique_global}") + boolean global_update = false; + @Value("${backend.updates.unique_server}") + boolean server_update = false; + + @Autowired + public UniqueUpdater(Stats stats, + JobScheduler jobScheduler) { + this.stats = stats; + this.jobScheduler = jobScheduler; + } + + @PostConstruct + public void updateValues() throws InterruptedException { + if(global_update) { + Thread.sleep(2000); + jobScheduler.enqueue(() -> UpdateServerUniqueTotal()); + jobScheduler.enqueue(() -> UpdateServerUniqueYear()); + jobScheduler.enqueue(() -> UpdateServerUniqueMonth()); + jobScheduler.enqueue(() -> UpdateServerUniqueDay()); + /////////////////////////////////////////////////////////////////////////////////////// + jobScheduler.scheduleRecurrently("0 0 */1 * *", () -> UpdateServerUniqueTotal()); + jobScheduler.scheduleRecurrently("0 0 */1 * *", () -> UpdateServerUniqueYear()); + jobScheduler.scheduleRecurrently("0 0 */1 * *", () -> UpdateServerUniqueMonth()); + jobScheduler.scheduleRecurrently("0 */1 * * *", () -> UpdateServerUniqueDay()); + } + /////////////////////////////////////////////////////////////////////////////////////// + if(server_update) { + stats.getServers().forEach((server_name, server) -> { + jobScheduler.enqueue(() -> getServerUnique(server_name, server.getDb())); + jobScheduler.scheduleRecurrently("backend.stats.unique.update." + server_name, "*/5 * * * *", () -> getServerUnique(server_name, server.getDb())); + }); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// + public Long getServerUniqueFromQuery(String query, String db) { + query = String.format(query, db); + query = "SELECT COUNT(*) as count FROM (" + query.substring(0, query.length()-7) + ") x;"; + return (Long) entityManager.createNativeQuery(query).getSingleResult(); + } + + public Long getServerUniqueFromQuery(String query) { + String final_query = "SELECT COUNT(*) as count FROM ("; + for(Server server: stats.getServers().values()){ + final_query += query.formatted(server.getDb()); + } + final_query = final_query.substring(0, final_query.length()-7) + ") x;"; + return (Long) entityManager.createNativeQuery(final_query).getSingleResult(); + } + /////////////////////////////////////////////////////////////////////////////////////////// + + @Job(name = "Get server unique statistic %0") + public void getServerUnique(String server_name, String db) { + stats.getServers().get(server_name).UpdateUniq("total", getServerUniqueFromQuery(query_total, db)); + stats.getServers().get(server_name).UpdateUniq("day", getServerUniqueFromQuery(query_day, db)); + stats.getServers().get(server_name).UpdateUniq("month", getServerUniqueFromQuery(query_month, db)); + stats.getServers().get(server_name).UpdateUniq("year", getServerUniqueFromQuery(query_year, db)); + } + + @Job(name = "Get total count unique players on all server") + public void UpdateServerUniqueTotal() { + stats.UpdateUniq("total", getServerUniqueFromQuery(query_total)); + } + + @Job(name = "Get year count unique players on all server") + public void UpdateServerUniqueYear() { + stats.UpdateUniq("year", getServerUniqueFromQuery(query_year)); + } + + @Job(name = "Get month count unique players on all server") + public void UpdateServerUniqueMonth() { + stats.UpdateUniq("month", getServerUniqueFromQuery(query_month)); + } + + @Job(name = "Get day count unique players on all server") + public void UpdateServerUniqueDay() { + stats.UpdateUniq("day", getServerUniqueFromQuery(query_day)); + } +} diff --git a/src/main/java/app/utils/SteamIDConverter.java b/src/main/java/app/utils/SteamIDConverter.java new file mode 100644 index 0000000..15173cc --- /dev/null +++ b/src/main/java/app/utils/SteamIDConverter.java @@ -0,0 +1,85 @@ +package app.utils; + +import app.entities.other.SteamID; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.math.BigInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +//https://github.com/SteamRun/SteamIDConverter/blob/master/SteamIDConverter.html +public class SteamIDConverter { + private static final String SID64_1 = "7656"; + private static final long SID64_S = Long.valueOf("1197960265728"); + + private static final Pattern PatternSteam3ID = Pattern.compile("^\\[([Ug]):([0-9]):([0-9]+)\\]$"); + private static final Pattern PatternSteamID32 = Pattern.compile("^STEAM_([0-9]):([0-9]):([0-9]+)$"); + private static final Pattern PatternSteamID64 = Pattern.compile("7656([0-9]{12,14})"); + private final Pattern PatternCustomUrl = Pattern.compile("steamcommunity\\.com\\/id\\/([A-Za-z0-9-_]{2,32})"); + private final Pattern PatternMaybeCustomUrl = Pattern.compile("^([A-Za-z0-9-_]{2,32})$"); + + public static SteamID getSteamID(String text){ + boolean isSteamID = false; + + String steam3; + String steam2; + String steam64; + + String S3ID_1; + long S3ID_2 = 0; + long S3ID_3 = 0; + + long SID32_1 = 0; + long SID32_2 = 0; + long SID32_3 = 0; + + long SID64_2 = 0; + + Matcher result; + + if((result = PatternSteam3ID.matcher(text)).find()){ + //Matcher result = PatternSteam3ID.matcher(text); + S3ID_1 = result.group(1); + S3ID_2 = Long.parseLong(result.group(2)); + S3ID_3 = Long.parseLong(result.group(3)); + + if(Math.abs(S3ID_3 % 2) == 1){ + SID32_2 = 1; + } else { + SID32_2 = 0; + } + + SID32_3 = (S3ID_3 - SID32_2) / 2; + SID64_2 = S3ID_3 + SID64_S; + isSteamID = true; + } else if ((result = PatternSteamID32.matcher(text)).find()){ + //Matcher result = PatternSteamID32.matcher(text); + SID32_1 = Long.parseLong(result.group(1)); + SID32_2 = Long.parseLong(result.group(2)); + SID32_3 = Long.parseLong(result.group(3)); + S3ID_3 = SID32_3 * 2 + SID32_2; + SID64_2 = S3ID_3 + SID64_S; + isSteamID = true; + } else if ((result = PatternSteamID64.matcher(text)).find()) { + //Matcher result = PatternSteamID64.matcher(text); + SID64_2 = Long.parseLong(result.group(1)); + S3ID_3 = SID64_2 - SID64_S; + + if(Math.abs(S3ID_3 % 2) == 1){ + SID32_2 = 1; + } else { + SID32_2 = 0; + } + + SID32_3 = (S3ID_3 - SID32_2) / 2; + isSteamID = true; + } + + if(!isSteamID) return null; + + steam3 = String.format("[U:1:%s]",S3ID_3); + steam2 = String.format("STEAM_0:%s:%s", SID32_2, SID32_3); + steam64 = String.format("%s%s", SID64_1, SID64_2); + return new SteamID(steam3, steam2, steam64, S3ID_3); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000..bd46c09 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,35 @@ +server: + port: 8080 + +spring: + application: + name: facti13_web_backend + datasource: + driver-class-name: com.mysql.jdbc.Driver + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + jpa: + hibernate: + ddl-auto: none + show-sql: true + +org: + jobrunr: + background-job-server: + enabled: true + dashboard: + enabled: true + +backend: + servers_file: ${SERVERS_FILE} + geoip_file: ${GEOIP_FILE} + updates: + unique_global: false + unique_server: false + ban_count: false + a2s: false + countries: true +logging: + level: + com.ibasco.agql.core.util.*: OFF \ No newline at end of file