Skip to content

Commit

Permalink
Merge pull request #100 from BoiHanny/Pre-Master
Browse files Browse the repository at this point in the history
New feature implementation and code refactoring
  • Loading branch information
BoiHanny authored Jan 12, 2025
2 parents 499e80a + 05f55cf commit 046b6f0
Show file tree
Hide file tree
Showing 29 changed files with 4,744 additions and 4,121 deletions.
24 changes: 24 additions & 0 deletions MagicChatboxAPI/Enums/BanDetectedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace MagicChatboxAPI.Events
{
/// <summary>
/// Event arguments fired when a newly banned user is detected.
/// </summary>
public class BanDetectedEventArgs : EventArgs
{
/// <summary>
/// The user ID found to be banned in the latest check.
/// </summary>
public string BannedUserId { get; }

/// <summary>
/// Initializes a new instance of <see cref="BanDetectedEventArgs"/>.
/// </summary>
/// <param name="bannedUserId">ID of the newly banned user.</param>
public BanDetectedEventArgs(string bannedUserId)
{
BannedUserId = bannedUserId ?? string.Empty;
}
}
}
26 changes: 26 additions & 0 deletions MagicChatboxAPI/Enums/VRChatUserCheckResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace MagicChatboxAPI.Enums
{
/// <summary>
/// Detailed information from a check operation.
/// </summary>
public class VRChatUserCheckResult
{
/// <summary>
/// Overall status of the check.
/// </summary>
public VRChatUserCheckStatus Status { get; set; }

/// <summary>
/// Indicates if the check completed with any user
/// being allowed or no new bans found.
/// </summary>
public bool AnyUserAllowed { get; set; }

/// <summary>
/// Optional error message if something went wrong.
/// </summary>
public string ErrorMessage { get; set; }

Check warning on line 24 in MagicChatboxAPI/Enums/VRChatUserCheckResult.cs

View workflow job for this annotation

GitHub Actions / build-and-release

Non-nullable property 'ErrorMessage' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}
}
17 changes: 17 additions & 0 deletions MagicChatboxAPI/Enums/VRChatUserCheckStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace MagicChatboxAPI.Enums
{
/// <summary>
/// Possible outcomes for user checks.
/// </summary>
public enum VRChatUserCheckStatus
{
Success = 0,
NoFolderFound,
NoUserIdsFound,
ApiError,
ApiTimeout,
UnknownError
}
}
9 changes: 9 additions & 0 deletions MagicChatboxAPI/MagicChatboxAPI.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
268 changes: 268 additions & 0 deletions MagicChatboxAPI/Services/AllowedForUsingService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
using MagicChatboxAPI.Enums;
using MagicChatboxAPI.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Json;
using System.Text;
using System.Threading.Tasks;

namespace MagicChatboxAPI.Services
{

public interface IAllowedForUsingService
{
void StartUserMonitoring(TimeSpan interval);
void StopUserMonitoring();

event EventHandler<BanDetectedEventArgs> BanDetected;
}

public class AllowedForUsingService : IAllowedForUsingService
{
#region Constants and Fields

// External API endpoint for checking a user's ban status
private const string ApiEndpoint = "https://api.magicchatbox.com/moderation/checkIfClientIsAllowed";

private readonly HttpClient _httpClient;

private Timer _timer;
private bool _isMonitoring;
private readonly object _monitorLock = new();

private List<string> _allUserIds;

private readonly Dictionary<string, bool> _userAllowedCache = new();

#endregion

#region Events


public event EventHandler<BanDetectedEventArgs> BanDetected;

#endregion

#region Constructor


public AllowedForUsingService()

Check warning on line 50 in MagicChatboxAPI/Services/AllowedForUsingService.cs

View workflow job for this annotation

GitHub Actions / build-and-release

Non-nullable field '_timer' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 50 in MagicChatboxAPI/Services/AllowedForUsingService.cs

View workflow job for this annotation

GitHub Actions / build-and-release

Non-nullable field '_allUserIds' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.

Check warning on line 50 in MagicChatboxAPI/Services/AllowedForUsingService.cs

View workflow job for this annotation

GitHub Actions / build-and-release

Non-nullable event 'BanDetected' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the event as nullable.
{
_httpClient = new HttpClient();
}

#endregion

#region Public Methods

public void StartUserMonitoring(TimeSpan interval)
{
lock (_monitorLock)
{
if (_isMonitoring)
return;

_allUserIds = ScanAllVrChatUserIds();

foreach (var userId in _allUserIds)
{
_userAllowedCache[userId] = true;
}

if (_allUserIds.Count == 0)
{
return;
}

_timer = new Timer(async _ => await UserMonitorCallback(),
null,
TimeSpan.Zero,
interval);
_isMonitoring = true;
}
}


public void StopUserMonitoring()
{
lock (_monitorLock)
{
if (!_isMonitoring)
return;

_timer?.Dispose();
_timer = null;

Check warning on line 95 in MagicChatboxAPI/Services/AllowedForUsingService.cs

View workflow job for this annotation

GitHub Actions / build-and-release

Cannot convert null literal to non-nullable reference type.
_isMonitoring = false;
}
}

#endregion

#region Private Methods

/// <summary>
/// Scans the VRChat OSC folder once, collecting all user IDs.
/// </summary>
/// <returns>List of all user IDs found (excluding "usr_" prefix).</returns>
private List<string> ScanAllVrChatUserIds()
{
var userIds = new List<string>();

try
{
// Base path to VRChat's OSC user folders
var basePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"AppData", "LocalLow", "VRChat", "VRChat", "OSC");

// If folder doesn't exist, return empty
if (!Directory.Exists(basePath))
{
Console.WriteLine($"[AllowedForUsingService] VRChat OSC folder not found: {basePath}");
return userIds;
}

// Get all directories matching "usr_*"
var userDirectories = Directory.GetDirectories(basePath, "usr_*");
if (userDirectories == null || userDirectories.Length == 0)
{
Console.WriteLine("[AllowedForUsingService] No user directories found.");
return userIds;
}

// Extract the user IDs
foreach (var directory in userDirectories)
{
var directoryName = Path.GetFileName(directory);
if (!string.IsNullOrEmpty(directoryName) && directoryName.StartsWith("usr_"))
{
var extractedUserId = directoryName.Substring("usr_".Length).Trim();
if (!string.IsNullOrWhiteSpace(extractedUserId))
{
userIds.Add(extractedUserId);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"[AllowedForUsingService] Error scanning user IDs: {ex.Message}");
}

return userIds.Distinct().ToList(); // Remove duplicates just in case
}

/// <summary>
/// Timer callback: checks the ban status of all known user IDs via API.
/// Fires the BanDetected event immediately when a user transitions from allowed to banned.
/// </summary>
private async Task UserMonitorCallback()
{
if (_allUserIds == null || !_allUserIds.Any())
return; // Skip if no users are loaded

try
{
// Iterate over known user IDs to check their ban status
foreach (var userId in _allUserIds)
{
bool isCurrentlyAllowed = await CheckSingleUserAsync(userId);

lock (_userAllowedCache)
{
// If we have cached state for this user
if (_userAllowedCache.TryGetValue(userId, out bool wasAllowed))
{
// If the user was previously allowed but now banned
if (wasAllowed && !isCurrentlyAllowed)
{
// Update the cache for consistency
_userAllowedCache[userId] = isCurrentlyAllowed;

// Fire the BanDetected event immediately with the banned user ID
BanDetected?.Invoke(
this,
new BanDetectedEventArgs(userId)
);

// Break out of the loop once a banned user is found
// to trigger the event without checking further users
return;
}
}
else
{
// In case user is not present in the cache, add them
_userAllowedCache[userId] = isCurrentlyAllowed;
}

// Update the user's allowed status if no ban was detected
_userAllowedCache[userId] = isCurrentlyAllowed;
}
}
}
catch (Exception ex)
{
// Log or handle exception as needed
Console.WriteLine($"[AllowedForUsingService] Monitoring error: {ex.Message}");
}
}


/// <summary>
/// Calls the external API for a single user to determine if they are banned.
/// Returns true if the user is allowed (not banned); false if banned.
/// </summary>
/// <param name="userId">Unique portion of the user ID (e.g., after 'usr_').</param>
private async Task<bool> CheckSingleUserAsync(string userId)
{
var payload = new { userId };
try
{
var response = await _httpClient.PostAsJsonAsync(ApiEndpoint, payload);

if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"[AllowedForUsingService] API returned {response.StatusCode}: {errorContent}");
// Treat as banned (false) to be safe
return true;
}

var apiResponse = await response.Content.ReadFromJsonAsync<ApiResponse>();
if (apiResponse == null)
{
Console.WriteLine("[AllowedForUsingService] API response was null.");
// Treat as banned (false) to be safe
return true;
}

// If "isBanned" is true in the API response, the user is banned (not allowed).
return !apiResponse.isBanned;
}
catch (Exception ex)
{
Console.WriteLine($"[AllowedForUsingService] CheckSingleUserAsync error for userId={userId}: {ex.Message}");
// On exception, treat as banned for safety
return true;
}
}

#endregion

#region Internal Model

/// <summary>
/// Internal class that maps the JSON structure from the external API.
/// Adjust properties to match the actual API response.
/// </summary>
private class ApiResponse
{
public string userId { get; set; }

Check warning on line 262 in MagicChatboxAPI/Services/AllowedForUsingService.cs

View workflow job for this annotation

GitHub Actions / build-and-release

Non-nullable property 'userId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
public bool isBanned { get; set; }
}

#endregion
}
}
8 changes: 8 additions & 0 deletions vrcosc-magicchatbox.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.32112.339
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicChatbox", "vrcosc-magicchatbox\MagicChatbox.csproj", "{76FB3E35-94A5-445C-87F2-D75E9F701E5F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicChatboxAPI", "MagicChatboxAPI\MagicChatboxAPI.csproj", "{C0B24731-C59E-4AC1-B4B7-988B254F645E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Beta|Any CPU = Beta|Any CPU
Expand All @@ -18,6 +20,12 @@ Global
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Release|Any CPU.Build.0 = Release|Any CPU
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Beta|Any CPU.ActiveCfg = Debug|Any CPU
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Beta|Any CPU.Build.0 = Debug|Any CPU
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading

0 comments on commit 046b6f0

Please sign in to comment.