From 88a6c1b32d200399111d1f15a66705f5aa9fa117 Mon Sep 17 00:00:00 2001 From: Jay Long Date: Fri, 13 Mar 2026 12:42:42 -0500 Subject: [PATCH 01/11] Small refactors to streamline code, Naming convetion corrections --- src/Clients/VRChat/VRChat.cs | 477 +++++++++++------------ src/PlayerManagement/PlayerManagement.cs | 5 +- 2 files changed, 224 insertions(+), 258 deletions(-) diff --git a/src/Clients/VRChat/VRChat.cs b/src/Clients/VRChat/VRChat.cs index dfafd88..03ac09c 100644 --- a/src/Clients/VRChat/VRChat.cs +++ b/src/Clients/VRChat/VRChat.cs @@ -16,11 +16,15 @@ namespace Tailgrab.Clients.VRChat public class VRChatClient { private const string URI_VRC_BASE_API = "https://api.vrchat.cloud"; - public static string UserAgent = "Tailgrab/1.1.0"; - public static Logger logger = LogManager.GetCurrentClassLogger(); + private const string APP_NAME = "Tailgrab"; + private const string API_VERSION = "1.1.2"; + private const string APP_CONTACT = "jlong@rabbitearsvideoproduction.com"; + private const string UserAgent = $"{APP_NAME}/{API_VERSION}"; + private static Logger logger = LogManager.GetCurrentClassLogger(); private IVRChat? _vrchat; + #region Authentication public async Task Initialize() { string? username = ConfigStore.LoadSecret(CommonConst.Registry_VRChat_Web_UserName); @@ -37,127 +41,175 @@ public async Task Initialize() return; } - //string cookiePath = Path.Combine(Directory.GetCurrentDirectory(), "cookies.json"); - string cookiePath = Path.Combine(CommonConst.APPLICATION_LOCAL_DATA_PATH, "cookies.json"); - - // Try to load cookies from disk and use them if they are present and not expired - List? loadedCookies = LoadValidCookiesFromFile(cookiePath); - - VRChatClientBuilder builder = new VRChatClientBuilder() - .WithApplication(name: "Tailgrab", version: "1.1.0", contact: "jlong@rabbitearsvideoproduction.com"); - - if (loadedCookies != null && loadedCookies.Count > 0) + if (LoginVRChat() && _vrchat != null) { - logger.Info("Loaded valid cookies from disk, attempting to use them for authentication..."); - //// Try to call WithCookies via reflection (some SDKs expose it) - //var withCookiesMethod = builder.GetType() - // .GetMethods() - // .FirstOrDefault(m => m.Name == "WithCookies" && m.GetParameters().Length == 1); - - //if (withCookiesMethod != null) - //{ - // var result = withCookiesMethod.Invoke(builder, new object[] { loadedCookies }); - // if (result is VRChatClientBuilder cb) - // { - // builder = cb; - // } - // else - // { - // // fallback to username/password if return type not expected - // builder = builder.WithUsername(username).WithPassword(password); - // } - //} - //else - //{ - // // no WithCookies method; fall back to username/password - // builder = builder.WithUsername(username).WithPassword(password); - //} - - string authCookieValue = string.Empty; - string twoFactorCookieValue = string.Empty; - foreach (var cookie in loadedCookies) + var response = await _vrchat.Authentication.GetCurrentUserAsync(); + if (response != null && response is not null) { - if (cookie.Name == "auth") + if (response.RequiresTwoFactorAuth != null && response.RequiresTwoFactorAuth.Contains("emailOtp")) { - authCookieValue = cookie.Value; + logger.Warn("An verification code was sent to your email address!"); + logger.Warn("Prompt user for code: "); + + string code = Microsoft.VisualBasic.Interaction.InputBox("Please enter EMail OTP code (6 digits)"); + var otpResponse = await _vrchat.Authentication.Verify2FAEmailCodeAsync(new TwoFactorEmailCode(code)); } - else if (cookie.Name == "twoFactorAuth") + else if (response.RequiresTwoFactorAuth != null && response.RequiresTwoFactorAuth.Contains("totp")) { - twoFactorCookieValue = cookie.Value; + var totp = new Totp(Base32Encoding.ToBytes(twoFactorSecret)); + string code = totp.ComputeTotp(); + + var otpResponse = await _vrchat.Authentication.Verify2FAAsync(new TwoFactorAuthCode(code)); } + + var currentUser = await _vrchat.Authentication.GetCurrentUserAsync(); + logger.Info($"Logged in as \"{currentUser.DisplayName}\""); + + var cookies = _vrchat.GetCookies(); + PersistCookies(cookies); } - _vrchat = builder.WithAuthCookie(authCookieValue, twoFactorCookieValue).Build(); - } - else + } else { - logger.Info("No valid cookies found on disk, falling back to username/password authentication."); - _vrchat = builder.WithUsername(username).WithPassword(password).Build(); + logger.Warn("Unable to login to VRChat "); + System.Windows.MessageBox.Show("VR Chat Web API failed to log in, check the log file and restart Tailgrab.", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + return; } + } + catch (Exception ex) + { + logger.Error(ex, $"Failed to Log Into VRC and to save cookies': {ex.Message}"); + System.Windows.MessageBox.Show($"Failed to Log Into VRChat Web API, check logs for details. Error: {ex.Message}", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + } + } + + private bool LoginVRChat() + { + // Try to load cookies from disk and use them if they are present and not expired + List? loadedCookies = LoadCookies(); - var response = await _vrchat.Authentication.GetCurrentUserAsync(); - if (response != null && response is CurrentUser ) + VRChatClientBuilder builder = new VRChatClientBuilder() + .WithApplication(name: APP_NAME, version: API_VERSION, contact: APP_CONTACT); + + if (loadedCookies != null && loadedCookies.Count > 0) + { + logger.Info("Loaded valid cookies from disk, attempting to use them for authentication..."); + + string authCookieValue = string.Empty; + string twoFactorCookieValue = string.Empty; + foreach (var cookie in loadedCookies) { - if (response.RequiresTwoFactorAuth != null && response.RequiresTwoFactorAuth.Contains("emailOtp")) + if (cookie.Name == "auth") { - logger.Info("An verification code was sent to your email address!"); - logger.Info("Enter code: "); - //string code = Console.ReadLine(); - string code = "1234"; - var otpResponse = await _vrchat.Authentication.Verify2FAEmailCodeAsync(new TwoFactorEmailCode(code)); + authCookieValue = cookie.Value; } - else if (response.RequiresTwoFactorAuth != null && response.RequiresTwoFactorAuth.Contains("totp")) + else if (cookie.Name == "twoFactorAuth") { - var totp = new Totp(Base32Encoding.ToBytes(twoFactorSecret)); - string code = totp.ComputeTotp(); - - var otpResponse = await _vrchat.Authentication.Verify2FAAsync(new TwoFactorAuthCode(code)); + twoFactorCookieValue = cookie.Value; } + } + _vrchat = builder.WithAuthCookie(authCookieValue, twoFactorCookieValue).Build(); + return true; + } + else + { + string? username = ConfigStore.LoadSecret(CommonConst.Registry_VRChat_Web_UserName); + string? password = ConfigStore.LoadSecret(CommonConst.Registry_VRChat_Web_Password); + if (username != null && password != null) + { + logger.Info("No valid cookies found on disk, falling back to username/password authentication."); + _vrchat = builder.WithUsername(username).WithPassword(password).Build(); + return true; + } + } + + return false; + } - var currentUser = await _vrchat.Authentication.GetCurrentUserAsync(); - logger.Info($"Logged in as \"{currentUser.DisplayName}\""); - var cookies = _vrchat.GetCookies(); + #endregion - SaveCookiesToFile(cookiePath, cookies); + public List GetAvatarModerations() + { + List moderations = []; + try + { + if (_vrchat != null) + { + moderations = _vrchat.Authentication.GetGlobalAvatarModerations(); } } catch (Exception ex) { - logger.Error(ex, $"Failed to Log Into VRC and to save cookies': {ex.Message}"); - System.Windows.MessageBox.Show($"Failed to Log Into VRChat Web API, check logs for details. Error: {ex.Message}", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + logger.Error($"Error fetching avatar moderations: {ex.Message}"); } + return moderations; } - private static List? LoadValidCookiesFromFile(string filePath) + public async Task BlockAvatarGlobal(string avatarId) { - if (!System.IO.File.Exists(filePath)) - return null; - try { - var json = System.IO.File.ReadAllText(filePath); - var dtoList = JsonConvert.DeserializeObject>(json); - if (dtoList == null || dtoList.Count == 0) - return null; + if (_vrchat == null) + { + logger.Info($"Failed Block avatar {avatarId} globally, not logged in."); + return false; + } - // If any cookie has an Expires value set and is expired, treat the whole set as invalid - DateTime now = DateTime.UtcNow; - foreach (var dto in dtoList) + // Create HTTP client with cookies + using HttpClient httpClient = CreateHttpClientWithCookies(); + + AvatarModerationItem rpt = new() { - if (dto.Expires != DateTime.MinValue && dto.Expires.ToUniversalTime() <= now) - { - return null; - } + TargetAvatarId = avatarId, + AvatarModerationType = "block" + }; + + HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{URI_VRC_BASE_API}/api/1/auth/user/avatarmoderations?targetAvatarId={avatarId}&avatarModerationType=block", rpt); + string responseContent = await response.Content.ReadAsStringAsync(); + logger.Debug($"Response from Block avatar {avatarId} globally: {responseContent}"); + logger.Info($"Submitted Block avatar {avatarId} globally."); + response.EnsureSuccessStatusCode(); + + return response.IsSuccessStatusCode; + } + catch (Exception ex) + { + logger.Error($"Error setting avatar moderation status: {ex.Message}"); + } + + return false; + } + + public async Task DeleteAvatarGlobal(string avatarId) + { + try + { + if (_vrchat == null) + { + logger.Info($"Failed Unblock avatar {avatarId} globally, not logged in."); + return false; } - var cookies = dtoList.Select(d => d.ToCookie()).ToList(); - return cookies; + // Create HTTP client with cookies + using HttpClient httpClient = CreateHttpClientWithCookies(); + + HttpResponseMessage response = await httpClient.DeleteAsync($"{URI_VRC_BASE_API}/api/1/auth/user/avatarmoderations?targetAvatarId={avatarId}&avatarModerationType=block"); + string responseContent = await response.Content.ReadAsStringAsync(); + logger.Debug($"Response from Block avatar {avatarId} globally: {responseContent}"); + logger.Info($"Submitted Block avatar {avatarId} globally."); + response.EnsureSuccessStatusCode(); + + return response.IsSuccessStatusCode; } - catch + catch (Exception ex) { - return null; + logger.Error($"Error setting avatar moderation status: {ex.Message}"); } + + return false; } + + #region Avatar Management public Avatar? GetAvatarById(string avatarId) { Avatar? avatar = null; @@ -178,7 +230,7 @@ public async Task Initialize() public List GetAvatarsByUserId(string userId) { - List avatars = new List(); + List avatars = []; try { if (_vrchat != null) @@ -193,11 +245,12 @@ public List GetAvatarsByUserId(string userId) return avatars; } + #endregion - + #region Profile Management public User GetProfile(string userId) { - User profile = new User(); + User profile = new (); try { if (_vrchat != null) @@ -215,7 +268,7 @@ public User GetProfile(string userId) public List GetProfileGroups(string userId) { - List groups = new List(); + List groups = []; try { if (_vrchat != null) @@ -245,19 +298,7 @@ public List GetProfileGroups(string userId) string url = $"{URI_VRC_BASE_API}/api/1/user/{userId}/inventory/{itemId}"; // Create HTTP client with cookies - var handler = new HttpClientHandler - { - CookieContainer = new CookieContainer() - }; - - var cookies = _vrchat.GetCookies(); - foreach (var cookie in cookies) - { - handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); - } - - using var httpClient = new HttpClient(handler); - httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); + using HttpClient httpClient = CreateHttpClientWithCookies(); var response = await httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); @@ -277,7 +318,9 @@ public List GetProfileGroups(string userId) return item; } + #endregion + #region Image Assets public async Task GetImageReference(string inventoryId, string userId, List imageUrlList) { try @@ -289,23 +332,11 @@ public List GetProfileGroups(string userId) } // Create HTTP client with cookies - var handler = new HttpClientHandler - { - CookieContainer = new CookieContainer() - }; - - var cookies = _vrchat.GetCookies(); - foreach (var cookie in cookies) - { - handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); - } - - using HttpClient httpClient = new HttpClient(handler); - httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); + using HttpClient httpClient = CreateHttpClientWithCookies(); // Download the image string md5Hash = string.Empty; - List imageList = new List(); + List imageList = []; int imageCount = 0; foreach (string imageUrl in imageUrlList) { @@ -318,7 +349,7 @@ public List GetProfileGroups(string userId) imageList.Add(contentB64); } - ImageReference iref = new ImageReference + ImageReference iref = new () { Base64Data = imageList, Md5Hash = md5Hash, @@ -335,7 +366,9 @@ public List GetProfileGroups(string userId) return null; } } + #endregion + #region Print Management public Print? GetPrintInfo(string fileURL) { Print? printInfo = null; @@ -354,198 +387,130 @@ public List GetProfileGroups(string userId) return printInfo; } + #endregion - public Inventory? GetInventoryInfo(string fileURL) + #region Group Management + internal Group? GetGroupById(string id) { - Inventory? printInfo = null; + Group? group = null; try { if (_vrchat != null) { - printInfo = _vrchat.Inventory.GetInventory(); + group = _vrchat.Groups.GetGroup(id); } } catch (Exception ex) { - logger.Error($"Error fetching avatar: {ex.Message}"); + logger.Error($"Error fetching Group information: {ex.Message}"); } - return printInfo; - } - - public List GetAvatarModerations() - { - List moderations = new List(); - try - { - if (_vrchat != null) - { - moderations = _vrchat.Authentication.GetGlobalAvatarModerations(); - } - } - catch (Exception ex) - { - logger.Error($"Error fetching avatar moderations: {ex.Message}"); - } - return moderations; + return group; } + #endregion - public async Task BlockAvatarGlobal(string avatarId) + #region Moderation Management + internal async Task SubmitModerationReportAsync(ModerationReportPayload rpt) { try { if (_vrchat == null) { - logger.Info($"Failed Block avatar {avatarId} globally, not logged in."); + logger.Error("VRChat client not initialized"); return false; } // Create HTTP client with cookies - var handler = new HttpClientHandler - { - CookieContainer = new CookieContainer() - }; - - var cookies = _vrchat.GetCookies(); - foreach (var cookie in cookies) - { - handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); - } - - using HttpClient httpClient = new HttpClient(handler); - httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); - - AvatarModerationItem rpt = new AvatarModerationItem - { - TargetAvatarId = avatarId, - AvatarModerationType = "block" - }; + // Create HTTP client with cookies + using HttpClient httpClient = CreateHttpClientWithCookies(); - HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{URI_VRC_BASE_API}/api/1/auth/user/avatarmoderations?targetAvatarId={avatarId}&avatarModerationType=block", rpt); + // Submit the moderation report + HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{URI_VRC_BASE_API}/api/1/moderationReports", rpt); string responseContent = await response.Content.ReadAsStringAsync(); - logger.Debug($"Response from Block avatar {avatarId} globally: {responseContent}"); - logger.Info($"Submitted Block avatar {avatarId} globally."); + logger.Debug($"Response from submitting moderation report for content {rpt.ContentId}: {responseContent}"); + logger.Info($"Submitted moderation report for content {rpt.ContentId} with reason: {rpt.Reason}\n{responseContent}"); response.EnsureSuccessStatusCode(); return response.IsSuccessStatusCode; + } catch (Exception ex) { - logger.Error($"Error setting avatar moderation status: {ex.Message}"); + logger.Error(ex, $"Error Reporting image from URI: {rpt}"); + return false; } - - return false; } + #endregion - - public async Task DeleteAvatarGlobal(string avatarId) + #region Cookie Persistence + private static List? LoadCookies() { + string filePath = Path.Combine(CommonConst.APPLICATION_LOCAL_DATA_PATH, "cookies.json"); + + if (!System.IO.File.Exists(filePath)) + return null; + try { - if (_vrchat == null) - { - logger.Info($"Failed Unblock avatar {avatarId} globally, not logged in."); - return false; - } + var json = System.IO.File.ReadAllText(filePath); + var dtoList = JsonConvert.DeserializeObject>(json); + if (dtoList == null || dtoList.Count == 0) + return null; - // Create HTTP client with cookies - var handler = new HttpClientHandler + // If any cookie has an Expires value set and is expired, treat the whole set as invalid + DateTime now = DateTime.UtcNow; + foreach (var dto in dtoList) { - CookieContainer = new CookieContainer() - }; + logger.Debug($"Loaded cookie: {dto.Name}, Expires: {dto.Expires}"); - var cookies = _vrchat.GetCookies(); - foreach (var cookie in cookies) - { - handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); + if (dto.Expires != DateTime.MinValue && dto.Expires.ToUniversalTime() <= now) + { + return null; + } } - using HttpClient httpClient = new HttpClient(handler); - httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); - - HttpResponseMessage response = await httpClient.DeleteAsync($"{URI_VRC_BASE_API}/api/1/auth/user/avatarmoderations?targetAvatarId={avatarId}&avatarModerationType=block"); - string responseContent = await response.Content.ReadAsStringAsync(); - logger.Debug($"Response from Block avatar {avatarId} globally: {responseContent}"); - logger.Info($"Submitted Block avatar {avatarId} globally."); - response.EnsureSuccessStatusCode(); - - return response.IsSuccessStatusCode; + var cookies = dtoList.Select(d => d.ToCookie()).ToList(); + return cookies; } - catch (Exception ex) + catch { - logger.Error($"Error setting avatar moderation status: {ex.Message}"); + return null; } - - return false; } - - private static void SaveCookiesToFile(string filePath, List cookies) + private static void PersistCookies(List cookies) { + string filePath = Path.Combine(CommonConst.APPLICATION_LOCAL_DATA_PATH, "cookies.json"); + var dtoList = cookies.Select(c => SerializableCookie.FromCookie(c)).ToList(); var json = JsonConvert.SerializeObject(dtoList, Formatting.Indented); System.IO.File.WriteAllText(filePath, json); } - internal Group? getGroupById(string id) + private HttpClient CreateHttpClientWithCookies() { - Group? group = null; - try + if (_vrchat == null) { - if (_vrchat != null) - { - group = _vrchat.Groups.GetGroup(id); - } - } - catch (Exception ex) - { - logger.Error($"Error fetching Group information: {ex.Message}"); + logger.Error("VRChat client not initialized, cannot create HTTP client with cookies."); + throw new InvalidOperationException("VRChat client not initialized"); } - return group; - } - - internal async Task SubmitModerationReportAsync(ModerationReportPayload rpt) - { - try + var handler = new HttpClientHandler { - if (_vrchat == null) - { - logger.Error("VRChat client not initialized"); - return false; - } - - // Create HTTP client with cookies - var handler = new HttpClientHandler - { - CookieContainer = new CookieContainer() - }; - - var cookies = _vrchat.GetCookies(); - foreach (var cookie in cookies) - { - handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); - } - - using HttpClient httpClient = new HttpClient(handler); - httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); - - // Submit the moderation report - HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{URI_VRC_BASE_API}/api/1/moderationReports", rpt); - string responseContent = await response.Content.ReadAsStringAsync(); - logger.Debug($"Response from submitting moderation report for content {rpt.ContentId}: {responseContent}"); - logger.Info($"Submitted moderation report for content {rpt.ContentId} with reason: {rpt.Reason}\n{responseContent}"); - response.EnsureSuccessStatusCode(); + CookieContainer = new CookieContainer() + }; - return response.IsSuccessStatusCode; - - } - catch (Exception ex) + var cookies = _vrchat.GetCookies(); + foreach (var cookie in cookies) { - logger.Error(ex, $"Error Reporting image from URI: {rpt}"); - return false; + handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie); } + HttpClient httpClient = new(handler); + httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent); + + return httpClient; } + #endregion #region Non Public Helper Types private class SerializableCookie @@ -636,7 +601,7 @@ public class VRChatInventoryItem public string FirstAncestorHolderId { get; set; } = string.Empty; [JsonProperty("collections")] - public List Collections { get; set; } = new List(); + public List Collections { get; set; } = []; [JsonProperty("created_at")] public DateTime CreatedAt { get; set; } @@ -651,22 +616,22 @@ public class VRChatInventoryItem public DateTime TemplateUpdatedAt { get; set; } [JsonProperty("defaultAttributes")] - public Dictionary DefaultAttributes { get; set; } = new Dictionary(); + public Dictionary DefaultAttributes { get; set; } = []; [JsonProperty("userAttributes")] - public Dictionary UserAttributes { get; set; } = new Dictionary(); + public Dictionary UserAttributes { get; set; } = []; [JsonProperty("equipSlot")] public string EquipSlot { get; set; } = string.Empty; [JsonProperty("equipSlots")] - public List EquipSlots { get; set; } = new List(); + public List EquipSlots { get; set; } = []; [JsonProperty("expiryDate")] public DateTime? ExpiryDate { get; set; } [JsonProperty("flags")] - public List Flags { get; set; } = new List(); + public List Flags { get; set; } = []; [JsonProperty("isArchived")] public bool IsArchived { get; set; } @@ -681,7 +646,7 @@ public class VRChatInventoryItem public bool Quantifiable { get; set; } [JsonProperty("tags")] - public List Tags { get; set; } = new List(); + public List Tags { get; set; } = []; [JsonProperty("templateId")] public string TemplateId { get; set; } = string.Empty; @@ -726,7 +691,7 @@ public class ModerationReportPayload public string Description { get; set; } = string.Empty; [JsonProperty("details")] - public List Details { get; set; } = new List(); + public List Details { get; set; } = []; } public class ModerationReportDetails diff --git a/src/PlayerManagement/PlayerManagement.cs b/src/PlayerManagement/PlayerManagement.cs index 563fc53..59237af 100644 --- a/src/PlayerManagement/PlayerManagement.cs +++ b/src/PlayerManagement/PlayerManagement.cs @@ -853,7 +853,8 @@ internal async void AddPrintData(string printId) public Player? UpdatePlayerUserFromVRCProfile(User profile, string profileHash) { - if (profile != null) + logger.Warn($"Updating UserInfo for user {profile.DisplayName} (ID: {profile.Id}) with DateJoined: {profile.DateJoined} and ProfileHash: {profileHash}"); + if (profile != null && profile.Id != null) { TailgrabDBContext dbContext = serviceRegistry.GetDBContext(); Player? player = GetPlayerByUserId(profile.Id); @@ -903,7 +904,7 @@ internal async void AddPrintData(string printId) try { VRChatClient vrcClient = serviceRegistry.GetVRChatAPIClient(); - VRChat.API.Model.Group? group = vrcClient.getGroupById(groupId); + VRChat.API.Model.Group? group = vrcClient.GetGroupById(groupId); if (group != null) { TailgrabDBContext dbContext = serviceRegistry.GetDBContext(); From 5b7a3bf5d5ee8dfb727a32be512db45965365804 Mon Sep 17 00:00:00 2001 From: Jay Long Date: Fri, 13 Mar 2026 15:48:14 -0500 Subject: [PATCH 02/11] Refactor AvatarManagement to PlayerManager, Add Open Log Management --- BuildNumber.txt | 2 +- BuildVersion.txt | 1 + src/AvatarManagement/AvatarManagement.cs | 489 ------------ src/Clients/Ollama/Ollama.cs | 14 +- src/LineHandlers/OnPlayerNetworkHandler.cs | 3 +- src/LineHandlers/PenNetworkIdHandler.cs | 12 +- src/LineHandlers/QuitHandler.cs | 3 +- src/LineHandlers/StickerHandler.cs | 4 +- src/LineHandlers/VTKHandler.cs | 2 +- src/LineHandlers/WarnKickHandler.cs | 2 +- src/LineHandlers/WorldChangeHandler.cs | 5 +- src/PlayerManagement/PlayerManagement.cs | 751 +++++++++++++----- src/PlayerManagement/TailgrabPanel.xaml | 70 ++ src/PlayerManagement/TailgrabPanel.xaml.cs | 298 ++++--- src/Program.cs | 121 +-- src/ServiceRegistry.cs | 16 +- src/configuration/AvatarBosGistListManager.cs | 10 +- tailgrab.csproj | 30 +- tailgrab.csproj.user | 9 - tailgrab.sln | 21 +- 20 files changed, 963 insertions(+), 900 deletions(-) create mode 100644 BuildVersion.txt delete mode 100644 src/AvatarManagement/AvatarManagement.cs delete mode 100644 tailgrab.csproj.user diff --git a/BuildNumber.txt b/BuildNumber.txt index 1cb1a4d..cfa5180 100644 --- a/BuildNumber.txt +++ b/BuildNumber.txt @@ -1 +1 @@ -1988 +2022 diff --git a/BuildVersion.txt b/BuildVersion.txt new file mode 100644 index 0000000..28b8ef6 --- /dev/null +++ b/BuildVersion.txt @@ -0,0 +1 @@ +1.1.2 \ No newline at end of file diff --git a/src/AvatarManagement/AvatarManagement.cs b/src/AvatarManagement/AvatarManagement.cs deleted file mode 100644 index 321a90b..0000000 --- a/src/AvatarManagement/AvatarManagement.cs +++ /dev/null @@ -1,489 +0,0 @@ -using ConcurrentPriorityQueue.Core; -using Microsoft.EntityFrameworkCore; -using NLog; -using Polly; -using Tailgrab.Clients.Ollama; -using Tailgrab.Common; -using Tailgrab.Models; -using VRChat.API.Model; - -namespace Tailgrab.AvatarManagement -{ - public class AvatarManagementService - { - public static Logger logger = LogManager.GetCurrentClassLogger(); - - private ServiceRegistry _serviceRegistry; - - private ConcurrentPriorityQueue, int> priorityQueue = new ConcurrentPriorityQueue, int>(); - private Dictionary recentlyProcessedAvatars = new Dictionary(); - - public int GetQueueCount() - { - return priorityQueue.Count; - } - - public AvatarManagementService(ServiceRegistry serviceRegistry) - { - _serviceRegistry = serviceRegistry; - - _ = Task.Run(() => AvatarCheckTask(priorityQueue, _serviceRegistry)); - - } - - public void AddAvatar(AvatarInfo avatar) - { - try - { - _serviceRegistry.GetDBContext().AvatarInfos.Add(avatar); - _serviceRegistry.GetDBContext().SaveChanges(); - } - catch (Exception ex) - { - logger.Error($"Error creating avatar: {ex.Message}"); - } - } - - public AvatarInfo? GetAvatarById(string avatarId) - { - return _serviceRegistry.GetDBContext().AvatarInfos.Find(avatarId); - } - - public void UpdateAvatar(AvatarInfo avatar) - { - try - { - avatar.UpdatedAt = DateTime.UtcNow; - _serviceRegistry.GetDBContext().AvatarInfos.Update(avatar); - _serviceRegistry.GetDBContext().SaveChanges(); - } - catch (Exception ex) - { - logger.Error($"Error updating avatar: {ex.Message}"); - } - } - - public void DeleteAvatar(string avatarId) - { - var avatar = _serviceRegistry.GetDBContext().AvatarInfos.Find(avatarId); - if (avatar != null) - { - _serviceRegistry.GetDBContext().AvatarInfos.Remove(avatar); - _serviceRegistry.GetDBContext().SaveChanges(); - } - } - - public void CacheAvatars(List avatarIdInCache) - { - TailgrabDBContext dbContext = _serviceRegistry.GetDBContext(); - foreach (var avatarId in avatarIdInCache) - { - EnqueueAvatarForCheck(avatarId); - } - } - - private void EnqueueAvatarForCheck(string avatarId) - { - if (recentlyProcessedAvatars.TryGetValue(avatarId, out DateTime dateTime)) - { - if ((DateTime.UtcNow - dateTime).TotalMinutes < 60) - { - return; - } - } - recentlyProcessedAvatars.Add(avatarId, DateTime.UtcNow); - - var queuedItem = new QueuedAvatarProcess(5, avatarId); - - priorityQueue.Enqueue(queuedItem); - } - - public void EnqueueWatchAvatarForCheck(QueuedAvatarWatch watch) - { - priorityQueue.Enqueue(watch); - } - - public void EnqueueModeratedAvatarForCheck(QueuedModeratedAvatarWatch watch) - { - priorityQueue.Enqueue(watch); - } - - - public void GetAvatarsFromUser(string userId, string avatarName) - { - - logger.Debug($"Fetching avatars for user {userId} to find avatar named {avatarName}"); - - try - { - // Avatar already exists in the database and was updated within the last 12 hours - System.Threading.Thread.Sleep(500); - List avatarData = _serviceRegistry.GetVRChatAPIClient().GetAvatarsByUserId(userId); - foreach (var avatar in avatarData) - { - logger.Debug(avatar.ToString()); - if (avatar.Name.Equals(avatarName, StringComparison.OrdinalIgnoreCase)) - { - AvatarInfo? dbAvatarInfo = GetAvatarById(avatar.Id); - - if (dbAvatarInfo == null) - { - var avatarInfo = new AvatarInfo - { - AvatarId = avatar.Id, - UserId = avatar.AuthorId, - AvatarName = avatar.Name, - ImageUrl = avatar.ImageUrl, - CreatedAt = avatar.CreatedAt, - UpdatedAt = DateTime.UtcNow, - AlertType = AlertTypeEnum.None, - UserName = avatar.AuthorName - }; - - AddAvatar(avatarInfo); - } - else - { - dbAvatarInfo.UserId = avatar.AuthorId; - dbAvatarInfo.UserName = avatar.AuthorName; - dbAvatarInfo.AvatarName = avatar.Name; - dbAvatarInfo.ImageUrl = avatar.ImageUrl; - dbAvatarInfo.CreatedAt = avatar.CreatedAt; - UpdateAvatar(dbAvatarInfo); - } - } - } - } - catch (Exception ex) - { - logger.Error($"Error fetching avatar: {ex.Message}"); - } - } - - public void CompactDatabase() - { - _serviceRegistry.GetDBContext().Database.ExecuteSqlRaw("VACUUM;"); - } - - internal AvatarInfo? CheckAvatarByName(string avatarName) - { - var bannedAvatars = _serviceRegistry.GetDBContext().AvatarInfos - .Where(b => b.AvatarName != null && b.AvatarName.Equals(avatarName) && b.AlertType > 0) - .OrderByDescending(b => b.AlertType) - .ToList(); - - if (bannedAvatars.Count > 0) - { - // Play alert sound based on the highest alert type found for the avatar - AlertTypeEnum maxAlertType = bannedAvatars[0].AlertType; - SoundManager.PlayAlertSound(CommonConst.Avatar_Alert_Key, maxAlertType); - - return bannedAvatars[0]; - } - - return null; - } - - public static async Task AvatarCheckTask(ConcurrentPriorityQueue, int> priorityQueue, ServiceRegistry serviceRegistry) - { - OllamaClient.logger.Info($"Amplitude Avatar Cache Queue Running"); - TailgrabDBContext dBContext = serviceRegistry.GetDBContext(); - while (true) - { - // Process items from the priority queue - while (true) - { - var result = priorityQueue.Dequeue(); - if( result.IsSuccess ) - { - if (result.Value is QueuedAvatarProcess item && item.AvatarId != null) - { - await UpdateAmpAvatarRecord(serviceRegistry, dBContext, item.AvatarId); - } - else if (result.Value is QueuedAvatarWatch item2) - { - await UpdateWatchedAvatarRecord(serviceRegistry, dBContext, item2); - } - else if (result.Value is QueuedModeratedAvatarWatch item3) - { - await UpdateModeratedAvatarRecord(serviceRegistry, dBContext, item3); - } - } - else - { - // No more items to process - break; - } - } - // Wait for a short period before checking the queue again - await Task.Delay(5000); - } - } - - private static async Task UpdateAmpAvatarRecord(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, string avatarId) - { - try - { - AvatarInfo? dbAvatarInfo = dBContext.AvatarInfos.Find(avatarId); - bool updateNeeded = false; - if (dbAvatarInfo == null) - { - updateNeeded = true; - } - else if (dbAvatarInfo.AlertType == AlertTypeEnum.None && - (!dbAvatarInfo.UpdatedAt.HasValue || dbAvatarInfo.UpdatedAt.Value >= DateTime.UtcNow.AddHours(-2))) - { - updateNeeded = true; - } - - if (updateNeeded) - { - // Adds and Updates avatar info in the database, if it doesn't exist or was last updated more than 2 hours ago - Avatar? avatarData = FetchUpdateAvatarData(serviceRegistry, dBContext, avatarId, dbAvatarInfo); - - if (avatarData == null && dbAvatarInfo == null) - { - // Private Avatar - CreateAvatarInfoForPrivate(dBContext, avatarId); - } - - // Wait for a short period before checking the queue again - await Task.Delay(1000); - } - } - catch (Exception ex) - { - logger.Error(ex, $"Error fetching user profile for userId: {avatarId}"); - } - } - - - private static async Task UpdateModeratedAvatarRecord(ServiceRegistry _serviceRegistry, TailgrabDBContext dbContext, QueuedModeratedAvatarWatch watch) - { - try - { - // Fetch the AvatarInfo record - AvatarInfo? avatarInfo = await dbContext.AvatarInfos.FindAsync(watch.AvatarId); - AvatarManagementService.FetchUpdateAvatarData(_serviceRegistry, dbContext, watch.AvatarId, avatarInfo); - avatarInfo = await dbContext.AvatarInfos.FindAsync(watch.AvatarId); - - if (avatarInfo == null) - { - logger.Debug($"Line {watch.LineNumber}: Avatar ID '{watch.AvatarId}' not found in database/vrc, skipping."); - } - else if (avatarInfo.AlertType == AlertTypeEnum.None) - { - avatarInfo.AlertType = AlertTypeEnum.Nuisance; - avatarInfo.UpdatedAt = DateTime.UtcNow; - dbContext.AvatarInfos.Update(avatarInfo); - dbContext.SaveChanges(); - } - else - { - logger.Debug($"Line {watch.LineNumber}: Avatar ID '{watch.AvatarId}' already has Has an Alert, skipping."); - } - } - catch (Exception ex) - { - logger.Error(ex, $"Line {watch.LineNumber}: Error processing avatar ID '{watch.AvatarId}'"); - } - - // Throttle processing to avoid overwhelming the API - await Task.Delay(1000); - } - - private static async Task UpdateWatchedAvatarRecord(ServiceRegistry _serviceRegistry, TailgrabDBContext dbContext, QueuedAvatarWatch watch) - { - try - { - // Fetch the AvatarInfo record - AvatarInfo? avatarInfo = await dbContext.AvatarInfos.FindAsync(watch.AvatarId); - AvatarManagementService.FetchUpdateAvatarData(_serviceRegistry, dbContext, watch.AvatarId, avatarInfo); - avatarInfo = await dbContext.AvatarInfos.FindAsync(watch.AvatarId); - - if (avatarInfo == null) - { - logger.Debug($"Line {watch.LineNumber}: Avatar ID '{watch.AvatarId}' not found in database/vrc, skipping."); - } - else if (avatarInfo.AlertType == AlertTypeEnum.None) - { - - avatarInfo.AlertType = watch.AlertType; - avatarInfo.UpdatedAt = DateTime.UtcNow; - dbContext.AvatarInfos.Update(avatarInfo); - dbContext.SaveChanges(); - - if (avatarInfo.AlertType >= AlertTypeEnum.Nuisance) - { - await _serviceRegistry.GetVRChatAPIClient().BlockAvatarGlobal(avatarInfo.AvatarId); - } - else - { - await _serviceRegistry.GetVRChatAPIClient().DeleteAvatarGlobal(avatarInfo.AvatarId); - } - - logger.Debug($"Line {watch.LineNumber}: Set Watch State for Avatar ID '{watch.AvatarId}'"); - - } - else - { - logger.Debug($"Line {watch.LineNumber}: Avatar ID '{watch.AvatarId}' already has Has an Alert, skipping."); - } - } - catch (Exception ex) - { - logger.Error(ex, $"Line {watch.LineNumber}: Error processing avatar ID '{watch.AvatarId}'"); - } - - // Throttle processing to avoid overwhelming the API - await Task.Delay(3000); - } - - - private static void CreateAvatarInfoForPrivate(TailgrabDBContext dBContext, string AvatarId) - { - var avatarInfo = new AvatarInfo - { - AvatarId = AvatarId, - UserId = "", - AvatarName = $"Unknown Avatar {AvatarId}", - ImageUrl = "", - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }; - - try - { - dBContext.Add(avatarInfo); - dBContext.SaveChanges(); - logger.Debug($"Adding fallback avatar record for {avatarInfo.ToString()}"); - } - catch (Exception ex) - { - logger.Error($"Error adding fallback avatar record for {AvatarId}: {ex.Message}"); - } - } - - public static Avatar? FetchUpdateAvatarData(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, string AvatarId, AvatarInfo? dbAvatarInfo) - { - Avatar? avatarData = null; - try - { - // Avatar already exists in the database and was updated within the last 12 hours - System.Threading.Thread.Sleep(500); - avatarData = serviceRegistry.GetVRChatAPIClient().GetAvatarById(AvatarId); - if (avatarData != null) - { - if (dbAvatarInfo == null) - { - var avatarInfo = new AvatarInfo - { - AvatarId = avatarData.Id, - UserId = avatarData.AuthorId, - UserName = avatarData.AuthorName, - AvatarName = avatarData.Name, - ImageUrl = avatarData.ImageUrl, - CreatedAt = avatarData.CreatedAt, - UpdatedAt = DateTime.UtcNow - }; - - try - { - dBContext.Add(avatarInfo); - dBContext.SaveChanges(); - } - catch (Exception ex) - { - logger.Error($"Error adding avatar record for {AvatarId}: {ex.Message}"); - } - } - else - { - // Ensure entity is attached to the dbContext before updating to avoid Detached state errors - var entry = dBContext.Entry(dbAvatarInfo); - if (entry.State == Microsoft.EntityFrameworkCore.EntityState.Detached) - { - dBContext.Attach(dbAvatarInfo); - entry = dBContext.Entry(dbAvatarInfo); - } - - dbAvatarInfo.UserId = avatarData.AuthorId; - dbAvatarInfo.UserName = avatarData.AuthorName; - dbAvatarInfo.AvatarName = avatarData.Name; - dbAvatarInfo.ImageUrl = avatarData.ImageUrl; - dbAvatarInfo.CreatedAt = avatarData.CreatedAt; - dbAvatarInfo.UpdatedAt = DateTime.UtcNow; - - try - { - entry.State = Microsoft.EntityFrameworkCore.EntityState.Modified; - dBContext.SaveChanges(); - } - catch (Exception ex) - { - logger.Error($"Error updating avatar record for {AvatarId}: {ex.Message}"); - } - } - } - } - catch (Exception ex) - { - logger.Error($"Error fetching avatar: {ex.Message}"); - } - - return avatarData; - } - } - - internal class QueuedAvatarProcess : IHavePriority - { - public QueuedAvatarProcess(int priority, string avatarId) - { - Priority = priority; - AvatarId = avatarId; - } - - public int Priority { get; set; } - - public string AvatarId { get; set; } - } - - - public class QueuedAvatarWatch : IHavePriority - { - public QueuedAvatarWatch( int priority, string avatarId, AlertTypeEnum alertType, int lineNumber) - { - Priority = priority; - AvatarId = avatarId; - AlertType = alertType; - LineNumber = lineNumber; - } - - public int Priority { get; set; } - - public string AvatarId { get; set; } - - public AlertTypeEnum AlertType { get; set; } - - public int LineNumber { get; set; } - } - - public class QueuedModeratedAvatarWatch : IHavePriority - { - public QueuedModeratedAvatarWatch(int priority, string avatarId, AlertTypeEnum alertType, int lineNumber) - { - Priority = priority; - AvatarId = avatarId; - AlertType = alertType; - LineNumber = lineNumber; - } - - public int Priority { get; set; } - - public string AvatarId { get; set; } - - public AlertTypeEnum AlertType { get; set; } - - public int LineNumber { get; set; } - } -} diff --git a/src/Clients/Ollama/Ollama.cs b/src/Clients/Ollama/Ollama.cs index b6e58d4..3f4c06b 100644 --- a/src/Clients/Ollama/Ollama.cs +++ b/src/Clients/Ollama/Ollama.cs @@ -146,7 +146,7 @@ public static async Task ProfileCheckTask(ConcurrentPriorityQueue GetUserGroupInformation(ServiceRegistry serviceR { bool saveGroups = ConfigStore.GetStoredKeyBool(CommonConst.Registry_Discovered_Group_Caching, true); logger.Debug($"Processing User Group subscription for userId: {item.UserId}"); - Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(item.UserId ?? string.Empty); + Player? player = PlayerManager.GetPlayerByUserId(item.UserId ?? string.Empty); if (player != null) { AlertTypeEnum maxAlertType = AlertTypeEnum.None; @@ -201,7 +201,7 @@ private async static Task GetUserGroupInformation(ServiceRegistry serviceR if (groupInfo.AlertType > AlertTypeEnum.None) { - player = serviceRegistry.GetPlayerManager().AddPlayerEventByUserId(item.UserId ?? string.Empty, PlayerEvent.EventType.GroupWatch, $"User is member of group: {groupInfo.GroupName} with alert level {groupInfo.AlertType}"); + player = PlayerManager.AddPlayerEventByUserId(item.UserId ?? string.Empty, PlayerEvent.EventType.GroupWatch, $"User is member of group: {groupInfo.GroupName} with alert level {groupInfo.AlertType}"); player?.AddAlertMessage(AlertClassEnum.Group, groupInfo.AlertType, groupInfo.GroupName); maxAlertType = maxAlertType < groupInfo.AlertType ? groupInfo.AlertType : maxAlertType; } @@ -245,7 +245,7 @@ await ollamaApi.GenerateAsync(request).StreamToEndAsync(responseTask => dBContext.Add(evaluation); dBContext.SaveChanges(); - Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(item.UserId ?? string.Empty); + Player? player = PlayerManager.GetPlayerByUserId(item.UserId ?? string.Empty); if (player != null) { player.UserBio = item.UserBio; @@ -270,7 +270,7 @@ private static void GetEvaluationFromStore(ServiceRegistry serviceRegistry, Prof if (item != null && item.UserId != null) { - Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(item.UserId); + Player? player = PlayerManager.GetPlayerByUserId(item.UserId); if (player != null) { player.AIEval = System.Text.Encoding.UTF8.GetString(evaluated.Evaluation); @@ -314,10 +314,10 @@ private static void ProfileViewUpdate(ServiceRegistry serviceRegistry, Player pl break; } - serviceRegistry.GetPlayerManager().AddPlayerEventByUserId(player.UserId ?? string.Empty, + PlayerManager.AddPlayerEventByUserId(player.UserId ?? string.Empty, PlayerEvent.EventType.ProfileWatch, $"User profile was flagged by the AI : {profileWatch}"); } - serviceRegistry.GetPlayerManager().OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); + PlayerManager.OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); } private static string? EvaluateProfile(string? profileText) diff --git a/src/LineHandlers/OnPlayerNetworkHandler.cs b/src/LineHandlers/OnPlayerNetworkHandler.cs index 5a9d42d..0b30e1f 100644 --- a/src/LineHandlers/OnPlayerNetworkHandler.cs +++ b/src/LineHandlers/OnPlayerNetworkHandler.cs @@ -1,6 +1,7 @@ namespace Tailgrab.LineHandler; using System.Text.RegularExpressions; +using Tailgrab.PlayerManagement; public class OnPlayerNetworkHandler : AbstractLineHandler { @@ -31,7 +32,7 @@ public override bool HandleLine(string line) } ExecuteActions(); - _serviceRegistry.GetPlayerManager().AssignPlayerNetworkId(userName, networkId); + PlayerManager.AssignPlayerNetworkId(userName, networkId); return true; } diff --git a/src/LineHandlers/PenNetworkIdHandler.cs b/src/LineHandlers/PenNetworkIdHandler.cs index 827a0f8..631d6b9 100644 --- a/src/LineHandlers/PenNetworkIdHandler.cs +++ b/src/LineHandlers/PenNetworkIdHandler.cs @@ -59,17 +59,17 @@ public override bool HandleLine(string line) { string fromPlayerName = "Unknown"; string toPlayerName = "Unknown"; - if (_serviceRegistry.GetPlayerManager().GetPlayerByNetworkId(fromUserId) is Player fromPlayer) + if (PlayerManager.GetPlayerByNetworkId(fromUserId) is Player fromPlayer) { fromPlayerName = fromPlayer.DisplayName; - _serviceRegistry.GetPlayerManager().AddPenEventByDisplayName(fromPlayerName, $"-Pen '{penColor}'."); - _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(fromPlayerName, PlayerEvent.EventType.PenActivity, $"Lost ownership of pen '{penColor}'."); + PlayerManager.AddPenEventByDisplayName(fromPlayerName, $"-Pen '{penColor}'."); + PlayerManager.AddPlayerEventByDisplayName(fromPlayerName, PlayerEvent.EventType.PenActivity, $"Lost ownership of pen '{penColor}'."); } - if (_serviceRegistry.GetPlayerManager().GetPlayerByNetworkId(toUserId) is Player toPlayer) + if (PlayerManager.GetPlayerByNetworkId(toUserId) is Player toPlayer) { toPlayerName = toPlayer.DisplayName; - _serviceRegistry.GetPlayerManager().AddPenEventByDisplayName(toPlayerName, $"+Pen '{penColor}'."); - _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(toPlayerName, PlayerEvent.EventType.PenActivity, $"Took ownership of pen '{penColor}'."); + PlayerManager.AddPenEventByDisplayName(toPlayerName, $"+Pen '{penColor}'."); + PlayerManager.AddPlayerEventByDisplayName(toPlayerName, PlayerEvent.EventType.PenActivity, $"Took ownership of pen '{penColor}'."); } if (LogOutput) { diff --git a/src/LineHandlers/QuitHandler.cs b/src/LineHandlers/QuitHandler.cs index 7be5eca..071cdf3 100644 --- a/src/LineHandlers/QuitHandler.cs +++ b/src/LineHandlers/QuitHandler.cs @@ -2,6 +2,7 @@ namespace Tailgrab.LineHandler; using System.Text.RegularExpressions; using Tailgrab.Common; +using Tailgrab.PlayerManagement; public class QuitHandler : AbstractLineHandler { @@ -35,7 +36,7 @@ public override bool HandleLine(string line) logger.Info($"{COLOR_PREFIX}Application Stop : {time} {COLOR_RESET.GetAnsiEscape()}"); } - _serviceRegistry.GetPlayerManager().ClearAllPlayers(this); + PlayerManager.ClearAllPlayers(this); ExecuteActions(); return true; diff --git a/src/LineHandlers/StickerHandler.cs b/src/LineHandlers/StickerHandler.cs index be9937b..bc61e68 100644 --- a/src/LineHandlers/StickerHandler.cs +++ b/src/LineHandlers/StickerHandler.cs @@ -2,6 +2,7 @@ namespace Tailgrab.LineHandler; using System.Text.RegularExpressions; using Tailgrab.Common; +using Tailgrab.PlayerManagement; public class StickerHandler : AbstractLineHandler { @@ -24,7 +25,6 @@ public override bool HandleLine(string line) Match m = regex.Match(line); if (m.Success) { - string timestamp = m.Groups[VRC_DATETIME].Value; string fileURL = m.Groups[VRC_FILEURL].Value; string userName = m.Groups[VRC_DISPLAYNAME].Value; string userId = m.Groups[VRC_USERID].Value; @@ -32,7 +32,7 @@ public override bool HandleLine(string line) { logger.Info($"{COLOR_PREFIX}{userName} ({userId}) - {fileURL}{COLOR_RESET.GetAnsiEscape()}"); } - _serviceRegistry.GetPlayerManager().AddStickerEvent(userName, userId, fileURL); + PlayerManager.AddStickerEvent(userName, fileURL); ExecuteActions(); return true; diff --git a/src/LineHandlers/VTKHandler.cs b/src/LineHandlers/VTKHandler.cs index 6f84b48..7fd806e 100644 --- a/src/LineHandlers/VTKHandler.cs +++ b/src/LineHandlers/VTKHandler.cs @@ -30,7 +30,7 @@ public override bool HandleLine(string line) logger.Info($"{COLOR_PREFIX}VTK : {userName}{COLOR_RESET.GetAnsiEscape()}"); } - Player? player = _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(userName, PlayerEvent.EventType.Moderation, "Vote kick initiated against player."); + Player? player = PlayerManager.AddPlayerEventByDisplayName(userName, PlayerEvent.EventType.Moderation, "Vote kick initiated against player."); if (player != null) { player.AddAlertMessage(AlertClassEnum.Profile, AlertTypeEnum.Nuisance, $"VTK"); diff --git a/src/LineHandlers/WarnKickHandler.cs b/src/LineHandlers/WarnKickHandler.cs index e3280bd..9ecadc4 100644 --- a/src/LineHandlers/WarnKickHandler.cs +++ b/src/LineHandlers/WarnKickHandler.cs @@ -32,7 +32,7 @@ public override bool HandleLine(string line) logger.Info($"{COLOR_PREFIX}User Moderation : {userName} to {action}{COLOR_RESET.GetAnsiEscape()}"); } - Player? player = _serviceRegistry.GetPlayerManager().AddPlayerEventByDisplayName(userName, PlayerEvent.EventType.Moderation, $"User has been {action}."); + Player? player = PlayerManager.AddPlayerEventByDisplayName(userName, PlayerEvent.EventType.Moderation, $"User has been {action}."); if (player != null) { player.AddAlertMessage(AlertClassEnum.Profile, AlertTypeEnum.Nuisance, action); diff --git a/src/LineHandlers/WorldChangeHandler.cs b/src/LineHandlers/WorldChangeHandler.cs index ba97812..8b22405 100644 --- a/src/LineHandlers/WorldChangeHandler.cs +++ b/src/LineHandlers/WorldChangeHandler.cs @@ -2,6 +2,7 @@ namespace Tailgrab.LineHandler; using System.Text.RegularExpressions; using Tailgrab.Common; +using Tailgrab.PlayerManagement; public class WorldChangeHandler : AbstractLineHandler { @@ -31,8 +32,8 @@ public override bool HandleLine(string line) logger.Info($"{COLOR_PREFIX}World Join : {worldId} as instance {instanceId}{COLOR_RESET.GetAnsiEscape()}"); } - _serviceRegistry.GetPlayerManager().UpdateCurrentSession(worldId, instanceId); - _serviceRegistry.GetPlayerManager().ClearAllPlayers(this); + PlayerManager.UpdateCurrentSession(worldId, instanceId); + PlayerManager.ClearAllPlayers(this); ExecuteActions(); return true; diff --git a/src/PlayerManagement/PlayerManagement.cs b/src/PlayerManagement/PlayerManagement.cs index 59237af..e0aa263 100644 --- a/src/PlayerManagement/PlayerManagement.cs +++ b/src/PlayerManagement/PlayerManagement.cs @@ -1,7 +1,9 @@ +using ConcurrentPriorityQueue.Core; +using Microsoft.EntityFrameworkCore; using NLog; using System.ComponentModel; using System.Text; -using Tailgrab.AvatarManagement; +using Tailgrab.Clients.Ollama; using Tailgrab.Clients.VRChat; using Tailgrab.Common; using Tailgrab.LineHandler; @@ -10,7 +12,7 @@ namespace Tailgrab.PlayerManagement { - public class PlayerEvent + public class PlayerEvent(PlayerEvent.EventType type, string eventDescription) { public enum EventType { @@ -28,88 +30,48 @@ public enum EventType } public DateTime EventTime { get; set; } = DateTime.Now; - public EventType Type { get; set; } - public string EventDescription { get; set; } - - public PlayerEvent(EventType type, string eventDescription) - { - Type = type; - EventDescription = eventDescription; - } + public EventType Type { get; set; } = type; + public string EventDescription { get; set; } = eventDescription; } - public class PlayerInventory + public class PlayerInventory(string inventoryId, string itemName, string itemUrl, string inventoryType, string aIEvaluation) { - public string InventoryId { get; set; } - public string ItemName { get; set; } - public string ItemUrl { get; set; } - public string InventoryType { get; set; } - public string AIEvaluation { get; set; } - public DateTime SpawnedAt { get; set; } - public PlayerInventory(string inventoryId, string itemName, string itemUrl, string inventoryType, string aIEvaluation) - { - InventoryId = inventoryId; - ItemName = itemName; - SpawnedAt = DateTime.Now; - ItemUrl = itemUrl; - InventoryType = inventoryType; - AIEvaluation = aIEvaluation; - } + public string InventoryId { get; set; } = inventoryId; + public string ItemName { get; set; } = itemName; + public string ItemUrl { get; set; } = itemUrl; + public string InventoryType { get; set; } = inventoryType; + public string AIEvaluation { get; set; } = aIEvaluation; + public DateTime SpawnedAt { get; set; } = DateTime.Now; } - public class PlayerPrint + public class PlayerPrint(VRChat.API.Model.Print p, string aiEvaluation, string aiClassification) { - public string PrintId { get; set; } - public string OwnerId { get; set; } - public DateTime Timestamp { get; set; } - public DateTime CreatedAt { get; set; } - public string PrintUrl { get; set; } - public string AIEvaluation { get; set; } - public string AIClass { get; set; } - public string AuthorName { get; set; } - - public PlayerPrint(VRChat.API.Model.Print p, string aiEvaluation, string aiClassification) - { - PrintId = p.Id; - OwnerId = p.OwnerId; - Timestamp = DateTime.Now; - CreatedAt = p.CreatedAt; - PrintUrl = p.Files.Image; - AuthorName = p.AuthorName; - AIEvaluation = aiEvaluation; - AIClass = aiClassification; - } + public string PrintId { get; set; } = p.Id; + public string OwnerId { get; set; } = p.OwnerId; + public DateTime Timestamp { get; set; } = DateTime.Now; + public DateTime CreatedAt { get; set; } = p.CreatedAt; + public string PrintUrl { get; set; } = p.Files.Image; + public string AIEvaluation { get; set; } = aiEvaluation; + public string AIClass { get; set; } = aiClassification; + public string AuthorName { get; set; } = p.AuthorName; } - public class AlertMessage + public class AlertMessage(AlertClassEnum alertClass, AlertTypeEnum alertType, string color, string message) { - public AlertClassEnum AlertClass { get; set; } - public AlertTypeEnum AlertType { get; set; } - public string Color { get; set; } - public string Message { get; set; } - public DateTime Timestamp { get; set; } - public AlertMessage(AlertClassEnum alertClass, AlertTypeEnum alertType, string color, string message) - { - AlertClass = alertClass; - AlertType = alertType; - Color = color; - Message = message; - Timestamp = DateTime.Now; - } + public AlertClassEnum AlertClass { get; set; } = alertClass; + public AlertTypeEnum AlertType { get; set; } = alertType; + public string Color { get; set; } = color; + public string Message { get; set; } = message; + public DateTime Timestamp { get; set; } = DateTime.Now; } - public class PlayerAvatar + public class PlayerAvatar(string avatarName, string createdBy) { - public string AvatarName { get; set; } - public string? CreatedBy { get; set; } - public PlayerAvatar(string avatarName, string createdBy) - { - AvatarName = avatarName; - CreatedBy = createdBy; - } + public string AvatarName { get; set; } = avatarName; + public string? CreatedBy { get; set; } = createdBy; } - public class Player : INotifyPropertyChanged + public class Player(string userId, string displayName, SessionInfo session) : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; @@ -118,23 +80,23 @@ protected virtual void OnPropertyChanged(string propertyName) PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - public string UserId { get; set; } - public string DisplayName { get; set; } - public string AvatarName { get; set; } - public string PenActivity { get; set; } + public string UserId { get; set; } = userId; + public string DisplayName { get; set; } = displayName; + public string AvatarName { get; set; } = ""; + public string PenActivity { get; set; } = ""; public int NetworkId { get; set; } - public DateTime InstanceStartTime { get; set; } + public DateTime InstanceStartTime { get; set; } = DateTime.Now; public DateTime? InstanceEndTime { get; set; } - public List Events { get; set; } = new List(); - public List Inventory { get; set; } = new List(); - public SessionInfo Session { get; set; } + public List Events { get; set; } = []; + public List Inventory { get; set; } = []; + public SessionInfo Session { get; set; } = session; public string? LastStickerUrl { get; set; } = string.Empty; - public Dictionary PrintData = new Dictionary(); + public Dictionary PrintData = []; public string? UserBio { get; set; } public string? AIEval { get; set; } - public List _AlertMessage = new List(); + public List _AlertMessage = []; public string AlertMessage { @@ -231,7 +193,7 @@ public bool IsWatched { get { - if (_AlertMessage.Count() > 0) + if (_AlertMessage.Count > 0) { return true; } @@ -256,21 +218,10 @@ public bool IsFriend { _isFriend = value; } } - public Player(string userId, string displayName, SessionInfo session) - { - UserId = userId; - DisplayName = displayName; - AvatarName = ""; - PenActivity = ""; - InstanceStartTime = DateTime.Now; - Session = session; - } - - public void AddAlertMessage(AlertClassEnum alertClass, AlertTypeEnum alertType, string message) { string alertColor = PlayerManager.GetAlertColor(alertClass, alertType); - AlertMessage newAlert = new AlertMessage(alertClass, alertType, alertColor, message); + AlertMessage newAlert = new (alertClass, alertType, alertColor, message); _AlertMessage.Add(newAlert); foreach (AlertMessage alert in _AlertMessage) @@ -298,7 +249,7 @@ public override string ToString() public string ToString(bool full) { - StringBuilder sb = new System.Text.StringBuilder(); + StringBuilder sb = new (); sb.AppendLine($"DisplayName: {DisplayName}"); sb.AppendLine($"UserId: {UserId}"); sb.AppendLine($"Current Avatar Name: {(string.IsNullOrEmpty(AvatarName) ? string.Empty : AvatarName)}"); @@ -345,20 +296,14 @@ public string ToString(bool full) } } - public class SessionInfo + public class SessionInfo(string worldId, string instanceId) { - public string WorldId { get; set; } - public string InstanceId { get; set; } - public DateTime startDateTime { get; } = DateTime.Now; - public SessionInfo(string worldId, string instanceId) - { - WorldId = worldId; - InstanceId = instanceId; - startDateTime = DateTime.Now; - } + public string WorldId { get; set; } = worldId; + public string InstanceId { get; set; } = instanceId; + public DateTime StartDateTime { get; } = DateTime.Now; } - public class PlayerChangedEventArgs : EventArgs + public class PlayerChangedEventArgs(PlayerChangedEventArgs.ChangeType type, Player player) : EventArgs { public enum ChangeType { @@ -368,41 +313,39 @@ public enum ChangeType Cleared } - public ChangeType Type { get; } - public Player Player { get; } - - public PlayerChangedEventArgs(ChangeType type, Player player) - { - Type = type; - Player = player; - } + public ChangeType Type { get; } = type; + public Player Player { get; } = player; } public class PlayerManager { - private ServiceRegistry serviceRegistry; + private static ServiceRegistry serviceRegistry; + public PlayerManager(ServiceRegistry registry) + { + serviceRegistry = registry; + } - private static Dictionary playersByUserId = new Dictionary(); - private static Dictionary userIdByNetworkId = new Dictionary(); - private static Dictionary userIdByDisplayName = new Dictionary(); - private static Dictionary avatarByDisplayName = new Dictionary(); - private static Dictionary PlayerAvatarByName = new Dictionary(); + private static Dictionary playersByUserId = []; + private static Dictionary userIdByNetworkId = []; + private static Dictionary userIdByDisplayName = []; + private static Dictionary avatarByDisplayName = []; + private static Dictionary PlayerAvatarByName = []; + public static SessionInfo CurrentSession = new("", ""); public static readonly AnsiColor COLOR_PREFIX_LEAVE = AnsiColor.Yellow; public static readonly AnsiColor COLOR_PREFIX_JOIN = AnsiColor.Green; public static readonly AnsiColor COLOR_RESET = AnsiColor.Reset; - public static Logger logger = LogManager.GetCurrentClassLogger(); - public static SessionInfo CurrentSession = new SessionInfo("", ""); + protected static readonly Logger logger = LogManager.GetCurrentClassLogger(); // Event for UI and other listeners public static event EventHandler? PlayerChanged; - public PlayerManager(ServiceRegistry registry) - { - serviceRegistry = registry; - } + private static ConcurrentPriorityQueue, int> priorityQueue = new(); + private static Dictionary recentlyProcessedAvatars = []; + + - public Player? GetPlayerByDisplayName(string displayName) + public static Player? GetPlayerByDisplayName(string displayName) { if (userIdByDisplayName.TryGetValue(displayName, out string? userId)) { @@ -411,7 +354,7 @@ public PlayerManager(ServiceRegistry registry) return null; } - public Player? GetPlayerByNetworkId(int networkId) + public static Player? GetPlayerByNetworkId(int networkId) { if (userIdByNetworkId.TryGetValue(networkId, out string? userId)) { @@ -420,14 +363,14 @@ public PlayerManager(ServiceRegistry registry) return null; } - public Player? GetPlayerByUserId(string userId) + public static Player? GetPlayerByUserId(string userId) { playersByUserId.TryGetValue(userId, out Player? player); return player; } - public void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, Player player) + public static void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, Player player) { try { @@ -439,7 +382,7 @@ public void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, Player } } - public void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, string displayName) + public static void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, string displayName) { try { @@ -455,15 +398,15 @@ public void OnPlayerChanged(PlayerChangedEventArgs.ChangeType changeType, string } } - public void UpdateCurrentSession(string worldId, string instanceId) + public static void UpdateCurrentSession(string worldId, string instanceId) { CurrentSession = new SessionInfo(worldId, instanceId); } public void PlayerJoined(string userId, string displayName, AbstractLineHandler handler) { - Player? player = null; - if (!playersByUserId.ContainsKey(userId)) + Player? player; + if (!playersByUserId.TryGetValue(userId, out Player? value)) { player = new Player(userId, displayName, CurrentSession); if (handler.LogOutput) @@ -474,7 +417,7 @@ public void PlayerJoined(string userId, string displayName, AbstractLineHandler else { // If existing, treat as update (display name may have changed etc.) - player = playersByUserId[userId]; + player = value; if (player.DisplayName != displayName) { // remove old display-name mapping if present @@ -527,12 +470,14 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler) UserInfo? user = dBContext.UserInfos.Find(player.UserId); if (user == null) { - user = new UserInfo(); - user.DisplayName = player.DisplayName; - user.UserId = player.UserId; - user.CreatedAt = DateTime.Now; - user.UpdatedAt = DateTime.Now; - user.ElapsedMinutes = timeDifference.TotalMinutes; + user = new UserInfo + { + DisplayName = player.DisplayName, + UserId = player.UserId, + CreatedAt = DateTime.Now, + UpdatedAt = DateTime.Now, + ElapsedMinutes = timeDifference.TotalMinutes + }; dBContext.Add(user); dBContext.SaveChanges(); } @@ -540,7 +485,7 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler) { user.DisplayName = player.DisplayName; user.UpdatedAt = DateTime.Now; - user.ElapsedMinutes = user.ElapsedMinutes + timeDifference.TotalMinutes; + user.ElapsedMinutes += timeDifference.TotalMinutes; dBContext.Update(user); dBContext.SaveChanges(); } @@ -559,7 +504,7 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler) } } - public Player? AssignPlayerNetworkId(string displayName, int networkId) + public static Player? AssignPlayerNetworkId(string displayName, int networkId) { Player? player = GetPlayerByDisplayName(displayName); if (player != null) @@ -571,12 +516,12 @@ public void PlayerLeft(string displayName, AbstractLineHandler handler) return player; } - public IEnumerable GetAllPlayers() + public static IEnumerable GetAllPlayers() { return playersByUserId.Values; } - public void ClearAllPlayers(AbstractLineHandler handler) + public static void ClearAllPlayers(AbstractLineHandler handler) { foreach (var player in playersByUserId.Values) { @@ -598,12 +543,12 @@ public void ClearAllPlayers(AbstractLineHandler handler) OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Cleared, new Player("", "", CurrentSession) { InstanceStartTime = DateTime.MinValue }); } - public int GetPlayerCount() + public static int GetPlayerCount() { return playersByUserId.Count; } - public void LogAllPlayers(AbstractLineHandler handler) + public static void LogAllPlayers(AbstractLineHandler handler) { if (handler.LogOutput) { @@ -614,7 +559,7 @@ public void LogAllPlayers(AbstractLineHandler handler) } } - public Player? AddPlayerEventByDisplayName(string displayName, PlayerEvent.EventType eventType, string eventDescription) + public static Player? AddPlayerEventByDisplayName(string displayName, PlayerEvent.EventType eventType, string eventDescription) { if (userIdByDisplayName.TryGetValue(displayName, out string? userId)) @@ -625,11 +570,11 @@ public void LogAllPlayers(AbstractLineHandler handler) return null; } - public Player? AddPlayerEventByUserId(string userId, PlayerEvent.EventType eventType, string eventDescription) + public static Player? AddPlayerEventByUserId(string userId, PlayerEvent.EventType eventType, string eventDescription) { if (playersByUserId.TryGetValue(userId, out Player? player)) { - PlayerEvent newEvent = new PlayerEvent(eventType, eventDescription); + PlayerEvent newEvent = new(eventType, eventDescription); player.AddEvent(newEvent); return player; } @@ -644,13 +589,13 @@ public void SetAvatarForPlayer(string displayName, string avatarName) Player? player = AddPlayerEventByDisplayName(displayName, PlayerEvent.EventType.AvatarWatch, $"User switched to Avatar : {avatarName}"); ; if (player != null) { - AvatarInfo? watchedAvatar = serviceRegistry.GetAvatarManager().CheckAvatarByName(avatarName); + AvatarInfo? watchedAvatar = PlayerManager.CheckAvatarByName(avatarName); if (watchedAvatar != null) { - logger.Info($"{COLOR_PREFIX_LEAVE.GetAnsiEscape()}Watched Avatar Detected for Player {displayName}: {avatarName} with AlertType {watchedAvatar.AlertType.ToString()}{COLOR_RESET.GetAnsiEscape()}"); + logger.Info($"{COLOR_PREFIX_LEAVE.GetAnsiEscape()}Watched Avatar Detected for Player {displayName}: {avatarName} with AlertType {watchedAvatar.AlertType}{COLOR_RESET.GetAnsiEscape()}"); if (watchedAvatar.AlertType > AlertTypeEnum.None) { - player = AddPlayerEventByDisplayName(displayName, PlayerEvent.EventType.AvatarWatch, $"User has used a watched Avatar : {avatarName} alertType: {watchedAvatar.AlertType.ToString()}"); + player = AddPlayerEventByDisplayName(displayName, PlayerEvent.EventType.AvatarWatch, $"User has used a watched Avatar : {avatarName} alertType: {watchedAvatar.AlertType}"); player?.AddAlertMessage(AlertClassEnum.Avatar, watchedAvatar.AlertType, $"{avatarName}"); } } @@ -662,15 +607,7 @@ public void SetAvatarForPlayer(string displayName, string avatarName) } } - public void UnpackAvatar(string avatarName, string uploadedBy) - { - PlayerAvatar playerAvatar = UpdatePlayerAvatar(avatarName, uploadedBy); - - // - // Check Avatar IDs - } - - public PlayerAvatar UpdatePlayerAvatar(string avatarName, string uploadedBy) + public static PlayerAvatar UpdatePlayerAvatar(string avatarName, string uploadedBy) { if (PlayerAvatarByName.TryGetValue(avatarName, out PlayerAvatar? playerAvatar)) @@ -686,12 +623,12 @@ public PlayerAvatar UpdatePlayerAvatar(string avatarName, string uploadedBy) return playerAvatar; } - private void PrintPlayerInfo(Player player) + private static void PrintPlayerInfo(Player player) { - logger.Info($"{COLOR_PREFIX_LEAVE.GetAnsiEscape()}Player Left: \n{player.ToString()}{COLOR_RESET.GetAnsiEscape()}"); + logger.Info($"{COLOR_PREFIX_LEAVE.GetAnsiEscape()}Player Left: \n{player}{COLOR_RESET.GetAnsiEscape()}"); } - internal void AddPenEventByDisplayName(string displayName, string eventText) + internal static void AddPenEventByDisplayName(string displayName, string eventText) { Player? player = GetPlayerByDisplayName(displayName); if (player != null) @@ -711,8 +648,6 @@ internal async void AddInventorySpawn(string userId, string inventoryId) string itemContent = ""; string inventoryType = "Unknown Type"; string aiClassification = "OK"; - string evaluatedText = "Not Evaluated"; - try { var inventoryItem = await serviceRegistry.GetVRChatAPIClient()?.GetUserInventoryItem(userId, inventoryId)!; @@ -736,10 +671,10 @@ internal async void AddInventorySpawn(string userId, string inventoryId) var ollamaClient = serviceRegistry.GetOllamaAPIClient(); if (ollamaClient != null) { - ImageEvaluation? evaluated = await ollamaClient.ClassifyImageList(userId, inventoryId, new List { itemUrl, itemContent }); + ImageEvaluation? evaluated = await ollamaClient.ClassifyImageList(userId, inventoryId, [itemUrl, itemContent]); if (evaluated != null) { - evaluatedText = System.Text.Encoding.UTF8.GetString(evaluated.Evaluation); + string evaluatedText = System.Text.Encoding.UTF8.GetString(evaluated.Evaluation); aiClassification = EvaluateImageClass(evaluatedText) ?? "OK"; logger.Info($"Ollama classification for inventory item {inventoryId}: {aiClassification}: {evaluatedText}"); if (!aiClassification.Equals("OK") && !evaluated.IsIgnored) @@ -750,7 +685,7 @@ internal async void AddInventorySpawn(string userId, string inventoryId) } } - PlayerInventory inventory = new PlayerInventory(inventoryId, itemName, itemContent, inventoryType, aiClassification); + PlayerInventory inventory = new(inventoryId, itemName, itemContent, inventoryType, aiClassification); player.Inventory.Add(inventory); AddPlayerEventByUserId(userId, PlayerEvent.EventType.Emoji, $"Spawned Item: {itemName} ({inventoryId})"); @@ -783,7 +718,7 @@ internal async void AddInventorySpawn(string userId, string inventoryId) } private static bool CheckLines(string input, string knownString) { - string[] lines = input.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + string[] lines = input.Split(['\n'], StringSplitOptions.RemoveEmptyEntries); if (lines.Length < 2) { @@ -795,7 +730,7 @@ private static bool CheckLines(string input, string knownString) return firstLineContains; } - internal void AddStickerEvent(string displayName, string userId, string fileURL) + internal static void AddStickerEvent(string displayName, string fileURL) { Player? player = GetPlayerByDisplayName(displayName); if (player != null) @@ -806,11 +741,6 @@ internal void AddStickerEvent(string displayName, string userId, string fileURL) } } - internal void CompactDatabase() - { - serviceRegistry.GetAvatarManager().CompactDatabase(); - } - internal async void AddPrintData(string printId) { if (serviceRegistry.GetVRChatAPIClient() != null) @@ -822,15 +752,14 @@ internal async void AddPrintData(string printId) if (player != null) { logger.Info($"Fetched print info for print {printId} owned by {player.DisplayName} (ID: {printInfo.OwnerId})"); - ImageEvaluation? evaluated = null; string evaluatedText = "Not Evaluated"; string aiClassification = "OK"; var ollamaClient = serviceRegistry.GetOllamaAPIClient(); if (ollamaClient != null) { - List imageUrls = new List(); + List imageUrls = []; imageUrls.Add(printInfo.Files.Image); - evaluated = await ollamaClient.ClassifyImageList(printInfo.OwnerId, printInfo.Id, imageUrls); + ImageEvaluation? evaluated = await ollamaClient.ClassifyImageList(printInfo.OwnerId, printInfo.Id, imageUrls); if (evaluated != null) { evaluatedText = System.Text.Encoding.UTF8.GetString(evaluated.Evaluation); @@ -875,13 +804,15 @@ internal async void AddPrintData(string printId) } else { - user = new UserInfo(); - user.DisplayName = profile.DisplayName; - user.UserId = profile.Id; - user.CreatedAt = DateTime.UtcNow; - user.UpdatedAt = DateTime.UtcNow; - user.DateJoined = profile.DateJoined; - user.LastProfileChecksum = profileHash; + user = new UserInfo + { + DisplayName = profile.DisplayName, + UserId = profile.Id, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + DateJoined = profile.DateJoined, + LastProfileChecksum = profileHash + }; dbContext.Add(user); } dbContext.SaveChanges(); @@ -911,7 +842,7 @@ internal async void AddPrintData(string printId) GroupInfo? existing = dbContext.GroupInfos.Find(group.Id); if (existing == null) { - GroupInfo newEntity = new GroupInfo + GroupInfo newEntity = new() { GroupId = group.Id, GroupName = group.Name ?? string.Empty, @@ -964,8 +895,8 @@ public void SyncAvatarModerations() AvatarInfo? existingAvatar = dBContext.AvatarInfos.Find(mod.TargetAvatarId); if (existingAvatar == null || existingAvatar.AlertType < AlertTypeEnum.Nuisance ) { - QueuedModeratedAvatarWatch watchItem = new QueuedModeratedAvatarWatch(2, mod.TargetAvatarId, AlertTypeEnum.Nuisance, lineNumber); - serviceRegistry.GetAvatarManager().EnqueueModeratedAvatarForCheck(watchItem); + QueuedModeratedAvatarWatch watchItem = new(2, mod.TargetAvatarId, AlertTypeEnum.Nuisance, lineNumber); + EnqueueModeratedAvatarForCheck(watchItem); } } } @@ -996,5 +927,447 @@ public static string GetAlertColor(AlertClassEnum alertClass, AlertTypeEnum aler } #endregion + + + #region Avatar Management + public static int GetQueueCount() + { + return priorityQueue.Count; + } + + public void AddAvatar(AvatarInfo avatar) + { + try + { + serviceRegistry.GetDBContext().AvatarInfos.Add(avatar); + serviceRegistry.GetDBContext().SaveChanges(); + } + catch (Exception ex) + { + logger.Error($"Error creating avatar: {ex.Message}"); + } + } + + public static AvatarInfo? GetAvatarById(string avatarId) + { + return serviceRegistry.GetDBContext().AvatarInfos.Find(avatarId); + } + + public void UpdateAvatar(AvatarInfo avatar) + { + try + { + avatar.UpdatedAt = DateTime.UtcNow; + serviceRegistry.GetDBContext().AvatarInfos.Update(avatar); + serviceRegistry.GetDBContext().SaveChanges(); + } + catch (Exception ex) + { + logger.Error($"Error updating avatar: {ex.Message}"); + } + } + + public void DeleteAvatar(string avatarId) + { + var avatar = serviceRegistry.GetDBContext().AvatarInfos.Find(avatarId); + if (avatar != null) + { + serviceRegistry.GetDBContext().AvatarInfos.Remove(avatar); + serviceRegistry.GetDBContext().SaveChanges(); + } + } + + public static void CacheAvatars(List avatarIdInCache) + { + foreach (var avatarId in avatarIdInCache) + { + EnqueueAvatarForCheck(avatarId); + } + } + + private static void EnqueueAvatarForCheck(string avatarId) + { + if (recentlyProcessedAvatars.TryGetValue(avatarId, out DateTime dateTime)) + { + if ((DateTime.UtcNow - dateTime).TotalMinutes < 60) + { + return; + } + } + recentlyProcessedAvatars.Add(avatarId, DateTime.UtcNow); + + var queuedItem = new QueuedAvatarProcess(5, avatarId); + + priorityQueue.Enqueue(queuedItem); + } + + public static void EnqueueWatchAvatarForCheck(QueuedAvatarWatch watch) + { + priorityQueue.Enqueue(watch); + } + + public void EnqueueModeratedAvatarForCheck(QueuedModeratedAvatarWatch watch) + { + priorityQueue.Enqueue(watch); + } + + + public void GetAvatarsFromUser(string userId, string avatarName) + { + + logger.Debug($"Fetching avatars for user {userId} to find avatar named {avatarName}"); + + try + { + // Avatar already exists in the database and was updated within the last 12 hours + System.Threading.Thread.Sleep(500); + List avatarData = serviceRegistry.GetVRChatAPIClient().GetAvatarsByUserId(userId); + foreach (var avatar in avatarData) + { + logger.Debug(avatar.ToString()); + if (avatar.Name.Equals(avatarName, StringComparison.OrdinalIgnoreCase)) + { + AvatarInfo? dbAvatarInfo = GetAvatarById(avatar.Id); + + if (dbAvatarInfo == null) + { + var avatarInfo = new AvatarInfo + { + AvatarId = avatar.Id, + UserId = avatar.AuthorId, + AvatarName = avatar.Name, + ImageUrl = avatar.ImageUrl, + CreatedAt = avatar.CreatedAt, + UpdatedAt = DateTime.UtcNow, + AlertType = AlertTypeEnum.None, + UserName = avatar.AuthorName + }; + + AddAvatar(avatarInfo); + } + else + { + dbAvatarInfo.UserId = avatar.AuthorId; + dbAvatarInfo.UserName = avatar.AuthorName; + dbAvatarInfo.AvatarName = avatar.Name; + dbAvatarInfo.ImageUrl = avatar.ImageUrl; + dbAvatarInfo.CreatedAt = avatar.CreatedAt; + UpdateAvatar(dbAvatarInfo); + } + } + } + } + catch (Exception ex) + { + logger.Error($"Error fetching avatar: {ex.Message}"); + } + } + + public void CompactDatabase() + { + serviceRegistry.GetDBContext().Database.ExecuteSqlRaw("VACUUM;"); + } + + public static AvatarInfo? CheckAvatarByName(string avatarName) + { + var bannedAvatars = serviceRegistry.GetDBContext().AvatarInfos + .Where(b => b.AvatarName != null && b.AvatarName.Equals(avatarName) && b.AlertType > 0) + .OrderByDescending(b => b.AlertType) + .ToList(); + + if (bannedAvatars.Count > 0) + { + // Play alert sound based on the highest alert type found for the avatar + AlertTypeEnum maxAlertType = bannedAvatars[0].AlertType; + SoundManager.PlayAlertSound(CommonConst.Avatar_Alert_Key, maxAlertType); + + return bannedAvatars[0]; + } + + return null; + } + + public static async Task AvatarCheckTask(ConcurrentPriorityQueue, int> priorityQueue, ServiceRegistry serviceRegistry) + { + OllamaClient.logger.Info($"Amplitude Avatar Cache Queue Running"); + TailgrabDBContext dBContext = serviceRegistry.GetDBContext(); + while (true) + { + // Process items from the priority queue + while (true) + { + var result = priorityQueue.Dequeue(); + if (result.IsSuccess) + { + if (result.Value is QueuedAvatarProcess item && item.AvatarId != null) + { + await UpdateAmpAvatarRecord(serviceRegistry, dBContext, item.AvatarId); + } + else if (result.Value is QueuedAvatarWatch item2) + { + await UpdateWatchedAvatarRecord(serviceRegistry, dBContext, item2); + } + else if (result.Value is QueuedModeratedAvatarWatch item3) + { + await UpdateModeratedAvatarRecord(serviceRegistry, dBContext, item3); + } + } + else + { + // No more items to process + break; + } + } + // Wait for a short period before checking the queue again + await Task.Delay(5000); + } + } + + private static async Task UpdateAmpAvatarRecord(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, string avatarId) + { + try + { + AvatarInfo? dbAvatarInfo = dBContext.AvatarInfos.Find(avatarId); + bool updateNeeded = false; + if (dbAvatarInfo == null) + { + updateNeeded = true; + } + else if (dbAvatarInfo.AlertType == AlertTypeEnum.None && + (!dbAvatarInfo.UpdatedAt.HasValue || dbAvatarInfo.UpdatedAt.Value >= DateTime.UtcNow.AddHours(-2))) + { + updateNeeded = true; + } + + if (updateNeeded) + { + // Adds and Updates avatar info in the database, if it doesn't exist or was last updated more than 2 hours ago + Avatar? avatarData = FetchUpdateAvatarData(serviceRegistry, dBContext, avatarId, dbAvatarInfo); + + if (avatarData == null && dbAvatarInfo == null) + { + // Private Avatar + CreateAvatarInfoForPrivate(dBContext, avatarId); + } + + // Wait for a short period before checking the queue again + await Task.Delay(1000); + } + } + catch (Exception ex) + { + logger.Error(ex, $"Error fetching user profile for userId: {avatarId}"); + } + } + + + private static async Task UpdateModeratedAvatarRecord(ServiceRegistry _serviceRegistry, TailgrabDBContext dbContext, QueuedModeratedAvatarWatch watch) + { + try + { + // Fetch the AvatarInfo record + AvatarInfo? avatarInfo = await dbContext.AvatarInfos.FindAsync(watch.AvatarId); + PlayerManager.FetchUpdateAvatarData(_serviceRegistry, dbContext, watch.AvatarId, avatarInfo); + avatarInfo = await dbContext.AvatarInfos.FindAsync(watch.AvatarId); + + if (avatarInfo == null) + { + logger.Debug($"Line {watch.LineNumber}: Avatar ID '{watch.AvatarId}' not found in database/vrc, skipping."); + } + else if (avatarInfo.AlertType == AlertTypeEnum.None) + { + avatarInfo.AlertType = AlertTypeEnum.Nuisance; + avatarInfo.UpdatedAt = DateTime.UtcNow; + dbContext.AvatarInfos.Update(avatarInfo); + dbContext.SaveChanges(); + } + else + { + logger.Debug($"Line {watch.LineNumber}: Avatar ID '{watch.AvatarId}' already has Has an Alert, skipping."); + } + } + catch (Exception ex) + { + logger.Error(ex, $"Line {watch.LineNumber}: Error processing avatar ID '{watch.AvatarId}'"); + } + + // Throttle processing to avoid overwhelming the API + await Task.Delay(1000); + } + + private static async Task UpdateWatchedAvatarRecord(ServiceRegistry _serviceRegistry, TailgrabDBContext dbContext, QueuedAvatarWatch watch) + { + try + { + // Fetch the AvatarInfo record + AvatarInfo? avatarInfo = await dbContext.AvatarInfos.FindAsync(watch.AvatarId); + PlayerManager.FetchUpdateAvatarData(_serviceRegistry, dbContext, watch.AvatarId, avatarInfo); + avatarInfo = await dbContext.AvatarInfos.FindAsync(watch.AvatarId); + + if (avatarInfo == null) + { + logger.Debug($"Line {watch.LineNumber}: Avatar ID '{watch.AvatarId}' not found in database/vrc, skipping."); + } + else if (avatarInfo.AlertType == AlertTypeEnum.None) + { + + avatarInfo.AlertType = watch.AlertType; + avatarInfo.UpdatedAt = DateTime.UtcNow; + dbContext.AvatarInfos.Update(avatarInfo); + dbContext.SaveChanges(); + + if (avatarInfo.AlertType >= AlertTypeEnum.Nuisance) + { + await _serviceRegistry.GetVRChatAPIClient().BlockAvatarGlobal(avatarInfo.AvatarId); + } + else + { + await _serviceRegistry.GetVRChatAPIClient().DeleteAvatarGlobal(avatarInfo.AvatarId); + } + + logger.Debug($"Line {watch.LineNumber}: Set Watch State for Avatar ID '{watch.AvatarId}'"); + + } + else + { + logger.Debug($"Line {watch.LineNumber}: Avatar ID '{watch.AvatarId}' already has Has an Alert, skipping."); + } + } + catch (Exception ex) + { + logger.Error(ex, $"Line {watch.LineNumber}: Error processing avatar ID '{watch.AvatarId}'"); + } + + // Throttle processing to avoid overwhelming the API + await Task.Delay(3000); + } + + + private static void CreateAvatarInfoForPrivate(TailgrabDBContext dBContext, string AvatarId) + { + var avatarInfo = new AvatarInfo + { + AvatarId = AvatarId, + UserId = "", + AvatarName = $"Unknown Avatar {AvatarId}", + ImageUrl = "", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + try + { + dBContext.Add(avatarInfo); + dBContext.SaveChanges(); + logger.Debug($"Adding fallback avatar record for {avatarInfo}"); + } + catch (Exception ex) + { + logger.Error($"Error adding fallback avatar record for {AvatarId}: {ex.Message}"); + } + } + + public static Avatar? FetchUpdateAvatarData(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, string AvatarId, AvatarInfo? dbAvatarInfo) + { + Avatar? avatarData = null; + try + { + // Avatar already exists in the database and was updated within the last 12 hours + System.Threading.Thread.Sleep(500); + avatarData = serviceRegistry.GetVRChatAPIClient().GetAvatarById(AvatarId); + if (avatarData != null) + { + if (dbAvatarInfo == null) + { + var avatarInfo = new AvatarInfo + { + AvatarId = avatarData.Id, + UserId = avatarData.AuthorId, + UserName = avatarData.AuthorName, + AvatarName = avatarData.Name, + ImageUrl = avatarData.ImageUrl, + CreatedAt = avatarData.CreatedAt, + UpdatedAt = DateTime.UtcNow + }; + + try + { + dBContext.Add(avatarInfo); + dBContext.SaveChanges(); + } + catch (Exception ex) + { + logger.Error($"Error adding avatar record for {AvatarId}: {ex.Message}"); + } + } + else + { + // Ensure entity is attached to the dbContext before updating to avoid Detached state errors + var entry = dBContext.Entry(dbAvatarInfo); + if (entry.State == Microsoft.EntityFrameworkCore.EntityState.Detached) + { + dBContext.Attach(dbAvatarInfo); + entry = dBContext.Entry(dbAvatarInfo); + } + + dbAvatarInfo.UserId = avatarData.AuthorId; + dbAvatarInfo.UserName = avatarData.AuthorName; + dbAvatarInfo.AvatarName = avatarData.Name; + dbAvatarInfo.ImageUrl = avatarData.ImageUrl; + dbAvatarInfo.CreatedAt = avatarData.CreatedAt; + dbAvatarInfo.UpdatedAt = DateTime.UtcNow; + + try + { + entry.State = Microsoft.EntityFrameworkCore.EntityState.Modified; + dBContext.SaveChanges(); + } + catch (Exception ex) + { + logger.Error($"Error updating avatar record for {AvatarId}: {ex.Message}"); + } + } + } + } + catch (Exception ex) + { + logger.Error($"Error fetching avatar: {ex.Message}"); + } + + return avatarData; + } + } + #endregion + + #region Avatar Queue Classes + internal class QueuedAvatarProcess(int priority, string avatarId) : IHavePriority + { + public int Priority { get; set; } = priority; + + public string AvatarId { get; set; } = avatarId; + } + + + public class QueuedAvatarWatch(int priority, string avatarId, AlertTypeEnum alertType, int lineNumber) : IHavePriority + { + public int Priority { get; set; } = priority; + + public string AvatarId { get; set; } = avatarId; + + public AlertTypeEnum AlertType { get; set; } = alertType; + + public int LineNumber { get; set; } = lineNumber; + } + + public class QueuedModeratedAvatarWatch(int priority, string avatarId, AlertTypeEnum alertType, int lineNumber) : IHavePriority + { + public int Priority { get; set; } = priority; + + public string AvatarId { get; set; } = avatarId; + + public AlertTypeEnum AlertType { get; set; } = alertType; + + public int LineNumber { get; set; } = lineNumber; } + #endregion } diff --git a/src/PlayerManagement/TailgrabPanel.xaml b/src/PlayerManagement/TailgrabPanel.xaml index 3dc95d9..125d1fd 100644 --- a/src/PlayerManagement/TailgrabPanel.xaml +++ b/src/PlayerManagement/TailgrabPanel.xaml @@ -1457,6 +1457,76 @@ Title="Tailgrab Player Panel" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +