Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ Click the windows application or open a Powershell or Command Line prompt in you

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 \}```
```.\tailgrab.exe -l {full path to VR Chat logs ending with a \}```

If you need to clear all registry settings stored for TailGrab, you can run:

```.\tailgrab.exe -clear```

This will remove all stored configuration and secret values from the Windows Registry for TailGrab, you can then reconfigure the application as needed, save them, restart and get back to watching the instance.

## VRChat Source Log Files

Expand Down
22 changes: 11 additions & 11 deletions src/Actions/Actions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Tailgrab.Actions
public interface IAction
{
void PerformAction();
}
}


public class DelayAction : IAction
Expand All @@ -22,13 +22,13 @@ public class DelayAction : IAction
public DelayAction(int delayMilliseconds)
{
DelayMilliseconds = delayMilliseconds;
logger.Warn($"Added DelayAction: Will delay for : '{DelayMilliseconds}' milliseconds.");
logger.Warn($"Added DelayAction: Will delay for : '{DelayMilliseconds}' milliseconds.");

}

public void PerformAction()
{
if( DelayMilliseconds <= 0 )
if (DelayMilliseconds <= 0)
{
return;
}
Expand All @@ -50,14 +50,14 @@ public KeystrokesAction(string windowTitle, string keys)
WindowTitle = windowTitle;
Keys = keys;

logger.Warn($"Added KeystrokesAction: Window Title: '{WindowTitle}' with Keys: {Keys}.");
logger.Warn($"Added KeystrokesAction: Window Title: '{WindowTitle}' with Keys: {Keys}.");
}

public void PerformAction()
{
if( WindowTitle == null || Keys == null )
if (WindowTitle == null || Keys == null)
{
logger.Warn($"KeystrokesAction: Window Title: '{WindowTitle}' or Keys: {Keys} not supplied.");
logger.Warn($"KeystrokesAction: Window Title: '{WindowTitle}' or Keys: {Keys} not supplied.");
return;
}

Expand Down Expand Up @@ -252,9 +252,9 @@ public OSCAction(string parameterName, OscType type, string value)
OscTypeValue = type;
Value = value;

logger.Warn($"Added OSCAction: Parameter: '{ParameterName}'; Type: {OscTypeValue}; Value: {Value}.");
logger.Warn($"Added OSCAction: Parameter: '{ParameterName}'; Type: {OscTypeValue}; Value: {Value}.");

}
}

public void PerformAction()
{
Expand All @@ -269,7 +269,7 @@ public void PerformAction()
{
case OscType.Bool:
if (bool.TryParse(value, out bool boolValue))
{
{
OscParameter.SendValue(parameterName, boolValue);
}
break;
Expand All @@ -285,15 +285,15 @@ public void PerformAction()
OscParameter.SendValue(parameterName, floatValue);
}
break;
}
}
}
}

public class TTSAction : IAction
{
public Logger logger = LogManager.GetCurrentClassLogger();

public int Volume{ get; set; } = 100;
public int Volume { get; set; } = 100;

public int Rate { get; set; } = 0;

Expand Down
197 changes: 182 additions & 15 deletions src/AvatarManagement/AvatarManagement.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Microsoft.EntityFrameworkCore;
using ConcurrentPriorityQueue.Core;
using Microsoft.EntityFrameworkCore;
using NLog;
using System.Media;
using Tailgrab;
using Tailgrab.Clients.Ollama;
using Tailgrab.Common;
using Tailgrab.Config;
using Tailgrab.Models;
using Tailgrab.Clients.VRChat;
using VRChat.API.Model;

namespace Tailgrab.AvatarManagement
Expand All @@ -14,11 +16,15 @@ public class AvatarManagementService

private ServiceRegistry _serviceRegistry;

private static List<string> avatarsInSession = new List<string>();
private ConcurrentPriorityQueue<IHavePriority<int>, int> priorityQueue = new ConcurrentPriorityQueue<IHavePriority<int>, int>();


public AvatarManagementService(ServiceRegistry serviceRegistry)
{
_serviceRegistry = serviceRegistry;
_serviceRegistry = serviceRegistry;

_ = Task.Run(() => AvatarCheckTask(priorityQueue, _serviceRegistry));

}

public void AddAvatar(AvatarInfo avatar)
Expand Down Expand Up @@ -69,7 +75,8 @@ public void CacheAvatars(List<string> avatarIdInCache)

int postion = 0;
foreach (var avatarId in avatarIdInCache)
{
{
EnqueueAvatarForCheck(avatarId);
AvatarInfo? dbAvatarInfo = dbContext.AvatarInfos.Find(avatarId);
bool updateNeeded = false;
if (dbAvatarInfo == null)
Expand Down Expand Up @@ -121,7 +128,7 @@ public void CacheAvatars(List<string> avatarIdInCache)
var entry = dbContext.Entry(dbAvatarInfo);
if (entry.State == Microsoft.EntityFrameworkCore.EntityState.Detached)
{
dbContext.Attach(dbAvatarInfo);
dbContext.Attach(dbAvatarInfo);
entry = _serviceRegistry.GetDBContext().Entry(dbAvatarInfo);
}

Expand All @@ -147,8 +154,8 @@ public void CacheAvatars(List<string> avatarIdInCache)
{
logger.Error($"Error fetching avatar: {ex.Message}");
}
if (avatarData == null && dbAvatarInfo == null )

if (avatarData == null && dbAvatarInfo == null)
{
var avatarInfo = new AvatarInfo
{
Expand Down Expand Up @@ -176,11 +183,20 @@ public void CacheAvatars(List<string> avatarIdInCache)

postion++;
}
}

avatarsInSession.Clear();
private void EnqueueAvatarForCheck(string avatarId)
{
var queuedItem = new QueuedAvatarProcess
{
AvatarId = avatarId,
Priority = 1
};

priorityQueue.Enqueue(queuedItem);
}

public void GetAvatarsFromUser( string userId, string avatarName )
public void GetAvatarsFromUser(string userId, string avatarName)
{

logger.Debug($"Fetching avatars for user {userId} to find avatar named {avatarName}");
Expand Down Expand Up @@ -243,19 +259,170 @@ internal bool CheckAvatarByName(string avatarName)

if (bannedAvatars.Count > 0)
{
SystemSounds.Hand.Play();
string? soundSetting = ConfigStore.LoadSecret(Common.Common.Registry_Alert_Avatar) ?? "Hand";
SoundManager.PlaySound(soundSetting);
return true;
}

return false;
}

internal void AddAvatarsInSession(string avatarName)
public static async Task AvatarCheckTask(ConcurrentPriorityQueue<IHavePriority<int>, 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 && result.Value is QueuedAvatarProcess item && item.AvatarId != null)
{
try
{
AvatarInfo? dbAvatarInfo = dBContext.AvatarInfos.Find(item.AvatarId);
bool updateNeeded = false;
if (dbAvatarInfo == null)
{
updateNeeded = true;
}
else if (!dbAvatarInfo.IsBos &&
(!dbAvatarInfo.UpdatedAt.HasValue || dbAvatarInfo.UpdatedAt.Value >= DateTime.UtcNow.AddHours(-24)))
{
updateNeeded = true;
}

if (updateNeeded)
{
Avatar? avatarData = FetchUpdateAvatarData(serviceRegistry, dBContext, item.AvatarId, dbAvatarInfo);

if (avatarData == null && dbAvatarInfo == null)
{
CreateAvatarInfoForPrivate(dBContext, item.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: {item.AvatarId}");
}
}
else
{
// No more items to process
break;
}
}
// Wait for a short period before checking the queue again
await Task.Delay(5000);
}
}

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,
IsBos = false
};

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}");
}
}

private static Avatar? FetchUpdateAvatarData(ServiceRegistry serviceRegistry, TailgrabDBContext dBContext, string AvatarId, AvatarInfo? dbAvatarInfo)
{
if (!avatarsInSession.Contains(avatarName))
Avatar? avatarData = null;
try
{
avatarsInSession.Add(avatarName);
// 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,
AvatarName = avatarData.Name,
ImageUrl = avatarData.ImageUrl,
CreatedAt = avatarData.CreatedAt,
UpdatedAt = DateTime.UtcNow,
IsBos = false
};

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.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<int>
{
public int Priority { get; set; }

public string? AvatarId { get; set; }
}


}
Loading
Loading