This commit is contained in:
skidfire
2026-09-06 19:59:27 -04:00
parent 55d8e11a5d
commit fe3468c65d
247 changed files with 338183 additions and 0 deletions
+2772
View File
@@ -0,0 +1,2772 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration.UserSecrets;
using Newtonsoft.Json;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using Vanadium.Auth;
using Vanadium.Classes;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using Vanadium.Classes.Rooms;
using Vanadium.Utils.NotiController;
using System.Net.WebSockets;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.RegularExpressions;
using System.Text.Json;
using System.Xml;
using static ImageMetadataDB;
using static Vanadium.Classes.DBs.DBClasses.EventDBClasses;
using static Vanadium.Classes.DBs.DBClasses.FriendsDBClasses;
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
using static Vanadium.Classes.DBs.DBClasses.RRPlus;
using Vanadium.Classes.WebSocket;
// :3
namespace Vanadium.Controllers
{
[ApiController]
public partial class APIController : ControllerBase
{
[HttpGet("/")]
/*public IActionResult OldNameserver()
{
return Ok(new { RecNetStatus = "You're on an old version of Vanadium.\n\nUpdate to continue playing!" });
}*/
[HttpGet("/altns")]
public async Task<IActionResult> GetNS([FromQuery] int v)
{
if (Request.Path == "/" && v == 2)
return Ok(new { RecNetStatus = "You're on an old version of Vanadium.\n\nUpdate to continue playing!" });
string url = ServerConfig.BaseURL;
return Ok(new
{
Accounts = url + "/acc",
AI = url,
API = url,
Auth = url + "/auth",
BugReporting = url,
Cards = url,
CDN = url + "/cdn",
Chat = url + "/chat",
Clubs = url,
CMS = url,
Commerce = url,
Data = url,
DataCollection = url,
Discovery = url,
Econ = url,
GameLogs = url,
Geo = url,
Images = url + "/imageserver",
//Images = "img.rec.net",
Leaderboard = url,
Link = url,
Lists = url,
Matchmaking = url + "/match",
Moderation = url,
Notifications = url + "/notifications",
PlatformNotifications = url,
PlayerSettings = url,
RoomComments = url,
Rooms = url + "/roomserver",
RoomieIntegrations = url,
Storage = url,
Strings = url,
StringsCDN = url,
Studio = url,
Thorn = url,
Videos = url,
WWW = url
});
}
[HttpGet("api/versioncheck/v4")]
public async Task<IActionResult> VersionCheck([FromQuery] string v)
{
string cfgPath = System.IO.Path.Combine(Program.configDir, "versioncheck.cfg");
string[] validVersions = Array.Empty<string>();
if (System.IO.File.Exists(cfgPath))
{
validVersions = (await System.IO.File.ReadAllLinesAsync(cfgPath))
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
.ToArray();
}
if (!validVersions.Contains(v) && HttpContext.Connection.RemoteIpAddress?.ToString() != "2600:1700:95d0:1070:5d5a:ec7a:8afa:9142")
{
return NotFound("");
}
return Ok(new
{
ValidVersion = 0,
VersionStatus = 0,
UpdateNotificationStage = 0,
IsVersionIslanded = false,
IsCrossPlayDisabled = false
});
}
[HttpGet("api/gameconfigs/v1/all")]
public async Task<IActionResult> GetGameConfigs()
{
string path = Path.Join(Program.dataDir, "APIS", "GameConfigs.json");
return System.IO.File.Exists(path) ? Content(System.IO.File.ReadAllText(path), "application/json") : NotFound();
}
[HttpGet("api/config/v1/amplitude")]
public async Task<IActionResult> GetAmplitude()
{
return Ok(new
{
AmplitudeKey = "cb2fb2ecb9953512c29af5bca58f2b4a",
UseRudderStack = false,
RudderStackKey = "23NiJHIgu3koaGNCZIiuYvIQNCu",
UseStatSig = true,
StatSigKey = "client-SBZkOrjD3r1Cat3f3W8K6sBd11WKlXZXIlCWj6l4Aje",
StatSigEnvironment = 0
});
}
[HttpGet("api/avatar/v1/defaultunlocked")]
public async Task<IActionResult> GetDefaultUnlocked()
{
return Ok(ServerConfig.Bracket);
}
[HttpGet("api/challenge/v2/getCurrent")]
public IActionResult GetCurrentChallengeBro()
{
return Ok(ServerConfig.Bracket);
}
[HttpGet("api/avatar/v1/defaultbaseavataritems")]
public async Task<IActionResult> GetDefaultBaseAvatarItems()
{
return Ok(ServerConfig.Bracket);
}
[HttpGet("/api/objectives/v1/myprogress")]
public async Task<IActionResult> GetMyObjectiveProgress()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return Ok(new
{
Objectives = ServerConfig.Bracket,
ObjectiveGroups = ServerConfig.Bracket
});
}
[HttpGet("/api/avatar/v2")]
[HttpGet("/api/avatar/v2/{playerId}")]
public async Task<IActionResult> GetMyAvatar(long? playerId = null)
{
if (playerId.HasValue)
{
var player = PlayerDB.Players.FindById(playerId.Value);
if (player == null)
return NotFound("");
return Ok(player.Player?.PlayerExtra.Avatar);
}
var currentPlayer = AuthStuff.GetCurrentPlayer(Request);
if (currentPlayer == null)
return Unauthorized("");
return Ok(currentPlayer.Player?.PlayerExtra.Avatar);
}
[HttpPost("/api/avatar/v2/set")]
public async Task<IActionResult> SetMyAvatar([FromBody] PlayerDBClasses.Avatar request)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var result = PlayerDB.SetAvatar((long)id, request);
return Ok(result);
}
[HttpGet("/api/avatar/v4/items")]
public async Task<IActionResult> GetMyAvatarItems()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var version = AuthStuff.GetAndEnsureAppVersion(Request);
if (version == "20240105")
{
string path2024 = Path.Join(Program.dataDir, "APIS", "Items", "AvatarItems2024.json");
return System.IO.File.Exists(path2024) ? Content(System.IO.File.ReadAllText(path2024), "application/json") : NotFound();
}
string path = Path.Join(Program.dataDir, "APIS", "Items", "AvatarItems.json");
return System.IO.File.Exists(path) ? Content(System.IO.File.ReadAllText(path), "application/json") : NotFound();
}
[HttpPost("/api/playerwarnings")] // dont touch.
public async Task<IActionResult> PlayerWarningStuff()
{
return Ok(new { success = true, error_message = (string?)null, value = (object?)null });
}
private static readonly HttpClient _httpClient = new HttpClient();
private const string ReportWebhookUrl = "https://discord.com/api/webhooks/1545909321139748955/d3GbBbDyB6YAJ7BZowiNwmsum0ziu_7avdJxMFaolhqLxdM50wn07WqHDBpAZFK_20AS";
[HttpPost("/api/PlayerReporting/v3/create")]
public async Task<IActionResult> ReportPlayerCreate()
{
var callerId = AuthStuff.GetPlayerId(Request);
if (callerId == null)
return StatusCode(403);
var form = await Request.ReadFormAsync();
if (!long.TryParse(form["PlayerIdReported"], out long reportedPlayerId) || reportedPlayerId <= 0)
return BadRequest(new { success = false });
var caller = PlayerDB.Players.FindById(callerId.Value);
if (caller == null)
return Ok(new { success = true });
var target = PlayerDB.Players.FindById(reportedPlayerId);
if (target?.Player == null)
return Ok(new { success = false });
int.TryParse(form["ReportCategory"], out int reportCategoryInt);
string reportReason = form["Details"].ToString();
if (string.IsNullOrWhiteSpace(reportReason))
reportReason = form["Comments"].ToString();
if (string.IsNullOrWhiteSpace(reportReason))
reportReason = ReportCategoryReason(reportCategoryInt);
string callerUsername = caller.Player?.Username ?? callerId.Value.ToString();
string targetUsername = target.Player?.Username ?? reportedPlayerId.ToString();
bool isDev = caller.PlayerRoles.Contains(PlayerRoles.Developer);
bool isMod = caller.PlayerRoles.Contains(PlayerRoles.Moderator);
bool isPrivileged = isDev || isMod;
string webhookContent;
if (isPrivileged && TryParseDurationReason(reportReason, out int durationSeconds, out string parsedReason))
{
string roleLabel = isDev && isMod ? "Dev/Mod" : isDev ? "Dev" : "Mod";
if (durationSeconds == 0)
{
UnbanPlayer(target);
webhookContent = $"# **🟢 {roleLabel} unbanned player!** \nPlayer: @{targetUsername} ``{reportedPlayerId}`` \nReason: {parsedReason} \nComing From: {callerUsername}";
}
else
{
if (durationSeconds == -1)
{
durationSeconds = int.MaxValue; // Permanent ban
}
await BanPlayer(target, reportedPlayerId, reportCategoryInt, parsedReason, durationSeconds);
string durationText = FormatDuration(durationSeconds);
webhookContent = $"# **🔴 {roleLabel} banned user!** \nReported Player: @{targetUsername} ``{reportedPlayerId}`` \nDuration: {durationText} \nReason: {parsedReason} \nComing From: {callerUsername}";
}
}
else
{
string prefix = isPrivileged
? $"🟠 {(isDev && isMod ? "Dev/Mod" : isDev ? "Dev" : "Mod")} reported player! (not in ban format so not banned)"
: "🟠 Player Reported!";
webhookContent = $"# **{prefix}** \nReported Player: @{targetUsername} ``{reportedPlayerId}`` \nReason: {reportReason} \nComing From: {callerUsername}";
}
await SendWebhook(webhookContent);
return Ok(new { success = true });
}
private static bool TryParseDurationReason(string input, out int durationSeconds, out string reason)
{
durationSeconds = 0;
reason = input;
if (string.IsNullOrWhiteSpace(input))
return false;
int commaIndex = input.IndexOf(',');
if (commaIndex <= -1)
return false;
string durationPart = input.Substring(0, commaIndex).Trim();
string reasonPart = input.Substring(commaIndex + 1).Trim();
if (!int.TryParse(durationPart, out int seconds) || seconds < -1)
return false;
durationSeconds = seconds;
reason = string.IsNullOrWhiteSpace(reasonPart) ? "No reason given" : reasonPart;
return true;
}
private static string FormatDuration(int seconds)
{
if (seconds == int.MaxValue)
return "Permanent";
var span = TimeSpan.FromSeconds(seconds);
if (span.TotalDays >= 1)
return $"{span.TotalDays:0.##} day(s)";
if (span.TotalHours >= 1)
return $"{span.TotalHours:0.##} hour(s)";
if (span.TotalMinutes >= 1)
return $"{span.TotalMinutes:0.##} minute(s)";
return $"{seconds} second(s)";
}
private async Task BanPlayer(FullPlayer target, long reportedPlayerId, int reportCategoryInt, string reportReason, int durationSeconds) // bro dont use this
{
var mbd = new ModerationBlockDetails
{
IsBan = reportCategoryInt < 100 && durationSeconds != int.MaxValue, // If the report category is less than 100 and the duration is not permanent, it's a ban
ReportCategory = Enum.IsDefined(typeof(ReportCategory), reportCategoryInt) ? (ReportCategory)reportCategoryInt : ReportCategory.Moderator,
Duration = durationSeconds,
Message = reportReason,
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
target.Player.PlayerExtra ??= new PlayerExtra();
target.Player.PlayerExtra.ModerationBlockDetails = mbd;
target.Player.PlayerExtra.Heartbeat = new Heartbeat
{
playerId = reportedPlayerId,
isOnline = false,
roomInstance = null,
errorCode = 0
};
PlayerDB.Players.Update(target);
await NotiController.SendPresenceUpdate(reportedPlayerId, target.Player.PlayerExtra.Heartbeat);
await NotiController.SendAccountUpdate(reportedPlayerId, PlayerDB.GetAccountMe(reportedPlayerId));
await NotificationsController.SendBanned(reportedPlayerId, mbd);
}
private void UnbanPlayer(FullPlayer target)
{
target.Player.PlayerExtra ??= new PlayerExtra();
target.Player.PlayerExtra.ModerationBlockDetails = new ModerationBlockDetails
{
IsBan = false,
ReportCategory = 0,
Duration = 0,
Message = ""
};
PlayerDB.Players.Update(target);
}
private static async Task SendWebhook(string content)
{
var payload = System.Text.Json.JsonSerializer.Serialize(new { content });
await _httpClient.PostAsync(ReportWebhookUrl, new StringContent(payload, System.Text.Encoding.UTF8, "application/json"));
}
private static string ReportCategoryReason(int category) => category switch
{
1 => "Microphone abuse",
2 => "Harassment",
3 => "Cheating or exploiting",
5 => "AFK",
6 => "Disruptive behavior",
7 => "Underage",
100 => "Underage (Code of Conduct)",
101 => "Sexual content",
102 => "Discrimination",
103 => "Trolling",
104 => "Inappropriate name or profile",
200 => "Inappropriate outfit or avatar",
_ => "Violation of the Student Code of Conduct"
};
public async static Task<string> FormatModerationBlockMessageString(ModerationBlockDetails mb)
{
var ReportCategory = mb?.ReportCategory ?? 0;
string suffix = mb?.Duration == 2147483647 ? ".)" : ". Future bans may be longer or permanent.)";
switch (ReportCategory)
{
case ReportCategory.Moderator:
mb.Message = "Moderator (" + mb.Message + suffix;
break;
case ReportCategory.Harassment:
case ReportCategory.CoC_Discrimination:
mb.Message = "Harassment or discrimination (" + mb.Message + suffix;
break;
case ReportCategory.Cheating:
mb.Message = "Cheating (" + mb.Message + suffix;
break;
case ReportCategory.InappropriateClothing:
mb.Message = "Inappropriate custom clothing (" + mb.Message + suffix;
break;
case ReportCategory.CoC_Trolling:
mb.Message = "Trolling (" + mb.Message + suffix;
break;
case ReportCategory.CoC_NameOrProfile:
mb.Message = "Inappropriate name, bio or profile photo (" + mb.Message + suffix;
break;
case ReportCategory.Underage:
case ReportCategory.CoC_Underage:
mb.Message = "Player under 13 must be on a junior account (" + mb.Message + suffix;
break;
case ReportCategory.CoC_Sexual:
mb.Message = "Sexually explicit behavior or photos (" + mb.Message + suffix;
break;
}
return mb?.Message ?? "";
}
[HttpPost("/api/PlayerReporting/v1/moderationBlockDetails")] // dont allow httpget so people cant play on old versions
public async Task<IActionResult> GetMyModerationBlockDetails()
{
var player = AuthStuff.GetCurrentPlayer(Request);
if (player == null)
return Unauthorized("");
await Services.ToxModService.EnsureCurrent(player);
var mb = player.Player.PlayerExtra.ModerationBlockDetails;
mb.Message = await FormatModerationBlockMessageString(mb);
return Ok(mb);
}
[HttpGet("/api/PlayerReporting/v1/voteToKickReasons")]
public IActionResult GetVoteToKickReasons()
{
string path = Path.Join(Program.dataDir, "APIS", "VoteToKickReasons.json");
if (System.IO.File.Exists(path))
return Content(System.IO.File.ReadAllText(path), "application/json");
var reasons = new[]
{
new { ReportCategory = (int)ReportCategory.Harassment, Reason = "Verbal harassment or bullying" },
new { ReportCategory = (int)ReportCategory.Harassment, Reason = "Hate speech or discrimination" },
new { ReportCategory = (int)ReportCategory.Harassment, Reason = "Inappropriate voice chat" },
new { ReportCategory = (int)ReportCategory.Cheating, Reason = "Cheating or exploiting" },
new { ReportCategory = (int)ReportCategory.AFK, Reason = "AFK or inactive" },
new { ReportCategory = (int)ReportCategory.Misc, Reason = "Blocking or disrupting gameplay" },
new { ReportCategory = (int)ReportCategory.Misc, Reason = "Trolling" },
new { ReportCategory = (int)ReportCategory.InappropriateClothing, Reason = "Inappropriate outfit or avatar" }
};
return Ok(reasons);
}
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, (HashSet<long> Voters, DateTime Started)> _voteKickTally = new();
[HttpPost("/api/PlayerReporting/v3/voteToKick")]
public async Task<IActionResult> VoteToKick()
{
var voterId = AuthStuff.GetPlayerId(Request);
if (voterId == null)
return Unauthorized(new { Success = false, Value = (object?)null, Error = "Not authenticated." });
long targetId = 0;
long gameSessionId = 0;
if (Request.HasFormContentType)
{
var form = await Request.ReadFormAsync();
targetId = ReadLong(form, "PlayerId", "playerId", "ReportedPlayer", "TargetPlayerId", "targetPlayerId", "AccountId");
gameSessionId = ReadLong(form, "GameSessionId", "gameSessionId", "gamesessionid");
}
if (targetId == 0 && long.TryParse(Request.Query["playerId"], out var qp)) targetId = qp;
if (targetId == 0 || targetId == voterId.Value)
return Ok(new { Success = false, Value = (object?)null, Error = "Invalid vote target." });
var target = PlayerDB.Players.FindById(targetId);
var targetInstance = target?.Player?.PlayerExtra?.Heartbeat?.roomInstance;
if (target == null || targetInstance == null || target.Player?.PlayerExtra?.Heartbeat?.isOnline != true)
return Ok(new { Success = false, Value = (object?)null, Error = "Target is not in a session." });
string key = $"{targetInstance.roomInstanceId}:{targetId}";
var entry = _voteKickTally.AddOrUpdate(key,
_ => (new HashSet<long> { voterId.Value }, DateTime.UtcNow),
(_, cur) =>
{
if (DateTime.UtcNow - cur.Started > TimeSpan.FromMinutes(3))
return (new HashSet<long> { voterId.Value }, DateTime.UtcNow);
cur.Voters.Add(voterId.Value);
return cur;
});
int othersInInstance = PlayerDB.Players.FindAll().Count(p =>
p.PlayerId != targetId &&
p.Player?.PlayerExtra?.Heartbeat?.isOnline == true &&
p.Player.PlayerExtra.Heartbeat.roomInstance?.roomInstanceId == targetInstance.roomInstanceId);
int needed = Math.Max(2, (othersInInstance / 2) + 1);
int votes = entry.Voters.Count;
bool passed = votes >= needed;
if (passed)
{
_voteKickTally.TryRemove(key, out _);
string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject(
Vanadium.Classes.WebSocket.WebsocketEvents.CreateWSEventString("ModerationQuitGame", new { }));
await NotificationsController.SendToPlayer(targetId, wsEvent);
}
return Ok(new { Success = true, Value = new { Passed = passed, Votes = votes, Needed = needed }, Error = (string?)null });
}
private static long ReadLong(IFormCollection form, params string[] keys)
{
foreach (var k in keys)
if (form.TryGetValue(k, out var v) && long.TryParse(v, out var parsed))
return parsed;
return 0;
}
[HttpGet("/beta/verifydiscord")]
public IActionResult VerifyDiscordPage()
{
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", "VerifyDiscord.html");
if (!System.IO.File.Exists(path))
return NotFound("VerifyDiscord.html not found.");
Response.Headers.CacheControl = "no-cache";
return PhysicalFile(path, "text/html");
}
[HttpPost("/beta/verifydiscord")]
public async Task<IActionResult> VerifyDiscord([FromBody] VerifyDiscordRequest body)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized(new { success = false, error = "Not signed in. Open this page from in-game." });
var pending = Services.PlayerCountService.ConsumeCode(body?.Code);
if (pending == null)
return BadRequest(new { success = false, error = "That code is invalid or expired." });
var already = PlayerDB.Players.FindOne(p =>
p.Player != null && p.Player.DiscordUserId == pending.DiscordUserId.ToString());
if (already != null && already.PlayerId != playerId.Value)
return BadRequest(new { success = false, error = "That Discord account is already linked to another player." });
var player = PlayerDB.Players.FindById(playerId.Value);
if (player?.Player == null)
return NotFound(new { success = false, error = "Player not found." });
player.Player.DiscordUserId = pending.DiscordUserId.ToString();
player.Player.DiscordUsername = pending.DiscordUsername;
player.Player.DiscordLinked = true;
player.Player.DiscordLinkedAt = DateTime.UtcNow;
PlayerDB.Players.Update(player);
await NotificationsController.CreateAndSendWSMessageRecieved(
playerId.Value, 1, PlayerDBClasses.MessageType.TextMessage,
$"Your Discord account (@{pending.DiscordUsername}) is now linked to your Vanadium account.");
return Ok(new { success = true, discordUsername = pending.DiscordUsername, discordUserId = pending.DiscordUserId.ToString() });
}
public class VerifyDiscordRequest { public string? Code { get; set; } }
private async Task PushRelToPlayer(long toPlayerId, long aboutPlayerId)
{
var rel = FriendsDB.GetRelationships(toPlayerId).FirstOrDefault(x => x.OtherPlayerID == aboutPlayerId);
var payload = new
{
Id = "1",
Msg = new
{
PlayerID = aboutPlayerId,
RelationshipType = rel?.RelationshipType ?? RelationshipType.None,
Muted = rel?.Muted ?? ReciprocalStatus.None,
Ignored = rel?.Ignored ?? ReciprocalStatus.None,
Favorited = rel?.Favorited ?? ReciprocalStatus.None
}
};
await NotificationsController.SendToPlayer(
toPlayerId,
JsonConvert.SerializeObject(payload)
);
}
private async Task DeleteFriendInvites(long fromPlayerId, long toPlayerId)
{
var messages = PlayerDB.GetMessages(toPlayerId);
if (messages == null) return;
var messageToDelete = messages.FirstOrDefault(m =>
m.FromPlayerId == fromPlayerId &&
m.Type == PlayerDBClasses.MessageType.FriendInvite
);
if (messageToDelete != null)
{
PlayerDB.DeleteMessage(toPlayerId, messageToDelete.Id);
var payload = new
{
Id = "3",
Msg = new
{
Id = messageToDelete.Id
}
};
await NotificationsController.SendToPlayer(
toPlayerId,
JsonConvert.SerializeObject(payload)
);
}
}
[HttpGet("/api/relationships/v2/get")]
public IActionResult GetMyRelationships()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var rels = FriendsDB.GetRelationships(id.Value)
.Select(r => new
{
PlayerID = r.OtherPlayerID,
r.RelationshipType,
r.Muted,
r.Ignored,
r.Favorited
});
return Ok(rels);
}
[HttpGet("/api/relationships/mutualfriends")]
public IActionResult MutualFriendsGetBitch([FromQuery] long id)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null)
return Unauthorized("");
if (id == 0 || id == me.Value)
return Ok(Array.Empty<object>());
var mine = FriendsDB.GetRelationships(me.Value)
.Where(r => r.RelationshipType == RelationshipType.Friend)
.Select(r => r.OtherPlayerID)
.ToHashSet();
var theirs = FriendsDB.GetRelationships(id)
.Where(r => r.RelationshipType == RelationshipType.Friend)
.Select(r => r.OtherPlayerID)
.ToHashSet();
mine.IntersectWith(theirs);
if (mine.Count == 0)
return Ok(Array.Empty<object>());
var result = mine.Select(mutualId => new
{
Id = (int)(mutualId & 0x7FFFFFFF),
PlayerID = mutualId,
OtherPlayerID = mutualId,
RelationshipType = 3,
Favorited = 0,
Muted = 0,
Ignored = 0,
VoiceVolume = 100
}).ToList();
return Ok(result);
}
[HttpPost("/api/relationships/v1/mute")]
public async Task<IActionResult> Mute([FromForm] long playerId)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null) return Unauthorized("");
if (playerId == me.Value || playerId == 0)
return Ok();
var rel = FriendsDB.UpdateMute(me.Value, playerId, ReciprocalStatus.Local);
await PushRelToPlayer(me.Value, playerId);
await PushRelToPlayer(playerId, me.Value);
return Ok(new
{
PlayerID = playerId,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
[HttpPost("/api/relationships/v1/unmute")]
public async Task<IActionResult> Unmute([FromForm] long playerId)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null) return Unauthorized("");
var rel = FriendsDB.UpdateMute(me.Value, playerId, ReciprocalStatus.None);
await PushRelToPlayer(me.Value, playerId);
await PushRelToPlayer(playerId, me.Value);
return Ok(new
{
PlayerID = playerId,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
[HttpGet("/api/relationships/v2/addfriend")]
[HttpGet("/api/relationships/v2/sendfriendrequest")]
public async Task<IActionResult> SendFriendRequest([FromQuery] long id)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null)
return StatusCode(403);
var player = PlayerDB.Players.FindById(me.Value);
if (player == null || PlayerDB.IsBanned(player.PlayerId))
return StatusCode(StatusCodes.Status403Forbidden);
if (id == 0 || id == me.Value)
return Ok();
var preRelationships = FriendsDB.GetRelationships(me.Value);
var existingRel = preRelationships.FirstOrDefault(x => x.OtherPlayerID == id);
var targetRelationships = FriendsDB.GetRelationships(id);
var targetRel = targetRelationships.FirstOrDefault(x => x.OtherPlayerID == me.Value);
if (existingRel?.RelationshipType == RelationshipType.Friend)
return Ok();
if (targetRel?.RelationshipType == RelationshipType.Sent || existingRel?.RelationshipType == RelationshipType.Received)
{
var rel = FriendsDB.AcceptFriendRequest(me.Value, id);
if (rel == null) return BadRequest();
await DeleteFriendInvites(me.Value, id);
await DeleteFriendInvites(id, me.Value);
var acceptedMsg = PlayerDB.AddMessage(id, me.Value, PlayerDBClasses.MessageType.FriendRequestAccepted, me.Value.ToString());
if (acceptedMsg != null)
{
var toTarget = JsonConvert.SerializeObject(new
{
Id = "2",
Msg = new
{
acceptedMsg.Id,
FromPlayerId = me.Value,
acceptedMsg.SentTime,
Type = (int)PlayerDBClasses.MessageType.FriendRequestAccepted,
Data = me.Value.ToString(),
RoomId = (long?)null,
PlayerEventId = (long?)null
}
});
await NotificationsController.SendToPlayer(id, toTarget);
}
await PushRelToPlayer(me.Value, id);
await PushRelToPlayer(id, me.Value);
return Ok(new
{
PlayerID = id,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
var newRel = FriendsDB.SendFriendRequest(me.Value, id);
await DeleteFriendInvites(id, me.Value);
var inviteMsg = PlayerDB.AddMessage(id, me.Value, PlayerDBClasses.MessageType.FriendInvite, me.Value.ToString());
if (inviteMsg != null)
{
var toTarget = JsonConvert.SerializeObject(new
{
Id = "2",
Msg = new
{
inviteMsg.Id,
FromPlayerId = me.Value,
inviteMsg.SentTime,
Type = (int)PlayerDBClasses.MessageType.FriendInvite,
Data = me.Value.ToString(),
RoomId = (long?)null,
PlayerEventId = (long?)null
}
});
await NotificationsController.SendToPlayer(id, toTarget);
}
await PushRelToPlayer(me.Value, id);
await PushRelToPlayer(id, me.Value);
return Ok(new
{
PlayerID = id,
newRel.RelationshipType,
newRel.Muted,
newRel.Ignored,
newRel.Favorited
});
}
[HttpGet("/api/relationships/v2/acceptfriendrequest")]
public async Task<IActionResult> AcceptFriendRequest([FromQuery] long id)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null)
return StatusCode(403);
var player = PlayerDB.Players.FindById(me.Value);
if (player == null || PlayerDB.IsBanned(player.PlayerId))
return StatusCode(StatusCodes.Status403Forbidden);
if (id == 0 || id == me.Value)
return BadRequest();
var rel = FriendsDB.AcceptFriendRequest(me.Value, id);
if (rel == null)
return BadRequest();
await DeleteFriendInvites(id, me.Value);
await DeleteFriendInvites(me.Value, id);
var acceptedMsg = PlayerDB.AddMessage(id, me.Value, PlayerDBClasses.MessageType.FriendRequestAccepted, me.Value.ToString());
if (acceptedMsg != null)
{
var toTarget = JsonConvert.SerializeObject(new
{
Id = "2",
Msg = new
{
acceptedMsg.Id,
FromPlayerId = me.Value,
acceptedMsg.SentTime,
Type = (int)PlayerDBClasses.MessageType.FriendRequestAccepted,
Data = me.Value.ToString(),
RoomId = (long?)null,
PlayerEventId = (long?)null
}
});
await NotificationsController.SendToPlayer(id, toTarget);
}
await PushRelToPlayer(me.Value, id);
await PushRelToPlayer(id, me.Value);
return Ok(new
{
PlayerID = id,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
[HttpGet("/api/relationships/v2/removefriend")]
public async Task<IActionResult> RemoveFriend([FromQuery] long id)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null)
return StatusCode(403);
var player = PlayerDB.Players.FindById(me.Value);
if (player == null || PlayerDB.IsBanned(player.PlayerId))
return StatusCode(StatusCodes.Status403Forbidden);
if (id == 0 || id == me.Value)
return BadRequest();
var rel = FriendsDB.RemoveFriend(me.Value, id);
await DeleteFriendInvites(id, me.Value);
await DeleteFriendInvites(me.Value, id);
await PushRelToPlayer(me.Value, id);
await PushRelToPlayer(id, me.Value);
return Ok(new
{
PlayerID = id,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
[HttpPost("/api/relationships/v1/ignore")]
public async Task<IActionResult> Ignore([FromForm] long playerId)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null) return Unauthorized("");
var rel = FriendsDB.UpdateIgnore(me.Value, playerId, ReciprocalStatus.Local);
await PushRelToPlayer(me.Value, playerId);
await PushRelToPlayer(playerId, me.Value);
return Ok(new
{
PlayerID = playerId,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
[HttpPost("/api/relationships/v1/unignore")]
public async Task<IActionResult> Unignore([FromForm] long playerId)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null) return Unauthorized("");
var rel = FriendsDB.UpdateIgnore(me.Value, playerId, ReciprocalStatus.None);
await PushRelToPlayer(me.Value, playerId);
await PushRelToPlayer(playerId, me.Value);
return Ok(new
{
PlayerID = playerId,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
[HttpGet("/api/relationships/v1/favorite")]
public IActionResult Friend_Favorite([FromQuery] long id)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null) return Unauthorized("");
var rel = FriendsDB.SetFavorite(me.Value, id, ReciprocalStatus.Local);
return Ok(new
{
PlayerID = id,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
[HttpGet("/api/relationships/v1/unfavorite")]
public IActionResult Friend_UnFavorite([FromQuery] long id)
{
var me = AuthStuff.GetPlayerId(Request);
if (me == null) return Unauthorized("");
var rel = FriendsDB.SetFavorite(me.Value, id, ReciprocalStatus.None);
return Ok(new
{
PlayerID = id,
rel.RelationshipType,
rel.Muted,
rel.Ignored,
rel.Favorited
});
}
[HttpGet("/acc/account/search")]
public async Task<IActionResult> SearchAccounts([FromQuery] string name)
{
/*var authId = AuthStuff.GetPlayerId(Request);
if (authId == null)
return StatusCode(403);*/
//if (PlayerDB.IsBanned(player.PlayerId))
// return StatusCode(StatusCodes.Status403Forbidden);
if (string.IsNullOrWhiteSpace(name))
return Ok(new List<PlayerDTOBase>());
name = name.Trim();
bool isAtSearch = name.StartsWith("@");
if (isAtSearch)
{
var target = name.Substring(1).ToLowerInvariant();
var match = PlayerDB.Players.FindAll()
.FirstOrDefault(p =>
string.Equals(p.Player?.Username ?? "", target, StringComparison.OrdinalIgnoreCase));
if (match == null)
return Ok(new List<PlayerDTOBase>());
var pl = match.Player;
int platformFlags = match.PlatformIds?.Aggregate(0, (acc, pid) => acc | (int)pid.Platform) ?? 0;
return Ok(new List<PlayerDTOBase>
{
new PlayerDTO
{
accountId = match.PlayerId,
username = pl?.Username,
displayName = pl?.DisplayName,
profileImage = pl?.ProfileImage,
isJunior = pl?.IsJunior ?? false,
createdAt = pl?.CreatedAt ?? DateTime.UtcNow,
platforms = platformFlags,
personalPronouns = 0,
identityFlags = 0
}
});
}
var q = name.ToLowerInvariant();
var results = PlayerDB.Players.FindAll()
.Where(p =>
(!string.IsNullOrEmpty(p.Player?.Username) &&
p.Player.Username.ToLowerInvariant().Contains(q)) ||
(!string.IsNullOrEmpty(p.Player?.DisplayName) &&
p.Player.DisplayName.ToLowerInvariant().Contains(q))
)
.Select(p =>
{
var username = p.Player?.Username?.ToLowerInvariant() ?? "";
var display = p.Player?.DisplayName?.ToLowerInvariant() ?? "";
int score = 0;
if (username.StartsWith(q) || display.StartsWith(q))
score += 100;
if (username.Contains(q) || display.Contains(q))
score += 50;
score -= Math.Min(username.Length, display.Length);
return new { Player = p, Score = score };
})
.OrderByDescending(x => x.Score)
.Take(50)
.Select(x =>
{
var pl = x.Player.Player;
int platformFlags = x.Player.PlatformIds?.Aggregate(0, (acc, pid) => acc | (int)pid.Platform) ?? 0;
return new PlayerDTO
{
accountId = x.Player.PlayerId,
username = pl?.Username,
displayName = pl?.DisplayName,
profileImage = pl?.ProfileImage,
isJunior = pl?.IsJunior ?? false,
createdAt = pl?.CreatedAt ?? DateTime.UtcNow,
platforms = platformFlags,
personalPronouns = 0,
identityFlags = 0
} as PlayerDTOBase;
})
.ToList();
return Ok(results);
}
[HttpPost("/data/heartbeat")]
public async Task<IActionResult> Heartbeat()
{
var jwtPlayerId = AuthStuff.GetPlayerId(Request);
using var reader = new StreamReader(Request.Body);
var raw = await reader.ReadToEndAsync();
if (string.IsNullOrWhiteSpace(raw))
return BadRequest("Empty body");
var parsed = System.Web.HttpUtility.ParseQueryString(raw);
var eventDataEncoded = parsed["eventData"];
if (string.IsNullOrWhiteSpace(eventDataEncoded))
return BadRequest("Missing eventData");
string eventDataJson;
try
{
eventDataJson = Uri.UnescapeDataString(eventDataEncoded);
}
catch
{
eventDataJson = eventDataEncoded;
}
using var outerDoc = JsonDocument.Parse(eventDataJson);
var root = outerDoc.RootElement;
string? eventType = null;
if (root.TryGetProperty("EventType", out var et))
eventType = et.GetString();
string? eventParamsRaw = null;
if (root.TryGetProperty("EventParams", out var ep))
eventParamsRaw = ep.GetString();
long resolvedAccountId = jwtPlayerId ?? 0;
string? timeUtc = null;
if (!string.IsNullOrWhiteSpace(eventParamsRaw))
{
try
{
using var innerDoc = JsonDocument.Parse(eventParamsRaw);
var inner = innerDoc.RootElement;
if (inner.TryGetProperty("_AccountId", out var aid))
{
if (aid.ValueKind == JsonValueKind.Number)
resolvedAccountId = aid.GetInt64();
else if (aid.ValueKind == JsonValueKind.String && long.TryParse(aid.GetString(), out var parsedId))
resolvedAccountId = parsedId;
}
if (jwtPlayerId != null && resolvedAccountId != jwtPlayerId.Value)
{
Console.WriteLine($"[account/me switch detected] JWT player ID {jwtPlayerId.Value} does not match resolved account ID {resolvedAccountId}");
APIController.SendNonHileWebhook(jwtPlayerId.Value, $"[account/me switch detected] @everyone JWT player ID {jwtPlayerId.Value} does not match resolved account ID {resolvedAccountId}");
if (!PlayerDB.IsBanned(jwtPlayerId.Value))
{
var player = AuthStuff.GetCurrentPlayer(Request);
var mbd = new ModerationBlockDetails
{
IsBan = true,
ReportCategory = ReportCategory.Cheating,
Duration = 2147483647,
Message = "Cheating",
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
player.Player ??= new Player();
player.Player.PlayerExtra ??= new PlayerExtra();
PlayerDB.ApplyBan(jwtPlayerId.Value, "Cheating");
PlayerDB.Players.Update(player);
string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject(
WebsocketEvents.CreateWSEventString("ModerationQuitGame", new { }));
await NotificationsController.SendToPlayer(jwtPlayerId.Value, wsEvent);
await NotificationsController.SendBanned(jwtPlayerId.Value, mbd);
}
}
if (inner.TryGetProperty("_TimeUtc", out var tu))
timeUtc = tu.GetString();
}
catch (Exception ex)
{
Console.WriteLine("EventParams parse failed:");
Console.WriteLine(eventParamsRaw);
Console.WriteLine(ex.Message);
}
}
// Console.WriteLine($"Heartbeat | Type={eventType} | Account={resolvedAccountId} | Time={timeUtc}");
if (resolvedAccountId <= 0)
return Unauthorized("");
var heartbeat = PlayerDB.GetPlayerHeartbeat(resolvedAccountId);
return Ok(heartbeat);
}
[HttpPost("/upload")]
public async Task<IActionResult> Upload()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var player = PlayerDB.Players.FindById(id.Value);
if (player == null || PlayerDB.IsBanned(player.PlayerId)) return Unauthorized("");
var file = Request.Form.Files.FirstOrDefault();
if (file == null || file.Length == 0)
return BadRequest("");
if (!Request.Form.TryGetValue("fileType", out var fileTypeRaw) || !int.TryParse(fileTypeRaw.FirstOrDefault(), out int fileType))
return BadRequest("");
(string subDir, string extension) = fileType switch
{
0 => ("unknown", ".bin"),
1 => ("room", ".room"),
2 => ("htr", ".htr"),
3 => ("Images", ".png"),
//4 => ("video", ".mp4"),
5 => ("inv", ".inv"),
6 => ("meta", ".meta"),
_ => ((string)null, (string)null)
};
if (subDir == null)
return BadRequest($"{fileType}, boii");
string basePath;
if (fileType == 3)
{
basePath = Path.GetFullPath(Path.Join(Program.dataDir, subDir));
}
else
{
basePath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", subDir));
}
Directory.CreateDirectory(basePath);
var safeName = $"{Guid.NewGuid()}{extension}";
var filePath = Path.Join(basePath, safeName);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(stream);
}
Console.WriteLine($":3, {safeName}");
return Ok(new
{
filename = safeName
});
}
[HttpGet("/api/messages/v2/get")]
public async Task<IActionResult> GetMyMessages()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return Ok(PlayerDB.GetMessages((long)id));
}
[HttpPost("/api/messages/v3/delete")]
public async Task<IActionResult> DeleteMyMessages([FromBody] DeleteMessagesRequest request)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
if (request?.MessageIds == null || request.MessageIds.Count == 0)
return BadRequest();
PlayerDB.DeleteMessagesBulk((long)id, request.MessageIds);
return NoContent();
}
public class DeleteMessagesRequest
{
public List<long> MessageIds { get; set; }
}
[HttpGet("/playersettings")]
public async Task<IActionResult> GetMySettings()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return Ok(PlayerDB.GetPlayerSettings(id.Value));
}
[HttpPut("/playersettings")]
public async Task<IActionResult> SetMySettings([FromForm] string key, [FromForm] string value)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
PlayerDB.SetPlayerSetting(key, value ?? "", (long)id);
var version = AuthStuff.GetAndEnsureAppVersion(Request);
if (version == "20250724")
return Ok(new
{
success = true,
errorId = (string?)null,
error = (string?)null
});
return NoContent();
}
[HttpDelete("/playersettings")]
public IActionResult DeleteMySettings([FromForm] string key)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
if (string.IsNullOrWhiteSpace(key))
return BadRequest("");
PlayerDB.DeletePlayerSetting(key, (long)id);
return Ok(new
{
success = true,
errorId = (string?)null,
error = (string?)null
});
}
[HttpGet("/api/checklist/v1/current")]
public async Task<IActionResult> GetMyChecklist()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return Ok(ServerConfig.Bracket);
}
[HttpGet("/api/players/v2/progression/bulk")]
public async Task<IActionResult> GetProgressionBulk([FromQuery(Name = "id")] List<long> ids)
{
if (ids == null || ids.Count == 0)
return Ok(Array.Empty<object>());
ids = ids.Distinct().ToList();
var results = PlayerDB.GetProgressionBulk(ids);
var foundIds = results.Select(r => r.PlayerId).ToHashSet();
foreach (var id in ids.Where(i => !foundIds.Contains(i)))
{
results.Add(new PlayerProgressionDTO { PlayerId = id, Level = 1, XP = 0 });
}
/*foreach (var prog in results)
{
_ = NotiController.SendEvent(prog.PlayerId, "PlayerProgressionLevelUpdate", new
{
PlayerId = prog.PlayerId,
Level = prog.Level,
XP = prog.XP
});
}*/
return Ok(results.OrderBy(r => r.PlayerId));
}
[HttpGet("/api/playerReputation/v2/bulk")]
public async Task<IActionResult> GetReputationBulk([FromQuery] List<long> id)
{
var authId = AuthStuff.GetPlayerId(Request);
if (authId == null)
return Unauthorized("");
if (id == null || id.Count == 0)
return Ok(new List<PlayerDBClasses.Reputation>());
var results = PlayerDB.GetReputationBulk(id);
return Ok(results);
}
private const string WebhookUrl = "https://discord.com/api/webhooks/1519900900372647937/5Q8tUiljiix2PclNDt1YtCUEPqf2H9c3iMB5dkVhf9q_4hamIpxzo2-hbYhvcJ5VRudx";
private static async Task SendSanitizeWebhook(long accountId, string message, string instance)
{
var payload = new
{
username = "Vanadium",
embeds = new[]
{
new
{
title = "Vanadium Sanitize",
color = 3066993,
fields = new[]
{
new { name = "Account ID", value = accountId.ToString(), inline = true },
new { name = "Raw Message", value = message, inline = true },
new { name = "Instance", value = instance, inline = true }
}
}
}
};
var content = new StringContent(System.Text.Json.JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
try { await _webhookClient.PostAsync(WebhookUrl, content); } catch { }
}
private static async Task<string> AppendCoachResponse(string message)
{
var response = await ChatController.RequestNvidiaAPIResponseAndReturnString(message);
if (response == null)
return message;
return $"{message}\n\ncoach response: {response}";
}
[HttpPost("/v1/batch/rudderstack")]
public IActionResult RudderstackShit()
{
return Ok(ServerConfig.Bracket);
}
[HttpPost("/api/sanitize/v1")]
public async Task<IActionResult> SanitizeV1([FromBody] SanitizeRequest request)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null) return StatusCode(403);
var currentPlayerHeartbeat = PlayerDB.GetCurrentPlayer((long)playerId)?.Player?.PlayerExtra?.Heartbeat?.roomInstance;
if (request.Value.ToLower().Contains("@coach"))
request.Value = await AppendCoachResponse(request.Value);
_ = SendSanitizeWebhook((long)playerId, request.Value, currentPlayerHeartbeat?.Name ?? "Unknown Room");
if (string.IsNullOrEmpty(request?.Value))
{
return new JsonResult(string.Empty);
}
if (ServerConfig.AllowSwears)
{
return new JsonResult(request.Value);
}
if (request.Value.StartsWith("@Coach Videos"))
{
var filePath2 = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "video");
string message = "Here is a list of all videos on the server:\n";
if (Directory.Exists(filePath2))
{
var videoFiles = Directory.GetFiles(filePath2, "*.mp4");
if (videoFiles.Length > 0)
{
foreach (var videoFile in videoFiles)
{
var fileName = Path.GetFileName(videoFile);
message += $"- {fileName}\n";
}
}
else
{
message += "No videos found.";
}
}
else
{
message += "Video directory does not exist.";
}
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new
{
Id = "2",
Msg = new
{
FromPlayerId = 1,
Id = new Random().Next(1, 0x7ffffff),
SentTime = DateTime.UtcNow,
Type = 100,
Data = message,
RoomId = 1
}
});
await NotificationsController.SendToPlayer((long)playerId, json);
}
string filePath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "swears.txt");
if (!System.IO.File.Exists(filePath))
{
return NotFound("");
}
string sanitizedText = request.Value;
var target = PlayerDB.Players.FindById(playerId);
if (target.PlayerRoles.Contains(PlayerRoles.Developer))
{
return new JsonResult(sanitizedText);
}
var swearWords = await System.IO.File.ReadAllLinesAsync(filePath);
foreach (var word in swearWords.Select(w => w.Trim()).Where(w => !string.IsNullOrEmpty(w)))
{
string pattern = $@"(?<=[^a-zA-Z]|^)({Regex.Escape(word)})(?=[^a-zA-Z]|$)|(?<=[^a-zA-Z]|^)({Regex.Escape(word)})([^a-zA-Z\s]+)|([^a-zA-Z\s]+)({Regex.Escape(word)})(?=[^a-zA-Z]|$)|([^a-zA-Z\s]+)({Regex.Escape(word)})([^a-zA-Z\s]+)";
sanitizedText = Regex.Replace(sanitizedText, pattern, m =>
{
var groups = m.Groups;
if (groups[1].Success) return new string('*', groups[1].Length);
if (groups[2].Success) return new string('*', groups[2].Length) + groups[3].Value;
if (groups[5].Success) return groups[4].Value + new string('*', groups[5].Length);
return groups[6].Value + new string('*', groups[7].Length) + groups[8].Value;
}, RegexOptions.IgnoreCase);
}
return new JsonResult(sanitizedText);
}
[HttpPost("/api/sanitize/v1/isPure")]
public async Task<IActionResult> SanitizeV1IsPure([FromBody] SanitizeRequest request)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null) return StatusCode(403);
var target = PlayerDB.Players.FindById(playerId);
if (target.PlayerRoles.Contains(PlayerRoles.Developer))
{
return Ok(new { IsPure = true });
}
if (ServerConfig.AllowSwears || string.IsNullOrEmpty(request?.Value))
{
return Ok(new { IsPure = true });
}
string filePath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "swears.txt");
if (!System.IO.File.Exists(filePath))
{
return NotFound("");
}
var swearWords = await System.IO.File.ReadAllLinesAsync(filePath);
foreach (var word in swearWords.Select(w => w.Trim()).Where(w => !string.IsNullOrEmpty(w)))
{
string pattern = $@"\b{Regex.Escape(word)}\b";
if (Regex.IsMatch(request.Value, pattern, RegexOptions.IgnoreCase))
{
return Ok(new { IsPure = false });
}
}
return Ok(new { IsPure = true });
}
[HttpPut("/reports/{reportuuid}")]
public async Task<IActionResult> InitializeReport(string reportuuid, [FromBody] ReportsDBClasses.ReportInitializationRequest request)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null) return StatusCode(403);
if (request == null || string.IsNullOrEmpty(reportuuid)) return BadRequest();
var report = ReportsDB.GetOrCreateReport(reportuuid, (long)playerId);
if (report.PlayerId != (long)playerId) return StatusCode(403);
if (request.Client != null)
{
report.TriggerId = request.Client.TriggerId;
report.TriggerType = request.Client.TriggerType;
report.TriggeredAt = request.Client.TriggeredAt;
}
ReportsDB.UpdateReport(report);
return Ok(new { success = true });
}
[HttpPut("/reports/{reportuuid}/attachments/Trace.log")]
public async Task<IActionResult> UploadTraceLog(string reportuuid)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null) return StatusCode(403);
var report = ReportsDB.GetReport(reportuuid);
if (report == null || report.PlayerId != (long)playerId) return StatusCode(403);
string dir = Path.Combine(Program.dataDir, "TempReports", reportuuid);
Directory.CreateDirectory(dir);
string filePath = Path.Combine(dir, "Trace.log");
using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
await Request.Body.CopyToAsync(fs);
}
report.TraceLogPath = filePath;
ReportsDB.UpdateReport(report);
return Ok(new { success = true });
}
[HttpPut("/reports/{reportuuid}/userinfo")]
public async Task<IActionResult> UploadReportUserInfo(string reportuuid, [FromBody] ReportsDBClasses.ReportUserInfoRequest request)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null) return StatusCode(403);
if (request == null) return BadRequest();
var report = ReportsDB.GetReport(reportuuid);
if (report == null || report.PlayerId != (long)playerId) return StatusCode(403);
report.Title = request.Title;
report.Description = request.Description;
ReportsDB.UpdateReport(report);
return Ok(new { success = true });
}
[HttpPut("/reports/{reportuuid}/attachments/game_screenshot.png")]
public async Task<IActionResult> UploadGameScreenshot(string reportuuid)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null) return StatusCode(403);
var report = ReportsDB.GetReport(reportuuid);
if (report == null || report.PlayerId != (long)playerId) return StatusCode(403);
string dir = Path.Combine(Program.dataDir, "TempReports", reportuuid);
Directory.CreateDirectory(dir);
string filePath = Path.Combine(dir, "game_screenshot.png");
using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
await Request.Body.CopyToAsync(fs);
}
report.GameScreenshotPath = filePath;
ReportsDB.UpdateReport(report);
return Ok(new { success = true });
}
[HttpPut("/Reports/{reportuuid}/Complete")]
public async Task<IActionResult> CompleteReport(string reportuuid)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null) return StatusCode(403);
var report = ReportsDB.GetReport(reportuuid);
if (report == null || report.PlayerId != (long)playerId) return StatusCode(403);
if (string.IsNullOrEmpty(report.Title) || string.IsNullOrEmpty(report.TraceLogPath) || !System.IO.File.Exists(report.TraceLogPath))
{
return BadRequest(new { success = false });
}
string reporterText = "Unknown Reporter";
var reporter = PlayerDB.Players.FindById((long)playerId);
if (reporter?.Player != null)
{
reporterText = $"{reporter.Player.DisplayName} (@{reporter.Player.Username})";
}
string targetChannelId = "1507514835443650590";
try
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bot", ServerConfig.CountBotToken);
using var form = new MultipartFormDataContent();
var embedObj = new System.Dynamic.ExpandoObject() as IDictionary<string, object>;
embedObj["title"] = $"**{report.Title}**";
embedObj["description"] = string.IsNullOrWhiteSpace(report.Description) ? "*No description provided.*" : report.Description;
embedObj["color"] = 16731136;
embedObj["footer"] = new { text = $"Uploaded by: {reporterText}" };
embedObj["timestamp"] = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
if (!string.IsNullOrEmpty(report.GameScreenshotPath) && System.IO.File.Exists(report.GameScreenshotPath))
{
var screenshotBytes = await System.IO.File.ReadAllBytesAsync(report.GameScreenshotPath);
var screenshotContent = new ByteArrayContent(screenshotBytes);
screenshotContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png");
form.Add(screenshotContent, "files[1]", "game_screenshot.png");
embedObj["thumbnail"] = new { url = "attachment://game_screenshot.png" };
}
var logBytes = await System.IO.File.ReadAllBytesAsync(report.TraceLogPath);
var logContent = new ByteArrayContent(logBytes);
logContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("text/plain");
form.Add(logContent, "files[0]", "Trace.log");
var payloadJson = System.Text.Json.JsonSerializer.Serialize(new
{
embeds = new[] { embedObj }
});
form.Add(new StringContent(payloadJson, Encoding.UTF8, "application/json"), "payload_json");
var response = await client.PostAsync($"https://discord.com/api/v10/channels/{targetChannelId}/messages", form);
if (response.IsSuccessStatusCode)
{
report.IsCompleted = true;
ReportsDB.UpdateReport(report);
try
{
if (System.IO.File.Exists(report.TraceLogPath)) System.IO.File.Delete(report.TraceLogPath);
if (!string.IsNullOrEmpty(report.GameScreenshotPath) && System.IO.File.Exists(report.GameScreenshotPath)) System.IO.File.Delete(report.GameScreenshotPath);
Directory.Delete(Path.Combine(Program.dataDir, "TempReports", reportuuid));
}
catch { }
return Ok(new { success = true });
}
else
{
string errorContent = await response.Content.ReadAsStringAsync();
Console.WriteLine($"[Discord Error] failed to post bug report: {errorContent}");
}
}
catch (Exception ex)
{
Console.WriteLine($"[Reports Controller Error]: {ex.Message}");
}
return Ok(new { success = false });
}
[HttpGet("/api/rooms/v1/filters")]
public IActionResult GetRoomTags()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var roomTags = new
{
PinnedFilters = new[]
{
"recroomoriginal", "community", "featured", "quest", "pvp", "hangout", "game",
"art", "store", "tutorial", "fandom", "performance", "action", "horror"
},
PopularFilters = new[]
{
"pvp", "quest", "game", "hangout", "art"
},
TrendingFilters = new[]
{
"roleplay", "nomp", "rp", "casual", "fun", "action", "military", "sports"
}
};
return new ContentResult
{
Content = JsonConvert.SerializeObject(roomTags),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpPost("/api/rooms/v2/report")]
public async Task<IActionResult> ReportRoom([FromForm] long roomId, [FromForm] string details, [FromForm] int reportCategory)
{
var reporterId = AuthStuff.GetPlayerId(Request);
if (reporterId == null)
return StatusCode(403);
string categoryName = reportCategory switch
{
101 => "Sexual Content in a Public Room",
102 => "Racist or discriminatory content",
104 => "Inappropriate name, picture or description",
103 => "Disruptive Trolling",
11 => "Misleading purchases",
6 => "Other",
_ => $"Unknown Category ({reportCategory})"
};
string roomName = "Unknown Room";
string roomOwnerText = "Unknown Owner";
string thumbnailUrl = ServerConfig.BaseURL + "/cdn/images/DefaultRoom.png";
var room = RoomDB.Rooms.FindById(roomId);
if (room != null)
{
roomName = room.Name ?? "Unnamed Room";
if (!string.IsNullOrWhiteSpace(room.ImageName))
{
thumbnailUrl = $"{ServerConfig.BaseURL}/imageserver/{room.ImageName}";
}
var owner = PlayerDB.Players.FindById(room.CreatorAccountId);
if (owner?.Player != null)
{
roomOwnerText = $"{owner.Player.DisplayName} (@{owner.Player.Username})";
}
}
string reporterText = "Unknown Reporter";
var reporter = PlayerDB.Players.FindById((long)reporterId);
if (reporter?.Player != null)
{
reporterText = $"{reporter.Player.DisplayName} (@{reporter.Player.Username})";
}
var payload = new
{
embeds = new[]
{
new
{
title = $"**{categoryName}**",
description = string.IsNullOrWhiteSpace(details) ? "*No details provided.*" : details,
color = 15158332,
thumbnail = new { url = thumbnailUrl },
fields = new[]
{
new { name = "Room Name", value = roomName, inline = true },
new { name = "Room Owner", value = roomOwnerText, inline = true },
new { name = "Reporter", value = reporterText, inline = false }
},
footer = new { text = "Vanadium" },
timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
}
}
};
try
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bot", ServerConfig.CountBotToken);
var json = System.Text.Json.JsonSerializer.Serialize(payload);
using var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await client.PostAsync("https://discord.com/api/v10/channels/1507514835443650590/messages", content);
if (response.IsSuccessStatusCode)
{
return Ok(new { success = true });
}
}
catch { }
return Ok(new { success = false });
}
[Route("/api/PlayerReporting/v1/hile")]
public async Task<IActionResult> PlayerReportingHile([FromForm] string? Message, [FromForm] HileType? Type, [FromForm] long? ReportedPlayer)
{
var authId = AuthStuff.GetPlayerId(Request);
if (authId == null)
return Unauthorized("");
long playerId = (long)authId;
bool isFullyIgnored = Type == HileType.ImageSignature
|| Type == HileType.AppData_Boot_InvalidSignature
|| Type == HileType.AppData_Boot_UnableToVerifySignatures
|| Type == HileType.Driver_Invalid_Signature
|| Type == HileType.Obscured;
bool isLogOnly = Type == HileType.Photon_InstantiateTool;
if (!isFullyIgnored)
{
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
string? webhookMessage = isLogOnly ? $"{Message} (not banned)" : Message;
await SendHileWebhook(playerId, webhookMessage, Type, ReportedPlayer);
if (!isLogOnly && !PlayerDB.IsBanned(playerId))
{
var mbd = new ModerationBlockDetails
{
IsBan = true,
ReportCategory = ReportCategory.Cheating,
Duration = 2147483647,
Message = "Cheating ((Abusing cheats or exploits to disrupt other players experience. (ban_code: door) (DM a mod or admin to appeal)) Future bans may be longer or permanent).",
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
player.Player ??= new Player();
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.ModerationBlockDetails = mbd;
player.Player.PlayerExtra.Heartbeat = new Heartbeat
{
playerId = playerId,
isOnline = false,
roomInstance = null,
errorCode = 0
};
PlayerDB.Players.Update(player);
await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat);
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
await NotificationsController.SendBanned(playerId, mbd);
}
}
return Ok(false);
}
internal static async Task SendHileWebhook(
long accountId,
string? message,
HileType? type,
long? reportedPlayer,
byte[]? imageBytes = null,
string? imageFileName = null)
{
reportedPlayer ??= 0;
var embed = new Dictionary<string, object?>
{
["title"] = "Vanadium Hile API (game anticheat 1) bro cheating",
["color"] = 3066993,
["fields"] = new object[]
{
new { name = "Account ID", value = accountId.ToString(), inline = true },
new { name = "Type", value = type?.ToString() ?? "Unknown", inline = true },
new { name = "Message", value = message ?? "No message provided", inline = false },
new { name = "Reported Player", value = reportedPlayer.ToString(), inline = false }
}
};
if (imageBytes != null && imageFileName != null)
embed["image"] = new { url = $"attachment://{imageFileName}" };
var payload = new
{
username = "Vanadium",
embeds = new[] { embed }
};
try
{
if (imageBytes != null && imageFileName != null)
{
using var form = new MultipartFormDataContent();
form.Add(new StringContent(
System.Text.Json.JsonSerializer.Serialize(payload),
Encoding.UTF8, "application/json"), "payload_json");
var fileContent = new ByteArrayContent(imageBytes);
fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png");
form.Add(fileContent, "files[0]", imageFileName);
await _webhookClient.PostAsync(
"https://discord.com/api/webhooks/1517032697371824178/UmlTVL9C_ur41T1q9sybTkK2F9pvNGp7WZ4GJIzxaSOo7T545aUAchPsK_AVDM0TDPEH",
form);
}
else
{
var content = new StringContent(
System.Text.Json.JsonSerializer.Serialize(payload),
Encoding.UTF8, "application/json");
await _webhookClient.PostAsync(
"https://discord.com/api/webhooks/1517032697371824178/UmlTVL9C_ur41T1q9sybTkK2F9pvNGp7WZ4GJIzxaSOo7T545aUAchPsK_AVDM0TDPEH",
content);
}
}
catch { }
}
internal static async Task SendNonHileWebhook(
long accountId,
string? message)
{
var embed = new Dictionary<string, object?>
{
["title"] = $"ID = {accountId}",
["color"] = 3066993,
["fields"] = new object[]
{
new { name = "Message", value = message ?? "No message provided", inline = false }
}
};
var payload = new
{
username = "Vanadium",
embeds = new[] { embed }
};
try
{
var content = new StringContent(
System.Text.Json.JsonSerializer.Serialize(payload),
Encoding.UTF8, "application/json");
await _webhookClient.PostAsync(
"https://discord.com/api/webhooks/1535007757286113353/gX1m4BxZVN8ZCcvHzUxB7CZO8KkQSSI6fmYWczk7jbh7FMV0bzpNCQg5ZWLaSo_sofDq",
content);
}
catch { }
}
public static readonly HttpClient _webhookClient = new();
[HttpGet("api/config/v2")]
public async Task<IActionResult> ApiConfigV2()
{
string path = Path.Join(Program.dataDir, "APIS", "ConfigV2.json");
return System.IO.File.Exists(path) ? Content(System.IO.File.ReadAllText(path), "application/json") : NotFound();
}
[HttpGet("api/config/v1/backtrace")]
public async Task<IActionResult> BacktraceTemp([FromQuery] string platformType, [FromQuery] bool allocate)
{
return Ok(new { success = true, error_message = (string?)null, value = (object?)null });
}
[HttpGet("/cdn/config/LoadingScreenTipData")]
public async Task<IActionResult> GetLoadingScreenTips()
{
string path = Path.Join(Program.dataDir, "APIS", "loadingscreens.json");
return System.IO.File.Exists(path) ? Content(System.IO.File.ReadAllText(path), "application/json") : NotFound();
}
[HttpGet("/cdn/config/{fileName}")]
public async Task<IActionResult> GetConfig2025(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
return BadRequest("failed to find");
string basePath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", "config"));
string filePath = Path.Join(basePath, fileName);
if (!System.IO.File.Exists(filePath))
return NotFound("");
try
{
var fileBytes = System.IO.File.ReadAllBytes(filePath);
return File(fileBytes, SafeGetContentType(fileName), fileName);
}
catch
{
return StatusCode(500);
}
}
[HttpGet("/api/progressionEvents/active")]
public async Task<IActionResult> progressionActiveEvenet()
{
return Ok(false);
}
[HttpGet("/outfits/me/saved")]
[HttpGet("/api/avatar/v{ver}/saved")]
public ActionResult GetOutfit()
{
long? id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var fullPlayer = PlayerDB.GetCurrentPlayer(id.Value);
if (fullPlayer?.Player?.PlayerExtra == null)
return NotFound("");
var savedOutfits = fullPlayer.Player.PlayerExtra.SavedAvatars;
string jsonResponse = System.Text.Json.JsonSerializer.Serialize(savedOutfits);
return new ContentResult()
{
Content = jsonResponse,
ContentType = "application/json",
StatusCode = 200
};
}
[HttpPost("/api/avatar/v{ver}/saved/set")]
public async Task<IActionResult> SetSavedAvatar([FromBody] SavedAvatarRequest request)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
if (request == null)
return BadRequest();
var player = PlayerDB.Players.FindById(id.Value);
if (player?.Player?.PlayerExtra == null)
return NotFound("");
player.Player.PlayerExtra.SavedAvatars ??= new List<PlayerDBClasses.SavedOutfit>();
var existing = player.Player.PlayerExtra.SavedAvatars
.FirstOrDefault(s => s.Slot == request.Slot);
if (existing != null)
{
existing.OutfitSelections = request.OutfitSelections ?? existing.OutfitSelections;
existing.FaceFeatures = request.FaceFeatures ?? existing.FaceFeatures;
existing.SkinColor = request.SkinColor ?? existing.SkinColor;
existing.HairColor = request.HairColor ?? existing.HairColor;
existing.PreviewImageName = request.PreviewImageName ?? existing.PreviewImageName;
existing.Name = request.Name ?? existing.Name;
}
else
{
player.Player.PlayerExtra.SavedAvatars.Add(new PlayerDBClasses.SavedOutfit
{
Slot = request.Slot,
OutfitSelections = request.OutfitSelections ?? "",
FaceFeatures = request.FaceFeatures ?? "",
SkinColor = request.SkinColor ?? "",
HairColor = request.HairColor ?? "",
PreviewImageName = request.PreviewImageName,
Name = request.Name
});
}
PlayerDB.Players.Update(player);
return Ok(new { success = true });
}
/*[HttpGet("/api/equipment/v2/getUnlocked")]
public async Task<IActionResult> Test2025Skins()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
string path = Path.Join(Program.dataDir, "APIS", "2025Consumables", "Equipment.json");
return PhysicalFile(path, "application/json");
}*/
[HttpGet("/sections/pagesource/{page}")]
public IActionResult WatchPagesTypeShit(string page)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "2025Watch", $"{page}.json");
if (!System.IO.File.Exists(path))
return Ok();
return PhysicalFile(path, "application/json");
}
[HttpGet("/api/freegifts/v1/sendmultiple")] // to-do gifts system
public async Task<IActionResult> SendFreeGift()
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null)
return Unauthorized("");
return Ok(new { success = true });
}
[HttpGet("/api/roomcurrencies/v1/currencies")]
public IActionResult idkWhatThisIs() => Ok();
[HttpGet("/api/keepsakes/globalconfig")]
public async Task<IActionResult> GetKeepsakeGlobalConfig()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return Ok(new
{
KeepsakeFeatureEnabled = true,
KeepsakeRoomLimit = 10,
SocialXpBoostEnabled = false
});
}
[HttpPost("/api/screensharereports/v1/report")]
public async Task<IActionResult> ReportScreenShareIssue([FromForm] string imageName, [FromForm] long? reportedPlayerId, [FromForm] long? roomId, [FromForm] long? roomInstanceId, [FromForm] string roomInstanceType, [FromForm] string details)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var value = new Dictionary<string, object>();
if (!string.IsNullOrEmpty(imageName))
value["imageName"] = imageName;
if (reportedPlayerId.HasValue)
value["reportedPlayerId"] = reportedPlayerId.Value;
if (roomId.HasValue)
value["roomId"] = roomId.Value;
if (roomInstanceId.HasValue)
value["roomInstanceId"] = roomInstanceId.Value;
if (!string.IsNullOrEmpty(roomInstanceType))
value["roomInstanceType"] = roomInstanceType;
if (!string.IsNullOrEmpty(details))
value["details"] = details;
if (value.Count == 0)
return Ok(new { success = true });
return Ok(new { success = true, value });
}
[HttpGet("/api/quickPlay/v1/getandclear")]
public async Task<IActionResult> GetAndClearQuickPlay()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return NoContent();
}
[HttpGet("/api/keepsakes/rooms/{roomId}")]
public async Task<IActionResult> GetKeepsakesForRoom(ulong roomId)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return NoContent();
}
[HttpGet("/api/activities/charades/v1/words/Icebreakers")]
public async Task<IActionResult> GetIcebreakers()
{
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "Icebreakers.json");
if (!System.IO.File.Exists(path))
return Ok(Array.Empty<object>());
return PhysicalFile(path, "application/json");
}
[HttpGet("/api/activities/charades/v1/words/Charades")]
public async Task<IActionResult> GetCharades()
{
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "Charades.json");
if (!System.IO.File.Exists(path))
return Ok(Array.Empty<object>());
return PhysicalFile(path, "application/json");
}
// RoomConsumableController.cs replaces bracket response
[HttpGet("/api/influencerpartnerprogram/influencers")]
public async Task<IActionResult> GetAllInfluencers()
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null)
return Unauthorized("");
return new ContentResult
{
Content = JsonConvert.SerializeObject(new
{
InfluencerIds = PlayerDB.GetInfluencerIds(),
ContinuationToken = (string?)null
}),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpGet("/api/storefronts/v1/toptoday")]
public async Task<IActionResult> TopTodayStore()
{
return Ok(ServerConfig.Bracket);
}
[HttpPost("/api/influencerpartnerprogram/support")]
public async Task<IActionResult> SupportInfluencer()
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null)
return Unauthorized("");
string? SupportIdStr = HttpContext.Request.Form["influencerAccountId"].ToString();
ulong SupportId = ulong.Parse(SupportIdStr);
PlayerDB.SupportInfluencer(account.Value, (long)SupportId);
return new ContentResult
{
Content = JsonConvert.SerializeObject(new
{
success = true,
error = "",
value = (string?)null
}),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpPost("/api/influencerpartnerprogram/remove")]
public async Task<IActionResult> RemoveInfluencer()
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null)
return Unauthorized("");
PlayerDB.SupportInfluencer(account.Value, 0);
return new ContentResult
{
Content = JsonConvert.SerializeObject(new
{
success = true,
error = "",
value = (string?)null
}),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpGet("/api/influencerpartnerprogram/influencer")]
public IActionResult GetInfluencer([FromQuery] long accountId)
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null)
return Unauthorized("");
var player = PlayerDB.Players.FindById(accountId);
return Ok(player?.Player?.PlayerExtra?.Influencer?.SupportingInfluencer ?? 0);
}
[HttpGet("/api/influencerpartnerprogram/myinfluencer")]
public async Task<IActionResult> MyInfluencer()
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null)
return Unauthorized("");
var myInfluencer = PlayerDB.GetMyInfluencer(account.Value);
if (myInfluencer == 0)
return NotFound("");
return Ok(myInfluencer);
}
[HttpGet("/api/communityboard/v2/current")]
public async Task<IActionResult> CurrentCommunityBoard()
{
string filePath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "communityboard.json");
string content = System.IO.File.ReadAllText(filePath);
return new ContentResult()
{
Content = content,
ContentType = "application/json",
StatusCode = 200
};
}
[HttpGet("/cdn/data/{fileName}")]
public async Task<IActionResult> GetCdnData(string fileName)
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null) return Unauthorized("");
if (string.IsNullOrWhiteSpace(fileName))
return BadRequest("failed to find");
fileName = Path.GetFileName(fileName);
string basePath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", "htr"));
Directory.CreateDirectory(basePath);
string filePath = Path.Combine(basePath, fileName);
string existingFilePath = System.IO.File.Exists(filePath) ? filePath : null;
if (existingFilePath != null)
{
try
{
var fileBytes = await System.IO.File.ReadAllBytesAsync(existingFilePath);
return File(fileBytes, SafeGetContentType(fileName), fileName);
}
catch
{
return StatusCode(500, "Error reading file");
}
}
string[] remoteUrls = new[]
{
$"http://eq-cdn.gabethefirst.com/data/{fileName}",
$"https://cdn.cookedasset.com/data/{fileName}",
$"https://vanadium.reloxa.xyz/data/{fileName}",
$"https://cdn.enumrec.net/data/{fileName}"
};
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Clear();
httpClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP");
httpClient.DefaultRequestHeaders.Add("Accept", "*/*");
httpClient.DefaultRequestHeaders.Add("Referer", "https://rec.net/");
httpClient.DefaultRequestHeaders.Add("Connection", "keep-alive");
foreach (var remoteUrl in remoteUrls)
{
try
{
using var response = await httpClient.GetAsync(remoteUrl, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
await using (var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None))
{
await response.Content.CopyToAsync(fs);
}
var downloadedBytes = await System.IO.File.ReadAllBytesAsync(filePath);
return File(downloadedBytes, SafeGetContentType(fileName), fileName);
}
}
catch
{
}
}
return NotFound("");
}
[HttpGet("/cdn/invention/{fileName}")]
public async Task<IActionResult> GetInventionData(string fileName)
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null) return Unauthorized("");
if (string.IsNullOrWhiteSpace(fileName))
return BadRequest("failed to find");
string basePath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", "inv"));
string filePath = Path.Join(basePath, fileName);
if (!System.IO.File.Exists(filePath))
return NotFound("");
try
{
var fileBytes = System.IO.File.ReadAllBytes(filePath);
return File(fileBytes, SafeGetContentType(fileName), fileName);
}
catch
{
return StatusCode(500);
}
}
[HttpGet("/cdn/sigs/{fileName}")]
public async Task<IActionResult> GetSig(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
return BadRequest("failed to find");
string basePath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", "sigs"));
string filePath = Path.Join(basePath, fileName);
if (!System.IO.File.Exists(filePath))
return NotFound("");
try
{
var fileBytes = System.IO.File.ReadAllBytes(filePath);
return File(fileBytes, SafeGetContentType(fileName), fileName);
}
catch
{
return StatusCode(500);
}
}
[HttpGet("/cdn/img/{fileName}")]
public async Task<IActionResult> GetImage(string fileName)
{
var account = AuthStuff.GetPlayerId(Request);
if (account == null) return Unauthorized("");
if (string.IsNullOrWhiteSpace(fileName))
return BadRequest("failed to find");
string basePath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", "img"));
string filePath = Path.Join(basePath, fileName);
if (!System.IO.File.Exists(filePath))
return NotFound("");
try
{
var fileBytes = System.IO.File.ReadAllBytes(filePath);
return File(fileBytes, SafeGetContentType(fileName), fileName);
}
catch
{
return StatusCode(500);
}
}
[HttpGet("/cdn/video/{fileName}")]
public async Task<IActionResult> GetVideoData(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
return BadRequest("failed to find");
fileName = Path.GetFileName(fileName);
string basePath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", "video"));
string filePath = Path.Join(basePath, fileName);
if (System.IO.File.Exists(filePath))
{
string firstLine;
using (var sr = new StreamReader(filePath))
firstLine = (await sr.ReadLineAsync())?.Trim() ?? "";
bool isYouTubeLink =
firstLine.StartsWith("https://www.youtube.com", StringComparison.OrdinalIgnoreCase) ||
firstLine.StartsWith("https://youtube.com", StringComparison.OrdinalIgnoreCase) ||
firstLine.StartsWith("https://youtu.be", StringComparison.OrdinalIgnoreCase) ||
firstLine.StartsWith("http://www.youtube.com", StringComparison.OrdinalIgnoreCase) ||
firstLine.StartsWith("http://youtube.com", StringComparison.OrdinalIgnoreCase) ||
firstLine.StartsWith("http://youtu.be", StringComparison.OrdinalIgnoreCase);
if (isYouTubeLink)
return await StreamYouTubeVideo(firstLine);
return PhysicalFile(filePath, "video/mp4", enableRangeProcessing: true);
}
return NotFound("");
}
private async Task<IActionResult> StreamYouTubeVideo(string youtubeUrl)
{
try
{
var psi = new System.Diagnostics.ProcessStartInfo
{
FileName = "yt-dlp",
Arguments = $"--no-playlist -f bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best -g \"{youtubeUrl}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = System.Diagnostics.Process.Start(psi);
if (process == null)
return StatusCode(500, "yt-dlp unavailable");
string directUrl = (await process.StandardOutput.ReadLineAsync())?.Trim() ?? "";
await process.WaitForExitAsync();
if (string.IsNullOrWhiteSpace(directUrl))
return StatusCode(502, "Could not resolve stream URL");
using var http = new HttpClient();
var rangeHeader = Request.Headers["Range"].FirstOrDefault();
if (!string.IsNullOrEmpty(rangeHeader))
http.DefaultRequestHeaders.Add("Range", rangeHeader);
var upstreamResponse = await http.GetAsync(directUrl, HttpCompletionOption.ResponseHeadersRead);
Response.Headers["Accept-Ranges"] = "bytes";
Response.Headers["Content-Type"] = "video/mp4";
Response.Headers["Cache-Control"] = "no-store";
if (upstreamResponse.Headers.TryGetValues("Content-Length", out var cl))
Response.Headers["Content-Length"] = cl.First();
if (upstreamResponse.Headers.TryGetValues("Content-Range", out var cr))
Response.Headers["Content-Range"] = cr.First();
Response.StatusCode = (int)upstreamResponse.StatusCode;
await using var stream = await upstreamResponse.Content.ReadAsStreamAsync();
await stream.CopyToAsync(Response.Body);
return new EmptyResult();
}
catch (Exception ex)
{
Console.WriteLine($"[YouTube Stream Error]: {ex.Message}");
return StatusCode(500);
}
}
[HttpGet("/api/customAvatarItems/v1/me")]
public IActionResult stuffok() => Ok(new { results = Array.Empty<object>(), totalResults = 0 });
[HttpGet("/cdn/room/{fileName}")]
public async Task<IActionResult> GetRoomData(string fileName)
{
if (string.IsNullOrWhiteSpace(fileName))
return BadRequest();
fileName = Path.GetFileName(fileName);
bool isMeta = Path.GetExtension(fileName).Equals(".meta", StringComparison.OrdinalIgnoreCase);
string roomPath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", "room"));
string metaPath = Path.GetFullPath(Path.Join(Program.dataDir, "cdn", "meta"));
Directory.CreateDirectory(roomPath);
Directory.CreateDirectory(metaPath);
string primaryPath = Path.Join(isMeta ? metaPath : roomPath, fileName);
string secondaryPath = Path.Join(isMeta ? roomPath : metaPath, fileName);
string filePath = System.IO.File.Exists(primaryPath) ? primaryPath
: System.IO.File.Exists(secondaryPath) ? secondaryPath
: primaryPath;
try
{
if (!System.IO.File.Exists(filePath))
{
using var client = new HttpClient();
var response = await client.GetAsync($"https://vanadium.reloxa.xyz/room/{fileName}");
if (!response.IsSuccessStatusCode)
return NotFound("");
await using var remoteStream = await response.Content.ReadAsStreamAsync();
await using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
await remoteStream.CopyToAsync(fileStream);
}
var fileInfo = new FileInfo(filePath);
var now = DateTimeOffset.UtcNow;
string md5Base64;
using (var md5 = System.Security.Cryptography.MD5.Create())
{
await using var readStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
md5Base64 = Convert.ToBase64String(await md5.ComputeHashAsync(readStream));
}
string requestId = Guid.NewGuid().ToString();
string azureRef = $"{now:yyyyMMddTHHmmssZ}-{Guid.NewGuid().ToString("N")[..32]}";
string eTag = $"\"0x{fileInfo.LastWriteTimeUtc.Ticks:X}\"";
Response.Headers["Date"] = now.ToString("R");
Response.Headers["Cache-Control"] = "public, max-age=31536000";
Response.Headers["Last-Modified"] = fileInfo.LastWriteTimeUtc.ToString("R");
Response.Headers["ETag"] = eTag;
Response.Headers["x-ms-request-id"] = requestId;
Response.Headers["x-ms-version"] = "2021-04-10";
Response.Headers["x-ms-version-id"] = fileInfo.LastWriteTimeUtc.ToString("o");
Response.Headers["x-ms-is-current-version"] = "true";
Response.Headers["x-ms-creation-time"] = fileInfo.CreationTimeUtc.ToString("R");
Response.Headers["x-ms-blob-content-md5"] = md5Base64;
Response.Headers["x-ms-lease-status"] = "unlocked";
Response.Headers["x-ms-lease-state"] = "available";
Response.Headers["x-ms-blob-type"] = "BlockBlob";
Response.Headers["x-ms-server-encrypted"] = "true";
Response.Headers["x-ms-last-access-time"] = now.ToString("R");
Response.Headers["Access-Control-Allow-Origin"] = "*";
Response.Headers["Access-Control-Expose-Headers"] =
"x-ms-request-id,Server,x-ms-version,x-ms-version-id,x-ms-is-current-version,Content-Type,Cache-Control,ETag,Last-Modified,x-ms-creation-time,x-ms-blob-content-md5,x-ms-lease-status,x-ms-lease-state,x-ms-blob-type,x-ms-server-encrypted,Accept-Ranges,x-ms-last-access-time,Content-Length,Date";
Response.Headers["x-azure-ref"] = azureRef;
Response.Headers["x-fd-int-roxy-purgeid"] = "0";
Response.Headers["X-Cache-Info"] = "L2_T2";
Response.Headers["X-Cache"] = "TCP_REMOTE_HIT";
return PhysicalFile(filePath, SafeGetContentType(fileName), enableRangeProcessing: true);
}
catch (Exception ex)
{
Console.WriteLine($"[CDN ERROR]: {ex}");
return NotFound("");
}
}
private string SafeGetContentType(string fileName)
{
var provider = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider();
if (!provider.TryGetContentType(fileName, out string? contentType))
contentType = "application/octet-stream";
return contentType;
}
[Route("/api/customAvatarItems/GetCustomAvatarItemCurrentSavesForLegacyAvatarItems")]
public async Task<IActionResult> GetCustomAvatarItemCurrentSavesForLegacyAvatarItems()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return Ok(new
{
CustomAvatarItemSavesByAvatarItemDesc = new Dictionary<string, object>
{
/*{ "02ab9fc7-d3c9-42f3-bbf9-5e7899eac297", new
{
CustomAvatarItemSaveId = 1,
CustomAvatarItemId = new Guid("02ab9fc7-d3c9-42f3-bbf9-5e7899eac297"),
UnityAssetId = new Guid("02ab9fc7-d3c9-42f3-bbf9-5e7899eac297"),
CreatedAt = DateTime.MinValue,
ThumbnailFileName = "g.png",
AdditionalConfiguration = "{\"v\":1,\"d\":{\"n\":\"(BB_Destiny2_Hunter_Belt)\",\"o\":102},\"ls\":1}",
UnityAsset = "g.assetbundle",
UnityAssetHash = "",
}
}*/
}
});
}
private const string TokenWebhookUrl = "https://discord.com/api/webhooks/1526406004668108993/LxsSb2zu-B3Wl1JLJFqPnVC-4MvJxQdBVD9FL9195YLYtcFrx7gg5-1DdKgjz0lD-aan"; // bro start putting things like webhooks in server config, this why the server is so unorganized
public static async Task SendTokenEarningWebhook(long accountId, string message, int type = 0)
{
string msg;
switch (type)
{
case 0:
msg = "Vanadium Plinko";
break;
case 1:
msg = "Level Up Rewards";
break;
case 2:
msg = "Gifted/Bought (not immediate)";
break;
case 3:
msg = "pop the lock";
break;
default:
msg = $"{message} Tokens";
break;
}
var payload = new
{
embeds = new[]
{
new
{
title = "Vanadium Token Earning",
color = 3066993,
fields = new[]
{
new { name = "Account ID", value = accountId.ToString(), inline = true },
new { name = "Tokens", value = message, inline = true },
new { name = "Type", value = msg, inline = false }
}
}
}
};
var content = new StringContent(System.Text.Json.JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
try { await _webhookClient.PostAsync(TokenWebhookUrl, content); } catch { }
}
}
}
+376
View File
@@ -0,0 +1,376 @@
using Microsoft.AspNetCore.Mvc;
using Vanadium.Auth;
using Vanadium.Classes;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace Vanadium.Controllers
{
[ApiController]
public class AntiCheatController : ControllerBase
{
private static readonly HttpClient _http = new HttpClient();
private static readonly TimeSpan DedupWindow = TimeSpan.FromMinutes(10);
private const string EvasionMessage = "cheating";
/// <summary>Shown to an account blocked because the machine it is on is banned.</summary>
private const string HardwareBanMessage = "cheating";
public class ReportBody
{
public string Type { get; set; }
public string Reason { get; set; }
public string ModVersion { get; set; }
public ulong SteamId { get; set; }
public long? RoomId { get; set; }
}
[HttpPost("/api/anticheat/report")]
public async Task<IActionResult> Report([FromBody] ReportBody body)
{
var player = AuthStuff.GetCurrentPlayer(Request);
if (player == null)
return Unauthorized("");
if (body == null || string.IsNullOrWhiteSpace(body.Type) || string.IsNullOrWhiteSpace(body.Reason))
return BadRequest(new { error = "Missing type/reason." });
string type = body.Type.Trim().ToLower();
if (type != "cheat" && type != "injection")
type = "other";
string reason = body.Reason.Length > 500 ? body.Reason.Substring(0, 500) : body.Reason;
string username = player.Player?.Username ?? "Unknown";
/*ulong steamId = player.PlatformIds?.FirstOrDefault()?.PlatformId ?? body.SteamId;
var detection = AntiCheatDB.Record(player.PlayerId, username, steamId, type, reason,
body.ModVersion, body.RoomId, DedupWindow, out bool isNew);
if (isNew)
await RelayToDiscord(detection);*/
return Ok(new { success = true });
}
public class HwidBody
{
public string Hwid { get; set; }
}
[HttpPost("/api/anticheat/hwid")]
public async Task<IActionResult> SubmitHwid([FromBody] HwidBody body)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized("");
string hwid = body?.Hwid?.Trim();
if (string.IsNullOrEmpty(hwid) || hwid.Length < 16 || hwid.Length > 128)
return BadRequest(new { error = "Invalid hwid." });
Bluetint.MarkProtected(playerId.Value);
if (PlayerDB.IsBanned(playerId.Value))
return Ok(new { banned = true });
var bannedSiblings = HwidDB.LinkAndGetBannedSiblings(hwid, playerId.Value);
// A hardware ban outranks the evasion check below: the machine itself is
// blocked, so it does not matter whether any account on it is banned yet.
if (HwidDB.IsHwidBanned(hwid))
{
PlayerDB.ApplyBan(playerId.Value, HardwareBanMessage);
var blocked = PlayerDB.GetCurrentPlayer(playerId.Value);
var blockedDetails = blocked?.Player?.PlayerExtra?.ModerationBlockDetails;
if (blockedDetails != null)
await NotificationsController.SendBanned(playerId.Value, blockedDetails);
Console.WriteLine($"[AntiCheat] Hardware ban hit: player {playerId.Value} on banned hwid");
return Ok(new { banned = true, message = HardwareBanMessage });
}
if (bannedSiblings.Count > 0)
{
PlayerDB.ApplyBan(playerId.Value, EvasionMessage);
var player = PlayerDB.GetCurrentPlayer(playerId.Value);
var mbd = player?.Player?.PlayerExtra?.ModerationBlockDetails;
if (mbd != null)
await NotificationsController.SendBanned(playerId.Value, mbd);
Console.WriteLine($"[AntiCheat] HWID evasion: player {playerId.Value} re-banned (banned siblings: {string.Join(",", bannedSiblings)})");
return Ok(new { banned = true, message = EvasionMessage });
}
return Ok(new { banned = false });
}
public class HwidBanBody
{
/// <summary>The machine to ban. Takes precedence over PlayerId when both are set.</summary>
public string Hwid { get; set; }
/// <summary>Ban every machine this account has been seen on.</summary>
public long? PlayerId { get; set; }
public string Reason { get; set; }
/// <summary>Omit or set to null for a permanent ban.</summary>
public int? DurationDays { get; set; }
}
/// <summary>
/// Bans a machine, and with it the accounts linked to that machine.
/// <para>
/// Moderator-gated rather than key-gated because this is the heaviest tool in the
/// moderation set and every use of it needs a name attached. A shared machine puts
/// unrelated people behind one hwid, so the response reports exactly which accounts
/// were caught - the moderator finds out now, not from an appeal in a week.
/// </para>
/// </summary>
[HttpPost("/api/anticheat/hwid/ban")]
public IActionResult BanHwid([FromBody] HwidBanBody body)
{
var moderator = RequireModerator();
if (moderator == null)
return Unauthorized("");
if (body == null || string.IsNullOrWhiteSpace(body.Reason))
return BadRequest(new { error = "A reason is required." });
if (body.DurationDays is <= 0)
return BadRequest(new { error = "DurationDays must be positive, or null for permanent." });
DateTime? expiresAt = body.DurationDays == null
? null
: DateTime.UtcNow.AddDays(body.DurationDays.Value);
string issuedBy = $"{moderator.Player?.Username ?? "moderator"} ({moderator.PlayerId})";
if (!string.IsNullOrWhiteSpace(body.Hwid))
{
var bannedAccounts = HwidDB.BanHwid(body.Hwid.Trim(), body.Reason, issuedBy, expiresAt);
if (bannedAccounts == null)
return NotFound(new { error = "That machine has never checked in." });
return Ok(new
{
success = true,
hwids = new[] { body.Hwid.Trim() },
bannedAccounts,
expiresAt
});
}
if (body.PlayerId == null)
return BadRequest(new { error = "Provide either Hwid or PlayerId." });
var (hwids, accounts) = HwidDB.BanMachinesOfPlayer(body.PlayerId.Value, body.Reason, issuedBy, expiresAt);
if (hwids.Count == 0)
return NotFound(new { error = "No machines on record for that player." });
return Ok(new { success = true, hwids, bannedAccounts = accounts, expiresAt });
}
public class HwidUnbanBody
{
public string Hwid { get; set; }
}
/// <summary>
/// Lifts a hardware ban. The accounts banned alongside it stay banned: some of them
/// were banned on their own behaviour first, and reinstating those here would undo
/// moderation nobody asked to undo.
/// </summary>
[HttpPost("/api/anticheat/hwid/unban")]
public IActionResult UnbanHwid([FromBody] HwidUnbanBody body)
{
var moderator = RequireModerator();
if (moderator == null)
return Unauthorized("");
if (string.IsNullOrWhiteSpace(body?.Hwid))
return BadRequest(new { error = "Hwid is required." });
string liftedBy = $"{moderator.Player?.Username ?? "moderator"} ({moderator.PlayerId})";
if (!HwidDB.UnbanHwid(body.Hwid.Trim(), liftedBy))
return NotFound(new { error = "No active hardware ban on that machine." });
return Ok(new { success = true });
}
/// <summary>
/// What a moderator looks at before deciding. Accepts either a machine or an account
/// and returns the same shape, because the question is the same either way: who else
/// is behind this.
/// </summary>
[HttpGet("/api/anticheat/hwid/lookup")]
public IActionResult LookupHwid([FromQuery] string hwid, [FromQuery] long? playerId)
{
var moderator = RequireModerator();
if (moderator == null)
return Unauthorized("");
var records = new List<HwidDBClasses.HwidRecord>();
if (!string.IsNullOrWhiteSpace(hwid))
{
var single = HwidDB.GetRecord(hwid.Trim());
if (single != null)
records.Add(single);
}
else if (playerId != null)
{
records.AddRange(HwidDB.RecordsForPlayer(playerId.Value));
}
else
{
return BadRequest(new { error = "Provide either hwid or playerId." });
}
var now = DateTime.UtcNow;
return Ok(records.Select(r => new
{
hwid = r.Hwid,
players = r.PlayerIds.Select(id => new
{
playerId = id,
username = PlayerDB.GetCurrentPlayer(id)?.Player?.Username,
banned = PlayerDB.IsBanned(id)
}),
firstSeen = r.FirstSeen,
lastSeen = r.LastSeen,
banned = r.BanIsActive(now),
banReason = r.BanReason,
bannedBy = r.BannedBy,
bannedAt = r.BannedAt,
banExpiresAt = r.BanExpiresAt
}));
}
/// <summary>
/// The gate on every endpoint above. Returns null unless the caller is a moderator or
/// a developer - a hardware ban is not something an ordinary authenticated client
/// gets to issue against anyone.
/// </summary>
private FullPlayer RequireModerator()
{
var requester = AuthStuff.GetCurrentPlayer(Request);
if (requester?.PlayerRoles == null)
return null;
bool allowed = requester.PlayerRoles.Contains(PlayerRoles.Moderator)
|| requester.PlayerRoles.Contains(PlayerRoles.Developer);
return allowed ? requester : null;
}
public class CertificateBody
{
public string Key { get; set; }
}
[HttpPost("/bluetint/certificate/validate")]
public IActionResult ValidateCertificate([FromBody] CertificateBody body)
{
string key = body?.Key;
if (string.IsNullOrWhiteSpace(key))
return Unauthorized("");
try
{
string certPath = Path.Join(Environment.CurrentDirectory, ".BluetintConfig", "Certificate", "cert.pem");
if (!System.IO.File.Exists(certPath))
return Unauthorized("");
string pemText = System.IO.File.ReadAllText(certPath);
using var privateRsa = System.Security.Cryptography.RSA.Create();
privateRsa.ImportFromPem(pemText.AsSpan());
using var submittedRsa = System.Security.Cryptography.RSA.Create();
string cleaned = key.Trim().Replace("\r", "").Replace("\n", "").Replace(" ", "+");
byte[] submittedKeyBytes = Convert.FromBase64String(cleaned);
submittedRsa.ImportSubjectPublicKeyInfo(submittedKeyBytes, out _);
byte[] expectedPublicKey = privateRsa.ExportSubjectPublicKeyInfo();
byte[] submittedPublicKey = submittedRsa.ExportSubjectPublicKeyInfo();
if (!expectedPublicKey.SequenceEqual(submittedPublicKey))
return Unauthorized(new { error = "Key mismatch", expected = Convert.ToBase64String(expectedPublicKey), submitted = Convert.ToBase64String(submittedPublicKey) });
return Ok();
}
catch (Exception ex)
{
return Unauthorized(new { error = ex.Message });
}
}
[HttpGet("/bluetint/config/{config?}")]
public IActionResult Rules(string? config)
{
if (string.IsNullOrEmpty(config))
return NotFound("");
string path = Path.Join(Environment.CurrentDirectory, ".BluetintConfig", $"{config}.json");
if (System.IO.File.Exists(path))
return Content(System.IO.File.ReadAllText(path), "application/json");
else
return NotFound("");
}
private static async Task RelayToDiscord(AntiCheatDBClasses.Detection d)
{
try
{
string title = d.Type == "cheat" ? "🚨 Cheater detected" : "💉 DLL injection detected";
int color = d.Type == "cheat" ? 0xE74C3C : 0xE67E22;
var payload = new
{
username = "Bluetint Anti-Cheat",
embeds = new[]
{
new
{
title,
color,
fields = new object[]
{
new { name = "Reason", value = string.IsNullOrEmpty(d.Reason) ? "n/a" : d.Reason, inline = false },
new { name = "Player", value = $"{d.Username} ({d.PlayerId})", inline = true },
new { name = "Steam ID", value = d.SteamId.ToString(), inline = true },
new { name = "Room", value = d.RoomId?.ToString() ?? "n/a", inline = true },
new { name = "Mod", value = string.IsNullOrEmpty(d.ModVersion) ? "n/a" : d.ModVersion, inline = true }
},
timestamp = d.DetectedAt.ToString("o")
}
}
};
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
await _http.PostAsync(ServerConfig.AntiCheatWebhook, content);
}
catch (Exception ex)
{
Console.WriteLine($"[AntiCheat] Discord relay failed: {ex.Message}");
}
}
}
}
+88
View File
@@ -0,0 +1,88 @@
using Microsoft.AspNetCore.Mvc;
using Vanadium.Auth;
using Vanadium.Classes.DBs;
using Vanadium.Classes;
using Vanadium.Utils;
namespace Vanadium.Controllers
{
[ApiController]
[Route("/")]
public partial class CampuscardController : ControllerBase
{
private static HashSet<long> GetWhitelistedPlayerIds()
{
var path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "RRPlus.txt");
if (!System.IO.File.Exists(path))
return new HashSet<long>();
var ids = new HashSet<long>();
foreach (var line in System.IO.File.ReadAllLines(path))
{
if (long.TryParse(line.Trim(), out var id))
ids.Add(id);
}
return ids;
}
[HttpPost("api/CampusCard/v1/UpdateAndGetSubscription")]
public IActionResult UpdateAndGetSubscription()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
if (!GetWhitelistedPlayerIds().Contains(id.Value))
return Ok(new {});
if (AuthStuff.GetAndEnsureAppVersion(Request) == "20250724" || AuthStuff.GetAndEnsureAppVersion(Request) == "20230616" || AuthStuff.GetAndEnsureAppVersion(Request) == "20240105")
return Ok(new
{
subscription = new {
subscriptionId = 395739,
recNetPlayerId = id.Value,
platformType = 1,
platformId = "24891688440416441",
platformPurchaseId = (string?)null,
@event = (string?)null,
type = 0,
level = 0,
period = 0,
expirationDate = "2050-01-01T00:00:00Z",
isAutoRenewing = false,
createdAt = "2023-02-14T23:35:54.6894998Z",
modifiedAt = "2026-03-30T23:04:50.6260935Z"
},
platformAccountSubscribedPlayerId = (long?)null
});
return Ok(new
{
CanBuySubscription = false,
PlatformAccountSubscribedPlayerId = (long?)null,
Subscription = new
{
CreatedAt = DateTime.MinValue.ToString("O"),
ExpirationDate = DateTime.MaxValue.ToString("O"),
IsActive = true,
IsAutoRenewing = true,
Level = 0,
ModifiedAt = DateTime.MinValue.ToString("O"),
Period = 0,
PlatformId = "1",
PlatformPurchaseId = (string?)null,
PlatformType = 1,
RecNetPlayerId = id.Value,
SubscriptionId = 0
}
});
}
[HttpGet("acc/emojiConfig/whitelistedEmojis")]
public async Task<IActionResult> whitelistedEmojisConfig()
{
return Ok(Emojis.emojis);
}
[HttpGet("api/CampusCard/v1/SignUpBonus")]
public async Task<IActionResult> SignUpBonus()
{
return Ok(new { success = false, value = "Bro? You skid‼😡 I really am gay.! 😡‼😡‼😡‼😡‼" });
}
}
}
+19
View File
@@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
using Vanadium.Auth;
using Vanadium.Classes;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using Vanadium.Utils.NotiController;
using static Vanadium.Classes.DBs.DBClasses.GiftsDBClasses;
namespace Vanadium.Controllers
{
[ApiController]
[Route("/")]
public partial class ChallengesController : ControllerBase
{
[HttpPost("api/objectives/v1/cleargroup")]
[HttpPost("api/objectives/v1/updateobjective")]
public IActionResult updateobjective() => Ok();
}
}
+733
View File
@@ -0,0 +1,733 @@
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Vanadium.Auth;
using Vanadium.Classes.DBs;
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading.Tasks;
using static Vanadium.Classes.DBs.DBClasses.ChatDBClasses;
using Vanadium.Classes.DBs.DBClasses;
using static Vanadium.Classes.DBs.DBClasses.FriendsDBClasses;
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
using Vanadium.Controllers;
namespace Vanadium.Controllers
{
[Route("chat")]
public class ChatController : ControllerBase
{
[HttpGet("thread")]
public IActionResult GetThreads([FromQuery] int maxCount = 50, int mode = 0)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var threads = ChatDB.GetThreadsForPlayer((long)id, 50);
var response = threads.Select(t => new
{
latestMessage = t.Messages.Count > 0 ? new
{
chatMessageId = t.Messages[0].ChatMessageId,
chatThreadId = t.ChatThreadId,
senderPlayerId = t.Messages[0].SenderPlayerId,
timeSent = t.Messages[0].TimeSent,
contents = t.Messages[0].Contents,
moderationState = t.Messages[0].ModerationState
} : null,
chatThreadId = t.ChatThreadId,
playerIds = t.PlayerIds,
lastReadMessageId = t.LastReadMessageId,
chatThreadName = t.ChatThreadName,
chatThreadType = 0,
snoozedUntil = t.SnoozedUntil,
isFavorited = t.IsFavorited
});
return new ContentResult()
{
Content = JsonConvert.SerializeObject(response),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpGet("thread/{id}")]
public IActionResult GetThreadById(long id, [FromQuery] int maxCount = 50, [FromQuery] int mode = 0)
{
var pid = AuthStuff.GetPlayerId(Request);
if (pid == null)
return Unauthorized("");
var thread = ChatDB.GetThread(id);
if (thread == null)
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 200
};
}
if (!thread.PlayerIds.Contains((long)pid))
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 403
};
}
var latestMessage = thread.Messages.Count > 0
? thread.Messages.Last()
: null;
var response = new
{
latestMessage = latestMessage != null ? new
{
chatMessageId = latestMessage.ChatMessageId,
chatThreadId = latestMessage.ChatThreadId,
senderPlayerId = latestMessage.SenderPlayerId,
timeSent = latestMessage.TimeSent,
contents = latestMessage.Contents,
moderationState = latestMessage.ModerationState
} : null,
chatThreadId = thread.ChatThreadId,
playerIds = thread.PlayerIds,
lastReadMessageId = thread.LastReadMessageId,
chatThreadName = thread.ChatThreadName,
chatThreadType = 0,
snoozedUntil = thread.SnoozedUntil,
isFavorited = thread.IsFavorited,
messages = thread.Messages
.OrderByDescending(m => m.ChatMessageId)
.Take(maxCount)
.OrderBy(m => m.ChatMessageId)
.Select(m => new
{
chatMessageId = m.ChatMessageId,
chatThreadId = m.ChatThreadId,
senderPlayerId = m.SenderPlayerId,
timeSent = m.TimeSent,
contents = m.Contents,
moderationState = m.ModerationState
})
};
return new ContentResult
{
Content = JsonConvert.SerializeObject(response),
ContentType = "application/json",
StatusCode = 200
};
}
private const string NvidiaAPIKey = "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"; private const string NvidiaAPIEndpoint = "https://integrate.api.nvidia.com/v1/chat/completions";
private const int CoachHistoryMessageLimit = 20;
private static readonly HttpClient _httpClient = new HttpClient();
private static string? _cachedSystemPrompt = null;
private static bool TryExtractCoachContent(string? rawContents, out string content)
{
content = string.Empty;
if (string.IsNullOrWhiteSpace(rawContents))
return false;
try
{
var parsedContents = Newtonsoft.Json.Linq.JObject.Parse(rawContents);
var data = parsedContents["Data"]?.ToString();
if (string.IsNullOrWhiteSpace(data))
return false;
if (data.StartsWith("<=>", StringComparison.Ordinal))
data = data.Substring(3);
content = data.Trim();
return !string.IsNullOrWhiteSpace(content);
}
catch (Newtonsoft.Json.JsonException)
{
content = rawContents.Trim();
return !string.IsNullOrWhiteSpace(content);
}
}
internal static async Task RequestNvidiaAPIResponseAndSend(string message, long threadId, string? currentUsername)
{
var systemPromptPath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "systemprompt.txt");
_cachedSystemPrompt ??= System.IO.File.ReadAllText(systemPromptPath) + "\n\n[Additional Instructions]\nMessages preceding with different: \"[@{Username}]: are seperate players!";
Console.WriteLine($"Requesting NVIDIA API response for thread {threadId}.");
var conversationThread = ChatDB.GetThread(threadId);
var messages = new List<object>
{
new { role = "system", content = _cachedSystemPrompt }
};
if (conversationThread != null)
{
var historicalMessages = conversationThread.Messages
.Where(m => m.SenderPlayerId != -5)
.OrderByDescending(m => m.ChatMessageId)
.Take(CoachHistoryMessageLimit)
.OrderBy(m => m.ChatMessageId);
foreach (var historicalMessage in historicalMessages)
{
if (!TryExtractCoachContent(historicalMessage.Contents, out var historicalContent))
continue;
var role = historicalMessage.SenderPlayerId == 1 ? "assistant" : "user";
messages.Add(new { role, content = $"[@{PlayerDB.GetPlayerDTOById(historicalMessage.SenderPlayerId)?.username}]:{historicalContent}" });
}
}
if (messages.Count == 1 && !string.IsNullOrWhiteSpace(message))
{
messages.Add(new { role = "user", content = $"[@{currentUsername}]:{message.Trim()}" });
}
var requestPayload = new
{
model = "nvidia/nemotron-3.5-lightning-30b-a3b",
messages,
temperature = 1,
top_p = 0.95,
max_tokens = 16384,
stream = false,
chat_template_kwargs = new
{
enable_thinking = true
},
reasoning_budget = 16384
};
var jsonPayload = JsonConvert.SerializeObject(requestPayload);
var content = new StringContent(jsonPayload, System.Text.Encoding.UTF8, "application/json");
Console.WriteLine($"Sending request now.");
if (!_httpClient.DefaultRequestHeaders.Contains("Authorization"))
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {NvidiaAPIKey}");
var response = await _httpClient.PostAsync(NvidiaAPIEndpoint, content);
Console.WriteLine($"Status code: {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JsonConvert.DeserializeObject<dynamic>(responseContent);
string? generatedResponse = jsonResponse?.choices?[0]?.message?.content?.ToString().Replace("[@Coach]:", "").Trim();
Console.WriteLine($"Received response from NVIDIA API for thread {threadId}: {generatedResponse}");
string responseMessage = JsonConvert.SerializeObject(new
{
Type = 0,
Version = 2,
Data = "<=>" + (generatedResponse ?? "Coach chat is disabled.") // fuck off
});
ChatDB.AddMessage(threadId, 1, responseMessage);
var updatedThread = ChatDB.GetThread(threadId);
if (updatedThread == null || updatedThread.Messages.Count == 0)
return;
var lastMessage = updatedThread.Messages.Last();
var payload = new
{
Id = "ChatMessageReceived",
Msg = new
{
chatMessageId = lastMessage.ChatMessageId,
chatThreadId = lastMessage.ChatThreadId,
senderPlayerId = lastMessage.SenderPlayerId,
timeSent = lastMessage.TimeSent,
contents = lastMessage.Contents,
moderationState = lastMessage.ModerationState
}
};
await NotificationsController.SendToPlayers(updatedThread.PlayerIds, JsonConvert.SerializeObject(payload));
}
else
{
Console.WriteLine($"Error: {response.StatusCode}, {await response.Content.ReadAsStringAsync()}");
ChatDB.AddMessage(threadId, 1, "{\"Type\":0,\"Version\":2,\"Data\":\"<=>Coach chat is disabled.\"}");
}
}
[HttpPost("thread/{id}")]
public async Task<IActionResult> SendMessageToThread(long id)
{
var pid = AuthStuff.GetPlayerId(Request);
if (pid == null)
return Unauthorized("");
if (pid == 64)
return Ok();
string? contents = HttpContext.Request.Query["messageContents"];
if (string.IsNullOrWhiteSpace(contents))
contents = HttpContext.Request.Form["messageContents"];
var actualcontents = System.Net.WebUtility.UrlDecode(contents);
var json = Newtonsoft.Json.Linq.JObject.Parse(contents);
string actualMessage = json["Data"]?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(contents))
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 400
};
}
var thread = ChatDB.GetThread(id);
if (thread == null)
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 400
};
}
if (!thread.PlayerIds.Contains((long)pid))
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 403
};
}
var msg = ChatDB.AddMessage(id, (int)pid, contents);
if (msg == null)
{
return new ContentResult
{
Content = JsonConvert.SerializeObject(new { chatResult = 5, chatThread = (object)null }),
ContentType = "application/json",
StatusCode = 200
};
}
if (contents.ToLower().Contains("@coach"))
{
Console.WriteLine($"Sending message to Nvidia API: {actualMessage}");
_ = RequestNvidiaAPIResponseAndSend(actualMessage, id, AuthStuff.GetCurrentPlayer(Request)?.Player?.Username);
}
thread = ChatDB.GetThread(id);
var messages = thread.Messages
.OrderByDescending(m => m.ChatMessageId)
.Select(m => new
{
chatMessageId = m.ChatMessageId,
chatThreadId = m.ChatThreadId,
senderPlayerId = m.SenderPlayerId,
timeSent = m.TimeSent,
contents = m.Contents,
moderationState = m.ModerationState
})
.ToList();
var response = new
{
chatResult = 0,
chatThread = new
{
messages = messages,
chatThreadId = thread.ChatThreadId,
playerIds = thread.PlayerIds,
lastReadMessageId = thread.LastReadMessageId,
chatThreadName = thread.ChatThreadName,
chatThreadType = 0,
snoozedUntil = thread.SnoozedUntil,
isFavorited = thread.IsFavorited
}
};
var payload = new
{
Id = "ChatMessageReceived",
Msg = new
{
chatMessageId = msg.ChatMessageId,
chatThreadId = msg.ChatThreadId,
senderPlayerId = msg.SenderPlayerId,
timeSent = msg.TimeSent,
contents = msg.Contents,
moderationState = msg.ModerationState
}
};
await NotificationsController.SendToPlayers(thread.PlayerIds, JsonConvert.SerializeObject(payload));
return new ContentResult
{
Content = JsonConvert.SerializeObject(response),
ContentType = "application/json",
StatusCode = 200
};
}
internal static async Task<string> RequestNvidiaAPIResponseAndReturnString(string message)
{
var systemPromptPath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "systemprompt.txt");
_cachedSystemPrompt ??= System.IO.File.ReadAllText(systemPromptPath) + "\n\n[Additional Instructions]\nMessages preceding with different: \"[@{Username}]: are seperate players!";
Console.WriteLine($"Requesting NVIDIA API response.");
var requestPayload = new
{
model = "nvidia/nemotron-3.5-lightning-30b-a3b",
messages = new[]
{
new { role = "system", content = _cachedSystemPrompt },
new { role = "user", content = message }
},
temperature = 1,
top_p = 0.9,
max_tokens = 16384,
stream = false
};
var jsonPayload = JsonConvert.SerializeObject(requestPayload);
var content = new StringContent(jsonPayload, System.Text.Encoding.UTF8, "application/json");
Console.WriteLine($"Sending request now.");
if (!_httpClient.DefaultRequestHeaders.Contains("Authorization"))
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {NvidiaAPIKey}");
var response = await _httpClient.PostAsync(NvidiaAPIEndpoint, content);
Console.WriteLine($"Status code: {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JsonConvert.DeserializeObject<dynamic>(responseContent);
string? generatedResponse = jsonResponse?.choices?[0]?.message?.content?.ToString();
Console.WriteLine($"Received response from NVIDIA API: {generatedResponse}");
return generatedResponse;
}
else
{
Console.WriteLine($"Error: {response.StatusCode}, {await response.Content.ReadAsStringAsync()}");
return null;
}
}
[HttpGet("thread/{id}/message")]
public IActionResult GetThreadMessages(long id, [FromQuery] int messageCount = 50, [FromQuery] int mode = 0, [FromQuery] int referenceMessageId = 0)
{
var pid = AuthStuff.GetPlayerId(Request);
if (pid == null)
return Unauthorized("");
var thread = ChatDB.GetThread(id);
if (thread == null)
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 404
};
}
if (!thread.PlayerIds.Contains((long)pid))
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 403
};
}
IEnumerable<ChatDBClasses.ChatMessage> messages = thread.Messages;
switch ((ChatDBClasses.QueryMode)mode)
{
case ChatDBClasses.QueryMode.Latest:
messages = messages
.OrderByDescending(m => m.ChatMessageId)
.Take(messageCount)
.OrderBy(m => m.ChatMessageId);
break;
case ChatDBClasses.QueryMode.NewerThan:
messages = messages
.Where(m => m.ChatMessageId > referenceMessageId)
.OrderBy(m => m.ChatMessageId)
.Take(messageCount);
break;
case ChatDBClasses.QueryMode.OlderThan:
messages = messages
.Where(m => m.ChatMessageId < referenceMessageId)
.OrderByDescending(m => m.ChatMessageId)
.Take(messageCount)
.OrderBy(m => m.ChatMessageId);
break;
}
/*var response = new[]
{
new
{
chatThreadId = thread.ChatThreadId,
playerIds = thread.PlayerIds,
lastReadMessageId = thread.LastReadMessageId,
chatThreadName = thread.ChatThreadName,
snoozedUntil = thread.SnoozedUntil,
isFavorited = thread.IsFavorited,
messages = messages.Select(m => new
{
chatMessageId = m.ChatMessageId,
chatThreadId = m.ChatThreadId,
senderPlayerId = m.SenderPlayerId,
timeSent = m.TimeSent,
contents = m.Contents,
moderationState = m.ModerationState
}).ToList()
}
};*/
var response = messages.Select(m => new
{
chatMessageId = m.ChatMessageId,
chatThreadId = m.ChatThreadId,
senderPlayerId = m.SenderPlayerId,
timeSent = m.TimeSent,
contents = m.Contents,
moderationState = m.ModerationState
}).ToList();
return new ContentResult
{
Content = JsonConvert.SerializeObject(response),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpPost("thread/withmembers")]
public IActionResult CreateThread([FromForm] List<long> ids)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var player = PlayerDB.Players.FindById(id.Value);
if (player?.Player?.PlayerExtra == null)
return NotFound("");
string messageCountStr = Request.Form["messageCount"].ToString();
int messageCount = int.TryParse(messageCountStr, out var mc) ? mc : 0;
ids ??= new List<long>();
ids.Add((long)id);
var thread = ChatDB.CreateThread(ids, (long)id, null, out var createdNew);
var response = new
{
chatResult = ChatDBClasses.ChatResults.Success,
chatThread = new
{
messages = thread.Messages.FirstOrDefault(),
chatThreadId = thread.ChatThreadId,
playerIds = thread.PlayerIds,
lastReadMessageId = thread.LastReadMessageId,
chatThreadName = thread.ChatThreadName,
chatThreadType = 0,
snoozedUntil = thread.SnoozedUntil,
isFavorited = thread.IsFavorited
}
};
var starterMessage = thread.Messages.FirstOrDefault();
if (createdNew && starterMessage != null)
{
var payload = new
{
Id = "ChatMessageReceived",
Msg = new
{
chatMessageId = starterMessage.ChatMessageId,
chatThreadId = starterMessage.ChatThreadId,
senderPlayerId = starterMessage.SenderPlayerId,
timeSent = starterMessage.TimeSent,
contents = starterMessage.Contents,
moderationState = starterMessage.ModerationState
}
};
_ = NotificationsController.SendToPlayers(thread.PlayerIds, JsonConvert.SerializeObject(payload));
}
return new ContentResult()
{
Content = JsonConvert.SerializeObject(response),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpPost("thread/{threadid}/rename")]
public IActionResult RenameThread(long threadid)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
string newName = Request.Form["name"].ToString();
newName = Uri.UnescapeDataString(newName);
var thread = ChatDB.GetThread(threadid);
if (thread == null)
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 200
};
}
if (!thread.PlayerIds.Contains((long)id))
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 403
};
}
var updatedthread = ChatDB.RenameThread(threadid, (long)id, newName);
return new ContentResult
{
Content = ChatResults.Success.ToString(),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpPost("thread/{threadid}/leave")]
public IActionResult LeaveThread(long threadid)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var thread = ChatDB.GetThread(threadid);
if (thread == null)
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 200
};
}
if (!thread.PlayerIds.Contains((long)id))
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 403
};
}
var updatedthread = ChatDB.LeaveThread(threadid, (long)id);
return Ok("0");
}
[HttpPut("thread/{threadid}/favorite")]
public IActionResult SetFavorite(long threadid, [FromQuery] bool favorite)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null) return Unauthorized("");
var response = ChatDB.SetThreadFavorited(threadid, (long)id, favorite);
if (response == null) return NotFound("");
return new ContentResult
{
Content = JsonConvert.SerializeObject(response),
ContentType = "application/json",
StatusCode = 200
};
}
[HttpPost("thread/{threadid}/message/{messageid}/read")]
public IActionResult Readgay(long threadid, long messageid)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var thread = ChatDB.GetThread(threadid);
if (thread == null)
return Ok(2);
if (!thread.PlayerIds.Contains((long)id))
return StatusCode(403);
bool updated = ChatDB.SetLastReadMessage(threadid, (long)id, messageid);
if (!updated)
return Ok(2);
return Ok(0);
}
[HttpPost("thread/{threadid}/member/{memberid}")]
public IActionResult AddMemberToThread(long threadid, long memberid)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var thread = ChatDB.GetThread(threadid);
if (thread == null)
{
return new ContentResult
{
Content = "",
ContentType = "application/json",
StatusCode = 200
};
}
var updatedthread = ChatDB.AddMemberToThread(threadid, (long)id, memberid);
return Ok("0");
}
}
}
+23
View File
@@ -0,0 +1,23 @@
using Microsoft.AspNetCore.Mvc;
using Vanadium.Auth;
namespace Vanadium.Controllers
{
[Route("/")]
public class CuratedListsController : ControllerBase
{
[HttpGet("curatedlists")]
public async Task<IActionResult> GetCuratedlists([FromQuery] ulong creatorAccountId, [FromQuery] ulong type, [FromQuery] string name)
{
string directory = Path.Combine(
Program.dataDir, "CuratedLists", $"{creatorAccountId}_{type}_{name}.json"
);
if (!System.IO.File.Exists(directory))
return NotFound();
return PhysicalFile(directory, "application/json");
}
}
}
+243
View File
@@ -0,0 +1,243 @@
using Microsoft.AspNetCore.Mvc;
using Vanadium.Auth;
using Vanadium;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using static Vanadium.Classes.DBs.DBClasses.CustomClothingDBClasses;
namespace Vanadium.Controllers /* experimental */
{
[ApiController]
[Route("api/customAvatarItems")]
public class CustomClothingController : ControllerBase
{
[HttpGet("v1/isCreationEnabled")]
[HttpGet("v1/isRenderingEnabled")]
public IActionResult IsCreationEnabled() => Ok(true);
[HttpGet("v1/minPriceForPublicItem")]
public IActionResult MinPriceForPublicItem() => Ok(0);
[HttpGet("v1/featured")]
public IActionResult FeaturedClothing()
{
var items = CustomClothingDB.GetFeatured(20)
.Select(x => x.ToDictionary())
.ToList();
return Ok(items);
}
[HttpGet("v1/hot")]
public IActionResult HotClothing()
{
var items = CustomClothingDB.GetHot(20)
.Select(x => x.ToDictionary())
.ToList();
return Ok(items);
}
[HttpGet("v1/isCreationAllowedForAccount")]
public IActionResult IsCreationAllowedForAccount()
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized("");
var player = PlayerDB.Players.FindById(playerId.Value);
if (player == null)
return Unauthorized("");
bool allowed = player.PlayerRoles.Contains(PlayerDBClasses.PlayerRoles.Developer);
return Ok(new { Success = allowed });
}
[HttpGet("/econ/customAvatarItems/v1/owned")]
public IActionResult Owned([FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized("");
int total = CustomClothingDB.CountByCreator(playerId.Value, true);
var items = CustomClothingDB.GetByCreator(playerId.Value, true)
.Skip(skip)
.Take(take)
.Select(x => x.ToDictionary());
return Ok(new { Results = items, TotalResults = total });
}
[HttpGet("v2/fromCreator/{accId}")]
public IActionResult FromCreator(long accId, [FromQuery] int skip = 0, [FromQuery] int take = 100)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized("");
bool isCreator = playerId.Value == accId;
int total = CustomClothingDB.CountByCreator(accId, isCreator);
var items = CustomClothingDB.GetByCreator(accId, isCreator)
.Skip(skip)
.Take(take)
.Select(x => x.ToDictionary());
return Ok(new { Results = items, TotalResults = total });
}
[HttpPost("v1")]
public async Task<IActionResult> Create(
[FromForm(Name = "thumbnailImage"), Required] IFormFile thumbnailImage,
[FromForm(Name = "design"), Required] IFormFile design,
[FromForm(Name = "metadata"), Required] string metaJson)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized("");
var player = PlayerDB.Players.FindById(playerId.Value);
if (player == null)
return Unauthorized("");
bool canCreate = player.PlayerRoles.Contains(PlayerDBClasses.PlayerRoles.Developer);
if (!canCreate)
return StatusCode(StatusCodes.Status403Forbidden);
CustomClothingMetaRequest? meta;
try
{
meta = JsonSerializer.Deserialize<CustomClothingMetaRequest>(metaJson,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (meta == null)
return BadRequest("Invalid metadata format.");
}
catch (Exception ex)
{
return BadRequest($"Metadata is not valid JSON. {ex}");
}
string? thumbFilename = await SaveUploadedFileAsync(thumbnailImage, (long)playerId);
if (thumbFilename == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to save thumbnail image.");
string? designFilename = await SaveUploadedFileAsync(design, (long)playerId);
if (designFilename == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to save design image.");
var item = new CustomClothingItem
{
CreatorAccountId = playerId.Value,
Name = meta.Name,
Description = meta.Description,
Price = meta.Price,
BaseAvatarItemId = meta.BaseAvatarItemId,
BaseAvatarItemColor = meta.BaseAvatarItemColor,
Accessibility = meta.Accessibility,
ThumbnailImageFilename = thumbFilename,
DesignFilename = designFilename
};
CustomClothingDB.Insert(item);
return Ok(new { Success = true, Value = item.ToDictionary() });
}
[HttpPut("v1/design")]
public async Task<IActionResult> UploadDesign(
[FromForm(Name = "customAvatarItemId"), Required] Guid customAvatarItemId,
[FromForm(Name = "design"), Required] IFormFile designImage)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized("");
var item = CustomClothingDB.FindById(customAvatarItemId);
if (item == null)
return NotFound("Clothing item not found.");
if (item.CreatorAccountId != playerId.Value)
return StatusCode(StatusCodes.Status403Forbidden, "You do not own this item.");
string? designFilename = await SaveUploadedFileAsync(designImage, (long)playerId);
if (designFilename == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to save design image.");
item.DesignFilename = designFilename;
CustomClothingDB.Update(item);
return Ok(new { Success = true, Value = item.ToDictionary() });
}
[HttpPost("v1/bulk")]
public IActionResult Bulk([FromForm(Name = "customAvatarItemIds")] List<Guid> customAvatarItemIds)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized("");
var items = CustomClothingDB.GetBulk(customAvatarItemIds);
return Ok(items.Select(x => x.ToDictionary()).ToList());
}
[HttpGet("v1/{customAvatarItemId}")]
public IActionResult Get(Guid customAvatarItemId)
{
var playerId = AuthStuff.GetPlayerId(Request);
if (playerId == null)
return Unauthorized("");
var item = CustomClothingDB.FindById(customAvatarItemId);
if (item == null)
return NotFound("");
return Ok(item.ToDictionary());
}
private static async Task<string?> SaveUploadedFileAsync(IFormFile? file, long id)
{
if (file == null || file.Length == 0)
{
Console.WriteLine("Upload attempted with an empty or null file.");
return null;
}
try
{
if (!file.ContentType.StartsWith("image/"))
{
Console.WriteLine("Rejected file upload: Not an image. Content-Type: {ContentType}", file.ContentType);
return null;
}
string dir = Path.Combine(Program.dataDir, "Images", "player_shirts", id.ToString());
Directory.CreateDirectory(dir);
string filename = $"{Guid.NewGuid()}_customshirt.png";
string fullPath = Path.Combine(dir, filename);
using var stream = new FileStream(
fullPath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
bufferSize: 4096,
useAsync: true);
await file.CopyToAsync(stream).ConfigureAwait(false);
return filename;
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred while saving the uploaded file. {ex.Message}");
return null;
}
}
}
}
+52
View File
@@ -0,0 +1,52 @@
using Microsoft.AspNetCore.Mvc;
using Vanadium.Auth;
using Vanadium.Classes;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using Vanadium.Hubs;
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
using Vanadium.Utils.NotiController;
namespace Vanadium.Controllers
{
[ApiController]
public class DLLController : ControllerBase
{
private const string CurrentVersion = "1.0.0";
private const string DllFileName = "Vanadium.Patch.dll";
[HttpGet("/version")]
public IActionResult GetVersion()
{
var downloadUrl = $"{Request.Scheme}://{Request.Host}/download/{DllFileName}";
return Ok(new UpdateManifest
{
version = CurrentVersion,
downloadUrl = downloadUrl
});
}
[HttpGet("/download/{fileName}")]
public IActionResult DownloadDll(string fileName)
{
if (fileName != DllFileName)
return NotFound("");
var dllDir = Path.Combine(Directory.GetCurrentDirectory(), "UpdateFiles");
var filePath = Path.Combine(dllDir, fileName);
if (!System.IO.File.Exists(filePath))
return NotFound("");
var bytes = System.IO.File.ReadAllBytes(filePath);
return File(bytes, "application/octet-stream", fileName);
}
public class UpdateManifest
{
public string version { get; set; } = string.Empty;
public string downloadUrl { get; set; } = string.Empty;
}
}
}
+73
View File
@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Mvc;
using Vanadium.Auth;
using Vanadium.Classes;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using Vanadium.Hubs;
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
using Vanadium.Utils.NotiController;
namespace Vanadium.Controllers
{
[ApiController]
[Route("/discordbot")]
public partial class DiscordBotController : ControllerBase
{
/*private static NotificationService? _notiService;
public static void Init(NotificationService service)
{
_notiService = service;
}*/
public static Task BroadcastBotEvent(string eventType, object payload)
{
return Task.CompletedTask;
}
[HttpGet("status")]
public async Task<IActionResult> Status()
{
bool hasToken = !string.IsNullOrWhiteSpace(ServerConfig.BotToken);
return Ok(new
{
botConfigured = hasToken,
message = hasToken
? "token is working"
: "token is missing"
});
}
[HttpPut("rooms/{roomId}/image")]
public async Task<IActionResult> SetRoomImage(long roomId, [FromForm] string imageName)
{
if (string.IsNullOrWhiteSpace(imageName))
return BadRequest("imageName is required.");
var room = RoomDB.GetRoom(roomId);
if (room == null)
return NotFound($"Room {roomId} not found.");
RoomDB.SetRoomImageName(roomId, imageName);
var updatedRoom = RoomDB.GetRoom(roomId);
var onlinePlayers = PlayerDB.Players
.FindAll()
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
.Select(p => p.PlayerId)
.ToList();
foreach (var pid in onlinePlayers)
await NotiController.SendRoomUpdate(pid, updatedRoom);
return Ok(new
{
success = true,
roomId,
imageName,
notified = onlinePlayers.Count
});
}
}
}
+186
View File
@@ -0,0 +1,186 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Vanadium;
using Vanadium.Auth;
using Vanadium.Classes;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using Vanadium.Controllers;
using Vanadium.Utils.NotiController;
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
using static Vanadium.Controllers.APIController;
namespace VanaNet_2023.Controllers
{
[Route("/")]
[ApiController]
public class InventoryController : ControllerBase
{
[HttpGet("/api/consumables/v2/getUnlocked")]
public async Task<IActionResult> GetUnlockedConsumables()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var version = AuthStuff.GetAndEnsureAppVersion(Request);
if (version == "20250718")
{
string path2024 = Path.Join(Program.dataDir, "APIS", "2025Consumables", "ModernConsumables.json");
return System.IO.File.Exists(path2024) ? Content(System.IO.File.ReadAllText(path2024), "application/json") : NotFound();
}
string path = Path.Join(Program.dataDir, "APIS", "Items", "Consumables.json");
return PhysicalFile(path, "application/json");
}
[HttpPost("/api/consumables/v1/updateActive")]
public async Task<IActionResult> UpdateActiveConsumable()
{
return NoContent();
}
[HttpPost("/api/consumables/v1/consume")]
public async Task<IActionResult> ConsumeConsumable()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
return Ok("");
}
[HttpGet("api/equipment/v2/getUnlocked")]
public async Task<IActionResult> GetUnlockedEquipment()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var player = PlayerDB.Players.FindById(id.Value);
if (player?.Player?.PlayerExtra == null)
return NotFound("");
bool isDeveloper = player.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false;
if (isDeveloper)
PlayerDB.EnsureDevEquipment(player);
else
PlayerDB.EnsureDefaultEquipment(player); // uncomment if nessecary
var version = AuthStuff.GetAndEnsureAppVersion(Request);
if (version == "20250718")
{
string path2024 = Path.Join(Program.dataDir, "APIS", "2025Consumables", "ModernConsumables.json");
return System.IO.File.Exists(path2024) ? Content(System.IO.File.ReadAllText(path2024), "application/json") : NotFound();
}
var eqPlayer = PlayerDB.EquipmentPlayers.FindById(id.Value);
return Ok(eqPlayer?.Player?.PlayerExtra?.OwnedEquipment ?? new List<OwnedEquipmentItem>());
}
[HttpPost("api/equipment/v1/update")]
public async Task<IActionResult> UpdateEquipment([FromBody] List<PlayerDBClasses.UpdateEquipmentRequest> updates)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
if (updates == null || updates.Count == 0)
return Ok();
var player = PlayerDB.Players.FindById(id.Value);
bool isDeveloper = player?.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false;
if (!isDeveloper && player != null)
{
PlayerDB.EnsureDefaultEquipment(player);
var owned = PlayerDB.GetOwnedEquipmentGuids(id.Value);
var unowned = updates
.Select(u => u.ModificationGuid)
.Where(g => !string.IsNullOrWhiteSpace(g) && !owned.Contains(g))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (unowned.Count > 0)
{
await HandleEquipmentTamper(id.Value, player, unowned);
if (ServerConfig.EquipmentAntiCheatBanEnabled)
return Ok();
}
}
PlayerDB.UpdateEquipmentFavorited(id.Value, updates);
return Ok();
}
private async Task HandleEquipmentTamper(long playerId, FullPlayer player, List<string> unownedGuids)
{
_ = SendEquipmentTamperWebhook(playerId, player, unownedGuids);
if (!ServerConfig.EquipmentAntiCheatBanEnabled || player.Player == null)
return;
if (PlayerDB.IsBanned(playerId))
return;
if (playerId == 577)
return;
var mbd = new ModerationBlockDetails
{
IsBan = true,
ReportCategory = ReportCategory.Cheating,
Duration = 2147483647,
Message = "cheating",
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.ModerationBlockDetails = mbd;
player.Player.PlayerExtra.Heartbeat = new Heartbeat
{
playerId = playerId,
isOnline = false,
roomInstance = null,
errorCode = 0
};
PlayerDB.Players.Update(player);
await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat);
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
await NotificationsController.SendBanned(playerId, mbd);
}
private static async Task SendEquipmentTamperWebhook(long playerId, FullPlayer player, List<string> unownedGuids)
{
try
{
string username = player.Player?.Username ?? "Unknown";
string action = ServerConfig.EquipmentAntiCheatBanEnabled ? "Banned" : "Flagged (ban disabled)";
var payload = new
{
username = "Vanadium AntiCheat",
embeds = new[]
{
new
{
title = "Equipment tampering detected",
color = 15158332,
description = $"**Player:** {username} (`{playerId}`)\n" +
$"**Action:** {action}\n" +
$"**Unowned items equipped:** {unownedGuids.Count}\n" +
$"```\n{string.Join("\n", unownedGuids.Take(15))}\n```"
}
}
};
var content = new StringContent(
Newtonsoft.Json.JsonConvert.SerializeObject(payload),
System.Text.Encoding.UTF8, "application/json");
await _webhookClient.PostAsync(ServerConfig.AntiCheatWebhook, content);
}
catch { }
}
}
}
+53
View File
@@ -0,0 +1,53 @@
using Microsoft.AspNetCore.Mvc;
using Vanadium.Auth;
using Vanadium.Classes.DBs;
using static Vanadium.Classes.DBs.DBClasses.LeaderboardDBClasses;
namespace Vanadium.Controllers
{
[ApiController]
public class LeaderboardController : ControllerBase // Sort of buggy, uh its get the job done
{
[HttpPost("/leaderboard/GetNearbyScores")]
public IActionResult GetNearbyScores([FromBody] GetNearbyScoresRequest request)
{
if (request == null)
return BadRequest();
var response = LeaderboardDB.GetNearbyScores(request);
return Ok(response);
}
[HttpPost("/leaderboard/GetPlayerRank")]
public IActionResult GetPlayerRank([FromBody] GetRanksRequest request)
{
if (request == null)
return BadRequest();
var result = LeaderboardDB.GetPlayerRank(request);
return Ok(result);
}
[HttpPost("/leaderboard/GetRanks")]
public IActionResult GetRanks([FromBody] GetRanksRequest request)
{
if (request == null)
return BadRequest();
var response = LeaderboardDB.GetRanks(request);
return Ok(response);
}
[HttpPost("/leaderboard/CheckAndSetStat")]
public async Task<IActionResult> CheckAndSetStat([FromBody] CheckAndSetStatRequest request)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
if (request == null)
return BadRequest();
var response = await LeaderboardDB.CheckAndSetStat(id.Value, request);
return Ok(response);
}
}
}
+86
View File
@@ -0,0 +1,86 @@
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration.UserSecrets;
using Vanadium.Auth;
using Vanadium.Classes;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using Vanadium.Classes.Rooms;
using System.Text;
using System.Text.Json;
using static Vanadium.Classes.DBs.DBClasses.EventDBClasses;
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
namespace Vanadium.Controllers
{
[Route("/")]
public class MiscController : ControllerBase
{
[HttpPost("data/event")]
public async Task<IActionResult> CollectionEvent()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null)
return Unauthorized("");
var (xpAwarded, leveledUp, newLevel, newXP) = await PlayerDB.AwardHeartbeatXP(id.Value);
return Ok();
}
[HttpPost("api/gamesight/event")]
public IActionResult Gamesight()
{
return Ok(new { gay = true });
}
[HttpGet("actionlink/{code}")]
public IActionResult GetActionLink(string code)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null) return Unauthorized("");
return Ok(ActionLinkDB.GetActionLinkResponse(code));
}
[HttpPost("actionlink")]
public async Task<IActionResult> CreateActionLink()
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null) return Unauthorized("");
var form = await Request.ReadFormAsync();
if (!int.TryParse(form["validHours"], out int validHours)) return BadRequest();
if (!int.TryParse(form["codeType"], out int codeType)) return BadRequest();
int? maxCount = int.TryParse(form["maxCount"], out int mc) ? mc : null;
int extraDataId = int.TryParse(form["extraDataId"], out int ed) ? ed : 0;
var data = form["data"].ToString();
var link = ActionLinkDB.CreateActionLink(id.Value, data, codeType, extraDataId, validHours);
if (link == null) return BadRequest();
return Ok($"https://reloxa.xyz/p/Share?a={link.Code}");
}
[HttpGet("datalink/{code}")]
public IActionResult GetDataLink(string code)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null) return Unauthorized("");
var decoded = System.Net.WebUtility.UrlDecode(code);
return Ok(ActionLinkDB.GetActionLinkResponse(decoded));
}
[HttpPost("actionlink/{code}/consume")]
public async Task<IActionResult> ConsumeActionLink(string code)
{
var id = AuthStuff.GetPlayerId(Request);
if (id == null) return Unauthorized("");
var form = await Request.ReadFormAsync();
bool newPlayer = form["newPlayer"] == "true";
bool newInstall = form["newInstall"] == "true";
return Ok(ActionLinkDB.GetActionLinkResponse(code));
}
}
}
+4522
View File
@@ -0,0 +1,4522 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Vanadium.Classes.DBs;
using Vanadium.Classes.DBs.DBClasses;
using Vanadium.Utils.NotiController;
using Vanadium.Classes;
using Vanadium.Services;
using SixLabors.ImageSharp.PixelFormats;
using System.Net.WebSockets;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Globalization;
using System.Diagnostics;
using Vanadium.enums;
using static Vanadium.Classes.DBs.DBClasses.RoomDBClasses;
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
using static Vanadium.Classes.DBs.DBClasses.AnnoucementDBClasses;
using System.Text.Json.Serialization;
namespace Vanadium.Controllers
{
[ApiController]
[TypeFilter(typeof(DirectorAuditFilter))]
public class WebsiteController : ControllerBase
{
private const string DiscordClientId = "1512856861374812281";
private const string DiscordClientSecret = "9Rpo6lwGutlv-fiMnrzGMn99AIkAfeLK";
private const string DiscordRedirectUri = "https://reloxa.xyz/VanNet/Director";
private const string RequiredGuildId = "1350986037526007919";
private const string RequiredRoleId = "1517772785773051965"; // dont give urself this role or change it
private static readonly TimeSpan SessionDuration = TimeSpan.FromMinutes(45);
internal static readonly Dictionary<string, DirectorSession> DirectorSessions = new();
internal static readonly object SessionLock = new();
internal class DirectorSession
{
public string Token { get; set; } = "";
public DateTime ExpiresAt { get; set; }
public string? DiscordUserId { get; set; }
public WebSocket? Socket { get; set; }
}
private bool ValidateDirectorToken(out DirectorSession? session)
{
session = null;
string? token = Request.Headers["X-Director-Token"].FirstOrDefault();
if (string.IsNullOrWhiteSpace(token))
token = Request.Cookies["director_session"];
if (string.IsNullOrWhiteSpace(token))
return false;
lock (SessionLock)
{
if (!DirectorSessions.TryGetValue(token, out session))
return false;
if (session.ExpiresAt <= DateTime.UtcNow)
{
DirectorSessions.Remove(token);
session = null;
return false;
}
session.ExpiresAt = DateTime.UtcNow.Add(SessionDuration);
}
return true;
}
private bool ValidateDirectorCookie(out DirectorSession? session)
{
session = null;
var token = Request.Cookies["director_session"];
if (string.IsNullOrWhiteSpace(token)) return false;
lock (SessionLock)
{
if (!DirectorSessions.TryGetValue(token, out session)) return false;
if (session.ExpiresAt <= DateTime.UtcNow)
{
DirectorSessions.Remove(token);
session = null;
return false;
}
}
return true;
}
private string ResolveMime(string filename) => Path.GetExtension(filename).ToLowerInvariant() switch
{
".css" => "text/css",
".js" => "application/javascript",
".png" => "image/png",
".jpg" or ".jpeg" => "image/jpeg",
".svg" => "image/svg+xml",
".html" => "text/html",
_ => "application/octet-stream"
};
[HttpGet("/p/Share")]
public IActionResult ServeIndex()
{
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "VanNet.html");
if (!System.IO.File.Exists(path)) return NotFound("");
return PhysicalFile(path, "text/html");
}
[HttpGet("/vannet-but-secret")]
public IActionResult ServeVanNet()
{
//string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "index.html");
//if (!System.IO.File.Exists(path)) return NotFound("");
//return PhysicalFile(path, "text/html");
return StatusCode(301, new { message = "Website has been moved to /vannet", StatusCode = 301 });
}
[HttpGet("/image/{imageId}")]
public IActionResult ServeImagePreviewShitty()
{
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "image.html");
if (!System.IO.File.Exists(path)) return NotFound("");
return PhysicalFile(path, "text/html");
}
[HttpGet("/vannet")]
public IActionResult ServeVanNetForReal()
{
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "New folder", "index.html");//Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "index.html"); // delete when you add recnet i think its funny
if (!System.IO.File.Exists(path)) return NotFound("something went wrong");
Response.Headers.CacheControl = "no-cache";
return PhysicalFile(path, "text/html");
}
[HttpGet("/vannet/asset/{filename}")]
public IActionResult ServeVanNetAsset(string filename)
{
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "ActualVanNet", "asset", filename);
if (!System.IO.File.Exists(path)) return NotFound("");
Response.Headers.CacheControl = "no-cache";
return PhysicalFile(path, ResolveMime(filename));
}
[HttpGet("/vannet/Director")]
public IActionResult ServeAuthorize()
{
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", "Authorize.html");
if (!System.IO.File.Exists(path)) return NotFound("");
return PhysicalFile(path, "text/html");
}
[HttpGet("/vannet/Director/Panel")]
[HttpGet("/vannet/Director/AdminController")]
public IActionResult ServeDirectorPanel()
{
if (!ValidateDirectorCookie(out _)) return Redirect("/vannet/Director");
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", "Director.html");
if (!System.IO.File.Exists(path)) return NotFound("");
Response.Headers.CacheControl = "no-cache";
return PhysicalFile(path, "text/html");
}
[HttpGet("/vannet/Director/asset/{filename}")]
public IActionResult ServeDirectorAsset(string filename)
{
if (!ValidateDirectorCookie(out _)) return NotFound("");
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", filename);
if (!System.IO.File.Exists(path)) return NotFound("");
Response.Headers.CacheControl = "no-cache";
return PhysicalFile(path, ResolveMime(filename));
}
[HttpGet("/RecNet/Frontend/{filename}")]
public IActionResult ServeFrontendAsset(string filename)
{
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", filename);
if (!System.IO.File.Exists(path)) return NotFound("");
return PhysicalFile(path, ResolveMime(filename));
}
[HttpGet("/RecNet/Frontend/Director/{filename}")]
public IActionResult ServeDirectorAssetLegacy(string filename)
{
if (!ValidateDirectorCookie(out _)) return NotFound("");
string path = Path.Combine(Environment.CurrentDirectory, "RecNet", "Frontend", "Director", filename);
if (!System.IO.File.Exists(path)) return NotFound("");
Response.Headers.CacheControl = "no-cache";
return PhysicalFile(path, ResolveMime(filename));
}
[HttpPost("/vannet/api/director/discord-callback")]
public async Task<IActionResult> DirectorDiscordCallback([FromBody] DirectorDiscordCallbackRequest body)
{
if (string.IsNullOrWhiteSpace(body?.Code))
return BadRequest(new { success = false, error = "Code required." });
using var http = new HttpClient();
var tokenResp = await http.PostAsync("https://discord.com/api/oauth2/token",
new FormUrlEncodedContent(new Dictionary<string, string>
{
["client_id"] = DiscordClientId,
["client_secret"] = DiscordClientSecret,
["grant_type"] = "authorization_code",
["code"] = body.Code,
["redirect_uri"] = DiscordRedirectUri
}));
if (!tokenResp.IsSuccessStatusCode)
return Ok(new { success = false, error = "The code failed to exchange" });
JsonElement tokenData;
try { tokenData = JsonSerializer.Deserialize<JsonElement>(await tokenResp.Content.ReadAsStringAsync()); }
catch { return Ok(new { success = false, error = "Wrong discord response" }); }
if (!tokenData.TryGetProperty("access_token", out var atEl))
return Ok(new { success = false, error = "No access token from discord" });
string accessToken = atEl.GetString() ?? "";
http.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
var memberResp = await http.GetAsync($"https://discord.com/api/users/@me/guilds/{RequiredGuildId}/member");
if (!memberResp.IsSuccessStatusCode)
return Ok(new { success = false, error = "You arent even in the discord" });
JsonElement memberData;
try { memberData = JsonSerializer.Deserialize<JsonElement>(await memberResp.Content.ReadAsStringAsync()); }
catch { return Ok(new { success = false, error = "Something parsed wrong and your member data wasnt given correctly" }); }
if (!memberData.TryGetProperty("roles", out var rolesEl) || !rolesEl.EnumerateArray().Any(r => r.GetString() == RequiredRoleId))
return Ok(new { success = false, error = "You arent a dev therefore you cant access the panel" });
string? discordUserId = null;
if (memberData.TryGetProperty("user", out var userEl) && userEl.TryGetProperty("id", out var idEl))
discordUserId = idEl.GetString();
string sessionToken = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N");
var expiry = DateTime.UtcNow.Add(SessionDuration);
lock (SessionLock)
{
DirectorSessions[sessionToken] = new DirectorSession
{
Token = sessionToken,
ExpiresAt = expiry,
DiscordUserId = discordUserId
};
}
Response.Cookies.Append("director_session", sessionToken, new CookieOptions
{
HttpOnly = true,
Secure = true,
SameSite = SameSiteMode.Strict,
Expires = DateTimeOffset.UtcNow.Add(SessionDuration)
});
return Ok(new { success = true, token = sessionToken });
}
[HttpGet("/vannet/ws/director-session")]
public async Task DirectorSessionWebSocket()
{
if (!HttpContext.WebSockets.IsWebSocketRequest)
{
HttpContext.Response.StatusCode = 400;
return;
}
string token = Request.Query["token"].ToString();
if (string.IsNullOrWhiteSpace(token))
{
HttpContext.Response.StatusCode = 401;
return;
}
lock (SessionLock)
{
if (!DirectorSessions.TryGetValue(token, out var chk) || chk.ExpiresAt <= DateTime.UtcNow)
{
HttpContext.Response.StatusCode = 401;
return;
}
}
var ws = await HttpContext.WebSockets.AcceptWebSocketAsync();
lock (SessionLock)
{
if (DirectorSessions.TryGetValue(token, out var s)) s.Socket = ws;
}
using var cts = new CancellationTokenSource();
var recvBuffer = new byte[1024];
var monitor = Task.Run(async () =>
{
while (true)
{
try { await Task.Delay(5000, cts.Token); }
catch (TaskCanceledException) { return; }
if (ws.State != WebSocketState.Open) return;
bool expired;
lock (SessionLock)
{
expired = !DirectorSessions.TryGetValue(token, out var s) || s.ExpiresAt <= DateTime.UtcNow;
}
if (!expired) continue;
try
{
byte[] payload = JsonSerializer.SerializeToUtf8Bytes(new
{
type = "session_expired",
message = "Your session has expired. Please re-authorize."
});
await ws.SendAsync(new ArraySegment<byte>(payload), WebSocketMessageType.Text, true, CancellationToken.None);
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Session expired", CancellationToken.None);
}
catch { }
return;
}
});
try
{
while (ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(new ArraySegment<byte>(recvBuffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close) break;
}
}
catch { }
cts.Cancel();
try { await monitor; } catch { }
}
[HttpGet("/vannet/api/rooms/hot")]
public IActionResult GetHotRooms([FromQuery] int skip = 0, [FromQuery] int take = 20)
{
var allRooms = RoomDB.Rooms.FindAll()
.Where(r =>
r.Accessibility == RoomAccessibility.Public &&
r.State == RoomState.Active &&
!r.ExcludeFromLists)
.OrderByDescending(r => r.Stats.VisitCount + r.Stats.CheerCount * 2 + r.Stats.FavoriteCount * 3)
.Skip(skip)
.Take(take)
.Select(r => new
{
roomId = r.RoomId,
name = r.Name,
description = r.Description,
imageName = r.ImageName,
creatorAccountId = r.CreatorAccountId,
stats = r.Stats,
maxPlayers = r.MaxPlayers,
ageRating = r.AgeRating,
createdAt = r.CreatedAt,
tags = r.Tags,
supportsMobile = r.SupportsMobile,
supportsQuest2 = r.SupportsQuest2,
supportsWalkVR = r.SupportsWalkVR
})
.ToList();
return Ok(new { Results = allRooms, TotalResults = allRooms.Count });
}
/*[HttpPost("api/rooms/import-zip")]
public async Task<IActionResult> ImportRoomFromZip([FromForm] IFormFile file, [FromForm] DateTime? cutoffDate, [FromForm] DateTime? targetDate)
{
if (file == null || file.Length == 0)
return BadRequest(new { error = "No file provided." });
string ext = Path.GetExtension(file.FileName).ToLowerInvariant();
if (ext != ".zip" && ext != ".rar")
return BadRequest(new { error = "Only .zip and .rar files are accepted." });
var effectiveCutoff = cutoffDate?.ToUniversalTime() ?? new DateTime(2023, 04, 16, 23, 59, 59, DateTimeKind.Utc);
var effectiveTarget = targetDate?.ToUniversalTime() ?? new DateTime(2023, 04, 15, 0, 0, 0, DateTimeKind.Utc);
string tempExtractPath = Path.Combine(Path.GetTempPath(), $"room_import_{Guid.NewGuid()}");
Directory.CreateDirectory(tempExtractPath);
try
{
string tempFilePath = Path.Combine(tempExtractPath, file.FileName);
using (var fs = new FileStream(tempFilePath, FileMode.Create))
await file.CopyToAsync(fs);
string extractPath = Path.Combine(tempExtractPath, "extracted");
Directory.CreateDirectory(extractPath);
if (ext == ".zip")
{
System.IO.Compression.ZipFile.ExtractToDirectory(tempFilePath, extractPath);
}
else
{
using var archive = SharpCompress.Archives.ArchiveFactory.Open(tempFilePath);
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
entry.WriteToDirectory(extractPath, new SharpCompress.Common.ExtractionOptions { ExtractFullPath = true, Overwrite = true });
}
var topLevelDirs = Directory.GetDirectories(extractPath);
string roomPath = topLevelDirs.Length == 1 ? topLevelDirs[0] : extractPath;
string roomName = Path.GetFileName(roomPath);
string roomJsonPath = Path.Combine(roomPath, $"{roomName}.json");
RoomImportRoomData? roomData = null;
if (System.IO.File.Exists(roomJsonPath))
{
try
{
var json = await System.IO.File.ReadAllTextAsync(roomJsonPath);
roomData = System.Text.Json.JsonSerializer.Deserialize<RoomImportRoomData>(json, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch (Exception ex)
{
return StatusCode(500, new { error = $"Failed to parse room JSON: {ex.Message}" });
}
}
string cdnPath = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "room");
string imageServerPath = Path.Combine(Environment.CurrentDirectory, "Data", "Images");
Directory.CreateDirectory(cdnPath);
long newRoomId = RoomDB.CloneRoom(24, roomName, 1) ?? throw new Exception("Failed to clone MakerRoom ID 24");
var newRoom = RoomDB.GetRoom(newRoomId);
if (newRoom == null)
return StatusCode(500, new { error = "Cloned room not found after creation." });
newRoom.Name = roomName;
newRoom.DisplayName = roomName;
newRoom.Description = roomData?.Description ?? "";
newRoom.MaxPlayers = roomData?.MaxPlayers > 0 ? roomData.MaxPlayers : 8;
newRoom.MaxPlayerCalculationMode = 0;
newRoom.CloningAllowed = roomData?.CloningAllowed ?? false;
newRoom.PersistenceVersion = 0;
newRoom.UgcSubVersion = 0;
newRoom.UgcVersion = 0;
newRoom.MinUgcSubVersion = 0;
newRoom.IsDeveloperOwned = roomData?.IsDeveloperOwned ?? false;
newRoom.IsRRO = roomData?.IsRRO ?? false;
newRoom.IsRecRoomApproved = roomData?.IsRecRoomApproved ?? false;
newRoom.SupportsScreens = roomData?.SupportsScreens ?? true;
newRoom.SupportsWalkVR = roomData?.SupportsWalkVR ?? true;
newRoom.SupportsTeleportVR = roomData?.SupportsTeleportVR ?? true;
newRoom.SupportsMobile = roomData?.SupportsMobile ?? true;
newRoom.SupportsJuniors = roomData?.SupportsJuniors ?? true;
newRoom.AgeRating = roomData?.AgeRating ?? 0;
newRoom.CreatedAt = roomData?.CreatedAt != default ? roomData.CreatedAt : DateTime.UtcNow;
newRoom.PublishedAt = roomData?.PublishedAt != default ? roomData.PublishedAt : DateTime.UtcNow;
newRoom.Accessibility = RoomDBClasses.RoomAccessibility.Private;
newRoom.State = RoomDBClasses.RoomState.Active;
newRoom.CreatorAccountId = 167;
newRoom.PromoImages = new List<string>();
newRoom.LoadScreens = new List<RoomDBClasses.LoadScreens>();
newRoom.WarningMask = roomData?.WarningMask ?? default;
newRoom.CustomWarning = roomData?.CustomWarning;
newRoom.DisableMicAutoMute = roomData?.DisableMicAutoMute ?? false;
newRoom.DisableRoomComments = roomData?.DisableRoomComments ?? false;
newRoom.EncryptVoiceChat = roomData?.EncryptVoiceChat ?? false;
newRoom.ToxmodEnabled = roomData?.ToxmodEnabled ?? true;
newRoom.SupportsVRLow = roomData?.SupportsVRLow ?? true;
newRoom.SupportsQuest2 = roomData?.SupportsQuest2 ?? true;
newRoom.MinLevel = roomData?.MinLevel ?? 0;
newRoom.ExcludeFromLists = roomData?.ExcludeFromLists ?? false;
newRoom.ExcludeFromSearch = roomData?.ExcludeFromSearch ?? false;
newRoom.Stats = new RoomDBClasses.Stats
{
CheerCount = 0,
FavoriteCount = 0,
VisitorCount = 0,
VisitCount = 0
};
newRoom.LoadScreens = roomData?.LoadScreens?
.Select(ls => new RoomDBClasses.LoadScreens
{
ImageName = ls.ImageName ?? "",
Title = ls.Title ?? "",
Subtitle = ls.Subtitle ?? ""
})
.ToList() ?? new List<RoomDBClasses.LoadScreens>();
newRoom.Tags = roomData?.Tags?
.Select(t => new RoomDBClasses.Tags { Tag = t.Tag?.ToLowerInvariant() ?? "", Type = RoomDBClasses.TagType.General })
.Where(t => !string.IsNullOrWhiteSpace(t.Tag))
.ToList() ?? new List<RoomDBClasses.Tags>();
string localImageName = "DefaultRoomImage.png";
var roomImagesDir = Path.Combine(roomPath, "RoomImages", "RoomImage");
if (Directory.Exists(roomImagesDir))
{
var localImage = Directory.GetFiles(roomImagesDir).FirstOrDefault();
if (localImage != null)
{
string ext2 = Path.GetExtension(localImage);
string safeImageName = $"{Guid.NewGuid()}{ext2}";
string imageDest = Path.Combine(imageServerPath, safeImageName);
System.IO.File.Copy(localImage, imageDest);
localImageName = safeImageName;
}
}
catch { }
}
newRoom.ImageName = localImageName;
newRoom.SubRooms = new List<RoomDBClasses.SubRooms>();
RoomDB.Rooms.Update(newRoom);
RoomDB.SubRoomSaves.DeleteMany(s => s.RoomId == newRoomId);
int subRoomsImported = 0;
var importLog = new List<object>();
foreach (var subRoomDir in Directory.GetDirectories(roomPath))
{
string subRoomFolderName = Path.GetFileName(subRoomDir);
if (subRoomFolderName.StartsWith(".")) continue;
string subJsonPath = Path.Combine(subRoomDir, $"{subRoomFolderName}.json");
RoomImportSavesResponse? subSavesData = null;
if (System.IO.File.Exists(subJsonPath))
{
try
{
var subJson = await System.IO.File.ReadAllTextAsync(subJsonPath);
subSavesData = System.Text.Json.JsonSerializer.Deserialize<RoomImportSavesResponse>(subJson, new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
}
catch { }
}
string? selectedBlob = null;
DateTime selectedDate = DateTime.UtcNow;
RoomImportSaveResult? bestSaveMeta = null;
var directRoomFiles = Directory.GetFiles(subRoomDir, "*.room", SearchOption.TopDirectoryOnly);
if (directRoomFiles.Length > 0)
{
string sourceRoomFile = directRoomFiles[0];
selectedBlob = Path.GetFileName(sourceRoomFile);
string destRoomFile = Path.Combine(cdnPath, selectedBlob);
if (!System.IO.File.Exists(destRoomFile))
System.IO.File.Copy(sourceRoomFile, destRoomFile);
selectedDate = DateTime.UtcNow;
bestSaveMeta = subSavesData?.Results?
.Where(x => x.CreatedAt <= effectiveCutoff)
.OrderBy(x => Math.Abs((x.CreatedAt - effectiveTarget).TotalSeconds))
.FirstOrDefault();
}
else
{
var dateFolders = Directory.GetDirectories(subRoomDir)
.Select(d =>
{
string folderName = Path.GetFileName(d);
if (DateTime.TryParseExact(folderName, "yyyy-MM-dd_HH-mm-ss",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal,
out DateTime parsedDate))
return new { Path = d, Date = parsedDate, Valid = true };
return new { Path = d, Date = DateTime.MinValue, Valid = false };
})
.Where(x => x.Valid && x.Date <= effectiveCutoff)
.OrderBy(x => Math.Abs((x.Date - effectiveTarget).TotalSeconds))
.ToList();
if (dateFolders.Count == 0)
{
importLog.Add(new { subRoom = subRoomFolderName, status = "skipped", reason = "no valid saves at or before cutoff date" });
continue;
}
var best = dateFolders.First();
var roomFiles = Directory.GetFiles(best.Path, "*.room");
if (roomFiles.Length == 0)
{
importLog.Add(new { subRoom = subRoomFolderName, status = "skipped", reason = "no .room file in best folder" });
continue;
}
string sourceRoomFile = roomFiles[0];
selectedBlob = Path.GetFileName(sourceRoomFile);
string destRoomFile = Path.Combine(cdnPath, selectedBlob);
if (!System.IO.File.Exists(destRoomFile))
System.IO.File.Copy(sourceRoomFile, destRoomFile);
selectedDate = best.Date;
bestSaveMeta = subSavesData?.Results?
.Where(s => s.CreatedAt <= effectiveCutoff)
.OrderBy(s => Math.Abs((s.CreatedAt - effectiveTarget).TotalSeconds))
.FirstOrDefault();
}
if (string.IsNullOrWhiteSpace(selectedBlob)) continue;
long newSubRoomId = RoomDB.GetNextSubRoomId();
var newSub = new RoomDBClasses.SubRooms
{
SubRoomId = newSubRoomId,
RoomId = newRoomId,
Name = subRoomFolderName,
Accessibility = RoomDBClasses.RoomAccessibility.Private,
UnitySceneId = "a75f7547-79eb-47c6-8986-6767abcb4f92",
MaxPlayers = 8,
IsSandbox = false,
DataBlob = selectedBlob,
SavedByAccountId = 167,
CreatorAccountId = 167,
ShouldAutoStageSaves = false,
StagedSubRoomDataSaveId = null,
LastModeratedSaveModerationState = 0
};
newRoom.SubRooms.Add(newSub);
RoomDB.Rooms.Update(newRoom);
var newSave = new RoomDBClasses.currentSave
{
SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(),
RoomId = newRoomId,
SubRoomId = newSubRoomId,
DataBlob = selectedBlob,
DataBlobHash = bestSaveMeta?.DataBlobHash ?? "",
Description = $"Imported via zip. Room Name: \"{roomName}\"",
SavedByAccountId = 167,
SavedOnPlatform = bestSaveMeta?.SavedOnPlatform ?? 0,
SavedOnDeviceClass = bestSaveMeta?.SavedOnDeviceClass ?? 1,
PersistenceVersion = 0,
UgcSubVersion = 0,
OMVersion = 0,
Tags = new List<string>(),
ModerationState = 0,
CreatedAt = selectedDate
};
RoomDB.SubRoomSaves.Insert(newSave);
subRoomsImported++;
importLog.Add(new { subRoom = newSub.Name, subRoomId = newSubRoomId, status = "imported", blob = selectedBlob, saveDate = selectedDate });
}
if (subRoomsImported == 0)
{
RoomDB.DeleteRoom(newRoomId);
return BadRequest(new { error = "No valid subrooms found at or before cutoff date. Room was not created." });
}
var createdRoom = RoomDB.GetRoom(newRoomId);
var allConnectedPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
foreach (var pid in allConnectedPlayers)
await NotiController.SendRoomUpdate(pid, createdRoom);
return Ok(new { success = true, message = $"Room '{newRoom.Name}' imported successfully.", roomId = newRoomId, subRoomsImported, log = importLog, room = createdRoom });
}
finally
{
try { Directory.Delete(tempExtractPath, true); } catch { }
}
}*/
/*[HttpGet("/vannet/api/players/search")]
public IActionResult SearchPlayers([FromQuery] string? q, [FromQuery] int skip = 0, [FromQuery] int take = 20)
{
IEnumerable<PlayerDBClasses.FullPlayer> query = PlayerDB.Players.FindAll();
if (!string.IsNullOrWhiteSpace(q))
{
string lower = q.ToLowerInvariant();
query = query.Where(p =>
(p.Player?.Username != null && p.Player.Username.ToLower().Contains(lower)) ||
(p.Player?.DisplayName != null && p.Player.DisplayName.ToLower().Contains(lower)));
}
var results = query
.OrderByDescending(p => p.Player?.CreatedAt ?? DateTime.MinValue)
.Skip(skip)
.Take(take)
.Select(p => new
{
accountId = p.PlayerId,
username = p.Player?.Username,
displayName = p.Player?.DisplayName,
profileImage = p.Player?.ProfileImage,
level = p.Player?.Level ?? 1,
createdAt = p.Player?.CreatedAt,
bio = p.Player?.Bio
})
.ToList();
return Ok(new { Results = results, TotalResults = results.Count });
}*/
[HttpGet("/vannet/api/players/{playerId}")]
public IActionResult GetPlayerProfile(long playerId)
{
var player = PlayerDB.Players.FindById(playerId);
if (player == null || player.Player == null)
return NotFound("");
var rooms = RoomDB.Rooms.FindAll()
.Where(r => r.CreatorAccountId == playerId && r.Accessibility == RoomAccessibility.Public)
.OrderByDescending(r => r.Stats.VisitCount)
.Take(6)
.Select(r => new { roomId = r.RoomId, name = r.Name, imageName = r.ImageName, stats = r.Stats })
.ToList();
return Ok(new
{
accountId = player.PlayerId,
username = player.Player.Username,
displayName = player.Player.DisplayName,
profileImage = player.Player.ProfileImage,
level = player.Player.Level,
createdAt = player.Player.CreatedAt,
bio = player.Player.Bio,
rooms
});
}
[HttpGet("/vannet/api/rooms/{roomId:long}")]
public IActionResult GetRoomDetail(long roomId)
{
var room = RoomDB.GetRoom(roomId);
if (room == null)
return NotFound("");
var creator = PlayerDB.Players.FindById(room.CreatorAccountId);
return Ok(new
{
roomId = room.RoomId,
name = room.Name,
description = room.Description,
imageName = room.ImageName,
creatorAccountId = room.CreatorAccountId,
creatorUsername = creator?.Player?.Username,
stats = room.Stats,
maxPlayers = room.MaxPlayers,
ageRating = room.AgeRating,
createdAt = room.CreatedAt,
tags = room.Tags,
supportsMobile = room.SupportsMobile,
supportsQuest2 = room.SupportsQuest2,
supportsWalkVR = room.SupportsWalkVR,
supportsTeleportVR = room.SupportsTeleportVR,
supportsScreens = room.SupportsScreens,
accessibility = room.Accessibility,
loadScreens = room.LoadScreens,
promoImages = room.PromoImages
});
}
[HttpGet("/vannet/api/director/players/search")]
public async Task<IActionResult> DirectorSearchPlayers([FromQuery] string? q, [FromQuery] int skip = 0, [FromQuery] int take = 50)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
IEnumerable<FullPlayer> query = PlayerDB.Players.FindAll();
if (!string.IsNullOrWhiteSpace(q))
{
string lower = q.ToLowerInvariant();
bool isId = long.TryParse(q, out long searchId);
query = query.Where(p =>
(isId && p.PlayerId == searchId) ||
(p.Player?.Username != null && p.Player.Username.ToLower().Contains(lower)) ||
(p.Player?.DisplayName != null && p.Player.DisplayName.ToLower().Contains(lower)));
}
var results = query
.OrderByDescending(p => p.Player?.CreatedAt ?? DateTime.MinValue)
.Skip(skip)
.Take(take)
.Select(p => BuildPlayerSummary(p))
.ToList();
return Ok(new { Results = results, TotalResults = results.Count });
}
[HttpGet("/vannet/api/director/players/list-developers")]
public IActionResult DirectorListDevelopers()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var results = PlayerDB.Players.FindAll()
.Where(p => p.PlayerRoles != null && p.PlayerRoles.Contains(PlayerRoles.Developer))
.OrderByDescending(p => p.Player?.CreatedAt ?? DateTime.MinValue)
.Select(p => BuildPlayerSummary(p))
.ToList();
return Ok(new { Results = results, TotalResults = results.Count });
}
[HttpGet("/vannet/api/director/players/list-moderators")]
public IActionResult DirectorListModerators()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var results = PlayerDB.Players.FindAll()
.Where(p => p.PlayerRoles != null && p.PlayerRoles.Contains(PlayerRoles.Moderator))
.OrderByDescending(p => p.Player?.CreatedAt ?? DateTime.MinValue)
.Select(p => BuildPlayerSummary(p))
.ToList();
return Ok(new { Results = results, TotalResults = results.Count });
}
[HttpGet("/vannet/api/director/players/{playerId:long}")]
public async Task<IActionResult> DirectorGetPlayer(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
await ToxModService.EnsureCurrent(player);
return Ok(BuildPlayerFull(player));
}
[HttpGet("/vannet/api/director/players/{playerId:long}/rooms")]
public IActionResult DirectorGetPlayerRooms(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var rooms = RoomDB.Rooms.FindAll()
.Where(r => r.CreatorAccountId == playerId)
.OrderByDescending(r => r.Stats.VisitCount)
.Take(50)
.Select(r => new
{
roomId = r.RoomId,
name = r.Name,
imageName = r.ImageName,
stats = r.Stats,
accessibility = r.Accessibility,
state = r.State
})
.ToList();
return Ok(new { rooms });
}
[HttpGet("/vannet/api/director/players/{playerId:long}/heartbeat")]
public IActionResult DirectorGetHeartbeat(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
return Ok(player.Player?.PlayerExtra?.Heartbeat);
}
[HttpPost("/vannet/api/director/players/{playerId:long}/rename")]
public async Task<IActionResult> DirectorRenamePlayer(long playerId, [FromBody] DirectorRenameRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.Username))
return BadRequest(new { error = "Username required." });
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
string old = player.Player.Username;
player.Player.Username = body.Username;
player.Player.DisplayName = body.Username;
PlayerDB.Players.Update(player);
var updated = PlayerDB.GetAccountMe(playerId);
await NotiController.SendAccountUpdate(playerId, updated);
await NotificationsController.RefreshAccount(playerId);
return Ok(new { success = true, oldName = old, newName = body.Username });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/clear-name")]
public async Task<IActionResult> DirectorClearName(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
string? old = player.Player.Username;
string newName = NameGen.GetRandomName();
player.Player.Username = newName;
player.Player.DisplayName = newName;
PlayerDB.Players.Update(player);
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
await NotificationsController.RefreshAccount(playerId);
return Ok(new { success = true, oldName = old, newName });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/name-lock")]
public async Task<IActionResult> DirectorNameLock(long playerId, [FromBody] DirectorNameLockRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
bool locked = body?.Locked ?? true;
player.Player.NameLocked = locked;
PlayerDB.Players.Update(player);
return Ok(new { success = true, playerId, nameLocked = locked });
}
[HttpPost("/vannet/api/director/players/create")]
public IActionResult DirectorCreatePlayer([FromBody] DirectorCreatePlayerRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (body == null || body.PlayerId <= 0)
return BadRequest(new { error = "A positive PlayerId is required." });
string? username = string.IsNullOrWhiteSpace(body.Username) ? null : body.Username.Trim();
var player = PlayerDB.CreateAccountWithId(
body.PlayerId,
username,
Platforms.Standalone,
$"director-created-{Guid.NewGuid()}",
body.IsJunior,
out string? error);
if (player == null)
return BadRequest(new { error = error ?? "Failed to create account." });
return Ok(new { success = true, playerId = player.PlayerId, username = player.Player?.Username });
}
[HttpPost("/vannet/api/director/announcement")]
public async Task<IActionResult> DirectorSetAnnouncement()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
DirectorAnnouncementRequest body;
try
{
var options = new JsonSerializerOptions
{
Converters = { new JsonStringEnumConverter() },
PropertyNameCaseInsensitive = true
};
body = await JsonSerializer.DeserializeAsync<DirectorAnnouncementRequest>(Request.Body, options);
}
catch (JsonException ex)
{
return BadRequest(new { error = "Invalid request body.", detail = ex.Message });
}
if (body?.Clear == true)
{
await AnnoucementDB.DeleteAllAnnouncements(sendWs: true);
return Ok(new { success = true, cleared = true });
}
if (string.IsNullOrWhiteSpace(body?.Title) && string.IsNullOrWhiteSpace(body?.Body))
return BadRequest(new { error = "Title or body required." });
var announcement = new AnnoucementDBClasses.DirectorAnnouncementRequest
{
AnnouncementType = body.AnnouncementType,
Body = body.Body ?? "",
ImageName = body.ImageName ?? "",
LinkName = body.LinkName ?? "",
LinkType = body.LinkType,
LinkUri = body.LinkUri ?? "",
Title = body.Title ?? ""
};
bool created = await AnnoucementDB.CreateAnnouncement(announcement, sendWs: true);
if (!created)
return BadRequest(new { error = "Failed to create announcement." });
return Ok(new { success = true });
}
[HttpGet("/vannet/api/director/announcements")]
public IActionResult DirectorGetAnnouncements()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
return Ok(AnnoucementDB.GetAllAnnouncements());
}
[HttpPost("/vannet/api/director/announcement/delete/{announcementId:long}")]
public async Task<IActionResult> DirectorDeleteAnnouncement(long announcementId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
bool deleted = AnnoucementDB.DeleteAnnouncement(announcementId);
if (!deleted)
return NotFound(new { error = "Announcement not found." });
await NotificationsController.SendAnnoucementDelete(announcementId);
return Ok(new { success = true, announcementId });
}
[HttpGet("/vannet/api/director/community-banner")]
public IActionResult DirectorGetCommunityBanner()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "communityboard.json");
if (!System.IO.File.Exists(path))
return Ok(new { message = "", moreInfoUrl = "" });
var ann = System.Text.Json.Nodes.JsonNode.Parse(System.IO.File.ReadAllText(path))?["CurrentAnnouncement"];
return Ok(new
{
message = ann?["Message"]?.GetValue<string>() ?? "",
moreInfoUrl = ann?["MoreInfoUrl"]?.GetValue<string>() ?? ""
});
}
[HttpPost("/vannet/api/director/community-banner")]
public async Task<IActionResult> DirectorSetCommunityBanner([FromBody] DirectorCommunityBannerRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "communityboard.json");
if (!System.IO.File.Exists(path))
return NotFound(new { error = "communityboard.json not found." });
if (System.Text.Json.Nodes.JsonNode.Parse(System.IO.File.ReadAllText(path)) is not System.Text.Json.Nodes.JsonObject root)
return StatusCode(500, new { error = "Malformed communityboard.json." });
root["CurrentAnnouncement"] = new System.Text.Json.Nodes.JsonObject
{
["Message"] = body?.Message ?? "",
["MoreInfoUrl"] = body?.MoreInfoUrl ?? ""
};
string json = root.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
System.IO.File.WriteAllText(path, json);
var cbPayload = new { Id = "CommunityBoardUpdate", Msg = System.Text.Json.JsonSerializer.Deserialize<object>(json) };
await NotificationsController.SendToAll(System.Text.Json.JsonSerializer.Serialize(cbPayload));
return Ok(new { success = true });
}
[HttpGet("/vannet/api/director/git/diff")]
public async Task<IActionResult> DirectorGitDiff()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
const string repoPath = "/home/container/VanaNet";
if (!Directory.Exists(repoPath))
return NotFound(new { error = "Repo path not found.", path = repoPath });
try
{
async Task<(int ExitCode, string StdOut, string StdErr)> RunGit(string args)
{
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = args,
WorkingDirectory = repoPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null)
throw new InvalidOperationException("Failed to start git process.");
string stdOut = await process.StandardOutput.ReadToEndAsync();
string stdErr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return (process.ExitCode, stdOut, stdErr);
}
var repoTop = await RunGit("rev-parse --show-toplevel");
var branch = await RunGit("rev-parse --abbrev-ref HEAD");
var status = await RunGit("status --porcelain");
var diff = await RunGit("diff HEAD");
var outBuilder = new StringBuilder();
if (!string.IsNullOrWhiteSpace(diff.StdOut))
{
outBuilder.Append(diff.StdOut.TrimEnd());
}
else
{
outBuilder.AppendLine("No diff output from 'git diff HEAD'.");
outBuilder.AppendLine("This usually means there are no unstaged tracked file changes in that repo.");
}
outBuilder.AppendLine();
outBuilder.AppendLine("[debug]");
outBuilder.AppendLine($"configuredRepoPath: {repoPath}");
outBuilder.AppendLine($"gitTopLevel: {(string.IsNullOrWhiteSpace(repoTop.StdOut) ? "(none)" : repoTop.StdOut.Trim())}");
outBuilder.AppendLine($"branch: {(string.IsNullOrWhiteSpace(branch.StdOut) ? "(unknown)" : branch.StdOut.Trim())}");
if (string.IsNullOrWhiteSpace(status.StdOut))
outBuilder.AppendLine("statusPorcelain: (clean)");
else
outBuilder.AppendLine($"statusPorcelain:\n{status.StdOut.TrimEnd()}");
if (!string.IsNullOrWhiteSpace(diff.StdErr))
outBuilder.AppendLine($"diffStderr:\n{diff.StdErr.TrimEnd()}");
if (repoTop.ExitCode != 0 && !string.IsNullOrWhiteSpace(repoTop.StdErr))
outBuilder.AppendLine($"revParseTopLevelStderr:\n{repoTop.StdErr.TrimEnd()}");
if (branch.ExitCode != 0 && !string.IsNullOrWhiteSpace(branch.StdErr))
outBuilder.AppendLine($"revParseBranchStderr:\n{branch.StdErr.TrimEnd()}");
if (status.ExitCode != 0 && !string.IsNullOrWhiteSpace(status.StdErr))
outBuilder.AppendLine($"statusStderr:\n{status.StdErr.TrimEnd()}");
bool allSucceeded = repoTop.ExitCode == 0 && branch.ExitCode == 0 && status.ExitCode == 0 && diff.ExitCode == 0;
return Ok(new
{
success = allSucceeded,
exitCode = diff.ExitCode,
output = outBuilder.ToString()
});
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to run git diff.", detail = ex.Message });
}
}
[HttpGet("/vannet/api/director/git/status")]
public async Task<IActionResult> DirectorGitStatus()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
const string repoPath = "/home/container/VanaNet";
if (!Directory.Exists(repoPath))
return NotFound(new { error = "Repo path not found.", path = repoPath });
var psi = new ProcessStartInfo
{
FileName = "git",
Arguments = "log --branches --not --remotes -p @{u}..",
WorkingDirectory = repoPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
try
{
using var process = Process.Start(psi);
if (process == null)
return StatusCode(500, new { error = "Failed to start git process." });
string stdout = await process.StandardOutput.ReadToEndAsync();
string stderr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
string output = string.IsNullOrWhiteSpace(stderr)
? stdout
: string.IsNullOrWhiteSpace(stdout)
? stderr
: stdout + "\n\n[stderr]\n" + stderr;
if (string.IsNullOrWhiteSpace(output))
output = "No status output.";
return Ok(new
{
success = process.ExitCode == 0,
exitCode = process.ExitCode,
output = output
});
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to run git status.", detail = ex.Message });
}
}
[HttpGet("/vannet/api/director/git/push")]
public async Task<IActionResult> DirectorGitPush()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
const string repoPath = "/home/container/VanaNet";
if (!Directory.Exists(repoPath))
return NotFound(new { error = "Repo path not found.", path = repoPath });
var psi = new ProcessStartInfo
{
FileName = "bash",
Arguments = $"-c \"cd {repoPath} && git add -A && git commit -m 'Director Commit' && git push https://ada:Avoacado1!@ada.drewcotech.com/vanadium/vanadium HEAD:master\"",
WorkingDirectory = repoPath,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
try
{
using var process = Process.Start(psi);
if (process == null)
return StatusCode(500, new { error = "Failed to start git process." });
string stdout = await process.StandardOutput.ReadToEndAsync();
string stderr = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
string output = string.IsNullOrWhiteSpace(stderr)
? stdout
: string.IsNullOrWhiteSpace(stdout)
? stderr
: stdout + "\n\n[stderr]\n" + stderr;
if (string.IsNullOrWhiteSpace(output))
output = "No push output.";
return Ok(new
{
success = process.ExitCode == 0,
exitCode = process.ExitCode,
output = output
});
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Failed to run git push.", detail = ex.Message });
}
}
private static string RRPlusPath => Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "RRPlus.txt");
public static HashSet<long> ReadRRPlusIds()
{
var ids = new HashSet<long>();
if (!System.IO.File.Exists(RRPlusPath)) return ids;
foreach (var line in System.IO.File.ReadAllLines(RRPlusPath))
if (long.TryParse(line.Trim(), out var id))
ids.Add(id);
return ids;
}
[HttpPost("/vannet/api/director/players/{playerId:long}/rrplus")]
public async Task<IActionResult> DirectorSetRRPlus(long playerId, [FromBody] DirectorRRPlusRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
bool enabled = body?.Enabled ?? true;
var ids = ReadRRPlusIds();
if (enabled) ids.Add(playerId);
else ids.Remove(playerId);
Directory.CreateDirectory(Path.GetDirectoryName(RRPlusPath)!);
System.IO.File.WriteAllLines(RRPlusPath, ids.Select(i => i.ToString()));
await NotificationsController.RefreshAccount(playerId);
return Ok(new { success = true, playerId, recRoomPlus = enabled });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/set-bio")]
public async Task<IActionResult> DirectorSetBio(long playerId, [FromBody] DirectorBioRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
player.Player.Bio = body?.Bio ?? "";
PlayerDB.Players.Update(player);
var updated = PlayerDB.GetAccountMe(playerId);
await NotiController.SendAccountUpdate(playerId, updated);
await NotificationsController.RefreshAccount(playerId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/set-password")]
public async Task<IActionResult> DirectorSetPassword(long playerId, [FromBody] DirectorPasswordRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
PlayerDB.PasswordManager.ChangePassword(player.PlayerId, body?.Password ?? "");
var updated = PlayerDB.GetAccountMe(playerId);
await NotiController.SendAccountUpdate(playerId, updated);
await NotificationsController.RefreshAccount(playerId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/upload-pfp")]
public async Task<IActionResult> DirectorUploadPlayerPfp(long playerId, [FromForm] IFormFile file)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null) return NotFound("");
if (file == null || file.Length == 0)
return BadRequest(new { error = "No file provided." });
string ext = Path.GetExtension(file.FileName).ToLowerInvariant();
if (ext != ".jpg" && ext != ".jpeg" && ext != ".png")
return BadRequest(new { error = "Only JPG and PNG files are allowed." });
string fileName = $"pfp_{playerId}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
string savePath = Path.Combine(Environment.CurrentDirectory, "Data", "Images", fileName);
Directory.CreateDirectory(Path.GetDirectoryName(savePath)!);
using (var stream = new FileStream(savePath, FileMode.Create))
await file.CopyToAsync(stream);
var db = ImageMetadataDB.GetDb();
var imageCol = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
imageCol.Insert(new ImageMetadataDB.FullSavedImage
{
PlayerId = (ulong)playerId,
ImageName = fileName
});
player.Player.ProfileImage = fileName;
PlayerDB.Players.Update(player);
var updated = PlayerDB.GetAccountMe(playerId);
await NotificationsController.RefreshAccount(playerId);
return Ok(new { success = true, imageName = fileName });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/upload-image")]
public async Task<IActionResult> DirectorUploadRoomImage(long roomId, [FromForm] IFormFile file)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null) return NotFound("");
if (file == null || file.Length == 0)
return BadRequest(new { error = "No file provided." });
string ext = Path.GetExtension(file.FileName).ToLowerInvariant();
if (ext != ".jpg" && ext != ".jpeg" && ext != ".png")
return BadRequest(new { error = "Only JPG and PNG files are allowed." });
string fileName = $"room_{roomId}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}{ext}";
string savePath = Path.Combine(Environment.CurrentDirectory, "Data", "Images", fileName);
Directory.CreateDirectory(Path.GetDirectoryName(savePath)!);
using (var stream = new FileStream(savePath, FileMode.Create))
await file.CopyToAsync(stream);
room.ImageName = fileName;
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true, imageName = fileName });
}
[HttpPost("/vannet/api/director/players/shove-all")]
public async Task<IActionResult> DirectorShoveAll([FromBody] DirectorShoveAllRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.GetRoom(body.RoomId);
if (room == null) return NotFound(new { error = "Room not found." });
var onlinePlayers = PlayerDB.Players.FindAll()
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
.ToList();
int count = 0;
foreach (var fp in onlinePlayers)
{
var heartbeat = Sessions.CreateRoom(fp.PlayerId, body.RoomId, body.SubRoomId, joinMode: 0, additionalPlayersAutoFollow: false);
if (!string.IsNullOrWhiteSpace(body.InstanceId) && heartbeat.roomInstance != null)
heartbeat.roomInstance.photonRoomId = body.InstanceId;
fp.Player.PlayerExtra ??= new PlayerExtra();
fp.Player.PlayerExtra.Heartbeat = heartbeat;
PlayerDB.Players.Update(fp);
await NotiController.SendPresenceUpdate(fp.PlayerId, heartbeat);
count++;
}
return Ok(new { success = true, playersShoved = count });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/set-pfp")]
public async Task<IActionResult> DirectorSetPfp(long playerId, [FromBody] DirectorSetPfpRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.ImageName))
return BadRequest(new { error = "ImageName required." });
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
player.Player.ProfileImage = body.ImageName;
PlayerDB.Players.Update(player);
var updated = PlayerDB.GetAccountMe(playerId);
await NotificationsController.RefreshAccount(playerId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/reset-pfp")]
public async Task<IActionResult> DirectorResetPfp(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
player.Player.ProfileImage = "DefaultPFP.png";
PlayerDB.Players.Update(player);
var updated = PlayerDB.GetAccountMe(playerId);
await NotificationsController.RefreshAccount(playerId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/set-level")]
public async Task<IActionResult> DirectorSetLevel(long playerId, [FromBody] DirectorSetLevelRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
int oldLevel = player.Player.Level;
int oldXP = player.Player.XP;
int newXP = body.Xp.HasValue ? body.Xp.Value : PlayerDB.GetXPForLevel(body.Level);
player.Player.Level = body.Level;
player.Player.XP = newXP;
PlayerDB.Players.Update(player);
var json = JsonSerializer.Serialize(new
{
Id = NotificationsController.EventTypes.PlayerProgressionLevelUpdate,
Msg = new
{
PlayerId = playerId,
Level = body.Level,
XP = newXP
}
});
await NotificationsController.SendToAll(json);
return Ok(new { success = true, oldLevel, newLevel = body.Level, oldXP, newXP });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/set-junior")] // removed message because it unessecary lowkey
public async Task<IActionResult> DirectorSetJunior(long playerId, [FromBody] DirectorSetJuniorRequest body)
{
if (!ValidateDirectorToken(out var session)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
player.Player ??= new Player();
player.Player.PlayerExtra ??= new PlayerExtra();
bool wasJunior = player.Player.IsJunior == true;
bool isJunior = body?.IsJunior ?? false;
player.Player.IsJunior = isJunior;
string? message = null;
if (isJunior)
{
string? reason = string.IsNullOrWhiteSpace(body?.Reason) ? null : body.Reason.Trim();
player.Player.PlayerExtra.JuniorStatus = new JuniorStatusData
{
Reason = reason,
SetBy = session?.DiscordUserId,
SetAt = DateTime.UtcNow
};
if (!wasJunior)
{
string newName = NameGen.GetRandomName();
player.Player.Username = newName;
player.Player.DisplayName = newName;
player.Player.Bio = "";
}
message = reason != null
? $"Your communication privileges have been restricted: {reason}. Contact a moderator to appeal."
: "Your communication privileges have been restricted by a moderator. Contact a moderator to appeal.";
}
else
{
player.Player.PlayerExtra.JuniorStatus = null;
if (wasJunior)
message = "Your communication privileges have been restored.";
}
PlayerDB.Players.Update(player);
await NotificationsController.RefreshAccount(playerId);
if (message != null)
{
// await SendCoachPopup(playerId, message);
await NotificationsController.CreateAndSendWSMessageRecieved(
playerId, 1, MessageType.TextMessage, message);
}
return Ok(new { success = true, playerId, isJunior });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/coach-dm")]
public async Task<IActionResult> DirectorCoachDm(long playerId, [FromBody] DirectorCoachDmRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.Message))
return BadRequest(new { error = "Message required." });
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
bool sent = await NotificationsController.CreateAndSendWSMessageRecieved(
playerId, 1, MessageType.TextMessage, body.Message);
return Ok(new { success = sent, playerId });
}
[HttpGet("/vannet/api/director/players/{playerId:long}/toxmod")]
public async Task<IActionResult> DirectorGetToxModStatus(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
await ToxModService.EnsureCurrent(player);
return Ok(ToxModService.BuildStatus(player));
}
[HttpPost("/vannet/api/director/players/{playerId:long}/toxmod/ban")]
public async Task<IActionResult> DirectorToxModBan(long playerId, [FromBody] DirectorToxModBanRequest body)
{
if (!ValidateDirectorToken(out var session)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
await ToxModService.IssueVoiceBan(player, body?.Category, body?.Note, body?.DurationSeconds, session?.DiscordUserId);
if (body?.Loud == true)
{
string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject(
Vanadium.Classes.WebSocket.WebsocketEvents.CreateWSEventString("ModerationQuitGame", new { }));
await NotificationsController.SendToPlayer(playerId, wsEvent);
}
return Ok(new { success = true, status = ToxModService.BuildStatus(player) });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/toxmod/unban")]
public async Task<IActionResult> DirectorToxModUnban(long playerId, [FromBody] DirectorToxModUnbanRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
await ToxModService.LiftVoiceBan(player, body?.ResetStrikes ?? false);
return Ok(new { success = true, status = ToxModService.BuildStatus(player) });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/set-platform-id")]
public IActionResult DirectorSetPlatformId(long playerId, [FromBody] DirectorSetPlatformIdRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (!ulong.TryParse(body?.PlatformId, out ulong newPid))
return BadRequest(new { error = "Invalid platform ID." });
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
if (player.PlatformIds == null || player.PlatformIds.Count == 0)
return BadRequest(new { error = "No platform IDs on this player." });
string old = player.PlatformIds[0].PlatformId;
player.PlatformIds[0].PlatformId = body.PlatformId;
PlayerDB.Players.Update(player);
return Ok(new { success = true, oldPlatformId = old, newPlatformId = body.PlatformId });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/heartbeat-clear")]
public async Task<IActionResult> DirectorHeartbeatClear(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Heartbeat = new Heartbeat
{
playerId = playerId,
isOnline = false,
roomInstance = null,
errorCode = 0
};
PlayerDB.Players.Update(player);
await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/shove")]
public async Task<IActionResult> DirectorShove(long playerId, [FromBody] DirectorShoveRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound(new { error = "Player not found." });
var room = RoomDB.GetRoom(body.RoomId);
if (room == null)
return NotFound(new { error = "Room not found." });
var heartbeat = Sessions.CreateRoom(playerId, body.RoomId, body.SubRoomId, joinMode: 0, additionalPlayersAutoFollow: false);
if (!string.IsNullOrWhiteSpace(body.InstanceId) && heartbeat.roomInstance != null)
heartbeat.roomInstance.photonRoomId = body.InstanceId;
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Heartbeat = heartbeat;
PlayerDB.Players.Update(player);
await NotiController.SendPresenceUpdate(playerId, heartbeat);
return Ok(new { success = true, roomId = body.RoomId });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/force-bring")]
public async Task<IActionResult> DirectorForceBring(long playerId, [FromBody] DirectorForceBringRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.Username))
return BadRequest(new { error = "Input field for username" });
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
var target = PlayerDB.Players.FindAll()
.FirstOrDefault(p => string.Equals(p.Player?.Username, body.Username, StringComparison.OrdinalIgnoreCase));
if (target == null)
return NotFound(new { error = "No user found with this username!" });
var targetHb = target.Player?.PlayerExtra?.Heartbeat;
if (targetHb?.roomInstance == null)
return BadRequest(new { error = "This user isnt online" });
var heartbeat = Sessions.CreateRoom(playerId, targetHb.roomInstance.roomId, targetHb.roomInstance.subRoomId, joinMode: 0, additionalPlayersAutoFollow: false);
if (heartbeat.roomInstance != null)
{
heartbeat.roomInstance.photonRoomId = targetHb.roomInstance.photonRoomId;
heartbeat.roomInstance.photonRegion = targetHb.roomInstance.photonRegion;
heartbeat.roomInstance.roomInstanceId = targetHb.roomInstance.roomInstanceId;
}
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Heartbeat = heartbeat;
PlayerDB.Players.Update(player);
await NotificationsController.RefreshHeartbeat(playerId, heartbeat);
return Ok(new { success = true, roomId = targetHb.roomInstance.roomId });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/transfer-rooms")]
public async Task<IActionResult> DirectorTransferRooms(long playerId, [FromBody] DirectorTransferRoomsRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var oldPlayer = PlayerDB.Players.FindById(playerId);
var newPlayer = PlayerDB.Players.FindById(body.NewPlayerId);
if (oldPlayer == null)
return NotFound(new { error = "Source player not found." });
if (newPlayer == null)
return NotFound(new { error = "Target player not found." });
var rooms = RoomDB.Rooms.Find(r => r.CreatorAccountId == playerId).ToList();
foreach (var room in rooms)
{
room.CreatorAccountId = body.NewPlayerId;
room.Roles ??= new List<Roles>();
room.Roles.RemoveAll(r => r.AccountId == playerId && r.Role == Role.Creator);
room.Roles.RemoveAll(r => r.AccountId == body.NewPlayerId && r.Role == Role.Creator);
room.Roles.Add(new Roles { AccountId = body.NewPlayerId, Role = Role.Creator, InvitedRole = Role.Creator, LastChangedByAccountId = body.NewPlayerId });
if (body.KeepCoOwner)
{
room.Roles.RemoveAll(r => r.AccountId == playerId);
room.Roles.Add(new Roles { AccountId = playerId, Role = Role.CoOwner, InvitedRole = Role.CoOwner, LastChangedByAccountId = body.NewPlayerId });
}
if (room.SubRooms != null)
foreach (var sub in room.SubRooms)
{
sub.CreatorAccountId = body.NewPlayerId;
sub.SavedByAccountId = body.NewPlayerId;
}
RoomDB.Rooms.Update(room);
}
var onlinePlayers = PlayerDB.Players.FindAll()
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
.Select(p => p.PlayerId)
.ToList();
foreach (var pid in onlinePlayers)
foreach (var r in rooms)
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(r.RoomId));
return Ok(new { success = true, roomsTransferred = rooms.Count });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/repair")]
public async Task<IActionResult> DirectorRepair(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
var now = DateTime.UtcNow;
if (player.Player.CreatedAt == default || player.Player.CreatedAt.Year == 1) player.Player.CreatedAt = now;
if (player.Player.LastLoginAt == default || player.Player.LastLoginAt.Year == 1) player.Player.LastLoginAt = now;
player.Player.IsJunior ??= false;
if (string.IsNullOrWhiteSpace(player.Player.ProfileImage)) player.Player.ProfileImage = "DefaultPFP.png";
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Avatar ??= new Avatar();
player.Player.PlayerExtra.Influencer ??= new Influencer();
player.Player.PlayerExtra.Heartbeat ??= new Heartbeat();
player.Player.PlayerExtra.Heartbeat.isOnline = false;
player.Player.PlayerExtra.Heartbeat.roomInstance = null;
player.Player.PlayerExtra.Heartbeat.errorCode = 0;
if (player.Player.PlayerExtra.DormRoomId == 0)
{
bool dormExists = RoomDB.DoesPlayerDormExist(player.PlayerId);
if (!dormExists)
{
long newDormId = RoomDB.CloneDormRoom(player.PlayerId);
player.Player.PlayerExtra.DormRoomId = newDormId;
}
else
{
var dorm = RoomDB.GetPlayerDormRoom(player.PlayerId);
if (dorm != null) player.Player.PlayerExtra.DormRoomId = dorm.RoomId;
}
}
PlayerDB.Players.Update(player);
var repaired = PlayerDB.GetAccountMe(playerId);
await NotificationsController.RefreshAccount(playerId);
await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat);
return Ok(new { success = true, dormRoomId = player.Player.PlayerExtra.DormRoomId });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/delete-all-images")]
public IActionResult DirectorDeleteAllImages(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
var db = ImageMetadataDB.GetDb();
var imageCol = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var images = imageCol.Query().Where(x => x.PlayerId == (ulong)playerId).ToList();
int deletedFiles = 0;
foreach (var image in images)
{
try
{
string path = Path.Combine("Data", "Images", image.ImageName);
if (System.IO.File.Exists(path))
{
System.IO.File.Delete(path);
deletedFiles++;
}
}
catch { }
}
ImageMetadataDB.DeleteImagesFromPlayerId((ulong)playerId);
return Ok(new { success = true, deletedRecords = images.Count, deletedFiles });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/delete-all-rooms")]
public IActionResult DirectorDeleteAllRooms(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
var rooms = RoomDB.Rooms.Find(r => r.CreatorAccountId == playerId).ToList();
int deletedSubroomSaves = 0;
int deletedRooms = 0;
foreach (var room in rooms)
{
try
{
RoomDB.SubRoomSaves.DeleteMany(s => s.RoomId == room.RoomId);
deletedSubroomSaves++;
if (RoomDB.Rooms.Delete(room.RoomId))
{
deletedRooms++;
}
}
catch { }
}
return Ok(new {
success = true,
deletedRecords = rooms.Count,
deletedFiles = deletedRooms,
deletedSaves = deletedSubroomSaves
});
}
[HttpDelete("/vannet/api/director/players/{playerId:long}/delete")]
public IActionResult DirectorDeletePlayer(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
PlayerDB.Players.Delete(playerId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/roles")]
public async Task<IActionResult> DirectorPlayerRoles(long playerId, [FromBody] DirectorRoleRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.Role) || string.IsNullOrWhiteSpace(body.Action))
return BadRequest(new { error = "Role and action required." });
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
if (!Enum.TryParse<PlayerRoles>(body.Role, out var role))
return BadRequest(new { error = "Invalid role." });
player.PlayerRoles ??= new List<PlayerRoles>();
if (body.Action == "add" && !player.PlayerRoles.Contains(role))
player.PlayerRoles.Add(role);
else if (body.Action == "remove")
player.PlayerRoles.Remove(role);
PlayerDB.Players.Update(player);
var updated = PlayerDB.GetAccountMe(playerId);
await NotiController.SendAccountUpdate(playerId, updated);
return Ok(new { success = true, roles = player.PlayerRoles.Select(r => r.ToString()).ToList() });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/influencer")]
public IActionResult DirectorPlayerInfluencer(long playerId, [FromBody] DirectorInfluencerRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
bool isAdd = body?.Action == "add";
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Influencer ??= new Influencer();
player.Player.PlayerExtra.Influencer.IsInfluencer = isAdd;
PlayerDB.Players.Update(player);
return Ok(new { success = true, isInfluencer = isAdd });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/ban")]
public async Task<IActionResult> DirectorBanPlayer(long playerId, [FromBody] DirectorBanRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (playerId == 1305)
return Ok(new { success = true });
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
var mbd = new ModerationBlockDetails
{
IsBan = true,
ReportCategory = ReportCategory.Moderator,
Duration = body?.Duration ?? 2147483647,
Message = body?.Reason ?? "Banned by admin",
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.ModerationBlockDetails = mbd;
player.Player.PlayerExtra.Heartbeat = new Heartbeat
{
playerId = playerId,
isOnline = false,
roomInstance = null,
errorCode = 0
};
PlayerDB.Players.Update(player);
await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat);
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
await NotificationsController.SendBanned(playerId, mbd);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/unban")]
public async Task<IActionResult> DirectorUnbanPlayer(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (playerId == 1305)
return Ok(new { success = true });
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.ModerationBlockDetails = new ModerationBlockDetails { IsBan = false };
var mbd = new ModerationBlockDetails
{
IsBan = false,
ReportCategory = ReportCategory.Moderator,
Duration = 0,
Message = "Unbanned",
ModerationSetUnixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
PlayerDB.Players.Update(player);
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
await NotificationsController.SendBanned(playerId, mbd);
return Ok(new { success = true });
}
/// <summary>
/// Machines matching a hwid or an account, with everyone linked to them.
/// <para>
/// This is the screen a moderator is meant to read before banning anything. A machine
/// id is shared by everyone who has played on that computer, so the linked account
/// list is not a detail - it is the decision. Eleven accounts behind one hwid is a
/// different call from one, and the panel puts that in front of them first.
/// </para>
/// </summary>
[HttpGet("/vannet/api/director/anticheat/machines")]
public IActionResult DirectorMachineLookup([FromQuery] string? hwid, [FromQuery] long? playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var records = new List<HwidDBClasses.HwidRecord>();
if (!string.IsNullOrWhiteSpace(hwid))
{
var single = HwidDB.GetRecord(hwid.Trim());
if (single != null)
records.Add(single);
}
else if (playerId != null)
{
records.AddRange(HwidDB.RecordsForPlayer(playerId.Value));
}
else
{
return BadRequest(new { error = "Provide either hwid or playerId." });
}
return Ok(records.Select(ProjectMachine));
}
/// <summary>Every machine under an active hardware ban. The review queue.</summary>
[HttpGet("/vannet/api/director/anticheat/machines/banned")]
public IActionResult DirectorBannedMachines()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
return Ok(HwidDB.AllBanned().Select(ProjectMachine));
}
/// <summary>
/// Bans a machine, or every machine an account has been seen on, and the accounts
/// linked to them. The Discord identity behind the Director session is recorded as
/// the issuer: a hardware ban with no name attached is one nobody can review later.
/// </summary>
[HttpPost("/vannet/api/director/anticheat/machines/ban")]
public async Task<IActionResult> DirectorBanMachine([FromBody] DirectorHwidBanRequest body)
{
if (!ValidateDirectorToken(out var session)) return NotFound("");
if (body == null || string.IsNullOrWhiteSpace(body.Reason))
return BadRequest(new { error = "A reason is required." });
if (body.DurationDays is <= 0)
return BadRequest(new { error = "DurationDays must be positive, or blank for permanent." });
DateTime? expiresAt = body.DurationDays == null
? null
: DateTime.UtcNow.AddDays(body.DurationDays.Value);
string issuedBy = $"director:{session?.DiscordUserId ?? "unknown"}";
List<string> hwids;
List<long> accounts;
if (!string.IsNullOrWhiteSpace(body.Hwid))
{
var banned = HwidDB.BanHwid(body.Hwid.Trim(), body.Reason, issuedBy, expiresAt);
if (banned == null)
return NotFound(new { error = "That machine has never checked in." });
hwids = new List<string> { body.Hwid.Trim() };
accounts = banned;
}
else if (body.PlayerId != null)
{
(hwids, accounts) = HwidDB.BanMachinesOfPlayer(body.PlayerId.Value, body.Reason, issuedBy, expiresAt);
if (hwids.Count == 0)
return NotFound(new { error = "No machines on record for that player." });
}
else
{
return BadRequest(new { error = "Provide either Hwid or PlayerId." });
}
// HwidDB applies the account bans; pushing them out is the Director job, so a
// player caught by a machine ban drops now rather than at their next login.
foreach (var id in accounts)
await PushBanToClient(id);
return Ok(new { success = true, hwids, bannedAccounts = accounts, expiresAt });
}
/// <summary>
/// Lifts a hardware ban. The accounts banned alongside it stay banned on purpose:
/// some were banned for their own behaviour before the machine ever came up, and
/// sweeping those back in would undo moderation nobody asked to undo. The panel says
/// so, and those accounts are unbanned individually from the Players screen.
/// </summary>
[HttpPost("/vannet/api/director/anticheat/machines/unban")]
public IActionResult DirectorUnbanMachine([FromBody] DirectorHwidUnbanRequest body)
{
if (!ValidateDirectorToken(out var session)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.Hwid))
return BadRequest(new { error = "Hwid is required." });
string liftedBy = $"director:{session?.DiscordUserId ?? "unknown"}";
if (!HwidDB.UnbanHwid(body.Hwid.Trim(), liftedBy))
return NotFound(new { error = "No active hardware ban on that machine." });
return Ok(new { success = true });
}
/// <summary>
/// Drops an already-banned account offline and tells its client why. Mirrors what
/// DirectorBanPlayer does after writing the ban, so a machine ban and a manual ban
/// land identically from the client side.
/// </summary>
private async Task PushBanToClient(long playerId)
{
var player = PlayerDB.Players.FindById(playerId);
var mbd = player?.Player?.PlayerExtra?.ModerationBlockDetails;
if (player == null || mbd == null)
return;
player.Player.PlayerExtra.Heartbeat = new Heartbeat
{
playerId = playerId,
isOnline = false,
roomInstance = null,
errorCode = 0
};
PlayerDB.Players.Update(player);
await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat);
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
await NotificationsController.SendBanned(playerId, mbd);
}
/// <summary>
/// Shapes one machine for the panel. Resolves linked accounts to names and ban state
/// here rather than making the browser fan out one request per player.
/// </summary>
private static object ProjectMachine(HwidDBClasses.HwidRecord record)
{
var now = DateTime.UtcNow;
return new
{
hwid = record.Hwid,
firstSeen = record.FirstSeen,
lastSeen = record.LastSeen,
banned = record.BanIsActive(now),
banReason = record.BanReason,
bannedBy = record.BannedBy,
bannedAt = record.BannedAt,
banExpiresAt = record.BanExpiresAt,
players = (record.PlayerIds ?? new List<long>()).Select(id =>
{
var p = PlayerDB.Players.FindById(id);
return new
{
playerId = id,
username = p?.Player?.Username,
profileImage = p?.Player?.ProfileImage,
banned = p?.Player?.PlayerExtra?.ModerationBlockDetails?.IsBan == true
};
}).ToList()
};
}
[HttpPost("/vannet/api/director/players/close-all")] // does not apply to devs
public async Task<IActionResult> DirectorCrashAll()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var onlinePlayers = PlayerDB.Players.FindAll()
.Where(p =>
p.Player?.PlayerExtra?.Heartbeat?.isOnline == true &&
(p.PlayerRoles == null || !p.PlayerRoles.Contains(PlayerRoles.Developer)))
.ToList();
int count = 0;
foreach (var fp in onlinePlayers)
{
string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject(
Vanadium.Classes.WebSocket.WebsocketEvents.CreateWSEventString("ModerationQuitGame", new { }));
await NotificationsController.SendToPlayer(fp.PlayerId, wsEvent);
count++;
}
return Ok(new { success = true, playersCrashed = count });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/close-game")]
public async Task<IActionResult> DirectorCloseGame(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject(
Vanadium.Classes.WebSocket.WebsocketEvents.CreateWSEventString("ModerationQuitGame", new { }));// damn, this from the ancient days from sscs
await NotificationsController.SendToPlayer(playerId, wsEvent);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/logout")]
public async Task<IActionResult> DirectorLogoutPlayer(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
string wsEvent = Newtonsoft.Json.JsonConvert.SerializeObject(
Vanadium.Classes.WebSocket.WebsocketEvents.CreateLogoutResponse());
await NotificationsController.SendToPlayer(playerId, wsEvent);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/send-ws-hex")]
public async Task<IActionResult> DirectorSendPlayerWsHex(long playerId, [FromBody] DirectorSendWsHexRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
var hex = body?.Hex ?? string.Empty;
hex = new string(hex.Where(c => !char.IsWhiteSpace(c)).ToArray());
hex = hex.Replace("0x", "", StringComparison.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(hex))
return BadRequest(new { error = "Hex payload is required." });
if (hex.Length % 2 != 0)
return BadRequest(new { error = "Hex payload must have an even number of characters." });
if (hex.Any(c => !Uri.IsHexDigit(c)))
return BadRequest(new { error = "Hex payload contains invalid characters." });
byte[] payload;
try
{
payload = Convert.FromHexString(hex);
}
catch
{
return BadRequest(new { error = "Hex payload could not be parsed." });
}
if (body?.Exact == true)
{
await NotificationsController.SendRawToPlayer(playerId, payload);
}
else
{
await NotificationsController.SendToPlayer(playerId, Encoding.UTF8.GetString(payload));
}
return Ok(new { success = true, bytes = payload.Length, exact = body?.Exact == true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/send-ws-json")]
public async Task<IActionResult> DirectorSendPlayerWsJson(long playerId, [FromBody] DirectorSendWsJsonRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
var json = body?.Json ?? string.Empty;
if (string.IsNullOrWhiteSpace(json))
return BadRequest(new { error = "JSON payload is required." });
try
{
using var _ = JsonDocument.Parse(json);
}
catch
{
return BadRequest(new { error = "Payload is not valid JSON." });
}
await NotificationsController.SendToPlayer(playerId, json);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/send-coach-message")]
public async Task<IActionResult> DirectorPlayerCoachMessage(long playerId, [FromBody] DirectorPlayerCoachMessageRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
await SendCoachPopup(playerId, body?.Message ?? "");
return Ok(new { success = true });
}
internal static async Task SendCoachPopup(long playerId, string message)
{
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new
{
Id = "2",
Msg = new
{
FromPlayerId = 1,
Id = new Random().Next(1, 0x7ffffff),
SentTime = DateTime.UtcNow,
Type = 100,
Data = message,
RoomId = 1
}
});
await NotificationsController.SendToPlayer(playerId, json);
}
[HttpPost("/vannet/api/director/players/{playerId:long}/set-tokens")]
public async Task<IActionResult> DirectorSetTokens(long playerId, [FromBody] DirectorSetTokensRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.Currencies ??= new List<PlayerCurrency>();
var balanceType = (BalanceType)(body?.BalanceType ?? 0);
var existing = player.Player.PlayerExtra.Currencies
.FirstOrDefault(c => c.CurrencyType == CurrencyType.RecCenterTokens && c.BalanceType == balanceType);
int old = 0;
if (existing != null)
{
old = existing.Balance;
existing.Balance = body.Amount;
}
else
{
player.Player.PlayerExtra.Currencies.Add(new PlayerCurrency
{
CurrencyType = CurrencyType.RecCenterTokens,
BalanceType = balanceType,
Balance = body.Amount
});
}
PlayerDB.Players.Update(player);
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
return Ok(new { success = true, oldBalance = old, newBalance = body.Amount });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/give-skin")]
public IActionResult DirectorGiveSkin(long playerId, [FromBody] DirectorGiveSkinRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player == null)
return NotFound("");
string equipPath = Path.Join(Program.dataDir, "APIS", "Items", "Equipment.json");
if (!System.IO.File.Exists(equipPath))
return NotFound(new { error = "Equipment.json not found." });
var allItems = System.Text.Json.JsonSerializer.Deserialize<List<OwnedEquipmentItem>>(
System.IO.File.ReadAllText(equipPath),
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
var target = allItems?.FirstOrDefault(i =>
string.Equals(i.FriendlyName, body?.Skin, StringComparison.OrdinalIgnoreCase));
if (target == null)
return NotFound(new { error = "Skin not found." });
player.Player.PlayerExtra ??= new PlayerExtra();
if (player != null && player.Player != null && player.Player.PlayerExtra != null && player.Player.PlayerExtra.OwnedEquipment == null)
{
player.Player.PlayerExtra.OwnedEquipment = new List<OwnedEquipmentItem>();
}
bool owns = player.Player.PlayerExtra.OwnedEquipment
.Any(e => string.Equals(e.ModificationGuid, target.ModificationGuid, StringComparison.OrdinalIgnoreCase));
if (body.Action == "add")
{
if (owns)
return Conflict(new { error = "Player already owns this skin." });
player.Player.PlayerExtra.OwnedEquipment.Add(new OwnedEquipmentItem
{
PrefabName = target.PrefabName,
ModificationGuid = target.ModificationGuid,
FriendlyName = target.FriendlyName,
PlatformMask = target.PlatformMask,
Tooltip = target.Tooltip,
Rarity = target.Rarity,
Favorited = false
});
}
else
{
if (!owns)
return NotFound(new { error = "Player does not own this skin." });
player.Player.PlayerExtra.OwnedEquipment.RemoveAll(e =>
string.Equals(e.ModificationGuid, target.ModificationGuid, StringComparison.OrdinalIgnoreCase));
}
PlayerDB.Players.Update(player);
return Ok(new { success = true });
}
[HttpGet("/vannet/api/director/skins")]
public IActionResult DirectorGetSkins()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
string path = Path.Join(Program.dataDir, "APIS", "Items", "Equipment.json");
if (!System.IO.File.Exists(path))
return Ok(new List<object>());
var items = System.Text.Json.JsonSerializer.Deserialize<List<OwnedEquipmentItem>>(
System.IO.File.ReadAllText(path),
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
return Ok(items ?? new List<OwnedEquipmentItem>());
}
[HttpGet("/vannet/api/director/rooms/hot")]
public IActionResult DirectorHotRooms([FromQuery] int skip = 0, [FromQuery] int take = 50)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var rooms = RoomDB.Rooms.FindAll()
.Where(r => !r.IsDorm)
.OrderByDescending(r => r.Stats.CheerCount)
.Skip(skip)
.Take(take)
.Select(r => BuildRoomSummary(r))
.ToList();
return Ok(new { Results = rooms, TotalResults = rooms.Count });
}
[HttpGet("/vannet/api/director/rooms/search")]
public IActionResult DirectorSearchRooms([FromQuery] string? q, [FromQuery] int skip = 0, [FromQuery] int take = 50)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
IEnumerable<Room> query = RoomDB.Rooms.FindAll().Where(r => !r.IsDorm);
if (!string.IsNullOrWhiteSpace(q))
{
string lower = q.ToLowerInvariant();
bool isId = long.TryParse(q, out long searchId);
query = query.Where(r =>
(isId && r.RoomId == searchId) ||
(r.Name != null && r.Name.ToLower().Contains(lower)) ||
(r.Name != null && r.Name.ToLower().Contains(lower)));
}
var results = query
.OrderByDescending(r => r.Stats.VisitCount)
.Skip(skip)
.Take(take)
.Select(r => BuildRoomSummary(r))
.ToList();
return Ok(new { Results = results, TotalResults = results.Count });
}
[HttpGet("/vannet/api/director/rooms/{roomId:long}")]
public IActionResult DirectorGetRoom(long roomId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.GetRoom(roomId);
if (room == null)
return NotFound("");
var creator = PlayerDB.Players.FindById(room.CreatorAccountId);
return Ok(new
{
roomId = room.RoomId,
name = room.Name,
description = room.Description,
imageName = room.ImageName,
creatorAccountId = room.CreatorAccountId,
creatorUsername = creator?.Player?.Username,
stats = room.Stats,
maxPlayers = room.MaxPlayers,
ageRating = room.AgeRating,
createdAt = room.CreatedAt,
publishedAt = room.PublishedAt,
tags = room.Tags,
subRooms = room.SubRooms,
roles = room.Roles,
supportsMobile = room.SupportsMobile,
supportsQuest2 = room.SupportsQuest2,
supportsWalkVR = room.SupportsWalkVR,
supportsTeleportVR = room.SupportsTeleportVR,
supportsScreens = room.SupportsScreens,
supportsJuniors = room.SupportsJuniors,
accessibility = room.Accessibility,
state = room.State,
cloningAllowed = room.CloningAllowed,
isRRO = room.IsRRO,
isDorm = room.IsDorm,
loadScreens = room.LoadScreens,
promoImages = room.PromoImages,
warningMask = room.WarningMask
});
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-name")]
public async Task<IActionResult> DirectorSetRoomName(long roomId, [FromBody] DirectorRoomNameRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.Name))
return BadRequest(new { error = "Name required." });
var room = RoomDB.SetRoomName(roomId, body.Name);
if (room == null)
return NotFound("");
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-description")]
public async Task<IActionResult> DirectorSetRoomDescription(long roomId, [FromBody] DirectorRoomDescRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.SetRoomDescription(roomId, body?.Description ?? "");
if (room == null)
return NotFound("");
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-image")]
public async Task<IActionResult> DirectorSetRoomImage(long roomId, [FromBody] DirectorRoomImageRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.ImageName))
return BadRequest(new { error = "ImageName required." });
var room = RoomDB.SetRoomImageName(roomId, body.ImageName);
if (room == null)
return NotFound("");
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/reset-image")]
public async Task<IActionResult> DirectorResetRoomImage(long roomId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
room.ImageName = "DefaultRoomImage.jpg";
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-accessibility")]
public async Task<IActionResult> DirectorSetRoomAccessibility(long roomId, [FromBody] DirectorRoomAccessibilityRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.SetRoomAccessibility(roomId, (RoomAccessibility)body.Accessibility);
if (room == null)
return NotFound("");
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-cloning")]
public async Task<IActionResult> DirectorSetRoomCloning(long roomId, [FromBody] DirectorRoomCloningRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.SetRoomCloning(roomId, body.Allowed);
if (room == null)
return NotFound("");
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-restrictions")]
public async Task<IActionResult> DirectorSetRoomRestrictions(long roomId, [FromBody] DirectorRoomRestrictionsRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
room.SupportsScreens = body.SupportsScreens;
room.SupportsWalkVR = body.SupportsWalkVR;
room.SupportsTeleportVR = body.SupportsTeleportVR;
room.SupportsJuniors = body.SupportsJuniors;
room.SupportsMobile = body.SupportsMobile;
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-age-rating")]
public async Task<IActionResult> DirectorSetRoomAgeRating(long roomId, [FromBody] DirectorRoomAgeRatingRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
room.AgeRating = body.AgeRating;
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-max-players")]
public async Task<IActionResult> DirectorSetRoomMaxPlayers(long roomId, [FromBody] DirectorRoomMaxPlayersRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (body.MaxPlayers < 1)
return BadRequest(new { error = "MaxPlayers must be at least 1." });
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
room.MaxPlayers = body.MaxPlayers;
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/change-owner")]
public async Task<IActionResult> DirectorChangeRoomOwner(long roomId, [FromBody] DirectorChangeOwnerRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
var newOwner = PlayerDB.Players.FindById(body.NewOwnerId);
if (room == null) return NotFound(new { error = "Room not found." });
if (newOwner == null) return NotFound(new { error = "New owner not found." });
room.CreatorAccountId = body.NewOwnerId;
room.Roles ??= new List<Roles>();
room.Roles.RemoveAll(r => r.Role == Role.Creator);
room.Roles.Add(new Roles { AccountId = body.NewOwnerId, Role = Role.Creator, InvitedRole = Role.Creator, LastChangedByAccountId = body.NewOwnerId });
if (room.SubRooms != null)
foreach (var sub in room.SubRooms)
{
sub.CreatorAccountId = body.NewOwnerId;
sub.SavedByAccountId = body.NewOwnerId;
}
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-role")]
public async Task<IActionResult> DirectorSetRoomRole(long roomId, [FromBody] DirectorSetRoomRoleRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.GetRoom(roomId);
if (room == null)
return NotFound("");
room.Roles ??= new List<Roles>();
room.Roles.RemoveAll(r => r.AccountId == body.PlayerId);
var newRole = (Role)body.Role;
if (newRole != Role.None)
room.Roles.Add(new Roles { AccountId = body.PlayerId, Role = newRole, InvitedRole = Role.None, LastChangedByAccountId = 2 });
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
if (newRole == Role.CoOwner || newRole == Role.TemporaryCoOwner)
{
var coachJson = Newtonsoft.Json.JsonConvert.SerializeObject(new
{
Id = "2",
Msg = new
{
FromPlayerId = body.PlayerId,
Id = Random.Shared.Next(1, 0x7ffffff),
SentTime = DateTime.UtcNow,
Type = Vanadium.Classes.DBs.DBClasses.PlayerDBClasses.MessageType.CoachMessage,
Data = $"You were given Co-Owner inside ^{room.Name}",
RoomId = roomId,
PlayerEventId = (long?)null
}
});
await NotificationsController.SendToPlayer(body.PlayerId, coachJson);
}
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/toggle-beta")]
public async Task<IActionResult> DirectorToggleBeta(long roomId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
room.Tags ??= new List<Tags>();
var betaTag = room.Tags.FirstOrDefault(t => t.Tag.Equals("beta", StringComparison.OrdinalIgnoreCase) && (int)t.Type == 1);
bool added;
if (betaTag != null)
{
room.Tags.Remove(betaTag);
added = false;
}
else
{
room.Tags.Add(new Tags { Tag = "beta", Type = (TagType)1 });
added = true;
}
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true, betaEnabled = added });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/tags")]
public async Task<IActionResult> DirectorRoomTags(long roomId, [FromBody] DirectorRoomTagsRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
room.Tags ??= new List<Tags>();
foreach (var tag in body.Tags ?? new List<string>())
{
string t = tag.Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(t)) continue;
if (body.Action == "add")
{
if (!room.Tags.Any(x => x.Tag.Equals(t, StringComparison.OrdinalIgnoreCase)))
room.Tags.Add(new Tags { Tag = t, Type = (TagType)(body.Type ?? 0) });
}
else
{
room.Tags.RemoveAll(x => x.Tag.Equals(t, StringComparison.OrdinalIgnoreCase));
}
}
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true, tags = room.Tags.Select(t => new { tag = t.Tag, type = (int)t.Type }).ToList() });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/change-datablob")]
public async Task<IActionResult> DirectorChangeDatablob(long roomId, [FromBody] DirectorChangeDatablobRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound(new { error = "Room not found." });
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == body.SubRoomId);
if (subRoom == null)
return NotFound(new { error = "SubRoom not found." });
string datablob = Path.GetFileName(body.Datablob);
string cdnFolder = Path.Combine(Environment.CurrentDirectory, "Data", "cdn");
//Directory.CreateDirectory(cdnFolder);
string localPath = Path.Combine(cdnFolder, datablob);
string downloadStatus = "Already cached";
if (!System.IO.File.Exists(localPath))
{
try
{
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("User-Agent", "BestHTTP");
http.DefaultRequestHeaders.Add("Accept", "*/*");
http.DefaultRequestHeaders.Add("Referer", "https://rec.net/");
var response = await http.GetAsync($"https://cdn.rec.net/room/{datablob}");
if (response.IsSuccessStatusCode)
{
await System.IO.File.WriteAllBytesAsync(localPath, await response.Content.ReadAsByteArrayAsync());
downloadStatus = "Downloaded from RecNet CDN";
}
else downloadStatus = $"Failed HTTP {(int)response.StatusCode}";
}
catch (Exception ex) { downloadStatus = "Download error: " + ex.Message; }
}
var newSave = new currentSave
{
SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(),
RoomId = roomId,
SubRoomId = body.SubRoomId,
DataBlob = datablob,
Description = $"Imported via Director Panel",
SavedByAccountId = 1,
SavedOnPlatform = 0,
SavedOnDeviceClass = 1,
CreatedAt = DateTime.UtcNow
};
RoomDB.SubRoomSaves.Insert(newSave);
subRoom.DataBlob = datablob;
subRoom.CurrentSave = newSave;
subRoom.Accessibility = RoomAccessibility.Public;
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true, saveId = newSave.SubRoomDataSaveId, downloadStatus });
}
[HttpGet("/vannet/api/director/rooms/{roomId:long}/instances")]
public IActionResult DirectorGetRoomInstances(long roomId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.GetRoom(roomId);
if (room == null)
return NotFound("");
var live = PlayerDB.Players.FindAll()
.Where(p =>
p.Player?.PlayerExtra?.Heartbeat?.isOnline == true &&
p.Player.PlayerExtra.Heartbeat.roomInstance != null &&
p.Player.PlayerExtra.Heartbeat.roomInstance.roomId == roomId)
.Select(p => p.Player!.PlayerExtra!.Heartbeat!.roomInstance!)
.ToList();
var subRooms = (room.SubRooms ?? new List<SubRooms>()).Select(sub =>
{
var mine = live.Where(ri => ri.subRoomId == sub.SubRoomId).ToList();
return new
{
subRoomId = sub.SubRoomId,
publicInstances = mine.Where(ri => !ri.isPrivate).Select(ri => ri.roomInstanceId).Distinct().Count(),
publicPlayers = mine.Count(ri => !ri.isPrivate),
privateInstances = mine.Where(ri => ri.isPrivate).Select(ri => ri.roomInstanceId).Distinct().Count(),
privatePlayers = mine.Count(ri => ri.isPrivate)
};
}).ToList();
return Ok(new { roomId, subRooms });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/refresh-instances")]
public async Task<IActionResult> DirectorRefreshInstances(long roomId, [FromBody] DirectorRefreshInstancesRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.GetRoom(roomId);
if (room == null)
return NotFound(new { error = "Room not found." });
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == body.SubRoomId);
if (subRoom == null)
return NotFound(new { error = "SubRoom not found." });
var (instancesRefreshed, playersMoved) = await Sessions.RefreshPublicInstances(roomId, body.SubRoomId);
return Ok(new { success = true, instancesRefreshed, playersMoved });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-instances-privacy")]
public async Task<IActionResult> DirectorSetInstancesPrivacy(long roomId, [FromBody] DirectorSetInstancesPrivacyRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.GetRoom(roomId);
if (room == null)
return NotFound(new { error = "Room not found." });
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == body.SubRoomId);
if (subRoom == null)
return NotFound(new { error = "SubRoom not found." });
var instancesChanged = await Sessions.SetInstancesPrivacy(roomId, body.SubRoomId, body.IsPrivate);
return Ok(new { success = true, instancesChanged, isPrivate = body.IsPrivate });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/upload-datablob")]
public async Task<IActionResult> DirectorUploadDatablob(long roomId, [FromForm] long subRoomId, [FromForm] IFormFile file)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound(new { error = "Room not found." });
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
if (subRoom == null)
return NotFound(new { error = "SubRoom not found." });
if (file == null || file.Length == 0)
return BadRequest(new { error = "No file provided." });
string fileName = Path.GetFileName(file.FileName);
string cdnFolder = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "room");
Directory.CreateDirectory(cdnFolder);
string savePath = Path.Combine(cdnFolder, fileName);
using (var stream = new FileStream(savePath, FileMode.Create))
await file.CopyToAsync(stream);
var newSave = new currentSave
{
SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(),
RoomId = roomId,
SubRoomId = subRoomId,
DataBlob = fileName,
Description = "Imported by Coach!",
SavedByAccountId = 1,
SavedOnPlatform = 0,
SavedOnDeviceClass = 1,
CreatedAt = DateTime.UtcNow
};
RoomDB.SubRoomSaves.Insert(newSave);
subRoom.DataBlob = fileName;
subRoom.CurrentSave = newSave;
subRoom.Accessibility = RoomAccessibility.Public;
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true, saveId = newSave.SubRoomDataSaveId, fileName });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-room-type")]
public async Task<IActionResult> DirectorSetRoomType(long roomId, [FromBody] DirectorRoomCloningRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
if (body.Allowed)
room.UgcVersion = 2;
else
room.UgcVersion = 1;
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/{roomId:long}/set-modern-persistence")]
public async Task<IActionResult> DirectorSetModernPersistence(long roomId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
room.PersistenceVersion = 203;
room.UgcSubVersion = 367;
room.MinUgcSubVersion = 367;
for (int i = 0; i < room.SubRooms.Count; i++)
{
var subRoom = room.SubRooms[i].CurrentSave;
if (subRoom == null) continue;
subRoom.PersistenceVersion = 203;
subRoom.OMVersion = 164;
subRoom.UgcSubVersion = 367;
}
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(roomId);
return Ok(new { success = true });
}
[HttpDelete("/vannet/api/director/rooms/{roomId:long}/delete")]
public async Task<IActionResult> DirectorDeleteRoom(long roomId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var room = RoomDB.Rooms.FindById(roomId);
if (room == null)
return NotFound("");
RoomDB.DeleteRoom(roomId);
var onlinePlayers = PlayerDB.Players.FindAll()
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
.Select(p => p.PlayerId)
.ToList();
foreach (var pid in onlinePlayers)
await NotiController.SendRoomUpdate(pid, new { roomId, deleted = true });
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/rooms/import-epicquest")] // experimental 2
public async Task<IActionResult> DirectorImportRecconnedRoom([FromQuery] string? name)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(name))
return BadRequest(new { error = "The name is required" });
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("User-Agent", "BestHTTP");
HttpResponseMessage meowResp;
try
{
meowResp = await http.GetAsync($"https://recconned.gabethefirst.com/roomserver/rooms?name={Uri.EscapeDataString(name)}");
}
catch (Exception ex)
{
return StatusCode(500, new { error = $"error at recconned.gabethefirst.com: {ex.Message}" });
}
if (!meowResp.IsSuccessStatusCode)
return StatusCode((int)meowResp.StatusCode, new { error = $"recconned.gabethefirst.com returned {(int)meowResp.StatusCode}" });
JsonElement meow;
try
{
meow = JsonSerializer.Deserialize<JsonElement>(await meowResp.Content.ReadAsStringAsync());
}
catch (Exception ex)
{
return StatusCode(500, new { error = $"error parsing epicquest response {ex.Message}" });
}
string meowName = meow.TryGetProperty("Name", out var nProp) ? nProp.GetString() ?? name : name;
string resolvedName = meowName;
if (RoomDB.GetRoomByName(resolvedName) != null)
{
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
var rand = new Random();
char[] seg = new char[4];
string RandomSeg() { for (int i = 0; i < 4; i++) seg[i] = chars[rand.Next(chars.Length)]; return new string(seg); }
resolvedName = $"{RandomSeg()}-{RandomSeg()}-{RandomSeg()}-{RandomSeg()}";
}
string imageServerPath = Path.Combine(Environment.CurrentDirectory, "Data", "Images");
string cdnRoomPath = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "room");
Directory.CreateDirectory(imageServerPath);
Directory.CreateDirectory(cdnRoomPath);
string localImageName = "DefaultRoomImage.jpg";
if (meow.TryGetProperty("ImageName", out var imgProp) && !string.IsNullOrWhiteSpace(imgProp.GetString()))
{
string remoteImageName = imgProp.GetString()!;
string localImageDest = Path.Combine(imageServerPath, remoteImageName);
if (!System.IO.File.Exists(localImageDest))
{
try
{
using var imgClient = new HttpClient();
imgClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP");
var imgResp = await imgClient.GetAsync($"https://eq-img.gabethefirst.com/{remoteImageName}");
if (imgResp.IsSuccessStatusCode)
await System.IO.File.WriteAllBytesAsync(localImageDest, await imgResp.Content.ReadAsByteArrayAsync());
}
catch { }
}
if (System.IO.File.Exists(localImageDest))
localImageName = remoteImageName;
}
WarningMaskType warningMask = default;
if (meow.TryGetProperty("WarningMask", out var wmProp))
{
try { warningMask = (WarningMaskType)wmProp.GetInt32(); } catch { }
}
var newRoom = new Room
{
Name = resolvedName,
Description = (meow.TryGetProperty("Description", out var descProp) ? descProp.GetString() ?? "" : "") + " (Imported from EpicQuest)",
ImageName = localImageName,
CreatorAccountId = 2,
Accessibility = RoomAccessibility.Public,
CloningAllowed = true,
State = RoomState.Active,
IsDorm = false,
IsRRO = false,
IsDeveloperOwned = meow.TryGetProperty("IsDeveloperOwned", out var idoProp) && idoProp.GetBoolean(),
ToxmodEnabled = false,
AutoLocalizeRoom = meow.TryGetProperty("AutoLocalizeRoom", out var alProp) && alProp.GetBoolean(),
LoadScreenLocked = meow.TryGetProperty("LoadScreenLocked", out var lslProp) && lslProp.GetBoolean(),
MaxPlayerCalculationMode = meow.TryGetProperty("MaxPlayerCalculationMode", out var mpcmProp) ? mpcmProp.GetInt32() : 1,
MaxPlayers = meow.TryGetProperty("MaxPlayers", out var mpProp) ? mpProp.GetInt32() : 20,
MinLevel = meow.TryGetProperty("MinLevel", out var mlProp) ? mlProp.GetInt32() : 0,
PersistenceVersion = meow.TryGetProperty("PersistenceVersion", out var pvProp) ? pvProp.GetInt32() : 2,
UgcVersion = 1,
DisableMicAutoMute = meow.TryGetProperty("DisableMicAutoMute", out var dmamProp) && dmamProp.GetBoolean(),
DisableRoomComments = meow.TryGetProperty("DisableRoomComments", out var drcProp) && drcProp.GetBoolean(),
EncryptVoiceChat = meow.TryGetProperty("EncryptVoiceChat", out var evcProp) && evcProp.GetBoolean(),
SupportsJuniors = meow.TryGetProperty("SupportsJuniors", out var sjProp) && sjProp.GetBoolean(),
SupportsLevelVoting = meow.TryGetProperty("SupportsLevelVoting", out var slvProp) && slvProp.GetBoolean(),
SupportsMobile = meow.TryGetProperty("SupportsMobile", out var smProp) && smProp.GetBoolean(),
SupportsQuest2 = meow.TryGetProperty("SupportsQuest2", out var sq2Prop) && sq2Prop.GetBoolean(),
SupportsScreens = meow.TryGetProperty("SupportsScreens", out var ssProp) && ssProp.GetBoolean(),
SupportsTeleportVR = meow.TryGetProperty("SupportsTeleportVR", out var stvrProp) && stvrProp.GetBoolean(),
SupportsVRLow = meow.TryGetProperty("SupportsVRLow", out var svrlProp) && svrlProp.GetBoolean(),
SupportsWalkVR = meow.TryGetProperty("SupportsWalkVR", out var swvrProp) && swvrProp.GetBoolean(),
WarningMask = warningMask,
CustomWarning = meow.TryGetProperty("CustomWarning", out var cwProp) ? cwProp.GetString() : null,
CreatedAt = DateTime.UtcNow,
PublishedAt = DateTime.UtcNow,
Stats = new Stats { CheerCount = 0, FavoriteCount = 0, VisitorCount = 0, VisitCount = 0 },
Roles = new List<Roles>
{
new Roles { AccountId = 2, Role = Role.Creator, InvitedRole = Role.None }
},
Tags = new List<Tags>(),
PromoImages = new List<string>(),
PromoExternalContent = new List<PromoExternalContent>(),
LoadScreens = new List<LoadScreens>(),
SubRooms = new List<SubRooms>()
};
if (meow.TryGetProperty("Tags", out var tagsProp) && tagsProp.ValueKind == JsonValueKind.Array)
{
foreach (var t in tagsProp.EnumerateArray())
{
int tagType = 0;
if (t.TryGetProperty("Type", out var ttProp)) tagType = ttProp.GetInt32();
if (tagType != 0) continue;
string? tagVal = t.TryGetProperty("Tag", out var tvProp) ? tvProp.GetString() : null;
if (!string.IsNullOrWhiteSpace(tagVal))
newRoom.Tags.Add(new Tags { Tag = tagVal.ToLowerInvariant(), Type = TagType.General });
}
}
newRoom.Tags.Add(new Tags { Tag = "imported", Type = (TagType)2 });
long newRoomId = RoomDB.AddRoomSync(newRoom, shouldAssignNewIds: true);
var insertedRoom = RoomDB.GetRoom(newRoomId);
if (insertedRoom == null)
return StatusCode(500, new { error = "made room but couldnt get data" });
var importLog = new List<object>();
if (meow.TryGetProperty("SubRooms", out var subRoomsProp) && subRoomsProp.ValueKind == JsonValueKind.Array)
{
foreach (var sr in subRoomsProp.EnumerateArray())
{
string? srDataBlob = sr.TryGetProperty("DataBlob", out var srDbProp) ? srDbProp.GetString() : null;
string? srName = sr.TryGetProperty("Name", out var srNameProp) ? srNameProp.GetString() : null;
int srAccessibility = sr.TryGetProperty("Accessibility", out var srAccProp) ? srAccProp.GetInt32() : 0;
bool srIsSandbox = sr.TryGetProperty("IsSandbox", out var srIsSbProp) && srIsSbProp.GetBoolean();
int srMaxPlayers = sr.TryGetProperty("MaxPlayers", out var srMpProp) ? srMpProp.GetInt32() : 20;
string srUnitySceneId = sr.TryGetProperty("UnitySceneId", out var srUsProp) ? srUsProp.GetString() ?? "a75f7547-79eb-47c6-8986-6767abcb4f92" : "a75f7547-79eb-47c6-8986-6767abcb4f92";
string? localDataBlob = null;
if (!string.IsNullOrWhiteSpace(srDataBlob))
{
string blobFileName = srDataBlob;
string blobDest = Path.Combine(cdnRoomPath, blobFileName);
if (!System.IO.File.Exists(blobDest))
{
try
{
using var blobClient = new HttpClient();
blobClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP");
var blobResp = await blobClient.GetAsync($"https://eq-cdn.gabethefirst.com/room/{blobFileName}");
if (blobResp.IsSuccessStatusCode)
{
await System.IO.File.WriteAllBytesAsync(blobDest, await blobResp.Content.ReadAsByteArrayAsync());
localDataBlob = blobFileName;
}
else
{
importLog.Add(new { subRoom = srName, status = "blob_download_failed", code = (int)blobResp.StatusCode });
}
}
catch (Exception ex)
{
importLog.Add(new { subRoom = srName, status = "blob_download_error", error = ex.Message });
}
}
else
{
localDataBlob = blobFileName;
}
}
long newSubRoomId = RoomDB.GetNextSubRoomId();
var newSub = new SubRooms
{
SubRoomId = newSubRoomId,
RoomId = newRoomId,
Name = srName ?? "SubRoom",
Accessibility = (RoomAccessibility)srAccessibility,
IsSandbox = srIsSandbox,
MaxPlayers = srMaxPlayers,
UnitySceneId = srUnitySceneId,
DataBlob = localDataBlob,
SavedByAccountId = 1806,
CreatorAccountId = 1806,
ShouldAutoStageSaves = false,
LastModeratedSaveModerationState = 0
};
currentSave? newSave = null;
if (localDataBlob != null)
{
newSave = new currentSave
{
SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(),
RoomId = newRoomId,
SubRoomId = newSubRoomId,
DataBlob = localDataBlob,
Description = $"Imported from EpicQuest Recconned",
SavedByAccountId = 1806,
SavedOnPlatform = 0,
SavedOnDeviceClass = 1,
PersistenceVersion = 0,
UgcSubVersion = 0,
OMVersion = 0,
Tags = new List<string>(),
ModerationState = 0,
CreatedAt = DateTime.UtcNow
};
RoomDB.SubRoomSaves.Insert(newSave);
newSub.CurrentSave = newSave;
}
insertedRoom.SubRooms.Add(newSub);
importLog.Add(new { subRoom = srName, subRoomId = newSubRoomId, status = "imported", blob = localDataBlob });
}
RoomDB.Rooms.Update(insertedRoom);
}
var onlinePlayers = PlayerDB.Players.FindAll()
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
.Select(p => p.PlayerId)
.ToList();
foreach (var pid in onlinePlayers)
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(newRoomId));
return Ok(new { success = true, roomId = newRoomId, name = resolvedName, subRoomsImported = importLog.Count, log = importLog });
}
[HttpPost("/vannet/api/director/rooms/import-meownet")] // experimental
public async Task<IActionResult> DirectorImportMeowiiRoom([FromQuery] string? name)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(name))
return BadRequest(new { error = "The name is required" });
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("User-Agent", "BestHTTP");
http.DefaultRequestHeaders.Add("key", "c0f5eff84baf6fc808f98a8a9813748053deb3b0ce1978d08ab275e54e08dcda");
HttpResponseMessage meowResp;
try
{
meowResp = await http.GetAsync($"https://live.meowii.app/rooms?name={Uri.EscapeDataString(name)}");
}
catch (Exception ex)
{
return StatusCode(500, new { error = $"error at meowii.app: {ex.Message}" });
}
if (!meowResp.IsSuccessStatusCode)
return StatusCode((int)meowResp.StatusCode, new { error = $"meowii.app returned {(int)meowResp.StatusCode}" });
JsonElement meow;
try
{
meow = JsonSerializer.Deserialize<JsonElement>(await meowResp.Content.ReadAsStringAsync());
}
catch (Exception ex)
{
return StatusCode(500, new { error = $"error parsing meowii response {ex.Message}" });
}
string meowName = meow.TryGetProperty("Name", out var nProp) ? nProp.GetString() ?? name : name;
string resolvedName = meowName;
if (RoomDB.GetRoomByName(resolvedName) != null)
{
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
var rand = new Random();
char[] seg = new char[4];
string RandomSeg() { for (int i = 0; i < 4; i++) seg[i] = chars[rand.Next(chars.Length)]; return new string(seg); }
resolvedName = $"{RandomSeg()}-{RandomSeg()}-{RandomSeg()}-{RandomSeg()}";
}
string imageServerPath = Path.Combine(Environment.CurrentDirectory, "Data", "Images");
string cdnRoomPath = Path.Combine(Environment.CurrentDirectory, "Data", "cdn", "room");
Directory.CreateDirectory(imageServerPath);
Directory.CreateDirectory(cdnRoomPath);
string localImageName = "DefaultRoomImage.jpg";
if (meow.TryGetProperty("ImageName", out var imgProp) && !string.IsNullOrWhiteSpace(imgProp.GetString()))
{
string remoteImageName = imgProp.GetString()!;
string localImageDest = Path.Combine(imageServerPath, remoteImageName);
if (!System.IO.File.Exists(localImageDest))
{
try
{
using var imgClient = new HttpClient();
imgClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP");
imgClient.DefaultRequestHeaders.Add("key", "c0f5eff84baf6fc808f98a8a9813748053deb3b0ce1978d08ab275e54e08dcda");
var imgResp = await imgClient.GetAsync($"https://cdn.cookedasset.com/{remoteImageName}");
if (imgResp.IsSuccessStatusCode)
await System.IO.File.WriteAllBytesAsync(localImageDest, await imgResp.Content.ReadAsByteArrayAsync());
}
catch { }
}
if (System.IO.File.Exists(localImageDest))
localImageName = remoteImageName;
}
WarningMaskType warningMask = default;
if (meow.TryGetProperty("WarningMask", out var wmProp))
{
try { warningMask = (WarningMaskType)wmProp.GetInt32(); } catch { }
}
var newRoom = new Room
{
Name = resolvedName,
Description = (meow.TryGetProperty("Description", out var descProp) ? descProp.GetString() ?? "" : "") + " (Imported from MeowNet)",
ImageName = localImageName,
CreatorAccountId = 1806,
Accessibility = RoomAccessibility.Public,
CloningAllowed = true,
State = RoomState.Active,
IsDorm = false,
IsRRO = false,
IsDeveloperOwned = meow.TryGetProperty("IsDeveloperOwned", out var idoProp) && idoProp.GetBoolean(),
ToxmodEnabled = false,
AutoLocalizeRoom = meow.TryGetProperty("AutoLocalizeRoom", out var alProp) && alProp.GetBoolean(),
LoadScreenLocked = meow.TryGetProperty("LoadScreenLocked", out var lslProp) && lslProp.GetBoolean(),
MaxPlayerCalculationMode = meow.TryGetProperty("MaxPlayerCalculationMode", out var mpcmProp) ? mpcmProp.GetInt32() : 1,
MaxPlayers = meow.TryGetProperty("MaxPlayers", out var mpProp) ? mpProp.GetInt32() : 20,
MinLevel = meow.TryGetProperty("MinLevel", out var mlProp) ? mlProp.GetInt32() : 0,
PersistenceVersion = meow.TryGetProperty("PersistenceVersion", out var pvProp) ? pvProp.GetInt32() : 2,
UgcVersion = meow.TryGetProperty("UgcVersion", out var uvProp) ? uvProp.GetInt32() : 12,
DisableMicAutoMute = meow.TryGetProperty("DisableMicAutoMute", out var dmamProp) && dmamProp.GetBoolean(),
DisableRoomComments = meow.TryGetProperty("DisableRoomComments", out var drcProp) && drcProp.GetBoolean(),
EncryptVoiceChat = meow.TryGetProperty("EncryptVoiceChat", out var evcProp) && evcProp.GetBoolean(),
SupportsJuniors = meow.TryGetProperty("SupportsJuniors", out var sjProp) && sjProp.GetBoolean(),
SupportsLevelVoting = meow.TryGetProperty("SupportsLevelVoting", out var slvProp) && slvProp.GetBoolean(),
SupportsMobile = meow.TryGetProperty("SupportsMobile", out var smProp) && smProp.GetBoolean(),
SupportsQuest2 = meow.TryGetProperty("SupportsQuest2", out var sq2Prop) && sq2Prop.GetBoolean(),
SupportsScreens = meow.TryGetProperty("SupportsScreens", out var ssProp) && ssProp.GetBoolean(),
SupportsTeleportVR = meow.TryGetProperty("SupportsTeleportVR", out var stvrProp) && stvrProp.GetBoolean(),
SupportsVRLow = meow.TryGetProperty("SupportsVRLow", out var svrlProp) && svrlProp.GetBoolean(),
SupportsWalkVR = meow.TryGetProperty("SupportsWalkVR", out var swvrProp) && swvrProp.GetBoolean(),
WarningMask = warningMask,
CustomWarning = meow.TryGetProperty("CustomWarning", out var cwProp) ? cwProp.GetString() : null,
CreatedAt = DateTime.UtcNow,
PublishedAt = DateTime.UtcNow,
Stats = new Stats { CheerCount = 0, FavoriteCount = 0, VisitorCount = 0, VisitCount = 0 },
Roles = new List<Roles>
{
new Roles { AccountId = 1806, Role = Role.Creator, InvitedRole = Role.None }
},
Tags = new List<Tags>(),
PromoImages = new List<string>(),
PromoExternalContent = new List<PromoExternalContent>(),
LoadScreens = new List<LoadScreens>(),
SubRooms = new List<SubRooms>()
};
if (meow.TryGetProperty("Tags", out var tagsProp) && tagsProp.ValueKind == JsonValueKind.Array)
{
foreach (var t in tagsProp.EnumerateArray())
{
int tagType = 0;
if (t.TryGetProperty("Type", out var ttProp)) tagType = ttProp.GetInt32();
if (tagType != 0) continue;
string? tagVal = t.TryGetProperty("Tag", out var tvProp) ? tvProp.GetString() : null;
if (!string.IsNullOrWhiteSpace(tagVal))
newRoom.Tags.Add(new Tags { Tag = tagVal.ToLowerInvariant(), Type = TagType.General });
}
}
newRoom.Tags.Add(new Tags { Tag = "imported", Type = (TagType)2 });
long newRoomId = RoomDB.AddRoomSync(newRoom, shouldAssignNewIds: true);
var insertedRoom = RoomDB.GetRoom(newRoomId);
if (insertedRoom == null)
return StatusCode(500, new { error = "made room but couldnt get data" });
var importLog = new List<object>();
if (meow.TryGetProperty("SubRooms", out var subRoomsProp) && subRoomsProp.ValueKind == JsonValueKind.Array)
{
foreach (var sr in subRoomsProp.EnumerateArray())
{
string? srDataBlob = sr.TryGetProperty("DataBlob", out var srDbProp) ? srDbProp.GetString() : null;
string? srName = sr.TryGetProperty("Name", out var srNameProp) ? srNameProp.GetString() : null;
int srAccessibility = sr.TryGetProperty("Accessibility", out var srAccProp) ? srAccProp.GetInt32() : 0;
bool srIsSandbox = sr.TryGetProperty("IsSandbox", out var srIsSbProp) && srIsSbProp.GetBoolean();
int srMaxPlayers = sr.TryGetProperty("MaxPlayers", out var srMpProp) ? srMpProp.GetInt32() : 20;
string srUnitySceneId = sr.TryGetProperty("UnitySceneId", out var srUsProp) ? srUsProp.GetString() ?? "a75f7547-79eb-47c6-8986-6767abcb4f92" : "a75f7547-79eb-47c6-8986-6767abcb4f92";
string? localDataBlob = null;
if (!string.IsNullOrWhiteSpace(srDataBlob))
{
string blobFileName = srDataBlob;
string blobDest = Path.Combine(cdnRoomPath, blobFileName);
if (!System.IO.File.Exists(blobDest))
{
try
{
using var blobClient = new HttpClient();
blobClient.DefaultRequestHeaders.Add("User-Agent", "BestHTTP");
blobClient.DefaultRequestHeaders.Add("key", "c0f5eff84baf6fc808f98a8a9813748053deb3b0ce1978d08ab275e54e08dcda");
var blobResp = await blobClient.GetAsync($"https://cdn.cookedasset.com/room/{blobFileName}");
if (blobResp.IsSuccessStatusCode)
{
await System.IO.File.WriteAllBytesAsync(blobDest, await blobResp.Content.ReadAsByteArrayAsync());
localDataBlob = blobFileName;
}
else
{
importLog.Add(new { subRoom = srName, status = "blob_download_failed", code = (int)blobResp.StatusCode });
}
}
catch (Exception ex)
{
importLog.Add(new { subRoom = srName, status = "blob_download_error", error = ex.Message });
}
}
else
{
localDataBlob = blobFileName;
}
}
long newSubRoomId = RoomDB.GetNextSubRoomId();
var newSub = new SubRooms
{
SubRoomId = newSubRoomId,
RoomId = newRoomId,
Name = srName ?? "SubRoom",
Accessibility = (RoomAccessibility)srAccessibility,
IsSandbox = srIsSandbox,
MaxPlayers = srMaxPlayers,
UnitySceneId = srUnitySceneId,
DataBlob = localDataBlob,
SavedByAccountId = 1806,
CreatorAccountId = 1806,
ShouldAutoStageSaves = false,
LastModeratedSaveModerationState = 0
};
currentSave? newSave = null;
if (localDataBlob != null)
{
newSave = new currentSave
{
SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(),
RoomId = newRoomId,
SubRoomId = newSubRoomId,
DataBlob = localDataBlob,
Description = $"Imported from meowii.app",
SavedByAccountId = 1806,
SavedOnPlatform = 0,
SavedOnDeviceClass = 1,
PersistenceVersion = 0,
UgcSubVersion = 0,
OMVersion = 0,
Tags = new List<string>(),
ModerationState = 0,
CreatedAt = DateTime.UtcNow
};
RoomDB.SubRoomSaves.Insert(newSave);
newSub.CurrentSave = newSave;
}
insertedRoom.SubRooms.Add(newSub);
importLog.Add(new { subRoom = srName, subRoomId = newSubRoomId, status = "imported", blob = localDataBlob });
}
RoomDB.Rooms.Update(insertedRoom);
}
var onlinePlayers = PlayerDB.Players.FindAll()
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
.Select(p => p.PlayerId)
.ToList();
foreach (var pid in onlinePlayers)
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(newRoomId));
return Ok(new { success = true, roomId = newRoomId, name = resolvedName, subRoomsImported = importLog.Count, log = importLog });
}
[HttpPost("/vannet/api/director/players/gift-all-tokens")]
public async Task<IActionResult> DirectorGiftAllTokens([FromBody] DirectorGiftAllTokensRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (body == null || body.Amount <= 0)
return BadRequest(new { error = "Amount must be anything above 0." });
var allPlayers = PlayerDB.Players.FindAll().ToList();
int count = 0;
foreach (var fp in allPlayers)
{
var gift = GiftsDB.CreateGift(
toPlayerId: fp.PlayerId,
fromPlayerId: 1,
currency: body.Amount,
currencyType: 2,
balanceType: -2,
giftContext: GiftType.GameRewards_Tokens,
giftRarity: 50,
message: string.IsNullOrWhiteSpace(body.Message) ? "A gift for you <3" : body.Message,
platform: -1,
platformsToSpawnOn: -1
);
await NotiController.SendEvent(fp.PlayerId, "31", GiftsDB.MapToDTO(gift));
count++;
}
return Ok(new { success = true, playersGifted = count });
}
[HttpGet("/vannet/api/director/images/player/{playerId}")]
public IActionResult DirectorGetPlayerImages(long playerId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var db = ImageMetadataDB.GetDb();
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var images = col.Query().Where(x => x.PlayerId == (ulong)playerId).ToList()
.Select(x => new
{
savedImageId = x.Id,
playerId = x.PlayerId,
imageName = x.ImageName,
accessibility = (int)x.Accessibility,
accessibilityLocked = x.AccessibilityLocked,
cheerCount = x.CheerCount,
commentCount = x.CommentCount,
createdAt = x.CreatedAt,
description = x.Description,
type = (int)x.Type,
devLocked = x.DevLocked
}).ToList();
return Ok(images);
}
[HttpGet("/vannet/api/director/images/global")]
public IActionResult DirectorGetGlobalImages()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var images = ImageMetadataDB.GetGlobalImages();
return Ok(images);
}
[HttpGet("/vannet/api/director/images/{imageId:int}")]
public IActionResult DirectorGetImageById(int imageId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var db = ImageMetadataDB.GetDb();
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var img = col.FindById(imageId);
if (img == null) return NotFound(new { error = "Image not found." });
return Ok(img);
}
[HttpPost("/vannet/api/director/images/{imageId:int}/set-accessibility")]
public IActionResult DirectorSetImageAccessibility(int imageId, [FromBody] DirectorImageAccessibilityRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var db = ImageMetadataDB.GetDb();
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var img = col.FindById(imageId);
if (img == null) return NotFound(new { error = "Image not found." });
img.Accessibility = (ImageMetadataDB.ImageAccessibility)body.Accessibility;
col.Update(img);
return Ok(new { success = true, accessibility = img.Accessibility });
}
[HttpPost("/vannet/api/director/images/{imageId:int}/set-accessibility-lock")]
public IActionResult DirectorSetImageAccessibilityLock(int imageId, [FromBody] DirectorImageLockRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var db = ImageMetadataDB.GetDb();
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var img = col.FindById(imageId);
if (img == null) return NotFound(new { error = "Image not found." });
img.AccessibilityLocked = body.Locked;
col.Update(img);
return Ok(new { success = true, accessibilityLocked = img.AccessibilityLocked });
}
[HttpPost("/vannet/api/director/images/{imageId:int}/set-description")]
public IActionResult DirectorSetImageDescription(int imageId, [FromBody] DirectorImageDescriptionRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var db = ImageMetadataDB.GetDb();
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var img = col.FindById(imageId);
if (img == null) return NotFound(new { error = "Image not found." });
img.Description = string.IsNullOrWhiteSpace(body.Description) ? null : body.Description.Trim();
col.Update(img);
return Ok(new { success = true, description = img.Description });
}
[HttpPost("/vannet/api/director/images/{imageId:int}/reset-cheers")]
public IActionResult DirectorResetImageCheers(int imageId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
/*var db = ImageMetadataDB.GetDb();
var imgCol = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var cheerCol = db.GetCollection<ImageMetadataDB.Cheer>("cheers");
var img = imgCol.FindById(imageId);
if (img == null) return NotFound(new { error = "Image not found." });
cheerCol.DeleteMany(c => c.SavedImageId == imageId);
img.CheerCount = 0;
imgCol.Update(img);*/
return NotFound(new { error = "This has been disabled" });
}
[HttpPost("/vannet/api/director/images/{imageId:int}/delete")]
public IActionResult DirectorDeleteImage(int imageId)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var db = ImageMetadataDB.GetDb();
var imgCol = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var cheerCol = db.GetCollection<ImageMetadataDB.Cheer>("cheers");
var img = imgCol.FindById(imageId);
if (img == null) return NotFound(new { error = "Image not found." });
var filePath = Path.Combine("Data", "Images", img.PlayerId.ToString(), img.ImageName);
bool fileDeleted = false;
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
fileDeleted = true;
}
cheerCol.DeleteMany(c => c.SavedImageId == imageId);
imgCol.Delete(imageId);
return Ok(new { success = true, fileDeleted });
}
[HttpPost("/vannet/api/director/images/{imageId:int}/set-dev-lock")]
public IActionResult DirectorSetImageDevLock(int imageId, [FromBody] DirectorImageDevLockRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var db = ImageMetadataDB.GetDb();
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var img = col.FindById(imageId);
if (img == null) return NotFound(new { error = "Image not found." });
img.DevLocked = body.DevLocked;
col.Update(img);
return Ok(new { success = true, devLocked = img.DevLocked });
}
[HttpPost("/vannet/api/director/images/{imageId:int}/transfer")]
public IActionResult DirectorTransferImage(int imageId, [FromBody] DirectorImageTransferRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var db = ImageMetadataDB.GetDb();
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
var img = col.FindById(imageId);
if (img == null) return NotFound(new { error = "Image not found." });
var newOwner = PlayerDB.Players.FindById(body.NewPlayerId);
if (newOwner == null) return NotFound(new { error = "Target player not found." });
img.PlayerId = (ulong)body.NewPlayerId;
col.Update(img);
return Ok(new { success = true, imageId, newPlayerId = body.NewPlayerId });
}
public class DirectorImageAccessibilityRequest { public int Accessibility { get; set; } }
public class DirectorImageLockRequest { public bool Locked { get; set; } }
public class DirectorImageDescriptionRequest { public string? Description { get; set; } }
public class DirectorImageDevLockRequest { public bool DevLocked { get; set; } }
public class DirectorImageTransferRequest { public long NewPlayerId { get; set; } }
[HttpGet("/vannet/apis/api/images/v3/feed/global")]
public IActionResult GetGlobalImagesPublic([FromQuery] int skip = 0, [FromQuery] int take = 10, [FromQuery] string? since = null)
{
DateTime? parsedSince = null;
if (!string.IsNullOrWhiteSpace(since))
{
if (!DateTimeOffset.TryParse(since, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var sinceDto))
return BadRequest();
parsedSince = sinceDto.UtcDateTime.AddDays(-1);
}
return Ok(ImageMetadataDB.GetGlobalImagesPublic(skip, take, parsedSince));
}
[HttpGet("/vannet/api/director/db/info")]
public IActionResult DirectorDbInfo()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
return Ok(new
{
players = PlayerDB.Players.Count(),
rooms = RoomDB.Rooms.Count(),
saves = RoomDB.SubRoomSaves.Count(),
inventions = InventionDB.Inventions.Count()
});
}
[HttpPost("/vannet/api/director/db/execute")]
public async Task<IActionResult> DirectorDbExecute([FromBody] DirectorDbExecRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
if (string.IsNullOrWhiteSpace(body?.Expression))
return BadRequest(new { error = "Expression required." });
string expression = body.Expression.Trim();
object? result = null;
string? error = null;
try
{
var setRoomMatch = System.Text.RegularExpressions.Regex.Match(expression,
@"^RoomDB\.Rooms\.Set\((\d+),\s*(\w+),\s*(.*)\)$",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
var setPlayerMatch = System.Text.RegularExpressions.Regex.Match(expression,
@"^PlayerDB\.Players\.Set\((\d+),\s*(\w+),\s*(.*)\)$",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
if (setRoomMatch.Success)
{
long rid = long.Parse(setRoomMatch.Groups[1].Value);
string field = setRoomMatch.Groups[2].Value;
string rawValue = setRoomMatch.Groups[3].Value.Trim().Trim('"');
var room = RoomDB.Rooms.FindOne(r => r.RoomId == rid);
if (room == null) { error = $"Room {rid} not found."; }
else
{
var prop = typeof(Room).GetProperty(field, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase);
if (prop == null) { error = $"Room has no property '{field}'."; }
else
{
object? converted = prop.PropertyType.IsEnum
? Enum.Parse(prop.PropertyType, rawValue, true)
: Convert.ChangeType(rawValue, prop.PropertyType);
string oldVal = prop.GetValue(room)?.ToString() ?? "null";
prop.SetValue(room, converted);
RoomDB.Rooms.Update(room);
await NotifyRoomUpdate(rid);
result = new { updated = true, roomId = rid, field, oldValue = oldVal, newValue = rawValue };
}
}
}
else if (setPlayerMatch.Success)
{
long pid = long.Parse(setPlayerMatch.Groups[1].Value);
string field = setPlayerMatch.Groups[2].Value;
string rawValue = setPlayerMatch.Groups[3].Value.Trim().Trim('"');
var player = PlayerDB.Players.FindById(pid);
if (player == null) { error = $"Player {pid} not found."; }
else
{
var prop = typeof(Player).GetProperty(field, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.IgnoreCase);
if (prop == null) { error = $"Player has no property '{field}'."; }
else
{
object? converted = prop.PropertyType.IsEnum
? Enum.Parse(prop.PropertyType, rawValue, true)
: Convert.ChangeType(rawValue, prop.PropertyType);
string oldVal = prop.GetValue(player.Player)?.ToString() ?? "null";
prop.SetValue(player.Player, converted);
PlayerDB.Players.Update(player);
await NotiController.SendAccountUpdate(pid, PlayerDB.GetAccountMe(pid));
result = new { updated = true, playerId = pid, field, oldValue = oldVal, newValue = rawValue };
}
}
}
else if (expression.StartsWith("RoomDB.Rooms.Count()", StringComparison.OrdinalIgnoreCase))
result = RoomDB.Rooms.Count();
else if (expression.StartsWith("RoomDB.Rooms.FindAll()", StringComparison.OrdinalIgnoreCase))
result = RoomDB.Rooms.FindAll().Select(r => new { roomId = r.RoomId, name = r.Name }).ToList();
else if (expression.StartsWith("RoomDB.SubRoomSaves.Count()", StringComparison.OrdinalIgnoreCase))
result = RoomDB.SubRoomSaves.Count();
else if (expression.StartsWith("PlayerDB.Players.Count()", StringComparison.OrdinalIgnoreCase))
result = PlayerDB.Players.Count();
else if (expression.StartsWith("PlayerDB.Players.FindAll()", StringComparison.OrdinalIgnoreCase))
result = PlayerDB.Players.FindAll().Select(p => new { playerId = p.PlayerId, username = p.Player?.Username, displayName = p.Player?.DisplayName }).ToList();
else if (expression.StartsWith("reset artixsu dorm room", StringComparison.OrdinalIgnoreCase))
{
var player = PlayerDB.Players.FindById(577);
if (player?.Player?.PlayerExtra != null)
{
player.Player.PlayerExtra.DormRoomId = 4298;
PlayerDB.Players.Update(player);
result = true;
}
else
{
result = false;
}
}
else if (expression.StartsWith("GetLogins With Platform ID: ", StringComparison.OrdinalIgnoreCase))
{
var platformIdStr = expression.Substring("GetLogins With Platform ID: ".Length);
if (long.TryParse(platformIdStr, out long platformId))
{
result = PlayerDB.Players.FindAll()
.Where(p => p.PlatformIds != null &&
p.PlatformIds.Any(pid => pid.Platform == 0 && pid.PlatformId == platformIdStr))
.OrderByDescending(x => x.Player.LastLoginAt)
.ToList();
}
else
{
error = "Invalid platform ID.";
}
}
else if (expression.StartsWith("PlayerDB.ResetAllHeartbeats()", StringComparison.OrdinalIgnoreCase))
{
PlayerDB.ResetAllHeartbeats();
result = true;
}
else if (expression.StartsWith("LeaderboardDB.ClearRoomLeaderboard(4974)", StringComparison.OrdinalIgnoreCase))
result = LeaderboardDB.ClearRoomLeaderboard(4974);
else if (expression.StartsWith("Add everyone to group chat 61", StringComparison.OrdinalIgnoreCase))
{
var chat = ChatDB.GetThread(61);
foreach (var player in PlayerDB.Players.FindAll())
{
if (player.Player != null)
{
Console.WriteLine($"[Director] Adding player {player.PlayerId} ({player.Player.Username}) to chat thread 61.");
if (!chat.PlayerIds.Any(m => m == player.PlayerId))
{
ChatDB.AddMemberToThread(61, 1, player.PlayerId);
Console.Write("Done");
}
}
}
result = true;
}
else if (expression.StartsWith("Clear group chat 61", StringComparison.OrdinalIgnoreCase))
{
var thread = ChatDB.GetThread(61);
thread.Messages.Clear();
ChatDB.ChatDBFile.GetCollection<ChatDBClasses.ChatThread>("threads").Update(thread);
result = true;
}
else if (expression.StartsWith("Erase all player ids from group chat 61", StringComparison.OrdinalIgnoreCase))
{
var thread = ChatDB.GetThread(61);
thread.PlayerIds.Clear();
ChatDB.ChatDBFile.GetCollection<ChatDBClasses.ChatThread>("threads").Update(thread);
result = true;
}
else if (expression.StartsWith("Delete 49 data", StringComparison.OrdinalIgnoreCase))
{
PlayerDB.SetPlayerData(49, 181, "");
PlayerDB.SetPlayerData(49, 4974, "");
result = true;
}
else if (expression.StartsWith("Delete 786 data", StringComparison.OrdinalIgnoreCase))
{
PlayerDB.SetPlayerData(786, 181, "");
PlayerDB.SetPlayerData(786, 4974, "");
result = true;
}
else if (expression.StartsWith("publish my invention", StringComparison.OrdinalIgnoreCase))
{
var invention = InventionDB.Inventions.FindOne(i => i.InventionId == 5012352472715840770);
invention.Accessibility = RoomAccessibility.Public;
invention.GeneralPermission = 20;
invention.IsAGInvention = true;
invention.IsCertifiedInvention = true;
invention.IsPublished = true;
invention.IsFeatured = true;
invention.IsRecRoomApproved = true;
InventionDB.Inventions.Update(invention);
result = true;
}
else if (expression.StartsWith("GetInventionBlobNameByInventionName:", StringComparison.OrdinalIgnoreCase))
{
expression = expression.Substring("GetInventionBlobNameByInventionName:".Length).Trim();
var invention = InventionDB.Inventions.FindOne(i => i.Name.Equals(expression, StringComparison.OrdinalIgnoreCase));
if (invention != null)
result = invention.CurrentVersion.BlobName;
else
result = false;
}
else if (expression.StartsWith("Get 2340 avatar", StringComparison.OrdinalIgnoreCase))
{
var player = PlayerDB.Players.FindById(2672);
player.Player.PlayerExtra.Avatar = PlayerDB.Players.FindById(2340)?.Player.PlayerExtra.Avatar;
PlayerDB.Players.Update(player);
result = player.Player.PlayerExtra?.Avatar;
}
else if (expression.StartsWith("playersettings 2300", StringComparison.OrdinalIgnoreCase))
result = PlayerDB.Players.FindById(1904800648)?.Player?.PlayerExtra?.Settings;
else if (expression.StartsWith("Backfill HAS_COMPLETED_ORIENTATION for unfinished accounts", StringComparison.OrdinalIgnoreCase))
{
var updatedPlayerIds = new List<long>();
foreach (var player in PlayerDB.Players.FindAll().ToList())
{
var settings = PlayerDB.SettingsPlayers.FindById(player.PlayerId)?.Player?.PlayerExtra?.Settings
?? player.Player?.PlayerExtra?.Settings;
if (settings == null) continue;
var hasFinished = settings.FirstOrDefault(s => s.Key == "Recroom.AccountCreation.HasFinished");
Console.WriteLine($"[Director] Player {player.PlayerId} ({player.Player?.Username}) has HAS_COMPLETED_ORIENTATION: {hasFinished?.Value ?? "null"}");
if (player.PlayerId > 1186)
{
Console.Write($"[Director] Backfilling HAS_COMPLETED_ORIENTATION for player {player.PlayerId} ({player.Player?.Username})");
PlayerDB.SetPlayerSetting("HAS_COMPLETED_ORIENTATION", "True", player.PlayerId);
PlayerDB.SetPlayerSetting("OrientationCompletionTime", "2025-07-09T21%3A52%3A18.8129095Z", player.PlayerId);
updatedPlayerIds.Add(player.PlayerId);
Console.WriteLine(" - Done");
}
}
result = new { updatedCount = updatedPlayerIds.Count, playerIds = updatedPlayerIds };
}
else if (expression.StartsWith("backfill new", StringComparison.OrdinalIgnoreCase))
{
foreach (var player in PlayerDB.Players.FindAll().ToList())
{
if (player.Player != null)
{
var id = player.PlayerId;
Console.WriteLine($"[Director] Setting player {id} ({player.Player.Username}) setting.");
PlayerDB.SetPlayerSetting("Recroom.AccountCreation.HasFinished", "False", id);
PlayerDB.SetPlayerSetting("Recroom.AccountCreation.HasCreatedPassword", "False", id);
PlayerDB.SetPlayerSetting("HAS_COMPLETED_ORIENTATION", "True", id);
PlayerDB.SetPlayerSetting("OrientationCompletionTime", "2025-07-09T21%3A52%3A18.8129095Z", id);
if (player.Player.Username.Length >= 4 && char.IsDigit(player.Player.Username[player.Player.Username.Length - 4]) &&
char.IsDigit(player.Player.Username[player.Player.Username.Length - 3]) &&
char.IsDigit(player.Player.Username[player.Player.Username.Length - 2]) &&
char.IsDigit(player.Player.Username[player.Player.Username.Length - 1]))
PlayerDB.SetPlayerSetting("Recroom.AccountCreation.HasChosenUsername", "False", id);
else
PlayerDB.SetPlayerSetting("Recroom.AccountCreation.HasChosenUsername", "True", id);
Console.WriteLine($"[Director] Player {id} ({player.Player.Username}) settings updated.");
}
}
result = true;
}
else if (expression.StartsWith("list players 100000+ tokens", StringComparison.OrdinalIgnoreCase))
{
var richPlayers = new List<object>();
foreach (var player in PlayerDB.Players.FindAll().ToList())
{
var tokenBalance = player.Player?.PlayerExtra?.Currencies?
.FirstOrDefault(c => c.CurrencyType == CurrencyType.RecCenterTokens && c.BalanceType == BalanceType.NonPurchasedDefault)?.Balance ?? 0;
if (tokenBalance >= 100000)
{
richPlayers.Add(new
{
PlayerId = player.PlayerId,
Username = player.Player?.Username,
Tokens = tokenBalance
});
}
}
result = richPlayers;
}
else
{
var ridMatch = System.Text.RegularExpressions.Regex.Match(expression, @"RoomDB\.Rooms\.FindOne.*?(\d+)");
var pidMatch = System.Text.RegularExpressions.Regex.Match(expression, @"PlayerDB\.Players\.FindOne.*?(\d+)");
if (ridMatch.Success && long.TryParse(ridMatch.Groups[1].Value, out long rid))
result = RoomDB.GetRoom(rid);
else if (pidMatch.Success && long.TryParse(pidMatch.Groups[1].Value, out long pid))
result = PlayerDB.Players.FindById(pid);
else
error = "Unrecognised expression pattern.";
}
}
catch (Exception ex)
{
error = ex.Message;
}
if (error != null)
return BadRequest(new { error });
return Ok(new { result });
}
[HttpPost("/vannet/api/director/rooms/clear")]
public async Task<IActionResult> DirectorClearRooms()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
//await RoomDB.ClearRooms();
//await RoomDB.ImportRooms(Path.Join(Environment.CurrentDirectory, "Data", "Imports", "ImportRooms.json"));
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/db/reset")]
public async Task<IActionResult> DirectorDbReset()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
//await RoomDB.ClearRooms();
//PlayerDB.Players.DeleteAll();
//InventionDB.Inventions.DeleteAll();
//FriendsDB.FriendsDBFile.GetCollection<FriendsDBClasses.FriendEntry>("Friends").DeleteAll();
//EventDB.Events.DeleteAll();
//await RoomDB.ImportRooms(Path.Join(Environment.CurrentDirectory, "Data", "Imports", "ImportRooms.json"));
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/coach/send-msg")]
public async Task<IActionResult> SendCoachMessage([FromBody] DirectorCoachMessageRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
await NotiController.SendCoahMessage(body?.Message ?? "");
return Ok(new { success = true });
}
private static readonly object MaintenanceConfigLock = new(); // i might not have a brain gentlemen but i have an IDEA
[HttpPost("/vannet/api/director/maintenance/schedule")]
public async Task<IActionResult> DirectorScheduleMaintenance([FromBody] DirectorMaintenanceRequest body)
{
if (!ValidateDirectorToken(out var session)) return NotFound("");
var minutes = Math.Clamp(body?.Minutes ?? 10, 0, 1440);
var path = Path.Join(Program.dataDir, "APIS", "ConfigV2.json");
try
{
lock (MaintenanceConfigLock)
{
if (!System.IO.File.Exists(path))
return NotFound(new { error = "ConfigV2.json not found." });
var root = JsonNode.Parse(System.IO.File.ReadAllText(path))?.AsObject();
if (root == null)
return StatusCode(500, new { error = "ConfigV2.json is malformed." });
root.Remove("ServerMaintainence");
root["ServerMaintenance"] = new JsonObject { ["StartsInMinutes"] = minutes };
System.IO.File.WriteAllText(path, root.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
}
}
catch (Exception ex)
{
Console.WriteLine($"[Director] Failed to write maintenance config: {ex.Message}");
return StatusCode(500, new { error = "Failed to update config." });
}
var push = JsonSerializer.Serialize(new { Id = ((int)Vanadium.Classes.WebSocket.WebsocketEvents.ResponseResults.ServerMaintenance).ToString(), Msg = new { StartsInMinutes = minutes } });
await NotificationsController.SendToAll(push);
Console.WriteLine($"[Director] Maintenance set to {minutes}m by {session?.DiscordUserId ?? "unknown"}");
return Ok(new { success = true, minutes });
}
[HttpGet("/vannet/api/director/system-prompt")]
public IActionResult DirectorGetSystemPrompt()
{
if (!ValidateDirectorToken(out _)) return NotFound("");
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "systemprompt.txt");
if (!System.IO.File.Exists(path))
return Ok(new { message = "" });
return Ok(new
{
message = System.IO.File.ReadAllText(path)
});
}
[HttpPost("/vannet/api/director/system-prompt")]
public async Task<IActionResult> DirectorSetSystemPrompt([FromBody] string body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "systemprompt.txt");
if (!System.IO.File.Exists(path))
return NotFound(new { error = "systemprompt.txt not found." });
System.IO.File.WriteAllText(path, body ?? "");
return Ok(new { success = true });
}
[HttpPost("/vannet/api/director/players/{playerId:long}/raw-mod-details")]
public async Task<IActionResult> DirectorSetRawModDetails(long playerId, [FromBody] DirectorRawModDetailsRequest body)
{
if (!ValidateDirectorToken(out _)) return NotFound("");
var player = PlayerDB.Players.FindById(playerId);
if (player?.Player == null) return NotFound(new { error = "Player not found." });
var mbd = new ModerationBlockDetails
{
ReportCategory = body.ReportCategory,
Duration = body.Duration,
GameSessionId = body.GameSessionId,
IsHostKick = body.IsHostKick,
Message = body.Message,
PlayerIdReporter = body.PlayerIdReporter,
IsBan = body.IsBan,
IsVoiceModAutoban = body.IsVoiceModAutoban,
IsDeviceBan = body.IsDeviceBan,
IsWarning = body.IsWarning,
VoteKickReason = body.VoteKickReason,
TimeoutStartedAt = body.TimeoutStartedAt,
AssociatedAccountUsername = body.AssociatedAccountUsername
};
player.Player.PlayerExtra ??= new PlayerExtra();
player.Player.PlayerExtra.ModerationBlockDetails = mbd;
if (body.BootToDorm)
{
player.Player.PlayerExtra.Heartbeat = new Heartbeat
{
playerId = playerId,
isOnline = false,
roomInstance = null,
errorCode = 0
};
PlayerDB.Players.Update(player);
await NotiController.SendPresenceUpdate(playerId, player.Player.PlayerExtra.Heartbeat);
await NotiController.SendAccountUpdate(playerId, PlayerDB.GetAccountMe(playerId));
await NotificationsController.SendBanned(playerId, mbd);
}
else
{
PlayerDB.Players.Update(player);
}
return Ok(new { success = true });
}
[HttpGet("/vannet/api/players/v1/online")]
public async Task<IActionResult> PlayersOnline()
{
var onlinePlayers = PlayerDB.Players.FindAll()
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
.Select(p => new
{
accountId = p.PlayerId,
username = p.Player?.Username,
displayName = p.Player?.DisplayName,
roomName = p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.Name
})
.ToList();
return Ok(new { count = onlinePlayers.Count, players = onlinePlayers });
}
private async Task NotifyRoomUpdate(long roomId)
{
var updated = RoomDB.GetRoom(roomId);
var onlinePlayers = PlayerDB.Players.FindAll()
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
.Select(p => p.PlayerId)
.ToList();
foreach (var pid in onlinePlayers)
await NotiController.SendRoomUpdate(pid, updated);
}
private static object BuildPlayerSummary(FullPlayer p) => new
{
accountId = p.PlayerId,
username = p.Player?.Username,
displayName = p.Player?.DisplayName,
profileImage = p.Player?.ProfileImage,
level = p.Player?.Level ?? 1,
createdAt = p.Player?.CreatedAt,
isOnline = p.Player?.PlayerExtra?.Heartbeat?.isOnline ?? false
};
private static object BuildPlayerFull(FullPlayer p) => new
{
accountId = p.PlayerId,
username = p.Player?.Username,
displayName = p.Player?.DisplayName,
profileImage = p.Player?.ProfileImage,
level = p.Player?.Level ?? 1,
xp = p.Player?.XP ?? 0,
bio = p.Player?.Bio,
createdAt = p.Player?.CreatedAt,
lastLoginAt = p.Player?.LastLoginAt,
isJunior = p.Player?.IsJunior,
roles = p.PlayerRoles?.Select(r => r.ToString()).ToList() ?? new List<string>(),
heartbeat = p.Player?.PlayerExtra?.Heartbeat,
dormRoomId = p.Player?.PlayerExtra?.DormRoomId,
moderationBlockDetails = p.Player?.PlayerExtra?.ModerationBlockDetails,
toxMod = p.Player?.PlayerExtra?.ToxMod,
juniorStatus = p.Player?.PlayerExtra?.JuniorStatus,
currencies = p.Player?.PlayerExtra?.Currencies,
ownedEquipment = p.Player?.PlayerExtra?.OwnedEquipment,
influencer = p.Player?.PlayerExtra?.Influencer,
platformIds = p.PlatformIds,
nameLocked = p.Player?.NameLocked ?? false,
recRoomPlus = ReadRRPlusIds().Contains(p.PlayerId),
discordUserId = p.Player?.DiscordUserId,
discordUsername = p.Player?.DiscordUsername,
discordLinked = p.Player?.DiscordLinked ?? false,
discordLinkedAt = p.Player?.DiscordLinkedAt
};
private static object BuildRoomSummary(Room r) => new
{
roomId = r.RoomId,
name = r.Name,
imageName = r.ImageName,
creatorAccountId = r.CreatorAccountId,
stats = r.Stats,
accessibility = r.Accessibility,
state = r.State,
tags = r.Tags,
maxPlayers = r.MaxPlayers,
ageRating = r.AgeRating,
createdAt = r.CreatedAt
};
public class DirectorCoachMessageRequest { public string? Message { get; set; } }
public class DirectorPlayerCoachMessageRequest { public string? Message { get; set; } }
public class DirectorSendWsHexRequest
{
public string? Hex { get; set; }
public bool Exact { get; set; }
}
public class DirectorSendWsJsonRequest { public string? Json { get; set; } }
public class DirectorShoveAllRequest { public long RoomId { get; set; } public long SubRoomId { get; set; } public string? InstanceId { get; set; } }
public class DirectorDiscordCallbackRequest { public string? Code { get; set; } }
public class DirectorRenameRequest { public string? Username { get; set; } }
public class DirectorCreatePlayerRequest { public long PlayerId { get; set; } public string? Username { get; set; } public bool IsJunior { get; set; } = false; }
public class DirectorNameLockRequest { public bool Locked { get; set; } = true; }
public class DirectorAnnouncementRequest { public string? Title { get; set; } public string? Body { get; set; } public string? ImageName { get; set; } public string? LinkName { get; set; } public string? LinkUri { get; set; } public AnnoucementDBClasses.LinkType LinkType { get; set; } public AnnoucementDBClasses.AnnouncementType AnnouncementType { get; set; } public bool Clear { get; set; } = false; }
public class DirectorCommunityBannerRequest { public string? Message { get; set; } public string? MoreInfoUrl { get; set; } }
public class DirectorRRPlusRequest { public bool Enabled { get; set; } = true; }
public class DirectorBioRequest { public string? Bio { get; set; } }
public class DirectorPasswordRequest { public string? Password { get; set; } }
public class DirectorSetPfpRequest { public string? ImageName { get; set; } }
public class DirectorSetLevelRequest { public int Level { get; set; } public int? Xp { get; set; } }
public class DirectorSetJuniorRequest { public bool IsJunior { get; set; } = false; public string? Reason { get; set; } }
public class DirectorCoachDmRequest { public string? Message { get; set; } }
public class DirectorSetPlatformIdRequest { public string? PlatformId { get; set; } }
public class DirectorShoveRequest { public long RoomId { get; set; } public long? SubRoomId { get; set; } public string? InstanceId { get; set; } }
public class DirectorTransferRoomsRequest { public long NewPlayerId { get; set; } public bool KeepCoOwner { get; set; } = false; }
public class DirectorRoleRequest { public string? Role { get; set; } public string? Action { get; set; } }
public class DirectorInfluencerRequest { public string? Action { get; set; } }
public class DirectorBanRequest { public string? Reason { get; set; } public int Duration { get; set; } = 99999; }
public class DirectorHwidBanRequest { public string? Hwid { get; set; } public long? PlayerId { get; set; } public string? Reason { get; set; } public int? DurationDays { get; set; } }
public class DirectorHwidUnbanRequest { public string? Hwid { get; set; } }
public class DirectorSetTokensRequest { public int Amount { get; set; } public int BalanceType { get; set; } = 0; }
public class DirectorGiveSkinRequest { public string? Skin { get; set; } public string? Action { get; set; } }
public class DirectorRoomNameRequest { public string? Name { get; set; } }
public class DirectorRoomDescRequest { public string? Description { get; set; } }
public class DirectorRoomImageRequest { public string? ImageName { get; set; } }
public class DirectorRoomAccessibilityRequest { public int Accessibility { get; set; } }
public class DirectorRoomCloningRequest { public bool Allowed { get; set; } }
public class DirectorRoomRestrictionsRequest { public bool SupportsScreens { get; set; } public bool SupportsWalkVR { get; set; } public bool SupportsTeleportVR { get; set; } public bool SupportsJuniors { get; set; } public bool SupportsMobile { get; set; } }
public class DirectorRoomAgeRatingRequest { public int AgeRating { get; set; } }
public class DirectorRoomMaxPlayersRequest { public int MaxPlayers { get; set; } }
public class DirectorChangeOwnerRequest { public long NewOwnerId { get; set; } }
public class DirectorSetRoomRoleRequest { public long PlayerId { get; set; } public int Role { get; set; } }
public class DirectorRoomTagsRequest { public List<string>? Tags { get; set; } public string? Action { get; set; } public int? Type { get; set; } }
public class DirectorChangeDatablobRequest { public long SubRoomId { get; set; } public string? Datablob { get; set; } }
public class DirectorRefreshInstancesRequest
{
// the panel can hand us the id as a string when safeParse quotes large ids
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long SubRoomId { get; set; }
}
public class DirectorSetInstancesPrivacyRequest
{
// the panel can hand us the id as a string when safeParse quotes large ids
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long SubRoomId { get; set; }
public bool IsPrivate { get; set; }
}
public class DirectorDbExecRequest { public string? Expression { get; set; } }
public class DirectorMaintenanceRequest { public int Minutes { get; set; } = 10; }
public class DirectorToxModBanRequest { public string? Category { get; set; } public string? Note { get; set; } public long? DurationSeconds { get; set; } public bool Loud { get; set; } = false; }
public class DirectorToxModUnbanRequest { public bool ResetStrikes { get; set; } = false; }
public class DirectorGiftAllTokensRequest { public int Amount { get; set; } public string? Message { get; set; } }
public class DirectorForceBringRequest { public string? Username { get; set; } }
public class DirectorRawModDetailsRequest
{
public ReportCategory ReportCategory { get; set; } = ReportCategory.Moderator;
public int Duration { get; set; } = 0;
public long GameSessionId { get; set; } = 0;
public bool? IsHostKick { get; set; } = false;
public string? Message { get; set; }
public ulong? PlayerIdReporter { get; set; }
public bool? IsBan { get; set; } = false;
public bool? IsVoiceModAutoban { get; set; } = false;
public bool? IsDeviceBan { get; set; } = false;
public bool? IsWarning { get; set; } = false;
public string? VoteKickReason { get; set; }
public DateTime? TimeoutStartedAt { get; set; }
public string? AssociatedAccountUsername { get; set; }
public bool BootToDorm { get; set; } = false;
}
public class DirectorAuditFilter : IAsyncActionFilter
{
private const string WebhookUrl = "https://discord.com/api/webhooks/1545617304208023625/rRR-9CQjtLZtWpnOoHtCrIKfelX1EPn8AI0I4OopG6e-TN0MBl8e8QWmrWpiXDqOK--O";
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var request = context.HttpContext.Request;
if (!request.Path.Value?.StartsWith("/vannet/api/director/") == true || request.Method.Equals("GET", StringComparison.OrdinalIgnoreCase) || request.Path.Value!.Equals("/vannet/api/director/discord-callback", StringComparison.OrdinalIgnoreCase))
{
await next();
return;
}
string? discordUserId = null;
string? token = request.Headers["X-Director-Token"].FirstOrDefault()
?? request.Cookies["director_session"];
if (!string.IsNullOrWhiteSpace(token))
{
lock (WebsiteController.SessionLock)
{
if (WebsiteController.DirectorSessions.TryGetValue(token, out var s) && s.ExpiresAt > DateTime.UtcNow)
discordUserId = s.DiscordUserId;
}
}
string requestData = ReadRequestDataAsync(context);
var executed = await next();
string responseData = ReadResponseData(executed.Result);
string action = request.Path.Value ?? "unknown";
if (request.QueryString.HasValue)
action += request.QueryString.Value;
_ = SendEmbedAsync(discordUserId, action, requestData, responseData);
}
private static string ReadRequestDataAsync(ActionExecutingContext context)
{
try
{
var request = context.HttpContext.Request;
var parts = new List<string>();
if (request.Query.Count > 0)
parts.AddRange(request.Query.Keys.Select(k => $"{k}: {request.Query[k]}"));
foreach (var arg in context.ActionArguments.Values)
{
if (arg == null) continue;
try
{
var json = JsonSerializer.Serialize(arg, new JsonSerializerOptions { WriteIndented = false });
var doc = JsonDocument.Parse(json);
foreach (var prop in doc.RootElement.EnumerateObject())
parts.Add($"{prop.Name}: {prop.Value}");
}
catch { parts.Add(arg.ToString() ?? ""); }
}
return parts.Count > 0 ? string.Join("\n", parts) : "*(no body)*";
}
catch { }
return "*(no body)*";
}
private static string ReadResponseData(IActionResult? result)
{
try
{
if (result is ObjectResult obj && obj.Value != null)
{
string json = JsonSerializer.Serialize(obj.Value, new JsonSerializerOptions { WriteIndented = false });
if (json.Length > 800) json = json[..800] + "…";
try
{
var doc = JsonDocument.Parse(json);
var sb = new StringBuilder();
foreach (var prop in doc.RootElement.EnumerateObject())
sb.Append($"{prop.Name}: {prop.Value} ");
return sb.ToString().TrimEnd();
}
catch { return json; }
}
if (result is StatusCodeResult sc)
return $"status: {sc.StatusCode}";
}
catch { }
return "*(no response)*";
}
private static async Task SendEmbedAsync(string? discordUserId, string action, string requestData, string responseData)
{
try
{
using var http = new HttpClient();
var embed = new
{
color = 0x1a1a1a,
description = $"<@{discordUserId ?? "unknown"}> executed action: **{action}**",
fields = new[]
{
new { name = "Request", value = string.IsNullOrWhiteSpace(requestData) ? "*(no body)*" : $"```{requestData}```", inline = false },
new { name = "Response", value = string.IsNullOrWhiteSpace(responseData) ? "*(no response)*" : $"```{responseData}```", inline = false }
}
};
var payload = JsonSerializer.Serialize(new { embeds = new[] { embed } });
await http.PostAsync(WebhookUrl, new StringContent(payload, Encoding.UTF8, "application/json"));
}
catch { }
}
}
}
}