From 53e71c1cfa781397510623affcd4f3209cb9af6f Mon Sep 17 00:00:00 2001 From: Jay Long Date: Mon, 26 Jan 2026 17:45:58 -0600 Subject: [PATCH] Migrated all secrets to a Settings Pannel & Registry --- README.md | 73 +++++------ src/Clients/Ollama/Ollama.cs | 127 +++++++++++--------- src/Clients/VRChat/VRChat.cs | 20 +-- src/Common/Common.cs | 20 +++ src/Config/ConfigStore.cs | 58 +++++++++ src/PlayerManagement/TailgrabPannel.xaml | 67 ++++++++++- src/PlayerManagement/TailgrabPannel.xaml.cs | 41 ++++++- src/Program.cs | 5 + src/Properties/Resources.Designer.cs | 4 +- tailgrab.csproj | 6 +- 10 files changed, 315 insertions(+), 106 deletions(-) create mode 100644 src/Common/Common.cs create mode 100644 src/Config/ConfigStore.cs diff --git a/README.md b/README.md index 3cd1290..372ed5f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,19 @@ # TailGrab -VRChat Log Parser and Automation tool to help moderators manage trouble makers in VRChat since VRChat will not take moderation seriously. +VRChat Log Parser and Automation tool to help moderators manage trouble makers in VRChat since VRChat Management Team is not taking moderation seriously; ever. + +# Capabilities + +The core concept of the TailGrab was to create a Windows friendly ```grep``` of VR Chat log events that would allow a group moderation team to review, get insights of bad actors and with the action framework to perform a scripted reaction to a VR Chat game log event. + +EG: +A ```Vote To Kick``` is received from a patreon, the action sequence could: +- Send a OSC Avatar Parameter(s) that change the avatar's ear position +- Delay for a quarter of a second +- Send a keystroke to your soundboard application +- Send a keystroke to OBS to start recording # Usage -Open a Powershell or Command Line prompt in your windows host, change directory to where ```tailgrab``` has been extracted to and start it with: +Click the windows application or open a Powershell or Command Line prompt in your windows host, change directory to where ```tailgrab.exe``` has been extracted to and start it with: ```.\tailgrab.exe``` @@ -10,28 +21,42 @@ Or if you have moved where the VR Chat ```output_log_*.txt``` are located; then: ```.\tailgrab.exe {full path to VR Chat logs ending with a \}``` +## VRChat Source Log Files + +By default TailGrab will look for VRChat log files in the default location of: + +```YourUserHome\AppData\LocalLow\VRChat\VRChat\``` + +This can be overridden by passing the full path to the VRChat log files as the first argument to the application. + +```.\\tailgrab.exe D:\MyVRChatLogs\``` + +## Watching TailGrab Application Logs + +The TailGrab application will log it's internal operations to the ```./logs``` folder in the same directory as the application executable. Each run of the application will create a new log file with a timestamp in the filename. + +If you want to watch the application logs in real time, you can use a tool like ```tail``` from Git Bash or ```Get-Content``` from Powershell session with the log filename. + +```Get-Content -Path .\logs\tailgrab-2026-01-26.log -wait``` + # Configuration -## Environment Variables -The TailGrab application will look for the following environment variables to connect to your VRChat API and OLLama AI services. +## VR Chat and OLLama API Credentials Tailgrab uses VR Chat's public API to get information about avatars for the BlackListed Database (SQLite Local DB) and to get user profile infoformation for Profile Evaluation with the AI services. OLLama Cloud AI services are used to evaluate user profiles for potential bad actors based on your custom prompt criteria. The OLLama API is called only once for a MD5 checksummed profile to reduce API usage and cost. +The TailGrab application will look for the following credentials to connect to your VRChat API and OLLama AI services from the Windows Registry in a encyrpted format. On the first Run you may receive a Popup Message to set the values on the Config -> Secrets Tab and restart the application to get the services running properly. -|Environment Variable | Definition | -|--------|--------| -| VRCHAT_USERNAME | Your VRChat User Name (What you use to sign into the VR Chat Web Page). | -| VRCHAT_PASSWORD | Your VRChat Password (What you use to sign into the VR Chat Web Page). | -| VRCHAT_TWO_FACTOR_SECRET | Your VRChat Two Factor Authentication Secret (If you have 2FA enabled on your account; **RECOMENDED** ). See https://docs.vrchat.com/docs/using-2fa for more information. | -| OLLAMA_API_KEY | Your OLLama API Key to access your AI services. See https://ollama.com/docs/api for more information. | +## Getting your VR Chat 2 Factor Authentication key + +I certainly hope you are using LastPass Authenticator or Google Authenticator to manage your 2FA codes for VRChat. If you are not, please stop reading this and go set that up now to protect your Online Accounts. -On Windows 11 you can set these environment variables by searching for "Environment Variables" in the Start Menu and selecting "Edit the User environment variables". Then click on the "Environment Variables" button and add the variables under "User variables" (Suggested) or "System variables". -If launching from a Powershell or Command Line prompt, you will need to close the window for the values to be set for that session +On LastPass Authenticator for the your VR Chat Entry, you can use the right Hamburger menu icon to get a dialog of options, one of which is to 'Edit Account', select that and you will see the 'Secret Key' field, copy the 'Secret Key' value to your clipboard and paste to something you can transfer to your PC (Or tediously type it in from the screen). ## "Config.json" File -The confiuration for TailGrab uses a JSON formated payload of the base attribute "lineHandlers" that contains a array of LineHandler Objects, Those may have a attribute of "actions" that contain an array of Action Objects. +The confiuration for TailGrab uses a JSON formated payload of the base attribute "lineHandlers" that contains a array of LineHandler Objects, Those may have a attribute of "actions" that contain an array of Action Objects. This configuration is loaded on application start. ## LineHandler Definition @@ -161,25 +186,3 @@ To specify keys combined with any combination of the SHIFT, CTRL, and ALT keys, To specify that any combination of SHIFT, CTRL, and ALT should be held down while several other keys are pressed, enclose the code for those keys in parentheses. For example, to specify to hold down SHIFT while E and C are pressed, use ```"+(EC)"```. To specify to hold down SHIFT while E is pressed, followed by C without SHIFT, use ```"+EC"```. To specify repeating keys, use the form ```{key number}```. You must put a space between key and number. For example, ```{LEFT 42}``` means press the LEFT ARROW key 42 times; ```{h 10}``` means press H 10 times. - - - -# Capabilities - -The core concept of the TailGrab was to create a Windows friendly ```grep``` of VR Chat log events that would allow a group moderation team to review, get insights of bad actors and with the action framework to perform a scripted reaction to a log event. - -EG: -A ```Vote To Kick``` is received from a patreon, the action sequence could: -- Send a OSC Avatar Parameter(s) that change the avatar's ear position -- Delay for a quarter of a second -- Send a keystroke to your soundboard application -- Send a keystroke to OBS to start recording - - -## POC Version -- Parse VRChat log files -- World ```Furry Hideout``` will record User Pen Interaction -- Record User's avatar usage while in the instance -- Record User's moderation while in the instance (Warn and final Kick) -- Partial work with OSC Triggered events to send to your avatar -- Partial work with Keystroke events sent to a application of your choice diff --git a/src/Clients/Ollama/Ollama.cs b/src/Clients/Ollama/Ollama.cs index 7ff9469..a243622 100644 --- a/src/Clients/Ollama/Ollama.cs +++ b/src/Clients/Ollama/Ollama.cs @@ -1,13 +1,16 @@ using ConcurrentPriorityQueue.Core; +using Microsoft.EntityFrameworkCore; using NLog; using OllamaSharp; using OllamaSharp.Models; using System.Net.Http; using System.Text.RegularExpressions; using Tailgrab.Common; +using Tailgrab.Config; using Tailgrab.Models; using Tailgrab.PlayerManagement; using VRChat.API.Model; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.StartPanel; namespace Tailgrab.Clients.Ollama { @@ -77,15 +80,21 @@ public void CheckUserProfile(string userId) } public static async Task ProfileCheckTask(ConcurrentPriorityQueue, int> priorityQueue, Dictionary processData, ServiceRegistry serviceRegistry ) { - string? ollamaCloudKey = Environment.GetEnvironmentVariable("OLLAMA_CLOUD_KEY"); + string? ollamaCloudKey = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Key); + if (ollamaCloudKey is null) - throw new InvalidOperationException("OLLAMA_CLOUD_KEY environment variable is not set."); + { + System.Windows.MessageBox.Show("Ollama API Credentials are not set yet, use the Config / Secrets tab to update credenials and restart Tailgrab.", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + return; + } + string ollamaEndpoint = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Endpoint) ?? Tailgrab.Common.Common.Default_Ollama_API_Endpoint; HttpClient client = new HttpClient(); - client.BaseAddress = new Uri("https://ollama.com"); + client.BaseAddress = new Uri(ollamaEndpoint); client.DefaultRequestHeaders.Add("Authorization", "Bearer " + ollamaCloudKey); - OllamaApiClient ollamaApi = new OllamaApiClient(client); - ollamaApi.SelectedModel = "gemma3:27b"; + OllamaApiClient? ollamaApi = new OllamaApiClient(client); + string ollamaModel = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Model) ?? Tailgrab.Common.Common.Default_Ollama_API_Model; + ollamaApi.SelectedModel = ollamaModel; OllamaClient.logger.Info($"OLlama Queue Running"); while (true) @@ -104,62 +113,27 @@ public static async Task ProfileCheckTask(ConcurrentPriorityQueue userGroups, QueuedProcess item ) + { + logger.Debug($"Processing User Group subscription for userId: {item.UserId}"); + bool isSuspectGroup = false; + foreach (LimitedUserGroups group in userGroups) + { + GroupInfo? groupInfo = dBContext.GroupInfos.Find(group.GroupId); + if (groupInfo == null) + { + groupInfo = new GroupInfo + { + GroupId = group.GroupId, + GroupName = group.Name, + IsBos = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + dBContext.GroupInfos.Add(groupInfo); + dBContext.SaveChanges(); + } + else + { + groupInfo.GroupName = group.Name; + dBContext.GroupInfos.Update(groupInfo); + dBContext.SaveChanges(); + + if (groupInfo.IsBos) + { + isSuspectGroup = true; + } + } + } + + if (isSuspectGroup) + { + Player? player = serviceRegistry.GetPlayerManager().GetPlayerByUserId(item.UserId ?? string.Empty); + if (player != null) + { + player.IsGroupWatch = true; + serviceRegistry.GetPlayerManager().OnPlayerChanged(PlayerChangedEventArgs.ChangeType.Updated, player); + } + } + } + private async static void GetEvaluationFromCloud(OllamaApiClient ollamaApi, ServiceRegistry serviceRegistry, QueuedProcess item) { + string? ollamaPrompt = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_Ollama_API_Prompt); GenerateRequest request = new GenerateRequest { Model = ollamaApi.SelectedModel, - Prompt = "From the following block of text, classify the contents into a single class; 'OK', 'Explicit Sexual', 'Harrassment & Bullying', 'Self Harm' or 'Other'. When replying, give a single line for the Classification and then a new line for the resoning: \n" + item.UserBio ?? string.Empty, + Prompt = ollamaPrompt ?? Tailgrab.Common.Common.Default_Ollama_API_Prompt + item.UserBio ?? string.Empty, Stream = false }; diff --git a/src/Clients/VRChat/VRChat.cs b/src/Clients/VRChat/VRChat.cs index cdbee03..60dd163 100644 --- a/src/Clients/VRChat/VRChat.cs +++ b/src/Clients/VRChat/VRChat.cs @@ -3,6 +3,7 @@ using OtpNet; using System.IO; using System.Net; +using Tailgrab.Config; using VRChat.API.Client; using VRChat.API.Model; @@ -17,17 +18,16 @@ public class VRChatClient public async void Initialize() { - string? username = Environment.GetEnvironmentVariable("VRCHAT_USERNAME"); - string? password = Environment.GetEnvironmentVariable("VRCHAT_PASSWORD"); - string? twoFactorSecret = Environment.GetEnvironmentVariable("VRCHAT_TWO_FACTOR_SECRET"); - - if (username is null) - throw new InvalidOperationException("VRCHAT_USERNAME environment variable is not set."); - if (password is null) - throw new InvalidOperationException("VRCHAT_PASSWORD environment variable is not set."); - if (twoFactorSecret is null) - throw new InvalidOperationException("VRCHAT_TWO_FACTOR_SECRET environment variable is not set."); + string? username = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_UserName); + string? password = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_Password); + string? twoFactorSecret = ConfigStore.LoadSecret(Tailgrab.Common.Common.Registry_VRChat_Web_2FactorKey); + if (username is null || password is null || twoFactorSecret is null) + { + System.Windows.MessageBox.Show("VR Chat Web API Credentials are not set yet, use the Config / Secrets tab to update credenials and restart Tailgrab.", "Error", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error); + return; + } + string cookiePath = Path.Combine(Directory.GetCurrentDirectory(), "cookies.json"); // Try to load cookies from disk and use them if they are present and not expired diff --git a/src/Common/Common.cs b/src/Common/Common.cs new file mode 100644 index 0000000..14d0e99 --- /dev/null +++ b/src/Common/Common.cs @@ -0,0 +1,20 @@ +namespace Tailgrab.Common +{ + public static class Common + { + public const string ApplicationName = "Tailgrab"; + public const string CompanyName = "DeviousFox"; + public const string ConfigRegistryPath = "Software\\DeviousFox\\Tailgrab\\Config"; + public const string Registry_VRChat_Web_UserName = "VRCHAT_USERNAME"; + public const string Registry_VRChat_Web_Password = "VRCHAT_PASSWORD"; + public const string Registry_VRChat_Web_2FactorKey = "VRCHAT_2FA"; + public const string Registry_Ollama_API_Key = "OLLAMA_API_KEY"; + public const string Registry_Ollama_API_Endpoint = "OLLAMA_API_ENDPOINT"; + public const string Registry_Ollama_API_Prompt = "OLLAMA_API_PROMPT"; + public const string Registry_Ollama_API_Model = "OLLAMA_API_Model"; + public const string Default_Ollama_API_Prompt = "From the following block of text, classify the contents into a single class;\n'OK', 'Explicit Sexual', 'Harrassment & Bullying', 'Self Harm' or 'Other'.\nWhen replying, give a single line for the Classification and then a new line for the resoning: \n"; + public const string Default_Ollama_API_Endpoint = "https://ollama.com"; + public const string Default_Ollama_API_Model = "gemma3:27b"; + + } +} diff --git a/src/Config/ConfigStore.cs b/src/Config/ConfigStore.cs new file mode 100644 index 0000000..5b2e04c --- /dev/null +++ b/src/Config/ConfigStore.cs @@ -0,0 +1,58 @@ +using Microsoft.Win32; +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Tailgrab.Config +{ + public static class ConfigStore + { + private const string RegistryPath = "Software\\DeviousFox\\Tailgrab\\Config"; + + public static void SaveSecret(string name, string value) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + if (value == null) value = string.Empty; + + var bytes = Encoding.UTF8.GetBytes(value); + var protectedBytes = ProtectedData.Protect(bytes, null, DataProtectionScope.CurrentUser); + var base64 = Convert.ToBase64String(protectedBytes); + + using (var key = Registry.CurrentUser.CreateSubKey(RegistryPath)) + { + key.SetValue(name, base64, RegistryValueKind.String); + } + } + + public static string? LoadSecret(string name) + { + if (name == null) throw new ArgumentNullException(nameof(name)); + + using (var key = Registry.CurrentUser.OpenSubKey(RegistryPath)) + { + if (key == null) return null; + var base64 = key.GetValue(name) as string; + if (string.IsNullOrEmpty(base64)) return null; + try + { + var protectedBytes = Convert.FromBase64String(base64); + var bytes = ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser); + return Encoding.UTF8.GetString(bytes); + } + catch + { + return null; + } + } + } + + public static void DeleteSecret(string name) + { + using (var key = Registry.CurrentUser.OpenSubKey(RegistryPath, writable: true)) + { + if (key == null) return; + key.DeleteValue(name, throwOnMissingValue: false); + } + } + } +} diff --git a/src/PlayerManagement/TailgrabPannel.xaml b/src/PlayerManagement/TailgrabPannel.xaml index f029843..abfa7c1 100644 --- a/src/PlayerManagement/TailgrabPannel.xaml +++ b/src/PlayerManagement/TailgrabPannel.xaml @@ -364,7 +364,7 @@