diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0d54e95..d6c60c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,11 @@ jobs: env: QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} + - name: Run QDNET Scan + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: ${{ runner.temp }}/qodana/results/qodana.sarif.json + build: name: Build and Upload Artifacts runs-on: ubuntu-latest @@ -36,30 +41,47 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: 7.0.x + - name: Restore Dependencies + run: dotnet restore - name: Get Commit Hash id: hash run: echo "hash=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - name: Restore dependencies - run: dotnet restore - - - name: Publish for Windows x64 - run: dotnet publish -c Release -r win-x64 --self-contained - - name: Publish for Linux x64 - run: dotnet publish -c Release -r linux-x64 --self-contained + - name: Publish CLI for Windows x64 + run: dotnet publish -c Release -r win-x64 --self-contained PLRPC + - name: Publish CLI for Linux x64 + run: dotnet publish -c Release -r linux-x64 --self-contained PLRPC + - name: Publish GUI for Windows x64 + run: dotnet publish -c Release -r win-x64 --self-contained PLRPC.GUI.Windows + - name: Publish GUI for Linux x64 + run: dotnet publish -c Release -r linux-x64 --self-contained PLRPC.GUI.Linux - - name: Upload Windows x64 build + - name: Upload Windows x64 CLI build uses: actions/upload-artifact@v3.1.1 with: - name: PLRPC Windows x64 [${{ steps.hash.outputs.hash }}] + name: PLRPC CLI Windows x64 [${{ steps.hash.outputs.hash }}] path: "/home/runner/work/PLRPC/PLRPC/PLRPC/bin/Release/net7.0/win-x64/publish/" if-no-files-found: error retention-days: 3 - - name: Upload Linux x64 build + - name: Upload Linux x64 CLI build uses: actions/upload-artifact@v3.1.1 with: - name: PLRPC Linux x64 [${{ steps.hash.outputs.hash }}] + name: PLRPC CLI Linux x64 [${{ steps.hash.outputs.hash }}] path: "/home/runner/work/PLRPC/PLRPC/PLRPC/bin/Release/net7.0/linux-x64/publish/" if-no-files-found: error retention-days: 3 + - name: Upload Windows x64 GUI build + uses: actions/upload-artifact@v3.1.1 + with: + name: PLRPC GUI Windows x64 [${{ steps.hash.outputs.hash }}] + path: "/home/runner/work/PLRPC/PLRPC/PLRPC.GUI.Windows/bin/Release/net7.0-windows/win-x64/publish/" + if-no-files-found: error + retention-days: 3 + - name: Upload Linux x64 GUI build + uses: actions/upload-artifact@v3.1.1 + with: + name: PLRPC GUI Linux x64 [${{ steps.hash.outputs.hash }}] + path: "/home/runner/work/PLRPC/PLRPC/PLRPC.GUI.Linux/bin/Release/net7.0/linux-x64/publish/" + if-no-files-found: error + retention-days: 3 \ No newline at end of file diff --git a/PLRPC.GUI.Linux/PLRPC.GUI.Linux.csproj b/PLRPC.GUI.Linux/PLRPC.GUI.Linux.csproj new file mode 100644 index 0000000..c7bf914 --- /dev/null +++ b/PLRPC.GUI.Linux/PLRPC.GUI.Linux.csproj @@ -0,0 +1,25 @@ + + + + LBPUnion.PLRPC.GUI + enable + disable + + + + Exe + true + net7.0 + linux-x64 + + + + false + + + + + + + + diff --git a/PLRPC.GUI.Linux/Program.cs b/PLRPC.GUI.Linux/Program.cs new file mode 100644 index 0000000..2dc1374 --- /dev/null +++ b/PLRPC.GUI.Linux/Program.cs @@ -0,0 +1,10 @@ +namespace LBPUnion.PLRPC.GUI; + +public static class Program +{ + [STAThread] + public static void Main() + { + Gui.Initialize(); + } +} \ No newline at end of file diff --git a/PLRPC.GUI.Windows/PLRPC.GUI.Windows.csproj b/PLRPC.GUI.Windows/PLRPC.GUI.Windows.csproj new file mode 100644 index 0000000..ce31494 --- /dev/null +++ b/PLRPC.GUI.Windows/PLRPC.GUI.Windows.csproj @@ -0,0 +1,30 @@ + + + + LBPUnion.PLRPC.GUI + enable + disable + + + + Exe + true + net7.0-windows + win-x64 + + + + false + + + + + + + + + + + + + diff --git a/PLRPC.GUI.Windows/Program.cs b/PLRPC.GUI.Windows/Program.cs new file mode 100644 index 0000000..2dc1374 --- /dev/null +++ b/PLRPC.GUI.Windows/Program.cs @@ -0,0 +1,10 @@ +namespace LBPUnion.PLRPC.GUI; + +public static class Program +{ + [STAThread] + public static void Main() + { + Gui.Initialize(); + } +} \ No newline at end of file diff --git a/PLRPC.GUI/Forms/MainForm.cs b/PLRPC.GUI/Forms/MainForm.cs new file mode 100644 index 0000000..a8d87cb --- /dev/null +++ b/PLRPC.GUI/Forms/MainForm.cs @@ -0,0 +1,170 @@ +using System.Text; +using Eto.Drawing; +using Eto.Forms; +using LBPUnion.PLRPC.Helpers; +using LBPUnion.PLRPC.Types.Logging; +using Serilog; + +namespace LBPUnion.PLRPC.GUI.Forms; + +public class MainForm : Form +{ + private static readonly TextBox username; + private static readonly TextBox serverUrl; + private static readonly TextBox applicationId; + + public MainForm() + { + this.Title = "PLRPC"; + this.ClientSize = new Size(400, -1); + this.Resizable = false; + + this.Content = this.tableLayout; + + Log.Logger = Program.Logger; + } + + private static readonly GroupBox configurationEntries = new() + { + Text = Strings.MainForm.Configuration, + Content = new TableLayout + { + Padding = new Padding(3, 3, 3, 3), + Spacing = new Size(3, 3), + Rows = + { + new TableRow(new List + { + new(new Label + { + Text = Strings.MainForm.Username, + }), + new(username = new TextBox()), + }), + new TableRow(new List + { + new(new Label + { + Text = Strings.MainForm.ServerUrl, + }), + new(serverUrl = new TextBox + { + Text = "https://lighthouse.lbpunion.com/", + Enabled = false, + }), + }), + new TableRow(new List + { + new(new Label + { + Text = Strings.MainForm.ApplicationId, + }), + new(applicationId = new TextBox + { + Text = "1060973475151495288", + Enabled = false, + }), + }), + }, + }, + }; + + private static readonly Button connectButton = new(InitializeClientHandler) + { + Text = Strings.MainForm.Connect, + }; + + private static readonly Button unlockDefaultsButton = new(UnlockDefaultsHandler) + { + Text = Strings.MainForm.UnlockDefaults, + }; + + private readonly TableLayout tableLayout = new() + { + Padding = new Padding(10, 10, 10, 10), + Spacing = new Size(5, 5), + Rows = + { + new TableRow(configurationEntries), + new TableRow(connectButton), + new TableRow(unlockDefaultsButton), + }, + }; + + private static async void InitializeClientHandler(object sender, EventArgs eventArgs) + { + List arguments = new() + { + serverUrl, + username, + applicationId, + }; + + switch (arguments) + { + case not null when arguments.Any(a => string.IsNullOrWhiteSpace(a.Text)): + { + MessageBox.Show(Strings.MainForm.BlankFieldsError, MessageBoxButtons.OK, MessageBoxType.Error); + return; + } + case not null when !ValidationHelper.IsValidUsername(username.Text): + { + MessageBox.Show(Strings.MainForm.InvalidUsernameError, MessageBoxButtons.OK, MessageBoxType.Error); + return; + } + case not null when !ValidationHelper.IsValidUrl(serverUrl.Text): + { + MessageBox.Show(Strings.MainForm.InvalidUrlError, MessageBoxButtons.OK, MessageBoxType.Error); + return; + } + } + + try + { + // Text changes + connectButton.Text = Strings.MainForm.Connected; + + // Button states + connectButton.Enabled = false; + unlockDefaultsButton.Enabled = false; + + // Field states + serverUrl.Enabled = false; + username.Enabled = false; + applicationId.Enabled = false; + + await Program.InitializeLighthouseClient(serverUrl.Text.Trim('/'), username.Text, applicationId.Text); + } + catch (Exception exception) + { + StringBuilder exceptionBuilder = new(); + + exceptionBuilder.AppendLine($"{Strings.MainForm.InitializationError}\n"); + exceptionBuilder.AppendLine($"{exception.Message}\n"); + exceptionBuilder.AppendLine($"{exception.Source}"); + + Log.Error(exception, "{@Area}: Failed to initialize the client", + LogArea.LighthouseClient); + + MessageBox.Show(exceptionBuilder.ToString(), MessageBoxButtons.OK, MessageBoxType.Error); + } + } + + private static void UnlockDefaultsHandler(object sender, EventArgs eventArgs) + { + // Text changes + unlockDefaultsButton.Text = Strings.MainForm.UnlockedDefaults; + + // Button states + unlockDefaultsButton.Enabled = false; + + // Field states + serverUrl.Enabled = true; + applicationId.Enabled = true; + + MessageBox.Show(Strings.MainForm.UnlockedDefaultsWarning, + "Warning", + MessageBoxButtons.OK, + MessageBoxType.Warning); + } +} \ No newline at end of file diff --git a/PLRPC.GUI/Gui.cs b/PLRPC.GUI/Gui.cs new file mode 100644 index 0000000..45f8768 --- /dev/null +++ b/PLRPC.GUI/Gui.cs @@ -0,0 +1,12 @@ +using Eto.Forms; +using LBPUnion.PLRPC.GUI.Forms; + +namespace LBPUnion.PLRPC.GUI; + +public static class Gui +{ + public static void Initialize() + { + new Application().Run(new MainForm()); + } +} \ No newline at end of file diff --git a/PLRPC.GUI/PLRPC.GUI.csproj b/PLRPC.GUI/PLRPC.GUI.csproj new file mode 100644 index 0000000..0e5bd1e --- /dev/null +++ b/PLRPC.GUI/PLRPC.GUI.csproj @@ -0,0 +1,27 @@ + + + + true + net7.0 + win-x64;linux-x64 + + + + false + + + + LBPUnion.PLRPC.GUI + enable + disable + true + + + + + + + + + + diff --git a/PLRPC.GUI/Strings/MainForm.cs b/PLRPC.GUI/Strings/MainForm.cs new file mode 100644 index 0000000..6bfc4fa --- /dev/null +++ b/PLRPC.GUI/Strings/MainForm.cs @@ -0,0 +1,18 @@ +namespace LBPUnion.PLRPC.GUI.Strings; + +public static class MainForm +{ + public const string Configuration = "Configuration"; + public const string Username = "Username"; + public const string ServerUrl = "Server URL"; + public const string ApplicationId = "Application ID"; + public const string Connect = "Connect"; + public const string Connected = "Connected"; + public const string UnlockDefaults = "Unlock Defaults"; + public const string UnlockedDefaults = "Unlocked Defaults"; + public const string BlankFieldsError = "Please fill in all fields and try again."; + public const string InitializationError = "An error occurred while initializing the PLRPC client."; + public const string InvalidUrlError = "The URL specified is in an invalid format. Please try again."; + public const string InvalidUsernameError = "The username specified is invalid. Please try again."; + public const string UnlockedDefaultsWarning = "You have just unlocked defaults. Support will not be provided whilst using modified defaults. Continue at your own risk."; +} \ No newline at end of file diff --git a/PLRPC.sln b/PLRPC.sln index 78ec5cd..953028e 100644 --- a/PLRPC.sln +++ b/PLRPC.sln @@ -2,6 +2,16 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PLRPC", "PLRPC\PLRPC.csproj", "{60270A0A-A34A-40FB-BF97-F070B1325157}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PLRPC.GUI.Windows", "PLRPC.GUI.Windows\PLRPC.GUI.Windows.csproj", "{7B4DD2AF-4912-40C2-B6E4-C8FAE3695680}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PLRPC.GUI", "PLRPC.GUI\PLRPC.GUI.csproj", "{20C3CEC9-6328-43A5-AB59-FDCF9D637C40}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PLRPC.GUI.Linux", "PLRPC.GUI.Linux\PLRPC.GUI.Linux.csproj", "{5BC9F0D6-8F49-4EC4-BEA0-1C78DF3E0864}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GUI", "GUI", "{112C0D49-EA43-4443-A1D5-9BCBBEC7A9DB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{074150AA-731C-465B-8599-C41B56657C81}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +22,23 @@ Global {60270A0A-A34A-40FB-BF97-F070B1325157}.Debug|Any CPU.Build.0 = Debug|Any CPU {60270A0A-A34A-40FB-BF97-F070B1325157}.Release|Any CPU.ActiveCfg = Release|Any CPU {60270A0A-A34A-40FB-BF97-F070B1325157}.Release|Any CPU.Build.0 = Release|Any CPU + {7B4DD2AF-4912-40C2-B6E4-C8FAE3695680}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7B4DD2AF-4912-40C2-B6E4-C8FAE3695680}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B4DD2AF-4912-40C2-B6E4-C8FAE3695680}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7B4DD2AF-4912-40C2-B6E4-C8FAE3695680}.Release|Any CPU.Build.0 = Release|Any CPU + {20C3CEC9-6328-43A5-AB59-FDCF9D637C40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20C3CEC9-6328-43A5-AB59-FDCF9D637C40}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20C3CEC9-6328-43A5-AB59-FDCF9D637C40}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20C3CEC9-6328-43A5-AB59-FDCF9D637C40}.Release|Any CPU.Build.0 = Release|Any CPU + {5BC9F0D6-8F49-4EC4-BEA0-1C78DF3E0864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BC9F0D6-8F49-4EC4-BEA0-1C78DF3E0864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BC9F0D6-8F49-4EC4-BEA0-1C78DF3E0864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BC9F0D6-8F49-4EC4-BEA0-1C78DF3E0864}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {20C3CEC9-6328-43A5-AB59-FDCF9D637C40} = {112C0D49-EA43-4443-A1D5-9BCBBEC7A9DB} + {5BC9F0D6-8F49-4EC4-BEA0-1C78DF3E0864} = {112C0D49-EA43-4443-A1D5-9BCBBEC7A9DB} + {7B4DD2AF-4912-40C2-B6E4-C8FAE3695680} = {112C0D49-EA43-4443-A1D5-9BCBBEC7A9DB} + {60270A0A-A34A-40FB-BF97-F070B1325157} = {074150AA-731C-465B-8599-C41B56657C81} EndGlobalSection EndGlobal diff --git a/PLRPC.sln.DotSettings b/PLRPC.sln.DotSettings index af44414..310008c 100644 --- a/PLRPC.sln.DotSettings +++ b/PLRPC.sln.DotSettings @@ -51,7 +51,7 @@ NEVER False NEVER - ALWAYS + IF_OWNER_IS_SINGLE_LINE False True True @@ -64,7 +64,7 @@ NEXT_LINE True False - CHOP_IF_LONG + WRAP_IF_LONG CHOP_IF_LONG True True @@ -75,6 +75,7 @@ CHOP_IF_LONG CHOP_IF_LONG CHOP_IF_LONG + 140 CHOP_ALWAYS CHOP_IF_LONG CHOP_IF_LONG diff --git a/PLRPC/Types/ApiRepositoryImpl.cs b/PLRPC/ApiRepositoryImpl.cs similarity index 69% rename from PLRPC/Types/ApiRepositoryImpl.cs rename to PLRPC/ApiRepositoryImpl.cs index 159e621..2ed1595 100644 --- a/PLRPC/Types/ApiRepositoryImpl.cs +++ b/PLRPC/ApiRepositoryImpl.cs @@ -1,21 +1,23 @@ using System.Text.Json; using LBPUnion.PLRPC.Types.Entities; +using LBPUnion.PLRPC.Types.Enums; +using LBPUnion.PLRPC.Types.Interfaces; -namespace LBPUnion.PLRPC.Types; +namespace LBPUnion.PLRPC; public class ApiRepositoryImpl : IApiRepository { - private readonly int cacheExpirationTimeMs; + private readonly TimeSpan cacheExpirationTime; private readonly HttpClient httpClient; private readonly Dictionary slotCache = new(); private readonly Dictionary userCache = new(); private readonly Dictionary userStatusCache = new(); - public ApiRepositoryImpl(HttpClient httpClient, int cacheExpirationTimeMs) + public ApiRepositoryImpl(HttpClient httpClient, TimeSpan cacheExpirationTime) { this.httpClient = httpClient; - this.cacheExpirationTimeMs = cacheExpirationTimeMs; + this.cacheExpirationTime = cacheExpirationTime; } private static long TimestampMillis => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); @@ -24,9 +26,10 @@ public ApiRepositoryImpl(HttpClient httpClient, int cacheExpirationTimeMs) { if (this.GetFromCache(this.userCache, username, out User? cachedUser)) return cachedUser; - string userJson = await this.httpClient.GetStringAsync($"username/{username}"); + HttpResponseMessage userReq = await this.httpClient.GetAsync($"username/{username}"); + if (!userReq.IsSuccessStatusCode) return null; - User? user = JsonSerializer.Deserialize(userJson); + User? user = JsonSerializer.Deserialize(await userReq.Content.ReadAsStringAsync()); if (user == null) return null; this.userCache.TryAdd(username, (user, TimestampMillis)); @@ -37,9 +40,10 @@ public ApiRepositoryImpl(HttpClient httpClient, int cacheExpirationTimeMs) { if (this.GetFromCache(this.slotCache, slotId, out Slot? cachedSlot)) return cachedSlot; - string slotJson = await this.httpClient.GetStringAsync($"slot/{slotId}"); + HttpResponseMessage slotReq = await this.httpClient.GetAsync($"slot/{slotId}"); + if (!slotReq.IsSuccessStatusCode) return null; - Slot? slot = JsonSerializer.Deserialize(slotJson); + Slot? slot = JsonSerializer.Deserialize(await slotReq.Content.ReadAsStringAsync()); if (slot == null) return null; this.slotCache.TryAdd(slotId, (slot, TimestampMillis)); @@ -50,9 +54,10 @@ public ApiRepositoryImpl(HttpClient httpClient, int cacheExpirationTimeMs) { if (this.GetFromCache(this.userStatusCache, userId, out UserStatus? cachedUserStatus)) return cachedUserStatus; - string userStatusJson = await this.httpClient.GetStringAsync($"user/{userId}/status"); + HttpResponseMessage userStatusReq = await this.httpClient.GetAsync($"user/{userId}/status"); + if (!userStatusReq.IsSuccessStatusCode) return null; - UserStatus? userStatus = JsonSerializer.Deserialize(userStatusJson); + UserStatus? userStatus = JsonSerializer.Deserialize(await userStatusReq.Content.ReadAsStringAsync()); if (userStatus == null) return null; /* @@ -86,7 +91,7 @@ private bool GetFromCache(IReadOnlyDictionary cache, T1 val = default; if (!cache.TryGetValue(key, out (T2, long) entry)) return false; - if (entry.Item2 + this.cacheExpirationTimeMs > TimestampMillis) return false; + if (entry.Item2 + this.cacheExpirationTime.Milliseconds > TimestampMillis) return false; val = entry.Item1; return true; diff --git a/PLRPC/Configuration.cs b/PLRPC/Configuration.cs new file mode 100644 index 0000000..e50cdc8 --- /dev/null +++ b/PLRPC/Configuration.cs @@ -0,0 +1,48 @@ +using LBPUnion.PLRPC.Types.Configuration; +using LBPUnion.PLRPC.Types.Logging; +using System.Text.Json; +using Serilog; + +namespace LBPUnion.PLRPC; + +public static class Configuration +{ + private static readonly JsonSerializerOptions lenientJsonOptions = new() + { + AllowTrailingCommas = true, + WriteIndented = true, + ReadCommentHandling = JsonCommentHandling.Skip, + }; + + public static async Task LoadFromConfiguration() + { + if (!File.Exists("./config.json")) + { + Log.Warning("{@Area}: No configuration file exists, creating a base configuration", LogArea.Configuration); + Log.Warning("{@Area}: Please populate the configuration file and restart the program", LogArea.Configuration); + + PlrpcConfiguration defaultConfig = new(); + + await File.WriteAllTextAsync("./config.json", JsonSerializer.Serialize(defaultConfig, lenientJsonOptions)); + + return null; + } + + string configurationJson = await File.ReadAllTextAsync("./config.json"); + + try + { + PlrpcConfiguration? configuration = JsonSerializer.Deserialize(configurationJson, lenientJsonOptions); + + if (configuration is { ServerUrl: not null, Username: not null, ApplicationId: not null }) + return configuration; + + throw new JsonException("Deserialized configuration contains one or more null values"); + } + catch (Exception exception) + { + Log.Fatal(exception, "{@Area}: Failed to deserialize configuration file", LogArea.Configuration); + return null; + } + } +} \ No newline at end of file diff --git a/PLRPC/Extensions/GameVersionExtensions.cs b/PLRPC/Extensions/GameVersionExtensions.cs index 3e67ebc..df40dd6 100644 --- a/PLRPC/Extensions/GameVersionExtensions.cs +++ b/PLRPC/Extensions/GameVersionExtensions.cs @@ -1,4 +1,4 @@ -using LBPUnion.PLRPC.Types; +using LBPUnion.PLRPC.Types.Enums; namespace LBPUnion.PLRPC.Extensions; diff --git a/PLRPC/Extensions/PermissionLevelExtensions.cs b/PLRPC/Extensions/PermissionLevelExtensions.cs index 9e08014..e8c7947 100644 --- a/PLRPC/Extensions/PermissionLevelExtensions.cs +++ b/PLRPC/Extensions/PermissionLevelExtensions.cs @@ -1,4 +1,4 @@ -using LBPUnion.PLRPC.Types; +using LBPUnion.PLRPC.Types.Enums; namespace LBPUnion.PLRPC.Extensions; diff --git a/PLRPC/Helpers/ValidationHelper.cs b/PLRPC/Helpers/ValidationHelper.cs index 4f048f1..3b9f2d4 100644 --- a/PLRPC/Helpers/ValidationHelper.cs +++ b/PLRPC/Helpers/ValidationHelper.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using LBPUnion.PLRPC.Types.Logging; using Serilog; namespace LBPUnion.PLRPC.Helpers; @@ -9,7 +10,9 @@ public static bool IsValidUrl(string url) { if (Uri.TryCreate(url, UriKind.Absolute, out _)) return true; - Log.Error("The URL specified is in an invalid format. Please try again"); + Log.Error("{@Area}: The URL specified is in an invalid format. Please try again", + LogArea.Validation); + return false; } @@ -17,10 +20,13 @@ public static bool IsValidUsername(string username) { if (UsernameRegex().IsMatch(username)) return true; - Log.Error("The username specified is invalid. Please try again"); + Log.Error("{@Area}: The username specified is invalid. Please try again", + LogArea.Validation); + return false; } + // Getting an error here? Roslyn issue - don't worry about it :) [GeneratedRegex("^[a-zA-Z0-9_.-]{3,16}$")] private static partial Regex UsernameRegex(); } \ No newline at end of file diff --git a/PLRPC/LighthouseClient.cs b/PLRPC/LighthouseClient.cs index 168b664..93fdc42 100644 --- a/PLRPC/LighthouseClient.cs +++ b/PLRPC/LighthouseClient.cs @@ -1,8 +1,9 @@ using DiscordRPC; -using DiscordRPC.Logging; using LBPUnion.PLRPC.Extensions; -using LBPUnion.PLRPC.Types; using LBPUnion.PLRPC.Types.Entities; +using LBPUnion.PLRPC.Types.Enums; +using LBPUnion.PLRPC.Types.Interfaces; +using LBPUnion.PLRPC.Types.Logging; using Serilog; using User = LBPUnion.PLRPC.Types.Entities.User; @@ -11,32 +12,37 @@ namespace LBPUnion.PLRPC; public class LighthouseClient { private readonly IApiRepository apiRepository; - private readonly DiscordRpcClient discordClient; + private readonly DiscordRpcClient discordRpcClient; private readonly SemaphoreSlim readySemaphore = new(0, 1); private readonly string serverUrl; private readonly string username; - public LighthouseClient(string username, string serverUrl, IApiRepository apiRepository, DiscordRpcClient rpcClient) + public LighthouseClient(string username, string serverUrl, IApiRepository apiRepository, DiscordRpcClient discordRpcClient) { this.username = username; this.serverUrl = serverUrl; this.apiRepository = apiRepository; - this.discordClient = rpcClient; - this.discordClient.Initialize(); - this.discordClient.Logger = new ConsoleLogger - { - Level = LogLevel.Warning, - }; + this.discordRpcClient = discordRpcClient; + this.discordRpcClient.Initialize(); - this.discordClient.OnReady += (_, e) => - { - Log.Information("Connected to Discord Account {Username}", e.User.Username); - this.readySemaphore.Release(); - }; + this.discordRpcClient.OnReady += (_, _) => this.readySemaphore.Release(); + + this.discordRpcClient.OnReady += (_, _) => + Log.Information("{@Area}: Successfully established ready connection", + LogArea.LighthouseClient); - this.discordClient.OnPresenceUpdate += (_, e) => - Log.Information("{@Presence}: Presence updated", e.Presence.GetType()); + this.discordRpcClient.OnConnectionEstablished += (_, e) => + Log.Information("{@Area}: Successfully acquired the lock on RPC ({Pipe})", + LogArea.LighthouseClient, e.ConnectedPipe); + + this.discordRpcClient.OnConnectionFailed += (_, e) => + Log.Warning("{@Area}: Failed to acquire the lock on RPC ({Pipe})", + LogArea.LighthouseClient, e.FailedPipe); + + this.discordRpcClient.OnPresenceUpdate += (_, e) => + Log.Information("{@Area}: Updated client presence ({Party})", + LogArea.RichPresence, e.Presence.Party.ID); } private async Task UpdatePresence() @@ -44,14 +50,16 @@ private async Task UpdatePresence() User? user = await this.apiRepository.GetUser(this.username); if (user == null || user.PermissionLevel == PermissionLevel.Banned) { - Log.Warning("Failed to get user from the server"); + Log.Warning("{@Area}: Failed to get user from the server", + LogArea.ApiRepositoryImpl); return; } UserStatus? status = await this.apiRepository.GetStatus(user.UserId); if (status?.CurrentRoom?.Slot?.SlotId == null || status.CurrentRoom.PlayerIds == null) { - Log.Warning("Failed to get user status from the server"); + Log.Warning("{@Area}: Failed to get user status from the server", + LogArea.ApiRepositoryImpl); return; } @@ -63,7 +71,8 @@ private async Task UpdatePresence() slot = await this.apiRepository.GetSlot(status.CurrentRoom.Slot.SlotId); if (slot == null) { - Log.Warning("Failed to get user's current level from the server"); + Log.Warning("{@Area}: Failed to get user's current level from the server", + LogArea.ApiRepositoryImpl); return; } } @@ -73,6 +82,7 @@ private async Task UpdatePresence() { SlotType.Pod => "9c412649a07a8cb678a2a25214ed981001dd08ca", SlotType.Moon => "a891bbcf9ad3518b80c210813cce8ed292ed4c62", + SlotType.RemoteMoon => "a891bbcf9ad3518b80c210813cce8ed292ed4c62", SlotType.Developer => "7d3df5ce61ca90a80f600452cd3445b7a775d47e", SlotType.DeveloperAdventure => "7d3df5ce61ca90a80f600452cd3445b7a775d47e", SlotType.DlcLevel => "2976e45d66b183f6d3242eaf01236d231766295f", @@ -95,6 +105,7 @@ private async Task UpdatePresence() SlotType.User => $"{slot.Name}", SlotType.Pod => "Dwelling in the Pod", SlotType.Moon => "Creating on the Moon", + SlotType.RemoteMoon => "Creating on a Remote Moon", SlotType.Developer => "Playing a Story Level", SlotType.DeveloperAdventure => "Playing an Adventure Level", SlotType.DlcLevel => "Playing a DLC Level", @@ -139,24 +150,28 @@ private async Task UpdatePresence() }, }, }; - this.discordClient.SetPresence(newPresence); - Log.Information("{@Presence}: Sending presence update", newPresence.GetType()); + + Log.Information("{@Area}: Updating client presence ({Party})", + LogArea.RichPresence, newPresence.Party.ID); + + this.discordRpcClient.SetPresence(newPresence); } public async Task StartUpdateLoop() { await this.readySemaphore.WaitAsync(); this.readySemaphore.Dispose(); + while (true) { try { await this.UpdatePresence(); } - catch (Exception exception) + catch (Exception) { - this.discordClient.Dispose(); - Log.Fatal(exception, "Failed to update presence"); + this.discordRpcClient.ClearPresence(); + this.discordRpcClient.Dispose(); return; } await Task.Delay(30000); diff --git a/PLRPC/PLRPC.csproj b/PLRPC/PLRPC.csproj index bb58968..cb69f22 100644 --- a/PLRPC/PLRPC.csproj +++ b/PLRPC/PLRPC.csproj @@ -10,14 +10,23 @@ 2.0.0 MIT License Copyright (c) 2023 LBP Union LBP Union - Exe - net7.0 enable enable LBPUnion.PLRPC LBPUnion.PLRPC + + Exe + true + net7.0 + win-x64;linux-x64 + + + + false + + diff --git a/PLRPC/Program.cs b/PLRPC/Program.cs index 2b6ddf5..3224c5f 100644 --- a/PLRPC/Program.cs +++ b/PLRPC/Program.cs @@ -1,131 +1,101 @@ using System.Diagnostics.CodeAnalysis; -using System.Text.Json; using CommandLine; using DiscordRPC; using JetBrains.Annotations; using LBPUnion.PLRPC.Helpers; -using LBPUnion.PLRPC.Types; +using LBPUnion.PLRPC.Types.Configuration; +using LBPUnion.PLRPC.Types.Logging; using LBPUnion.PLRPC.Types.Updater; using Serilog; +using Serilog.Core; namespace LBPUnion.PLRPC; public static class Program { - private static readonly JsonSerializerOptions lenientJsonOptions = new() - { - AllowTrailingCommas = true, - WriteIndented = true, - ReadCommentHandling = JsonCommentHandling.Skip, - }; + public static readonly Logger Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .Enrich.With() + .WriteTo.Console(outputTemplate: "[{ProcessId} {Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}") + .CreateLogger(); public static async Task Main(string[] args) { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Information() - .Enrich.With() - .WriteTo.Console(outputTemplate: "[{ProcessId} {Timestamp:HH:mm:ss} {Level:u3}] {Message:l}{NewLine}{Exception}") - .CreateLogger(); + Log.Logger = Logger; #if !DEBUG await InitializeUpdateCheck(); #endif - await Parser.Default.ParseArguments(args).WithParsedAsync(ParseArguments); + await Parser.Default + .ParseArguments(args) + .WithParsedAsync(ProcessArguments); } - private static async Task ParseArguments(CommandLineArguments arguments) + // TODO: Make command line argument parsing more uniform and clean + private static async Task ProcessArguments(CommandLineArguments arguments) { switch (arguments) { case { UseConfig: true }: { - PlrpcConfiguration? configuration = LoadFromConfiguration().Result; + PlrpcConfiguration? configuration = Configuration.LoadFromConfiguration().Result; if (configuration is { ServerUrl: not null, Username: not null, ApplicationId: not null }) - await InitializeLighthouseClient(configuration.ServerUrl.TrimEnd('/'), - configuration.Username, - configuration.ApplicationId); + await InitializeLighthouseClient(configuration.ServerUrl, configuration.Username, configuration.ApplicationId); break; } case { ServerUrl: not null, Username: not null } when !ValidationHelper.IsValidUrl(arguments.ServerUrl): case { ServerUrl: not null, Username: not null } when !ValidationHelper.IsValidUsername(arguments.Username): return; case { ServerUrl: not null, Username: not null, ApplicationId: not null }: - await InitializeLighthouseClient(arguments.ServerUrl.TrimEnd('/'), - arguments.Username, - arguments.ApplicationId); + await InitializeLighthouseClient(arguments.ServerUrl, arguments.Username, arguments.ApplicationId); break; default: // ReSharper disable once TemplateIsNotCompileTimeConstantProblem Log.Error(arguments is { ServerUrl: null, Username: null, UseConfig: false } - ? "No arguments were passed to the client. Ensure you're running PLRPC through CLI" - : "Invalid argument(s) were passed to the client, please check them and try running again"); + ? "{@Area}: No arguments were passed to the client. Ensure you're running PLRPC through CLI" + : "{@Area}: Invalid argument(s) were passed to the client, please check them and try running again", + LogArea.Configuration); Console.ReadLine(); break; } } - private static async Task LoadFromConfiguration() - { - if (!File.Exists("./config.json")) - { - Log.Warning("No configuration file exists, creating a base configuration"); - Log.Warning("Please populate the configuration file and restart the program"); - - PlrpcConfiguration defaultConfig = new(); - - await File.WriteAllTextAsync("./config.json", JsonSerializer.Serialize(defaultConfig, lenientJsonOptions)); - - return null; - } - - string configurationJson = await File.ReadAllTextAsync("./config.json"); - - try - { - PlrpcConfiguration? configuration = - JsonSerializer.Deserialize(configurationJson, lenientJsonOptions); - - if (configuration is { ServerUrl: not null, Username: not null, ApplicationId: not null }) - return configuration; - - throw new JsonException("Deserialized configuration contains one or more null values"); - } - catch (Exception exception) - { - Log.Fatal(exception, "Failed to deserialize configuration file"); - return null; - } - } - [SuppressMessage("ReSharper", "UnusedMember.Local")] private static async Task InitializeUpdateCheck() { HttpClient updateClient = new(); + Updater updater = new(updateClient); + // Required by GitHub's API updateClient.DefaultRequestHeaders.UserAgent.ParseAdd("LBPUnion/1.0 (PLRPC; github-release) UpdateClient/1.1"); - Updater updater = new(updateClient); Release? updateResult = await updater.CheckForUpdate(); if (updateResult != null) { - Log.Information("***************************************"); - Log.Information("A new version of PLRPC is available!"); - Log.Information("{UpdateTag}: {UpdateUrl}", updateResult.TagName, updateResult.Url); - Log.Information("***************************************"); + Log.Information("{@Area}: A new version of PLRPC is available!", + LogArea.Updater); + Log.Information("{@Area}: {UpdateTag}: {UpdateUrl}", + LogArea.Updater, updateResult.TagName, updateResult.Url); } else { - Log.Information("There are no new updates available"); + Log.Information("{@Area}: There are no new updates available", + LogArea.Updater); } } - private static async Task InitializeLighthouseClient(string serverUrl, string username, string? applicationId) + public static async Task InitializeLighthouseClient(string serverUrl, string username, string? applicationId) { + Log.Information("{@Area}: Initializing new client and dependencies", + LogArea.LighthouseClient); + + string trimmedServerUrl = serverUrl.TrimEnd('/'); // trailing slashes cause issues with requests + HttpClient apiClient = new() { - BaseAddress = new Uri(serverUrl + "/api/v1/"), + BaseAddress = new Uri(trimmedServerUrl + "/api/v1/"), DefaultRequestHeaders = { { @@ -134,13 +104,11 @@ private static async Task InitializeLighthouseClient(string serverUrl, string us }, }; - const int cacheExpirationTime = 60 * 60 * 1000; // 1 hour + TimeSpan cacheExpirationTime = TimeSpan.FromHours(1); ApiRepositoryImpl apiRepository = new(apiClient, cacheExpirationTime); DiscordRpcClient discordRpcClient = new(applicationId); - LighthouseClient lighthouseClient = new(username, serverUrl, apiRepository, discordRpcClient); - - Log.Information("Initializing client..."); + LighthouseClient lighthouseClient = new(username, trimmedServerUrl, apiRepository, discordRpcClient); await lighthouseClient.StartUpdateLoop(); } diff --git a/PLRPC/Types/PlrpcConfiguration.cs b/PLRPC/Types/Configuration/PlrpcConfiguration.cs similarity index 89% rename from PLRPC/Types/PlrpcConfiguration.cs rename to PLRPC/Types/Configuration/PlrpcConfiguration.cs index 671af04..cde762f 100644 --- a/PLRPC/Types/PlrpcConfiguration.cs +++ b/PLRPC/Types/Configuration/PlrpcConfiguration.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace LBPUnion.PLRPC.Types; +namespace LBPUnion.PLRPC.Types.Configuration; public class PlrpcConfiguration { diff --git a/PLRPC/Types/Entities/Slot.cs b/PLRPC/Types/Entities/Slot.cs index d74c3b0..b40f48a 100644 --- a/PLRPC/Types/Entities/Slot.cs +++ b/PLRPC/Types/Entities/Slot.cs @@ -29,7 +29,7 @@ public enum SlotType // DeveloperGroup = 4, Pod = 5, // Fake = 6, - // RemoteMoon = 7, + RemoteMoon = 7, DlcLevel = 8, // DLCPack = 9, // Playlist = 10, diff --git a/PLRPC/Types/Entities/User.cs b/PLRPC/Types/Entities/User.cs index b4388ea..94e871f 100644 --- a/PLRPC/Types/Entities/User.cs +++ b/PLRPC/Types/Entities/User.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using LBPUnion.PLRPC.Types.Enums; using Newtonsoft.Json; namespace LBPUnion.PLRPC.Types.Entities; diff --git a/PLRPC/Types/Entities/UserStatus.cs b/PLRPC/Types/Entities/UserStatus.cs index 15f3253..d13f049 100644 --- a/PLRPC/Types/Entities/UserStatus.cs +++ b/PLRPC/Types/Entities/UserStatus.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using LBPUnion.PLRPC.Types.Enums; using Newtonsoft.Json; namespace LBPUnion.PLRPC.Types.Entities; diff --git a/PLRPC/Types/GameVersion.cs b/PLRPC/Types/Enums/GameVersion.cs similarity index 78% rename from PLRPC/Types/GameVersion.cs rename to PLRPC/Types/Enums/GameVersion.cs index 8729982..a151d1d 100644 --- a/PLRPC/Types/GameVersion.cs +++ b/PLRPC/Types/Enums/GameVersion.cs @@ -1,4 +1,4 @@ -namespace LBPUnion.PLRPC.Types; +namespace LBPUnion.PLRPC.Types.Enums; public enum GameVersion { diff --git a/PLRPC/Types/PermissionLevel.cs b/PLRPC/Types/Enums/PermissionLevel.cs similarity index 78% rename from PLRPC/Types/PermissionLevel.cs rename to PLRPC/Types/Enums/PermissionLevel.cs index 972de89..76cb814 100644 --- a/PLRPC/Types/PermissionLevel.cs +++ b/PLRPC/Types/Enums/PermissionLevel.cs @@ -1,4 +1,4 @@ -namespace LBPUnion.PLRPC.Types; +namespace LBPUnion.PLRPC.Types.Enums; public enum PermissionLevel { diff --git a/PLRPC/Types/IApiRepository.cs b/PLRPC/Types/Interfaces/IApiRepository.cs similarity index 83% rename from PLRPC/Types/IApiRepository.cs rename to PLRPC/Types/Interfaces/IApiRepository.cs index 292f899..8f504ea 100644 --- a/PLRPC/Types/IApiRepository.cs +++ b/PLRPC/Types/Interfaces/IApiRepository.cs @@ -1,6 +1,6 @@ using LBPUnion.PLRPC.Types.Entities; -namespace LBPUnion.PLRPC.Types; +namespace LBPUnion.PLRPC.Types.Interfaces; public interface IApiRepository { diff --git a/PLRPC/Types/LogEnrichers.cs b/PLRPC/Types/LogEnrichers.cs deleted file mode 100644 index b1a2d20..0000000 --- a/PLRPC/Types/LogEnrichers.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Serilog.Core; -using Serilog.Events; - -namespace LBPUnion.PLRPC.Types; - -public class LogEnrichers : ILogEventEnricher -{ - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - logEvent.AddPropertyIfAbsent( - propertyFactory.CreateProperty( - "ProcessId", - Environment.ProcessId)); - } -} \ No newline at end of file diff --git a/PLRPC/Types/Logging/LogArea.cs b/PLRPC/Types/Logging/LogArea.cs new file mode 100644 index 0000000..e1eff24 --- /dev/null +++ b/PLRPC/Types/Logging/LogArea.cs @@ -0,0 +1,11 @@ +namespace LBPUnion.PLRPC.Types.Logging; + +public enum LogArea +{ + ApiRepositoryImpl, + Configuration, + LighthouseClient, + RichPresence, + Updater, + Validation, +} \ No newline at end of file diff --git a/PLRPC/Types/Logging/LogEnrichers.cs b/PLRPC/Types/Logging/LogEnrichers.cs new file mode 100644 index 0000000..4e6c46b --- /dev/null +++ b/PLRPC/Types/Logging/LogEnrichers.cs @@ -0,0 +1,12 @@ +using Serilog.Core; +using Serilog.Events; + +namespace LBPUnion.PLRPC.Types.Logging; + +public class LogEnrichers : ILogEventEnricher +{ + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) + { + logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("ProcessId", Environment.ProcessId)); + } +} \ No newline at end of file diff --git a/PLRPC/Updater.cs b/PLRPC/Updater.cs index b076427..8f168c8 100644 --- a/PLRPC/Updater.cs +++ b/PLRPC/Updater.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using LBPUnion.PLRPC.Types.Logging; using LBPUnion.PLRPC.Types.Updater; using Serilog; @@ -17,13 +18,15 @@ public Updater(HttpClient updaterClient) { if (!File.Exists("./manifest.json")) { - Log.Warning("No update manifest file exists, creating a base manifest"); + Log.Warning("{@Area} No update manifest file exists, creating a base manifest", + LogArea.Updater); await this.GenerateManifest(); } string releaseManifest = await this.updaterHttpClient.GetStringAsync("https://api.github.com/repos/LBPUnion/PLRPC/releases/latest"); - string programManifest = await File.ReadAllTextAsync("./manifest.json"); + string programManifest = + await File.ReadAllTextAsync("./manifest.json"); Release? releaseObject = JsonSerializer.Deserialize(releaseManifest); Manifest? programObject = JsonSerializer.Deserialize(programManifest); diff --git a/README.md b/README.md index 13a8c41..6e8d2d1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # PLRPC [![Build Artifacts](https://github.com/LBPUnion/PLRPC/actions/workflows/build.yml/badge.svg)](https://github.com/LBPUnion/PLRPC/actions/workflows/build.yml) -[![CodeQL Analysis](https://github.com/LBPUnion/PLRPC/actions/workflows/codeql.yml/badge.svg)](https://github.com/LBPUnion/PLRPC/actions/workflows/codeql.yml) PLRPC (short for ProjectLighthouse Rich Presence Client) is a continuation of the LighthouseRichPresence client under the same premise. @@ -18,29 +17,37 @@ the same premise. - [ ] Stability (no fires) ## Installation Instructions + +**GUI Installation Steps (recommended)** -**Installation Steps:** +1. Navigate to the Releases Tab +2. Download the latest GUI build for Windows or Linux +3. Extract the build to any folder +4. Run the client + * **Windows:** Run the GUI by double clicking on the `PLRPC.GUI.Windows` executable + * **Linux:** Run the GUI by double clicking on the `PLRPC.GUI.Linux` executable + * You may need to mark the program as executable first, or run it from the command line + +**CLI Installation Steps (advanced)** + +> **Warning** +> These steps are only for advanced users who are comfortable with the command line. +> If you are not comfortable with the command line, please use the GUI instead. 1. Navigate to the Releases Tab -2. Download the latest build (or major version if you like somewhat-stability) +2. Download the latest CLI build for Windows or Linux 3. Extract the build to any folder 4. Run the client - **Configuration Mode:** `./path/to/PLRPC --config` (use `--config` each time) - **Manual Mode:** `./path/to/PLRPC --server https://lighthouse.instance.url --username instanceusername` -> **Warning** for **Windows Users**: -> -> Currently, you are unable to run the .exe file directly. You **must** open a Command Prompt or PowerShell -> window, navigate to the file path, and execute the binary manually. Refer -to [Installation Step #4](https://github.com/LBPUnion/PLRPC/blob/master/README.md#installation-instructions) -> for instructions on how to further configure and run the client. - **Post Install:** Please create an Issue if you encounter any bugs or weird errors. ## Helpful Information -* You can use the `--applicationid` command line argument, or change the `applicationId` entry in your configuration, - to override the default Discord Application ID. This can be useful if your Lighthouse instance or other service is - compatible with the PLRPC protocol and you want to display your own application name. \ No newline at end of file +* You can use the `--applicationid` command line argument, change the `applicationId` entry in your configuration, + or if using the GUI, unlock defaults and change the `Application ID` entry in the options to override the default + Discord Application ID. This can be useful if your Lighthouse instance or other service is compatible with the PLRPC + protocol and you want to display your own application name. \ No newline at end of file diff --git a/global.json b/global.json index 934805f..7cd6a1f 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { - "sdk": { - "version": "7.0.0", - "rollForward": "latestMajor", - "allowPrerelease": true - } + "sdk": { + "version": "7.0.0", + "rollForward": "latestMajor", + "allowPrerelease": true + } } \ No newline at end of file