Merge branch 'main' into user-pfp-discord-rpc

This commit is contained in:
axell 2024-09-04 17:16:11 +02:00 committed by GitHub
commit 3510730a6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 123 additions and 79 deletions

View File

@ -110,22 +110,24 @@ namespace Bloxstrap
public static async Task<GithubRelease?> GetLatestRelease() public static async Task<GithubRelease?> GetLatestRelease()
{ {
const string LOG_IDENT = "App::GetLatestRelease"; const string LOG_IDENT = "App::GetLatestRelease";
GithubRelease? releaseInfo = null;
try try
{ {
releaseInfo = await Http.GetJson<GithubRelease>($"https://api.github.com/repos/{ProjectRepository}/releases/latest"); var releaseInfo = await Http.GetJson<GithubRelease>($"https://api.github.com/repos/{ProjectRepository}/releases/latest");
if (releaseInfo is null || releaseInfo.Assets is null) if (releaseInfo is null || releaseInfo.Assets is null)
{
Logger.WriteLine(LOG_IDENT, "Encountered invalid data"); Logger.WriteLine(LOG_IDENT, "Encountered invalid data");
return null;
}
return releaseInfo;
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.WriteException(LOG_IDENT, ex); Logger.WriteException(LOG_IDENT, ex);
} }
return releaseInfo; return null;
} }
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bloxstrap.Exceptions
{
internal class InvalidHTTPResponseException : Exception
{
public InvalidHTTPResponseException(string message) : base(message) { }
}
}

View File

@ -1,4 +1,6 @@
namespace Bloxstrap.Integrations using System.Windows;
namespace Bloxstrap.Integrations
{ {
public class ActivityWatcher : IDisposable public class ActivityWatcher : IDisposable
{ {
@ -388,21 +390,19 @@
string location = ""; string location = "";
var ipInfo = await Http.GetJson<IPInfoResponse>($"https://ipinfo.io/{Data.MachineAddress}/json"); var ipInfo = await Http.GetJson<IPInfoResponse>($"https://ipinfo.io/{Data.MachineAddress}/json");
if (ipInfo is null) if (String.IsNullOrEmpty(ipInfo.City))
return $"? ({Strings.ActivityTracker_LookupFailed})"; throw new InvalidHTTPResponseException("Reported city was blank");
if (string.IsNullOrEmpty(ipInfo.Country)) if (ipInfo.City == ipInfo.Region)
location = "?";
else if (ipInfo.City == ipInfo.Region)
location = $"{ipInfo.Region}, {ipInfo.Country}"; location = $"{ipInfo.Region}, {ipInfo.Country}";
else else
location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}"; location = $"{ipInfo.City}, {ipInfo.Region}, {ipInfo.Country}";
if (!InGame)
return $"? ({Strings.ActivityTracker_LeftGame})";
GeolocationCache[Data.MachineAddress] = location; GeolocationCache[Data.MachineAddress] = location;
if (!InGame)
return "";
return location; return location;
} }
catch (Exception ex) catch (Exception ex)
@ -410,7 +410,9 @@
App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {Data.MachineAddress}"); App.Logger.WriteLine(LOG_IDENT, $"Failed to get server location for {Data.MachineAddress}");
App.Logger.WriteException(LOG_IDENT, ex); App.Logger.WriteException(LOG_IDENT, ex);
return $"? ({Strings.ActivityTracker_LookupFailed})"; Frontend.ShowMessageBox($"{Strings.ActivityWatcher_LocationQueryFailed}\n\n{ex.Message}", MessageBoxImage.Warning);
return "?";
} }
} }

View File

@ -1,3 +1,4 @@
using System.Windows;
using DiscordRPC; using DiscordRPC;
namespace Bloxstrap.Integrations namespace Bloxstrap.Integrations
@ -207,17 +208,21 @@ namespace Bloxstrap.Integrations
if (activity.UniverseDetails is null) if (activity.UniverseDetails is null)
{ {
await UniverseDetails.FetchSingle(activity.UniverseId); try
{
await UniverseDetails.FetchSingle(activity.UniverseId);
}
catch (Exception ex)
{
App.Logger.WriteException(LOG_IDENT, ex);
Frontend.ShowMessageBox($"{Strings.ActivityWatcher_RichPresenceLoadFailed}\n\n{ex.Message}", MessageBoxImage.Warning);
return false;
}
activity.UniverseDetails = UniverseDetails.LoadFromCache(activity.UniverseId); activity.UniverseDetails = UniverseDetails.LoadFromCache(activity.UniverseId);
} }
var universeDetails = activity.UniverseDetails; var universeDetails = activity.UniverseDetails!;
if (universeDetails is null)
{
Frontend.ShowMessageBox(Strings.ActivityTracker_RichPresenceLoadFailed, System.Windows.MessageBoxImage.Warning);
return false;
}
icon = universeDetails.Thumbnail.ImageUrl; icon = universeDetails.Thumbnail.ImageUrl;

View File

@ -46,7 +46,7 @@ namespace Bloxstrap
message = Strings.JsonManager_FastFlagsLoadFailed; message = Strings.JsonManager_FastFlagsLoadFailed;
if (!String.IsNullOrEmpty(message)) if (!String.IsNullOrEmpty(message))
Frontend.ShowMessageBox($"{message}\n\n{ex.GetType()}: {ex.Message}", System.Windows.MessageBoxImage.Warning); Frontend.ShowMessageBox($"{message}\n\n{ex.Message}", System.Windows.MessageBoxImage.Warning);
} }
Save(); Save();

View File

@ -21,17 +21,19 @@
return null; return null;
} }
public static Task<bool> FetchSingle(long id) => FetchBulk(id.ToString()); public static Task FetchSingle(long id) => FetchBulk(id.ToString());
public static async Task<bool> FetchBulk(string ids) public static async Task FetchBulk(string ids)
{ {
var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={ids}"); var gameDetailResponse = await Http.GetJson<ApiArrayResponse<GameDetailResponse>>($"https://games.roblox.com/v1/games?universeIds={ids}");
if (gameDetailResponse is null || !gameDetailResponse.Data.Any())
return false; if (!gameDetailResponse.Data.Any())
throw new InvalidHTTPResponseException("Roblox API for Game Details returned invalid data");
var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={ids}&returnPolicy=PlaceHolder&size=128x128&format=Png&isCircular=false"); var universeThumbnailResponse = await Http.GetJson<ApiArrayResponse<ThumbnailResponse>>($"https://thumbnails.roblox.com/v1/games/icons?universeIds={ids}&returnPolicy=PlaceHolder&size=128x128&format=Png&isCircular=false");
if (universeThumbnailResponse is null || !universeThumbnailResponse.Data.Any())
return false; if (!universeThumbnailResponse.Data.Any())
throw new InvalidHTTPResponseException("Roblox API for Game Thumbnails returned invalid data");
foreach (string strId in ids.Split(',')) foreach (string strId in ids.Split(','))
{ {
@ -43,8 +45,6 @@
Thumbnail = universeThumbnailResponse.Data.Where(x => x.TargetId == id).First(), Thumbnail = universeThumbnailResponse.Data.Where(x => x.TargetId == id).First(),
}); });
} }
return true;
} }
} }
} }

View File

@ -106,29 +106,20 @@ namespace Bloxstrap.Resources {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to left game. /// Looks up a localized string similar to Failed to query server location..
/// </summary> /// </summary>
public static string ActivityTracker_LeftGame { public static string ActivityWatcher_LocationQueryFailed {
get { get {
return ResourceManager.GetString("ActivityTracker.LeftGame", resourceCulture); return ResourceManager.GetString("ActivityWatcher.LocationQueryFailed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to lookup failed.
/// </summary>
public static string ActivityTracker_LookupFailed {
get {
return ResourceManager.GetString("ActivityTracker.LookupFailed", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Your current game will not show on your Discord presence because an error occurred when loading the game information.. /// Looks up a localized string similar to Your current game will not show on your Discord presence because an error occurred when loading the game information..
/// </summary> /// </summary>
public static string ActivityTracker_RichPresenceLoadFailed { public static string ActivityWatcher_RichPresenceLoadFailed {
get { get {
return ResourceManager.GetString("ActivityTracker.RichPresenceLoadFailed", resourceCulture); return ResourceManager.GetString("ActivityWatcher.RichPresenceLoadFailed", resourceCulture);
} }
} }

View File

@ -117,12 +117,6 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ActivityTracker.LeftGame" xml:space="preserve">
<value>left game</value>
</data>
<data name="ActivityTracker.LookupFailed" xml:space="preserve">
<value>lookup failed</value>
</data>
<data name="Bootstrapper.AutoUpdateFailed" xml:space="preserve"> <data name="Bootstrapper.AutoUpdateFailed" xml:space="preserve">
<value>Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website.</value> <value>Bloxstrap was unable to automatically update to version {0}. Please update it manually by downloading and running it from the website.</value>
</data> </data>
@ -1177,10 +1171,13 @@ Are you sure you want to continue?</value>
<data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve"> <data name="ContextMenu.GameHistory.Rejoin" xml:space="preserve">
<value>Rejoin</value> <value>Rejoin</value>
</data> </data>
<data name="ActivityTracker.RichPresenceLoadFailed" xml:space="preserve"> <data name="ActivityWatcher.RichPresenceLoadFailed" xml:space="preserve">
<value>Your current game will not show on your Discord presence because an error occurred when loading the game information.</value> <value>Your current game will not show on your Discord presence because an error occurred when loading the game information.</value>
</data> </data>
<data name="ContextMenu.GameHistory.Description" xml:space="preserve"> <data name="ContextMenu.GameHistory.Description" xml:space="preserve">
<value>Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them.</value> <value>Game history is only recorded for your current Roblox session. Games will appear here as you leave them or teleport within them.</value>
</data> </data>
<data name="ActivityWatcher.LocationQueryFailed" xml:space="preserve">
<value>Failed to query server location.</value>
</data>
</root> </root>

View File

@ -8,6 +8,7 @@
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.ContextMenu" xmlns:models="clr-namespace:Bloxstrap.UI.ViewModels.ContextMenu"
xmlns:resources="clr-namespace:Bloxstrap.Resources" xmlns:resources="clr-namespace:Bloxstrap.Resources"
xmlns:enums="clr-namespace:Bloxstrap.Enums"
d:DataContext="{d:DesignInstance Type=models:ServerHistoryViewModel}" d:DataContext="{d:DesignInstance Type=models:ServerHistoryViewModel}"
mc:Ignorable="d" mc:Ignorable="d"
Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}" Title="{x:Static resources:Strings.ContextMenu_GameHistory_Title}"
@ -30,11 +31,24 @@
<TextBlock Grid.Row="1" Margin="16,8,16,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Description}" TextWrapping="Wrap" /> <TextBlock Grid.Row="1" Margin="16,8,16,0" Text="{x:Static resources:Strings.ContextMenu_GameHistory_Description}" TextWrapping="Wrap" />
<TextBlock Grid.Row="2" Margin="16,8,16,0" Text="{Binding Error, Mode=OneWay}" TextWrapping="Wrap">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Failed}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</TextBlock.Style>
</TextBlock>
<Border Grid.Row="2"> <Border Grid.Row="2">
<Border.Style> <Border.Style>
<Style TargetType="Border"> <Style TargetType="Border">
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding GameHistory, Mode=OneWay}" Value="{x:Null}"> <DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Unknown}">
<Setter Property="Visibility" Value="Visible" /> <Setter Property="Visibility" Value="Visible" />
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
@ -49,11 +63,11 @@
<ListView.Style> <ListView.Style>
<Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}"> <Style TargetType="ListView" BasedOn="{StaticResource {x:Type ListView}}">
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding GameHistory, Mode=OneWay}" Value="{x:Null}"> <DataTrigger Binding="{Binding LoadState, Mode=OneWay}" Value="{x:Static enums:GenericTriState.Successful}">
<Setter Property="Visibility" Value="Collapsed" /> <Setter Property="Visibility" Value="Visible" />
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
<Setter Property="Visibility" Value="Visible" /> <Setter Property="Visibility" Value="Collapsed" />
</Style> </Style>
</ListView.Style> </ListView.Style>
<ListView.ItemTemplate> <ListView.ItemTemplate>

View File

@ -60,6 +60,10 @@ namespace Bloxstrap.UI
return; return;
string serverLocation = await _activityWatcher.GetServerLocation(); string serverLocation = await _activityWatcher.GetServerLocation();
if (string.IsNullOrEmpty(serverLocation))
return;
string title = _activityWatcher.Data.ServerType switch string title = _activityWatcher.Data.ServerType switch
{ {
ServerType.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public, ServerType.Public => Strings.ContextMenu_ServerInformation_Notification_Title_Public,

View File

@ -10,6 +10,10 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
public List<ActivityData>? GameHistory { get; private set; } public List<ActivityData>? GameHistory { get; private set; }
public GenericTriState LoadState { get; private set; } = GenericTriState.Unknown;
public string Error { get; private set; } = String.Empty;
public ICommand CloseWindowCommand => new RelayCommand(RequestClose); public ICommand CloseWindowCommand => new RelayCommand(RequestClose);
public EventHandler? RequestCloseEvent; public EventHandler? RequestCloseEvent;
@ -25,6 +29,9 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
private async void LoadData() private async void LoadData()
{ {
LoadState = GenericTriState.Unknown;
OnPropertyChanged(nameof(LoadState));
var entries = _activityWatcher.History.Where(x => x.UniverseDetails is null); var entries = _activityWatcher.History.Where(x => x.UniverseDetails is null);
if (entries.Any()) if (entries.Any())
@ -32,8 +39,22 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
// TODO: this will duplicate universe ids // TODO: this will duplicate universe ids
string universeIds = String.Join(',', entries.Select(x => x.UniverseId)); string universeIds = String.Join(',', entries.Select(x => x.UniverseId));
if (!await UniverseDetails.FetchBulk(universeIds)) try
{
await UniverseDetails.FetchBulk(universeIds);
}
catch (Exception ex)
{
App.Logger.WriteException("ServerHistoryViewModel::LoadData", ex);
Error = ex.Message;
OnPropertyChanged(nameof(Error));
LoadState = GenericTriState.Failed;
OnPropertyChanged(nameof(LoadState));
return; return;
}
foreach (var entry in entries) foreach (var entry in entries)
entry.UniverseDetails = UniverseDetails.LoadFromCache(entry.UniverseId); entry.UniverseDetails = UniverseDetails.LoadFromCache(entry.UniverseId);
@ -64,6 +85,9 @@ namespace Bloxstrap.UI.ViewModels.ContextMenu
} }
OnPropertyChanged(nameof(GameHistory)); OnPropertyChanged(nameof(GameHistory));
LoadState = GenericTriState.Successful;
OnPropertyChanged(nameof(LoadState));
} }
private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty); private void RequestClose() => RequestCloseEvent?.Invoke(this, EventArgs.Empty);

View File

@ -5,14 +5,6 @@ namespace Bloxstrap
{ {
static class Utilities static class Utilities
{ {
/// <summary>
/// Is process running as administrator
/// https://stackoverflow.com/a/11660205
/// </summary>
public static bool IsAdministrator =>
new WindowsPrincipal(WindowsIdentity.GetCurrent())
.IsInRole(WindowsBuiltInRole.Administrator);
public static void ShellExecute(string website) public static void ShellExecute(string website)
{ {
try try

View File

@ -2,22 +2,22 @@
{ {
internal static class Http internal static class Http
{ {
public static async Task<T?> GetJson<T>(string url) /// <summary>
/// Gets and deserializes a JSON API response to the specified object
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="url"></param>
/// <exception cref="HttpRequestException"></exception>
/// <exception cref="JsonException"></exception>
public static async Task<T> GetJson<T>(string url)
{ {
string LOG_IDENT = $"Http::GetJson<{typeof(T).Name}>"; var request = await App.HttpClient.GetAsync(url);
string json = await App.HttpClient.GetStringAsync(url); request.EnsureSuccessStatusCode();
try string json = await request.Content.ReadAsStringAsync();
{
return JsonSerializer.Deserialize<T>(json); return JsonSerializer.Deserialize<T>(json)!;
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT, $"Failed to deserialize JSON for {url}!");
App.Logger.WriteException(LOG_IDENT, ex);
return default;
}
} }
} }
} }