From 588bfc9e1f6fda8dd3f4a83b12e757738799ebda Mon Sep 17 00:00:00 2001 From: Larry Date: Sat, 13 Jan 2024 16:21:30 -0700 Subject: [PATCH 1/5] Init RCON --- .editorconfig | 86 +++++++++++++++++++ DiscordPlayerCountBot/Bot/BotInformation.cs | 1 + .../DiscordPlayerCountBot.csproj | 1 + .../{Enum => Enums}/DataProvider.cs | 5 +- .../{Enum => Enums}/EnumHelper.cs | 2 +- .../{Enum => Enums}/HostEnviroment.cs | 2 +- .../Enums/RconServiceType.cs | 9 ++ .../Exceptions/ConfigurationException.cs | 10 +++ .../Exceptions/ParsingException.cs | 10 +++ .../SteamQueryAuthenticationException.cs | 9 ++ DiscordPlayerCountBot/Program.cs | 2 +- .../Providers/RconProvider.cs | 50 +++++++++++ .../Services/Rcon/IRconService.cs | 9 ++ .../Rcon/Praser/ArkInformationParser.cs | 29 +++++++ .../Rcon/Praser/CSGOInformationParser.cs | 31 +++++++ .../Rcon/Praser/IRconInformationParser.cs | 7 ++ .../Rcon/Praser/MinecraftInformationParser.cs | 30 +++++++ .../Services/Rcon/RconService.cs | 45 ++++++++++ .../Services/Rcon/RconServiceInformation.cs | 16 ++++ 19 files changed, 349 insertions(+), 5 deletions(-) rename DiscordPlayerCountBot/{Enum => Enums}/DataProvider.cs (58%) rename DiscordPlayerCountBot/{Enum => Enums}/EnumHelper.cs (94%) rename DiscordPlayerCountBot/{Enum => Enums}/HostEnviroment.cs (70%) create mode 100644 DiscordPlayerCountBot/Enums/RconServiceType.cs create mode 100644 DiscordPlayerCountBot/Exceptions/ConfigurationException.cs create mode 100644 DiscordPlayerCountBot/Exceptions/ParsingException.cs create mode 100644 DiscordPlayerCountBot/Exceptions/SteamQueryAuthenticationException.cs create mode 100644 DiscordPlayerCountBot/Providers/RconProvider.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/IRconService.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/Praser/ArkInformationParser.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/Praser/CSGOInformationParser.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/Praser/IRconInformationParser.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/Praser/MinecraftInformationParser.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/RconService.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/RconServiceInformation.cs diff --git a/.editorconfig b/.editorconfig index e84e194..2db502b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,3 +2,89 @@ # CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. dotnet_diagnostic.CS8618.severity = suggestion +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_indent_labels = one_less_than_current +csharp_space_around_binary_operators = before_and_after + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf diff --git a/DiscordPlayerCountBot/Bot/BotInformation.cs b/DiscordPlayerCountBot/Bot/BotInformation.cs index 4b4b345..66e55c0 100644 --- a/DiscordPlayerCountBot/Bot/BotInformation.cs +++ b/DiscordPlayerCountBot/Bot/BotInformation.cs @@ -14,6 +14,7 @@ public class BotInformation public string? StatusFormat { get; set; } public int? SunriseHour { get; set; } public int? SunsetHour { get; set; } + public string? RconServiceName { get; set; } public Tuple GetAddressAndPort() { diff --git a/DiscordPlayerCountBot/DiscordPlayerCountBot.csproj b/DiscordPlayerCountBot/DiscordPlayerCountBot.csproj index 94d7fda..6a60158 100644 --- a/DiscordPlayerCountBot/DiscordPlayerCountBot.csproj +++ b/DiscordPlayerCountBot/DiscordPlayerCountBot.csproj @@ -16,6 +16,7 @@ + diff --git a/DiscordPlayerCountBot/Enum/DataProvider.cs b/DiscordPlayerCountBot/Enums/DataProvider.cs similarity index 58% rename from DiscordPlayerCountBot/Enum/DataProvider.cs rename to DiscordPlayerCountBot/Enums/DataProvider.cs index 51fd4a9..321e5eb 100644 --- a/DiscordPlayerCountBot/Enum/DataProvider.cs +++ b/DiscordPlayerCountBot/Enums/DataProvider.cs @@ -1,4 +1,4 @@ -namespace PlayerCountBot.Enum +namespace PlayerCountBot.Enums { public enum DataProvider { @@ -6,6 +6,7 @@ public enum DataProvider CFX, SCUM, MINECRAFT, - BATTLEMETRICS + BATTLEMETRICS, + RCONClient } } diff --git a/DiscordPlayerCountBot/Enum/EnumHelper.cs b/DiscordPlayerCountBot/Enums/EnumHelper.cs similarity index 94% rename from DiscordPlayerCountBot/Enum/EnumHelper.cs rename to DiscordPlayerCountBot/Enums/EnumHelper.cs index fdc9946..bc7eca2 100644 --- a/DiscordPlayerCountBot/Enum/EnumHelper.cs +++ b/DiscordPlayerCountBot/Enums/EnumHelper.cs @@ -1,4 +1,4 @@ -namespace PlayerCountBot.Enum +namespace PlayerCountBot.Enums { public static class EnumHelper { diff --git a/DiscordPlayerCountBot/Enum/HostEnviroment.cs b/DiscordPlayerCountBot/Enums/HostEnviroment.cs similarity index 70% rename from DiscordPlayerCountBot/Enum/HostEnviroment.cs rename to DiscordPlayerCountBot/Enums/HostEnviroment.cs index 1fa9e0f..f1bdfde 100644 --- a/DiscordPlayerCountBot/Enum/HostEnviroment.cs +++ b/DiscordPlayerCountBot/Enums/HostEnviroment.cs @@ -1,4 +1,4 @@ -namespace PlayerCountBot.Enum +namespace PlayerCountBot.Enums { public enum HostEnvironment { diff --git a/DiscordPlayerCountBot/Enums/RconServiceType.cs b/DiscordPlayerCountBot/Enums/RconServiceType.cs new file mode 100644 index 0000000..e04becd --- /dev/null +++ b/DiscordPlayerCountBot/Enums/RconServiceType.cs @@ -0,0 +1,9 @@ +namespace DiscordPlayerCountBot.Enums +{ + public enum RconServiceType + { + CSGO, + Minecraft, + Ark + } +} diff --git a/DiscordPlayerCountBot/Exceptions/ConfigurationException.cs b/DiscordPlayerCountBot/Exceptions/ConfigurationException.cs new file mode 100644 index 0000000..d4ffa67 --- /dev/null +++ b/DiscordPlayerCountBot/Exceptions/ConfigurationException.cs @@ -0,0 +1,10 @@ +namespace DiscordPlayerCountBot.Exceptions +{ + internal class ConfigurationException : Exception + { + public ConfigurationException(string? message) : base(message) + { + + } + } +} diff --git a/DiscordPlayerCountBot/Exceptions/ParsingException.cs b/DiscordPlayerCountBot/Exceptions/ParsingException.cs new file mode 100644 index 0000000..dcc7c60 --- /dev/null +++ b/DiscordPlayerCountBot/Exceptions/ParsingException.cs @@ -0,0 +1,10 @@ +namespace DiscordPlayerCountBot.Exceptions +{ + public class ParsingException : Exception + { + public ParsingException(string? message) : base(message) + { + + } + } +} diff --git a/DiscordPlayerCountBot/Exceptions/SteamQueryAuthenticationException.cs b/DiscordPlayerCountBot/Exceptions/SteamQueryAuthenticationException.cs new file mode 100644 index 0000000..a5309d8 --- /dev/null +++ b/DiscordPlayerCountBot/Exceptions/SteamQueryAuthenticationException.cs @@ -0,0 +1,9 @@ +namespace DiscordPlayerCountBot.Exceptions +{ + internal class RconAuthenticationException : Exception + { + public RconAuthenticationException(string? message) : base(message) + { + } + } +} diff --git a/DiscordPlayerCountBot/Program.cs b/DiscordPlayerCountBot/Program.cs index 92fa7e0..d25e85e 100644 --- a/DiscordPlayerCountBot/Program.cs +++ b/DiscordPlayerCountBot/Program.cs @@ -2,7 +2,7 @@ global using PlayerCountBot.Attributes; global using PlayerCountBot.Configuration.Base; global using PlayerCountBot.Data; -global using PlayerCountBot.Enum; +global using PlayerCountBot.Enums; global using PlayerCountBot.Http; global using PlayerCountBot.Json; global using PlayerCountBot.Services; diff --git a/DiscordPlayerCountBot/Providers/RconProvider.cs b/DiscordPlayerCountBot/Providers/RconProvider.cs new file mode 100644 index 0000000..595718e --- /dev/null +++ b/DiscordPlayerCountBot/Providers/RconProvider.cs @@ -0,0 +1,50 @@ +using DiscordPlayerCountBot.Enums; +using DiscordPlayerCountBot.Enums; +using DiscordPlayerCountBot.Exceptions; +using DiscordPlayerCountBot.Services; + +namespace PlayerCountBot.Providers +{ + [Name("Rcon")] + public class RconProvider : ServerInformationProvider + { + public RconProvider(BotInformation info) : base(info) + { + } + + public async override Task GetServerInformation(BotInformation information, Dictionary applicationVariables) + { + var values = $"Valid Values: {string.Join(",", Enum.GetNames())}"; + + if (information.RconServiceName == null) + { + throw new ConfigurationException($"Bot: {information.Name} must have RconServiceName specified in it's config. {values}"); + } + + var service = new RconService(); + + if(!Enum.TryParse(information.RconServiceName, true, out var serviceType)) + { + throw new ConfigurationException($"Bot: {information.Name} has an invalid RconServiceName specified in it's config. {values}"); + } + + try + { + var addressAndPort = information.GetAddressAndPort(); + var response = await service.GetRconResponse(addressAndPort.Item1, addressAndPort.Item2, applicationVariables["RconPassword"], serviceType); + + if(response == null) + throw new ApplicationException($"Server Address: {information.Address} was not found in Steam's directory."); + + HandleLastException(information); + + return response; + } + catch (Exception e) + { + HandleException(e); + return null; + } + } + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/IRconService.cs b/DiscordPlayerCountBot/Services/Rcon/IRconService.cs new file mode 100644 index 0000000..47ce035 --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/IRconService.cs @@ -0,0 +1,9 @@ +using DiscordPlayerCountBot.Enums; + +namespace DiscordPlayerCountBot.Services +{ + public interface IRconService + { + public Task GetRconResponse(string address, int port, string authorizationToken, RconServiceType serviceType); + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/Praser/ArkInformationParser.cs b/DiscordPlayerCountBot/Services/Rcon/Praser/ArkInformationParser.cs new file mode 100644 index 0000000..528c264 --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/Praser/ArkInformationParser.cs @@ -0,0 +1,29 @@ +using DiscordPlayerCountBot.Exceptions; +using System.Text.RegularExpressions; + +namespace DiscordPlayerCountBot.Services.Praser +{ + public class ArkInformationParser : IRconInformationParser + { + public BaseViewModel Parse(string message) + { + var match = Regex.Match(message, @"There are (\d+)/(\d+) players online(?:.*\nQueue:\s+(\d+)\s+players waiting)?"); + + if (!match.Success) + { + throw new ParsingException("Could not find players, max players, or queue information from an ARK RCON Response"); + } + + var players = int.Parse(match.Groups[1].Value); + var maxPlayers = int.Parse(match.Groups[2].Value); + int queuedPlayers = match.Groups.Count < 3 || string.IsNullOrEmpty(match.Groups[3].Value) ? 0 : int.Parse(match.Groups[3].Value); + + return new BaseViewModel() + { + Players = players, + MaxPlayers = maxPlayers, + QueuedPlayers = queuedPlayers + }; + } + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/Praser/CSGOInformationParser.cs b/DiscordPlayerCountBot/Services/Rcon/Praser/CSGOInformationParser.cs new file mode 100644 index 0000000..21b44e5 --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/Praser/CSGOInformationParser.cs @@ -0,0 +1,31 @@ +using DiscordPlayerCountBot.Exceptions; +using System.Text.RegularExpressions; + +namespace DiscordPlayerCountBot.Services.Praser +{ + public class CSGOInformationParser : IRconInformationParser + { + public BaseViewModel Parse(string message) + { + var playersMatch = Regex.Match(message, @"players\s+:\s+(\d+)\s+humans"); + var maxPlayersMatch = Regex.Match(message, @"\((\d+)/\d+\s+max\)"); + var queuedPlayersMatch = Regex.Match(message, @"queue\s+:\s+(\d+)\s+players waiting"); + + if (!playersMatch.Success || !maxPlayersMatch.Success) + { + throw new ParsingException("Could not find players or max players from a CSGO Rcon Response"); + } + + var players = int.Parse(playersMatch.Groups[0].Value); + var maxPlayers = int.Parse(maxPlayersMatch.Groups[0].Value); + int queuedPlayers = !queuedPlayersMatch.Success ? 0 : int.Parse(queuedPlayersMatch.Groups[0].Value); + + return new BaseViewModel() + { + Players = players, + MaxPlayers = maxPlayers, + QueuedPlayers = queuedPlayers + }; + } + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/Praser/IRconInformationParser.cs b/DiscordPlayerCountBot/Services/Rcon/Praser/IRconInformationParser.cs new file mode 100644 index 0000000..abd74d9 --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/Praser/IRconInformationParser.cs @@ -0,0 +1,7 @@ +namespace DiscordPlayerCountBot.Services.Praser +{ + public interface IRconInformationParser + { + public BaseViewModel Parse(string message); + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/Praser/MinecraftInformationParser.cs b/DiscordPlayerCountBot/Services/Rcon/Praser/MinecraftInformationParser.cs new file mode 100644 index 0000000..1e77d24 --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/Praser/MinecraftInformationParser.cs @@ -0,0 +1,30 @@ +using DiscordPlayerCountBot.Exceptions; +using System.Text.RegularExpressions; + +namespace DiscordPlayerCountBot.Services.Praser +{ + public class MinecraftInformationParser : IRconInformationParser + { + public BaseViewModel Parse(string message) + { + var playerInfoMatch = Regex.Match(message, @"There are (\d+)/(\d+) players online"); + var queuedPlayersMatch = Regex.Match(message, @"Queue:\s+(\d+)\s+players waiting"); + + if (!playerInfoMatch.Success || playerInfoMatch.Groups.Count != 2) + { + throw new ParsingException("Could not find players or max players from a CSGO Rcon Response"); + } + + var players = int.Parse(playerInfoMatch.Groups[0].Value); + var maxPlayers = int.Parse(playerInfoMatch.Groups[1].Value); + int queuedPlayers = !queuedPlayersMatch.Success ? 0 : int.Parse(queuedPlayersMatch.Groups[0].Value); + + return new BaseViewModel() + { + Players = players, + MaxPlayers = maxPlayers, + QueuedPlayers = queuedPlayers + }; + } + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/RconService.cs b/DiscordPlayerCountBot/Services/Rcon/RconService.cs new file mode 100644 index 0000000..556662b --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/RconService.cs @@ -0,0 +1,45 @@ +using DiscordPlayerCountBot.Enums; +using DiscordPlayerCountBot.Exceptions; +using DiscordPlayerCountBot.Services.Praser; +using RconSharp; + +namespace DiscordPlayerCountBot.Services +{ + public class RconService : IRconService + { + private Dictionary PlayerCommands = new() + { + {RconServiceType.CSGO, new RconServiceInformation("status", new CSGOInformationParser())}, + {RconServiceType.Minecraft, new RconServiceInformation("list", new MinecraftInformationParser()) }, + {RconServiceType.Ark, new RconServiceInformation("listplayers", new ArkInformationParser())} + }; + + public async Task GetRconResponse(string address, int port, string authorizationToken, RconServiceType serviceType) + { + if (!PlayerCommands.TryGetValue(serviceType, out var serviceInformation)) + { + throw new ArgumentException("Unsupported Rcon Service Type.... How did you get here?"); + } + + var client = RconClient.Create(address, port); + + await client.ConnectAsync(); + + var authenticated = await client.AuthenticateAsync(authorizationToken); + + if (!authenticated) + { + throw new RconAuthenticationException($"There was a failure to authenticate with server: {address}:{port}.\nIs your password correct?\nIs this the correct server address?"); + } + + var command = serviceInformation.PlayersCommand; + var response = await client.ExecuteCommandAsync(command); + var viewModel = serviceInformation.Parser.Parse(response); + + viewModel.Address = address; + viewModel.Port = port; + + return viewModel; + } + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/RconServiceInformation.cs b/DiscordPlayerCountBot/Services/Rcon/RconServiceInformation.cs new file mode 100644 index 0000000..c06e30c --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/RconServiceInformation.cs @@ -0,0 +1,16 @@ +using DiscordPlayerCountBot.Services.Praser; + +namespace DiscordPlayerCountBot.Services +{ + public class RconServiceInformation + { + public string PlayersCommand { get; private set; } + public IRconInformationParser Parser { get; private set; } + + public RconServiceInformation(string playersCommand, IRconInformationParser parser) + { + PlayersCommand = playersCommand; + Parser = parser; + } + } +} From b42f2b7c2b9f8ee9dd565213e561b059ea214162 Mon Sep 17 00:00:00 2001 From: Larry Date: Sat, 13 Jan 2024 18:43:45 -0700 Subject: [PATCH 2/5] Add Steam Query, and Rcon as data providers --- DiscordPlayerCountBot/Enums/DataProvider.cs | 3 +- .../Base/ServerInformationProvider.cs | 10 ++++- .../Providers/RconProvider.cs | 1 - .../Providers/SteamQueryProvider.cs | 37 +++++++++++++++++++ .../Services/Rcon/RconService.cs | 2 +- .../Services/SteamQuery/ISteamQueryService.cs | 9 +++++ .../Services/SteamQuery/SteamQueryService.cs | 29 +++++++++++++++ 7 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 DiscordPlayerCountBot/Providers/SteamQueryProvider.cs create mode 100644 DiscordPlayerCountBot/Services/SteamQuery/ISteamQueryService.cs create mode 100644 DiscordPlayerCountBot/Services/SteamQuery/SteamQueryService.cs diff --git a/DiscordPlayerCountBot/Enums/DataProvider.cs b/DiscordPlayerCountBot/Enums/DataProvider.cs index 321e5eb..66f7585 100644 --- a/DiscordPlayerCountBot/Enums/DataProvider.cs +++ b/DiscordPlayerCountBot/Enums/DataProvider.cs @@ -7,6 +7,7 @@ public enum DataProvider SCUM, MINECRAFT, BATTLEMETRICS, - RCONClient + RCONClient, + SteamQuery } } diff --git a/DiscordPlayerCountBot/Providers/Base/ServerInformationProvider.cs b/DiscordPlayerCountBot/Providers/Base/ServerInformationProvider.cs index 145bc97..1317f2c 100644 --- a/DiscordPlayerCountBot/Providers/Base/ServerInformationProvider.cs +++ b/DiscordPlayerCountBot/Providers/Base/ServerInformationProvider.cs @@ -1,4 +1,5 @@ -using System.Net; +using SteamServerQuery; +using System.Net; using System.Net.Http; namespace PlayerCountBot.Providers.Base @@ -17,7 +18,6 @@ protected void HandleLastException(BotInformation information) { if (WasLastExecutionAFailure) { - Info($"Bot named: {information.Name} at address: {information.Address} successfully fetched data after failure."); LastException = null; WasLastExecutionAFailure = false; @@ -76,6 +76,12 @@ protected void HandleException(Exception e) return; } + if(e is SteamException steamException) + { + Error($"There was an issue speaking with Steam Query Server.", e); + return; + } + Error($"There was an error speaking with {Label}.", e); throw e; } diff --git a/DiscordPlayerCountBot/Providers/RconProvider.cs b/DiscordPlayerCountBot/Providers/RconProvider.cs index 595718e..fa7eec5 100644 --- a/DiscordPlayerCountBot/Providers/RconProvider.cs +++ b/DiscordPlayerCountBot/Providers/RconProvider.cs @@ -1,5 +1,4 @@ using DiscordPlayerCountBot.Enums; -using DiscordPlayerCountBot.Enums; using DiscordPlayerCountBot.Exceptions; using DiscordPlayerCountBot.Services; diff --git a/DiscordPlayerCountBot/Providers/SteamQueryProvider.cs b/DiscordPlayerCountBot/Providers/SteamQueryProvider.cs new file mode 100644 index 0000000..8a02379 --- /dev/null +++ b/DiscordPlayerCountBot/Providers/SteamQueryProvider.cs @@ -0,0 +1,37 @@ +using DiscordPlayerCountBot.Services.SteamQuery; + +namespace PlayerCountBot.Providers +{ + [Name("Steam Query")] + public class SteamQueryProvider : ServerInformationProvider + { + public SteamQueryProvider(BotInformation info) : base(info) + { + } + + public async override Task GetServerInformation(BotInformation information, Dictionary applicationVariables) + { + var service = new SteamQueryService(); + + try + { + var addressAndPort = information.GetAddressAndPort(); + var response = await service.GetQueryResponse(addressAndPort.Item1, addressAndPort.Item2); + + if (response == null) + { + throw new ApplicationException($" Failed to get a Server Information response from Steam Query."); + } + + HandleLastException(information); + + return response; + } + catch (Exception e) + { + HandleException(e); + return null; + } + } + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/RconService.cs b/DiscordPlayerCountBot/Services/Rcon/RconService.cs index 556662b..b92f001 100644 --- a/DiscordPlayerCountBot/Services/Rcon/RconService.cs +++ b/DiscordPlayerCountBot/Services/Rcon/RconService.cs @@ -7,7 +7,7 @@ namespace DiscordPlayerCountBot.Services { public class RconService : IRconService { - private Dictionary PlayerCommands = new() + private readonly Dictionary PlayerCommands = new() { {RconServiceType.CSGO, new RconServiceInformation("status", new CSGOInformationParser())}, {RconServiceType.Minecraft, new RconServiceInformation("list", new MinecraftInformationParser()) }, diff --git a/DiscordPlayerCountBot/Services/SteamQuery/ISteamQueryService.cs b/DiscordPlayerCountBot/Services/SteamQuery/ISteamQueryService.cs new file mode 100644 index 0000000..5eb2f46 --- /dev/null +++ b/DiscordPlayerCountBot/Services/SteamQuery/ISteamQueryService.cs @@ -0,0 +1,9 @@ +using DiscordPlayerCountBot.Enums; + +namespace DiscordPlayerCountBot.Services.SteamQuery +{ + internal interface ISteamQueryService + { + public Task GetQueryResponse(string address, int port); + } +} diff --git a/DiscordPlayerCountBot/Services/SteamQuery/SteamQueryService.cs b/DiscordPlayerCountBot/Services/SteamQuery/SteamQueryService.cs new file mode 100644 index 0000000..884f1a4 --- /dev/null +++ b/DiscordPlayerCountBot/Services/SteamQuery/SteamQueryService.cs @@ -0,0 +1,29 @@ +using SteamServerQuery; + +namespace DiscordPlayerCountBot.Services.SteamQuery +{ + public class SteamQueryService : ISteamQueryService + { + public async Task GetQueryResponse(string address, int port, string authorizationToken) + { + var model = new BaseViewModel() + { + Address = address, + Port = port + }; + + try + { + var serverInformation = await SteamServer.QueryServerAsync(address, port = 0); + model.MaxPlayers = serverInformation.MaxPlayers; + model.Players = serverInformation.Players; + model.QueuedPlayers = 0; + }catch + { + throw; + } + + return model; + } + } +} From 9dd4ea8aef3f4016620af878597406b49be06dab Mon Sep 17 00:00:00 2001 From: Larry Date: Sat, 13 Jan 2024 18:53:36 -0700 Subject: [PATCH 3/5] Linting V1 --- .../Providers/Base/ServerInformationProvider.cs | 2 +- DiscordPlayerCountBot/Providers/RconProvider.cs | 4 ++-- .../Services/SteamQuery/SteamQueryService.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DiscordPlayerCountBot/Providers/Base/ServerInformationProvider.cs b/DiscordPlayerCountBot/Providers/Base/ServerInformationProvider.cs index 1317f2c..77e67dc 100644 --- a/DiscordPlayerCountBot/Providers/Base/ServerInformationProvider.cs +++ b/DiscordPlayerCountBot/Providers/Base/ServerInformationProvider.cs @@ -76,7 +76,7 @@ protected void HandleException(Exception e) return; } - if(e is SteamException steamException) + if (e is SteamException steamException) { Error($"There was an issue speaking with Steam Query Server.", e); return; diff --git a/DiscordPlayerCountBot/Providers/RconProvider.cs b/DiscordPlayerCountBot/Providers/RconProvider.cs index fa7eec5..d540515 100644 --- a/DiscordPlayerCountBot/Providers/RconProvider.cs +++ b/DiscordPlayerCountBot/Providers/RconProvider.cs @@ -22,7 +22,7 @@ public RconProvider(BotInformation info) : base(info) var service = new RconService(); - if(!Enum.TryParse(information.RconServiceName, true, out var serviceType)) + if (!Enum.TryParse(information.RconServiceName, true, out var serviceType)) { throw new ConfigurationException($"Bot: {information.Name} has an invalid RconServiceName specified in it's config. {values}"); } @@ -32,7 +32,7 @@ public RconProvider(BotInformation info) : base(info) var addressAndPort = information.GetAddressAndPort(); var response = await service.GetRconResponse(addressAndPort.Item1, addressAndPort.Item2, applicationVariables["RconPassword"], serviceType); - if(response == null) + if (response == null) throw new ApplicationException($"Server Address: {information.Address} was not found in Steam's directory."); HandleLastException(information); diff --git a/DiscordPlayerCountBot/Services/SteamQuery/SteamQueryService.cs b/DiscordPlayerCountBot/Services/SteamQuery/SteamQueryService.cs index 884f1a4..abf3df0 100644 --- a/DiscordPlayerCountBot/Services/SteamQuery/SteamQueryService.cs +++ b/DiscordPlayerCountBot/Services/SteamQuery/SteamQueryService.cs @@ -4,7 +4,7 @@ namespace DiscordPlayerCountBot.Services.SteamQuery { public class SteamQueryService : ISteamQueryService { - public async Task GetQueryResponse(string address, int port, string authorizationToken) + public async Task GetQueryResponse(string address, int port) { var model = new BaseViewModel() { @@ -18,7 +18,7 @@ public async Task GetQueryResponse(string address, int port, stri model.MaxPlayers = serverInformation.MaxPlayers; model.Players = serverInformation.Players; model.QueuedPlayers = 0; - }catch + } catch { throw; } From 88e224462998ddb7d570e10ea61871d427836db5 Mon Sep 17 00:00:00 2001 From: Larry Date: Sat, 13 Jan 2024 18:55:58 -0700 Subject: [PATCH 4/5] Linting V2 --- DiscordPlayerCountBot/DiscordPlayerCountBot.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DiscordPlayerCountBot/DiscordPlayerCountBot.csproj b/DiscordPlayerCountBot/DiscordPlayerCountBot.csproj index 6a60158..71a54fe 100644 --- a/DiscordPlayerCountBot/DiscordPlayerCountBot.csproj +++ b/DiscordPlayerCountBot/DiscordPlayerCountBot.csproj @@ -17,7 +17,7 @@ - + From 7979550374773d4c9e347afe785300ff1e9e03d2 Mon Sep 17 00:00:00 2001 From: Larry Date: Mon, 15 Jan 2024 12:21:26 -0700 Subject: [PATCH 5/5] Split up Service Information for later updates, first test with Minecraft was successful --- DiscordPlayerCountBot/Bot/Bot.cs | 2 ++ .../Rcon/Praser/MinecraftInformationParser.cs | 12 +++++----- .../Services/Rcon/RconService.cs | 14 +++++------ .../Services/Rcon/RconServiceInformation.cs | 16 +++++-------- .../ArkRconServiceInformation.cs | 23 +++++++++++++++++++ .../CSGORconServiceInformation.cs | 23 +++++++++++++++++++ .../MinecraftRconServiceInformation.cs | 23 +++++++++++++++++++ 7 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 DiscordPlayerCountBot/Services/Rcon/ServiceInformation/ArkRconServiceInformation.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/ServiceInformation/CSGORconServiceInformation.cs create mode 100644 DiscordPlayerCountBot/Services/Rcon/ServiceInformation/MinecraftRconServiceInformation.cs diff --git a/DiscordPlayerCountBot/Bot/Bot.cs b/DiscordPlayerCountBot/Bot/Bot.cs index 8a10036..89e488c 100644 --- a/DiscordPlayerCountBot/Bot/Bot.cs +++ b/DiscordPlayerCountBot/Bot/Bot.cs @@ -29,6 +29,8 @@ public void InitDataProviders() DataProviders.Add((int)DataProvider.CFX, new CFXProvider(Information!)); DataProviders.Add((int)DataProvider.MINECRAFT, new MinecraftProvider(Information!)); DataProviders.Add((int)DataProvider.BATTLEMETRICS, new BattleMetricsProvider(Information!)); + DataProviders.Add((int)DataProvider.RCONClient, new RconProvider(Information!)); + DataProviders.Add((int)DataProvider.SteamQuery, new SteamQueryProvider(Information!)); } public async Task StartAsync(bool shouldStart) diff --git a/DiscordPlayerCountBot/Services/Rcon/Praser/MinecraftInformationParser.cs b/DiscordPlayerCountBot/Services/Rcon/Praser/MinecraftInformationParser.cs index 1e77d24..2411fd1 100644 --- a/DiscordPlayerCountBot/Services/Rcon/Praser/MinecraftInformationParser.cs +++ b/DiscordPlayerCountBot/Services/Rcon/Praser/MinecraftInformationParser.cs @@ -7,17 +7,17 @@ public class MinecraftInformationParser : IRconInformationParser { public BaseViewModel Parse(string message) { - var playerInfoMatch = Regex.Match(message, @"There are (\d+)/(\d+) players online"); + var playerInfoMatch = Regex.Match(message, @"(\d+)\s+of a max of\s+(\d+)"); var queuedPlayersMatch = Regex.Match(message, @"Queue:\s+(\d+)\s+players waiting"); - if (!playerInfoMatch.Success || playerInfoMatch.Groups.Count != 2) + if (!playerInfoMatch.Success || playerInfoMatch.Groups.Count < 3) { - throw new ParsingException("Could not find players or max players from a CSGO Rcon Response"); + throw new ParsingException("Could not find players or max players from a Minecraft Rcon Response"); } - var players = int.Parse(playerInfoMatch.Groups[0].Value); - var maxPlayers = int.Parse(playerInfoMatch.Groups[1].Value); - int queuedPlayers = !queuedPlayersMatch.Success ? 0 : int.Parse(queuedPlayersMatch.Groups[0].Value); + var players = int.Parse(playerInfoMatch.Groups[1].Value); + var maxPlayers = int.Parse(playerInfoMatch.Groups[2].Value); + int queuedPlayers = !queuedPlayersMatch.Success ? 0 : int.Parse(queuedPlayersMatch.Groups[1].Value); return new BaseViewModel() { diff --git a/DiscordPlayerCountBot/Services/Rcon/RconService.cs b/DiscordPlayerCountBot/Services/Rcon/RconService.cs index b92f001..474025e 100644 --- a/DiscordPlayerCountBot/Services/Rcon/RconService.cs +++ b/DiscordPlayerCountBot/Services/Rcon/RconService.cs @@ -1,17 +1,17 @@ using DiscordPlayerCountBot.Enums; using DiscordPlayerCountBot.Exceptions; -using DiscordPlayerCountBot.Services.Praser; +using DiscordPlayerCountBot.Services.Rcon.ServiceInformation; using RconSharp; namespace DiscordPlayerCountBot.Services { public class RconService : IRconService { - private readonly Dictionary PlayerCommands = new() + private readonly Dictionary PlayerCommands = new() { - {RconServiceType.CSGO, new RconServiceInformation("status", new CSGOInformationParser())}, - {RconServiceType.Minecraft, new RconServiceInformation("list", new MinecraftInformationParser()) }, - {RconServiceType.Ark, new RconServiceInformation("listplayers", new ArkInformationParser())} + {RconServiceType.CSGO, new CSGORconServiceInformation()}, + {RconServiceType.Minecraft, new MinecraftRconServiceInformation()}, + {RconServiceType.Ark, new ArkRconServiceInformation()} }; public async Task GetRconResponse(string address, int port, string authorizationToken, RconServiceType serviceType) @@ -32,9 +32,9 @@ public async Task GetRconResponse(string address, int port, strin throw new RconAuthenticationException($"There was a failure to authenticate with server: {address}:{port}.\nIs your password correct?\nIs this the correct server address?"); } - var command = serviceInformation.PlayersCommand; + var command = serviceInformation.GetPlayerListCommand(); var response = await client.ExecuteCommandAsync(command); - var viewModel = serviceInformation.Parser.Parse(response); + var viewModel = serviceInformation.GetParser().Parse(response); viewModel.Address = address; viewModel.Port = port; diff --git a/DiscordPlayerCountBot/Services/Rcon/RconServiceInformation.cs b/DiscordPlayerCountBot/Services/Rcon/RconServiceInformation.cs index c06e30c..40f8184 100644 --- a/DiscordPlayerCountBot/Services/Rcon/RconServiceInformation.cs +++ b/DiscordPlayerCountBot/Services/Rcon/RconServiceInformation.cs @@ -1,16 +1,12 @@ -using DiscordPlayerCountBot.Services.Praser; +using DiscordPlayerCountBot.Enums; +using DiscordPlayerCountBot.Services.Praser; namespace DiscordPlayerCountBot.Services { - public class RconServiceInformation + public interface IRconServiceInformation { - public string PlayersCommand { get; private set; } - public IRconInformationParser Parser { get; private set; } - - public RconServiceInformation(string playersCommand, IRconInformationParser parser) - { - PlayersCommand = playersCommand; - Parser = parser; - } + public abstract RconServiceType GetServiceType(); + public abstract string GetPlayerListCommand(); + public abstract IRconInformationParser GetParser(); } } diff --git a/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/ArkRconServiceInformation.cs b/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/ArkRconServiceInformation.cs new file mode 100644 index 0000000..0fd54c9 --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/ArkRconServiceInformation.cs @@ -0,0 +1,23 @@ +using DiscordPlayerCountBot.Enums; +using DiscordPlayerCountBot.Services.Praser; + +namespace DiscordPlayerCountBot.Services.Rcon.ServiceInformation +{ + public class ArkRconServiceInformation : IRconServiceInformation + { + public IRconInformationParser GetParser() + { + return new ArkInformationParser(); + } + + public RconServiceType GetServiceType() + { + return RconServiceType.Ark; + } + + public string GetPlayerListCommand() + { + return "listplayers"; + } + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/CSGORconServiceInformation.cs b/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/CSGORconServiceInformation.cs new file mode 100644 index 0000000..1dc2ad3 --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/CSGORconServiceInformation.cs @@ -0,0 +1,23 @@ +using DiscordPlayerCountBot.Enums; +using DiscordPlayerCountBot.Services.Praser; + +namespace DiscordPlayerCountBot.Services.Rcon.ServiceInformation +{ + public class CSGORconServiceInformation : IRconServiceInformation + { + public IRconInformationParser GetParser() + { + return new CSGOInformationParser(); + } + + public RconServiceType GetServiceType() + { + return RconServiceType.CSGO; + } + + public string GetPlayerListCommand() + { + return "status"; + } + } +} diff --git a/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/MinecraftRconServiceInformation.cs b/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/MinecraftRconServiceInformation.cs new file mode 100644 index 0000000..e99a2e9 --- /dev/null +++ b/DiscordPlayerCountBot/Services/Rcon/ServiceInformation/MinecraftRconServiceInformation.cs @@ -0,0 +1,23 @@ +using DiscordPlayerCountBot.Enums; +using DiscordPlayerCountBot.Services.Praser; + +namespace DiscordPlayerCountBot.Services.Rcon.ServiceInformation +{ + public class MinecraftRconServiceInformation : IRconServiceInformation + { + public IRconInformationParser GetParser() + { + return new MinecraftInformationParser(); + } + + public RconServiceType GetServiceType() + { + return RconServiceType.Minecraft; + } + + public string GetPlayerListCommand() + { + return "list"; + } + } +}