Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
5b44e66
Updating the Titles on Moderation Report Dialogs.
Feb 17, 2026
44271ba
Migrate IsBOS in Avatar/Group to AlertTypeEnum Levels, Turn off Debug…
Feb 18, 2026
7e7809d
Updates to externalize build number & window title.
Feb 18, 2026
f00fb9c
Fixing autoincremented Build Numbering system
Feb 18, 2026
0240729
Configuration and Tab setup for multi alert configuration.
Feb 18, 2026
09c1e29
Combobox Theming fix!
Feb 18, 2026
a5d853e
Multi Alert Stable; Registry Save
Feb 18, 2026
46071ef
Constant Cleanup, Wire Alerts into areas.
Feb 18, 2026
0523abc
Tweaks to support the AlertLevels & Colors on the display lists
Feb 18, 2026
b0271af
Fixes around Alert Reporting, New Sounds, Dark Theme fixes
Feb 19, 2026
d4fbbe9
Update documentation for v1.1.0
Feb 20, 2026
c157108
Minor Formating Issues
Feb 20, 2026
eae7e98
Update README for changes in startup log detection.
Feb 22, 2026
cc740bf
Adding the A380 Retard Sound
Feb 22, 2026
25e4b2e
Updating the base Profile Evaluation prompt for less False Positives …
Feb 22, 2026
45167b0
Adding AI Delay to avoid API Throttling.
Feb 22, 2026
7795311
Correction of code comments
Feb 22, 2026
80386fc
Correction of daily log detection and timeout for performance.
Feb 22, 2026
ee96f9f
Adding Avatar Uploader Name to the Entity
Feb 22, 2026
781990d
Updating the UI for Highlights & Past Player historical reporting.
Feb 22, 2026
2c8b383
Migrate DB Backup to the DBContext Class
Feb 22, 2026
6ad6424
Final? Fix for the Past Player View and Reporting.
Feb 22, 2026
06a3a1b
Final Fix for past profile reporting! Woot!
Feb 23, 2026
fef771d
Moving Logging to AppData/Local/Tailgrab/logs
Feb 23, 2026
7fb855c
Remove BOS boolean for the AlertTypeEnum code, Update for GIST Changes
Feb 23, 2026
eee4773
Code Cleanup Profile
Feb 24, 2026
d6af393
Update Logging Level for Actions
Feb 24, 2026
a73c0ae
Update Icons
Feb 24, 2026
0c6276c
Have VTKs marked on the list as an alert
Feb 25, 2026
d1cf010
Migrating all User Data to AppData/Local/Tailgrab path
Feb 25, 2026
1a95a79
Update Icon to work with the installer
Feb 25, 2026
f758480
Update the Project File
Feb 25, 2026
94d713e
Adding Database Migration panel and functionality
Feb 26, 2026
4f2d0a5
Fix Color code for VTK Alerts
Feb 26, 2026
225ca60
Update Configuration for V1.1.0
Feb 26, 2026
2ee8417
Image update for documentation
Feb 26, 2026
152b460
Documentation Update
Feb 26, 2026
a37426d
Add Moderation Warn/Kick indicators to the Alerts
Feb 26, 2026
c1220ef
Sorting of the Alerts Highest severity decending and then time descen…
Feb 26, 2026
76e885d
Move sounds to the AppData path.
Feb 26, 2026
a3d9773
Update Documentation for Sounds
Feb 26, 2026
4b41bb6
Adding Exception Checking for Migration
Feb 27, 2026
8ead3be
Moved old Console Output to Logging context
Feb 27, 2026
f93f2fd
Update the QuitHandler to print in H:M:S
Feb 27, 2026
e619af0
Format User time seen as H:M in the list
Feb 27, 2026
3aa4efc
Adding Account Elapsed Time
Feb 27, 2026
1712efb
adjustment for Threaded GIST AvatarWatch.
Feb 27, 2026
6d3f09d
Adjust the size of the columns to support age
Feb 27, 2026
c27f918
Update to support a return of task State and control
Feb 27, 2026
7c62155
Migrate the GIST services to part of the service registry.
Feb 27, 2026
4e6876d
Reformat the Elapsed Account Age.
Feb 27, 2026
9b17dfb
Update Database Migrations
Mar 1, 2026
3ed43d3
Update AI Evaluations for user 'Ignore' override
Mar 1, 2026
ecc665b
Update the Readme for viewing the output logs
Mar 1, 2026
ddb697b
Update to remove Pen column
Mar 3, 2026
2db2dd1
Update to display Username on Avatar List
Mar 3, 2026
c97c7e2
A performance fix over existing watched records.
Mar 3, 2026
884fba8
Updates to fix author name when getting from GIST
Mar 3, 2026
2923c51
Update the avatar system to make HideGlobal AV into Nuisance.
Mar 3, 2026
e39f1b4
Update for 'test sound' buttons on Alert Configuration
Mar 3, 2026
65f0289
Fix/Update missing AlertColor relationships.
Mar 4, 2026
222b422
Fix for User Moderation download and Nuisance updates.
Mar 4, 2026
1094364
Standardized the sounds for WAV due to Win10 Limitations.
Mar 4, 2026
91f7765
Update the documentation for V1.1.0
Mar 5, 2026
e6f5db7
Correction of a issue with UTF8 Lowercase and Russian Names
Mar 6, 2026
36a4166
Configuration Failure message.
Mar 7, 2026
dc5567b
Marking friends in the list as Light Green
Mar 7, 2026
738c4f3
Introduce toggles for discovering avatars, groups, add indexing to sp…
Mar 7, 2026
9c2e3b9
Final Build
Mar 8, 2026
530c6d2
Merge branch 'main' into v1.1.1
jlong23 Mar 8, 2026
ebe5708
Fix merge error of double define.
Mar 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BuildNumber.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1976
1988
41 changes: 26 additions & 15 deletions src/Clients/Ollama/Ollama.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal class QueuedProcess : IHavePriority<int>
public int Priority { get; set; }
public string? UserId { get; set; }
public string? UserBio { get; set; }
public bool IsFriend { get; set; }

public string MD5Hash
{
Expand Down Expand Up @@ -116,6 +117,7 @@ public static async Task ProfileCheckTask(ConcurrentPriorityQueue<IHavePriority<
List<LimitedUserGroups> userGroups = serviceRegistry.GetVRChatAPIClient().GetProfileGroups(item.UserId);

string fullProfile = $"DisplayName: {profile.DisplayName}\nStatusDesc: {profile.StatusDescription}\nPronowns: {profile.Pronouns}\nProfileBio: {profile.Bio}\n";
item.IsFriend = profile.IsFriend;
item.UserBio = fullProfile;

serviceRegistry.GetPlayerManager().UpdatePlayerUserFromVRCProfile(profile, item.MD5Hash);
Expand All @@ -139,7 +141,7 @@ public static async Task ProfileCheckTask(ConcurrentPriorityQueue<IHavePriority<
}
else
{
GetEvaluationFromStore(serviceRegistry, evaluated, item.UserId);
GetEvaluationFromStore(serviceRegistry, evaluated, item);
}
}
}
Expand All @@ -165,6 +167,7 @@ public static async Task ProfileCheckTask(ConcurrentPriorityQueue<IHavePriority<

private async static Task<bool> GetUserGroupInformation(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, List<LimitedUserGroups> userGroups, QueuedProcess item)
{
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);
if (player != null)
Expand All @@ -175,19 +178,23 @@ private async static Task<bool> GetUserGroupInformation(ServiceRegistry serviceR
GroupInfo? groupInfo = dBContext.GroupInfos.Find(group.GroupId);
if (groupInfo == null)
{
groupInfo = new GroupInfo
if( saveGroups )
{
GroupId = group.GroupId,
GroupName = group.Name,
AlertType = AlertTypeEnum.None,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
dBContext.GroupInfos.Add(groupInfo);
dBContext.SaveChanges();
groupInfo = new GroupInfo
{
GroupId = group.GroupId,
GroupName = group.Name,
AlertType = AlertTypeEnum.None,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
dBContext.GroupInfos.Add(groupInfo);
dBContext.SaveChanges();
}
}
else
{
// We will update the group name on each lookup in case it changes, but not reset the alert level as that is user defined
groupInfo.GroupName = group.Name;
dBContext.GroupInfos.Update(groupInfo);
dBContext.SaveChanges();
Expand All @@ -199,6 +206,7 @@ private async static Task<bool> GetUserGroupInformation(ServiceRegistry serviceR
maxAlertType = maxAlertType < groupInfo.AlertType ? groupInfo.AlertType : maxAlertType;
}
}

}

if (player != null && player.IsWatched)
Expand Down Expand Up @@ -242,6 +250,7 @@ await ollamaApi.GenerateAsync(request).StreamToEndAsync(responseTask =>
{
player.UserBio = item.UserBio;
player.AIEval = response;
player.IsFriend = item.IsFriend;

ProfileViewUpdate(serviceRegistry, player);
}
Expand All @@ -256,23 +265,25 @@ await ollamaApi.GenerateAsync(request).StreamToEndAsync(responseTask =>
await Task.Delay(5000);
}

private static void GetEvaluationFromStore(ServiceRegistry serviceRegistry, ProfileEvaluation evaluated, string? userId)
private static void GetEvaluationFromStore(ServiceRegistry serviceRegistry, ProfileEvaluation evaluated, QueuedProcess item)
{
if (userId != null)
if (item != null && item.UserId != null)
{

Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(userId ?? string.Empty);
Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(item.UserId);
if (player != null)
{
player.AIEval = System.Text.Encoding.UTF8.GetString(evaluated.Evaluation);
player.UserBio = System.Text.Encoding.UTF8.GetString(evaluated.ProfileText);
player.IsFriend = item.IsFriend;


ProfileViewUpdate(serviceRegistry, player);
logger.Debug($"User profile already processed for userId: {userId}");
logger.Debug($"User profile already processed for userId: {item.UserId}");
}
else
{
logger.Debug($"User profile lookup fails for userId: {userId}");
logger.Debug($"User profile lookup fails for userId: {item.UserId}");
}

}
Expand Down
3 changes: 3 additions & 0 deletions src/Common/CommonConst.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,8 @@ public static class CommonConst
public const string Sound_Alert_Key = "Sound";
public const string Color_Alert_Key = "Color";

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";
}
}
53 changes: 53 additions & 0 deletions src/Common/ConfigStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public static void DeleteSecret(string name)
{
return GetStoredKeyString(CommonConst.ConfigRegistryPath, keyName);
}
public static bool GetStoredKeyBool(string keyName, bool defaultValue)
{
return GetStoredKeyBool(CommonConst.ConfigRegistryPath, keyName, defaultValue);
}

public static string? GetStoredKeyString(string keyPath, string keyName)
{
Expand Down Expand Up @@ -88,12 +92,46 @@ public static void DeleteSecret(string name)
return null;
}
}
public static bool GetStoredKeyBool(string keyPath, string keyName, bool defaultValue )
{
try
{
using (RegistryKey? key = Registry.CurrentUser.OpenSubKey(keyPath))
{
if (key == null)
{
logger.Debug($"Registry key does not exist {keyName}");
return defaultValue;
}

string? value = key.GetValue(keyName) as string;
if (string.IsNullOrEmpty(value))
{
logger.Debug($"No Value stored in registry. {keyName}");
return defaultValue;
}

bool.TryParse(value, out bool result);
return result;
}
}
catch (Exception ex)
{
logger.Error(ex, "Failed to read value from registry.");
return defaultValue;
}
}

public static void PutStoredKeyString(string keyName, string keyValue)
{
PutStoredKeyString(CommonConst.ConfigRegistryPath, keyName, keyValue);
}

public static void PutStoredKeyBool(string keyName, bool keyValue)
{
PutStoredKeyBool(CommonConst.ConfigRegistryPath, keyName, keyValue);
}

public static void PutStoredKeyString(string keyPath, string keyName, string keyValue)
{
try
Expand All @@ -108,6 +146,21 @@ public static void PutStoredKeyString(string keyPath, string keyName, string key
logger.Error(ex, $"Failed to save value to registry. {keyName}");
}
}
public static void PutStoredKeyBool(string keyPath, string keyName, bool keyValue)
{
try
{
using (RegistryKey key = Registry.CurrentUser.CreateSubKey(keyPath))
{
key.SetValue(keyName, keyValue, RegistryValueKind.String);
}
}
catch (Exception ex)
{
logger.Error(ex, $"Failed to save value to registry. {keyName}");
}
}


public static void RemoveStoredKeyString(string keyPath, string keyName)
{
Expand Down
1 change: 1 addition & 0 deletions src/Models/GroupInfo.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated>
#nullable disable
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using Tailgrab.Common;
Expand Down
38 changes: 26 additions & 12 deletions src/Models/TailgrabDBContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasKey(e => e.AvatarId);

entity.ToTable("AvatarInfo");

entity.HasIndex(a => a.AvatarName);
entity.HasIndex(a => a.UserName);
entity.HasIndex(a => a.AlertType);
});

modelBuilder.Entity<GroupInfo>(entity =>
Expand All @@ -81,6 +85,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

entity.Property(e => e.CreatedAt).HasColumnName("createDate");
entity.Property(e => e.UpdatedAt).HasColumnName("updateDate");

entity.HasIndex(g => g.GroupName);
entity.HasIndex(g => g.AlertType);
});

modelBuilder.Entity<ProfileEvaluation>(entity =>
Expand Down Expand Up @@ -119,11 +126,18 @@ 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(
"ALTER TABLE AvatarInfo ADD COLUMN alertType INTEGER NOT NULL DEFAULT 0",
"UPDATE AvatarInfo SET alertType = 1 WHERE IsBOS = 1",
"ALTER TABLE GroupInfo ADD COLUMN alertType INTEGER NOT NULL DEFAULT 0",
"UPDATE GroupInfo SET alertType = 1 WHERE IsBOS = 1"
"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)"
);
}

Expand All @@ -135,18 +149,18 @@ private void ExecuteSql(string sql)
private void ExecuteSqlTransaction(params string[] sqlStatements)
{
using var transaction = Database.BeginTransaction();
try
foreach (var sql in sqlStatements)
{
foreach (var sql in sqlStatements)
try
{
Database.ExecuteSqlRaw(sql);
}
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
catch
{
logger.Error($"Error executing SQL transaction. Rolling back changes. {sql}");
transaction.Rollback();
throw;
}
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/PlayerManagement/AvatarCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,14 @@ private void EnsureCount()

if (!string.IsNullOrWhiteSpace(_filterText))
{
var filterLower = _filterText.ToLower();
query = query.Where(a => a.AvatarName.ToLower().Contains(filterLower));
if (_filterText.StartsWith("avtr_", StringComparison.OrdinalIgnoreCase))
{
query = query.Where(a => a.AvatarId == _filterText);
}
else
{
query = query.Where(a => EF.Functions.Like(a.AvatarName, $"%{_filterText}%"));
}
}

var items = query.OrderBy(a => a.AvatarName).Skip(skip).Take(_pageSize).ToList();
Expand Down
22 changes: 20 additions & 2 deletions src/PlayerManagement/PlayerManagement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,6 @@ public string AlertMessage

public AlertTypeEnum MaxAlertType { get; private set; } = AlertTypeEnum.None;


private DateOnly? _dateJoined;
public DateOnly? DateJoined
{
Expand Down Expand Up @@ -241,6 +240,22 @@ public bool IsWatched
}
}


private bool _isFriend = false;
public bool IsFriend {
get
{
return _isFriend;
}
set
{
if( value == true)
{
AlertColor = "Friend";
}
_isFriend = value;
} }

public Player(string userId, string displayName, SessionInfo session)
{
UserId = userId;
Expand All @@ -263,7 +278,10 @@ public void AddAlertMessage(AlertClassEnum alertClass, AlertTypeEnum alertType,
if (alert.AlertType > MaxAlertType)
{
MaxAlertType = alert.AlertType;
AlertColor = alert.Color;
if( _isFriend == false)
{
AlertColor = alert.Color;
}
}
}
}
Expand Down
Loading
Loading