diff --git a/BuildNumber.txt b/BuildNumber.txt
index 1cb1a4d..102ae24 100644
--- a/BuildNumber.txt
+++ b/BuildNumber.txt
@@ -1 +1 @@
-1988
+2105
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/README.md b/README.md
index 3d36f71..2812ef8 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,9 @@ Tailgrab will read the VRChat Local Game Log files in real time, parse them for
- Group Flagging based on user directed database.
- Historical tracking of User elapsed time seen from your usage of the application.
- Trigger actions based on VRChat log events of "Vote To Kick" or "Group Moderation Action (Kick/Warn)", such as sending OSC Avatar Parameters, sending keystrokes to other applications, etc.
+- Allows capture of Groups and Avatars that are not currently in the instance for future alerting or ignoring them
+- Imports Moderated Users avatars as "Nuisance" level for quick identification in future instances.
+- Saves Layout of window size, position, columns and splitters for next open.
## Installation
@@ -214,4 +217,6 @@ https://gist.githubusercontent.com/jlong23/2b051df849cabb4da273eaf98225ae4e/raw/
[Config Tab, Alerts](./docs/Config_Alerts.md) Configure Alert Levels sounds and highlight colors.
+[Config Tab, Open Logs](./docs/Config_OpenLogs.md) Show and close open logs.
+
[Config Tab, Migrations](./docs/Config_Migrations.md) Migrate V1.0.9 Database to the new database.
diff --git a/docs/Config_OpenLogs.md b/docs/Config_OpenLogs.md
new file mode 100644
index 0000000..049fcc9
--- /dev/null
+++ b/docs/Config_OpenLogs.md
@@ -0,0 +1,6 @@
+[Back](../README.md)
+# Open Logs
+
+New to V1.1.2; To help users to understand what logs are being used by TailGrab what has activity (Creation Date, Last Update and Lines Processed). The action button can be used to close unused logs; especialy when you open and close VR Chat very often due to crashers or VR Chat's poor architecture that makes it unstable.
+
+[
](./tailgrab_tab_config_migration_complete.png)
\ No newline at end of file
diff --git a/docs/tailgrab_tab_config_openlogs.png b/docs/tailgrab_tab_config_openlogs.png
new file mode 100644
index 0000000..93b8a36
Binary files /dev/null and b/docs/tailgrab_tab_config_openlogs.png differ
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..aa88265 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);
@@ -300,7 +300,7 @@ private static void ProfileViewUpdate(ServiceRegistry serviceRegistry, Player pl
{
switch (profileWatch)
{
- case "Harrassment & Bullying":
+ case "Harassment & Bullying":
player.AddAlertMessage(AlertClassEnum.Profile, AlertTypeEnum.Nuisance, "Hate");
SoundManager.PlayAlertSound(CommonConst.Profile_Alert_Key, AlertTypeEnum.Nuisance);
break;
@@ -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)
@@ -331,9 +331,9 @@ private static void ProfileViewUpdate(ServiceRegistry serviceRegistry, Player pl
{
return "Explicit Sexual";
}
- else if (CheckLines(profileText, "Harrassment & Bullying"))
+ else if (CheckLines(profileText, "Harassment & Bullying"))
{
- return "Harrassment & Bullying";
+ return "Harassment & Bullying";
}
else if (CheckLines(profileText, "Self Harm"))
{
diff --git a/src/Clients/VRChat/VRChat.cs b/src/Clients/VRChat/VRChat.cs
index dfafd88..5914626 100644
--- a/src/Clients/VRChat/VRChat.cs
+++ b/src/Clients/VRChat/VRChat.cs
@@ -10,17 +10,20 @@
using VRChat.API.Client;
using VRChat.API.Model;
-
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 +40,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 +229,7 @@ public async Task Initialize()
public List GetAvatarsByUserId(string userId)
{
- List avatars = new List();
+ List avatars = [];
try
{
if (_vrchat != null)
@@ -193,11 +244,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 +267,7 @@ public User GetProfile(string userId)
public List GetProfileGroups(string userId)
{
- List groups = new List();
+ List groups = [];
try
{
if (_vrchat != null)
@@ -245,19 +297,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 +317,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 +331,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 +348,7 @@ public List GetProfileGroups(string userId)
imageList.Add(contentB64);
}
- ImageReference iref = new ImageReference
+ ImageReference iref = new ()
{
Base64Data = imageList,
Md5Hash = md5Hash,
@@ -335,7 +365,9 @@ public List GetProfileGroups(string userId)
return null;
}
}
+ #endregion
+ #region Print Management
public Print? GetPrintInfo(string fileURL)
{
Print? printInfo = null;
@@ -354,157 +386,115 @@ 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;
}
- public async Task BlockAvatarGlobal(string avatarId)
+ public async Task GetGroupMemberStatus(string groupId, string userId)
{
try
{
if (_vrchat == null)
{
- logger.Info($"Failed Block avatar {avatarId} globally, not logged in.");
- return false;
+ logger.Error("VRChat client not initialized");
+ return TGGroupMemberStatus.Unknown;
}
- // Create HTTP client with cookies
- var handler = new HttpClientHandler
- {
- CookieContainer = new CookieContainer()
- };
+ GroupLimitedMember membership = _vrchat.Groups.GetGroupMember(groupId, userId);
+ logger.Info($"Checking group {groupId} member status for user {userId}");
- var cookies = _vrchat.GetCookies();
- foreach (var cookie in cookies)
+ if( membership != null && membership.MembershipStatus != null)
{
- handler.CookieContainer.Add(new Uri(URI_VRC_BASE_API), cookie);
+ if (membership.MembershipStatus == GroupMemberStatus.Banned)
+ {
+ return TGGroupMemberStatus.Banned;
+ }
+ else if( membership.MembershipStatus == GroupMemberStatus.Member)
+ {
+ return TGGroupMemberStatus.Member;
+ }
}
-
- using HttpClient httpClient = new HttpClient(handler);
- httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
-
- AvatarModerationItem rpt = new AvatarModerationItem
- {
- 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;
+ return TGGroupMemberStatus.NotMember;
}
catch (Exception ex)
{
- logger.Error($"Error setting avatar moderation status: {ex.Message}");
+ logger.Error($"Error checking group member status: {ex.Message}");
+ return TGGroupMemberStatus.Unknown;
}
-
- return false;
}
-
- public async Task DeleteAvatarGlobal(string avatarId)
+ public async Task BanUserFromGroup(string groupId, string userId)
{
try
{
if (_vrchat == null)
{
- logger.Info($"Failed Unblock avatar {avatarId} globally, not logged in.");
+ logger.Error("VRChat client not initialized");
return false;
}
+ UserIdPayload request = new UserIdPayload { UserId = 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();
- HttpResponseMessage response = await httpClient.DeleteAsync($"{URI_VRC_BASE_API}/api/1/auth/user/avatarmoderations?targetAvatarId={avatarId}&avatarModerationType=block");
+ // Submit the moderation report
+ HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{URI_VRC_BASE_API}/api/1/groups/{groupId}/bans", request);
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: {responseContent}");
+ logger.Info($"Banning user {userId} from group {groupId}");
response.EnsureSuccessStatusCode();
return response.IsSuccessStatusCode;
+
}
catch (Exception ex)
{
- logger.Error($"Error setting avatar moderation status: {ex.Message}");
+ logger.Error($"Error banning user from group: {ex.Message}");
+ return false;
}
-
- return false;
}
-
- private static void SaveCookiesToFile(string filePath, List cookies)
+ public async Task UnbanUserFromGroup(string groupId, string userId)
{
- 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)
- {
- Group? group = null;
try
{
- if (_vrchat != null)
+ if (_vrchat == null)
{
- group = _vrchat.Groups.GetGroup(id);
+ logger.Error("VRChat client not initialized");
+ return false;
}
+
+ _vrchat.Groups.UnbanGroupMember(groupId, userId);
+ logger.Info($"Unbanning user {userId} from group {groupId}");
+
+ return true;
}
catch (Exception ex)
{
- logger.Error($"Error fetching Group information: {ex.Message}");
+ logger.Error($"Error unbanning user from group: {ex.Message}");
+ return false;
}
-
- return group;
}
+ #endregion
+ #region Moderation Management
internal async Task SubmitModerationReportAsync(ModerationReportPayload rpt)
{
try
@@ -516,19 +506,7 @@ internal async Task SubmitModerationReportAsync(ModerationReportPayload rp
}
// 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();
// Submit the moderation report
HttpResponseMessage response = await httpClient.PostAsJsonAsync($"{URI_VRC_BASE_API}/api/1/moderationReports", rpt);
@@ -546,6 +524,77 @@ internal async Task SubmitModerationReportAsync(ModerationReportPayload rp
return false;
}
}
+ #endregion
+
+ #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
+ {
+ var json = System.IO.File.ReadAllText(filePath);
+ var dtoList = JsonConvert.DeserializeObject>(json);
+ if (dtoList == null || dtoList.Count == 0)
+ return null;
+
+ // 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)
+ {
+ logger.Debug($"Loaded cookie: {dto.Name}, Expires: {dto.Expires}");
+
+ if (dto.Expires != DateTime.MinValue && dto.Expires.ToUniversalTime() <= now)
+ {
+ return null;
+ }
+ }
+
+ var cookies = dtoList.Select(d => d.ToCookie()).ToList();
+ return cookies;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ 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);
+ }
+
+ private HttpClient CreateHttpClientWithCookies()
+ {
+ if (_vrchat == null)
+ {
+ logger.Error("VRChat client not initialized, cannot create HTTP client with cookies.");
+ throw new InvalidOperationException("VRChat client not initialized");
+ }
+
+ 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);
+ }
+ HttpClient httpClient = new(handler);
+ httpClient.DefaultRequestHeaders.Add("User-Agent", UserAgent);
+
+ return httpClient;
+ }
+ #endregion
#region Non Public Helper Types
private class SerializableCookie
@@ -636,7 +685,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 +700,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 +730,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 +775,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
@@ -775,6 +824,20 @@ public class PrintFileInfo
[JsonProperty("image")]
public string ImageUrl { get; set; } = string.Empty;
}
+
+ public class UserIdPayload
+ {
+ [JsonProperty("userId")]
+ public string UserId { get; set; } = string.Empty;
+ }
+
+ public enum TGGroupMemberStatus
+ {
+ Unknown,
+ NotMember,
+ Member,
+ Banned
+ }
#endregion
}
}
diff --git a/src/Common/CommonConst.cs b/src/Common/CommonConst.cs
index deea7c5..7dd2f58 100644
--- a/src/Common/CommonConst.cs
+++ b/src/Common/CommonConst.cs
@@ -46,6 +46,42 @@ public static class CommonConst
public const string Sound_Alert_Key = "Sound";
public const string Color_Alert_Key = "Color";
+ // Highlight class color registry keys
+ public const string Registry_HighlightClass_Normal_Background = "HIGHLIGHT_NORMAL_BG";
+ public const string Registry_HighlightClass_Normal_Foreground = "HIGHLIGHT_NORMAL_FG";
+ public const string Registry_HighlightClass_Friend_Background = "HIGHLIGHT_FRIEND_BG";
+ public const string Registry_HighlightClass_Friend_Foreground = "HIGHLIGHT_FRIEND_FG";
+ public const string Registry_HighlightClass_Class01_Background = "HIGHLIGHT_CLASS01_BG";
+ public const string Registry_HighlightClass_Class01_Foreground = "HIGHLIGHT_CLASS01_FG";
+ public const string Registry_HighlightClass_Class02_Background = "HIGHLIGHT_CLASS02_BG";
+ public const string Registry_HighlightClass_Class02_Foreground = "HIGHLIGHT_CLASS02_FG";
+ public const string Registry_HighlightClass_Class03_Background = "HIGHLIGHT_CLASS03_BG";
+ public const string Registry_HighlightClass_Class03_Foreground = "HIGHLIGHT_CLASS03_FG";
+ public const string Registry_HighlightClass_Class04_Background = "HIGHLIGHT_CLASS04_BG";
+ public const string Registry_HighlightClass_Class04_Foreground = "HIGHLIGHT_CLASS04_FG";
+ public const string Registry_HighlightClass_Selected_Background = "HIGHLIGHT_SELECTED_BG";
+ public const string Registry_HighlightClass_Selected_Foreground = "HIGHLIGHT_SELECTED_FG";
+ public const string Registry_HighlightClass_MouseOver_Background = "HIGHLIGHT_MOUSEOVER_BG";
+ public const string Registry_HighlightClass_MouseOver_Foreground = "HIGHLIGHT_MOUSEOVER_FG";
+
+ // Default highlight class colors
+ public const string Default_HighlightClass_Normal_Background = "#FF1E1E1E";
+ public const string Default_HighlightClass_Normal_Foreground = "#FFE6E6E6";
+ public const string Default_HighlightClass_Friend_Background = "#FF1E1E1E";
+ public const string Default_HighlightClass_Friend_Foreground = "LightGreen";
+ public const string Default_HighlightClass_Class01_Background = "Yellow";
+ public const string Default_HighlightClass_Class01_Foreground = "Black";
+ public const string Default_HighlightClass_Class02_Background = "Red";
+ public const string Default_HighlightClass_Class02_Foreground = "Yellow";
+ public const string Default_HighlightClass_Class03_Background = "Purple";
+ public const string Default_HighlightClass_Class03_Foreground = "Yellow";
+ public const string Default_HighlightClass_Class04_Background = "Black";
+ public const string Default_HighlightClass_Class04_Foreground = "Yellow";
+ public const string Default_HighlightClass_Selected_Background = "#FF1d1db3";
+ public const string Default_HighlightClass_Selected_Foreground = "#FFFFFF00";
+ public const string Default_HighlightClass_MouseOver_Background = "#FF1d1db3";
+ public const string Default_HighlightClass_MouseOver_Foreground = "#FFFFFF00";
+
public const string Registry_Discovered_Avatar_Caching = "DISCOVERED_AVATAR_CACHING";
public const string Registry_Moderated_Avatar_Caching = "MODERATED_AVATAR_CACHING";
public const string Registry_Discovered_Group_Caching = "DISCOVERED_GROUP_CACHING";
diff --git a/src/Common/ConfigStore.cs b/src/Common/ConfigStore.cs
index 821ab87..9760f9b 100644
--- a/src/Common/ConfigStore.cs
+++ b/src/Common/ConfigStore.cs
@@ -171,5 +171,10 @@ public static void RemoveStoredKeyString(string keyPath, string keyName)
}
}
+ public static void RemoveStoredKeyString(string keyName)
+ {
+ RemoveStoredKeyString(CommonConst.ConfigRegistryPath, keyName);
+ }
+
}
}
diff --git a/src/Common/WindowLayoutManager.cs b/src/Common/WindowLayoutManager.cs
new file mode 100644
index 0000000..6f188e9
--- /dev/null
+++ b/src/Common/WindowLayoutManager.cs
@@ -0,0 +1,335 @@
+using Microsoft.Win32;
+using NLog;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Forms;
+
+namespace Tailgrab.Common
+{
+ public class WindowLayoutManager
+ {
+ private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+ private const string LayoutRegistryPath = "Software\\DeviousFox\\Tailgrab\\Layout";
+
+ // Default values from XAML
+ public const double DefaultWindowWidth = 1080;
+ public const double DefaultWindowHeight = 750;
+
+ // Active Players Tab Column Widths
+ public const double DefaultActiveDisplayNameWidth = 180;
+ public const double DefaultActiveAgeWidth = 60;
+ public const double DefaultActiveAvatarNameWidth = 180;
+ public const double DefaultActiveInstanceStartWidth = 160;
+ public const double DefaultActiveAlertMessagesWidth = 330;
+ public const double DefaultActiveCopyWidth = 60;
+ public const double DefaultActiveReportWidth = 60;
+
+ // Past Players Tab Column Widths
+ public const double DefaultPastDisplayNameWidth = 180;
+ public const double DefaultPastAgeWidth = 60;
+ public const double DefaultPastAvatarNameWidth = 180;
+ public const double DefaultPastInstanceEndWidth = 160;
+ public const double DefaultPastAlertMessagesWidth = 330;
+ public const double DefaultPastCopyWidth = 60;
+ public const double DefaultPastReportWidth = 60;
+
+ // Known Avatars Column Widths
+ public const double DefaultAvatarAlertWidth = 80;
+ public const double DefaultAvatarNameWidth = 150;
+ public const double DefaultAvatarIdWidth = 250;
+ public const double DefaultAvatarUserNameWidth = 150;
+ public const double DefaultAvatarUpdatedWidth = 150;
+ public const double DefaultAvatarBrowserWidth = 80;
+
+ // Known Groups Column Widths
+ public const double DefaultGroupAlertWidth = 120;
+ public const double DefaultGroupNameWidth = 200;
+ public const double DefaultGroupIdWidth = 300;
+ public const double DefaultGroupUpdatedWidth = 200;
+ public const double DefaultGroupBrowserWidth = 80;
+
+ // Known Users Column Widths
+ public const double DefaultUserDisplayNameWidth = 200;
+ public const double DefaultUserIdWidth = 300;
+ public const double DefaultUserElapsedWidth = 120;
+ public const double DefaultUserUpdatedWidth = 200;
+ public const double DefaultUserBrowserWidth = 80;
+
+ // Open Logs Column Widths
+ public const double DefaultLogFileNameWidth = 400;
+ public const double DefaultLogOpenedWidth = 180;
+ public const double DefaultLogLastLineWidth = 180;
+ public const double DefaultLogLinesProcessedWidth = 120;
+ public const double DefaultLogActionWidth = 80;
+
+ // GridSplitter positions (stored as GridLength values)
+ public const double DefaultActiveRowSplitterHeight = 100;
+ public const double DefaultPastRowSplitterHeight = 100;
+
+ // GridSplitter positions (stored as GridLength values)
+ public const double DefaultActiveColSplitterWidth = -1;
+ public const double DefaultPastColSplitterWidth = -1;
+
+ #region Save Methods
+
+ public static void SaveWindowSize(Window window)
+ {
+ try
+ {
+ if (window.WindowState == WindowState.Normal)
+ {
+ using (var key = Registry.CurrentUser.CreateSubKey(LayoutRegistryPath))
+ {
+ key.SetValue("WindowWidth", window.Width);
+ key.SetValue("WindowHeight", window.Height);
+ }
+ logger.Debug($"Saved window size: {window.Width}x{window.Height}");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to save window size to registry.");
+ }
+ }
+
+ public static void SaveWindowPosition(Window window)
+ {
+ try
+ {
+ if (window.WindowState == WindowState.Normal)
+ {
+ using (var key = Registry.CurrentUser.CreateSubKey(LayoutRegistryPath))
+ {
+ key.SetValue("WindowLeft", window.Left);
+ key.SetValue("WindowTop", window.Top);
+ }
+ logger.Debug($"Saved window position: Left={window.Left}, Top={window.Top}");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to save window position to registry.");
+ }
+ }
+
+ public static void SaveColumnWidth(string columnName, double width)
+ {
+ try
+ {
+ using (var key = Registry.CurrentUser.CreateSubKey(LayoutRegistryPath))
+ {
+ key.SetValue($"Column_{columnName}", width);
+ }
+ logger.Debug($"Saved column width for {columnName}: {width}");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Failed to save column width for {columnName}.");
+ }
+ }
+
+ public static void SaveSplitterPosition(string splitterName, double height)
+ {
+ try
+ {
+ using (var key = Registry.CurrentUser.CreateSubKey(LayoutRegistryPath))
+ {
+ key.SetValue($"Splitter_{splitterName}", height);
+ }
+ logger.Debug($"Saved splitter height for {splitterName}: {height}");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Failed to save splitter height for {splitterName}.");
+ }
+ }
+ #endregion
+
+ #region Load Methods
+ public static void LoadWindowSize(Window window)
+ {
+ try
+ {
+ using (var key = Registry.CurrentUser.OpenSubKey(LayoutRegistryPath))
+ {
+ if (key != null)
+ {
+ var width = key.GetValue("WindowWidth");
+ var height = key.GetValue("WindowHeight");
+
+ if (width != null && height != null)
+ {
+ window.Width = Convert.ToDouble(width);
+ window.Height = Convert.ToDouble(height);
+ logger.Debug($"Loaded window size: {window.Width}x{window.Height}");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to load window size from registry.");
+ }
+ }
+
+ public static void LoadWindowPosition(Window window)
+ {
+ try
+ {
+ using (var key = Registry.CurrentUser.OpenSubKey(LayoutRegistryPath))
+ {
+ if (key != null)
+ {
+ var left = key.GetValue("WindowLeft");
+ var top = key.GetValue("WindowTop");
+
+ if (left != null && top != null)
+ {
+ double leftPos = Convert.ToDouble(left);
+ double topPos = Convert.ToDouble(top);
+
+ // Validate that the position is within visible screen bounds
+ if (IsPositionValid(leftPos, topPos, window.Width, window.Height))
+ {
+ window.Left = leftPos;
+ window.Top = topPos;
+ logger.Debug($"Loaded window position: Left={window.Left}, Top={window.Top}");
+ }
+ else
+ {
+ logger.Warn("Saved window position is outside visible screen bounds. Using default position.");
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to load window position from registry.");
+ }
+ }
+
+ public static double LoadColumnWidth(string columnName, double defaultWidth)
+ {
+ try
+ {
+ using (var key = Registry.CurrentUser.OpenSubKey(LayoutRegistryPath))
+ {
+ if (key != null)
+ {
+ var value = key.GetValue($"Column_{columnName}");
+ if (value != null)
+ {
+ double width = Convert.ToDouble(value);
+ logger.Debug($"Loaded column width for {columnName}: {width}");
+ return width;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Failed to load column width for {columnName}.");
+ }
+ return defaultWidth;
+ }
+
+ public static double LoadSplitterHeight(string splitterName, double defaultHeight)
+ {
+ try
+ {
+ using (var key = Registry.CurrentUser.OpenSubKey(LayoutRegistryPath))
+ {
+ if (key != null)
+ {
+ var value = key.GetValue($"Splitter_{splitterName}");
+ if (value != null)
+ {
+ double height = Convert.ToDouble(value);
+ logger.Debug($"Loaded splitter height for {splitterName}: {height}");
+ return height;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Failed to load splitter height for {splitterName}.");
+ }
+ return defaultHeight;
+ }
+
+ #endregion
+
+ #region Reset Methods
+
+ public static void ResetLayoutSettings()
+ {
+ try
+ {
+ using (var key = Registry.CurrentUser.OpenSubKey(LayoutRegistryPath, writable: true))
+ {
+ if (key != null)
+ {
+ // Delete all layout values
+ foreach (var valueName in key.GetValueNames())
+ {
+ key.DeleteValue(valueName);
+ }
+ logger.Info("Reset all layout settings to defaults.");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to reset layout settings.");
+ throw;
+ }
+ }
+
+ #endregion
+
+ #region Helper Methods
+
+ private static bool IsPositionValid(double left, double top, double width, double height)
+ {
+ // Check if at least part of the window is visible on any screen
+ var windowRect = new Rect(left, top, width, height);
+
+ foreach (var screen in System.Windows.Forms.Screen.AllScreens)
+ {
+ var screenRect = new Rect(
+ screen.WorkingArea.Left,
+ screen.WorkingArea.Top,
+ screen.WorkingArea.Width,
+ screen.WorkingArea.Height);
+
+ if (windowRect.IntersectsWith(screenRect))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static void ApplyLayoutToGridView(GridView gridView, Dictionary columnWidths)
+ {
+ for (int i = 0; i < gridView.Columns.Count; i++)
+ {
+ var column = gridView.Columns[i];
+ var header = column.Header as GridViewColumnHeader;
+ if (header != null)
+ {
+ string columnName = header.Tag as string ?? header.Content?.ToString() ?? $"Column{i}";
+ if (columnWidths.TryGetValue(columnName, out double width))
+ {
+ column.Width = width;
+ }
+ }
+ }
+ }
+
+ #endregion
+ }
+}
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/Models/GroupInfo.cs b/src/Models/GroupInfo.cs
index 42d191c..bb8f61d 100644
--- a/src/Models/GroupInfo.cs
+++ b/src/Models/GroupInfo.cs
@@ -1,11 +1,9 @@
// This file has been auto generated by EF Core Power Tools.
#nullable disable
-using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using Tailgrab.Common;
-using VRChat.API.Model;
namespace Tailgrab.Models;
diff --git a/src/Models/GroupManagement.cs b/src/Models/GroupManagement.cs
new file mode 100644
index 0000000..c43de2a
--- /dev/null
+++ b/src/Models/GroupManagement.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using Tailgrab.Common;
+
+namespace tailgrab.src.Models
+{
+ public partial class GroupManagement
+ {
+ public string GroupId { get; set; }
+
+ public string GroupName { get; set; }
+
+ public DateTime CreatedAt { get; set; }
+
+ public DateTime? UpdatedAt { get; set; }
+
+ public GroupManagement()
+ {
+ UpdatedAt = DateTime.UtcNow;
+ }
+
+ public override string ToString()
+ {
+ return $"GroupId: {GroupId}, GroupName: {GroupName}, CreatedAt: {CreatedAt}, UpdatedAt: {UpdatedAt}";
+ }
+ }
+}
diff --git a/src/Models/TailgrabDBContext.cs b/src/Models/TailgrabDBContext.cs
index 5a3f56b..2929b7b 100644
--- a/src/Models/TailgrabDBContext.cs
+++ b/src/Models/TailgrabDBContext.cs
@@ -11,6 +11,7 @@
using System.IO;
using System.Text.Json;
using System.Windows.Media.Animation;
+using tailgrab.src.Models;
using Tailgrab.Common;
namespace Tailgrab.Models;
@@ -58,6 +59,8 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
public virtual DbSet GroupInfos { get; set; }
+ public virtual DbSet GroupManagements { get; set; }
+
public virtual DbSet ProfileEvaluations { get; set; }
public virtual DbSet ImageEvaluations { get; set; }
@@ -90,6 +93,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasIndex(g => g.AlertType);
});
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.GroupId);
+
+ entity.ToTable("GroupManagement");
+
+ entity.Property(e => e.CreatedAt).HasColumnName("createDate");
+ entity.Property(e => e.UpdatedAt).HasColumnName("updateDate");
+
+ entity.HasIndex(g => g.GroupName);
+ });
+
modelBuilder.Entity(entity =>
{
entity.HasKey(e => e.Md5checksum);
@@ -126,18 +141,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
public void UpgradeDatabase()
{
- // CREATE INDEX ix_avtr_aname ON AvatarInfo(AvatarName);
- // CREATE INDEX ix_avtr_uname ON AvatarInfo(UserName);
- // CREATE INDEX ix_avtr_alert ON AvatarInfo(alertType);
- // CREATE INDEX ix_grp_gname ON GroupInfo(groupName);
- // CREATE INDEX ix_grp_alert ON GroupInfo(alertType);
-
ExecuteSqlTransaction(
"CREATE INDEX IF NOT EXISTS ix_avtr_aname ON AvatarInfo(AvatarName)",
"CREATE INDEX IF NOT EXISTS ix_avtr_uname ON AvatarInfo(UserName)",
"CREATE INDEX IF NOT EXISTS ix_avtr_alert ON AvatarInfo(alertType)",
"CREATE INDEX IF NOT EXISTS ix_grp_gname ON GroupInfo(groupName)",
- "CREATE INDEX IF NOT EXISTS ix_grp_alert ON GroupInfo(alertType)"
+ "CREATE INDEX IF NOT EXISTS ix_grp_alert ON GroupInfo(alertType)",
+ "CREATE TABLE IF NOT EXISTS GroupManagement ( GroupId TEXT NOT NULL CONSTRAINT PK_GroupManagement PRIMARY KEY, GroupName TEXT NULL, createDate TEXT NOT NULL, updateDate TEXT NULL )",
+ "CREATE INDEX IF NOT EXISTS ix_grpm_gname ON GroupManagement(groupName)"
);
}
@@ -153,6 +164,7 @@ private void ExecuteSqlTransaction(params string[] sqlStatements)
{
try
{
+ logger.Warn(sql);
Database.ExecuteSqlRaw(sql);
}
catch
@@ -162,6 +174,7 @@ private void ExecuteSqlTransaction(params string[] sqlStatements)
throw;
}
}
+ transaction.Commit();
}
private async Task ExecuteSqlAsync(string sql)
diff --git a/src/PlayerManagement/AlertColorOption.cs b/src/PlayerManagement/AlertColorOption.cs
new file mode 100644
index 0000000..a727f8a
--- /dev/null
+++ b/src/PlayerManagement/AlertColorOption.cs
@@ -0,0 +1,22 @@
+using System.Windows.Media;
+
+namespace Tailgrab.PlayerManagement
+{
+ public class AlertColorOption
+ {
+ public string Name { get; set; }
+ public string Value { get; set; }
+ public System.Windows.Media.Brush BackgroundBrush { get; set; }
+ public System.Windows.Media.Brush ForegroundBrush { get; set; }
+
+ public AlertColorOption(string name, string value, System.Windows.Media.Brush backgroundBrush, System.Windows.Media.Brush foregroundBrush)
+ {
+ Name = name;
+ Value = value;
+ BackgroundBrush = backgroundBrush;
+ ForegroundBrush = foregroundBrush;
+ }
+
+ public override string ToString() => Name;
+ }
+}
diff --git a/src/PlayerManagement/ColorOption.cs b/src/PlayerManagement/ColorOption.cs
new file mode 100644
index 0000000..cea1343
--- /dev/null
+++ b/src/PlayerManagement/ColorOption.cs
@@ -0,0 +1,20 @@
+using System.Windows.Media;
+
+namespace Tailgrab.PlayerManagement
+{
+ public class ColorOption
+ {
+ public string Name { get; set; }
+ public string Value { get; set; }
+ public System.Windows.Media.Brush Brush { get; set; }
+
+ public ColorOption(string name, string value)
+ {
+ Name = name;
+ Value = value;
+ Brush = (System.Windows.Media.Brush)new BrushConverter().ConvertFromString(value)!;
+ }
+
+ public override string ToString() => Name;
+ }
+}
diff --git a/src/PlayerManagement/GroupBanItem.cs b/src/PlayerManagement/GroupBanItem.cs
new file mode 100644
index 0000000..1c8af7c
--- /dev/null
+++ b/src/PlayerManagement/GroupBanItem.cs
@@ -0,0 +1,112 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace Tailgrab.PlayerManagement
+{
+ public class GroupBanItem : INotifyPropertyChanged
+ {
+ private string _groupId = string.Empty;
+ private string _groupName = string.Empty;
+ private string _status = "Not Checked";
+ private bool _canBan = false;
+ private bool _canUnban = false;
+ private System.Windows.Media.Brush _statusColor = System.Windows.Media.Brushes.Gray;
+
+ public string GroupId
+ {
+ get => _groupId;
+ set
+ {
+ if (_groupId != value)
+ {
+ _groupId = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public string GroupName
+ {
+ get => _groupName;
+ set
+ {
+ if (_groupName != value)
+ {
+ _groupName = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public string Status
+ {
+ get => _status;
+ set
+ {
+ if (_status != value)
+ {
+ _status = value;
+ OnPropertyChanged();
+ UpdateStatusColor();
+ }
+ }
+ }
+
+ public bool CanBan
+ {
+ get => _canBan;
+ set
+ {
+ if (_canBan != value)
+ {
+ _canBan = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public bool CanUnban
+ {
+ get => _canUnban;
+ set
+ {
+ if (_canUnban != value)
+ {
+ _canUnban = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ public System.Windows.Media.Brush StatusColor
+ {
+ get => _statusColor;
+ private set
+ {
+ if (_statusColor != value)
+ {
+ _statusColor = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private void UpdateStatusColor()
+ {
+ StatusColor = Status switch
+ {
+ "Member" => System.Windows.Media.Brushes.LightGreen,
+ "Banned" => System.Windows.Media.Brushes.Red,
+ "Not Member" => System.Windows.Media.Brushes.Yellow,
+ _ => System.Windows.Media.Brushes.Gray
+ };
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/src/PlayerManagement/PlayerManagement.cs b/src/PlayerManagement/PlayerManagement.cs
index 563fc53..3be81ba 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;
+ _ = Task.Run(() => AvatarCheckTask(priorityQueue, serviceRegistry));
+ }
- 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);
@@ -853,7 +782,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);
@@ -874,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();
@@ -903,14 +835,14 @@ 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();
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,
@@ -963,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);
}
}
}
@@ -995,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..b604db9 100644
--- a/src/PlayerManagement/TailgrabPanel.xaml
+++ b/src/PlayerManagement/TailgrabPanel.xaml
@@ -12,6 +12,47 @@ Title="Tailgrab Player Panel"
+
+
+
@@ -22,61 +63,24 @@ Title="Tailgrab Player Panel"
-
+
-
+
-
+
-
+
-
-
-
-
+
+
+
+
-
+
-
+
-
+
@@ -188,7 +192,7 @@ Title="Tailgrab Player Panel"
-
+
@@ -284,12 +288,12 @@ Title="Tailgrab Player Panel"
-
+
-
+
-
+
@@ -299,46 +303,9 @@ Title="Tailgrab Player Panel"
-
-
-
+
+
+
@@ -433,11 +400,11 @@ Title="Tailgrab Player Panel"
-
+
-
+
@@ -451,7 +418,7 @@ Title="Tailgrab Player Panel"
-
+
@@ -972,7 +939,7 @@ Title="Tailgrab Player Panel"
-
+
@@ -1136,7 +1103,7 @@ Title="Tailgrab Player Panel"
-
+
@@ -1150,6 +1117,7 @@ Title="Tailgrab Player Panel"
+
@@ -1180,7 +1148,8 @@ Title="Tailgrab Player Panel"
-
+
+
@@ -1188,10 +1157,12 @@ Title="Tailgrab Player Panel"
-
+
+
-
+
+
@@ -1242,7 +1213,7 @@ Title="Tailgrab Player Panel"
-
+
@@ -1251,12 +1222,13 @@ Title="Tailgrab Player Panel"
-
-
+
+
+
-
+
@@ -1301,9 +1273,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1318,9 +1300,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1335,9 +1327,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1355,9 +1357,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1372,9 +1384,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1389,9 +1411,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1409,9 +1441,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1426,9 +1468,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1443,9 +1495,19 @@ Title="Tailgrab Player Panel"
-
+
+ SelectedValuePath="Value" Margin="2,2,2,2" Width="260">
+
+
+
+
+
+
+
+
+
+
@@ -1457,6 +1519,301 @@ Title="Tailgrab Player Panel"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1523,6 +1880,192 @@ Title="Tailgrab Player Panel"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PlayerManagement/TailgrabPanel.xaml.cs b/src/PlayerManagement/TailgrabPanel.xaml.cs
index 1487069..2c36ec1 100644
--- a/src/PlayerManagement/TailgrabPanel.xaml.cs
+++ b/src/PlayerManagement/TailgrabPanel.xaml.cs
@@ -26,10 +26,11 @@ public partial class TailgrabPanel : Window, IDisposable, INotifyPropertyChanged
private readonly DispatcherTimer fallbackTimer;
private readonly DispatcherTimer statusBarTimer;
- public ObservableCollection ActivePlayers { get; } = new ObservableCollection();
- public ObservableCollection PastPlayers { get; } = new ObservableCollection();
- public ObservableCollection PrintPlayers { get; } = new ObservableCollection();
- public ObservableCollection EmojiPlayers { get; } = new ObservableCollection();
+ public ObservableCollection ActivePlayers { get; } = [];
+ public ObservableCollection PastPlayers { get; } = [];
+ public ObservableCollection PrintPlayers { get; } = [];
+ public ObservableCollection EmojiPlayers { get; } = [];
+ public ObservableCollection OpenLogs { get; } = [];
public AvatarVirtualizingCollection AvatarDbItems { get; private set; }
public GroupVirtualizingCollection GroupDbItems { get; private set; }
public UserVirtualizingCollection UserDbItems { get; private set; }
@@ -41,6 +42,7 @@ public partial class TailgrabPanel : Window, IDisposable, INotifyPropertyChanged
public ICollectionView PastView { get; }
public ICollectionView PrintView { get; }
public ICollectionView EmojiView { get; }
+ public ICollectionView OpenLogsView { get; }
private PlayerViewModel? _selectedActive;
@@ -143,32 +145,357 @@ public string ElapsedTime
}
}
- public List> AlertTypeOptions { get; } = new List>
- {
+ public List> AlertTypeOptions { get; } =
+ [
new KeyValuePair("None", AlertTypeEnum.None),
new KeyValuePair("Watch", AlertTypeEnum.Watch),
new KeyValuePair("Nuisance", AlertTypeEnum.Nuisance),
new KeyValuePair("Crasher", AlertTypeEnum.Crasher)
- };
+ ];
- public List> AlertColorOptions { get; } = new List>
- {
- new KeyValuePair("*NONE", "Normal"),
- new KeyValuePair("Yellow", "Yellow"),
- new KeyValuePair("Purple", "Purple"),
- new KeyValuePair("Red", "Red"),
- };
+ private List? _alertColorOptions;
+ public List AlertColorOptions
+ {
+ get
+ {
+ if (_alertColorOptions == null)
+ {
+ _alertColorOptions =
+ [
+ new AlertColorOption("*NONE", "Normal", NormalBackground, NormalForeground),
+ new AlertColorOption("Class 1", "Class01", Class01Background, Class01Foreground),
+ new AlertColorOption("Class 2", "Class02", Class02Background, Class02Foreground),
+ new AlertColorOption("Class 3", "Class03", Class03Background, Class03Foreground),
+ new AlertColorOption("Class 4", "Class04", Class04Background, Class04Foreground),
+ ];
+ }
+ return _alertColorOptions;
+ }
+ }
- public List> AlertSoundOptions { get; set; } = new List>();
+ public List> AlertSoundOptions { get; set; } = [];
- public List ProfileReportReasonsOptions = new List
- {
+ public List ProfileReportReasonsOptions =
+ [
new ReportReasonItem("Sexual Content", "sexual"),
new ReportReasonItem("Hateful Content", "hateful"),
new ReportReasonItem("Gore and Violence", "gore"),
new ReportReasonItem("Child Exploitation", "child"),
new ReportReasonItem("Other", "other")
- };
+ ];
+
+ // Color options for selection
+ public List ColorOptions { get; } =
+ [
+ new ColorOption("Dark Gray", "#FF1E1E1E"),
+ new ColorOption("Light Gray", "#FFE6E6E6"),
+ new ColorOption("Black", "Black"),
+ new ColorOption("White", "White"),
+ new ColorOption("Red", "Red"),
+ new ColorOption("Yellow", "Yellow"),
+ new ColorOption("Green", "Green"),
+ new ColorOption("Blue", "Blue"),
+ new ColorOption("Purple", "Purple"),
+ new ColorOption("Orange", "Orange"),
+ new ColorOption("Pink", "Pink"),
+ new ColorOption("Light Green", "LightGreen"),
+ new ColorOption("Light Blue", "LightBlue"),
+ new ColorOption("Light Pink", "LightPink"),
+ new ColorOption("Dark Blue", "#FF1d1db3"),
+ new ColorOption("Bright Yellow", "#FFFFFF00"),
+ new ColorOption("Cyan", "Cyan"),
+ new ColorOption("Magenta", "Magenta"),
+ new ColorOption("Lime", "Lime"),
+ new ColorOption("Brown", "Brown"),
+ new ColorOption("Navy", "Navy"),
+ new ColorOption("Teal", "Teal"),
+ new ColorOption("Maroon", "Maroon"),
+ new ColorOption("Olive", "Olive"),
+ new ColorOption("Silver", "Silver"),
+ new ColorOption("Gold", "Gold"),
+ ];
+
+ // Highlight class color properties with backing fields
+ private System.Windows.Media.Brush _normalBackground = null!;
+ public System.Windows.Media.Brush NormalBackground
+ {
+ get => _normalBackground;
+ set { _normalBackground = value; OnPropertyChanged(nameof(NormalBackground)); }
+ }
+
+ private System.Windows.Media.Brush _normalForeground = null!;
+ public System.Windows.Media.Brush NormalForeground
+ {
+ get => _normalForeground;
+ set { _normalForeground = value; OnPropertyChanged(nameof(NormalForeground)); }
+ }
+
+ private System.Windows.Media.Brush _friendBackground = null!;
+ public System.Windows.Media.Brush FriendBackground
+ {
+ get => _friendBackground;
+ set { _friendBackground = value; OnPropertyChanged(nameof(FriendBackground)); }
+ }
+
+ private System.Windows.Media.Brush _friendForeground = null!;
+ public System.Windows.Media.Brush FriendForeground
+ {
+ get => _friendForeground;
+ set { _friendForeground = value; OnPropertyChanged(nameof(FriendForeground)); }
+ }
+
+ private System.Windows.Media.Brush _Class01Background = null!;
+ public System.Windows.Media.Brush Class01Background
+ {
+ get => _Class01Background;
+ set { _Class01Background = value; OnPropertyChanged(nameof(Class01Background)); }
+ }
+
+ private System.Windows.Media.Brush _Class01Foreground = null!;
+ public System.Windows.Media.Brush Class01Foreground
+ {
+ get => _Class01Foreground;
+ set { _Class01Foreground = value; OnPropertyChanged(nameof(Class01Foreground)); }
+ }
+
+ private System.Windows.Media.Brush _Class02Background = null!;
+ public System.Windows.Media.Brush Class02Background
+ {
+ get => _Class02Background;
+ set { _Class02Background = value; OnPropertyChanged(nameof(Class02Background)); }
+ }
+
+ private System.Windows.Media.Brush _Class02Foreground = null!;
+ public System.Windows.Media.Brush Class02Foreground
+ {
+ get => _Class02Foreground;
+ set { _Class02Foreground = value; OnPropertyChanged(nameof(Class02Foreground)); }
+ }
+
+ private System.Windows.Media.Brush _Class03Background = null!;
+ public System.Windows.Media.Brush Class03Background
+ {
+ get => _Class03Background;
+ set { _Class03Background = value; OnPropertyChanged(nameof(Class03Background)); }
+ }
+
+ private System.Windows.Media.Brush _Class03Foreground = null!;
+ public System.Windows.Media.Brush Class03Foreground
+ {
+ get => _Class03Foreground;
+ set { _Class03Foreground = value; OnPropertyChanged(nameof(Class03Foreground)); }
+ }
+
+ private System.Windows.Media.Brush _Class04Background = null!;
+ public System.Windows.Media.Brush Class04Background
+ {
+ get => _Class04Background;
+ set { _Class04Background = value; OnPropertyChanged(nameof(Class04Background)); }
+ }
+
+ private System.Windows.Media.Brush _Class04Foreground = null!;
+ public System.Windows.Media.Brush Class04Foreground
+ {
+ get => _Class04Foreground;
+ set { _Class04Foreground = value; OnPropertyChanged(nameof(Class04Foreground)); }
+ }
+
+ private System.Windows.Media.Brush _selectedBackground = null!;
+ public System.Windows.Media.Brush SelectedBackground
+ {
+ get => _selectedBackground;
+ set { _selectedBackground = value; OnPropertyChanged(nameof(SelectedBackground)); }
+ }
+
+ private System.Windows.Media.Brush _selectedForeground = null!;
+ public System.Windows.Media.Brush SelectedForeground
+ {
+ get => _selectedForeground;
+ set { _selectedForeground = value; OnPropertyChanged(nameof(SelectedForeground)); }
+ }
+
+ private System.Windows.Media.Brush _mouseOverBackground = null!;
+ public System.Windows.Media.Brush MouseOverBackground
+ {
+ get => _mouseOverBackground;
+ set { _mouseOverBackground = value; OnPropertyChanged(nameof(MouseOverBackground)); }
+ }
+
+ private System.Windows.Media.Brush _mouseOverForeground = null!;
+ public System.Windows.Media.Brush MouseOverForeground
+ {
+ get => _mouseOverForeground;
+ set { _mouseOverForeground = value; OnPropertyChanged(nameof(MouseOverForeground)); }
+ }
+
+ // Selected color options for ComboBoxes
+ private ColorOption? _selectedNormalBackground;
+ public ColorOption? SelectedNormalBackground
+ {
+ get => _selectedNormalBackground;
+ set
+ {
+ _selectedNormalBackground = value;
+ if (value != null) NormalBackground = value.Brush;
+ OnPropertyChanged(nameof(SelectedNormalBackground));
+ }
+ }
+
+ private ColorOption? _selectedNormalForeground;
+ public ColorOption? SelectedNormalForeground
+ {
+ get => _selectedNormalForeground;
+ set
+ {
+ _selectedNormalForeground = value;
+ if (value != null) NormalForeground = value.Brush;
+ OnPropertyChanged(nameof(SelectedNormalForeground));
+ }
+ }
+
+ private ColorOption? _selectedFriendBackground;
+ public ColorOption? SelectedFriendBackground
+ {
+ get => _selectedFriendBackground;
+ set
+ {
+ _selectedFriendBackground = value;
+ if (value != null) FriendBackground = value.Brush;
+ OnPropertyChanged(nameof(SelectedFriendBackground));
+ }
+ }
+
+ private ColorOption? _selectedFriendForeground;
+ public ColorOption? SelectedFriendForeground
+ {
+ get => _selectedFriendForeground;
+ set
+ {
+ _selectedFriendForeground = value;
+ if (value != null) FriendForeground = value.Brush;
+ OnPropertyChanged(nameof(SelectedFriendForeground));
+ }
+ }
+
+ private ColorOption? _selectedClass01Background;
+ public ColorOption? SelectedClass01Background
+ {
+ get => _selectedClass01Background;
+ set
+ {
+ _selectedClass01Background = value;
+ if (value != null) Class01Background = value.Brush;
+ OnPropertyChanged(nameof(SelectedClass01Background));
+ }
+ }
+
+ private ColorOption? _selectedClass01Foreground;
+ public ColorOption? SelectedClass01Foreground
+ {
+ get => _selectedClass01Foreground;
+ set
+ {
+ _selectedClass01Foreground = value;
+ if (value != null) Class01Foreground = value.Brush;
+ OnPropertyChanged(nameof(SelectedClass01Foreground));
+ }
+ }
+
+ private ColorOption? _selectedClass02Background;
+ public ColorOption? SelectedClass02Background
+ {
+ get => _selectedClass02Background;
+ set
+ {
+ _selectedClass02Background = value;
+ if (value != null) Class02Background = value.Brush;
+ OnPropertyChanged(nameof(SelectedClass02Background));
+ }
+ }
+
+ private ColorOption? _selectedClass02Foreground;
+ public ColorOption? SelectedClass02Foreground
+ {
+ get => _selectedClass02Foreground;
+ set
+ {
+ _selectedClass02Foreground = value;
+ if (value != null) Class02Foreground = value.Brush;
+ OnPropertyChanged(nameof(SelectedClass02Foreground));
+ }
+ }
+
+ private ColorOption? _selectedClass03Background;
+ public ColorOption? SelectedClass03Background
+ {
+ get => _selectedClass03Background;
+ set
+ {
+ _selectedClass03Background = value;
+ if (value != null) Class03Background = value.Brush;
+ OnPropertyChanged(nameof(SelectedClass03Background));
+ }
+ }
+
+ private ColorOption? _selectedClass03Foreground;
+ public ColorOption? SelectedClass03Foreground
+ {
+ get => _selectedClass03Foreground;
+ set
+ {
+ _selectedClass03Foreground = value;
+ if (value != null) Class04Foreground = value.Brush;
+ OnPropertyChanged(nameof(SelectedClass03Foreground));
+ }
+ }
+
+ private ColorOption? _selectedClass04Background;
+ public ColorOption? SelectedClass04Background
+ {
+ get => _selectedClass04Background;
+ set
+ {
+ _selectedClass04Background = value;
+ if (value != null) Class04Background = value.Brush;
+ OnPropertyChanged(nameof(SelectedClass04Background));
+ }
+ }
+
+ private ColorOption? _selectedClass04Foreground;
+ public ColorOption? SelectedClass04Foreground
+ {
+ get => _selectedClass04Foreground;
+ set
+ {
+ _selectedClass04Foreground = value;
+ if (value != null) Class04Foreground = value.Brush;
+ OnPropertyChanged(nameof(SelectedClass04Foreground));
+ }
+ }
+
+ private ColorOption? _selectedSelectedBackground;
+ public ColorOption? SelectedSelectedBackground
+ {
+ get => _selectedSelectedBackground;
+ set
+ {
+ _selectedSelectedBackground = value;
+ if (value != null) SelectedBackground = value.Brush;
+ OnPropertyChanged(nameof(SelectedSelectedBackground));
+ }
+ }
+
+ private ColorOption? _selectedSelectedForeground;
+ public ColorOption? SelectedSelectedForeground
+ {
+ get => _selectedSelectedForeground;
+ set
+ {
+ _selectedSelectedForeground = value;
+ if (value != null) SelectedForeground = value.Brush;
+ OnPropertyChanged(nameof(SelectedSelectedForeground));
+ }
+ }
public TailgrabPanel(ServiceRegistry serviceRegistry)
@@ -191,6 +518,9 @@ public TailgrabPanel(ServiceRegistry serviceRegistry)
PrintView = CollectionViewSource.GetDefaultView(PrintPlayers);
EmojiView = CollectionViewSource.GetDefaultView(EmojiPlayers);
+ OpenLogsView = CollectionViewSource.GetDefaultView(OpenLogs);
+ OpenLogsView.SortDescriptions.Add(new SortDescription("StartTime", ListSortDirection.Descending));
+
AvatarDbItems = new AvatarVirtualizingCollection(_serviceRegistry);
AvatarDbView = CollectionViewSource.GetDefaultView(AvatarDbItems);
// The virtualizing collection returns items ordered by AvatarName already.
@@ -235,7 +565,7 @@ public TailgrabPanel(ServiceRegistry serviceRegistry)
try
{
var sounds = SoundManager.GetAvailableSounds();
- AlertSoundOptions = sounds.Select(s => new KeyValuePair(s, s)).ToList();
+ AlertSoundOptions = [.. sounds.Select(s => new KeyValuePair(s, s))];
// Avatar Alerts
AvatarWarnSound.SelectedValue = GetAlertKeyString(CommonConst.Avatar_Alert_Key, AlertTypeEnum.Watch, CommonConst.Sound_Alert_Key) ?? "*NONE";
@@ -265,6 +595,9 @@ public TailgrabPanel(ServiceRegistry serviceRegistry)
ModeratedAvatarCaching.IsChecked = ConfigStore.GetStoredKeyBool(CommonConst.Registry_Moderated_Avatar_Caching, true);
DiscoveredGroupCaching.IsChecked = ConfigStore.GetStoredKeyBool(CommonConst.Registry_Discovered_Group_Caching, true);
+ // Load highlight colors from registry
+ LoadHighlightColors();
+
}
catch { }
@@ -296,6 +629,11 @@ public TailgrabPanel(ServiceRegistry serviceRegistry)
statusBarTimer.Start();
this.Closed += (s, e) => Dispose();
+
+ // Load window layout from registry
+ this.Loaded += Window_Loaded;
+ this.SizeChanged += Window_SizeChanged;
+ this.LocationChanged += Window_LocationChanged;
}
private void SaveConfig_Click(object sender, RoutedEventArgs e)
@@ -370,7 +708,73 @@ private void TestSound_Click(object sender, RoutedEventArgs e)
}
}
- private void SetAlertKeyString(string alertKey, AlertTypeEnum alertType, string subType, object value)
+ private void AlertsTab_GotFocus(object sender, RoutedEventArgs e)
+ {
+ // Invalidate cached AlertColorOptions to refresh color previews with current color settings
+ _alertColorOptions = null;
+ OnPropertyChanged(nameof(AlertColorOptions));
+ }
+
+ private void GistUrl_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ // Enable/disable the corresponding "Check Now" button based on whether there's text in the textbox
+ if (sender == avatarGistUrl)
+ {
+ avatarGistCheckButton.IsEnabled = !string.IsNullOrWhiteSpace(avatarGistUrl.Text);
+ }
+ else if (sender == groupGistUrl)
+ {
+ groupGistCheckButton.IsEnabled = !string.IsNullOrWhiteSpace(groupGistUrl.Text);
+ }
+ }
+
+ private async void CheckAvatarGist_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ avatarGistCheckButton.IsEnabled = false;
+ avatarGistCheckButton.Content = "Checking...";
+
+ await Task.Run(() => _serviceRegistry.ProcessAvatarGist());
+
+ System.Windows.MessageBox.Show("Avatar GIST list processing in the background.", "Check Avatar GIST", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to process Avatar GIST");
+ System.Windows.MessageBox.Show($"Failed to process Avatar GIST: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ avatarGistCheckButton.Content = "Check Now";
+ avatarGistCheckButton.IsEnabled = !string.IsNullOrWhiteSpace(avatarGistUrl.Text);
+ }
+ }
+
+ private async void CheckGroupGist_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ groupGistCheckButton.IsEnabled = false;
+ groupGistCheckButton.Content = "Checking...";
+
+ await Task.Run(() => _serviceRegistry.ProcessGroupGist());
+
+ System.Windows.MessageBox.Show("Group GIST list processing in the background.", "Check Group GIST", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to process Group GIST");
+ System.Windows.MessageBox.Show($"Failed to process Group GIST: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ finally
+ {
+ groupGistCheckButton.Content = "Check Now";
+ groupGistCheckButton.IsEnabled = !string.IsNullOrWhiteSpace(groupGistUrl.Text);
+ }
+ }
+
+ private static void SetAlertKeyString(string alertKey, AlertTypeEnum alertType, string subType, object value)
{
string key = CommonConst.ConfigRegistryPath + "\\" + alertKey + "\\" + alertType.ToString();
@@ -385,23 +789,178 @@ private void SetAlertKeyString(string alertKey, AlertTypeEnum alertType, string
}
- private string? GetAlertKeyString(string alertKey, AlertTypeEnum alertType, string subType)
+ private static string? GetAlertKeyString(string alertKey, AlertTypeEnum alertType, string subType)
{
string key = CommonConst.ConfigRegistryPath + "\\" + alertKey + "\\" + alertType.ToString();
return ConfigStore.GetStoredKeyString(key, subType);
}
+ private void LoadHighlightColors()
+ {
+ try
+ {
+ // Load colors from registry or use defaults
+ var normalBg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Normal_Background) ?? CommonConst.Default_HighlightClass_Normal_Background;
+ var normalFg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Normal_Foreground) ?? CommonConst.Default_HighlightClass_Normal_Foreground;
+ var friendBg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Friend_Background) ?? CommonConst.Default_HighlightClass_Friend_Background;
+ var friendFg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Friend_Foreground) ?? CommonConst.Default_HighlightClass_Friend_Foreground;
+ var class01Bg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Class01_Background) ?? CommonConst.Default_HighlightClass_Class01_Background;
+ var class01Fg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Class01_Foreground) ?? CommonConst.Default_HighlightClass_Class01_Foreground;
+ var class02Bg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Class02_Background) ?? CommonConst.Default_HighlightClass_Class02_Background;
+ var class02Fg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Class02_Foreground) ?? CommonConst.Default_HighlightClass_Class02_Foreground;
+ var class03Bg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Class03_Background) ?? CommonConst.Default_HighlightClass_Class03_Background;
+ var class03Fg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Class03_Foreground) ?? CommonConst.Default_HighlightClass_Class03_Foreground;
+ var class04Bg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Class04_Background) ?? CommonConst.Default_HighlightClass_Class04_Background;
+ var class04Fg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Class04_Foreground) ?? CommonConst.Default_HighlightClass_Class04_Foreground;
+ var selectedBg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Selected_Background) ?? CommonConst.Default_HighlightClass_Selected_Background;
+ var selectedFg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_Selected_Foreground) ?? CommonConst.Default_HighlightClass_Selected_Foreground;
+ var mouseOverBg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_MouseOver_Background) ?? CommonConst.Default_HighlightClass_MouseOver_Background;
+ var mouseOverFg = ConfigStore.GetStoredKeyString(CommonConst.Registry_HighlightClass_MouseOver_Foreground) ?? CommonConst.Default_HighlightClass_MouseOver_Foreground;
+
+ // Set the brush properties
+ var converter = new BrushConverter();
+ NormalBackground = (System.Windows.Media.Brush)converter.ConvertFromString(normalBg)!;
+ NormalForeground = (System.Windows.Media.Brush)converter.ConvertFromString(normalFg)!;
+ FriendBackground = (System.Windows.Media.Brush)converter.ConvertFromString(friendBg)!;
+ FriendForeground = (System.Windows.Media.Brush)converter.ConvertFromString(friendFg)!;
+ Class01Background = (System.Windows.Media.Brush)converter.ConvertFromString(class01Bg)!;
+ Class01Foreground = (System.Windows.Media.Brush)converter.ConvertFromString(class01Fg)!;
+ Class02Background = (System.Windows.Media.Brush)converter.ConvertFromString(class02Bg)!;
+ Class02Foreground = (System.Windows.Media.Brush)converter.ConvertFromString(class02Fg)!;
+ Class03Background = (System.Windows.Media.Brush)converter.ConvertFromString(class03Bg)!;
+ Class03Foreground = (System.Windows.Media.Brush)converter.ConvertFromString(class03Fg)!;
+ Class04Background = (System.Windows.Media.Brush)converter.ConvertFromString(class04Bg)!;
+ Class04Foreground = (System.Windows.Media.Brush)converter.ConvertFromString(class04Fg)!;
+ SelectedBackground = (System.Windows.Media.Brush)converter.ConvertFromString(selectedBg)!;
+ SelectedForeground = (System.Windows.Media.Brush)converter.ConvertFromString(selectedFg)!;
+ MouseOverBackground = (System.Windows.Media.Brush)converter.ConvertFromString(mouseOverBg)!;
+ MouseOverForeground = (System.Windows.Media.Brush)converter.ConvertFromString(mouseOverFg)!;
+
+ // Set selected options in ComboBoxes
+ SelectedNormalBackground = ColorOptions.FirstOrDefault(c => c.Value.Equals(normalBg, StringComparison.OrdinalIgnoreCase));
+ SelectedNormalForeground = ColorOptions.FirstOrDefault(c => c.Value.Equals(normalFg, StringComparison.OrdinalIgnoreCase));
+ SelectedFriendBackground = ColorOptions.FirstOrDefault(c => c.Value.Equals(friendBg, StringComparison.OrdinalIgnoreCase));
+ SelectedFriendForeground = ColorOptions.FirstOrDefault(c => c.Value.Equals(friendFg, StringComparison.OrdinalIgnoreCase));
+ SelectedClass01Background = ColorOptions.FirstOrDefault(c => c.Value.Equals(class01Bg, StringComparison.OrdinalIgnoreCase));
+ SelectedClass01Foreground = ColorOptions.FirstOrDefault(c => c.Value.Equals(class01Fg, StringComparison.OrdinalIgnoreCase));
+ SelectedClass02Background = ColorOptions.FirstOrDefault(c => c.Value.Equals(class02Bg, StringComparison.OrdinalIgnoreCase));
+ SelectedClass02Foreground = ColorOptions.FirstOrDefault(c => c.Value.Equals(class02Fg, StringComparison.OrdinalIgnoreCase));
+ SelectedClass03Background = ColorOptions.FirstOrDefault(c => c.Value.Equals(class03Bg, StringComparison.OrdinalIgnoreCase));
+ SelectedClass03Foreground = ColorOptions.FirstOrDefault(c => c.Value.Equals(class03Fg, StringComparison.OrdinalIgnoreCase));
+ SelectedClass04Background = ColorOptions.FirstOrDefault(c => c.Value.Equals(class04Bg, StringComparison.OrdinalIgnoreCase));
+ SelectedClass04Foreground = ColorOptions.FirstOrDefault(c => c.Value.Equals(class04Fg, StringComparison.OrdinalIgnoreCase));
+ SelectedSelectedBackground = ColorOptions.FirstOrDefault(c => c.Value.Equals(selectedBg, StringComparison.OrdinalIgnoreCase));
+ SelectedSelectedForeground = ColorOptions.FirstOrDefault(c => c.Value.Equals(selectedFg, StringComparison.OrdinalIgnoreCase));
+
+ // Invalidate cached alert color options so they reload with new brushes
+ _alertColorOptions = null;
+ OnPropertyChanged(nameof(AlertColorOptions));
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to load highlight colors");
+ }
+ }
+
+ private void SaveColors_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // Save all color settings to registry
+ if (SelectedNormalBackground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Normal_Background, SelectedNormalBackground.Value);
+ if (SelectedNormalForeground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Normal_Foreground, SelectedNormalForeground.Value);
+ if (SelectedFriendBackground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Friend_Background, SelectedFriendBackground.Value);
+ if (SelectedFriendForeground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Friend_Foreground, SelectedFriendForeground.Value);
+ if (SelectedClass01Background != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Class01_Background, SelectedClass01Background.Value);
+ if (SelectedClass01Foreground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Class01_Foreground, SelectedClass01Foreground.Value);
+ if (SelectedClass02Background != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Class02_Background, SelectedClass02Background.Value);
+ if (SelectedClass02Foreground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Class02_Foreground, SelectedClass02Foreground.Value);
+ if (SelectedClass03Background != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Class03_Background, SelectedClass03Background.Value);
+ if (SelectedClass03Foreground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Class03_Foreground, SelectedClass03Foreground.Value);
+ if (SelectedClass04Background != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Class04_Background, SelectedClass04Background.Value);
+ if (SelectedClass04Foreground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Class04_Foreground, SelectedClass04Foreground.Value);
+ if (SelectedSelectedBackground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Selected_Background, SelectedSelectedBackground.Value);
+ if (SelectedSelectedForeground != null)
+ ConfigStore.PutStoredKeyString(CommonConst.Registry_HighlightClass_Selected_Foreground, SelectedSelectedForeground.Value);
+
+ System.Windows.MessageBox.Show("Color settings saved successfully. Changes are applied immediately.", "Colors Saved", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to save color settings");
+ System.Windows.MessageBox.Show($"Failed to save color settings: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void ResetColors_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var result = System.Windows.MessageBox.Show(
+ "Are you sure you want to reset all colors to their default values?",
+ "Reset Colors",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ // Remove all color settings from registry
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Normal_Background);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Normal_Foreground);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Friend_Background);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Friend_Foreground);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Class01_Background);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Class01_Foreground);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Class02_Background);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Class02_Foreground);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Class03_Background);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Class03_Foreground);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Class04_Background);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Class04_Foreground);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Selected_Background);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_Selected_Foreground);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_MouseOver_Background);
+ ConfigStore.RemoveStoredKeyString(CommonConst.Registry_HighlightClass_MouseOver_Foreground);
+
+ // Reload colors with defaults
+ LoadHighlightColors();
+
+ System.Windows.MessageBox.Show("Colors reset to defaults successfully.", "Colors Reset", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to reset color settings");
+ System.Windows.MessageBox.Show($"Failed to reset color settings: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
private void StatusBarTimer_Tick(object? sender, EventArgs e)
{
try
{
- var avatarManager = _serviceRegistry.GetAvatarManager();
var ollamaClient = _serviceRegistry.GetOllamaAPIClient();
- AvatarQueueLength = avatarManager?.GetQueueCount() ?? 0;
+ AvatarQueueLength = PlayerManager.GetQueueCount();
OllamaQueueLength = ollamaClient?.GetQueueSize() ?? 0;
+ // Update Open Logs collection
+ RefreshOpenLogs();
+
// Update session info
var currentSession = PlayerManager.CurrentSession;
if (currentSession != null)
@@ -414,7 +973,7 @@ private void StatusBarTimer_Tick(object? sender, EventArgs e)
}
// Update elapsed time
- var elapsed = DateTime.Now - currentSession.startDateTime;
+ var elapsed = DateTime.Now - currentSession.StartDateTime;
int hours = (int)elapsed.TotalHours;
int minutes = elapsed.Minutes;
int seconds = elapsed.Seconds;
@@ -436,7 +995,7 @@ private void FallbackTimer_Tick(object? sender, EventArgs e)
// Ensure collections reflect PlayerManager state
try
{
- var players = _serviceRegistry.GetPlayerManager().GetAllPlayers().ToList();
+ var players = PlayerManager.GetAllPlayers().ToList();
UpdateCollectionsFromSnapshot(players);
}
catch { }
@@ -566,7 +1125,7 @@ private void MoveToPast(Player p)
private void PurgePastOlderThan(int minutes)
{
var olderThan = DateTime.Now.AddMinutes(minutes);
- List toRemove = new List();
+ List toRemove = [];
foreach (PlayerViewModel oldPlayer in PastPlayers)
{
if (!string.IsNullOrEmpty(oldPlayer.InstanceEndTime))
@@ -663,7 +1222,7 @@ private void CopyPlayer_Click(object sender, RoutedEventArgs e)
return null;
}
- private void ToggleSort(ICollectionView view, string property)
+ private static void ToggleSort(ICollectionView view, string property)
{
// If already sorted on property, flip direction. Otherwise set ascending.
var existing = view.SortDescriptions.FirstOrDefault(sd => sd.PropertyName == property);
@@ -684,7 +1243,7 @@ private void ToggleSort(ICollectionView view, string property)
view.Refresh();
}
- private void UpdateHeaderSortIndicator(GridViewColumnHeader clickedHeader, ICollectionView view, string property)
+ private static void UpdateHeaderSortIndicator(GridViewColumnHeader clickedHeader, ICollectionView view, string property)
{
// Clear indicators on sibling headers in same ListView
var lv = FindAncestor(clickedHeader);
@@ -784,7 +1343,7 @@ private void ShowProfileReportOverlay(string userId)
try
{
// Get the player from PlayerManager
- Player? player = _serviceRegistry.GetPlayerManager().GetPlayerByUserId(userId);
+ Player? player = PlayerManager.GetPlayerByUserId(userId);
if (player != null && !string.IsNullOrEmpty(player.AIEval))
{
@@ -907,17 +1466,21 @@ private async void OverlayProfileReportSubmit_Click(object sender, RoutedEventAr
// Reusable method to submit profile report - can be called from other places in the future if needed
private async Task SubmitProfileReport(string userId, string category, string reportReason, string reportDescription)
{
- ModerationReportPayload rpt = new ModerationReportPayload();
- rpt.Type = "user";
- rpt.Category = "profile";
- rpt.Reason = reportReason;
- rpt.ContentId = userId;
- rpt.Description = reportDescription;
+ ModerationReportPayload rpt = new()
+ {
+ Type = "user",
+ Category = "profile",
+ Reason = reportReason,
+ ContentId = userId,
+ Description = reportDescription
+ };
- ModerationReportDetails rptDtls = new ModerationReportDetails();
- rptDtls.InstanceType = "Group Public";
- rptDtls.InstanceAgeGated = false;
- rpt.Details = new List() { rptDtls };
+ ModerationReportDetails rptDtls = new()
+ {
+ InstanceType = "Group Public",
+ InstanceAgeGated = false
+ };
+ rpt.Details = [rptDtls];
bool success = await _serviceRegistry.GetVRChatAPIClient().SubmitModerationReportAsync(rpt);
if (success)
@@ -1305,18 +1868,22 @@ private async void PrintOverlaySubmit_Click(object sender, RoutedEventArgs e)
private async Task SubmitPrintReport(string userId, string printId, string category, string reportReason, string reportDescription)
{
- ModerationReportPayload rpt = new ModerationReportPayload();
- rpt.Type = category;
- rpt.Category = category;
- rpt.Reason = reportReason;
- rpt.ContentId = printId;
- rpt.Description = reportDescription;
+ ModerationReportPayload rpt = new()
+ {
+ Type = category,
+ Category = category,
+ Reason = reportReason,
+ ContentId = printId,
+ Description = reportDescription
+ };
- ModerationReportDetails rptDtls = new ModerationReportDetails();
- rptDtls.InstanceType = "Group Public";
- rptDtls.InstanceAgeGated = false;
- rptDtls.HolderId = userId;
- rpt.Details = new List() { rptDtls };
+ ModerationReportDetails rptDtls = new()
+ {
+ InstanceType = "Group Public",
+ InstanceAgeGated = false,
+ HolderId = userId
+ };
+ rpt.Details = [rptDtls];
bool success = await _serviceRegistry.GetVRChatAPIClient().SubmitModerationReportAsync(rpt);
if (success)
@@ -1353,7 +1920,7 @@ private async void OverlayPrintOnInputFieldChanged(object sender, TextChangedEve
PrintOverlayUserIdTextBox.Text = printInfo.OwnerId;
PrintOverlayInventoryIdTextBox.Text = printInfo.Id;
- List imageUrls = new List();
+ List imageUrls = [];
if (!string.IsNullOrEmpty(printInfo.Files.Image))
{
@@ -1448,13 +2015,13 @@ private void LoadPrintImage(string imageUrl)
//
// Emoji Handlers
#region Emoji handlers
- public List ReportReasons { get; } = new List
- {
+ public List ReportReasons { get; } =
+ [
new ReportReasonItem("Sexual Content", "sexual"),
new ReportReasonItem("Hateful Content", "hateful"),
new ReportReasonItem("Gore and Violence", "gore"),
new ReportReasonItem("Other", "other")
- };
+ ];
private void EmojiApplyFilter_Click(object sender, RoutedEventArgs e)
{
@@ -1659,18 +2226,22 @@ private async void OverlaySubmit_Click(object sender, RoutedEventArgs e)
private async Task SubmitInventoryReport(string userId, string inventoryId, string category, string reportReason, string reportDescription)
{
- ModerationReportPayload rpt = new ModerationReportPayload();
- rpt.Type = category.ToLower();
- rpt.Category = category.ToLower();
- rpt.Reason = reportReason;
- rpt.ContentId = inventoryId;
- rpt.Description = reportDescription;
+ ModerationReportPayload rpt = new()
+ {
+ Type = category.ToLower(),
+ Category = category.ToLower(),
+ Reason = reportReason,
+ ContentId = inventoryId,
+ Description = reportDescription
+ };
- ModerationReportDetails rptDtls = new ModerationReportDetails();
- rptDtls.InstanceType = "Group Public";
- rptDtls.InstanceAgeGated = false;
- rptDtls.HolderId = userId;
- rpt.Details = new List() { rptDtls };
+ ModerationReportDetails rptDtls = new()
+ {
+ InstanceType = "Group Public",
+ InstanceAgeGated = false,
+ HolderId = userId
+ };
+ rpt.Details = [rptDtls];
bool success = await _serviceRegistry.GetVRChatAPIClient().SubmitModerationReportAsync(rpt);
if (success)
@@ -1712,7 +2283,7 @@ private async void OverlayOnInputFieldChanged(object sender, TextChangedEventArg
OverlayCategoryTextBox.Text = "Unknown";
}
- List imageUrls = new List();
+ List imageUrls = [];
if (!string.IsNullOrEmpty(inventoryItem.Metadata?.ImageUrl))
{
@@ -2158,29 +2729,82 @@ private void UserHyperlink_RequestNavigate(object? sender, System.Windows.Naviga
#endregion
//
- // Migration UI handlers
- #region Migration handlers
- private void MigrationBrowse_Click(object sender, RoutedEventArgs e)
+ // Open Logs UI handlers
+ #region Open Logs handlers
+ private void RefreshOpenLogs()
{
- var openFileDialog = new Microsoft.Win32.OpenFileDialog
+ try
{
- Title = "Select avatars.sqlite file",
- Filter = "SQLite Database (*.sqlite)|*.sqlite|All Files (*.*)|*.*",
- FilterIndex = 1,
- CheckFileExists = true,
- CheckPathExists = true
- };
+ var activeTasks = FileTailer.GetActiveTailTasks();
- if (openFileDialog.ShowDialog() == true)
+ // Remove tasks that are no longer active
+ var toRemove = OpenLogs.Where(vm => !activeTasks.ContainsKey(vm.FilePath)).ToList();
+ foreach (var item in toRemove)
+ {
+ OpenLogs.Remove(item);
+ }
+
+ // Add or update tasks
+ foreach (var kvp in activeTasks)
+ {
+ var existing = OpenLogs.FirstOrDefault(vm => vm.FilePath == kvp.Key);
+ if (existing == null)
+ {
+ OpenLogs.Add(new TailTaskViewModel(kvp.Value));
+ }
+ else
+ {
+ existing.UpdateFromStatus(kvp.Value);
+ }
+ }
+ }
+ catch (Exception ex)
{
- MigrationFilePathTextBox.Text = openFileDialog.FileName;
- logger.Info($"Selected migration file: {openFileDialog.FileName}");
+ logger?.Error(ex, "Failed to refresh open logs");
}
}
- private void MigrationCancel_Click(object sender, RoutedEventArgs e)
+ private void CancelTailTask_Click(object sender, RoutedEventArgs e)
{
- MigrationFilePathTextBox.Text = string.Empty;
+ try
+ {
+ if (sender is System.Windows.Controls.Button button && button.Tag is TailTaskViewModel viewModel)
+ {
+ viewModel.RequestCancellation();
+ logger.Info($"Cancellation requested for: {viewModel.FilePath}");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger?.Error(ex, "Failed to cancel tail task");
+ }
+ }
+ #endregion
+
+ //
+ // Migration UI handlers
+ #region Migration handlers
+ private void MigrationBrowse_Click(object sender, RoutedEventArgs e)
+ {
+ var openFileDialog = new Microsoft.Win32.OpenFileDialog
+ {
+ Title = "Select avatars.sqlite file",
+ Filter = "SQLite Database (*.sqlite)|*.sqlite|All Files (*.*)|*.*",
+ FilterIndex = 1,
+ CheckFileExists = true,
+ CheckPathExists = true
+ };
+
+ if (openFileDialog.ShowDialog() == true)
+ {
+ MigrationFilePathTextBox.Text = openFileDialog.FileName;
+ logger.Info($"Selected migration file: {openFileDialog.FileName}");
+ }
+ }
+
+ private void MigrationCancel_Click(object sender, RoutedEventArgs e)
+ {
+ MigrationFilePathTextBox.Text = string.Empty;
logger.Info("Migration file selection cleared");
}
@@ -2204,7 +2828,7 @@ private void MigrationSubmit_Click(object sender, RoutedEventArgs e)
MigrationStatus status = _serviceRegistry.GetDBContext().MigrateOldVersion(filePath);
- StringBuilder sb = new StringBuilder();
+ StringBuilder sb = new();
foreach (var msg in status.Messages)
{
sb.AppendLine(msg);
@@ -2214,6 +2838,367 @@ private void MigrationSubmit_Click(object sender, RoutedEventArgs e)
}
#endregion
+ //
+ // Window Layout Management
+ #region Window Layout Management
+
+ private void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // Load window size and position
+ WindowLayoutManager.LoadWindowSize(this);
+ WindowLayoutManager.LoadWindowPosition(this);
+
+ // Load column widths for Active Players tab
+ LoadGridViewColumnWidths(ActiveListView, new Dictionary
+ {
+ { "DisplayName", WindowLayoutManager.DefaultActiveDisplayNameWidth },
+ { "Age", WindowLayoutManager.DefaultActiveAgeWidth },
+ { "AvatarName", WindowLayoutManager.DefaultActiveAvatarNameWidth },
+ { "InstanceStartTime", WindowLayoutManager.DefaultActiveInstanceStartWidth },
+ { "UserId", WindowLayoutManager.DefaultActiveAlertMessagesWidth },
+ });
+
+ // Load column widths for Past Players tab
+ LoadGridViewColumnWidths(PastListView, new Dictionary
+ {
+ { "DisplayName", WindowLayoutManager.DefaultPastDisplayNameWidth },
+ { "Age", WindowLayoutManager.DefaultPastAgeWidth },
+ { "AvatarName", WindowLayoutManager.DefaultPastAvatarNameWidth },
+ { "InstanceEndTime", WindowLayoutManager.DefaultPastInstanceEndWidth },
+ { "UserId", WindowLayoutManager.DefaultPastAlertMessagesWidth },
+ });
+
+ // Load column widths for Known Avatars DataGrid
+ LoadDataGridColumnWidths(AvatarDbGrid, new Dictionary
+ {
+ { "Alert", WindowLayoutManager.DefaultAvatarAlertWidth },
+ { "Avatar Name", WindowLayoutManager.DefaultAvatarNameWidth },
+ { "Avatar ID", WindowLayoutManager.DefaultAvatarIdWidth },
+ { "User Name", WindowLayoutManager.DefaultAvatarUserNameWidth },
+ { "Last Updated", WindowLayoutManager.DefaultAvatarUpdatedWidth },
+ { "Browser", WindowLayoutManager.DefaultAvatarBrowserWidth },
+ });
+
+ // Load column widths for Known Groups DataGrid
+ LoadDataGridColumnWidths(GroupDbGrid, new Dictionary
+ {
+ { "Alert", WindowLayoutManager.DefaultGroupAlertWidth },
+ { "Group Name", WindowLayoutManager.DefaultGroupNameWidth },
+ { "Group ID", WindowLayoutManager.DefaultGroupIdWidth },
+ { "Last Updated", WindowLayoutManager.DefaultGroupUpdatedWidth },
+ { "Browser", WindowLayoutManager.DefaultGroupBrowserWidth },
+ });
+
+ // Load column widths for Known Users DataGrid
+ LoadDataGridColumnWidths(UserDbGrid, new Dictionary
+ {
+ { "Display Name", WindowLayoutManager.DefaultUserDisplayNameWidth },
+ { "User ID", WindowLayoutManager.DefaultUserIdWidth },
+ { "Elapsed Time (hh:mm)", WindowLayoutManager.DefaultUserElapsedWidth },
+ { "Last Updated", WindowLayoutManager.DefaultUserUpdatedWidth },
+ { "Browser", WindowLayoutManager.DefaultUserBrowserWidth },
+ });
+
+ // Load column widths for Open Logs ListView
+ LoadGridViewColumnWidths(OpenLogsListView, new Dictionary
+ {
+ { "FileName", WindowLayoutManager.DefaultLogFileNameWidth },
+ { "StartTime", WindowLayoutManager.DefaultLogOpenedWidth },
+ { "LastLineProcessedTime", WindowLayoutManager.DefaultLogLastLineWidth },
+ { "LinesProcessed", WindowLayoutManager.DefaultLogLinesProcessedWidth },
+ });
+
+ // Load splitter positions
+ LoadRowSplitter("ActivePlayers", WindowLayoutManager.DefaultActiveRowSplitterHeight);
+ LoadRowSplitter("PastPlayers", WindowLayoutManager.DefaultPastRowSplitterHeight);
+ LoadColSplitter("ActivePlayers", WindowLayoutManager.DefaultActiveColSplitterWidth);
+ LoadColSplitter("PastPlayers", WindowLayoutManager.DefaultPastColSplitterWidth);
+
+ // Subscribe to column width change events
+ SubscribeToColumnWidthChanges();
+
+ logger.Info("Window layout loaded from registry");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to load window layout");
+ }
+ }
+
+ private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ if (this.WindowState == WindowState.Normal)
+ {
+ WindowLayoutManager.SaveWindowSize(this);
+ }
+ }
+
+ private void Window_LocationChanged(object? sender, EventArgs e)
+ {
+ if (this.WindowState == WindowState.Normal)
+ {
+ WindowLayoutManager.SaveWindowPosition(this);
+ }
+ }
+
+ private void LoadGridViewColumnWidths(System.Windows.Controls.ListView listView, Dictionary defaults)
+ {
+ if (listView.View is GridView gridView)
+ {
+ foreach (var column in gridView.Columns)
+ {
+ if (column.Header is GridViewColumnHeader header)
+ {
+ string columnName = (header.Tag as string) ?? header.Content?.ToString() ?? "";
+ if (!string.IsNullOrEmpty(columnName) && defaults.ContainsKey(columnName))
+ {
+ double width = WindowLayoutManager.LoadColumnWidth(
+ $"{listView.Name}_{columnName}",
+ defaults[columnName]);
+ column.Width = width;
+ }
+ }
+ }
+ }
+ }
+
+ private void LoadDataGridColumnWidths(System.Windows.Controls.DataGrid dataGrid, Dictionary defaults)
+ {
+ foreach (var column in dataGrid.Columns)
+ {
+ string columnName = column.Header?.ToString() ?? "";
+ if (!string.IsNullOrEmpty(columnName) && defaults.ContainsKey(columnName))
+ {
+ double width = WindowLayoutManager.LoadColumnWidth(
+ $"{dataGrid.Name}_{columnName}",
+ defaults[columnName]);
+ column.Width = new DataGridLength(width);
+ }
+ }
+ }
+
+ private void LoadRowSplitter(string tabName, double defaultHeight)
+ {
+ RowDefinition? gridRow = null;
+ double height = WindowLayoutManager.LoadSplitterHeight($"{tabName}_Horz", defaultHeight);
+
+ // Find the grid for the specified tab and set the row width
+ if (tabName == "ActivePlayers")
+ {
+ gridRow = ActivePlayerGridRow;
+ }
+ else if (tabName == "PastPlayers")
+ {
+ gridRow = PastPlayerGridRow;
+ }
+
+ if (gridRow != null)
+ {
+ logger.Debug($"Attempting to load splitter height for Row Splitter {tabName}, height to {height}");
+ gridRow.Height = new GridLength(height, GridUnitType.Star);
+ }
+ }
+
+ private void LoadColSplitter(string tabName, double defaultWidth)
+ {
+ ColumnDefinition? gridColumn = null;
+ double width = WindowLayoutManager.LoadSplitterHeight($"{tabName}_Vert", defaultWidth);
+
+ // Find the grid for the specified tab and set the row width
+ if (tabName == "ActivePlayers")
+ {
+ gridColumn = ActivePlayerGridColumn;
+ }
+ else if (tabName == "PastPlayers")
+ {
+ gridColumn = PastPlayerGridColumn;
+ }
+
+ if (gridColumn != null)
+ {
+ logger.Debug($"Attempting to load splitter width for Col Splitter {tabName}, width to {width}");
+ if (width == -1)
+ {
+ gridColumn.Width = new GridLength(1, GridUnitType.Star);
+ }
+ else
+ {
+ gridColumn.Width = new GridLength(width, GridUnitType.Pixel);
+ }
+ }
+ }
+
+
+
+ private void SubscribeToColumnWidthChanges()
+ {
+ // Subscribe to GridView column width changes
+ SubscribeToGridViewColumnChanges(ActiveListView);
+ SubscribeToGridViewColumnChanges(PastListView);
+ SubscribeToGridViewColumnChanges(OpenLogsListView);
+
+ // Subscribe to DataGrid column width changes
+ SubscribeToDataGridColumnChanges(AvatarDbGrid);
+ SubscribeToDataGridColumnChanges(GroupDbGrid);
+ SubscribeToDataGridColumnChanges(UserDbGrid);
+ }
+
+ private void SubscribeToGridViewColumnChanges(System.Windows.Controls.ListView listView)
+ {
+ if (listView.View is GridView gridView)
+ {
+ foreach (var column in gridView.Columns)
+ {
+ var dpd = DependencyPropertyDescriptor.FromProperty(
+ GridViewColumn.WidthProperty,
+ typeof(GridViewColumn));
+
+ dpd?.AddValueChanged(column, (s, e) =>
+ {
+ if (s is GridViewColumn col && col.Header is GridViewColumnHeader header)
+ {
+ string columnName = (header.Tag as string) ?? header.Content?.ToString() ?? "";
+ if (!string.IsNullOrEmpty(columnName))
+ {
+ WindowLayoutManager.SaveColumnWidth(
+ $"{listView.Name}_{columnName}",
+ col.ActualWidth);
+ }
+ }
+ });
+ }
+ }
+ }
+
+ private void SubscribeToDataGridColumnChanges(System.Windows.Controls.DataGrid dataGrid)
+ {
+ foreach (var column in dataGrid.Columns)
+ {
+ var dpd = DependencyPropertyDescriptor.FromProperty(
+ DataGridColumn.ActualWidthProperty,
+ typeof(DataGridColumn));
+
+ dpd?.AddValueChanged(column, (s, e) =>
+ {
+ if (s is DataGridColumn col)
+ {
+ string columnName = col.Header?.ToString() ?? "";
+ if (!string.IsNullOrEmpty(columnName))
+ {
+ WindowLayoutManager.SaveColumnWidth(
+ $"{dataGrid.Name}_{columnName}",
+ col.ActualWidth);
+ }
+ }
+ });
+ }
+ }
+
+ private void GridSplitter_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
+ {
+ if (sender is GridSplitter splitter && splitter.Parent is Grid grid)
+ {
+ string splitterName = splitter.Name;
+ if (string.IsNullOrEmpty(splitterName))
+ {
+ logger.Warn("GridSplitter has no name, cannot save position");
+ return;
+ }
+
+ string resizeDirection = splitter.ResizeDirection.ToString();
+ logger.Debug($"Grid Splitter '{splitterName}' dragged, ResizeDirection: {resizeDirection}");
+
+ if (resizeDirection == "Rows")
+ {
+ // Horizontal splitter - save row width
+ int rowIndex = Grid.GetRow(splitter);
+ if (rowIndex + 1 < grid.RowDefinitions.Count)
+ {
+ var rowDef = grid.RowDefinitions[rowIndex + 1];
+ if (rowDef.Height.IsStar || rowDef.Height.IsAbsolute)
+ {
+ double height = rowDef.Height.IsStar ? rowDef.Height.Value : rowDef.ActualHeight;
+ WindowLayoutManager.SaveSplitterPosition(splitterName, height);
+ logger.Debug($"Saved row width for splitter '{splitterName}': {height}");
+ }
+ }
+ }
+ else if (resizeDirection == "Columns")
+ {
+ // Vertical splitter - save column width
+ int colIndex = Grid.GetColumn(splitter);
+ if (colIndex == 1)
+ {
+ var colDef = grid.ColumnDefinitions[0];
+ if (colDef.Width.IsStar || colDef.Width.IsAbsolute)
+ {
+ double width = colDef.Width.IsStar ? colDef.Width.Value : colDef.ActualWidth;
+ WindowLayoutManager.SaveSplitterPosition(splitterName, width);
+ logger.Debug($"Saved column width for splitter '{splitterName}': {width}");
+ }
+ }
+ }
+ }
+ }
+
+ private static IEnumerable FindVisualChildren(DependencyObject depObj) where T : DependencyObject
+ {
+ if (depObj != null)
+ {
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
+ {
+ DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
+ if (child is T t)
+ {
+ yield return t;
+ }
+
+ foreach (T childOfChild in FindVisualChildren(child))
+ {
+ yield return childOfChild;
+ }
+ }
+ }
+ }
+
+ private void ResetLayout_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ var result = System.Windows.MessageBox.Show(
+ "This will reset the window size, position, all column widths, and splitter positions to their default values. Do you want to continue?",
+ "Reset Layout",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result == MessageBoxResult.Yes)
+ {
+ WindowLayoutManager.ResetLayoutSettings();
+
+ System.Windows.MessageBox.Show(
+ "Layout settings have been reset. Please restart the application for changes to take effect.",
+ "Reset Complete",
+ MessageBoxButton.OK,
+ MessageBoxImage.Information);
+
+ logger.Info("Layout settings reset to defaults");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to reset layout settings");
+ System.Windows.MessageBox.Show(
+ $"Failed to reset layout: {ex.Message}",
+ "Error",
+ MessageBoxButton.OK,
+ MessageBoxImage.Error);
+ }
+ }
+
+ #endregion
+
private void ApplyFilter(ICollectionView view, string filterText)
{
@@ -2236,6 +3221,395 @@ private void ApplyFilter(ICollectionView view, string filterText)
view.Refresh();
}
+ #region Ban Management handlers
+ private ObservableCollection _banMgmtGroupList = new ObservableCollection();
+ private string _currentBanMgmtUserId = string.Empty;
+ private User? _currentBanMgmtUser = null;
+
+ private async void BanMgmtLoadUser_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ string userId = BanMgmtUserIdTextBox.Text.Trim();
+
+ if (string.IsNullOrWhiteSpace(userId))
+ {
+ BanMgmtUserStatusText.Text = "Please enter a User ID";
+ BanMgmtUserStatusText.Foreground = System.Windows.Media.Brushes.Yellow;
+ return;
+ }
+
+ if (!userId.StartsWith("usr_"))
+ {
+ BanMgmtUserStatusText.Text = "Invalid User ID format (must start with usr_)";
+ BanMgmtUserStatusText.Foreground = System.Windows.Media.Brushes.Red;
+ return;
+ }
+
+ BanMgmtUserStatusText.Text = "Loading...";
+ BanMgmtUserStatusText.Foreground = System.Windows.Media.Brushes.Yellow;
+
+ // Call GetProfile
+ var user = _serviceRegistry.GetVRChatAPIClient().GetProfile(userId);
+
+ if (user == null || string.IsNullOrEmpty(user.Id))
+ {
+ BanMgmtUserStatusText.Text = "User not found";
+ BanMgmtUserStatusText.Foreground = System.Windows.Media.Brushes.Red;
+ BanMgmtUserInfoGroup.Visibility = Visibility.Collapsed;
+ return;
+ }
+
+ _currentBanMgmtUser = user;
+ _currentBanMgmtUserId = userId;
+
+ logger.Info($"Fetched user profile for ban management: {user.DisplayName} ({user})");
+
+ // Populate user info
+ BanMgmtUserName.Text = user.DisplayName ?? "Unknown";
+ BanMgmtUserStatusDesc.Text = user.StatusDescription ?? user.Status.ToString();
+ BanMgmtUserPronouns.Text = string.IsNullOrEmpty(user.Pronouns) ? "Not specified" : user.Pronouns;
+ BanMgmtUserJoinDate.Text = user.DateJoined.ToString("yyyy-MM-dd");
+ BanMgmtUserAgeVerified.Text = user.AgeVerified ? "Yes" : "No";
+ BanMgmtUserState.Text = user.State.ToString() ;
+
+
+ string? accountThumbnailUrl = !string.IsNullOrEmpty(user.ProfilePicOverrideThumbnail) ? user.ProfilePicOverrideThumbnail : user.CurrentAvatarThumbnailImageUrl;
+
+ // Load profile image if available
+ if (!string.IsNullOrEmpty(accountThumbnailUrl))
+ {
+ try
+ {
+ var bitmap = new System.Windows.Media.Imaging.BitmapImage();
+ bitmap.BeginInit();
+ bitmap.UriSource = new Uri(accountThumbnailUrl);
+ bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
+ bitmap.EndInit();
+ BanMgmtUserImage.Source = bitmap;
+ }
+ catch
+ {
+ BanMgmtUserImage.Source = null;
+ }
+ }
+ else
+ {
+ BanMgmtUserImage.Source = null;
+ }
+
+ BanMgmtUserStatusText.Text = "User loaded successfully";
+ BanMgmtUserStatusText.Foreground = System.Windows.Media.Brushes.LightGreen;
+ BanMgmtUserInfoGroup.Visibility = Visibility.Visible;
+ BanMgmtGroupListGroup.Visibility = Visibility.Visible;
+
+ // Load groups from database
+ await LoadBanManagementGroupsAsync();
+
+ logger.Info($"Loaded user profile for ban management: {user.DisplayName} ({userId})");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error loading user for ban management");
+ BanMgmtUserStatusText.Text = $"Error: {ex.Message}";
+ BanMgmtUserStatusText.Foreground = System.Windows.Media.Brushes.Red;
+ }
+ }
+
+ private async Task LoadBanManagementGroupsAsync()
+ {
+ try
+ {
+ _banMgmtGroupList.Clear();
+
+ // Load all groups from the database
+ var groups = _serviceRegistry.GetDBContext().GroupManagements.ToList();
+
+ foreach (var group in groups)
+ {
+ var item = new GroupBanItem
+ {
+ GroupId = group.GroupId,
+ GroupName = group.GroupName,
+ Status = "Checking...",
+ CanBan = false,
+ CanUnban = false
+ };
+
+ _banMgmtGroupList.Add(item);
+
+ // Check member status asynchronously
+ _ = Task.Run(async () =>
+ {
+ var status = await _serviceRegistry.GetVRChatAPIClient().GetGroupMemberStatus(group.GroupId, _currentBanMgmtUserId);
+
+ await Dispatcher.InvokeAsync(() =>
+ {
+ item.Status = status switch
+ {
+ VRChatClient.TGGroupMemberStatus.Member => "Member",
+ VRChatClient.TGGroupMemberStatus.Banned => "Banned",
+ VRChatClient.TGGroupMemberStatus.NotMember => "Not Member",
+ _ => "Unknown"
+ };
+
+ item.CanBan = status != VRChatClient.TGGroupMemberStatus.Banned && status != VRChatClient.TGGroupMemberStatus.Unknown;
+ item.CanUnban = status == VRChatClient.TGGroupMemberStatus.Banned;
+ });
+ });
+ }
+
+ BanMgmtGroupList.ItemsSource = _banMgmtGroupList;
+ logger.Info($"Loaded {_banMgmtGroupList.Count} groups for ban management");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error loading ban management groups");
+ }
+ }
+
+ private async void BanMgmtAddGroup_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ TailgrabDBContext dBContext = _serviceRegistry.GetDBContext();
+ string groupId = BanMgmtAddGroupIdTextBox.Text.Trim();
+
+ if (string.IsNullOrWhiteSpace(groupId))
+ {
+ System.Windows.MessageBox.Show("Please enter a Group ID",
+ "Invalid Input", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ if (!groupId.StartsWith("grp_"))
+ {
+ System.Windows.MessageBox.Show("Invalid Group ID format (must start with grp_)",
+ "Invalid Input", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // Check if group already exists in database
+ var existingGroup = dBContext.GroupManagements.FirstOrDefault(g => g.GroupId == groupId);
+
+ if (existingGroup != null)
+ {
+ System.Windows.MessageBox.Show("This group already exists in the database",
+ "Duplicate Group", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ // Verify group exists in VRChat
+ var group = _serviceRegistry.GetVRChatAPIClient().GetGroupById(groupId);
+ if (group == null || string.IsNullOrEmpty(group.Id))
+ {
+ System.Windows.MessageBox.Show("Group not found in VRChat. Please verify the Group ID.",
+ "Group Not Found", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+
+ // Add to database
+ var newGroup = new tailgrab.src.Models.GroupManagement
+ {
+ GroupId = groupId,
+ GroupName = group.Name ?? "Unknown",
+ CreatedAt = DateTime.UtcNow,
+ UpdatedAt = DateTime.UtcNow
+ };
+
+ dBContext.Add(newGroup);
+ dBContext.SaveChanges();
+
+ // Add to the UI list
+ var item = new GroupBanItem
+ {
+ GroupId = groupId,
+ GroupName = group.Name ?? "Unknown",
+ Status = "Checking...",
+ CanBan = false,
+ CanUnban = false
+ };
+
+ _banMgmtGroupList.Add(item);
+
+ // Check member status
+ var status = await _serviceRegistry.GetVRChatAPIClient().GetGroupMemberStatus(groupId, _currentBanMgmtUserId);
+
+ item.Status = status switch
+ {
+ VRChatClient.TGGroupMemberStatus.Member => "Member",
+ VRChatClient.TGGroupMemberStatus.Banned => "Banned",
+ VRChatClient.TGGroupMemberStatus.NotMember => "Not Member",
+ _ => "Unknown"
+ };
+
+ item.CanBan = status != VRChatClient.TGGroupMemberStatus.Banned && status != VRChatClient.TGGroupMemberStatus.Unknown;
+ item.CanUnban = status == VRChatClient.TGGroupMemberStatus.Banned;
+
+ // Clear the text box
+ BanMgmtAddGroupIdTextBox.Text = string.Empty;
+
+ logger.Info($"Added group {groupId} ({group.Name}) to ban management");
+ System.Windows.MessageBox.Show($"Group '{group.Name}' added successfully",
+ "Success", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error adding group to ban management");
+ System.Windows.MessageBox.Show($"Error: {ex.Message}",
+ "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void BanMgmtRemoveGroup_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ TailgrabDBContext dBContext = _serviceRegistry.GetDBContext();
+ if (sender is System.Windows.Controls.Button button && button.Tag is GroupBanItem item)
+ {
+ var result = System.Windows.MessageBox.Show(
+ $"Are you sure you want to remove group '{item.GroupName}' from the list?\n\nThis will remove it from the database.",
+ "Confirm Remove",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result != MessageBoxResult.Yes)
+ {
+ return;
+ }
+
+ // Remove from database
+ var dbGroup = dBContext.GroupManagements.FirstOrDefault(g => g.GroupId == item.GroupId);
+
+ if (dbGroup != null)
+ {
+ dBContext.GroupManagements.Remove(dbGroup);
+ dBContext.SaveChanges();
+ }
+
+ // Remove from UI
+ _banMgmtGroupList.Remove(item);
+
+ logger.Info($"Removed group {item.GroupId} ({item.GroupName}) from ban management");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error removing group from ban management");
+ System.Windows.MessageBox.Show($"Error: {ex.Message}",
+ "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private async void BanMgmtBanUser_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (sender is System.Windows.Controls.Button button && button.Tag is GroupBanItem item)
+ {
+ if (string.IsNullOrWhiteSpace(item.GroupId) || string.IsNullOrWhiteSpace(_currentBanMgmtUserId))
+ {
+ return;
+ }
+
+ var result = MessageBoxResult.Yes;
+ //var result = System.Windows.MessageBox.Show(
+ // $"Are you sure you want to ban {_currentBanMgmtUser?.DisplayName} from group {item.GroupName}?",
+ // "Confirm Ban",
+ // MessageBoxButton.YesNo,
+ // MessageBoxImage.Question);
+
+ if (result != MessageBoxResult.Yes)
+ {
+ return;
+ }
+
+ button.IsEnabled = false;
+ item.Status = "Banning...";
+
+ bool success = await _serviceRegistry.GetVRChatAPIClient().BanUserFromGroup(item.GroupId, _currentBanMgmtUserId);
+
+ if (success)
+ {
+ item.Status = "Banned";
+ item.CanBan = false;
+ item.CanUnban = true;
+ logger.Info($"Banned user {_currentBanMgmtUserId} from group {item.GroupId}");
+ //System.Windows.MessageBox.Show("User banned successfully", "Success",
+ // MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ else
+ {
+ item.Status = "Ban Failed";
+ button.IsEnabled = true;
+ //System.Windows.MessageBox.Show("Failed to ban user", "Error",
+ // MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error banning user from group");
+ System.Windows.MessageBox.Show($"Error: {ex.Message}", "Error",
+ MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private async void BanMgmtUnbanUser_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ if (sender is System.Windows.Controls.Button button && button.Tag is GroupBanItem item)
+ {
+ if (string.IsNullOrWhiteSpace(item.GroupId) || string.IsNullOrWhiteSpace(_currentBanMgmtUserId))
+ {
+ return;
+ }
+ var result = MessageBoxResult.Yes;
+ //var result = System.Windows.MessageBox.Show(
+ // $"Are you sure you want to unban {_currentBanMgmtUser?.DisplayName} from group {item.GroupName}?",
+ // "Confirm Unban",
+ // MessageBoxButton.YesNo,
+ // MessageBoxImage.Question);
+
+ if (result != MessageBoxResult.Yes)
+ {
+ return;
+ }
+
+ button.IsEnabled = false;
+ item.Status = "Unbanning...";
+
+ bool success = await _serviceRegistry.GetVRChatAPIClient().UnbanUserFromGroup(item.GroupId, _currentBanMgmtUserId);
+
+ if (success)
+ {
+ item.Status = "Not Member";
+ item.CanBan = true;
+ item.CanUnban = false;
+ logger.Info($"Unbanned user {_currentBanMgmtUserId} from group {item.GroupId}");
+ //System.Windows.MessageBox.Show("User unbanned successfully", "Success",
+ // MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ else
+ {
+ item.Status = "Unban Failed";
+ button.IsEnabled = true;
+ //System.Windows.MessageBox.Show("Failed to unban user", "Error",
+ // MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Error unbanning user from group");
+ System.Windows.MessageBox.Show($"Error: {ex.Message}", "Error",
+ MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+ #endregion
+
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
@@ -2271,8 +3645,8 @@ public class PlayerViewModel : INotifyPropertyChanged
public bool IsWatched { get; set; } = false;
public string History { get; set; } = string.Empty;
public string AlertMessages { get; set; } = string.Empty;
- public ObservableCollection Prints { get; private set; } = new ObservableCollection();
- public ObservableCollection Emojis { get; private set; } = new ObservableCollection();
+ public ObservableCollection Prints { get; private set; } = [];
+ public ObservableCollection Emojis { get; private set; } = [];
private bool IsFriend { get; set; }
private string _AlertColor = "Normal";
@@ -2393,27 +3767,16 @@ private void PopulateCollectionsFromPlayer(Player p)
}
}
- public class PrintInfoViewModel : INotifyPropertyChanged
+ public class PrintInfoViewModel(PlayerPrint p) : INotifyPropertyChanged
{
- public string PrintId { get; set; }
- public string OwnerId { get; set; }
- public DateTime CreatedAt { get; set; }
- public DateTime Timestamp { get; set; }
- public string PrintUrl { get; set; }
- public string AIEvaluation { get; set; }
- public string AIClass { get; set; }
- public string AuthorName { get; set; }
- public PrintInfoViewModel(PlayerPrint p)
- {
- PrintId = p.PrintId;
- OwnerId = p.OwnerId;
- CreatedAt = p.CreatedAt;
- Timestamp = p.Timestamp;
- PrintUrl = p.PrintUrl;
- AuthorName = p.AuthorName;
- AIEvaluation = p.AIEvaluation;
- AIClass = p.AIClass;
- }
+ public string PrintId { get; set; } = p.PrintId;
+ public string OwnerId { get; set; } = p.OwnerId;
+ public DateTime CreatedAt { get; set; } = p.CreatedAt;
+ public DateTime Timestamp { get; set; } = p.Timestamp;
+ public string PrintUrl { get; set; } = p.PrintUrl;
+ public string AIEvaluation { get; set; } = p.AIEvaluation;
+ public string AIClass { get; set; } = p.AIClass;
+ public string AuthorName { get; set; } = p.AuthorName;
public event PropertyChangedEventHandler? PropertyChanged;
@@ -2423,35 +3786,58 @@ protected void OnPropertyChanged(string propertyName)
}
}
- public class EmojiInfoViewModel
+ public class EmojiInfoViewModel(string userId, PlayerInventory i)
{
- public string UserId { get; set; }
- public string InventoryId { get; set; }
- public DateTime SpawnedAt { get; set; }
- public string ImageUrl { get; set; }
- public string InventoryType { get; set; }
- public string AIEvalutation { get; set; }
- public EmojiInfoViewModel(string userId, PlayerInventory i)
- {
- UserId = userId;
- InventoryId = i.InventoryId;
- SpawnedAt = i.SpawnedAt;
- ImageUrl = i.ItemUrl;
- InventoryType = i.InventoryType;
- AIEvalutation = i.AIEvaluation;
- }
+ public string UserId { get; set; } = userId;
+ public string InventoryId { get; set; } = i.InventoryId;
+ public DateTime SpawnedAt { get; set; } = i.SpawnedAt;
+ public string ImageUrl { get; set; } = i.ItemUrl;
+ public string InventoryType { get; set; } = i.InventoryType;
+ public string AIEvalutation { get; set; } = i.AIEvaluation;
}
- public class ReportReasonItem
+ public class TailTaskViewModel : INotifyPropertyChanged
{
- public string DisplayName { get; set; }
- public string Value { get; set; }
+ private readonly FileTailStatus _status;
+
+ public string FilePath => _status.FilePath;
+ public string FileName => Path.GetFileName(_status.FilePath);
+ public DateTime StartTime => _status.StartTime;
+ public int LinesProcessed => _status.LinesProcessed;
+ public DateTime? LastLineProcessedTime => _status.LastLineProcessedTime;
- public ReportReasonItem(string displayName, string value)
+ public string LastLineProcessedTimeFormatted =>
+ LastLineProcessedTime.HasValue ? LastLineProcessedTime.Value.ToString("u") : "N/A";
+
+ public TailTaskViewModel(FileTailStatus status)
+ {
+ _status = status;
+ }
+
+ public void UpdateFromStatus(FileTailStatus status)
{
- DisplayName = displayName;
- Value = value;
+ OnPropertyChanged(nameof(LinesProcessed));
+ OnPropertyChanged(nameof(LastLineProcessedTime));
+ OnPropertyChanged(nameof(LastLineProcessedTimeFormatted));
}
+
+ public void RequestCancellation()
+ {
+ _status.RequestCancellation();
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ public class ReportReasonItem(string displayName, string value)
+ {
+ public string DisplayName { get; set; } = displayName;
+ public string Value { get; set; } = value;
}
#endregion
}
\ No newline at end of file
diff --git a/src/Program.cs b/src/Program.cs
index b89c3b0..9e9ff81 100644
--- a/src/Program.cs
+++ b/src/Program.cs
@@ -2,16 +2,15 @@
using Microsoft.Win32;
using NLog;
using System.IO;
+using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Windows;
using System.Windows.Media;
-using Tailgrab.Clients.VRChat;
using Tailgrab.Common;
using Tailgrab.Configuration;
using Tailgrab.LineHandler;
-using Tailgrab.Models;
using Tailgrab.PlayerManagement;
namespace Tailgrab;
@@ -48,6 +47,8 @@ public class FileTailer
// At the class level, add a dictionary to track active tail tasks
static Dictionary ActiveTailTasks = new Dictionary();
+ public static IReadOnlyDictionary GetActiveTailTasks() => ActiveTailTasks;
+
///
/// Watch the VRChat log directory by default and process logs.
/// Show the TailgrabPanel UI on the STA thread before continuing to watch files.
@@ -135,6 +136,9 @@ public static void Main(string[] args)
//SyncAvatarModerations(_serviceRegistry);
+ // Check for updates before showing the main window
+ _ = Task.Run(async () => await CheckForUpdatesAsync());
+
BuildAppWindow(_serviceRegistry);
// When the window closes, allow Main to complete. The watcher task will be abandoned; if desired add cancellation.
@@ -142,8 +146,9 @@ public static void Main(string[] args)
///
/// Threaded tailing of a file, reading new lines as they are added.
+ /// Returns the FileTailStatus immediately and processes the file in the background.
///
- public static async Task TailFileAsync(string filePath)
+ public static FileTailStatus? TailFileAsync(string filePath)
{
if (OpenedFiles.Contains(filePath))
{
@@ -153,62 +158,86 @@ public static async Task TailFileAsync(string filePath)
var status = new FileTailStatus(filePath);
logger.Info($"Tailing file: {filePath}");
- using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
- using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
- {
- // Start at the end of the file
- long lastMaxOffset = fs.Length;
- fs.Seek(lastMaxOffset, SeekOrigin.Begin);
-
- OpenedFiles.Add(filePath);
- WatchedFiles[filePath] = new FileWatchItem(lastMaxOffset);
+ OpenedFiles.Add(filePath);
- while (!status.IsCancellationRequested)
+ // Start the file tailing process in the background
+ _ = Task.Run(async () =>
+ {
+ try
{
- // If the file size hasn't changed, wait
- if (fs.Length == lastMaxOffset)
+ using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ using (StreamReader sr = new StreamReader(fs, Encoding.UTF8))
{
- if (WatchedFiles.ContainsKey(filePath))
+ // Start at the end of the file
+ long lastMaxOffset = fs.Length;
+ fs.Seek(lastMaxOffset, SeekOrigin.Begin);
+
+ WatchedFiles[filePath] = new FileWatchItem(lastMaxOffset);
+
+ while (!status.IsCancellationRequested)
{
- WatchedFiles[filePath].ElapsedTime += 1;
- if (WatchedFiles[filePath].ElapsedTime >= 9000) // If we've been watching this file for 15 minutes without changes
+ // If the file size hasn't changed, wait
+ if (fs.Length == lastMaxOffset)
{
- logger.Info($"Timeout waiting for new lines in '{filePath}'");
- break;
+ if (WatchedFiles.ContainsKey(filePath))
+ {
+ WatchedFiles[filePath].ElapsedTime += 1;
+ if (WatchedFiles[filePath].ElapsedTime >= 9000) // If we've been watching this file for 15 minutes without changes
+ {
+ logger.Info($"Timeout waiting for new lines in '{filePath}'");
+ break;
+ }
+ }
+
+ await Task.Delay(100, status.CancellationSource.Token).ConfigureAwait(false);
+ continue;
}
- }
- await Task.Delay(100, status.CancellationSource.Token).ConfigureAwait(false);
- continue;
- }
+ // Read and display new lines
+ string? line;
+ while ((line = await sr.ReadLineAsync().ConfigureAwait(false)) != null)
+ {
+ if (status.IsCancellationRequested)
+ break;
- // Read and display new lines
- string? line;
- while ((line = await sr.ReadLineAsync().ConfigureAwait(false)) != null)
- {
- if (status.IsCancellationRequested)
- break;
+ foreach (ILineHandler handler in HandlerList)
+ {
+ if (handler.HandleLine(line))
+ {
+ break;
+ }
+ }
- foreach (ILineHandler handler in HandlerList)
- {
- if (handler.HandleLine(line))
- {
- break;
+ status.IncrementLinesProcessed();
}
- }
- status.IncrementLinesProcessed();
- }
+ // Update the offset to the new end of the file
+ lastMaxOffset = fs.Length;
- // Update the offset to the new end of the file
- lastMaxOffset = fs.Length;
+ // Reset the watch counter for this file since we have new data
+ WatchedFiles.Remove(filePath);
+ }
+ }
- // Reset the watch counter for this file since we have new data
+ logger.Info($"Stopped tailing file: {filePath}. Total lines processed: {status.LinesProcessed}");
+ }
+ catch (OperationCanceledException)
+ {
+ logger.Info($"Tailing cancelled for: {filePath}. Total lines processed: {status.LinesProcessed}");
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, $"Error tailing file: {filePath}");
+ }
+ finally
+ {
+ // Clean up
+ OpenedFiles.Remove(filePath);
WatchedFiles.Remove(filePath);
+ ActiveTailTasks.Remove(filePath);
}
- }
+ });
- logger.Info($"Stopped tailing file: {filePath}. Total lines processed: {status.LinesProcessed}");
return status;
}
@@ -223,7 +252,7 @@ public static async Task WatchPath(string path, ServiceRegistry _serviceRegistry
LogWatcher.Created += async (source, e) =>
{
- var status = await TailFileAsync(e.FullPath);
+ var status = TailFileAsync(e.FullPath);
if (status != null)
{
ActiveTailTasks[e.FullPath] = status;
@@ -232,7 +261,7 @@ public static async Task WatchPath(string path, ServiceRegistry _serviceRegistry
LogWatcher.Changed += async (source, e) =>
{
- var status = await TailFileAsync(e.FullPath);
+ var status = TailFileAsync(e.FullPath);
if (status != null)
{
ActiveTailTasks[e.FullPath] = status;
@@ -250,9 +279,9 @@ public static async Task WatchPath(string path, ServiceRegistry _serviceRegistry
foreach (var f in existing)
{
logger.Debug($"Found File to Tail: {f}");
- _ = Task.Run(async () =>
+ _ = Task.Run(() =>
{
- var status = await TailFileAsync(f);
+ var status = TailFileAsync(f);
if (status != null)
{
ActiveTailTasks[f] = status;
@@ -310,7 +339,7 @@ public static async Task ProcessAmplitudeCache(string filePath, ServiceRegistry
}
}
- ServiceRegistryInstance.GetAvatarManager().CacheAvatars(avatarIds);
+ PlayerManager.CacheAvatars(avatarIds);
}
}
}
@@ -497,6 +526,79 @@ private static void CreateDatabaseBackup()
}
}
+ ///
+ /// Check GitHub for new releases and notify the user if a newer version is available.
+ ///
+ private static async Task CheckForUpdatesAsync()
+ {
+ try
+ {
+ using var httpClient = new HttpClient();
+ httpClient.DefaultRequestHeaders.Add("User-Agent", "Tailgrab-Update-Checker");
+
+ var response = await httpClient.GetAsync("https://api.github.com/repos/jlong23/Tailgrab/releases/latest");
+
+ if (!response.IsSuccessStatusCode)
+ {
+ logger.Debug($"Failed to check for updates. Status: {response.StatusCode}");
+ return;
+ }
+
+ var json = await response.Content.ReadAsStringAsync();
+ using var doc = JsonDocument.Parse(json);
+ var root = doc.RootElement;
+
+ if (!root.TryGetProperty("tag_name", out var tagNameElement))
+ {
+ logger.Debug("No tag_name found in GitHub release response");
+ return;
+ }
+
+ if (!root.TryGetProperty("name", out var releaseNameElement))
+ {
+ logger.Debug("No tag_name found in GitHub release response");
+ return;
+ }
+
+ string latestVersion = tagNameElement.GetString() ?? "";
+ string latestVersionName = releaseNameElement.GetString() ?? "";
+ string currentVersion = BuildInfo.GetInformationalVersion();
+
+ // Remove 'v' prefix if present for comparison
+ latestVersion = latestVersion.TrimStart('v');
+ currentVersion = currentVersion.TrimStart('v');
+
+ // Parse and compare versions
+ if (Version.TryParse(latestVersion, out var latest) &&
+ Version.TryParse(currentVersion.Split('+')[0], out var current))
+ {
+ if (latest > current)
+ {
+ logger.Info($"New version available: {latestVersion} (current: {currentVersion})");
+
+ // Show update notification on the UI thread
+ System.Windows.Application.Current?.Dispatcher.Invoke(() =>
+ {
+ var dialog = new UpdateNotificationDialog(latestVersion, latestVersionName, currentVersion);
+ dialog.ShowDialog();
+ });
+ }
+ else
+ {
+ logger.Debug($"Already on latest version: {currentVersion}");
+ }
+ }
+ else
+ {
+ logger.Debug($"Failed to parse versions. Latest: {latestVersion}, Current: {currentVersion}");
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Warn(ex, "Failed to check for updates");
+ }
+ }
+
private static void BuildAppWindow(ServiceRegistry serviceRegistryInstance)
{
// Start WPF application and show the TailgrabPanel on this STA thread
@@ -747,6 +849,32 @@ private static void BuildAppWindow(ServiceRegistry serviceRegistryInstance)
app.Resources[typeof(System.Windows.Window)] = windowStyle;
var panel = new TailgrabPanel(serviceRegistryInstance);
+
+ try
+ {
+ string LayoutRegistryPath = "Software\\DeviousFox\\Tailgrab\\Layout";
+ using (var key = Registry.CurrentUser.OpenSubKey(LayoutRegistryPath))
+ {
+ if (key != null)
+ {
+ var width = key.GetValue("WindowWidth");
+ var height = key.GetValue("WindowHeight");
+
+ if (width != null && height != null)
+ {
+ panel.Width = Convert.ToDouble(width);
+ panel.Height = Convert.ToDouble(height);
+ logger.Debug($"Loaded window size: {panel.Width}x{panel.Height}");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.Error(ex, "Failed to load window size from registry.");
+ }
+
+
app.Run(panel);
}
}
diff --git a/src/ServiceRegistry.cs b/src/ServiceRegistry.cs
index 0e36d4b..9c01bee 100644
--- a/src/ServiceRegistry.cs
+++ b/src/ServiceRegistry.cs
@@ -2,7 +2,6 @@
using Microsoft.Extensions.DependencyInjection;
using NLog;
using System.IO;
-using Tailgrab.AvatarManagement;
using Tailgrab.Clients.Ollama;
using Tailgrab.Clients.VRChat;
using Tailgrab.Common;
@@ -16,9 +15,10 @@ public class ServiceRegistry
{
TailgrabDBContext? dbContext = null;
VRChatClient vrcAPIClient = new VRChatClient();
- AvatarManagementService? avatarManager = null;
PlayerManager? playerManager = null;
OllamaClient? ollamaAPIClient = null;
+ AvatarBosGistListManager? avatarGistMgr = null;
+ GroupBosGistListManager? groupGistMgr = null;
static Logger logger = LogManager.GetCurrentClassLogger();
ServiceCollection services = new ServiceCollection();
@@ -56,9 +56,6 @@ public async void StartAllServices()
logger.Info("Starting OLLama API Client...");
ollamaAPIClient = new OllamaClient(this);
- logger.Info("Starting Avatar Manager...");
- avatarManager = new AvatarManagementService(this);
-
logger.Info("Starting Player Manager...");
playerManager = new PlayerManager(this);
@@ -69,11 +66,11 @@ public async void StartAllServices()
}
logger.Info("Starting Avatar GIST Manager...");
- AvatarBosGistListManager avatarGistMgr = new AvatarBosGistListManager(avatarManager);
+ avatarGistMgr = new AvatarBosGistListManager();
_ = Task.Run(() => avatarGistMgr.ProcessAvatarGistList());
logger.Info("Starting Group GIST Manager...");
- GroupBosGistListManager groupGistMgr = new GroupBosGistListManager(dbContext, playerManager);
+ groupGistMgr = new GroupBosGistListManager(dbContext, playerManager);
_ = Task.Run(() => groupGistMgr.ProcessGroupGistList());
logger.Info("All services started.");
@@ -118,13 +115,34 @@ public OllamaClient GetOllamaAPIClient()
return ollamaAPIClient;
}
- public AvatarManagementService GetAvatarManager()
+ public async void ProcessAvatarGist()
+ {
+ if (avatarGistMgr == null)
+ {
+ logger.Info("Avatar GIST Manager not initialized, creating new instance...");
+ avatarGistMgr = new AvatarBosGistListManager();
+ }
+
+ logger.Info("Processing Avatar GIST list on demand...");
+ await avatarGistMgr.ProcessAvatarGistList();
+ logger.Info("Avatar GIST list processing completed.");
+ }
+
+ public async void ProcessGroupGist()
{
- if (avatarManager == null)
+ if (groupGistMgr == null)
{
- throw new InvalidOperationException("Avatar Manager has not been initialized. Call StartAllServices() first.");
+ if (dbContext == null || playerManager == null)
+ {
+ throw new InvalidOperationException("Database context and Player Manager must be initialized before processing Group GIST.");
+ }
+ logger.Info("Group GIST Manager not initialized, creating new instance...");
+ groupGistMgr = new GroupBosGistListManager(dbContext, playerManager);
}
- return avatarManager;
+
+ logger.Info("Processing Group GIST list on demand...");
+ await groupGistMgr.ProcessGroupGistList();
+ logger.Info("Group GIST list processing completed.");
}
}
}
diff --git a/src/UpdateNotificationDialog.xaml b/src/UpdateNotificationDialog.xaml
new file mode 100644
index 0000000..38dbb92
--- /dev/null
+++ b/src/UpdateNotificationDialog.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UpdateNotificationDialog.xaml.cs b/src/UpdateNotificationDialog.xaml.cs
new file mode 100644
index 0000000..1a900bc
--- /dev/null
+++ b/src/UpdateNotificationDialog.xaml.cs
@@ -0,0 +1,47 @@
+using System.Diagnostics;
+using System.Windows;
+
+namespace Tailgrab;
+
+///
+/// Interaction logic for UpdateNotificationDialog.xaml
+///
+public partial class UpdateNotificationDialog : Window
+{
+ private const string ReleasesUrl = "https://github.com/jlong23/Tailgrab/releases";
+
+ public UpdateNotificationDialog(string latestVersion, string latestVersionName, string currentVersion)
+ {
+ InitializeComponent();
+
+ CurrentVersionText.Text = $"Current version: {currentVersion}";
+ LatestVersionText.Text = $"New version available: {latestVersion}";
+ LatestVersionName.Text = $"{latestVersionName}";
+ }
+
+ private void ViewReleasesButton_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ // Open the releases page in the default browser
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = ReleasesUrl,
+ UseShellExecute = true
+ });
+ }
+ catch (Exception ex)
+ {
+ System.Windows.MessageBox.Show($"Failed to open browser: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ // Close the dialog
+ this.Close();
+ }
+
+ private void IgnoreButton_Click(object sender, RoutedEventArgs e)
+ {
+ // Simply close the dialog
+ this.Close();
+ }
+}
diff --git a/src/configuration/AvatarBosGistListManager.cs b/src/configuration/AvatarBosGistListManager.cs
index 80042ce..073964d 100644
--- a/src/configuration/AvatarBosGistListManager.cs
+++ b/src/configuration/AvatarBosGistListManager.cs
@@ -4,21 +4,19 @@
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
-using Tailgrab.AvatarManagement;
using Tailgrab.Common;
using Tailgrab.Models;
+using Tailgrab.PlayerManagement;
namespace Tailgrab.Configuration
{
public class AvatarBosGistListManager
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
- private readonly AvatarManagementService avatarManager;
private readonly HttpClient _httpClient;
- public AvatarBosGistListManager(AvatarManagementService avatarManagement)
+ public AvatarBosGistListManager()
{
- avatarManager = avatarManagement;
_httpClient = new HttpClient();
}
@@ -234,10 +232,10 @@ private async Task ProcessAvatarIdsAsync(string gistContent)
logger.Warn($"Line {lineNumber}: Invalid AlertType '{avatarAlert}' for Avatar ID '{avatarId}', defaulting to None.");
}
- AvatarInfo? info = avatarManager.GetAvatarById(avatarId);
+ AvatarInfo? info = PlayerManager.GetAvatarById(avatarId);
if (info == null || info.AlertType == AlertTypeEnum.None) {
QueuedAvatarWatch watchItem = new QueuedAvatarWatch(1, avatarId, alertType, lineNumber);
- avatarManager.EnqueueWatchAvatarForCheck(watchItem);
+ PlayerManager.EnqueueWatchAvatarForCheck(watchItem);
}
processedCount++;
}
diff --git a/tailgrab.csproj b/tailgrab.csproj
index 622d533..73cf341 100644
--- a/tailgrab.csproj
+++ b/tailgrab.csproj
@@ -12,15 +12,17 @@
true
enable
src\Resources\tailgrab.ico
-
+
$(MSBuildProjectDirectory)\BuildNumber.txt
$([System.IO.File]::ReadAllText('$(BuildNumberFile)').Trim())
-
-
- 1.1.1.$(BuildNumber)
- 1.1.1.$(BuildNumber)
- 1.1.1.$(BuildNumber)
+ $(MSBuildProjectDirectory)\BuildVersion.txt
+ $([System.IO.File]::ReadAllText('$(VersionNumberFile)').Trim())
+
+
+ $(VersionNumber).$(BuildNumber)
+ $(VersionNumber).$(BuildNumber)
+ $(VersionNumber).$(BuildNumber)
https://github.com/jlong23/Tailgrab
README.md
https://github.com/jlong23/Tailgrab
@@ -28,6 +30,12 @@
AnyCPU;x64
+
+
+ <_Parameter1>Tailgrab.Tests
+
+
+
@@ -120,6 +128,10 @@
+
+
+
+
True
@@ -138,9 +150,9 @@
$(NewBuildNumber)
- 1.1.1.$(BuildNumber)
- 1.1.1.$(BuildNumber)
- 1.1.1.$(BuildNumber)
+ $(VersionNumber).$(BuildNumber)
+ $(VersionNumber).$(BuildNumber)
+ $(VersionNumber).$(BuildNumber)
diff --git a/tailgrab.csproj.user b/tailgrab.csproj.user
deleted file mode 100644
index 3ae7619..0000000
--- a/tailgrab.csproj.user
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
- Designer
-
-
-
\ No newline at end of file
diff --git a/tailgrab.sln b/tailgrab.sln
index 8f9fae7..f358fb6 100644
--- a/tailgrab.sln
+++ b/tailgrab.sln
@@ -1,3 +1,4 @@
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.2.11430.68
@@ -10,24 +11,32 @@ Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Debug|Any CPU.ActiveCfg = Release|Any CPU
{910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Debug|Any CPU.Build.0 = Release|Any CPU
{910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Debug|x64.ActiveCfg = Debug|x64
{910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Debug|x64.Build.0 = Debug|x64
+ {910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Debug|x86.Build.0 = Debug|Any CPU
{910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Release|Any CPU.Build.0 = Release|Any CPU
{910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Release|x64.ActiveCfg = Release|Any CPU
{910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Release|x64.Build.0 = Release|Any CPU
- {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Debug|Any CPU.ActiveCfg = Debug
- {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Debug|x64.ActiveCfg = Debug
- {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Debug|x64.Build.0 = Debug
- {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Release|Any CPU.ActiveCfg = Release
- {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Release|x64.ActiveCfg = Release
- {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Release|x64.Build.0 = Release
+ {910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Release|x86.ActiveCfg = Release|Any CPU
+ {910B279B-72B3-3CFC-4BFE-5A237E51A72C}.Release|x86.Build.0 = Release|Any CPU
+ {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Debug|x64.ActiveCfg = Debug|x64
+ {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Debug|x64.Build.0 = Debug|x64
+ {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Debug|x86.ActiveCfg = Debug|x86
+ {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Release|x64.ActiveCfg = Release|x64
+ {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Release|x64.Build.0 = Release|x64
+ {793BC8C4-7D9E-1942-44B6-88CE35D69FDB}.Release|x86.ActiveCfg = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE