diff --git a/shared/commands/Modules/Render.cs b/shared/commands/Modules/Render.cs index c89456b5..57d364a4 100644 --- a/shared/commands/Modules/Render.cs +++ b/shared/commands/Modules/Render.cs @@ -103,46 +103,49 @@ long id _avatarSettings.RenderYDimension ); - try { + try + { - var (stream, fileName) = _avatarUtility.RenderUser( - id, - _avatarSettings.PlaceIdForRenders, - _avatarSettings.RenderXDimension, - _avatarSettings.RenderYDimension - ); + var (stream, fileName) = _avatarUtility.RenderUser( + id, + _avatarSettings.PlaceIdForRenders, + _avatarSettings.RenderXDimension, + _avatarSettings.RenderYDimension + ); - if (stream == null) - { - await FollowupAsync("An error occurred while rendering the character."); + if (stream == null) + { + await FollowupAsync("An error occurred while rendering the character."); - return; - } + return; + } - using (stream) - await FollowupWithFileAsync( - stream, - fileName - ); + using (stream) + await FollowupWithFileAsync( + stream, + fileName + ); } - catch (Exception e) + catch (ThumbnailResponseException e) { - _logger.Error("An error occurred while rendering the character for the user '{0}': {1}", id, e); + _logger.Warning("The thumbnail service responded with the following state: {0}, message: {1}", e.State, e.Message); - if (e is ThumbnailResponseException thumbnailResponseException) + if (e.State == ThumbnailResponseState.InReview) { - if (thumbnailResponseException.State == ThumbnailResponseState.InReview) - { - // Bogus error here for the sake of the user. Like flood checker error. - await FollowupAsync("You are sending render commands too quickly, please wait a few moments and try again."); - - return; - } + // Bogus error here for the sake of the user. Like flood checker error. + await FollowupAsync("The thumbnail service placed the request in review, please try again later."); - // Bogus error for anything else, we don't need this to be noted that we are using rbx-thumbnails. + return; } + // Bogus error for anything else, we don't need this to be noted that we are using rbx-thumbnails. + await FollowupAsync($"The thumbnail service responded with the following state: {e.State}"); + } + catch (Exception e) + { + _logger.Error("An error occurred while rendering the character for the user '{0}': {1}", id, e); + await FollowupAsync("An error occurred while rendering the character."); } } diff --git a/shared/settings/Providers/AvatarSettings.cs b/shared/settings/Providers/AvatarSettings.cs index 9eb6fee6..6a7582f3 100644 --- a/shared/settings/Providers/AvatarSettings.cs +++ b/shared/settings/Providers/AvatarSettings.cs @@ -97,4 +97,25 @@ public class AvatarSettings : BaseSettingsProvider nameof(LocalCacheTtl), TimeSpan.FromMinutes(5) ); + +#if USE_VAULT_SETTINGS_PROVIDER + /// + /// A list of user IDs that should be automatically blacklisted. + /// + public long[] BlacklistUserIds { + get => GetOrDefault( + nameof(BlacklistUserIds), + Array.Empty() + ); + set => Set(nameof(BlacklistUserIds), value); + } + + /// + /// Gets the period to wait before persisting the blacklist. + /// + public TimeSpan BlacklistPersistPeriod => GetOrDefault( + nameof(BlacklistPersistPeriod), + TimeSpan.FromMinutes(5) + ); +#endif } diff --git a/shared/utility/Implementation/AvatarUtility.cs b/shared/utility/Implementation/AvatarUtility.cs index 31a5de2e..75a1a713 100644 --- a/shared/utility/Implementation/AvatarUtility.cs +++ b/shared/utility/Implementation/AvatarUtility.cs @@ -16,6 +16,7 @@ using Grid.Commands; using GridJob = Grid.Client.Job; +using System.Collections.Concurrent; /// /// Exception thrown when rbx-thumbnails returns a state that is not pending or completed. @@ -54,6 +55,7 @@ public class AvatarUtility : IAvatarUtility private readonly IPercentageInvoker _percentageInvoker; private readonly ExpirableDictionary<(long, ThumbnailCommandType), string> _localCachedPaths; + private readonly ConcurrentBag _idsNotToUse = new(); // These IDs error out when trying to render (they are blocked? or they are moderated?) /// /// Construct a new instance of . @@ -82,7 +84,42 @@ IPercentageInvoker percentageInvoker _localCachedPaths = new(avatarSettings.LocalCacheTtl); _localCachedPaths.EntryRemoved += OnLocalCacheEntryRemoved; + +#if USE_VAULT_SETTINGS_PROVIDER + foreach (var id in avatarSettings.BlacklistUserIds) + _idsNotToUse.Add(id); + + if (!_idsNotToUse.IsEmpty) + _logger.Warning("Blacklisted user IDs: {0}", string.Join(", ", avatarSettings.BlacklistUserIds)); + + Task.Factory.StartNew(PersistBlacklistedIds, TaskCreationOptions.LongRunning); +#endif + } + +#if USE_VAULT_SETTINGS_PROVIDER + private void PersistBlacklistedIds() + { + while (true) + { + Task.Delay(_avatarSettings.BlacklistPersistPeriod).Wait(); + + if (_avatarSettings.BlacklistUserIds.SequenceEqual(_idsNotToUse)) + continue; + + // Logging purposes: grab any new ones that were added. + var newIds = _idsNotToUse.Except(_avatarSettings.BlacklistUserIds).ToArray(); + var removedIds = _avatarSettings.BlacklistUserIds.Except(_idsNotToUse).ToArray(); + + _logger.Warning( + "Blacklisted user IDs were updated. New IDs: {0}, Removed IDs: {1}", + string.Join(", ", newIds), + string.Join(", ", removedIds) + ); + + _avatarSettings.BlacklistUserIds = [.. _idsNotToUse]; + } } +#endif private void OnLocalCacheEntryRemoved(string path, RemovalReason reason) { @@ -117,7 +154,7 @@ private IEnumerable GetThumbnailArgs(string url, int x, int y) yield return 0; // cameraOffsetY } - private static string PollUntilCompleted(Func func) + private string PollUntilCompleted(long userId, Func func) { var response = func() ?? throw new ThumbnailResponseException(ThumbnailResponseState.Error, "The thumbnail response was null."); @@ -125,7 +162,12 @@ private static string PollUntilCompleted(Func func) return response.ImageUrl; if (response.State != ThumbnailResponseState.Pending) + { + if (response.State != ThumbnailResponseState.InReview) + _idsNotToUse.Add(userId); + throw new ThumbnailResponseException(response.State.GetValueOrDefault(), "The thumbnail response was not pending."); + } while (response.State == ThumbnailResponseState.Pending) { @@ -135,7 +177,12 @@ private static string PollUntilCompleted(Func func) } if (response.State != ThumbnailResponseState.Completed) + { + if (response.State != ThumbnailResponseState.InReview) + _idsNotToUse.Add(userId); + throw new ThumbnailResponseException(response.State.GetValueOrDefault(), "The thumbnail response was not completed."); + } return response.ImageUrl; } @@ -158,6 +205,12 @@ private static string DownloadFile(string url) private (Stream, string) GetThumbnail(long userId, ThumbnailCommandType thumbnailCommandType) { + if (userId == 0) + throw new ArgumentException("The user ID cannot be 0.", nameof(userId)); + + if (_idsNotToUse.Contains(userId)) + throw new ThumbnailResponseException(ThumbnailResponseState.Blocked, "The user ID is blacklisted."); + if (thumbnailCommandType != ThumbnailCommandType.Closeup && thumbnailCommandType != ThumbnailCommandType.Avatar_R15_Action) throw new ArgumentException("The thumbnail command type must be either closeup or avatar_r15_action.", nameof(thumbnailCommandType)); @@ -180,6 +233,7 @@ private static string DownloadFile(string url) { case ThumbnailCommandType.Closeup: url = PollUntilCompleted( + userId, () => _thumbnailsClient.GetAvatarHeadshotThumbnailAsync( userIds, _avatarSettings.RenderDimensions, @@ -191,6 +245,7 @@ private static string DownloadFile(string url) return DownloadFile(url); case ThumbnailCommandType.Avatar_R15_Action: url = PollUntilCompleted( + userId, () => _thumbnailsClient.GetAvatarThumbnailAsync( userIds, _avatarSettings.RenderDimensions, diff --git a/shared/utility/Shared.Utility.csproj b/shared/utility/Shared.Utility.csproj index c8b6f9e8..45336ac6 100644 --- a/shared/utility/Shared.Utility.csproj +++ b/shared/utility/Shared.Utility.csproj @@ -1,4 +1,8 @@ + + $(DefineConstants);USE_VAULT_SETTINGS_PROVIDER + + Shared utility classes used by the grid-bot true