Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Krapht committed May 11, 2024
1 parent e0e3af4 commit 12be645
Show file tree
Hide file tree
Showing 50 changed files with 3,811 additions and 3 deletions.
63 changes: 63 additions & 0 deletions InsightLogParser.Client/Beeper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

namespace InsightLogParser.Client;

internal class Beeper
{
private readonly Configuration _configuration;

public static int MinFrequency => 37;
public static int MaxFrequency => 32767;
public static int MinDuration => 50;
public static int MaxDuration => 1000;
public static int MinBeeps => 1;
public static int MaxBeeps => 10;
public static int MinDelay => 50;
public static int MaxDelay => 1000;

public Beeper(Configuration configuration)
{
_configuration = configuration;
}

public void BeepForNewParse()
{
if (!_configuration.BeepOnNewParse) return;
DoTheBeep(_configuration.NewParseBeepFrequency, _configuration.NewParseBeepDuration);
}

public void BeepForKnownParse()
{
if (!_configuration.BeepOnKnownParse) return;
DoTheBeep(_configuration.KnownParseBeepFrequency, _configuration.KnownParseBeepDuration);
}

public void BeepForOpeningSolvedPuzzle()
{
if (!_configuration.BeepOnOpeningSolvedPuzzle) return;
DoTheBeep(_configuration.OpenSolvedPuzzleBeepFrequency, _configuration.OpenSolvedPuzzleBeepDuration);
}

public async Task BeepForAttentionAsync()
{
if (!_configuration.BeepForAttention) return;
var count = _configuration.BeepForAttentionCount;
if( count < MinBeeps) return; //No beeps
if (count > MaxBeeps) return; //Too many beeps
for (int i = 0; i < count; i++)
{
DoTheBeep(_configuration.BeepForAttentionFrequency, _configuration.BeepForAttentionDuration);
if (i + 1 < count)
{
await Task.Delay(TimeSpan.FromMilliseconds(_configuration.BeepForAttentionInterval)).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
}
}
}

private void DoTheBeep(int frequency, int duration)
{
//Ignore invalid frequencies
if (frequency < MinFrequency) return;
if (frequency > MaxFrequency) return;
Console.Beep(frequency, duration);
}
}
182 changes: 182 additions & 0 deletions InsightLogParser.Client/Cetus/CetusClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using InsightLogParser.Common.ApiModels;
using InsightLogParser.Common.World;

namespace InsightLogParser.Client.Cetus;

internal class AuthResult
{
[JsonPropertyName("accessToken")]
public string? AccessToken { get; set; }

[JsonPropertyName("expiresIn")]
public int ExpiresIn { get; set; }
}

internal class CetusClient : ICetusClient
{
private readonly MessageWriter _messageWriter;
private readonly HttpClient _httpClient;
private DateTimeOffset? _tokenValid;
private string? _basicAuth;

public bool IsDummy() => false;

public CetusClient(string baseUri, MessageWriter messageWriter)
{
_messageWriter = messageWriter;
const string httpsPrefix = "https://";

if (!baseUri.StartsWith(httpsPrefix)) baseUri = $"{httpsPrefix}{baseUri}";
_httpClient = new HttpClient()
{
BaseAddress = new Uri(baseUri)
};
}

public async Task<bool> RefreshAuthIfNeeded()
{
if (_basicAuth == null || _tokenValid == null) return false;
if (DateTimeOffset.UtcNow.AddMinutes(1) < _tokenValid.Value) return true;

return await RefreshAuthAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}

private async Task<bool> RefreshAuthAsync()
{
_messageWriter.WriteDebug("CETUS: Refreshing Auth");
try
{
var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Get, "api/v1/auth/token")
{
Headers = { Authorization = new AuthenticationHeaderValue("Basic", _basicAuth) }
}).ConfigureAwait(ConfigureAwaitOptions.None);
_messageWriter.WriteDebug($"CETUS: Auth returned {(int)result.StatusCode}-{result.StatusCode}");
if (!result.IsSuccessStatusCode) return false;
if (result.StatusCode == HttpStatusCode.NoContent) return false;
var authResult = await JsonSerializer.DeserializeAsync<AuthResult>(await result.Content.ReadAsStreamAsync().ConfigureAwait(ConfigureAwaitOptions.None)).ConfigureAwait(false);
if (authResult?.AccessToken == null)
{
_messageWriter.WriteDebug("CETUS: Failed to parse token");
return false;
}
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
_tokenValid = DateTimeOffset.UtcNow.AddSeconds(authResult.ExpiresIn);
}
catch (Exception e)
{
_messageWriter.WriteDebug($"CETUS: Exception: {e}");
return false;
}
return true;
}

public async Task<bool> AuthenticateAsync(Guid playerId, string apiKey)
{
_basicAuth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{playerId:N}:{apiKey}"));
return await RefreshAuthAsync().ConfigureAwait(ConfigureAwaitOptions.None);
}

public void ClearAuth()
{
_messageWriter.WriteDebug("CETUS: Auth was cleared");
_httpClient.DefaultRequestHeaders.Authorization = null;
_tokenValid = null;
_basicAuth = null;
}

public void Dispose()
{
_httpClient.Dispose();
}

public async Task<SolvedResponse?> PostSolvedAsync(PlayerReport request)
{
await RefreshAuthIfNeeded().ConfigureAwait(ConfigureAwaitOptions.None);
_messageWriter.WriteDebug("CETUS: Posting solved");
return await MakePostAsync<SolvedResponse, PlayerReport>("api/v1/puzzle/solved", request).ConfigureAwait(ConfigureAwaitOptions.None);
}

public async Task<SeenResponse?> PostSeenAsync(PlayerReport request)
{
await RefreshAuthIfNeeded().ConfigureAwait(ConfigureAwaitOptions.None);
_messageWriter.WriteDebug($"CETUS: Posting seen");
return await MakePostAsync<SeenResponse, PlayerReport>("api/v1/puzzle/seen", request).ConfigureAwait(ConfigureAwaitOptions.None);
}

private async Task<TResponse?> MakePostAsync<TResponse, TRequest>(string requestUri, TRequest request, bool isRetry = false)
{
try
{
var result = await _httpClient.PostAsJsonAsync(requestUri, request)
.ConfigureAwait(ConfigureAwaitOptions.None);
_messageWriter.WriteDebug($"CETUS: Got a {(int)result.StatusCode}-{result.StatusCode}");
if (!result.IsSuccessStatusCode)
{
if (!isRetry && result.StatusCode == HttpStatusCode.Unauthorized)
{
_messageWriter.WriteDebug("CETUS: Retrying once with fresh auth");
var authResult = await RefreshAuthAsync().ConfigureAwait(ConfigureAwaitOptions.None);
if (!authResult) return default;
return await MakePostAsync<TResponse, TRequest>(requestUri, request, true).ConfigureAwait(ConfigureAwaitOptions.None);
}
return default;
}
return await result.Content.ReadFromJsonAsync<TResponse>().ConfigureAwait(ConfigureAwaitOptions.None);
}
catch (Exception e)
{
_messageWriter.WriteDebug($"CETUS: Exception: {e}");
return default;
}
}


public async Task<ZoneStatisticsResponse?> GetZoneStatisticsAsync(PuzzleZone zone)
{
await RefreshAuthIfNeeded().ConfigureAwait(ConfigureAwaitOptions.None);
_messageWriter.WriteDebug($"CETUS: Requesting statistics for {zone}");
return await MakeGetAsync<ZoneStatisticsResponse>($"api/v1.0/Puzzle/zone/{(int)zone}")
.ConfigureAwait(ConfigureAwaitOptions.None);
}

public async Task<Sighting[]?> GetSightingsAsync(PuzzleZone zone, PuzzleType type)
{
await RefreshAuthIfNeeded().ConfigureAwait(ConfigureAwaitOptions.None);
_messageWriter.WriteDebug($"CETUS: Requesting sightings for {zone} {type}");
return await MakeGetAsync<Sighting[]>($"api/v1.0/Puzzle/sightings/{(int)zone}/{(int)type}")
.ConfigureAwait(ConfigureAwaitOptions.None);
}

private async Task<T?> MakeGetAsync<T>(string requestUri, bool isRetry = false)
{
try
{
var result = await _httpClient.GetAsync(requestUri).ConfigureAwait(ConfigureAwaitOptions.None);
_messageWriter.WriteDebug($"CETUS: Got a {(int)result.StatusCode}-{result.StatusCode}");
if (!result.IsSuccessStatusCode)
{
if (!isRetry && result.StatusCode == HttpStatusCode.Unauthorized)
{
_messageWriter.WriteDebug("CETUS: Retrying once with fresh auth");
var authResult = await RefreshAuthAsync().ConfigureAwait(ConfigureAwaitOptions.None);
if (!authResult) return default;
return await MakeGetAsync<T>(requestUri, true).ConfigureAwait(ConfigureAwaitOptions.None);
}
return default;
}
return await result.Content.ReadFromJsonAsync<T>().ConfigureAwait(ConfigureAwaitOptions.None);
}
catch (Exception e)
{
_messageWriter.WriteDebug($"CETUS: Exception: {e}");
return default;
}
}
}
39 changes: 39 additions & 0 deletions InsightLogParser.Client/Cetus/DummyCetusClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

using InsightLogParser.Common.ApiModels;
using InsightLogParser.Common.World;

namespace InsightLogParser.Client.Cetus
{
internal class DummyCetusClient : ICetusClient
{
public bool IsDummy() => true;
public void ClearAuth() { }

public Task<SolvedResponse?> PostSolvedAsync(PlayerReport request)
{
return Task.FromResult<SolvedResponse?>(null);
}

public Task<SeenResponse?> PostSeenAsync(PlayerReport request)
{
return Task.FromResult<SeenResponse?>(null);
}

public Task<bool> AuthenticateAsync(Guid playerId, string? configurationCetusApiKey)
{
return Task.FromResult(true);
}

public Task<ZoneStatisticsResponse?> GetZoneStatisticsAsync(PuzzleZone zone)
{
return Task.FromResult<ZoneStatisticsResponse?>(null);
}

public Task<Sighting[]?> GetSightingsAsync(PuzzleZone zone, PuzzleType type)
{
return Task.FromResult<Sighting[]?>(null);
}

public void Dispose() { } //Noop
}
}
20 changes: 20 additions & 0 deletions InsightLogParser.Client/Cetus/ICetusClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using InsightLogParser.Common.ApiModels;
using InsightLogParser.Common.World;

namespace InsightLogParser.Client.Cetus;

internal interface ICetusClient : IDisposable
{
//Misc operations
bool IsDummy();
void ClearAuth();
Task<bool> AuthenticateAsync(Guid playerId, string configurationCetusApiKey);

//Get operations
Task<ZoneStatisticsResponse?> GetZoneStatisticsAsync(PuzzleZone zone);
Task<Sighting[]?> GetSightingsAsync(PuzzleZone zone, PuzzleType type);

//Post operations
Task<SeenResponse?> PostSeenAsync(PlayerReport request);
Task<SolvedResponse?> PostSolvedAsync(PlayerReport request);
}
35 changes: 35 additions & 0 deletions InsightLogParser.Client/Configuration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

namespace InsightLogParser.Client;

public class Configuration
{
public static readonly int CurrentConfigurationVersion = 1;

public int ConfigVersion { get; set; } = CurrentConfigurationVersion;

public string? ForceLogFolder { get; set; }
public string? ForceGameRootFolder { get; set; }
public string? ForcedParsedDatabasePath { get; set; }

public string? CetusApiKey { get; set; }
public string? CetusUri { get; set; }

public bool ShowGameLogLines { get; set; } = false;
public bool DebugMode { get; set; } = false;

public bool BeepOnNewParse { get; set; } = true;
public int NewParseBeepFrequency { get; set; } = 880;
public int NewParseBeepDuration { get; set; } = 100;
public bool BeepOnKnownParse { get; set; } = false;
public int KnownParseBeepFrequency { get; set; } = 220;
public int KnownParseBeepDuration { get; set; } = 200;
public bool BeepOnOpeningSolvedPuzzle { get; set; } = true;
public int OpenSolvedPuzzleBeepFrequency { get; set; } = 220;
public int OpenSolvedPuzzleBeepDuration { get; set; } = 200;
public bool BeepForAttention { get; set; } = true;
public int BeepForAttentionFrequency { get; set; } = 220;
public int BeepForAttentionDuration { get; set; } = 200;
public int BeepForAttentionCount { get; set; } = 3;
public int BeepForAttentionInterval { get; set; } = 200;

}
Loading

0 comments on commit 12be645

Please sign in to comment.