Skip to content

Commit 046b6f0

Browse files
authored
Merge pull request #100 from BoiHanny/Pre-Master
New feature implementation and code refactoring
2 parents 499e80a + 05f55cf commit 046b6f0

29 files changed

+4744
-4121
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
3+
namespace MagicChatboxAPI.Events
4+
{
5+
/// <summary>
6+
/// Event arguments fired when a newly banned user is detected.
7+
/// </summary>
8+
public class BanDetectedEventArgs : EventArgs
9+
{
10+
/// <summary>
11+
/// The user ID found to be banned in the latest check.
12+
/// </summary>
13+
public string BannedUserId { get; }
14+
15+
/// <summary>
16+
/// Initializes a new instance of <see cref="BanDetectedEventArgs"/>.
17+
/// </summary>
18+
/// <param name="bannedUserId">ID of the newly banned user.</param>
19+
public BanDetectedEventArgs(string bannedUserId)
20+
{
21+
BannedUserId = bannedUserId ?? string.Empty;
22+
}
23+
}
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
3+
namespace MagicChatboxAPI.Enums
4+
{
5+
/// <summary>
6+
/// Detailed information from a check operation.
7+
/// </summary>
8+
public class VRChatUserCheckResult
9+
{
10+
/// <summary>
11+
/// Overall status of the check.
12+
/// </summary>
13+
public VRChatUserCheckStatus Status { get; set; }
14+
15+
/// <summary>
16+
/// Indicates if the check completed with any user
17+
/// being allowed or no new bans found.
18+
/// </summary>
19+
public bool AnyUserAllowed { get; set; }
20+
21+
/// <summary>
22+
/// Optional error message if something went wrong.
23+
/// </summary>
24+
public string ErrorMessage { get; set; }
25+
}
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System;
2+
3+
namespace MagicChatboxAPI.Enums
4+
{
5+
/// <summary>
6+
/// Possible outcomes for user checks.
7+
/// </summary>
8+
public enum VRChatUserCheckStatus
9+
{
10+
Success = 0,
11+
NoFolderFound,
12+
NoUserIdsFound,
13+
ApiError,
14+
ApiTimeout,
15+
UnknownError
16+
}
17+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
using MagicChatboxAPI.Enums;
2+
using MagicChatboxAPI.Events;
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Net.Http.Json;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
10+
namespace MagicChatboxAPI.Services
11+
{
12+
13+
public interface IAllowedForUsingService
14+
{
15+
void StartUserMonitoring(TimeSpan interval);
16+
void StopUserMonitoring();
17+
18+
event EventHandler<BanDetectedEventArgs> BanDetected;
19+
}
20+
21+
public class AllowedForUsingService : IAllowedForUsingService
22+
{
23+
#region Constants and Fields
24+
25+
// External API endpoint for checking a user's ban status
26+
private const string ApiEndpoint = "https://api.magicchatbox.com/moderation/checkIfClientIsAllowed";
27+
28+
private readonly HttpClient _httpClient;
29+
30+
private Timer _timer;
31+
private bool _isMonitoring;
32+
private readonly object _monitorLock = new();
33+
34+
private List<string> _allUserIds;
35+
36+
private readonly Dictionary<string, bool> _userAllowedCache = new();
37+
38+
#endregion
39+
40+
#region Events
41+
42+
43+
public event EventHandler<BanDetectedEventArgs> BanDetected;
44+
45+
#endregion
46+
47+
#region Constructor
48+
49+
50+
public AllowedForUsingService()
51+
{
52+
_httpClient = new HttpClient();
53+
}
54+
55+
#endregion
56+
57+
#region Public Methods
58+
59+
public void StartUserMonitoring(TimeSpan interval)
60+
{
61+
lock (_monitorLock)
62+
{
63+
if (_isMonitoring)
64+
return;
65+
66+
_allUserIds = ScanAllVrChatUserIds();
67+
68+
foreach (var userId in _allUserIds)
69+
{
70+
_userAllowedCache[userId] = true;
71+
}
72+
73+
if (_allUserIds.Count == 0)
74+
{
75+
return;
76+
}
77+
78+
_timer = new Timer(async _ => await UserMonitorCallback(),
79+
null,
80+
TimeSpan.Zero,
81+
interval);
82+
_isMonitoring = true;
83+
}
84+
}
85+
86+
87+
public void StopUserMonitoring()
88+
{
89+
lock (_monitorLock)
90+
{
91+
if (!_isMonitoring)
92+
return;
93+
94+
_timer?.Dispose();
95+
_timer = null;
96+
_isMonitoring = false;
97+
}
98+
}
99+
100+
#endregion
101+
102+
#region Private Methods
103+
104+
/// <summary>
105+
/// Scans the VRChat OSC folder once, collecting all user IDs.
106+
/// </summary>
107+
/// <returns>List of all user IDs found (excluding "usr_" prefix).</returns>
108+
private List<string> ScanAllVrChatUserIds()
109+
{
110+
var userIds = new List<string>();
111+
112+
try
113+
{
114+
// Base path to VRChat's OSC user folders
115+
var basePath = Path.Combine(
116+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
117+
"AppData", "LocalLow", "VRChat", "VRChat", "OSC");
118+
119+
// If folder doesn't exist, return empty
120+
if (!Directory.Exists(basePath))
121+
{
122+
Console.WriteLine($"[AllowedForUsingService] VRChat OSC folder not found: {basePath}");
123+
return userIds;
124+
}
125+
126+
// Get all directories matching "usr_*"
127+
var userDirectories = Directory.GetDirectories(basePath, "usr_*");
128+
if (userDirectories == null || userDirectories.Length == 0)
129+
{
130+
Console.WriteLine("[AllowedForUsingService] No user directories found.");
131+
return userIds;
132+
}
133+
134+
// Extract the user IDs
135+
foreach (var directory in userDirectories)
136+
{
137+
var directoryName = Path.GetFileName(directory);
138+
if (!string.IsNullOrEmpty(directoryName) && directoryName.StartsWith("usr_"))
139+
{
140+
var extractedUserId = directoryName.Substring("usr_".Length).Trim();
141+
if (!string.IsNullOrWhiteSpace(extractedUserId))
142+
{
143+
userIds.Add(extractedUserId);
144+
}
145+
}
146+
}
147+
}
148+
catch (Exception ex)
149+
{
150+
Console.WriteLine($"[AllowedForUsingService] Error scanning user IDs: {ex.Message}");
151+
}
152+
153+
return userIds.Distinct().ToList(); // Remove duplicates just in case
154+
}
155+
156+
/// <summary>
157+
/// Timer callback: checks the ban status of all known user IDs via API.
158+
/// Fires the BanDetected event immediately when a user transitions from allowed to banned.
159+
/// </summary>
160+
private async Task UserMonitorCallback()
161+
{
162+
if (_allUserIds == null || !_allUserIds.Any())
163+
return; // Skip if no users are loaded
164+
165+
try
166+
{
167+
// Iterate over known user IDs to check their ban status
168+
foreach (var userId in _allUserIds)
169+
{
170+
bool isCurrentlyAllowed = await CheckSingleUserAsync(userId);
171+
172+
lock (_userAllowedCache)
173+
{
174+
// If we have cached state for this user
175+
if (_userAllowedCache.TryGetValue(userId, out bool wasAllowed))
176+
{
177+
// If the user was previously allowed but now banned
178+
if (wasAllowed && !isCurrentlyAllowed)
179+
{
180+
// Update the cache for consistency
181+
_userAllowedCache[userId] = isCurrentlyAllowed;
182+
183+
// Fire the BanDetected event immediately with the banned user ID
184+
BanDetected?.Invoke(
185+
this,
186+
new BanDetectedEventArgs(userId)
187+
);
188+
189+
// Break out of the loop once a banned user is found
190+
// to trigger the event without checking further users
191+
return;
192+
}
193+
}
194+
else
195+
{
196+
// In case user is not present in the cache, add them
197+
_userAllowedCache[userId] = isCurrentlyAllowed;
198+
}
199+
200+
// Update the user's allowed status if no ban was detected
201+
_userAllowedCache[userId] = isCurrentlyAllowed;
202+
}
203+
}
204+
}
205+
catch (Exception ex)
206+
{
207+
// Log or handle exception as needed
208+
Console.WriteLine($"[AllowedForUsingService] Monitoring error: {ex.Message}");
209+
}
210+
}
211+
212+
213+
/// <summary>
214+
/// Calls the external API for a single user to determine if they are banned.
215+
/// Returns true if the user is allowed (not banned); false if banned.
216+
/// </summary>
217+
/// <param name="userId">Unique portion of the user ID (e.g., after 'usr_').</param>
218+
private async Task<bool> CheckSingleUserAsync(string userId)
219+
{
220+
var payload = new { userId };
221+
try
222+
{
223+
var response = await _httpClient.PostAsJsonAsync(ApiEndpoint, payload);
224+
225+
if (!response.IsSuccessStatusCode)
226+
{
227+
var errorContent = await response.Content.ReadAsStringAsync();
228+
Console.WriteLine($"[AllowedForUsingService] API returned {response.StatusCode}: {errorContent}");
229+
// Treat as banned (false) to be safe
230+
return true;
231+
}
232+
233+
var apiResponse = await response.Content.ReadFromJsonAsync<ApiResponse>();
234+
if (apiResponse == null)
235+
{
236+
Console.WriteLine("[AllowedForUsingService] API response was null.");
237+
// Treat as banned (false) to be safe
238+
return true;
239+
}
240+
241+
// If "isBanned" is true in the API response, the user is banned (not allowed).
242+
return !apiResponse.isBanned;
243+
}
244+
catch (Exception ex)
245+
{
246+
Console.WriteLine($"[AllowedForUsingService] CheckSingleUserAsync error for userId={userId}: {ex.Message}");
247+
// On exception, treat as banned for safety
248+
return true;
249+
}
250+
}
251+
252+
#endregion
253+
254+
#region Internal Model
255+
256+
/// <summary>
257+
/// Internal class that maps the JSON structure from the external API.
258+
/// Adjust properties to match the actual API response.
259+
/// </summary>
260+
private class ApiResponse
261+
{
262+
public string userId { get; set; }
263+
public bool isBanned { get; set; }
264+
}
265+
266+
#endregion
267+
}
268+
}

vrcosc-magicchatbox.sln

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.32112.339
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MagicChatbox", "vrcosc-magicchatbox\MagicChatbox.csproj", "{76FB3E35-94A5-445C-87F2-D75E9F701E5F}"
77
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MagicChatboxAPI", "MagicChatboxAPI\MagicChatboxAPI.csproj", "{C0B24731-C59E-4AC1-B4B7-988B254F645E}"
9+
EndProject
810
Global
911
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1012
Beta|Any CPU = Beta|Any CPU
@@ -18,6 +20,12 @@ Global
1820
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
1921
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
2022
{76FB3E35-94A5-445C-87F2-D75E9F701E5F}.Release|Any CPU.Build.0 = Release|Any CPU
23+
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Beta|Any CPU.ActiveCfg = Debug|Any CPU
24+
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Beta|Any CPU.Build.0 = Debug|Any CPU
25+
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26+
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Debug|Any CPU.Build.0 = Debug|Any CPU
27+
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Release|Any CPU.ActiveCfg = Release|Any CPU
28+
{C0B24731-C59E-4AC1-B4B7-988B254F645E}.Release|Any CPU.Build.0 = Release|Any CPU
2129
EndGlobalSection
2230
GlobalSection(SolutionProperties) = preSolution
2331
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)