yikes,
This commit is contained in:
@@ -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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.! 😡‼😡‼😡‼😡‼" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,789 @@
|
||||
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 SixLabors.ImageSharp;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using Vanadium.Classes.Rooms;
|
||||
using static Vanadium.Classes.ServerConfig;
|
||||
using Vanadium.Utils;
|
||||
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;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class ImageController : ControllerBase
|
||||
{
|
||||
|
||||
|
||||
internal async Task SendImageHileWebhook(
|
||||
long accountId,
|
||||
string? message,
|
||||
string? imageName = null,
|
||||
string? headersPretty = null)
|
||||
{
|
||||
|
||||
var embed = new Dictionary<string, object?>
|
||||
{
|
||||
["title"] = $"ID = {accountId}",
|
||||
["color"] = 3066993,
|
||||
["fields"] = new object[]
|
||||
{
|
||||
new { name = "Headers", value = headersPretty ?? Request.Headers.ToString(), inline = false },
|
||||
new { name = "IP", value = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown", inline = false },
|
||||
new { name = "Image (in /Data/BadImages look at your own risk)", value = imageName ?? "No image provided", inline = true }
|
||||
},
|
||||
["footer"] = new { text = message ?? "No message provided" }
|
||||
};
|
||||
|
||||
var payload = new
|
||||
{
|
||||
embeds = new[] { embed }
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var content = new StringContent(
|
||||
System.Text.Json.JsonSerializer.Serialize(payload),
|
||||
Encoding.UTF8, "application/json");
|
||||
await APIController._webhookClient.PostAsync(
|
||||
"https://discord.com/api/webhooks/1544865393091547267/1ntP_8icrIw2O0OiLEt1Rk05lWANxOJksouFVQIcKLu9q9dFIBqSSxPBIkGn6KZz9dbp",
|
||||
content);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
string lastContentType = "";
|
||||
|
||||
public static string GetHeadersPretty(HttpRequest request)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var header in request.Headers)
|
||||
{
|
||||
sb.AppendLine($"{header.Key}: {header.Value}");
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
[HttpPost("/api/images/v4/uploadsaved")]
|
||||
public async Task<IActionResult> UploadSaved()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var player = PlayerDB.GetCurrentPlayer(id.Value);
|
||||
|
||||
bool isDeveloper = player?.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false;
|
||||
|
||||
string divertPaths = "";
|
||||
|
||||
var userAgent = Request.Headers.UserAgent.ToString();
|
||||
if (!userAgent.Contains("BestHTTP", StringComparison.OrdinalIgnoreCase))
|
||||
divertPaths = "Non-BestHTTP";
|
||||
|
||||
var contentType = Request.Headers.ContentType.ToString();
|
||||
if (!contentType.Contains("BestHTTP", StringComparison.OrdinalIgnoreCase) && !isDeveloper)
|
||||
divertPaths = "Non-image content type";
|
||||
|
||||
if (contentType == lastContentType)
|
||||
divertPaths = "Repeated content type";
|
||||
lastContentType = contentType;
|
||||
|
||||
if (!ServerConfig.PostingEnabled && !isDeveloper)
|
||||
return StatusCode(403, new { success = false, error = "Images.UserIsAcessibilityLocked" });
|
||||
|
||||
if (!isDeveloper && !PlayerDB.TryIncrementDailyImageUpload(id.Value))
|
||||
return StatusCode(403, new { success = false, error = "Images.DailyImagesUploadReached" });
|
||||
|
||||
if (PlayerDB.IsBanned(id.Value))
|
||||
divertPaths = "Banned player uploaded image";
|
||||
|
||||
var form = await Request.ReadFormAsync();
|
||||
var file = form.Files.FirstOrDefault();
|
||||
if (file == null || file.Length == 0)
|
||||
return BadRequest();
|
||||
|
||||
byte[] fileBytes;
|
||||
using (var input = file.OpenReadStream())
|
||||
using (var image = await SixLabors.ImageSharp.Image.LoadAsync<Rgba32>(input))
|
||||
using (var output = new MemoryStream())
|
||||
{
|
||||
await image.SaveAsync(output, new SixLabors.ImageSharp.Formats.Png.PngEncoder());
|
||||
fileBytes = output.ToArray();
|
||||
}
|
||||
|
||||
bool tripped = string.IsNullOrWhiteSpace(divertPaths) ? false : true;
|
||||
|
||||
var fileName = ImageMetadataDB.SaveImageFile(fileBytes, player.PlayerId, tripped);
|
||||
if (!form.TryGetValue("imgMeta", out var imgMetaValue) || string.IsNullOrWhiteSpace(imgMetaValue))
|
||||
return BadRequest();
|
||||
if (tripped)
|
||||
{
|
||||
await SendImageHileWebhook(player.PlayerId, divertPaths, fileName, GetHeadersPretty(Request));
|
||||
return Ok(new { ImageName = fileName });
|
||||
}
|
||||
|
||||
ImageMetadataDB.ImageMetadata? metadata;
|
||||
try
|
||||
{
|
||||
metadata = System.Text.Json.JsonSerializer.Deserialize<ImageMetadataDB.ImageMetadata>(
|
||||
imgMetaValue!,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
if (metadata == null)
|
||||
return BadRequest();
|
||||
|
||||
var newImage = new ImageMetadataDB.FullSavedImage
|
||||
{
|
||||
RoomId = metadata.roomId,
|
||||
PlayerId = (ulong)id.Value,
|
||||
ImageName = fileName,
|
||||
Description = metadata.description,
|
||||
Accessibility = metadata.accessibility,
|
||||
TaggedPlayerIds = metadata.playerIds ?? new List<ulong>(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Type = metadata.savedImageType
|
||||
};
|
||||
|
||||
ImageMetadataDB.InsertImage(newImage);
|
||||
|
||||
string playerName = PlayerDB.GetCurrentPlayer((long)id.Value)?.Player?.DisplayName
|
||||
?? $"User {id.Value}";
|
||||
string playerId = $"{newImage.PlayerId}";
|
||||
string roomName = RoomDB.GetRoom(metadata.roomId)?.Name
|
||||
?? $"Room {metadata.roomId}";
|
||||
|
||||
if (metadata.accessibility == ImageMetadataDB.ImageAccessibility.Public)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
var imageUrl = $"https://reloxa.xyz/imageserver/{fileName}";
|
||||
var uploaderProfilePic = $"https://reloxa.xyz/imageserver/{PlayerDB.GetCurrentPlayer((long)newImage.PlayerId)?.Player?.ProfileImage ?? "DefaultPFP.png"}?cropSquare=true";
|
||||
|
||||
bool roomIsPrivate = false;
|
||||
string roomDisplayName;
|
||||
var room = RoomDB.GetRoom(metadata.roomId);
|
||||
if (room == null || room.Accessibility != RoomDBClasses.RoomAccessibility.Public)
|
||||
{
|
||||
roomIsPrivate = true;
|
||||
roomDisplayName = "[PRIVATE ROOM]";
|
||||
}
|
||||
else
|
||||
{
|
||||
roomDisplayName = $"^{room.Name}";
|
||||
}
|
||||
|
||||
var payload = new
|
||||
{
|
||||
content = "",
|
||||
tts = false,
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
author = new
|
||||
{
|
||||
name = $"{playerName} uploaded a photo!",
|
||||
icon_url = uploaderProfilePic
|
||||
},
|
||||
title = $"Room - {roomDisplayName}",
|
||||
image = new { url = imageUrl },
|
||||
fields = Array.Empty<object>()
|
||||
}
|
||||
},
|
||||
components = Array.Empty<object>(),
|
||||
flags = 0
|
||||
};
|
||||
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
|
||||
await http.PostAsync(ServerConfig.ImagesWebhook, content);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return Ok(new { ImageName = fileName });
|
||||
}
|
||||
|
||||
[HttpPost("/api/images/v1/modifydescription")]
|
||||
public async Task<IActionResult> ModifyImageDescription([FromBody] ModifyDescriptionRequest request)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (!ImageMetadataDB.ModifyDescription(request.ImageName, playerId.Value, request.Description))
|
||||
return Ok(new { success = false });
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("/api/photos/v1/top/today")] // rec.net uses this, not the game
|
||||
public async Task<IActionResult> RecNetGetTodaysTopPhotos()
|
||||
{
|
||||
return Ok(new // temp
|
||||
{
|
||||
RoomId = 1,
|
||||
PlayerId = 1,
|
||||
ImageName = "RecCenter.jpg",
|
||||
Description = "Nothing to see here yet. This is temporary.",
|
||||
Accessibility = 1,
|
||||
TaggedPlayerIds = new List<ulong>(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Type = "ShareCamera"
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/api/images/v1/slideshow")]
|
||||
public async Task<IActionResult> GetSlideshow()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var images = ImageMetadataDB.GetGlobalImages();
|
||||
|
||||
var result = images.Select(img =>
|
||||
{
|
||||
var player = PlayerDB.GetCurrentPlayer((long)img.PlayerId);
|
||||
var room = RoomDB.GetRoom(img.RoomId);
|
||||
|
||||
return new
|
||||
{
|
||||
ImageName = img.ImageName,
|
||||
PlayerId = img.PlayerId,
|
||||
RoomName = room?.Name ?? "Unknown Room!",
|
||||
SavedImageId = img.Id,
|
||||
Username = player?.Player?.Username ?? $"User {img.PlayerId}"
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Images = result,
|
||||
ValidTill = DateTime.UtcNow.AddDays(1).ToString("O")
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/api/images/v2/named")]
|
||||
public async Task<IActionResult> ApiImagesV2Named()
|
||||
{
|
||||
return Ok(Get_named_image());
|
||||
}
|
||||
|
||||
public class NamedImage
|
||||
{
|
||||
public string? FriendlyImageName { get; set; }
|
||||
public string? ImageName { get; set; }
|
||||
public string? StartTime { get; set; }
|
||||
public string? EndTime { get; set; }
|
||||
}
|
||||
|
||||
public static List<NamedImage> Get_named_image()
|
||||
{
|
||||
const string start = "0001-01-01T00:00:00";
|
||||
const string end = "9999-12-31T23:59:59.9999999";
|
||||
const string img = "Gribbly_67.png";
|
||||
|
||||
return
|
||||
[
|
||||
new() { FriendlyImageName = "DormRoomBucket", ImageName = "room_127_1786398699787.png", StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "CreativePuzzles", ImageName = "waeboi_thelegaldocuments.jpg", StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "FeaturedLoft", ImageName = "cat.jpg", StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "TheaterWall", ImageName = "Vanadium.jpg", StartTime = start, EndTime = end },
|
||||
/*new() { FriendlyImageName = "Cafe", ImageName = img, StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "Loft", ImageName = img, StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "UpperLanding", ImageName = img, StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "PVPwall", ImageName = img, StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "BackStairs", ImageName = img, StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "GymWall", ImageName = img, StartTime = start, EndTime = end },
|
||||
new() { FriendlyImageName = "FrontStairs", ImageName = img, StartTime = start, EndTime = end }*/
|
||||
];
|
||||
}
|
||||
|
||||
[HttpPost("/api/images/v1/{imageId}/report")]
|
||||
public async Task<IActionResult> ReportImage(int imageId)
|
||||
{
|
||||
var callerId = AuthStuff.GetPlayerId(Request);
|
||||
if (callerId == null)
|
||||
return StatusCode(403);
|
||||
|
||||
const string reportWebhookUrl = "https://discord.com/api/webhooks/1524345788195410012/0KzQEOsJ_Bpe_WJ1ZFg1z3aWFmBwCDhrdnr54vrb8UDm5S2NhI670ixuziVc3e11maue";
|
||||
|
||||
var image = ImageMetadataDB.GetDb()
|
||||
.GetCollection<ImageMetadataDB.FullSavedImage>("images")
|
||||
.FindById(imageId);
|
||||
|
||||
string imageLink = image != null
|
||||
? $"https://reloxa.xyz/imageserver/{image.ImageName}"
|
||||
: $"https://reloxa.xyz/imageserver/unknown";
|
||||
|
||||
string webhookContent = $"**🟠 Image Reported!** \nImage Id: ``{imageId}`` \nComing From: {callerId.Value} \n{imageLink}";
|
||||
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
var payload = System.Text.Json.JsonSerializer.Serialize(new { content = webhookContent });
|
||||
await httpClient.PostAsync(reportWebhookUrl, new StringContent(payload, System.Text.Encoding.UTF8, "application/json"));
|
||||
}
|
||||
catch { }
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPost("/api/images/v1/deletesaved")]
|
||||
public async Task<IActionResult> DeleteSavedImage([FromBody] DeleteSavedImageRequest request)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request?.ImageName))
|
||||
return BadRequest(new { success = false });
|
||||
|
||||
var db = ImageMetadataDB.GetDb();
|
||||
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
|
||||
var image = col.FindOne(x => x.ImageName == request.ImageName);
|
||||
|
||||
if (image == null)
|
||||
return NotFound("");
|
||||
|
||||
if ((long)image.PlayerId != playerId.Value)
|
||||
return StatusCode(403, new { success = false });
|
||||
|
||||
col.Delete(image.Id);
|
||||
|
||||
var cheerCol = db.GetCollection<ImageMetadataDB.Cheer>("cheers");
|
||||
cheerCol.DeleteMany(c => c.SavedImageId == image.Id);
|
||||
|
||||
await NotiController.SendEvent(playerId.Value, "ImageDeleted", new
|
||||
{
|
||||
ImageName = request.ImageName,
|
||||
SavedImageId = image.Id
|
||||
});
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPost("/api/images/v{version}/modifyaccessibility")]
|
||||
public async Task<IActionResult> ModifyImageAccessibility([FromBody] ModifyAccessibilityRequest request)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request?.ImageName))
|
||||
return BadRequest(new { success = false });
|
||||
|
||||
var db = ImageMetadataDB.GetDb();
|
||||
var col = db.GetCollection<ImageMetadataDB.FullSavedImage>("images");
|
||||
var image = col.FindOne(x => x.ImageName == request.ImageName);
|
||||
|
||||
if (image == null)
|
||||
return NotFound("");
|
||||
|
||||
if ((long)image.PlayerId != playerId.Value)
|
||||
return StatusCode(403, new { success = false });
|
||||
|
||||
if (image.AccessibilityLocked)
|
||||
return StatusCode(403, new { success = false });
|
||||
|
||||
image.Accessibility = request.Accessibility;
|
||||
col.Update(image);
|
||||
|
||||
await NotiController.SendEvent(playerId.Value, "ImageAccessibilityChanged", new
|
||||
{
|
||||
ImageName = request.ImageName,
|
||||
SavedImageId = image.Id,
|
||||
Accessibility = (int)request.Accessibility
|
||||
});
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("/api/images/v5/bulk")]
|
||||
public async Task<IActionResult> GetImagesByBulkIds()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var idsRaw = Request.Query["ids"];
|
||||
|
||||
List<int> idList = idsRaw
|
||||
.SelectMany(x => x.Split(',', StringSplitOptions.RemoveEmptyEntries))
|
||||
.Select(x => x.Trim())
|
||||
.Where(x => int.TryParse(x, out _))
|
||||
.Select(int.Parse)
|
||||
.ToList();
|
||||
|
||||
if (idList.Count == 0)
|
||||
{
|
||||
return new ContentResult
|
||||
{
|
||||
Content = "",
|
||||
ContentType = "application/json",
|
||||
StatusCode = 400
|
||||
};
|
||||
}
|
||||
|
||||
var images = ImageMetadataDB.GetImagesByIds(idList);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(images),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/api/images/v5/player/{playerId}")]
|
||||
public async Task<IActionResult> GetPlayerImages(ulong playerId, [FromQuery] int sort = 0)
|
||||
{
|
||||
var me = AuthStuff.GetPlayerId(Request);
|
||||
if (me == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var images = ImageMetadataDB.GetPlayerImages((ulong)me.Value, playerId)
|
||||
.Where(x => x.SavedImageType == ImageMetadataDB.SavedImageTypeEnum.ShareCamera ||
|
||||
x.SavedImageType == ImageMetadataDB.SavedImageTypeEnum.RoomThumbnail ||
|
||||
x.SavedImageType == ImageMetadataDB.SavedImageTypeEnum.ProfileThumbnail)
|
||||
.ToList();
|
||||
|
||||
var sorted = sort switch
|
||||
{
|
||||
2 => images.OrderByDescending(x => x.CheerCount),
|
||||
3 => images.OrderByDescending(x => x.CommentCount),
|
||||
_ => images.OrderByDescending(x => x.CreatedAt)
|
||||
};
|
||||
|
||||
return Ok(sorted.ToList());
|
||||
}
|
||||
|
||||
[Route("/api/images/v5/cheered/bulk")]
|
||||
public async Task<IActionResult> ImagesCheered([FromQuery] int[] id)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return Unauthorized("");
|
||||
|
||||
long playerId = account.Value;
|
||||
|
||||
var results = id.Select(imageId => new
|
||||
{
|
||||
SavedImageId = imageId,
|
||||
IsCheered = ImageMetadataDB.HasUserCheered(imageId, (ulong)playerId)
|
||||
}).ToList();
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(results),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/api/images/v1/cheer")]
|
||||
public async Task<IActionResult> CheerImage([FromBody] CheerRequest request)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return Unauthorized("");
|
||||
|
||||
long playerId = account.Value;
|
||||
|
||||
bool isCheered = ImageMetadataDB.ToggleCheer(request.SavedImageId, (ulong)playerId, request.Cheer);
|
||||
int cheerCount = ImageMetadataDB.GetCheerCount(request.SavedImageId);
|
||||
|
||||
ImageMetadataDB.UpdateImageCheerCount(request.SavedImageId, cheerCount);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = "",
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/api/images/v4/room/{id:long}")]
|
||||
public async Task<IActionResult> GetRoomImages(
|
||||
long id,
|
||||
int sort = 0,
|
||||
int filter = 0,
|
||||
int take = 50,
|
||||
int skip = 0)
|
||||
{
|
||||
var pid = AuthStuff.GetPlayerId(Request);
|
||||
if (pid == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var images = ImageMetadataDB.GetRoomImages((ulong)pid.Value, id, sort, filter, take, skip);
|
||||
return Ok(images);
|
||||
}
|
||||
|
||||
[HttpGet("/api/images/v6")]
|
||||
public async Task<IActionResult> GetImageV6([FromQuery] string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return BadRequest();
|
||||
|
||||
return Ok(ImageMetadataDB.GetImageByName(name));
|
||||
}
|
||||
|
||||
[HttpGet("imageserver/{*img_path}")]
|
||||
public async Task<IActionResult> ImgServer(
|
||||
string img_path,
|
||||
[FromQuery] int width = 0,
|
||||
[FromQuery] int height = 0,
|
||||
[FromQuery] string? sig = null)
|
||||
{
|
||||
img_path = Uri.UnescapeDataString(img_path ?? "").TrimStart('/');
|
||||
|
||||
if (sig != null && sig != "p1")
|
||||
return BadRequest();
|
||||
|
||||
bool cropSquare = HttpContext.Request.Query.ContainsKey("cropsquare") &&
|
||||
(HttpContext.Request.Query["cropsquare"].ToString().ToLower() == "true" ||
|
||||
HttpContext.Request.Query["cropsquare"].ToString() == "1");
|
||||
|
||||
if (img_path.EndsWith(".ig"))
|
||||
{
|
||||
cropSquare = true;
|
||||
img_path = img_path.Substring(0, img_path.Length - 3);
|
||||
}
|
||||
|
||||
HttpContext.Response.Headers.Append("Content-Disposition", $"inline; filename=\"{Path.GetFileName(img_path)}\"");
|
||||
HttpContext.Response.Headers.Append("Access-Control-Allow-Origin", "*");
|
||||
HttpContext.Response.Headers.Append("Access-Control-Allow-Headers", "Content-Type, Authorization, Cache-Control");
|
||||
HttpContext.Response.Headers.Append("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
Response.Headers["Cache-Control"] = "public, max-age=14400";
|
||||
|
||||
var etag = $"\"{img_path.GetHashCode()}\"";
|
||||
HttpContext.Response.Headers.Append("ETag", etag);
|
||||
|
||||
string imagesPath = Path.Join(Program.dataDir, "Images");
|
||||
string cdnImagesPath = Path.GetFullPath(Path.Join(Program.dataDir, "..", "Data", "cdn", "images"));
|
||||
|
||||
string? foundLocalPath = null;
|
||||
var searchPaths = new List<string>();
|
||||
|
||||
if (Directory.Exists(imagesPath))
|
||||
searchPaths.Add(imagesPath);
|
||||
|
||||
if (Directory.Exists(cdnImagesPath))
|
||||
searchPaths.Add(cdnImagesPath);
|
||||
|
||||
foreach (var basePath in searchPaths)
|
||||
{
|
||||
string directPath = Path.Join(basePath, img_path);
|
||||
if (System.IO.File.Exists(directPath))
|
||||
{
|
||||
foundLocalPath = directPath;
|
||||
break;
|
||||
}
|
||||
|
||||
var subDirectories = Directory.EnumerateDirectories(basePath, "*", SearchOption.AllDirectories);
|
||||
foreach (var dir in subDirectories)
|
||||
{
|
||||
string testPath = Path.Join(dir, img_path);
|
||||
if (System.IO.File.Exists(testPath))
|
||||
{
|
||||
foundLocalPath = testPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundLocalPath != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (foundLocalPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageBytes = await System.IO.File.ReadAllBytesAsync(foundLocalPath);
|
||||
imageBytes = await ProcessImageAsync(imageBytes, cropSquare, width, height);
|
||||
|
||||
AppendSignatureHeader(imageBytes, sig, img_path);
|
||||
return File(imageBytes, GetMimeType(img_path));
|
||||
}
|
||||
catch
|
||||
{
|
||||
var fallbackBytes = await System.IO.File.ReadAllBytesAsync(foundLocalPath);
|
||||
AppendSignatureHeader(fallbackBytes, sig, img_path);
|
||||
return File(fallbackBytes, "image/jpg");
|
||||
}
|
||||
}
|
||||
|
||||
string recNetLocalPath = Path.Join(imagesPath, "RecNet", img_path);
|
||||
|
||||
/*try
|
||||
{
|
||||
using HttpClient client = new();
|
||||
byte[] data = await client.GetByteArrayAsync($"https://recroomcdn.blob.core.windows.net/img/{img_path}");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(recNetLocalPath)!);
|
||||
await System.IO.File.WriteAllBytesAsync(recNetLocalPath, data);
|
||||
|
||||
try
|
||||
{
|
||||
var processed = await ProcessImageAsync(data, cropSquare, width, height);
|
||||
AppendSignatureHeader(processed, sig, img_path);
|
||||
return File(processed, GetMimeType(img_path));
|
||||
}
|
||||
catch
|
||||
{
|
||||
AppendSignatureHeader(data, sig, img_path);
|
||||
return File(data, GetMimeType(img_path));
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
Console.WriteLine($"[Radie.app Fetch Failed] {ex.Message}");
|
||||
}*/
|
||||
|
||||
string? defaultFallbackPath = null;
|
||||
foreach (var basePath in searchPaths)
|
||||
{
|
||||
string directPath = Path.Join(basePath, "NotFound.png");
|
||||
if (System.IO.File.Exists(directPath))
|
||||
{
|
||||
defaultFallbackPath = directPath;
|
||||
break;
|
||||
}
|
||||
|
||||
var subDirectories = Directory.EnumerateDirectories(basePath, "*", SearchOption.AllDirectories);
|
||||
foreach (var dir in subDirectories)
|
||||
{
|
||||
string testPath = Path.Join(dir, "NotFound.png");
|
||||
if (System.IO.File.Exists(testPath))
|
||||
{
|
||||
defaultFallbackPath = testPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultFallbackPath != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (defaultFallbackPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fallbackBytes = await System.IO.File.ReadAllBytesAsync(defaultFallbackPath);
|
||||
try
|
||||
{
|
||||
fallbackBytes = await ProcessImageAsync(fallbackBytes, cropSquare, width, height);
|
||||
}
|
||||
catch { }
|
||||
|
||||
AppendSignatureHeader(fallbackBytes, sig, img_path);
|
||||
return File(fallbackBytes, "image/png");
|
||||
}
|
||||
catch (Exception fallbackEx)
|
||||
{
|
||||
Console.WriteLine($"[critical fallback failed] Could not read NotFound.png file: {fallbackEx.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return NotFound("");
|
||||
}
|
||||
|
||||
private void AppendSignatureHeader(byte[] payload, string? sig, string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sig))
|
||||
return;
|
||||
|
||||
string headerValue = ImageSignatureValidation.SignPayload(payload, sig);
|
||||
HttpContext.Response.Headers.Append("Content-Signature", headerValue);
|
||||
}
|
||||
|
||||
private static string GetMimeType(string filePath)
|
||||
{
|
||||
var provider = new Microsoft.AspNetCore.StaticFiles.FileExtensionContentTypeProvider();
|
||||
if (!provider.TryGetContentType(filePath, out var contentType))
|
||||
{
|
||||
contentType = "image/jpg";
|
||||
}
|
||||
return contentType;
|
||||
}
|
||||
|
||||
public class DeleteSavedImageRequest
|
||||
{
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ModifyAccessibilityRequest
|
||||
{
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public ImageMetadataDB.ImageAccessibility Accessibility { get; set; }
|
||||
}
|
||||
|
||||
private static async Task<byte[]> ProcessImageAsync(byte[] imageBytes, bool cropSquare, int width, int height)
|
||||
{
|
||||
using var ms = new MemoryStream(imageBytes);
|
||||
using var image = await SixLabors.ImageSharp.Image.LoadAsync<Rgba32>(ms);
|
||||
|
||||
if (cropSquare)
|
||||
{
|
||||
int size = Math.Min(image.Width, image.Height);
|
||||
int x = (image.Width - size) / 2;
|
||||
int y = (image.Height - size) / 2;
|
||||
image.Mutate(ctx => ctx.Crop(new Rectangle(x, y, size, size)));
|
||||
|
||||
int targetSize = width > 0 ? width : height;
|
||||
if (targetSize > 0)
|
||||
{
|
||||
image.Mutate(ctx => ctx.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(targetSize, targetSize),
|
||||
Mode = ResizeMode.Max,
|
||||
Sampler = KnownResamplers.Lanczos3
|
||||
}));
|
||||
}
|
||||
}
|
||||
else if (width > 0 || height > 0)
|
||||
{
|
||||
int resizeWidth = width;
|
||||
int resizeHeight = height;
|
||||
|
||||
if (width > 0 && height == 0)
|
||||
resizeHeight = (int)((double)image.Height / image.Width * width);
|
||||
else if (height > 0 && width == 0)
|
||||
resizeWidth = (int)((double)image.Width / image.Height * height);
|
||||
|
||||
image.Mutate(ctx => ctx.Resize(resizeWidth, resizeHeight));
|
||||
}
|
||||
|
||||
using var output = new MemoryStream();
|
||||
await image.SaveAsync(output, new PngEncoder());
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,904 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes.DBs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static Vanadium.Classes.DBs.DBClasses.ClubsDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class ClubsController : ControllerBase
|
||||
{
|
||||
private static int ParseVisibility(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return -1;
|
||||
return raw.ToLowerInvariant() switch
|
||||
{
|
||||
"public" => (int)ClubVisibility.Public,
|
||||
"unlisted" => (int)ClubVisibility.Unlisted,
|
||||
"private" => (int)ClubVisibility.Private,
|
||||
"0" => (int)ClubVisibility.Private,
|
||||
"1" => (int)ClubVisibility.Public,
|
||||
"2" => (int)ClubVisibility.Unlisted,
|
||||
_ => -1
|
||||
};
|
||||
}
|
||||
|
||||
private static int ParseJoinability(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return -1;
|
||||
return raw.ToLowerInvariant() switch
|
||||
{
|
||||
"open" => (int)ClubJoinability.Open,
|
||||
"approvalrequired" => (int)ClubJoinability.ApprovalRequired,
|
||||
"closed" => (int)ClubJoinability.Closed,
|
||||
"0" => (int)ClubJoinability.Open,
|
||||
"1" => (int)ClubJoinability.ApprovalRequired,
|
||||
"2" => (int)ClubJoinability.Closed,
|
||||
_ => -1
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/club/{clubId}/members/{userId}")]
|
||||
public IActionResult GetClubMember(long clubId, long userId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
var member = ClubsDB.GetClubMembers(clubId).FirstOrDefault(m => m.AccountId == userId);
|
||||
if (member == null) return Ok(new {});
|
||||
return Ok(member);
|
||||
}
|
||||
|
||||
[HttpGet("/club/search")]
|
||||
public IActionResult SearchClubs(
|
||||
[FromQuery] string? query,
|
||||
[FromQuery] string? category,
|
||||
[FromQuery] int sort = 0,
|
||||
[FromQuery] int count = 30,
|
||||
[FromQuery] int skip = 0)
|
||||
{
|
||||
count = Math.Clamp(count, 1, 100);
|
||||
var result = ClubsDB.Search(query, category, sort, count, skip);
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(result),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/club/create")]
|
||||
public IActionResult CreateClub(
|
||||
[FromForm] string name,
|
||||
[FromForm] string? description,
|
||||
[FromForm] string? category)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.CreateClub(
|
||||
id.Value,
|
||||
name.Trim(),
|
||||
description?.Trim() ?? "",
|
||||
(category ?? "Social").Trim());
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new
|
||||
{
|
||||
success = true,
|
||||
error = "",
|
||||
value = ClubsDB.ToDetails(club, id.Value)
|
||||
}),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/club/categoryTags")]
|
||||
public IActionResult GetClubCategoryTags()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var tags = new List<string>
|
||||
{
|
||||
"Social",
|
||||
"Creative",
|
||||
"Competitive",
|
||||
"Casual",
|
||||
"Educational",
|
||||
"Entertainment",
|
||||
"Lifestyle"
|
||||
};
|
||||
|
||||
return Ok(tags);
|
||||
}
|
||||
|
||||
[HttpGet("/club/mine/created")]
|
||||
public IActionResult GetMyCreatedClubs()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var clubs = ClubsDB.GetClubsCreatedBy(id.Value)
|
||||
.Select(ClubsDB.ToSummary)
|
||||
.ToList();
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(clubs),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/club/mine/member")]
|
||||
public IActionResult GetMyMemberClubs()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var clubs = ClubsDB.GetClubsJoinedBy(id.Value)
|
||||
.Where(c => c.Visibility != 2)
|
||||
.Select(ClubsDB.ToSummary)
|
||||
.ToList();
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(clubs),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/club/account/{player}/created")]
|
||||
public IActionResult GetOthersCreatedClubs(long player)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var clubs = ClubsDB.GetClubsCreatedBy(player)
|
||||
.Where(c => c.Visibility != 2)
|
||||
.Select(ClubsDB.ToSummary)
|
||||
.ToList();
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(clubs),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/club/{clubId}")]
|
||||
[HttpGet("/club/{clubId}/details")]
|
||||
public IActionResult GetClubDetails(long clubId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return Ok(new ClubDetailsDTO
|
||||
{
|
||||
Club = ClubsDB.ToSummary(club),
|
||||
CustomTags = club.CustomTags,
|
||||
AdditionalImages = club.AdditionalImages,
|
||||
CoownerPermissions = club.Permissions.FirstOrDefault(p => p.Type == (int)ClubMemberType.CoOwner),
|
||||
ModeratorPermissions = club.Permissions.FirstOrDefault(p => p.Type == (int)ClubMemberType.Moderator),
|
||||
MemberPermissions = club.Permissions.FirstOrDefault(p => p.Type == (int)ClubMemberType.Member),
|
||||
MyMembershipType = club.Members
|
||||
.FirstOrDefault(m => m.AccountId == id.Value)?.MemberType ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/club/{clubId}/members")]
|
||||
public IActionResult GetClubMembers(long clubId, [FromQuery] int? membershipType)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
var members = ClubsDB.GetClubMembers(clubId);
|
||||
if (membershipType.HasValue)
|
||||
members = members.Where(m => m.MembershipType == membershipType.Value).ToList();
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { value = members, success = true, error_id = (int?)null, error = (string?)null }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpDelete("/club/{clubId}")]
|
||||
public IActionResult DeleteClub(long clubId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
if (club.CreatorAccountId != id.Value)
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to delete club {clubId} but is not the creator.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
ClubsDB.DeleteClub(clubId);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/modify")]
|
||||
public IActionResult ModifyClub(
|
||||
long clubId,
|
||||
[FromForm] string? name,
|
||||
[FromForm] string? description,
|
||||
[FromForm] string? category)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.CanEditDetails(clubId, id.Value))
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to modify club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var club = ClubsDB.Modify(
|
||||
clubId,
|
||||
(name ?? existing.Name).Trim(),
|
||||
(description ?? existing.Description ?? "").Trim(),
|
||||
(category ?? existing.Category).Trim(),
|
||||
id.Value);
|
||||
|
||||
if (club == null) return Unauthorized("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/modifydetails")]
|
||||
public IActionResult ModifyClubDetails(
|
||||
long clubId,
|
||||
[FromForm] string? visibility,
|
||||
[FromForm] string? joinability,
|
||||
[FromForm] string? allowJuniors,
|
||||
[FromForm] string? customTags)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (!ClubsDB.CanEditDetails(clubId, id.Value))
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to modify club {clubId} but does not have permission. (2)");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
int? vis = visibility != null ? ParseVisibility(visibility) : (int?)null;
|
||||
int? join = joinability != null ? ParseJoinability(joinability) : (int?)null;
|
||||
if (join == 1) return StatusCode(501);
|
||||
bool? juniors = null;
|
||||
if (allowJuniors != null)
|
||||
juniors = allowJuniors.ToLowerInvariant() is "true" or "1";
|
||||
|
||||
List<string>? tags = null;
|
||||
if (customTags != null)
|
||||
tags = customTags
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(t => t.Trim())
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToList();
|
||||
|
||||
var club = ClubsDB.ModifyDetails(clubId, vis, join, juniors, tags, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/minlevel")]
|
||||
public IActionResult SetMinLevel(long clubId, [FromForm] int minLevel)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (!ClubsDB.CanEditDetails(clubId, id.Value))
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to set min level for club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var club = ClubsDB.SetMinLevel(clubId, minLevel, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/mainimage")]
|
||||
public IActionResult SetMainImage(long clubId, [FromForm] string imageName)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.CanEditDetails(clubId, id.Value))
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to set the main image for club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var club = ClubsDB.SetMainImage(clubId, imageName, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpDelete("/club/{clubId}/mainimage")]
|
||||
public IActionResult DeleteMainImage(long clubId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.CanEditDetails(clubId, id.Value))
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to delete the main image for club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
if (existing.AdditionalImages.Count == 0)
|
||||
return StatusCode(405);
|
||||
|
||||
var club = ClubsDB.DeleteMainImage(clubId, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/additionalimage/{index}")]
|
||||
public IActionResult SetAdditionalImage(long clubId, int index, [FromForm] string imageName)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.CanEditDetails(clubId, id.Value))
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to set additional image {index} for club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var club = ClubsDB.SetAdditionalImage(clubId, index, imageName, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/clubhouse")]
|
||||
public IActionResult SetClubhouse(long clubId, [FromForm] long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.IsOwnerOrCoOwner(clubId, id.Value) && existing.CreatorAccountId != id.Value)
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to set clubhouse for club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var club = ClubsDB.SetClubhouse(clubId, roomId, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpDelete("/club/{clubId}/clubhouse")]
|
||||
public IActionResult RemoveClubhouse(long clubId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.IsOwnerOrCoOwner(clubId, id.Value) && existing.CreatorAccountId != id.Value)
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to remove clubhouse for club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var club = ClubsDB.RemoveClubhouse(clubId, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/club/{clubId}/hasDisabledClubChat")]
|
||||
public IActionResult HasDisabledClubChat(long clubId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/members/requesttojoin")]
|
||||
public async Task<IActionResult> RequestToJoin(long clubId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
if (existing.Joinability == (int)ClubJoinability.Closed) return BadRequest(new { success = false, error = "The club is invite only", error_id = "Club.InviteOnly", value = (object?)null });
|
||||
|
||||
var club = ClubsDB.JoinClub(clubId, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
var joined = club.Members.FirstOrDefault(m => m.AccountId == id.Value);
|
||||
if (joined != null)
|
||||
{
|
||||
await NotificationsController.BroadcastClubMembershipUpdate(joined.MemberId, id.Value, clubId, joined.MemberType, joined.JoinedAt);
|
||||
await NotificationsController.BroadcastCreatorClubSubscriptionUpdate(club.CreatorAccountId, clubId, joined.MemberType);
|
||||
}
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/club/{clubId}/members/leave")]
|
||||
public async Task<IActionResult> RequestLeaveClub(long clubId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
var leaving = existing.Members.FirstOrDefault(m => m.AccountId == id.Value);
|
||||
|
||||
var club = ClubsDB.LeaveClub(clubId, id.Value);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
if (leaving != null)
|
||||
{
|
||||
await NotificationsController.BroadcastClubMembershipUpdate(leaving.MemberId, id.Value, clubId, 0, DateTime.UtcNow);
|
||||
await NotificationsController.BroadcastCreatorClubSubscriptionUpdate(existing.CreatorAccountId, clubId, 0);
|
||||
}
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/permissions/{membershipType}")]
|
||||
public IActionResult SetPermissions(long clubId, int membershipType, [FromForm] bool approveMember, [FromForm] bool banUnban, [FromForm] bool createEvent, [FromForm] bool editDetails, [FromForm] bool editPermissionSettings, [FromForm] bool postAnnouncement)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var existing = ClubsDB.GetClub(clubId);
|
||||
if (existing == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.IsOwnerOrCoOwner(clubId, id.Value))
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to set permissions for club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var club = ClubsDB.SetPermissions(clubId, membershipType, approveMember, banUnban, createEvent, editDetails, editPermissionSettings, postAnnouncement);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = ClubsDB.ToDetails(club, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/club/home/me")]
|
||||
public IActionResult GetMyHomeClub()
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null) return Unauthorized("");
|
||||
|
||||
int homeClubId = (int)player.Player.PlayerExtra.HomeClubhouseId;
|
||||
if (homeClubId == 0 || homeClubId == null) return NotFound("");
|
||||
|
||||
var club = ClubsDB.GetClub(homeClubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(ClubsDB.ToSummary(club)),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/home/me")]
|
||||
public IActionResult SetMyHomeClub([FromForm] long clubId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
player.Player.PlayerExtra.HomeClubhouseId = clubId;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
return Ok(new { success = true, error = (string?)null, value = (object?)null });
|
||||
}
|
||||
|
||||
[HttpDelete("/club/home/me")]
|
||||
public IActionResult DeleteMyHomeClub([FromForm] long clubId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null) return Unauthorized("");
|
||||
|
||||
player.Player.PlayerExtra.HomeClubhouseId = 0;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
return Ok(new { success = true, error = (string?)null, value = (object?)null });
|
||||
}
|
||||
|
||||
[HttpGet("/announcements/mine")]
|
||||
public IActionResult GetMyAnnouncements()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var result = ClubsDB.GetAnnouncementsForMyClubs(id.Value);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = result }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("/announcements/club/{clubId}")]
|
||||
public IActionResult GetClubAnnouncements(long clubId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var result = ClubsDB.GetClubAnnouncements(clubId, id.Value);
|
||||
if (result == null) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = result }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/announcements/club/{clubId}")]
|
||||
public IActionResult CreateAnnouncement(
|
||||
long clubId,
|
||||
[FromForm] string title,
|
||||
[FromForm] string body,
|
||||
[FromForm] string? imageName,
|
||||
[FromForm] string? meta)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title) || title.Length > 50)
|
||||
return BadRequest(new { success = false, error = "Title must be between 1 and 50 characters." });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body) || body.Length > 1000)
|
||||
return BadRequest(new { success = false, error = "Body must be between 1 and 1000 characters." });
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.CanPostAnnouncement(clubId, id.Value))
|
||||
return StatusCode(403);
|
||||
|
||||
var announcement = ClubsDB.CreateAnnouncement(clubId, id.Value, title, body, imageName, meta);
|
||||
if (announcement == null) return StatusCode(500);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = announcement.AnnouncementId }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/announcements/club/{clubId}/{announcementId}")]
|
||||
public IActionResult EditAnnouncement(
|
||||
long clubId,
|
||||
long announcementId,
|
||||
[FromForm] string title,
|
||||
[FromForm] string body,
|
||||
[FromForm] string? imageName,
|
||||
[FromForm] string? meta)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title) || title.Length > 50)
|
||||
return BadRequest(new { success = false, error = "Title must be between 1 and 50 characters." });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(body) || body.Length > 1000)
|
||||
return BadRequest(new { success = false, error = "Body must be between 1 and 1000 characters." });
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
var announcement = ClubsDB.EditAnnouncement(clubId, announcementId, id.Value, title, body, imageName, meta);
|
||||
if (announcement == null) return StatusCode(403);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = "", value = announcement.AnnouncementId }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpDelete("/announcements/club/{clubId}/{announcementId}")]
|
||||
public IActionResult DeleteAnnouncement(long clubId, long announcementId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
bool deleted = ClubsDB.DeleteAnnouncement(clubId, announcementId, id.Value);
|
||||
if (!deleted) return StatusCode(403);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = (object?)null }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPost("/announcements/club/{clubId}/{announcementId}/read")]
|
||||
public IActionResult MarkAnnouncementRead(long clubId, long announcementId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
bool ok = ClubsDB.MarkAnnouncementRead(clubId, announcementId, id.Value);
|
||||
if (!ok) return NotFound("");
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = (object?)null }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/members/{targetId}/role")]
|
||||
public async Task<IActionResult> SetMemberRole(long clubId, long targetId, [FromForm] int membershipType)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
if (!ClubsDB.IsOwnerOrCoOwner(clubId, id.Value) && club.CreatorAccountId != id.Value)
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to set role in club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var target = club.Members.FirstOrDefault(m => m.AccountId == targetId);
|
||||
if (target == null) return NotFound("");
|
||||
|
||||
bool ok = ClubsDB.SetMemberRole(clubId, targetId, membershipType, id.Value);
|
||||
if (!ok) return NotFound("");
|
||||
|
||||
await NotificationsController.BroadcastClubMembershipUpdate(target.MemberId, targetId, clubId, membershipType, target.JoinedAt);
|
||||
|
||||
var updated = ClubsDB.GetClub(clubId);
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = updated != null ? ClubsDB.ToDetails(updated, id.Value) : null }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/members/{targetId}/kick")]
|
||||
public async Task<IActionResult> KickMember(long clubId, long targetId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
var target = club.Members.FirstOrDefault(m => m.AccountId == targetId);
|
||||
|
||||
var result = ClubsDB.KickMember(clubId, targetId, id.Value);
|
||||
if (result == null)
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to kick {targetId} from club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
if (target != null)
|
||||
await NotificationsController.BroadcastClubMembershipUpdate(target.MemberId, targetId, clubId, 0, DateTime.UtcNow);
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = ClubsDB.ToDetails(result, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/members/{targetId}/ban")]
|
||||
public async Task<IActionResult> BanMember(long clubId, long targetId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
var target = club.Members.FirstOrDefault(m => m.AccountId == targetId);
|
||||
|
||||
bool ok = ClubsDB.BanMember(clubId, targetId, id.Value);
|
||||
if (!ok)
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to ban {targetId} from club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
if (target != null)
|
||||
await NotificationsController.BroadcastClubMembershipUpdate(target.MemberId, targetId, clubId, 0, DateTime.UtcNow);
|
||||
|
||||
var updated = ClubsDB.GetClub(clubId);
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = updated != null ? ClubsDB.ToDetails(updated, id.Value) : null }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/members/{targetId}/unban")]
|
||||
public IActionResult UnbanMember(long clubId, long targetId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
bool ok = ClubsDB.UnbanMember(clubId, targetId, id.Value);
|
||||
if (!ok)
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to unban {targetId} from club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var updated = ClubsDB.GetClub(clubId);
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = updated != null ? ClubsDB.ToDetails(updated, id.Value) : null }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("/club/{clubId}/members/{targetId}/approve")]
|
||||
public async Task<IActionResult> ApproveMember(long clubId, long targetId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var club = ClubsDB.GetClub(clubId);
|
||||
if (club == null) return NotFound("");
|
||||
|
||||
var result = ClubsDB.ApproveMember(clubId, targetId, id.Value);
|
||||
if (result == null)
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, $"Attempted to approve {targetId} in club {clubId} but does not have permission.");
|
||||
return StatusCode(403);
|
||||
}
|
||||
|
||||
var member = result.Members.FirstOrDefault(m => m.AccountId == targetId);
|
||||
if (member != null)
|
||||
{
|
||||
await NotificationsController.BroadcastClubMembershipUpdate(member.MemberId, targetId, clubId, member.MemberType, member.JoinedAt);
|
||||
await NotificationsController.BroadcastCreatorClubSubscriptionUpdate(result.CreatorAccountId, clubId, member.MemberType);
|
||||
}
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(new { success = true, error = (string?)null, value = ClubsDB.ToDetails(result, id.Value) }),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//using Microsoft.AspNetCore.Mvc;
|
||||
//using Vanadium.Auth;
|
||||
//using Vanadium.Classes;
|
||||
//using Vanadium.Classes.DBs;
|
||||
//using System.Text;
|
||||
//using System.Text.Json;
|
||||
//using System.Text.Json.Serialization;
|
||||
//using Vanadium.Classes;
|
||||
|
||||
//namespace Vanadium.Controllers
|
||||
//{
|
||||
// [ApiController]
|
||||
// public class PhotonController : ControllerBase
|
||||
// {
|
||||
// private readonly HttpClient _httpClient = new();
|
||||
|
||||
// private bool IsRealClient()
|
||||
// {
|
||||
// var accept = Request.Headers["Accept"].ToString();
|
||||
// var userAgent = Request.Headers["User-Agent"].ToString();
|
||||
|
||||
// return userAgent == "BestHTTP"
|
||||
// && accept.Contains("application/json")
|
||||
// && accept.Contains("image/png")
|
||||
// && accept.Contains("image/jpeg");
|
||||
// }
|
||||
|
||||
// private async Task SendFakeClientWebhook(string ip, string headersPretty, string body)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// const int maxLen = 950;
|
||||
// var truncatedHeaders = headersPretty.Length > maxLen
|
||||
// ? headersPretty.Substring(0, maxLen) + "\n... (truncated)"
|
||||
// : headersPretty;
|
||||
|
||||
// var embed = new
|
||||
// {
|
||||
// title = "⚠️ Fake Client Hit AppSettings (served decoy)",
|
||||
// color = 15158332,
|
||||
// fields = new object[]
|
||||
// {
|
||||
// new { name = "IP", value = $"`{ip}`", inline = false },
|
||||
// new { name = "Headers", value = $"```\n{truncatedHeaders}\n```", inline = false },
|
||||
// new { name = "Body", value = $"json```\n{body}\n```", inline = false }
|
||||
// },
|
||||
// timestamp = DateTime.UtcNow.ToString("O")
|
||||
// };
|
||||
// var payload = new
|
||||
// {
|
||||
// embeds = new[] { embed }
|
||||
// };
|
||||
// var content = new StringContent(
|
||||
// JsonSerializer.Serialize(payload),
|
||||
// Encoding.UTF8,
|
||||
// "application/json"
|
||||
// );
|
||||
// await _httpClient.PostAsync(ServerConfig.PhotonWebhook, content);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"Fake client webhook error: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
|
||||
// [HttpGet("/photon/appSettings")]
|
||||
// public IActionResult GetPhoton()
|
||||
// {
|
||||
// return NotFound("");
|
||||
// Console.WriteLine("photon called");
|
||||
// if (!IsRealClient()) // dont touch this a fix for the photon botter botting us
|
||||
// {
|
||||
// var fakeIp = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
// var fakeHeaders = GetHeadersPretty();
|
||||
// var decoy = new AppSettings
|
||||
// {
|
||||
// PhotonRealtimeId = "aaae496f-ece0-4b1b-82ac-aea17d766c82",
|
||||
// PhotonVoiceId = "d1b950c8-95e4-4725-9a3b-f6fc57853484",
|
||||
// PhotonRegion = "us"
|
||||
// };
|
||||
// string decoyJson = JsonSerializer.Serialize(decoy, new JsonSerializerOptions { WriteIndented = true });
|
||||
// _ = SendFakeClientWebhook(fakeIp, fakeHeaders, decoyJson);
|
||||
// return Ok(decoy);
|
||||
// }
|
||||
|
||||
// var ip = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
// var settings = ServerConfig.appSettings;
|
||||
|
||||
// string settingsJson = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
|
||||
// string headersPretty = GetHeadersPretty();
|
||||
|
||||
// _ = SendAppSettingsWebhook(ip, headersPretty, settingsJson);
|
||||
// return Ok(settings);
|
||||
//}
|
||||
|
||||
// private string GetHeadersPretty()
|
||||
// {
|
||||
// var sb = new StringBuilder();
|
||||
// foreach (var header in Request.Headers)
|
||||
// {
|
||||
// sb.AppendLine($"{header.Key}: {header.Value}");
|
||||
// }
|
||||
// return sb.ToString();
|
||||
// }
|
||||
|
||||
// [HttpPost("/photon/{url}")]
|
||||
// public async Task<IActionResult> stub(string url)
|
||||
// {
|
||||
// return await HandleStub("", "/photon", url);
|
||||
// }
|
||||
|
||||
// [HttpPost("/photon_realtime/{url}")]
|
||||
// public async Task<IActionResult> stubRealtime(string url)
|
||||
// {
|
||||
// return await HandleStub("Realtime", "/photon_realtime", url);
|
||||
// }
|
||||
|
||||
// [HttpPost("/photon_voice/{url}")]
|
||||
// public async Task<IActionResult> stubVoice(string url)
|
||||
// {
|
||||
// return await HandleStub("Voice", "/photon_voice", url);
|
||||
// }
|
||||
|
||||
// private async Task<IActionResult> HandleStub(string type, string urltype, string url)
|
||||
// {
|
||||
// Request.EnableBuffering();
|
||||
// string rawBody;
|
||||
// using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, bufferSize: 1024, leaveOpen: true))
|
||||
// {
|
||||
// rawBody = await reader.ReadToEndAsync();
|
||||
// Request.Body.Position = 0;
|
||||
// }
|
||||
// string prettyJson = rawBody;
|
||||
// try
|
||||
// {
|
||||
// if (!string.IsNullOrWhiteSpace(rawBody))
|
||||
// {
|
||||
// using var doc = JsonDocument.Parse(rawBody);
|
||||
// prettyJson = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
|
||||
// }
|
||||
// }
|
||||
// catch (JsonException)
|
||||
// {
|
||||
// }
|
||||
// Console.WriteLine($"Stub called: url={urltype}/{url}, body={rawBody}");
|
||||
// await SendStubWebhook(type, urltype, url, prettyJson);
|
||||
// return Ok(new
|
||||
// {
|
||||
// ResultCode = 0
|
||||
// });
|
||||
// }
|
||||
|
||||
// private async Task SendAppSettingsWebhook(string ip, string headersPretty, string bodyValue)
|
||||
//{
|
||||
// try
|
||||
// {
|
||||
// const int maxLen = 950;
|
||||
// var truncatedHeaders = headersPretty.Length > maxLen
|
||||
// ? headersPretty.Substring(0, maxLen) + "\n... (truncated)"
|
||||
// : headersPretty;
|
||||
|
||||
// var embed = new
|
||||
// {
|
||||
// title = "Photon GET AppSettings Called",
|
||||
// color = 15105570,
|
||||
// fields = new object[]
|
||||
// {
|
||||
// new { name = "IP", value = $"`{ip}`", inline = false },
|
||||
// new { name = "Headers", value = $"json```\n{truncatedHeaders}\n```", inline = false },
|
||||
// new { name = "Body", value = $"json```\n{bodyValue}\n```", inline = false }
|
||||
// },
|
||||
// timestamp = DateTime.UtcNow.ToString("O")
|
||||
// };
|
||||
// var payload = new
|
||||
// {
|
||||
// embeds = new[] { embed }
|
||||
// };
|
||||
// var content = new StringContent(
|
||||
// JsonSerializer.Serialize(payload),
|
||||
// Encoding.UTF8,
|
||||
// "application/json"
|
||||
// );
|
||||
// var response = await _httpClient.PostAsync(ServerConfig.PhotonWebhook, content);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// }
|
||||
//}
|
||||
// private async Task SendStubWebhook(string type, string urltype, string url, string prettyJson)
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// const int maxLen = 950;
|
||||
// var body = string.IsNullOrWhiteSpace(prettyJson) ? "(empty)" : prettyJson;
|
||||
// var truncated = body.Length > maxLen
|
||||
// ? body.Substring(0, maxLen) + "\n... (truncated)"
|
||||
// : body;
|
||||
// var bodyValue = body == "(empty)"
|
||||
// ? "(empty)"
|
||||
// : $"```json\n{truncated}\n```";
|
||||
// var embed = new
|
||||
// {
|
||||
// title = $"Photon {type} Called",
|
||||
// color = 15105570,
|
||||
// fields = new object[]
|
||||
// {
|
||||
// new { name = "URL", value = $"`{urltype}/{url}`", inline = false },
|
||||
// new { name = "Body", value = bodyValue, inline = false }
|
||||
// },
|
||||
// timestamp = DateTime.UtcNow.ToString("O")
|
||||
// };
|
||||
// var payload = new
|
||||
// {
|
||||
// embeds = new[] { embed }
|
||||
// };
|
||||
// var content = new StringContent(
|
||||
// JsonSerializer.Serialize(payload),
|
||||
// Encoding.UTF8,
|
||||
// "application/json"
|
||||
// );
|
||||
// var response = await _httpClient.PostAsync(ServerConfig.PhotonWebhook, content);
|
||||
// Console.WriteLine($"Stub webhook sent: {response.StatusCode}");
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Console.WriteLine($"Stub webhook error: {ex.Message}");
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,434 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes;
|
||||
using static Vanadium.Classes.DBs.DBClasses.EventDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public partial class PlayerEventsController : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/playerevents/v1/tagfilters")]
|
||||
public async Task<IActionResult> PlayerEventTags()
|
||||
{
|
||||
return Ok(System.Text.Json.JsonSerializer.Deserialize<object>(
|
||||
System.IO.File.ReadAllText(
|
||||
Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "EventTags.json"))));
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v1/all")]
|
||||
public async Task<IActionResult> GetAllPlayerEvents()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var created = EventDB.GetPlayerEvents(id.Value);
|
||||
var attending = EventDB.GetAttendingEvents(id.Value);
|
||||
var hosting = EventDB.GetHostingEvents(id.Value);
|
||||
var responses = EventDB.GetResponsesForPlayer(id.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Created = created,
|
||||
Attending = attending,
|
||||
Hosting = hosting,
|
||||
Responses = responses
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v1/searchlive")]
|
||||
public async Task<IActionResult> GetLiveEvents()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var events = EventDB.GetLiveEvents();
|
||||
return Ok(events);
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v1/search")]
|
||||
public async Task<IActionResult> SearchEvents(
|
||||
[FromQuery] string? query,
|
||||
[FromQuery] string? sort,
|
||||
[FromQuery] string? scheduleFilter)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var events = EventDB.SearchEvents(query, sort, scheduleFilter);
|
||||
return Ok(events);
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v1/room/{roomId}")]
|
||||
public async Task<IActionResult> GetEventsForRoom(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var events = EventDB.GetEventsByRoomId(roomId);
|
||||
return Ok(events);
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v1/club")]
|
||||
[HttpGet("/api/playerevents/v1/clubs")]
|
||||
public async Task<IActionResult> GetEventsForClub()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
return Ok(new List<object>());
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v1/{eventId}")]
|
||||
public async Task<IActionResult> GetPlayerEvent(long eventId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var ev = EventDB.GetPlayerEventById(eventId);
|
||||
if (ev == null)
|
||||
return NotFound("");
|
||||
|
||||
return Ok(ev);
|
||||
}
|
||||
|
||||
[HttpPost("/api/playerevents/v2")]
|
||||
public async Task<IActionResult> CreatePlayerEvent([FromBody] PlayerEventRequest request)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (Sanitize.ContainsSwears(request.Name) || Sanitize.ContainsSwears(request.Description))
|
||||
return Ok(new PlayerEventResponse { PlayerEvent = null, Result = 14 });
|
||||
|
||||
var response = EventDB.CreatePlayerEvent(id.Value, request);
|
||||
if (response.Result != 0)
|
||||
return Ok(response);
|
||||
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventCreated",
|
||||
Msg = response.PlayerEvent
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpDelete("/api/playerevents/v2/delete/{eventId}")]
|
||||
public async Task<IActionResult> DeletePlayerEvent(long eventId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var deleted = EventDB.DeletePlayerEvent(eventId, id.Value);
|
||||
return Ok(new { success = deleted });
|
||||
}
|
||||
|
||||
[HttpPost("/api/playerevents/v2/{eventId}/rsvp")]
|
||||
public async Task<IActionResult> RsvpPlayerEvent(long eventId, [FromBody] RsvpRequest request)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var ev = EventDB.GetPlayerEventById(eventId);
|
||||
if (ev == null)
|
||||
return NotFound("");
|
||||
|
||||
var response = EventDB.SetResponse(eventId, id.Value, request.Type);
|
||||
return Ok(new { success = true, value = response });
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v2/{eventId}/rsvp")]
|
||||
public async Task<IActionResult> GetRsvp(long eventId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var response = EventDB.GetResponse(eventId, id.Value);
|
||||
var type = response?.Type ?? (int)PlayerEventResponseType.None;
|
||||
return Ok(new { type });
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v2/{eventId}/responses")]
|
||||
public async Task<IActionResult> GetEventResponses(long eventId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var responses = EventDB.GetResponsesForEvent(eventId);
|
||||
return Ok(responses);
|
||||
}
|
||||
|
||||
[HttpPost("/api/playerevents/v1/respond")]
|
||||
public async Task<IActionResult> RespondToPlayerEvent([FromBody] RespondRequest request)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (request == null || request.PlayerEventId == 0)
|
||||
return BadRequest(new { success = false, error = "PlayerEventId required." });
|
||||
|
||||
var ev = EventDB.GetPlayerEventById(request.PlayerEventId);
|
||||
if (ev == null)
|
||||
return NotFound("");
|
||||
|
||||
int typeInt = request.Type switch
|
||||
{
|
||||
"Yes" => (int)PlayerEventResponseType.Yes,
|
||||
"Interested" => (int)PlayerEventResponseType.Interested,
|
||||
"No" => (int)PlayerEventResponseType.No,
|
||||
_ => (int)PlayerEventResponseType.None
|
||||
};
|
||||
|
||||
var response = EventDB.SetResponse(request.PlayerEventId, id.Value, typeInt);
|
||||
|
||||
var updatedEvent = EventDB.GetPlayerEventById(request.PlayerEventId);
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventResponseChanged",
|
||||
Msg = updatedEvent
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
|
||||
return Ok(new { success = true, value = response });
|
||||
}
|
||||
|
||||
[HttpGet("/api/playerevents/v1/{eventId}/responses")]
|
||||
public async Task<IActionResult> GetEventResponsesV1(long eventId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
return Ok(EventDB.GetResponsesForEvent(eventId));
|
||||
}
|
||||
|
||||
[HttpPut("/api/playerevents/v2/{eventId}/name")]
|
||||
[HttpPost("/api/playerevents/v2/{eventId}/name")]
|
||||
public async Task<IActionResult> EditEventName(long eventId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var name = await ReadBodyValue("name", "value");
|
||||
if (name == null)
|
||||
return BadRequest(new { success = false, error = "Name required." });
|
||||
|
||||
var ev = EventDB.EditPlayerEvent(eventId, id.Value, e => e.Name = name);
|
||||
if (ev == null) return NotFound();
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventUpdated",
|
||||
Msg = ev
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
return Ok(new { success = true, value = ev });
|
||||
}
|
||||
|
||||
[HttpPut("/api/playerevents/v2/{eventId}/description")]
|
||||
[HttpPost("/api/playerevents/v2/{eventId}/description")]
|
||||
public async Task<IActionResult> EditEventDescription(long eventId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var desc = await ReadBodyValue("description", "value");
|
||||
if (desc == null)
|
||||
return BadRequest(new { success = false, error = "Description required." });
|
||||
|
||||
var ev = EventDB.EditPlayerEvent(eventId, id.Value, e => e.Description = desc);
|
||||
if (ev == null) return NotFound();
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventUpdated",
|
||||
Msg = ev
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
return Ok(new { success = true, value = ev });
|
||||
}
|
||||
|
||||
[HttpPut("/api/playerevents/v2/{eventId}/image")]
|
||||
[HttpPost("/api/playerevents/v2/{eventId}/image")]
|
||||
public async Task<IActionResult> EditEventImage(long eventId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var image = await ReadBodyValue("imageName", "image", "value");
|
||||
if (image == null)
|
||||
return BadRequest(new { success = false, error = "ImageName required." });
|
||||
|
||||
var ev = EventDB.EditPlayerEvent(eventId, id.Value, e => e.ImageName = image);
|
||||
if (ev == null) return NotFound();
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventUpdated",
|
||||
Msg = ev
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
return Ok(new { success = true, value = ev });
|
||||
}
|
||||
|
||||
[HttpPut("/api/playerevents/v2/{eventId}/room")]
|
||||
[HttpPost("/api/playerevents/v2/{eventId}/room")]
|
||||
public async Task<IActionResult> EditEventRoom(long eventId, [FromBody] PlayerEventRoomRequest body)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (body == null || body.RoomId == 0)
|
||||
return BadRequest(new { success = false, error = "RoomId required." });
|
||||
|
||||
var ev = EventDB.EditPlayerEvent(eventId, id.Value, e =>
|
||||
{
|
||||
e.RoomId = body.RoomId;
|
||||
e.SubRoomId = body.SubRoomId;
|
||||
});
|
||||
if (ev == null) return NotFound();
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventUpdated",
|
||||
Msg = ev
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
return Ok(new { success = true, value = ev });
|
||||
}
|
||||
|
||||
[HttpPut("/api/playerevents/v2/{eventId}/time")]
|
||||
[HttpPost("/api/playerevents/v2/{eventId}/time")]
|
||||
public async Task<IActionResult> EditEventTime(long eventId, [FromBody] PlayerEventTimeRequest body)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (body == null || (string.IsNullOrWhiteSpace(body.StartTime) && string.IsNullOrWhiteSpace(body.EndTime)))
|
||||
return BadRequest(new { success = false, error = "StartTime or EndTime required." });
|
||||
|
||||
DateTime? start = null, end = null;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(body.StartTime)) start = DateTime.Parse(body.StartTime);
|
||||
if (!string.IsNullOrWhiteSpace(body.EndTime)) end = DateTime.Parse(body.EndTime);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest(new { success = false, error = "Invalid time format." });
|
||||
}
|
||||
|
||||
var ev = EventDB.EditPlayerEvent(eventId, id.Value, e =>
|
||||
{
|
||||
if (start.HasValue) e.StartTime = start.Value;
|
||||
if (end.HasValue) e.EndTime = end.Value;
|
||||
});
|
||||
if (ev == null) return NotFound();
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventUpdated",
|
||||
Msg = ev
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
return Ok(new { success = true, value = ev });
|
||||
}
|
||||
|
||||
[HttpPut("/api/playerevents/v2/{eventId}/club")]
|
||||
[HttpPost("/api/playerevents/v2/{eventId}/club")]
|
||||
public async Task<IActionResult> EditEventClub(long eventId, [FromBody] PlayerEventClubRequest body)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var ev = EventDB.EditPlayerEvent(eventId, id.Value, e => e.ClubId = body?.ClubId);
|
||||
if (ev == null) return NotFound();
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventUpdated",
|
||||
Msg = ev
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
return Ok(new { success = true, value = ev });
|
||||
}
|
||||
|
||||
[HttpPut("/api/playerevents/v2/{eventId}/multiinstance")]
|
||||
[HttpPost("/api/playerevents/v2/{eventId}/multiinstance")]
|
||||
public async Task<IActionResult> EditEventMultiInstance(long eventId, [FromBody] PlayerEventMultiInstanceRequest body)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (body == null)
|
||||
return BadRequest(new { success = false, error = "Body required." });
|
||||
|
||||
var ev = EventDB.EditPlayerEvent(eventId, id.Value, e =>
|
||||
{
|
||||
e.IsMultiInstance = body.IsMultiInstance;
|
||||
e.SupportMultiInstanceRoomChat = body.SupportMultiInstanceRoomChat;
|
||||
});
|
||||
if (ev == null) return NotFound();
|
||||
var wsJson = Newtonsoft.Json.JsonConvert.SerializeObject(new Vanadium.Classes.WebSocket.WebsocketEvents.Response
|
||||
{
|
||||
Id = "PlayerEventUpdated",
|
||||
Msg = ev
|
||||
});
|
||||
await NotificationsController.SendToAll(wsJson);
|
||||
return Ok(new { success = true, value = ev });
|
||||
}
|
||||
|
||||
private async Task<string?> ReadBodyValue(params string[] keys)
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var raw = (await reader.ReadToEndAsync())?.Trim();
|
||||
if (string.IsNullOrEmpty(raw))
|
||||
return null;
|
||||
|
||||
if (raw.StartsWith("{"))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(raw);
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
foreach (var k in keys)
|
||||
if (string.Equals(prop.Name, k, StringComparison.OrdinalIgnoreCase))
|
||||
return prop.Value.ValueKind == System.Text.Json.JsonValueKind.String
|
||||
? prop.Value.GetString()
|
||||
: prop.Value.ToString();
|
||||
}
|
||||
catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
if (raw.StartsWith("\""))
|
||||
{
|
||||
try { return System.Text.Json.JsonSerializer.Deserialize<string>(raw); }
|
||||
catch { return raw.Trim('"'); }
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
public class RsvpRequest
|
||||
{
|
||||
public int Type { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using Vanadium.Classes.WebSocket;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Hubs;
|
||||
using Vanadium.Utils.NotiController;
|
||||
using System.Text.RegularExpressions;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("/")]
|
||||
public class AccountController : ControllerBase
|
||||
{
|
||||
|
||||
public class VerifyDiscordConfirmRequest { public string? pin { get; set; } }
|
||||
|
||||
[HttpPost("beta/verifydiscord/confirm")] // lowk
|
||||
public async Task<IActionResult> BetaVerifyDiscordConfirm([FromBody] VerifyDiscordConfirmRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.pin) || !Program.PendingInGameCodes.TryRemove(req.pin.Trim(), out var data))
|
||||
return BadRequest(new { success = false, error = "Invalid or expired PIN." });
|
||||
|
||||
var player = PlayerDB.Players.FindById(data.AccountId);
|
||||
if (player == null)
|
||||
return BadRequest(new { success = false, error = "Account not found." });
|
||||
|
||||
player.Player.DiscordUserId = data.DiscordUserId.ToString();
|
||||
player.Player.DiscordLinked = true;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
/* Fixed reputation + cheering due to progression overhaul in a way */
|
||||
|
||||
[HttpPost("api/PlayerCheer/v1/create")]
|
||||
public async Task<IActionResult> CreatePlayerCheer([FromForm] long PlayerIdTo, [FromForm] int CheerCategory, [FromForm] long RoomId, [FromForm] bool Anonymous)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (id.Value == PlayerIdTo)
|
||||
return Ok(new { Message = "You can not cheer yourself silly!", success = false });
|
||||
|
||||
var target = PlayerDB.Players.FindById(PlayerIdTo);
|
||||
if (target == null)
|
||||
return NotFound("");
|
||||
|
||||
if (Anonymous)
|
||||
await NotificationsController.SendPlayerCheerAnonymous(PlayerIdTo, id.Value, CheerCategory);
|
||||
else
|
||||
await NotificationsController.SendPlayerCheer(PlayerIdTo, id.Value, CheerCategory);
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
// disabled SetSelectedCheer due to it breaking rr account birthdays!?!? idk but changing it doesnt let you spawn inventions or request to join someone
|
||||
|
||||
/*[HttpPost("api/PlayerCheer/v1/SetSelectedCheer")]
|
||||
public async Task<IActionResult> SetSelectedCheer([FromForm(Name = "CheerCategory")] long cheerCategoryValue)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player == null || player.Player == null)
|
||||
return NotFound("");
|
||||
|
||||
var requestedCheer = (CheerCategory)cheerCategoryValue;
|
||||
bool isDeveloper = player.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false;
|
||||
|
||||
if (requestedCheer == CheerCategory.RecRoomDeveloper && !isDeveloper && player.PlayerId != 216 && player.PlayerId != 936)
|
||||
return StatusCode(403);
|
||||
|
||||
player.Player.Reputation ??= new Reputation();
|
||||
player.Player.Reputation.SelectedCheer = requestedCheer;
|
||||
player.Player.Reputation.IsCheerful = true;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
var rep = player.Player.Reputation;
|
||||
var reupation = new // game doesnt wanna respect IsCheerful true so fuck it
|
||||
{
|
||||
AccountId = player.PlayerId,
|
||||
Notoriety = rep.Noteriety,
|
||||
IsCheerful = true,
|
||||
CheerGeneral = rep.CheerGeneral,
|
||||
CheerHelpful = rep.CheerHelpful,
|
||||
CheerGreatHost = rep.CheerGreatHost,
|
||||
CheerSportsman = rep.CheerSportsman,
|
||||
CheerCreative = rep.CheerCreative,
|
||||
CheerCredit = rep.CheerCredit,
|
||||
SelectedCheer = (int)rep.SelectedCheer
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response { Id = NotiEventTypes.ReputationUpdate, Msg = reupation });
|
||||
await Task.WhenAll(NotificationsController.SendToAll(json), NotificationsController.SendToPlayer(id.Value, json));
|
||||
await NotificationsController.RefreshAccount(id.Value);
|
||||
|
||||
return Ok(new { success = true, selectedCheer = (int)rep.SelectedCheer });
|
||||
}*/
|
||||
|
||||
/* End of rep/cheer */
|
||||
|
||||
[HttpGet("accounts/{playerId}/receives/GameplayInvites")]
|
||||
public IActionResult PlayerRecievesGameplayInvites(long playerId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
[HttpGet("acc/account/bulk")]
|
||||
public async Task<IActionResult> GetAccountsBulk([FromQuery] List<long> id)
|
||||
{
|
||||
return BuildAccountsBulkResponse(id);
|
||||
}
|
||||
|
||||
[Route("acc/account/bulk/email")]
|
||||
[Route("acc/account/bulk/phone")]
|
||||
public IActionResult GetiOSBulk() => Ok(ServerConfig.Bracket);
|
||||
|
||||
[HttpPost("acc/account/bulk")]
|
||||
[Consumes("application/x-www-form-urlencoded")]
|
||||
public async Task<IActionResult> PostAccountsBulk([FromForm] List<long> id)
|
||||
{
|
||||
return BuildAccountsBulkResponse(id);
|
||||
}
|
||||
|
||||
private IActionResult BuildAccountsBulkResponse(List<long> id)
|
||||
{
|
||||
if (id == null || id.Count == 0)
|
||||
return Ok(new List<PlayerDTO>());
|
||||
|
||||
if (id[0] == 0)
|
||||
return Ok(new List<PlayerDTO>
|
||||
{
|
||||
new PlayerDTO
|
||||
{
|
||||
accountId = 0,
|
||||
createdAt = DateTime.UtcNow,
|
||||
displayName = "@",
|
||||
displayEmoji = null,
|
||||
isJunior = false,
|
||||
platforms = 0,
|
||||
profileImage = "DefaultPFP.png",
|
||||
username = "@",
|
||||
personalPronouns = 0,
|
||||
identityFlags = 0
|
||||
}
|
||||
});
|
||||
|
||||
return Ok(PlayerDB.GetAccountsBulk(id));
|
||||
}
|
||||
|
||||
[HttpPut("acc/account/me/profileimage")]
|
||||
public async Task<IActionResult> PostAccountMeProfileImage([FromForm] string imageName)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(imageName))
|
||||
return BadRequest("come on bro");
|
||||
|
||||
if (PlayerDB.IsBanned(id.Value))
|
||||
{
|
||||
APIController.SendNonHileWebhook(id.Value, "not a hile, but a banned user tried to change their profile image");
|
||||
PlayerDB.UpdatePlayerHeartbeat(id.Value, null, online: false);
|
||||
return StatusCode(403, "recapi activate");
|
||||
|
||||
}
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player == null)
|
||||
return NotFound("");
|
||||
|
||||
player.Player.ProfileImage = imageName;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotificationsController.RefreshAccount(id.Value);
|
||||
|
||||
var updated = PlayerDB.GetAccountMe(id.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("api/banappeal/generateCode")]
|
||||
public IActionResult MakeBanAppealCode()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
return Ok(new { value = "appeal at discord.gg/rec-room" });
|
||||
}
|
||||
|
||||
[HttpPut("acc/account/me/bannerimage")]
|
||||
public async Task<IActionResult> PostAccountMeBannerImage([FromForm] string imageName)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(imageName))
|
||||
return BadRequest("hahaha you are bad at this");
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player == null)
|
||||
return NotFound("Player not found.");
|
||||
|
||||
player.Player.BannerImage = imageName;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotificationsController.RefreshAccount(id.Value);
|
||||
|
||||
var updated = PlayerDB.GetAccountMe(id.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("acc/account/me/bio")]
|
||||
public async Task<IActionResult> UpdateAccountMeBio([FromForm] string bio)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
bio = bio ?? "";
|
||||
|
||||
if (!ServerConfig.AllowSwears && !string.IsNullOrWhiteSpace(bio))
|
||||
{
|
||||
var swearPath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "swears.txt");
|
||||
if (System.IO.File.Exists(swearPath))
|
||||
{
|
||||
var swearWords = System.IO.File.ReadAllLines(swearPath);
|
||||
foreach (var word in swearWords.Select(w => w.Trim()).Where(w => !string.IsNullOrEmpty(w)))
|
||||
{
|
||||
if (Regex.IsMatch(bio, $@"(?<![a-zA-Z]){Regex.Escape(word)}(?![a-zA-Z])", RegexOptions.IgnoreCase))
|
||||
return Ok(new { error = "Bio contains prohibited text.", success = false, value = (object?)null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player == null || player.Player == null)
|
||||
return NotFound("");
|
||||
|
||||
player.Player.Bio = bio;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotificationsController.RefreshAccount(id.Value);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("acc/account/{playerId}/bio")]
|
||||
public async Task<IActionResult> GetAccountBio(long playerId)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null || player.Player == null)
|
||||
return NotFound("");
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
accountId = playerId,
|
||||
bio = player.Player.Bio ?? null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("/acc/account/me/username")]
|
||||
public async Task<IActionResult> UpdateUsername([FromForm] string username)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var player = PlayerDB.Players.FindById(account.Value);
|
||||
if (player == null || player.Player == null)
|
||||
return StatusCode(404);
|
||||
|
||||
if (player.Player.NameLocked)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
error = "Your name is locked and is unable to be changed.",
|
||||
success = false,
|
||||
value = (object?)null
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
error = "Name contains prohibited text.",
|
||||
success = false,
|
||||
value = (object?)null
|
||||
});
|
||||
}
|
||||
|
||||
username = username.Trim();
|
||||
|
||||
username = Regex.Replace(username, @"[\u200B-\u200D\uFEFF]", "");
|
||||
|
||||
if (!Regex.IsMatch(username, @"^[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*$"))
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
error = "Name contains prohibited text.",
|
||||
success = false,
|
||||
value = (object?)null
|
||||
});
|
||||
}
|
||||
|
||||
if (!ServerConfig.AllowSwears)
|
||||
{
|
||||
var swearPath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "swears.txt");
|
||||
|
||||
if (System.IO.File.Exists(swearPath))
|
||||
{
|
||||
var swearWords = System.IO.File.ReadAllLines(swearPath);
|
||||
|
||||
foreach (var word in swearWords.Select(w => w.Trim()).Where(w => !string.IsNullOrEmpty(w)))
|
||||
{
|
||||
string pattern = $@"\b{Regex.Escape(word)}\b";
|
||||
|
||||
if (Regex.IsMatch(username, pattern, RegexOptions.IgnoreCase))
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
error = "Name contains prohibited text.",
|
||||
success = false,
|
||||
value = (object?)null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var taken = PlayerDB.Players.FindAll()
|
||||
.Any(p => p.PlayerId != account.Value &&
|
||||
p.Player != null &&
|
||||
string.Equals(p.Player.Username, username, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (taken)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
errorCode = "Accounts.UsernameAlreadyInUse",
|
||||
error = "This username is already in use.",
|
||||
success = false,
|
||||
value = (object?)null
|
||||
});
|
||||
}
|
||||
|
||||
if (player.Player.LastUsernameChangeAt.HasValue &&
|
||||
(DateTime.UtcNow - player.Player.LastUsernameChangeAt.Value).TotalHours >= 2)
|
||||
{
|
||||
player.Player.AvailableUsernameChanges = 3;
|
||||
player.Player.LastUsernameChangeAt = null;
|
||||
}
|
||||
|
||||
if (player.Player.AvailableUsernameChanges <= 0)
|
||||
{
|
||||
APIController.SendNonHileWebhook(account.Value, "User tried to change their username but has no available changes left. current username: " + player.Player.Username + ", attempted new username: " + username);
|
||||
return Ok(new
|
||||
{
|
||||
error = "Please wait a while before changing your username!",
|
||||
success = false,
|
||||
value = (object?)null
|
||||
});
|
||||
}
|
||||
|
||||
player.Player.Username = username;
|
||||
player.Player.DisplayName = username;
|
||||
player.Player.AvailableUsernameChanges -= 1;
|
||||
player.Player.LastUsernameChangeAt ??= DateTime.UtcNow;
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotificationsController.RefreshAccount(account.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("/acc/account/me/displayname")]
|
||||
public async Task<IActionResult> UpdateDisplayName([FromForm] string displayName)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var player = PlayerDB.Players.FindById(account.Value);
|
||||
if (player == null || player.Player == null)
|
||||
return StatusCode(404);
|
||||
|
||||
if (player.Player.NameLocked)
|
||||
return Ok(new { error = "Your name is locked and is unable to be changed.", success = false, value = (object?)null });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(displayName))
|
||||
return Ok(new { error = "Name contains prohibited text.", success = false, value = (object?)null });
|
||||
|
||||
displayName = displayName.Trim();
|
||||
displayName = Regex.Replace(displayName, @"[\u200B-\u200D\uFEFF]", "");
|
||||
|
||||
if (!ServerConfig.AllowSwears)
|
||||
{
|
||||
var swearPath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "swears.txt");
|
||||
if (System.IO.File.Exists(swearPath))
|
||||
{
|
||||
var swearWords = System.IO.File.ReadAllLines(swearPath);
|
||||
foreach (var word in swearWords.Select(w => w.Trim()).Where(w => !string.IsNullOrEmpty(w)))
|
||||
{
|
||||
if (Regex.IsMatch(displayName, $@"\b{Regex.Escape(word)}\b", RegexOptions.IgnoreCase))
|
||||
return Ok(new { error = "Name contains prohibited text.", success = false, value = (object?)null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
player.Player.DisplayName = displayName;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotificationsController.RefreshAccount(account.Value);
|
||||
|
||||
return Ok(new { error = (object?)null, success = true });
|
||||
}
|
||||
|
||||
[HttpPut("/acc/account/me/personalpronouns")]
|
||||
public async Task<IActionResult> UpdatePersonalPronouns([FromForm] int pronounFlags)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var player = PlayerDB.Players.FindById(account.Value);
|
||||
if (player == null || player.Player == null)
|
||||
return StatusCode(404);
|
||||
|
||||
player.Player.PronounFlags = pronounFlags;
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotificationsController.RefreshAccount(account.Value);
|
||||
|
||||
var updated = PlayerDB.GetAccountMe(account.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("/acc/account/me/identityflags")]
|
||||
public async Task<IActionResult> UpdateIdentityFlags([FromForm] int identityFlags)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var player = PlayerDB.Players.FindById(account.Value);
|
||||
if (player == null || player.Player == null)
|
||||
return StatusCode(404);
|
||||
|
||||
player.Player.IdentityFlags = identityFlags;
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotificationsController.RefreshAccount(account.Value);
|
||||
|
||||
var updated = PlayerDB.GetAccountMe(account.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("/acc/account/me/emoji")]
|
||||
public async Task<IActionResult> UpdateDisplayEmoji([FromForm] string? displayEmoji)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var player = PlayerDB.Players.FindById(account.Value);
|
||||
if (player == null || player.Player == null)
|
||||
return StatusCode(404);
|
||||
|
||||
player.Player.DisplayEmoji = string.IsNullOrWhiteSpace(displayEmoji) ? null : displayEmoji;
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotificationsController.RefreshAccount(account.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("acc/account/me")]
|
||||
public async Task<IActionResult> GetAccountMe()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player == null)
|
||||
return NotFound("");
|
||||
|
||||
var account = PlayerDB.GetAccountMe(id.Value);
|
||||
return account != null ? Ok(account) : NotFound();
|
||||
}
|
||||
|
||||
[HttpPost("acc/account/me/email")]
|
||||
public async Task<IActionResult> AccAccountMeEmail()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
email = "vanadium@van.net",
|
||||
success = true
|
||||
});
|
||||
}
|
||||
|
||||
private static long EnsurePlayerClub(FullPlayer target)
|
||||
{
|
||||
long clubId = target.Player.PlayerExtra.PlayerClubId;
|
||||
|
||||
if (clubId > 0 && ClubsDB.GetClub(clubId) != null)
|
||||
return clubId;
|
||||
|
||||
var club = ClubsDB.CreatePlayerClub(target.PlayerId);
|
||||
target.Player.PlayerExtra.PlayerClubId = club.ClubId;
|
||||
PlayerDB.Players.Update(target);
|
||||
return club.ClubId;
|
||||
}
|
||||
|
||||
[HttpPost("subscription/{PlayerId}")]
|
||||
public async Task<IActionResult> Subscribe(ulong? PlayerId)
|
||||
{
|
||||
var user = AuthStuff.GetPlayerId(Request);
|
||||
if (user == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var target = PlayerDB.Players.FindById(PlayerId);
|
||||
if (target == null || target.Player?.PlayerExtra == null)
|
||||
return NotFound("");
|
||||
|
||||
long clubId = EnsurePlayerClub(target);
|
||||
var club = ClubsDB.JoinClub(clubId, user.Value);
|
||||
|
||||
var repUpdate = WebsocketEvents.CreateReputationUpdateResponse(target.PlayerId);
|
||||
await NotificationsController.SendToAll(JsonConvert.SerializeObject(repUpdate));
|
||||
|
||||
if (club != null)
|
||||
{
|
||||
var joined = club.Members.FirstOrDefault(m => m.AccountId == user.Value);
|
||||
if (joined != null)
|
||||
{
|
||||
await NotificationsController.BroadcastClubMembershipUpdate(joined.MemberId, user.Value, clubId, joined.MemberType, joined.JoinedAt);
|
||||
await NotificationsController.BroadcastCreatorClubSubscriptionUpdate(target.PlayerId, clubId, joined.MemberType);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
error = (object?)null,
|
||||
success = true,
|
||||
value = 0
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("acc/parentalcontrols/me")]
|
||||
public IActionResult ParentalControlShit()
|
||||
{
|
||||
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player == null)
|
||||
return NotFound("");
|
||||
|
||||
|
||||
return Ok(new { accountId = player, disallowInAppPurchases = false });
|
||||
}
|
||||
|
||||
[HttpGet("acc/accountprivacysettings/{playerId}")]
|
||||
public async Task<IActionResult> PrivacySettingsAcc(long playerId)
|
||||
{
|
||||
var user = AuthStuff.GetPlayerId(Request);
|
||||
if (user == null)
|
||||
return Unauthorized("");
|
||||
return Ok(new { accountId = playerId, isRecentHistoryVisible = true });
|
||||
}
|
||||
|
||||
[HttpGet("/config/categories")]
|
||||
public async Task<IActionResult> ConfigCategories(long PlayerId)
|
||||
{
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "categories.json");
|
||||
if (!System.IO.File.Exists(path)) return Ok(ServerConfig.Bracket);
|
||||
|
||||
return PhysicalFile(path, "application/json");
|
||||
}
|
||||
|
||||
[HttpGet("/preferences")]
|
||||
public async Task<IActionResult> Preferences(long PlayerId)
|
||||
{
|
||||
return Ok(new { mutedCategories = Array.Empty<string>() });
|
||||
}
|
||||
|
||||
[HttpDelete("subscription/{PlayerId}")]
|
||||
public async Task<IActionResult> Unsubscribe(long PlayerId)
|
||||
{
|
||||
var user = AuthStuff.GetPlayerId(Request);
|
||||
if (user == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var target = PlayerDB.Players.FindById(PlayerId);
|
||||
if (target == null || target.Player?.PlayerExtra == null)
|
||||
return NotFound();
|
||||
|
||||
long clubId = EnsurePlayerClub(target);
|
||||
|
||||
var clubBefore = ClubsDB.GetClub(clubId);
|
||||
var leaving = clubBefore?.Members.FirstOrDefault(m => m.AccountId == user.Value);
|
||||
|
||||
ClubsDB.LeaveClub(clubId, user.Value);
|
||||
|
||||
var repUpdate = WebsocketEvents.CreateReputationUpdateResponse(target.PlayerId);
|
||||
await NotificationsController.SendToAll(JsonConvert.SerializeObject(repUpdate));
|
||||
|
||||
if (leaving != null)
|
||||
{
|
||||
await NotificationsController.BroadcastClubMembershipUpdate(leaving.MemberId, user.Value, clubId, 0, DateTime.UtcNow);
|
||||
await NotificationsController.BroadcastCreatorClubSubscriptionUpdate(target.PlayerId, clubId, 0);
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
error = (object?)null,
|
||||
success = true,
|
||||
value = 1
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("subscription/subscriberCount/{targetPlayerId}")]
|
||||
public async Task<IActionResult> GetSubscriberCount(long targetPlayerId)
|
||||
{
|
||||
var target = PlayerDB.Players.FindById(targetPlayerId);
|
||||
if (target == null || target.Player?.PlayerExtra == null)
|
||||
return NotFound("");
|
||||
|
||||
long clubId = EnsurePlayerClub(target);
|
||||
int count = ClubsDB.GetSubscriberCount(clubId);
|
||||
|
||||
return Content(count.ToString(), "text/plain");
|
||||
}
|
||||
|
||||
[Route("/acc/account/me/haspassword")]
|
||||
public async Task<IActionResult> HasPassword()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player == null)
|
||||
return NotFound("");
|
||||
|
||||
return Ok(!string.IsNullOrEmpty(player.Password));
|
||||
}
|
||||
|
||||
[HttpPost("auth/account/me/changepassword")]
|
||||
public async Task<IActionResult> ChangePassword([FromForm] string newPassword)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player == null)
|
||||
return Ok(new { success = false, error = "Something went wrong trying to change your password." });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newPassword))
|
||||
return Ok(new { success = false, error = "Password cannot be empty." });
|
||||
|
||||
var (valid, theerror) = PlayerDB.PasswordManager.ValidatePassword(newPassword);
|
||||
if (!valid)
|
||||
return Ok(new { success = false, error = theerror });
|
||||
|
||||
var result = PlayerDB.PasswordManager.ChangePassword(id.Value, newPassword);
|
||||
if (!result)
|
||||
return Ok(new { success = false, error = "Something went wrong trying to change your password." });
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("subscription/details/{targetPlayerId}")]
|
||||
public async Task<IActionResult> GetSubscriptionDetails(long targetPlayerId)
|
||||
{
|
||||
var target = PlayerDB.Players.FindById(targetPlayerId);
|
||||
if (target == null || target.Player?.PlayerExtra == null)
|
||||
return NotFound("");
|
||||
|
||||
long clubId = EnsurePlayerClub(target);
|
||||
int count = ClubsDB.GetSubscriberCount(clubId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
accountId = targetPlayerId,
|
||||
clubId = clubId,
|
||||
subscriberCount = count
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using Vanadium.Auth;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("/auth")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
private static readonly HttpClient _webhookClient = new HttpClient();
|
||||
private const string WebhookUrl = "https://discord.com/api/webhooks/1515954214885654538/-OjeNp_0GYNuO-H7ztAU37_TtxxSfqI_cVuv9p-Lk-VzVw_OkD9Ptomfe3eSgZwxryMv";
|
||||
|
||||
private static async Task SendCreatedAccountWebhook(long accountId, DateTime createdAt, string platformId, Platforms platform)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
username = "Vanadium",
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
title = "Vanadium New Account",
|
||||
color = 3066993,
|
||||
fields = new[]
|
||||
{
|
||||
new { name = "Account ID", value = accountId.ToString(), inline = true },
|
||||
new { name = "Platform", value = platform.ToString(), inline = true },
|
||||
new { name = "Platform ID", value = platformId, inline = true },
|
||||
new { name = "Created At", value = createdAt.ToString("u"), inline = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try { await _webhookClient.PostAsync(WebhookUrl, content); } catch { }
|
||||
}
|
||||
|
||||
private static async Task SendSteamBlockedWebhook(Platforms platform, string platformId)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
username = "Vanadium",
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
title = "Vanadium New Account Failed (steam id blocked)",
|
||||
color = 15158332,
|
||||
fields = new[]
|
||||
{
|
||||
new { name = "Platform", value = platform.ToString(), inline = true },
|
||||
new { name = "Platform ID", value = platformId, inline = true },
|
||||
new { name = "Created At", value = DateTime.UtcNow.ToString("u"), inline = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try { await _webhookClient.PostAsync(WebhookUrl, content); } catch { }
|
||||
}
|
||||
|
||||
private static async Task SendOculusBlockedWebhook(Platforms platform, string platformId)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
username = "Vanadium",
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
title = "Vanadium Login Failed (oculus nonce rejected)",
|
||||
color = 15158332,
|
||||
fields = new[]
|
||||
{
|
||||
new { name = "Platform", value = platform.ToString(), inline = true },
|
||||
new { name = "Platform ID", value = platformId, inline = true },
|
||||
new { name = "Timestamp", value = DateTime.UtcNow.ToString("u"), inline = false },
|
||||
new { name = "Info", value = "Meta did not confirm the nonce was issued to this user id", inline = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try { await _webhookClient.PostAsync(WebhookUrl, content); } catch { }
|
||||
}
|
||||
|
||||
private static async Task SendTokenLoginWebhook(long accountId, string grantType, object requestData)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(accountId);
|
||||
bool isDeveloper = player?.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false;
|
||||
bool isRootAccount = accountId == 1;
|
||||
|
||||
if (!isDeveloper && !isRootAccount)
|
||||
return;
|
||||
|
||||
var requestJson = JsonSerializer.Serialize(requestData, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
var payload = new
|
||||
{
|
||||
username = "Vanadium",
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
title = $"Player {accountId} logged in",
|
||||
color = 5814783,
|
||||
fields = new[]
|
||||
{
|
||||
new { name = "Grant Type", value = grantType, inline = true },
|
||||
new { name = "Account ID", value = accountId.ToString(), inline = true },
|
||||
new { name = "Is Developer", value = isDeveloper.ToString(), inline = true },
|
||||
new { name = "Request Body", value = $"```json\n{requestJson}\n```", inline = false }
|
||||
},
|
||||
timestamp = DateTime.UtcNow.ToString("o")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try { await _webhookClient.PostAsync(WebhookUrl, content); } catch { }
|
||||
}
|
||||
|
||||
private static async Task SendSpoofAttemptWebhook(long claimedAccountId, Platforms platform, string platformId)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
username = "Vanadium",
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
title = "Unfamiliar login hash",
|
||||
color = 15158332,
|
||||
fields = new[]
|
||||
{
|
||||
new { name = "Claimed Account ID", value = claimedAccountId.ToString(), inline = true },
|
||||
new { name = "Platform", value = platform.ToString(), inline = true },
|
||||
new { name = "Platform ID", value = platformId, inline = true },
|
||||
new { name = "Timestamp", value = DateTime.UtcNow.ToString("u"), inline = false },
|
||||
new { name = "Info", value = "Someone either tried to spoof or logged in after playing another revival", inline = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try { await _webhookClient.PostAsync(WebhookUrl, content); } catch { }
|
||||
}
|
||||
|
||||
private static async Task SendSkidWebhook(Platforms platform, ulong platformId)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
username = "Vanadium",
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
title = "Blacklisted Platform Id Tryna Skid - cached login api GET",
|
||||
color = 15158332,
|
||||
fields = new[]
|
||||
{
|
||||
new { name = "Platform", value = platform.ToString(), inline = true },
|
||||
new { name = "Platform ID", value = platformId.ToString(), inline = true },
|
||||
new { name = "Timestamp", value = DateTime.UtcNow.ToString("u"), inline = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try { await _webhookClient.PostAsync(WebhookUrl, content); } catch { }
|
||||
}
|
||||
|
||||
internal async Task SendE12354Webhook(
|
||||
string? request,
|
||||
string? ip,
|
||||
string? headersPretty)
|
||||
{
|
||||
|
||||
var embed = new Dictionary<string, object?>
|
||||
{
|
||||
["title"] = $"Gotchu bru",
|
||||
["color"] = 3066993,
|
||||
["fields"] = new object[]
|
||||
{
|
||||
new { name = "Headers", value = headersPretty ?? Request.Headers.ToString(), inline = false },
|
||||
new { name = "IP", value = HttpContext.Connection.RemoteIpAddress?.ToString() ?? "Unknown", inline = false },
|
||||
new { name = "Request Body", value = request ?? "No request provided", inline = true }
|
||||
},
|
||||
};
|
||||
|
||||
var payload = new
|
||||
{
|
||||
embeds = new[] { embed }
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var content = new StringContent(
|
||||
System.Text.Json.JsonSerializer.Serialize(payload),
|
||||
Encoding.UTF8, "application/json");
|
||||
await APIController._webhookClient.PostAsync(
|
||||
"https://discord.com/api/webhooks/1544973415109234738/jlsJiqhHxQ145xDjhl5c68D614mKgzJCIdGTuPwvsFmdicxR26ujube-bR2hgbQrNjKq",
|
||||
content);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
[HttpGet("oculus/nonce")]
|
||||
public IActionResult QuestSupportFor2025()
|
||||
{
|
||||
var random = new Random();
|
||||
const string randomstuffatp = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
var randomString = new string(Enumerable.Repeat(randomstuffatp, 64).Select(s => s[random.Next(s.Length)]).ToArray());
|
||||
return Ok($"\"{randomString}\"");
|
||||
}
|
||||
|
||||
[HttpGet("cachedlogin/forplatformids")]
|
||||
public IActionResult GetPlayersForPlatformIds([FromQuery] List<string> id)
|
||||
{
|
||||
if (id == null || id.Count == 0)
|
||||
return Ok(Array.Empty<object>());
|
||||
|
||||
var results = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.PlatformIds != null &&
|
||||
p.PlatformIds.Any(pid => id.Contains(pid.PlatformId, StringComparer.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
|
||||
var dtos = results.Select(p => PlayerDB.GetAccountsBulk(new List<long> { p.PlayerId }).FirstOrDefault())
|
||||
.Where(p => p != null)
|
||||
.ToList();
|
||||
|
||||
return Ok(dtos);
|
||||
}
|
||||
|
||||
[HttpGet("eac/challenge")]
|
||||
public async Task<IActionResult> C_hallenge()
|
||||
{
|
||||
return Ok("\"AQAAAHsg7mW5FQEE9HVl9EKMWXrqDzQxUCdgV/IPuQfbRgTx+cGnQqhhAgv1RvpihEC77gQ29JdoGFn2806Q+QPEj7nYg9C8pynbaiSVO8rKLJPvROsHuSXVJpQMv3TD8KyK3Y+n5bb86vAb5kRdZGD//uC8HY+D9jJLlEfTUlU=\"");
|
||||
}
|
||||
|
||||
[HttpGet("cachedlogin/forplatformid/{platform}/{platformId}")]
|
||||
[HttpPost("cachedlogin/forplatformid/{platform}/{platformId}")]
|
||||
public async Task<IActionResult> GetCachedLogins(Platforms platform, string platformId)
|
||||
{
|
||||
/*if (ServerConfig.BlacklistedPlatformIds.Contains(platformId))
|
||||
{
|
||||
Console.WriteLine($"[AUTH] AutoAcc skid detected platform={platform} platformId={platformId} in blacklisted platform ids list");
|
||||
_ = SendSkidWebhook(platform, platformId);
|
||||
return Ok("");
|
||||
}*/
|
||||
|
||||
/*if (platform == Platforms.Steam && !ServerConfig.IsSteamIdWhitelisted(platformId))
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
error = "Accounts.UserIsBlacklisted",
|
||||
message_message = "Blacklisted from using Vanadium.",
|
||||
success = false
|
||||
});
|
||||
}*/
|
||||
|
||||
if (PlayerDB.GetLogins(platform, platformId, out var accounts) && accounts.Count > 0)
|
||||
return Ok(accounts);
|
||||
|
||||
if (ServerConfig.AutoAccount)
|
||||
{
|
||||
|
||||
/*if (PlayerDB.IsSteamPlayerInSupportedGame(platformId).Result == false)
|
||||
{
|
||||
Console.WriteLine($"[AUTH] AutoAcc player not playing Rec Room or Spacewar platform={platform} platformId={platformId} not in supported game");
|
||||
_ = SendSteamBlockedWebhook(platform, platformId);
|
||||
return Ok("");
|
||||
}
|
||||
var cachedLogin = new List<CachedLogins>
|
||||
{
|
||||
new CachedLogins
|
||||
{
|
||||
accountId = 0,
|
||||
lastLoginTime = DateTime.UtcNow,
|
||||
platform = platform,
|
||||
platformId = null
|
||||
}
|
||||
};
|
||||
return Ok(cachedLogin);*/
|
||||
Console.WriteLine($"[AUTH] AutoAcc no accounts for platform={platform} platformId={platformId} so we creating one");
|
||||
var newPlayer = PlayerDB.CreateAccount(platform, platformId, false);
|
||||
_ = SendCreatedAccountWebhook(newPlayer.PlayerId, newPlayer.Player.CreatedAt, platformId, platform);
|
||||
Console.WriteLine($"[AUTH] AutoAcc created account with id={newPlayer.PlayerId}");
|
||||
|
||||
var cachedLogin = new List<CachedLogins>
|
||||
{
|
||||
new CachedLogins
|
||||
{
|
||||
accountId = newPlayer.PlayerId,
|
||||
lastLoginTime = DateTime.UtcNow,
|
||||
platform = platform,
|
||||
platformId = platformId.ToString()
|
||||
}
|
||||
};
|
||||
return Ok(cachedLogin);
|
||||
}
|
||||
|
||||
return Unauthorized("");
|
||||
}
|
||||
|
||||
[HttpGet("/auth/role/developer/{playerId}")]
|
||||
public async Task<IActionResult> IsDeveloper(long playerId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var target = PlayerDB.Players.FindById(playerId);
|
||||
if (target == null)
|
||||
return NotFound("");
|
||||
|
||||
return Ok(target.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false);
|
||||
}
|
||||
|
||||
[HttpGet("/auth/role/moderator/{playerId}")]
|
||||
public async Task<IActionResult> IsModerator(long playerId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var target = PlayerDB.Players.FindById(playerId);
|
||||
if (target == null)
|
||||
return NotFound("");
|
||||
|
||||
return Ok(target.PlayerRoles?.Contains(PlayerRoles.Moderator) ?? false);
|
||||
}
|
||||
|
||||
[HttpGet("/auth/role/keepsake/{playerId}")]
|
||||
public async Task<IActionResult> HasScreenSharing(long playerId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var target = PlayerDB.Players.FindById(playerId);
|
||||
if (target == null)
|
||||
return Ok(false);
|
||||
|
||||
return Ok(target.PlayerRoles?.Contains(PlayerRoles.Keepsake) ?? false);
|
||||
}
|
||||
|
||||
[HttpGet("/auth/role/developer")]
|
||||
public async Task<IActionResult> IsDeveloper2025([FromQuery] long id)
|
||||
{
|
||||
var requesterId = AuthStuff.GetPlayerId(Request);
|
||||
if (requesterId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var target = PlayerDB.Players.FindById(id);
|
||||
if (target == null)
|
||||
return NotFound("");
|
||||
|
||||
return Ok(target.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false);
|
||||
}
|
||||
|
||||
[HttpGet("/auth/role/moderator")]
|
||||
public async Task<IActionResult> IsModerator2025([FromQuery] long id)
|
||||
{
|
||||
var requesterId = AuthStuff.GetPlayerId(Request);
|
||||
if (requesterId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var target = PlayerDB.Players.FindById(id);
|
||||
if (target == null)
|
||||
return NotFound("");
|
||||
|
||||
return Ok(target.PlayerRoles?.Contains(PlayerRoles.Moderator) ?? false);
|
||||
}
|
||||
|
||||
[HttpGet("/auth/role/keepsake")]
|
||||
public async Task<IActionResult> HasScreenSharing2025([FromQuery] long id)
|
||||
{
|
||||
var requesterId = AuthStuff.GetPlayerId(Request);
|
||||
if (requesterId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var target = PlayerDB.Players.FindById(id);
|
||||
if (target == null)
|
||||
return Ok(false);
|
||||
|
||||
return Ok(target.PlayerRoles?.Contains(PlayerRoles.Keepsake) ?? false);
|
||||
}
|
||||
|
||||
[HttpPost("auth/cachedlogin/forplatformids")]
|
||||
public async Task<IActionResult> PostCachedLoginForPlatformIds([FromForm] List<ulong> id)
|
||||
{
|
||||
var accounts = new List<CachedLogins>();
|
||||
foreach (var platformId in id)
|
||||
{
|
||||
if (PlayerDB.GetLogins(Platforms.Steam, platformId.ToString(), out var foundAccounts))
|
||||
accounts.AddRange(foundAccounts);
|
||||
}
|
||||
return Ok(accounts.OrderByDescending(a => a.lastLoginTime).ToList());
|
||||
}
|
||||
|
||||
private static void PreWarmDorm(long accountId)
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(accountId);
|
||||
if (player?.Player == null)
|
||||
return;
|
||||
|
||||
long dormRoomId = player.Player.PlayerExtra.DormRoomId;
|
||||
|
||||
if (dormRoomId <= 0)
|
||||
{
|
||||
var dormRoom = RoomDB.GetPlayerDormRoom(accountId);
|
||||
if (dormRoom == null)
|
||||
{
|
||||
dormRoomId = RoomDB.CloneDormRoom(accountId);
|
||||
player.Player.PlayerExtra.DormRoomId = dormRoomId;
|
||||
PlayerDB.UpdateDormRoomId(accountId, dormRoomId);
|
||||
}
|
||||
else
|
||||
{
|
||||
dormRoomId = dormRoom.RoomId;
|
||||
PlayerDB.UpdateDormRoomId(accountId, dormRoomId);
|
||||
}
|
||||
}
|
||||
|
||||
Sessions.CreateDorm(accountId, player.Player.Username, dormRoomId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task SendModernVersionWebhook(long accountId, double version)
|
||||
{
|
||||
var payload = new
|
||||
{
|
||||
username = "Vanadium",
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
title = "User logged in on modern version!",
|
||||
color = 0,
|
||||
fields = new[]
|
||||
{
|
||||
new { name = "Info", value = $"`{accountId}` logged in on version `{version.ToString(System.Globalization.CultureInfo.InvariantCulture)}`", inline = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
try { await _webhookClient.PostAsync(ServerConfig.AntiCheatWebhook, content); } catch { }
|
||||
}
|
||||
|
||||
private static readonly string TokenScope = "offline_access rn"; // i don't think i need this
|
||||
|
||||
private async Task<IActionResult?> Check2025ShitAsync(long playerId, string? ver)
|
||||
{
|
||||
if (!Build2025Validator.Is2025Build(ver)) return null;
|
||||
var (allowed, errorKey, errorDesc) = await Build2025Validator.ValidateAsync(playerId);
|
||||
if (allowed) return null;
|
||||
return Unauthorized(new { error = errorKey, error_description = errorDesc });
|
||||
}
|
||||
|
||||
private IActionResult TokenResponse(long accountId, string grantType, object requestData, DeviceClasses? deviceClass = null, double? ver = null)
|
||||
{
|
||||
// The patch gate. Every grant type funnels through here, so this is the one place
|
||||
// it has to be checked - and the refusal is shaped like the other login denials
|
||||
// above so the client shows the message instead of a generic failure.
|
||||
string? patchDenial = Bluetint.DenyLogin(accountId, Request);
|
||||
|
||||
if (patchDenial != null)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
error = "invalid_grant",
|
||||
error_description = patchDenial,
|
||||
access_token = (string?)null,
|
||||
key = (string?)null,
|
||||
refresh_token = (string?)null
|
||||
});
|
||||
}
|
||||
|
||||
var refreshToken = AuthStuff.GenRefreshToken(accountId);
|
||||
|
||||
PlayerDB.StoreLoginDeviceInfo(accountId, deviceClass, (int?)ver);
|
||||
|
||||
if (grantType != "refresh_token")
|
||||
PreWarmDorm(accountId);
|
||||
|
||||
_ = SendTokenLoginWebhook(accountId, grantType, requestData);
|
||||
|
||||
if (ver.HasValue && ver.Value > 20230616)
|
||||
_ = SendModernVersionWebhook(accountId, ver.Value);
|
||||
|
||||
string xrnsigKey = RNSIGHandler.IssuePlayerKey(accountId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
access_token = AuthStuff.Encode(accountId, xrnsigKey, ver?.ToString()),
|
||||
error = "",
|
||||
error_description = "",
|
||||
refresh_token = refreshToken,
|
||||
scope = TokenScope,
|
||||
key = xrnsigKey
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("connect/token")]
|
||||
public async Task<IActionResult> ConnectToken([FromForm] string grant_type, [FromForm] long? account_id, [FromForm] string? client_id, [FromForm] string? client_secret, [FromForm] string? username, [FromForm] string? password, [FromForm] Platforms? platform, [FromForm] string? platform_id, [FromForm] string? device_id, [FromForm] DeviceClasses? device_class, [FromForm] DateTime? time, [FromForm] string? ver, [FromForm] string? build_key, [FromForm] string? asid, [FromForm] string? eac_challenge, [FromForm] string? eac_response, [FromForm] string? platform_auth, [FromForm] string? refresh_token)
|
||||
|
||||
{
|
||||
double validVer;
|
||||
|
||||
if (!double.TryParse(ver, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out validVer))
|
||||
{
|
||||
validVer = 20250724;
|
||||
}
|
||||
|
||||
var requestData = new
|
||||
{
|
||||
grant_type,
|
||||
account_id,
|
||||
client_id,
|
||||
platform = platform?.ToString(),
|
||||
platform_id,
|
||||
device_id,
|
||||
device_class = device_class?.ToString(),
|
||||
time,
|
||||
ver = validVer,
|
||||
build_key,
|
||||
asid,
|
||||
eac_challenge,
|
||||
eac_response
|
||||
};
|
||||
|
||||
if (time == null || build_key == null)
|
||||
{
|
||||
SendE12354Webhook(JsonSerializer.Serialize(requestData), HttpContext.Connection.RemoteIpAddress?.ToString(), ImageController.GetHeadersPretty(Request));
|
||||
return StatusCode(500);
|
||||
}
|
||||
|
||||
// DevsOnlyToken uh this meaning uh only people that is devvings role can play :P
|
||||
|
||||
if (ServerConfig.DevsOnlyToken)
|
||||
{
|
||||
if (account_id == null || !(PlayerDB.Players.FindById(account_id.Value)?.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false))
|
||||
return Ok(new { error = "invalid_grant", error_description = "", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
|
||||
// dont let skids on or something blah blah
|
||||
|
||||
// READ THIS !!!!! BITCH
|
||||
|
||||
// after talking to "No Turning Back" (also known as: e12354, larpboi69, or sonimcrine) if we turn on pc source gets leaked (no we aint going open source)
|
||||
|
||||
// if (platform == Platforms.Steam && ServerConfig.LetPCPlay == false) // if (platform != Platforms.Oculus && ServerConfig.LetPCPlay == false)
|
||||
if (platform != Platforms.Oculus) // if (platform != Platforms.Oculus && ServerConfig.LetPCPlay == false)
|
||||
{
|
||||
if (account_id == null || !(PlayerDB.Players.FindById(account_id.Value)?.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false))
|
||||
{
|
||||
if (!(ServerConfig.PCMaximumIdForPlay > 0 && account_id.HasValue && account_id.Value > ServerConfig.PCMaximumIdForPlay))
|
||||
goto now_login;
|
||||
return Ok(new { error = "invalid_grant", error_description = "", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// end of checks thgbfs
|
||||
|
||||
now_login:
|
||||
switch (grant_type)
|
||||
{
|
||||
case "cached_login":
|
||||
{
|
||||
if (platform == null || string.IsNullOrWhiteSpace(platform_id))
|
||||
{
|
||||
return BadRequest(new { error = "missing platform info" });
|
||||
}
|
||||
|
||||
if (platform == Platforms.Steam)
|
||||
{
|
||||
if (await PlayerDB.ValidateSteamUserTicketAsync(platform_auth, ulong.Parse(platform_id)) == false)
|
||||
{
|
||||
_ = SendSteamBlockedWebhook(platform.Value, platform_id);
|
||||
return Ok(new { error = "invalid_grant", error_description = "platform verification failed", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
}
|
||||
|
||||
if (platform == Platforms.Oculus)
|
||||
{
|
||||
if (await PlayerDB.ValidateOculusUserNonceAsync(platform_auth, platform_id) == false)
|
||||
{
|
||||
_ = SendOculusBlockedWebhook(platform.Value, platform_id);
|
||||
return Ok(new { error = "invalid_grant", error_description = "platform verification failed", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
}
|
||||
|
||||
PlayerDB.GetLogins(platform.Value, platform_id, out var cachedLogins);
|
||||
var platformAccountIds = cachedLogins.Select(c => c.accountId).ToHashSet();
|
||||
|
||||
if (platformAccountIds.Count == 0)
|
||||
{
|
||||
if (!ServerConfig.AutoAccount)
|
||||
{
|
||||
return Ok(new { error = "invalid_grant", error_description = "an error occured", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
|
||||
var newPlayer = PlayerDB.CreateAccount(platform.Value, platform_id, false);
|
||||
/*var deny0 = await Check2025ShitAsync(newPlayer.PlayerId, ver);
|
||||
if (deny0 != null) return deny0;*/
|
||||
return TokenResponse(newPlayer.PlayerId, grant_type, requestData, device_class, validVer);
|
||||
}
|
||||
|
||||
var platformAccounts = PlayerDB.Players.FindAll()
|
||||
.Where(p => platformAccountIds.Contains(p.PlayerId))
|
||||
.ToList();
|
||||
|
||||
if (platformAccounts.Count == 0)
|
||||
{
|
||||
return Ok(new { error = "invalid_grant", error_description = "an error occured", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
|
||||
FullPlayer targetPlayer;
|
||||
|
||||
if (account_id != null)
|
||||
{
|
||||
targetPlayer = platformAccounts.FirstOrDefault(p => p.PlayerId == account_id.Value);
|
||||
|
||||
if (targetPlayer == null)
|
||||
{
|
||||
Console.WriteLine($"[AUTH] SPOOF ATTEMPT: account_id={account_id} not owned by platform={platform} platform_id={platform_id}");
|
||||
_ = SendSpoofAttemptWebhook(account_id.Value, platform.Value, platform_id);
|
||||
return Ok(new { error = "invalid_grant", error_description = "an error occured", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPlayer = platformAccounts.OrderByDescending(p => p.Player.LastLoginAt).First();
|
||||
}
|
||||
|
||||
Console.WriteLine($"[AUTH] cached_login accepted for account_id={targetPlayer.PlayerId}");
|
||||
/*var deny1 = await Check2025ShitAsync(targetPlayer.PlayerId, ver);
|
||||
if (deny1 != null) return deny1;*/
|
||||
return TokenResponse(targetPlayer.PlayerId, grant_type, requestData, device_class, validVer);
|
||||
}
|
||||
|
||||
case "create_account":
|
||||
{
|
||||
if (!ServerConfig.CA_Auth)
|
||||
return Ok("hello goodbye stop pleasseeeee im tired of skids");
|
||||
if (platform == null || string.IsNullOrWhiteSpace(platform_id))
|
||||
return Ok(new { error = "invalid_grant", error_description = "invalid platform", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
|
||||
if (platform == Platforms.Oculus)
|
||||
{
|
||||
if (await PlayerDB.ValidateOculusUserNonceAsync(platform_auth, platform_id) == false)
|
||||
{
|
||||
Console.WriteLine($"[AUTH] create_account rejected: oculus nonce not valid for platformId={platform_id}");
|
||||
_ = SendOculusBlockedWebhook(platform.Value, platform_id);
|
||||
return Ok(new { error = "invalid_grant", error_description = "platform verification failed", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
}
|
||||
|
||||
PlayerDB.GetLogins(platform.Value, platform_id, out var existingLogins);
|
||||
var existingAccountIds = existingLogins.Select(c => c.accountId).ToList();
|
||||
var existingAccount = PlayerDB.Players.FindAll()
|
||||
.FirstOrDefault(p => existingAccountIds.Contains(p.PlayerId));
|
||||
|
||||
if (existingAccount != null)
|
||||
{
|
||||
/*var deny2 = await Check2025ShitAsync(existingAccount.PlayerId, ver);
|
||||
if (deny2 != null) return deny2;*/
|
||||
return TokenResponse(existingAccount.PlayerId, grant_type, requestData, device_class, validVer);
|
||||
}
|
||||
|
||||
if (platform == Platforms.Steam)
|
||||
{
|
||||
if (await PlayerDB.ValidateSteamUserTicketAsync(platform_auth, ulong.Parse(platform_id)) == false)
|
||||
{
|
||||
Console.WriteLine($"[AUTH] AutoAcc player ticket not valid for Rec Room or Spacewar platform={platform} platformId={platform_id}");
|
||||
_ = SendSteamBlockedWebhook(platform.Value, platform_id);
|
||||
return Ok(new { error = "invalid_grant", error_description = "invalid platform", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
}
|
||||
}
|
||||
|
||||
var newPlayer = PlayerDB.CreateAccount(platform.Value, platform_id, false);
|
||||
|
||||
_ = SendCreatedAccountWebhook(newPlayer.PlayerId, newPlayer.Player.CreatedAt, platform_id, platform.Value);
|
||||
|
||||
/*var deny3 = await Check2025ShitAsync(newPlayer.PlayerId, ver);
|
||||
if (deny3 != null) return deny3;*/
|
||||
return TokenResponse(newPlayer.PlayerId, grant_type, requestData, device_class, validVer);
|
||||
}
|
||||
|
||||
case "refresh_token":
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(refresh_token))
|
||||
return BadRequest(new { error = "invalid_request", error_description = "refresh_token is required." });
|
||||
|
||||
var player = PlayerDB.Players.FindOne(p => p.RefreshToken == refresh_token);
|
||||
|
||||
if (player == null || player.RefreshTokenExpires < DateTime.UtcNow)
|
||||
{
|
||||
Console.WriteLine($"[AUTH] refresh_token REJECTED for token='{refresh_token}'");
|
||||
return Unauthorized(new { error = "invalid_grant", error_description = "Refresh token is invalid or expired" });
|
||||
}
|
||||
|
||||
player.RefreshToken = null;
|
||||
player.RefreshTokenExpires = DateTime.MinValue;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
Console.WriteLine($"[AUTH] refresh_token ACCEPTED for playerId={player.PlayerId}");
|
||||
return TokenResponse(player.PlayerId, grant_type, requestData);
|
||||
}
|
||||
|
||||
case "password":
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return Ok(new { error = "invalid_grant", error_description = "invalid_username_or_password", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
|
||||
var player = PlayerDB.Players.FindAll()
|
||||
.FirstOrDefault(p => string.Equals(p.Player?.Username, username, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (player == null || string.IsNullOrEmpty(player.Password))
|
||||
return Ok(new { error = "invalid_grant", error_description = "invalid_username_or_password", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
|
||||
if (!PlayerDB.PasswordManager.VerifyPassword(password, player.Password))
|
||||
return Ok(new { error = "invalid_grant", error_description = "invalid_username_or_password", access_token = (string?)null, key = (string?)null, refresh_token = (string?)null });
|
||||
|
||||
if (platform != null && !string.IsNullOrWhiteSpace(platform_id))
|
||||
{
|
||||
player.PlatformIds ??= new List<mPlatformID>();
|
||||
|
||||
bool alreadyLinked = player.PlatformIds.Any(p =>
|
||||
p.Platform == platform.Value &&
|
||||
string.Equals(p.PlatformId, platform_id, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!alreadyLinked)
|
||||
{
|
||||
player.PlatformIds.Add(new mPlatformID { Platform = platform.Value, PlatformId = platform_id });
|
||||
PlayerDB.Players.Update(player);
|
||||
Console.WriteLine($"[AUTH] password: linked platform={platform} platformId={platform_id} to playerId={player.PlayerId}");
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"[AUTH] password grant accepted for playerId={player.PlayerId}");
|
||||
/*var denyPw = await Check2025ShitAsync(player.PlayerId, ver);
|
||||
if (denyPw != null) return denyPw;*/
|
||||
return TokenResponse(player.PlayerId, grant_type, requestData, device_class, validVer);
|
||||
}
|
||||
|
||||
default:
|
||||
Console.WriteLine($"[AUTH] unsupported grant_type='{grant_type}'");
|
||||
return BadRequest(new { error = "unsupported_grant_type" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using static Vanadium.Classes.DBs.DBClasses.InventionDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class InventionController : ControllerBase
|
||||
{
|
||||
[HttpGet("api/inventions/v1/room")]
|
||||
public IActionResult RoomsInventions([FromQuery] long? id)
|
||||
=> Ok(ServerConfig.Bracket);
|
||||
|
||||
[HttpGet("/api/inventions/v1/tagfilters")]
|
||||
public IActionResult inventiontagfilters()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
string path = Path.Join(Program.dataDir, "APIS", "jsons", "InventionTagFilters.json");
|
||||
|
||||
if (System.IO.File.Exists(path))
|
||||
{
|
||||
return Content(System.IO.File.ReadAllText(path), "application/json");
|
||||
}
|
||||
else
|
||||
{
|
||||
var json = new
|
||||
{
|
||||
PinnedFilters = new[] { "art", "gadget", "sound", "dormskin", "rro" },
|
||||
PopularFilters = new[] { "art", "gadget", "sound", "dormskin" },
|
||||
TrendingFilters = (string[]?)null
|
||||
};
|
||||
|
||||
return Ok(json);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v2/mine")]
|
||||
public IActionResult Mine()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
return Ok(InventionDB.GetInventionsByPlayer((long)id));
|
||||
}
|
||||
|
||||
[HttpPost("/api/inventions/v7/save")]
|
||||
[HttpPost("/api/inventions/v6/save")]
|
||||
public IActionResult SaveInventionV6([FromBody] SaveInventionRequest request)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.name)) return BadRequest();
|
||||
|
||||
var result = InventionDB.SaveInvention((long)id, request);
|
||||
if (result == null) return BadRequest();
|
||||
|
||||
var (invention, version) = result.Value;
|
||||
return Ok(new { Status = 0, Invention = InventionDB.MapInventionFull(invention), InventionVersion = InventionDB.MapVersion(version) });
|
||||
}
|
||||
|
||||
[HttpPost("/api/inventions/v9/save")]
|
||||
public IActionResult SaveInventionV9([FromBody] SaveInventionRequest request)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
if (request == null || string.IsNullOrWhiteSpace(request.name)) return BadRequest();
|
||||
|
||||
var result = InventionDB.SaveInvention((long)id, request);
|
||||
if (result == null) return BadRequest();
|
||||
|
||||
var (invention, version) = result.Value;
|
||||
return Ok(new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
Status = 0,
|
||||
Invention = InventionDB.MapInventionFull(invention),
|
||||
InventionVersion = InventionDB.MapVersion(version),
|
||||
TagsResponse = new { Result = 0, Tags = new List<object>() }
|
||||
},
|
||||
success = true
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v{ver}/publish")]
|
||||
public IActionResult PublishInventionScary([FromQuery] long inventionId, [FromQuery] int permissionLevel, [FromQuery] int accessibility, [FromQuery] int price)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var result = InventionDB.PublishInvention(inventionId, (long)id.Value, permissionLevel, accessibility, price);
|
||||
if (result == null) return BadRequest();
|
||||
|
||||
return Ok(InventionDB.MapInventionFull(result));
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/unpublish")]
|
||||
public IActionResult UnpublishInvention([FromQuery] long inventionId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var result = InventionDB.UnpublishInvention(inventionId, (long)id.Value);
|
||||
if (result == null) return BadRequest();
|
||||
|
||||
return Ok(InventionDB.MapInventionFull(result));
|
||||
}
|
||||
|
||||
[HttpPatch("/api/inventions/v4/addversion")]
|
||||
public IActionResult AddVersion([FromBody] AddVersionRequest request)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
if (request == null) return Ok(new { Status = -1 });
|
||||
|
||||
var invention = InventionDB.GetInvention(request.inventionId);
|
||||
if (invention == null || invention.CreatorPlayerId != (long)id) return Ok(new { Status = -1 });
|
||||
if (string.IsNullOrEmpty(request.inventionDataFilename)) return Ok(new { Status = -1 });
|
||||
|
||||
var result = InventionDB.AddVersion(invention, request);
|
||||
if (result == null) return Ok(new { Status = -1 });
|
||||
|
||||
var (inv, version) = result.Value;
|
||||
return Ok(new { Status = 0, Invention = InventionDB.MapInventionToData(inv), InventionVersion = InventionDB.MapVersion(version) });
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/versions")]
|
||||
public IActionResult GetVersions([FromQuery] long inventionId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
if (InventionDB.GetInvention(inventionId) == null) return NotFound("");
|
||||
return Ok(InventionDB.GetVersionsForInvention(inventionId));
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v2/batch")]
|
||||
public IActionResult GetInventionsBatchGet([FromQuery(Name = "id")] List<long>? queryIds)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
var ids = queryIds ?? new List<long>();
|
||||
if (ids.Count == 0) return Ok(new List<object>());
|
||||
return Ok(InventionDB.GetInventionsBatch(ids).Select(inv => InventionDB.MapInventionFull(inv)).ToList());
|
||||
}
|
||||
|
||||
[HttpPost("/api/inventions/v2/batch")]
|
||||
public IActionResult GetInventionsBatchPost([FromBody] InventionBatchRequest? body, [FromQuery(Name = "id")] List<long>? queryIds)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var ids = (body != null && body.InventionIds.Count > 0) ? body.InventionIds
|
||||
: (queryIds != null && queryIds.Count > 0) ? queryIds
|
||||
: new List<long>();
|
||||
|
||||
if (ids.Count == 0) return Ok(new List<object>());
|
||||
return Ok(InventionDB.GetInventionsBatch(ids).Select(inv => InventionDB.MapInventionFull(inv)).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1")]
|
||||
[HttpGet("/api/inventions/v2/{inventionId:long}")]
|
||||
public IActionResult GetInvention([FromQuery] long inventionId, [FromRoute] long? inventionId2)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
var inv = InventionDB.GetInvention(inventionId > 0 ? inventionId : inventionId2 ?? 0);
|
||||
if (inv == null) return NotFound("");
|
||||
return Ok(InventionDB.MapInventionFull(inv));
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v2/{inventionId:long}/versions")]
|
||||
public IActionResult GetVersionsV2([FromRoute] long inventionId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
if (InventionDB.GetInvention(inventionId) == null) return NotFound("");
|
||||
return Ok(InventionDB.GetVersionsForInvention(inventionId));
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v2/{inventionId:long}/permission/me")]
|
||||
public IActionResult GetNetPermissionForMe([FromRoute] long inventionId)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null) return Unauthorized("");
|
||||
var invention = InventionDB.GetInvention(inventionId);
|
||||
if (invention == null) return NotFound("");
|
||||
bool isOwner = invention.CreatorPlayerId == playerId || playerId == 2;
|
||||
return Ok(new { Permission = isOwner ? InventionPermissions.Unlimited : InventionPermissions.Unassigned, IsOwner = isOwner });
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v2/creator/{accountId:long}")]
|
||||
public IActionResult GetInventionsByCreator([FromRoute] long accountId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
return Ok(InventionDB.GetInventionsByCreator(accountId));
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v2/room/{roomId:long}")]
|
||||
public IActionResult GetRoomInventions([FromRoute] long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
return Ok(InventionDB.GetInventionsByRoom(roomId));
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/details")]
|
||||
public IActionResult GetInventionDetails([FromQuery] long inventionId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
var invention = InventionDB.GetInvention(inventionId);
|
||||
if (invention == null) return NotFound("");
|
||||
return Ok(new DetailsResponse
|
||||
{
|
||||
Tags = (invention.Tags ?? new List<InventionTag>())
|
||||
.Select(t => new TagData { Tag = t.Tag, Type = t.Type }).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/fulllineageowner")]
|
||||
public IActionResult GetFullLineageOwner([FromQuery] long id)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null) return Unauthorized("");
|
||||
|
||||
bool isDeveloper = PlayerDB.Players.FindById((long)playerId)?.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false;
|
||||
|
||||
var invention = InventionDB.GetInvention(id);
|
||||
if (invention == null)
|
||||
{
|
||||
if (isDeveloper) return Ok(true);
|
||||
return Ok(new { success = false, error_id = "Inventions.FulllineageDoesNotExist", error = "*Invention Shit*\n This invention does not exist!" });
|
||||
}
|
||||
return Ok(invention.CreatorPlayerId == playerId || playerId == 2);
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/version")]
|
||||
public IActionResult GetInventionVersion([FromQuery] long inventionId, [FromQuery] int version)
|
||||
{
|
||||
var invention = InventionDB.GetInvention(inventionId);
|
||||
if (invention == null) return NotFound("");
|
||||
|
||||
var invVersion = InventionDB.GetVersion(inventionId, version)
|
||||
?? invention.CurrentVersion
|
||||
?? InventionDB.GetLatestVersion(inventionId);
|
||||
|
||||
if (invVersion == null) return NotFound("");
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
InventionAccessibility = (int)invention.Accessibility,
|
||||
InventionPrice = invention.Price,
|
||||
InventionGeneralPermission = invention.GeneralPermission,
|
||||
InventionCreatorPermission = invention.CreatorPermission,
|
||||
invVersion.BlobName,
|
||||
BlobHash = invVersion.BlobHash ?? string.Empty,
|
||||
HasBetaContent = false,
|
||||
HasDevContent = false,
|
||||
HasImageFileReference = !string.IsNullOrWhiteSpace(invention.ImageName),
|
||||
UgcAccessibility = 0,
|
||||
ReferencedUnityAssetIds = Array.Empty<object>(),
|
||||
ReferencedInventions = invention.ReferencedInventions ?? new List<long>(),
|
||||
invVersion.InventionId,
|
||||
invVersion.ReplicationId,
|
||||
invVersion.VersionNumber,
|
||||
invVersion.InstantiationCost,
|
||||
invVersion.LightsCost,
|
||||
invVersion.ChipsCost,
|
||||
invVersion.CloudVariablesCost,
|
||||
invVersion.AICost,
|
||||
invVersion.inkCost,
|
||||
CreatedAt = invVersion.CreatedAt.ToString("O"),
|
||||
invention.IsPublished,
|
||||
invention.AllowTrial,
|
||||
invention.Name,
|
||||
invention.Description,
|
||||
invention.ImageName,
|
||||
Tags = invention.Tags ?? new List<InventionTag>()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/featured")]
|
||||
public async Task<IActionResult> FeaturedInventions()
|
||||
{
|
||||
return Ok(InventionDB.GetTopTodayInventions(true));
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/toptoday")]
|
||||
public async Task<IActionResult> TopTodayInventions()
|
||||
{
|
||||
return Ok(InventionDB.GetTopTodayInventions(false));
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/update")]
|
||||
public IActionResult UpdateInvention(
|
||||
[FromQuery] long inventionId,
|
||||
[FromQuery] string? name,
|
||||
[FromQuery] string? description,
|
||||
[FromQuery] string? imageName,
|
||||
[FromQuery] int? price,
|
||||
[FromQuery] bool? isPublished,
|
||||
[FromQuery] bool? allowTrial,
|
||||
[FromQuery] bool? hideFromPlayer,
|
||||
[FromQuery] int? generalPermission,
|
||||
[FromQuery] int? creatorPermission)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null) return Unauthorized("");
|
||||
|
||||
var invention = InventionDB.UpdateInvention(inventionId, (long)playerId,
|
||||
name, description, imageName, price, isPublished, allowTrial,
|
||||
hideFromPlayer, generalPermission, creatorPermission);
|
||||
|
||||
if (invention == null) return NotFound("");
|
||||
return Ok(InventionDB.MapInventionToData(invention));
|
||||
}
|
||||
|
||||
[HttpPost("/api/inventions/v1/report")]
|
||||
public async Task<IActionResult> ReportaInvention([FromBody] ReportInvention request)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null) return Unauthorized("");
|
||||
if (request == null) return BadRequest();
|
||||
|
||||
var invention = InventionDB.GetInvention(request.InventionId);
|
||||
var player = PlayerDB.Players.FindById((long)playerId);
|
||||
var username = player?.Player?.Username ?? "Unknown";
|
||||
var categoryName = Enum.IsDefined(typeof(InventionReportCategory), request.ReportCategory)
|
||||
? Enum.GetName(typeof(InventionReportCategory), request.ReportCategory)!
|
||||
: request.ReportCategory.ToString();
|
||||
|
||||
var inventionName = invention?.Name ?? $"Unknown ({request.InventionId})";
|
||||
var imageUrl = !string.IsNullOrWhiteSpace(invention?.ImageName)
|
||||
? $"https://reloxa.xyz/imageserver/{invention.ImageName}"
|
||||
: null;
|
||||
|
||||
var lines = new List<string>
|
||||
{
|
||||
"# **🔵 New Invention Report!**",
|
||||
$"Reported Invention: {inventionName} `{request.InventionId}`"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Details))
|
||||
lines.Add($"Reason: {request.Details}");
|
||||
|
||||
lines.Add($"Invention Enum Category: \"{categoryName}\" `{(int)request.ReportCategory}`");
|
||||
lines.Add($"Coming From: @{username} `{playerId}`");
|
||||
|
||||
if (imageUrl != null)
|
||||
lines.Add(imageUrl);
|
||||
|
||||
var payload = new { content = string.Join("\n", lines) };
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(payload);
|
||||
|
||||
using var http = new HttpClient();
|
||||
await http.PostAsync(ServerConfig.InventionsReportWebhook,
|
||||
new StringContent(json, System.Text.Encoding.UTF8, "application/json"));
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/delete")]
|
||||
public IActionResult DeleteInvention([FromQuery] long inventionId)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null) return Unauthorized("");
|
||||
bool deleted = InventionDB.DeleteInvention(inventionId, (long)playerId);
|
||||
return Ok(new { success = deleted });
|
||||
}
|
||||
|
||||
[HttpPost("/api/inventions/v1/settags")]
|
||||
public IActionResult SetInventionTags([FromBody] SetTagsRequest request)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
if (request == null || request.InventionId <= 0) return BadRequest("Invalid request");
|
||||
|
||||
var response = InventionDB.SetTags(request.InventionId, request);
|
||||
if (response.Result != 0) return BadRequest("Failed to set tags");
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/personaldetails/{inventionId}")]
|
||||
public IActionResult GetInventionPersonalDetails([FromRoute] long inventionId)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null) return Unauthorized("");
|
||||
|
||||
var player = PlayerDB.Players.FindById((long)playerId);
|
||||
bool isCheering = player?.Player?.PlayerExtra?.PersonalDetails?.inventionsCheered?.Contains(inventionId) ?? false;
|
||||
|
||||
return Ok(new PersonalDetailsResponse { IsCheering = isCheering });
|
||||
}
|
||||
|
||||
[HttpPost("/api/inventions/v{ver}/cheer")]
|
||||
public IActionResult CheerInvention([FromBody] CheerInventionRequest request)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null) return Unauthorized("");
|
||||
|
||||
var result = InventionDB.CheerInvention(request.InventionId, (long)playerId, request.Cheer);
|
||||
if (result == null) return NotFound("");
|
||||
|
||||
return Ok(new { success = true, error = (object?)null, error_id = (object?)null, value = InventionDB.GetInvention(request.InventionId) });
|
||||
}
|
||||
|
||||
[HttpGet("/api/inventions/v1/fromcreators")]
|
||||
public IActionResult FromCreator()
|
||||
=> Ok(ServerConfig.Bracket);
|
||||
|
||||
[HttpGet("/api/inventions/v2/search")]
|
||||
public async Task<IActionResult> SearchInventions(
|
||||
[FromQuery] string query = "",
|
||||
[FromQuery(Name = "value")] string value = "",
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 999)
|
||||
{
|
||||
var searchText = string.IsNullOrWhiteSpace(query) ? value : query;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(searchText))
|
||||
return Ok(InventionDB.GetTopTodayInventions(false));
|
||||
|
||||
var searchResult = InventionDB.Search(searchText, skip: skip, take: take);
|
||||
return Ok(new
|
||||
{
|
||||
Results = searchResult.Results.Select(inv => InventionDB.MapInventionFull(inv)).ToList(),
|
||||
Total = searchResult.Total
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2049 @@
|
||||
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 static Vanadium.Classes.DBs.DBClasses.RoomDBClasses;
|
||||
using Vanadium.Classes.WebSocket;
|
||||
using Vanadium.Utils.NotiController;
|
||||
using Vanadium.Controllers;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("/match")]
|
||||
public partial class MatchController : ControllerBase
|
||||
{
|
||||
private static readonly ConcurrentDictionary<long, (long RoomId, DateTime MatchmadeAt)> _authorizedRoomSessions = new();
|
||||
private static readonly ConcurrentDictionary<(long, long), (int Count, DateTime Window)> _inviteRateLimit = new();
|
||||
|
||||
private static readonly string PhotonChatAppId = "9c035c47-8725-43d4-8c53-ab45ed381511";
|
||||
|
||||
private static readonly Random _photonRng = new Random();
|
||||
|
||||
public static long EnsurePlayerDorm(FullPlayer player)
|
||||
{
|
||||
long dormRoomId = player.Player.PlayerExtra.DormRoomId;
|
||||
|
||||
if (dormRoomId > 0 && RoomDB.DoesPlayerDormExist(player.PlayerId))
|
||||
return dormRoomId;
|
||||
|
||||
var dormRoom = RoomDB.GetPlayerDormRoom(player.PlayerId);
|
||||
if (dormRoom != null)
|
||||
{
|
||||
dormRoomId = dormRoom.RoomId;
|
||||
}
|
||||
else
|
||||
{
|
||||
dormRoomId = RoomDB.CloneDormRoom(player.PlayerId);
|
||||
}
|
||||
|
||||
PlayerDB.UpdateDormRoomId(player.PlayerId, dormRoomId);
|
||||
player.Player.PlayerExtra.DormRoomId = dormRoomId;
|
||||
return dormRoomId;
|
||||
}
|
||||
|
||||
private static void AuthorizeRoomSession(long playerId, long roomId)
|
||||
{
|
||||
_authorizedRoomSessions[playerId] = (roomId, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
private static void ClearRoomSession(long playerId)
|
||||
{
|
||||
_authorizedRoomSessions.TryRemove(playerId, out _);
|
||||
}
|
||||
|
||||
private static bool IsRoomSessionAuthorized(long playerId, long roomId)
|
||||
{
|
||||
if (!_authorizedRoomSessions.TryGetValue(playerId, out var session))
|
||||
return false;
|
||||
return session.RoomId == roomId && (DateTime.UtcNow - session.MatchmadeAt).TotalHours < 12;
|
||||
}
|
||||
|
||||
private static void KickPlayer(long playerId)
|
||||
{
|
||||
}
|
||||
|
||||
private static bool IsBannedFromRoom(Room? room, long playerId)
|
||||
{
|
||||
return room?.Roles?.Any(r => r.AccountId == playerId && r.Role == Role.Banned) ?? false;
|
||||
}
|
||||
|
||||
[HttpGet("player")]
|
||||
public async Task<IActionResult> GetPlayerHeartbeatBulk([FromQuery] List<long> id)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
return Ok(PlayerDB.GetPlayerHeartbeatsBulk(id));
|
||||
}
|
||||
|
||||
[HttpPost("roominstance/{roomInstanceId}/reportjoinresult")]
|
||||
public async Task<IActionResult> ReportJoinResultForRoomInstanceId(ulong roomInstanceId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
long resolvedAccountId = player.PlayerId;
|
||||
long newLevel = player.Player.Level;
|
||||
long newXP = player.Player.XP;
|
||||
|
||||
_ = NotiController.SendEvent(resolvedAccountId, "PlayerProgressionXPUpdate", new
|
||||
{
|
||||
PlayerId = resolvedAccountId,
|
||||
Level = newLevel,
|
||||
XP = newXP
|
||||
});
|
||||
|
||||
_ = NotiController.SendEvent(resolvedAccountId, "PlayerProgressionLevelUpdate", new
|
||||
{
|
||||
PlayerId = resolvedAccountId,
|
||||
Level = newLevel,
|
||||
XP = newXP
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPut("roominstance/{roomInstanceId}/markprivate")]
|
||||
public async Task<IActionResult> MarkRoomInstancePrivate(long roomInstanceId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
Sessions.MarkInstancePrivate(roomInstanceId);
|
||||
Sessions.FakeInstanceFull(roomInstanceId);
|
||||
|
||||
var playersInInstance = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
.ToList();
|
||||
|
||||
foreach (var fp in playersInInstance)
|
||||
{
|
||||
if (fp.Player?.PlayerExtra?.Heartbeat?.roomInstance == null) continue;
|
||||
fp.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate = true;
|
||||
PlayerDB.Players.Update(fp);
|
||||
}
|
||||
|
||||
var outsiderIds = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.isOnline == true
|
||||
&& p.Player.PlayerExtra.Heartbeat.roomInstance?.roomInstanceId != roomInstanceId)
|
||||
.Select(p => p.PlayerId)
|
||||
.ToList();
|
||||
|
||||
foreach (var fp in playersInInstance)
|
||||
{
|
||||
if (fp.Player?.PlayerExtra?.Heartbeat == null) continue;
|
||||
|
||||
var hbJson = Newtonsoft.Json.JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotificationsController.EventTypes.PresenceUpdate,
|
||||
Msg = fp.Player.PlayerExtra.Heartbeat
|
||||
});
|
||||
|
||||
await NotificationsController.SendToPlayer(fp.PlayerId, hbJson);
|
||||
await NotificationsController.SendToPlayers(outsiderIds, hbJson);
|
||||
}
|
||||
|
||||
return Ok(new StorefrontsDBClasses.SuccessResult());
|
||||
}
|
||||
|
||||
private static HashSet<long> RecRoomPlusCheckWhitelisgt()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
[HttpGet("rooms/requiring/developer")]
|
||||
public async Task<IActionResult> RoomsRequiringDeveloper()
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/requiring/rrplus")]
|
||||
public async Task<IActionResult> RoomsRequiringRRPlus()
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpPut("player/statusvisibility")]
|
||||
public IActionResult setStatusVisibilityShit()
|
||||
{
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPut("player/photonregionpings")]
|
||||
public IActionResult setPhotonRegionShit()
|
||||
{
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("room/{roomId}/instances")] // ripped from other code only for reference? i only needed the response data
|
||||
public async Task<IActionResult> GetRoomInstances(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return NotFound("");
|
||||
|
||||
var currentPlayer = PlayerDB.GetCurrentPlayer((long)id);
|
||||
|
||||
bool isDeveloper = currentPlayer?.PlayerRoles?.Any(r =>
|
||||
r == PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
|
||||
bool hasRoomModRole = room.CreatorAccountId == (long)id || (room.Roles?.Any(r =>
|
||||
r.AccountId == (long)id &&
|
||||
(int)r.Role >= (int)Role.Moderator) ?? false);
|
||||
|
||||
if (!isDeveloper && !hasRoomModRole)
|
||||
return StatusCode(403, "Permission Denied");
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance != null
|
||||
&& p.Player.PlayerExtra.Heartbeat.isOnline)
|
||||
.ToList();
|
||||
|
||||
var instanceGroups = allPlayers
|
||||
.Where(p => p.Player.PlayerExtra.Heartbeat.roomInstance.roomId == roomId)
|
||||
.GroupBy(p => p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId)
|
||||
.ToList();
|
||||
|
||||
if (!instanceGroups.Any())
|
||||
return Ok(Array.Empty<object>());
|
||||
|
||||
bool canViewAll = isDeveloper || room.CreatorAccountId == (long)id || (room.Roles?.Any(r =>
|
||||
r.AccountId == (long)id &&
|
||||
(int)r.Role >= (int)Role.Moderator) ?? false);
|
||||
|
||||
var result = instanceGroups
|
||||
.Where(g =>
|
||||
{
|
||||
var sample = g.First();
|
||||
var instance = sample.Player.PlayerExtra.Heartbeat.roomInstance;
|
||||
return canViewAll || instance.isPrivate != true;
|
||||
})
|
||||
.OrderByDescending(g => g.Count())
|
||||
.Select(g =>
|
||||
{
|
||||
var sample = g.First().Player.PlayerExtra.Heartbeat.roomInstance;
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
["RoomInstanceId"] = sample.roomInstanceId,
|
||||
["RoomId"] = roomId,
|
||||
["SubRoomId"] = sample.subRoomId,
|
||||
["IsFull"] = Sessions.IsInstanceActuallyFull(sample.roomInstanceId),
|
||||
["MaxCapacity"] = sample.maxCapacity,
|
||||
["IsPrivate"] = sample.isPrivate,
|
||||
["PlayerIds"] = g.Select(p => p.PlayerId).ToList()
|
||||
};
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost("player/exclusivelogin")]
|
||||
public async Task<IActionResult> PlayerExclusiveLogin()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
await NotificationsController.SendPlayerOnline(id.Value, new MatchDBClasses.PlayerOnlineDTO() { SenderIsMyFavoriteFriend = true });
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("player/login")]
|
||||
public async Task<IActionResult> PlayerLogin()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id != null)
|
||||
{
|
||||
ClearRoomSession(id.Value);
|
||||
PlayerDB.UpdateLastLoginAt(id.Value);
|
||||
PlayerDB.UpdatePlayerHeartbeat(id.Value, null, online: true);
|
||||
|
||||
var someRequiredSetings = new[]
|
||||
{
|
||||
("Recroom.AccountCreation.HasStarted", "True"),
|
||||
("Recroom.AccountCreation.HasChosenUsername", "False"),
|
||||
("Recroom.AccountCreation.HasCreatedPassword", "True"),
|
||||
("Recroom.AccountCreation.HasFinished", "False"),
|
||||
("TUTORIAL_COMPLETE_MASK", "77")
|
||||
};
|
||||
|
||||
var existingSettings = PlayerDB.GetPlayerSettings(id.Value);
|
||||
|
||||
foreach (var (key, value) in someRequiredSetings)
|
||||
{
|
||||
if (!existingSettings.Any(s => s.Key == key))
|
||||
PlayerDB.SetPlayerSetting(key, value, id.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("player/logout")]
|
||||
public async Task<IActionResult> PlayerLogout()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id != null)
|
||||
{
|
||||
ClearRoomSession(id.Value);
|
||||
PlayerDB.UpdatePlayerHeartbeat(id.Value, null, online: false);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("player/heartbeat")]
|
||||
public async Task<IActionResult> PostPlayerHeartbeat()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var playerLock = PlayerDB.GetPlayerLock(id.Value);
|
||||
await playerLock.WaitAsync();
|
||||
FullPlayer player;
|
||||
Heartbeat hb;
|
||||
try
|
||||
{
|
||||
player = PlayerDB.Players.FindById(id.Value);
|
||||
if (player?.Player?.PlayerExtra == null)
|
||||
return Unauthorized("");
|
||||
|
||||
hb = player.Player.PlayerExtra.Heartbeat ?? new Heartbeat();
|
||||
|
||||
if (hb.roomInstance != null)
|
||||
{
|
||||
long claimedRoomId = hb.roomInstance.roomId;
|
||||
|
||||
if (!IsRoomSessionAuthorized(id.Value, claimedRoomId))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
hb.playerId = id.Value;
|
||||
hb.isOnline = true;
|
||||
hb.errorCode = 0;
|
||||
hb.photonAuthToken = null;
|
||||
hb.appVersion = player.Player.PlayerExtra.Heartbeat?.appVersion ?? hb.appVersion;
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
if (form.TryGetValue("DeviceClass", out var dc) && int.TryParse(dc, out var dcVal))
|
||||
hb.deviceClass = (DeviceClasses)dcVal;
|
||||
if (form.TryGetValue("StatusVisibility", out var sv) && int.TryParse(sv, out var svVal))
|
||||
hb.statusVisibility = (StatusVisibility)svVal;
|
||||
if (form.TryGetValue("VRMovementMode", out var vrm) && int.TryParse(vrm, out var vrmVal))
|
||||
hb.vrMovementMode = vrmVal;
|
||||
}
|
||||
|
||||
player.Player.PlayerExtra.Heartbeat = hb;
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await Services.ToxModService.EnsureCurrent(player);
|
||||
}
|
||||
finally
|
||||
{
|
||||
playerLock.Release();
|
||||
}
|
||||
|
||||
/*var (xpAwarded, leveledUp, newLevel, newXP) = PlayerDB.AwardHeartbeatXP(id.Value);
|
||||
|
||||
if (xpAwarded)
|
||||
{
|
||||
var progressionPayload = Newtonsoft.Json.JsonConvert.SerializeObject(
|
||||
WebsocketEvents.CreateLevelUpdateResponse(id.Value, newLevel, newXP));
|
||||
|
||||
await NotificationsController.SendToPlayer(id.Value, progressionPayload);
|
||||
}*/
|
||||
|
||||
return Ok(hb);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/none")]
|
||||
public async Task<IActionResult> MatchmakeNone()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
ClearRoomSession(id.Value);
|
||||
|
||||
var player = PlayerDB.GetCurrentPlayer(id.Value);
|
||||
if (player != null)
|
||||
EnsurePlayerDorm(player);
|
||||
|
||||
var heartbeat = PlayerDB.GetPlayerHeartbeat(id.Value);
|
||||
|
||||
if (heartbeat != null)
|
||||
await NotificationsController.RefreshHeartbeat(id.Value, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpPost("goto/room/{roomName}")]
|
||||
public async Task<IActionResult> MatchmakeByRoomName(string roomName)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
});
|
||||
}
|
||||
|
||||
if (roomName.Equals("DormRoom", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
long dormRoomId = EnsurePlayerDorm(player);
|
||||
AuthorizeRoomSession(player.PlayerId, dormRoomId);
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(dormRoomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, dormRoomId);
|
||||
|
||||
var dormHeartbeat = Sessions.CreateDorm(player.PlayerId, player.Player.Username, dormRoomId);
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
dormHeartbeat.roomInstance.maxCapacity = 1;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, dormHeartbeat);
|
||||
return Ok(dormHeartbeat);
|
||||
}
|
||||
|
||||
var room = RoomDB.GetRoomByName(roomName);
|
||||
if (room == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
long roomId = room.RoomId;
|
||||
|
||||
if (IsBannedFromRoom(room, player.PlayerId))
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.BannedFromRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
bool isCreator = room.CreatorAccountId == player.PlayerId;
|
||||
bool hasAccess = room.Roles?.Any(r =>
|
||||
r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false;
|
||||
bool isOwnerOrCoOwner = isCreator || hasAccess;
|
||||
|
||||
bool hasOwner = room.CreatorAccountId > 0 ||
|
||||
(room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false);
|
||||
|
||||
if (!hasOwner)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AccountDoesNotExist,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted)
|
||||
{
|
||||
if (!isDeveloper && !isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.DeveloperOnly,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
else if (room.Accessibility == RoomAccessibility.Private)
|
||||
{
|
||||
if (!isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.RoomIsPrivate,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOwnerOrCoOwner)
|
||||
{
|
||||
if (player.Player?.IsJunior == true && room.SupportsJuniors == false)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.JuniorNotAllowed,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.LevelTooLow,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
var (joinMode, autoFollow, _, additionalPlayerIds) = await ParseMatchmakeForm(Request);
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var heartbeat = Sessions.CreateRoom(
|
||||
player.PlayerId,
|
||||
roomId,
|
||||
null,
|
||||
joinMode: joinMode,
|
||||
additionalPlayersAutoFollow: autoFollow);
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
if (additionalPlayerIds.Count > 0 && autoFollow)
|
||||
await ShovePlayersToInstance(additionalPlayerIds, roomId, 0, heartbeat);
|
||||
else if (additionalPlayerIds.Count > 0)
|
||||
await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/dorm")]
|
||||
public async Task<IActionResult> MatchmakeDorm()
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
long dormRoomId = EnsurePlayerDorm(player);
|
||||
AuthorizeRoomSession(player.PlayerId, dormRoomId);
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(dormRoomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, dormRoomId);
|
||||
|
||||
var heartbeat = Sessions.CreateDorm(player.PlayerId, player.Player.Username, dormRoomId);
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
heartbeat.roomInstance.maxCapacity = 1;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/event/{eventId}")]
|
||||
public async Task<IActionResult> MatchmakeEvent(long eventId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
});
|
||||
}
|
||||
|
||||
var ev = Vanadium.Classes.DBs.EventDB.GetPlayerEventById(eventId);
|
||||
if (ev == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchGame,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
long roomId = ev.RoomId;
|
||||
long? subRoomId = ev.SubRoomId;
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (IsBannedFromRoom(room, player.PlayerId))
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.BannedFromRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
var (joinMode, autoFollow, _, additionalPlayerIds) = await ParseMatchmakeForm(Request);
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var heartbeat = Sessions.CreateRoom(
|
||||
player.PlayerId,
|
||||
roomId,
|
||||
subRoomId,
|
||||
joinMode: joinMode,
|
||||
additionalPlayersAutoFollow: autoFollow);
|
||||
|
||||
if (heartbeat?.roomInstance != null)
|
||||
heartbeat.roomInstance.eventId = eventId;
|
||||
|
||||
var fp = PlayerDB.Players.FindById(player.PlayerId);
|
||||
if (fp?.Player?.PlayerExtra != null)
|
||||
{
|
||||
fp.Player.PlayerExtra.Heartbeat = heartbeat;
|
||||
PlayerDB.Players.Update(fp);
|
||||
}
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
if (additionalPlayerIds.Count > 0 && autoFollow)
|
||||
await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId ?? 0, heartbeat);
|
||||
else if (additionalPlayerIds.Count > 0)
|
||||
await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/club/{clubId}")]
|
||||
public async Task<IActionResult> MatchmakeToClub(long clubId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
return StatusCode(403);
|
||||
|
||||
var targetClub = Classes.DBs.ClubsDB.GetClub(clubId);
|
||||
if (targetClub == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchClub,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
long roomId = targetClub.ClubhouseRoomId ?? 0;
|
||||
if (roomId <= 0)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.ClubHasNoClubhouse,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.ClubHasNoClubhouse,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (IsBannedFromRoom(room, player.PlayerId))
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.BannedFromClub,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
bool isCreator = room.CreatorAccountId == player.PlayerId;
|
||||
bool hasAccess = room.Roles?.Any(r =>
|
||||
r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false;
|
||||
bool isOwnerOrCoOwner = isCreator || hasAccess;
|
||||
|
||||
bool hasOwner = room.CreatorAccountId > 0 ||
|
||||
(room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false);
|
||||
|
||||
if (!hasOwner)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AccountDoesNotExist,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted)
|
||||
{
|
||||
if (!isDeveloper && !isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.DeveloperOnly,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOwnerOrCoOwner)
|
||||
{
|
||||
if (player.Player?.IsJunior == true && room.SupportsJuniors == false)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.JuniorNotAllowed,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.LevelTooLow,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat;
|
||||
|
||||
var targetInstance = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomId == roomId)
|
||||
.Select(p => p.Player!.PlayerExtra!.Heartbeat!.roomInstance!)
|
||||
.GroupBy(ri => ri.roomInstanceId)
|
||||
.Select(g => g.First())
|
||||
.OrderByDescending(ri => Sessions.CountPlayersInInstance(ri.roomInstanceId))
|
||||
.FirstOrDefault();
|
||||
|
||||
if (targetInstance != null)
|
||||
{
|
||||
if (currentInstanceHb?.roomInstance?.roomInstanceId == targetInstance.roomInstanceId)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AlreadyInTargetInstance,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (targetInstance.isFull || Sessions.CountPlayersInInstance(targetInstance.roomInstanceId) >= targetInstance.maxCapacity)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.InsufficientSpace,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance);
|
||||
if (heartbeat == null)
|
||||
return StatusCode(500);
|
||||
|
||||
heartbeat.playerId = player.PlayerId;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var createdHeartbeat = Sessions.CreateRoom(player.PlayerId, roomId, joinMode: 0, additionalPlayersAutoFollow: false, roomNameOverride: targetClub.Name + " Clubhouse");
|
||||
if (createdHeartbeat == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.ClubHasNoClubhouse,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
createdHeartbeat.playerId = player.PlayerId;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, createdHeartbeat);
|
||||
|
||||
return Ok(createdHeartbeat);
|
||||
}
|
||||
|
||||
private static async Task ShovePlayersToInstance(List<long> playerIds, long roomId, long subRoomId, Heartbeat leaderHeartbeat)
|
||||
{
|
||||
foreach (var pid in playerIds)
|
||||
{
|
||||
var fp = PlayerDB.Players.FindById(pid);
|
||||
if (fp == null) continue;
|
||||
|
||||
var hb = Sessions.CreateRoom(pid, roomId, subRoomId, joinMode: 0, additionalPlayersAutoFollow: false);
|
||||
|
||||
if (leaderHeartbeat.roomInstance != null && hb.roomInstance != null)
|
||||
{
|
||||
hb.roomInstance.photonRoomId = leaderHeartbeat.roomInstance.photonRoomId;
|
||||
hb.roomInstance.photonRegion = leaderHeartbeat.roomInstance.photonRegion;
|
||||
hb.roomInstance.roomInstanceId = leaderHeartbeat.roomInstance.roomInstanceId;
|
||||
}
|
||||
|
||||
fp.Player.PlayerExtra ??= new PlayerExtra();
|
||||
fp.Player.PlayerExtra.Heartbeat = hb;
|
||||
PlayerDB.Players.Update(fp);
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(pid, hb);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<(int joinMode, bool autoFollow, bool bypassMovement, List<long> additionalPlayerIds)> ParseMatchmakeForm(HttpRequest request)
|
||||
{
|
||||
int joinMode = 0;
|
||||
bool autoFollow = false;
|
||||
bool bypassMovement = false;
|
||||
var additionalPlayerIds = new List<long>();
|
||||
|
||||
if (request.HasFormContentType)
|
||||
{
|
||||
var form = await request.ReadFormAsync();
|
||||
if (form.TryGetValue("JoinMode", out var joinModeStr))
|
||||
int.TryParse(joinModeStr, out joinMode);
|
||||
if (form.TryGetValue("AdditionalPlayersAutoFollow", out var autoFollowStr))
|
||||
autoFollow = autoFollowStr.ToString().Equals("True", StringComparison.OrdinalIgnoreCase);
|
||||
if (form.TryGetValue("BypassMovementModeRestriction", out var bypassStr))
|
||||
bypassMovement = bypassStr.ToString().Equals("True", StringComparison.OrdinalIgnoreCase);
|
||||
var additionalKey = form.Keys.FirstOrDefault(k => k.Equals("AdditionalPlayerIds", StringComparison.OrdinalIgnoreCase));
|
||||
if (additionalKey != null)
|
||||
{
|
||||
foreach (var val in form[additionalKey])
|
||||
{
|
||||
if (long.TryParse(val?.Trim(), out var pid))
|
||||
additionalPlayerIds.Add(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (joinMode, autoFollow, bypassMovement, additionalPlayerIds);
|
||||
}
|
||||
|
||||
[HttpPost("player/notifydisconnect")]
|
||||
public async Task<IActionResult> NotifyDisconnect()
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/instance/{roomInstanceId}")]
|
||||
public async Task<IActionResult> MatchmakeInstance(long roomInstanceId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
});
|
||||
}
|
||||
|
||||
var targetInstance = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
.Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (targetInstance == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchGame,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.InsufficientSpace,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat;
|
||||
if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AlreadyInTargetInstance,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, targetInstance.roomId);
|
||||
|
||||
var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance);
|
||||
heartbeat.playerId = player.PlayerId;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
var (_, autoFollowInstance, _, additionalPlayerIdsInstance) = await ParseMatchmakeForm(Request);
|
||||
if (additionalPlayerIdsInstance.Count > 0 && autoFollowInstance)
|
||||
await ShovePlayersToInstance(additionalPlayerIdsInstance, targetInstance.roomId, targetInstance.subRoomId, heartbeat);
|
||||
else if (additionalPlayerIdsInstance.Count > 0)
|
||||
await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIdsInstance, targetInstance.roomId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/v2/instance/{roomInstanceId}")]
|
||||
public async Task<IActionResult> MatchmakeInstanceV2(long roomInstanceId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new { success = false, error_id = "Matchmaking.Banned", error = "You are banned.", value = new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
}});
|
||||
}
|
||||
|
||||
var targetInstance = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
.Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (targetInstance == null)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.NoSuchGame", error = "Instance not found.", value = new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchGame, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.InsufficientSpace", error = "Instance is full.", value = new Heartbeat { errorCode = MatchmakingErrorCode.InsufficientSpace, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat;
|
||||
if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.AlreadyInTargetInstance", error = "Already in this instance.", value = new Heartbeat { errorCode = MatchmakingErrorCode.AlreadyInTargetInstance, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, targetInstance.roomId);
|
||||
|
||||
var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance);
|
||||
heartbeat.playerId = player.PlayerId;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
var (_, autoFollowInstance, _, additionalPlayerIdsInstance) = await ParseMatchmakeForm(Request);
|
||||
if (additionalPlayerIdsInstance.Count > 0 && autoFollowInstance)
|
||||
await ShovePlayersToInstance(additionalPlayerIdsInstance, targetInstance.roomId, targetInstance.subRoomId, heartbeat);
|
||||
else if (additionalPlayerIdsInstance.Count > 0)
|
||||
await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIdsInstance, targetInstance.roomId, heartbeat);
|
||||
|
||||
return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = heartbeat });
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/room/{roomId}")]
|
||||
public async Task<IActionResult> MatchmakeRoomRoomId(long roomId, [FromQuery] long? subRoomId = null)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
var bannedHeartbeat = new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
};
|
||||
|
||||
return Ok(bannedHeartbeat);
|
||||
}
|
||||
|
||||
var (joinMode, autoFollow, bypassMovement, additionalPlayerIds) = await ParseMatchmakeForm(Request);
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
if (room == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (AuthStuff.GetAndEnsureAppVersion(Request) != "20250724" && room.Tags.Any(x => x.Tag.Equals("2025", StringComparison.OrdinalIgnoreCase)))
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.UpdateRequired,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (IsBannedFromRoom(room, player.PlayerId))
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.BannedFromRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room != null)
|
||||
{
|
||||
bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
bool isCreator = room.CreatorAccountId == player.PlayerId;
|
||||
bool hasAccess = room.Roles?.Any(r =>
|
||||
r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false;
|
||||
bool isOwnerOrCoOwner = isCreator || hasAccess;
|
||||
|
||||
bool hasOwner = room.CreatorAccountId > 0 ||
|
||||
(room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false);
|
||||
|
||||
if (!hasOwner)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AccountDoesNotExist,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted)
|
||||
{
|
||||
if (!isDeveloper && !isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.DeveloperOnly,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
else if (room.Accessibility == RoomAccessibility.Private)
|
||||
{
|
||||
if (!isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.RoomIsPrivate,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOwnerOrCoOwner)
|
||||
{
|
||||
if (player.Player?.IsJunior == true && room.SupportsJuniors == false)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.JuniorNotAllowed,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.LevelTooLow,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var currentHb = player.Player?.PlayerExtra?.Heartbeat;
|
||||
if (bypassMovement && joinMode == 0 && currentHb?.roomInstance != null && currentHb.roomInstance.roomId == roomId && !currentHb.roomInstance.isPrivate)
|
||||
{
|
||||
long currentInstanceId = currentHb.roomInstance.roomInstanceId;
|
||||
|
||||
var otherInstance = PlayerDB.Players
|
||||
.FindAll()
|
||||
.Where(p =>
|
||||
p.PlayerId != player.PlayerId &&
|
||||
p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomId == roomId &&
|
||||
!p.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId != currentInstanceId &&
|
||||
Sessions.CountPlayersInInstance(p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId) < p.Player.PlayerExtra.Heartbeat.roomInstance.maxCapacity)
|
||||
.Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (otherInstance != null)
|
||||
{
|
||||
var hb = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, otherInstance);
|
||||
hb.playerId = player.PlayerId;
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, hb);
|
||||
return Ok(hb);
|
||||
}
|
||||
|
||||
joinMode = 1;
|
||||
}
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var heartbeat = Sessions.CreateRoom(
|
||||
player.PlayerId,
|
||||
roomId,
|
||||
subRoomId,
|
||||
joinMode: joinMode,
|
||||
additionalPlayersAutoFollow: autoFollow);
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
if (additionalPlayerIds.Count > 0 && autoFollow)
|
||||
await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId ?? 0, heartbeat);
|
||||
else if (additionalPlayerIds.Count > 0)
|
||||
await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/v2/room/{roomId}")]
|
||||
public async Task<IActionResult> MatchmakeRoomRoomIdV2(long roomId, [FromQuery] long? subRoomId = null)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new { success = false, error_id = "Matchmaking.Banned", error = "You are banned.", value = new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
}});
|
||||
}
|
||||
|
||||
var (joinMode, autoFollow, bypassMovement, additionalPlayerIds) = await ParseMatchmakeForm(Request);
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
if (room == null)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.NoSuchRoom", error = "Room not found.", value = new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (AuthStuff.GetAndEnsureAppVersion(Request) != "20250724" && room.Tags.Any(x => x.Tag.Equals("2025", StringComparison.OrdinalIgnoreCase)))
|
||||
return Ok(new { success = false, error_id = "Matchmaking.UpdateRequired", error = "Update required.", value = new Heartbeat { errorCode = MatchmakingErrorCode.UpdateRequired, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (IsBannedFromRoom(room, player.PlayerId))
|
||||
return Ok(new { success = false, error_id = "Matchmaking.BannedFromRoom", error = "You are banned from this room.", value = new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (room != null)
|
||||
{
|
||||
bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
bool isCreator = room.CreatorAccountId == player.PlayerId;
|
||||
bool hasAccess = room.Roles?.Any(r =>
|
||||
r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false;
|
||||
bool isOwnerOrCoOwner = isCreator || hasAccess;
|
||||
|
||||
bool hasOwner = room.CreatorAccountId > 0 ||
|
||||
(room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false);
|
||||
|
||||
if (!hasOwner)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.AccountDoesNotExist", error = "Room has no owner.", value = new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted)
|
||||
{
|
||||
if (!isDeveloper && !isCreator && !hasAccess)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.DeveloperOnly", error = "This room is developer only.", value = new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId } });
|
||||
}
|
||||
else if (room.Accessibility == RoomAccessibility.Private)
|
||||
{
|
||||
if (!isCreator && !hasAccess)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.RoomIsPrivate", error = "This room is private.", value = new Heartbeat { errorCode = MatchmakingErrorCode.RoomIsPrivate, isOnline = true, playerId = player.PlayerId } });
|
||||
}
|
||||
|
||||
if (!isOwnerOrCoOwner)
|
||||
{
|
||||
if (player.Player?.IsJunior == true && room.SupportsJuniors == false)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.JuniorNotAllowed", error = "Juniors are not allowed in this room.", value = new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.LevelTooLow", error = "Your level is too low.", value = new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId } });
|
||||
}
|
||||
}
|
||||
|
||||
var currentHb = player.Player?.PlayerExtra?.Heartbeat;
|
||||
if (bypassMovement && joinMode == 0 && currentHb?.roomInstance != null && currentHb.roomInstance.roomId == roomId && !currentHb.roomInstance.isPrivate)
|
||||
{
|
||||
long currentInstanceId = currentHb.roomInstance.roomInstanceId;
|
||||
|
||||
var otherInstance = PlayerDB.Players
|
||||
.FindAll()
|
||||
.Where(p =>
|
||||
p.PlayerId != player.PlayerId &&
|
||||
p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomId == roomId &&
|
||||
!p.Player.PlayerExtra.Heartbeat.roomInstance.isPrivate &&
|
||||
p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId != currentInstanceId &&
|
||||
Sessions.CountPlayersInInstance(p.Player.PlayerExtra.Heartbeat.roomInstance.roomInstanceId) < p.Player.PlayerExtra.Heartbeat.roomInstance.maxCapacity)
|
||||
.Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (otherInstance != null)
|
||||
{
|
||||
var hb = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, otherInstance);
|
||||
hb.playerId = player.PlayerId;
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, hb);
|
||||
return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = hb });
|
||||
}
|
||||
|
||||
joinMode = 1;
|
||||
}
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var heartbeat = Sessions.CreateRoom(
|
||||
player.PlayerId,
|
||||
roomId,
|
||||
subRoomId,
|
||||
joinMode: joinMode,
|
||||
additionalPlayersAutoFollow: autoFollow);
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
if (additionalPlayerIds.Count > 0 && autoFollow)
|
||||
await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId ?? 0, heartbeat);
|
||||
else if (additionalPlayerIds.Count > 0)
|
||||
await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat);
|
||||
|
||||
return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = heartbeat });
|
||||
}
|
||||
|
||||
[Route("player/avoidjuniors")]
|
||||
public IActionResult avoidjuniours()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/room/{roomId}/{subRoomId}")]
|
||||
public async Task<IActionResult> MatchmakeRoomWithSubRoom(long roomId, long subRoomId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
var bannedHeartbeat = new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
};
|
||||
|
||||
return Ok(bannedHeartbeat);
|
||||
}
|
||||
|
||||
var (joinMode, autoFollow, bypassMovement, additionalPlayerIds) = await ParseMatchmakeForm(Request);
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
if (room == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (IsBannedFromRoom(room, player.PlayerId))
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.BannedFromRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room != null)
|
||||
{
|
||||
bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
bool isCreator = room.CreatorAccountId == player.PlayerId;
|
||||
bool hasAccess = room.Roles?.Any(r =>
|
||||
r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false;
|
||||
bool isOwnerOrCoOwner = isCreator || hasAccess;
|
||||
|
||||
bool hasOwner = room.CreatorAccountId > 0 ||
|
||||
(room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false);
|
||||
|
||||
if (!hasOwner)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AccountDoesNotExist,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted)
|
||||
{
|
||||
if (!isDeveloper && !isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.DeveloperOnly,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
else if (room.Accessibility == RoomAccessibility.Private)
|
||||
{
|
||||
if (!isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.RoomIsPrivate,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOwnerOrCoOwner)
|
||||
{
|
||||
if (player.Player?.IsJunior == true && room.SupportsJuniors == false)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.JuniorNotAllowed,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.LevelTooLow,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var heartbeat = Sessions.CreateRoom(
|
||||
player.PlayerId,
|
||||
roomId,
|
||||
subRoomId,
|
||||
joinMode: joinMode,
|
||||
additionalPlayersAutoFollow: autoFollow);
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
if (additionalPlayerIds.Count > 0 && autoFollow)
|
||||
await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId, heartbeat);
|
||||
else if (additionalPlayerIds.Count > 0)
|
||||
await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/v2/room/{roomId}/{subRoomId}")]
|
||||
public async Task<IActionResult> MatchmakeRoomWithSubRoomV2(long roomId, long subRoomId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new { success = false, error_id = "Matchmaking.Banned", error = "You are banned.", value = new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
}});
|
||||
}
|
||||
|
||||
var (joinMode, autoFollow, bypassMovement, additionalPlayerIds) = await ParseMatchmakeForm(Request);
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
if (room == null)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.NoSuchRoom", error = "Room not found.", value = new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchRoom, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (IsBannedFromRoom(room, player.PlayerId))
|
||||
return Ok(new { success = false, error_id = "Matchmaking.BannedFromRoom", error = "You are banned from this room.", value = new Heartbeat { errorCode = MatchmakingErrorCode.BannedFromRoom, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (room != null)
|
||||
{
|
||||
bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
bool isCreator = room.CreatorAccountId == player.PlayerId;
|
||||
bool hasAccess = room.Roles?.Any(r =>
|
||||
r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false;
|
||||
bool isOwnerOrCoOwner = isCreator || hasAccess;
|
||||
|
||||
bool hasOwner = room.CreatorAccountId > 0 ||
|
||||
(room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false);
|
||||
|
||||
if (!hasOwner)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.AccountDoesNotExist", error = "Room has no owner.", value = new Heartbeat { errorCode = MatchmakingErrorCode.AccountDoesNotExist, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted)
|
||||
{
|
||||
if (!isDeveloper && !isCreator && !hasAccess)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.DeveloperOnly", error = "This room is developer only.", value = new Heartbeat { errorCode = MatchmakingErrorCode.DeveloperOnly, isOnline = true, playerId = player.PlayerId } });
|
||||
}
|
||||
else if (room.Accessibility == RoomAccessibility.Private)
|
||||
{
|
||||
if (!isCreator && !hasAccess)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.RoomIsPrivate", error = "This room is private.", value = new Heartbeat { errorCode = MatchmakingErrorCode.RoomIsPrivate, isOnline = true, playerId = player.PlayerId } });
|
||||
}
|
||||
|
||||
if (!isOwnerOrCoOwner)
|
||||
{
|
||||
if (player.Player?.IsJunior == true && room.SupportsJuniors == false)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.JuniorNotAllowed", error = "Juniors are not allowed in this room.", value = new Heartbeat { errorCode = MatchmakingErrorCode.JuniorNotAllowed, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.LevelTooLow", error = "Your level is too low.", value = new Heartbeat { errorCode = MatchmakingErrorCode.LevelTooLow, isOnline = true, playerId = player.PlayerId } });
|
||||
}
|
||||
}
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var heartbeat = Sessions.CreateRoom(
|
||||
player.PlayerId,
|
||||
roomId,
|
||||
subRoomId,
|
||||
joinMode: joinMode,
|
||||
additionalPlayersAutoFollow: autoFollow);
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
if (additionalPlayerIds.Count > 0 && autoFollow)
|
||||
await ShovePlayersToInstance(additionalPlayerIds, roomId, subRoomId, heartbeat);
|
||||
else if (additionalPlayerIds.Count > 0)
|
||||
await NotificationsController.SendPartyActivitySwitch(player.PlayerId, additionalPlayerIds, roomId, heartbeat);
|
||||
|
||||
return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = heartbeat });
|
||||
}
|
||||
|
||||
/* 20230616 & 20250724.01 photons */
|
||||
|
||||
private static readonly string[] _fallbackRealtime = new[]
|
||||
{
|
||||
"616d6ba8-edb1-47f8-a0de-31c8ce426685",
|
||||
"a7c732cd-5cea-4839-b0b3-ba8e9af14b06",
|
||||
"90d94f73-9797-409d-84ab-ed3992177612"
|
||||
};
|
||||
|
||||
private static readonly string[] _fallbackVoice = new[]
|
||||
{
|
||||
"b3a7f09a-b167-4ba1-87bb-2603da5a39be",
|
||||
"7e71f40c-d1fa-4e48-8851-981402e630e5",
|
||||
"3e010413-c8c8-48df-95cf-bf7f2bf430e6"
|
||||
};
|
||||
|
||||
[HttpGet("player/connection-info")]
|
||||
public async Task<IActionResult> GetPlayerConnectionInfo([FromQuery] long? roomInstanceId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (roomInstanceId == null || roomInstanceId <= 0)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error = (string?)null,
|
||||
value = new
|
||||
{
|
||||
PhotonRealtimeAppId = _fallbackRealtime[_photonRng.Next(_fallbackRealtime.Length)],
|
||||
PhotonChatAppId,
|
||||
PhotonVoiceAppId = _fallbackVoice[_photonRng.Next(_fallbackVoice.Length)],
|
||||
experiments = new { shouldUseGameServerNetworking = false }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var dbEntry = InstanceDB.Get(roomInstanceId.Value);
|
||||
|
||||
if (dbEntry == null)
|
||||
{
|
||||
var liveInstance = PlayerDB.Players.FindAll()
|
||||
.Select(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance)
|
||||
.FirstOrDefault(ri => ri?.roomInstanceId == roomInstanceId.Value);
|
||||
|
||||
if (liveInstance != null)
|
||||
{
|
||||
dbEntry = InstanceDB.Upsert(liveInstance, liveInstance.isPrivate);
|
||||
}
|
||||
}
|
||||
|
||||
if (dbEntry == null)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error = (string?)null,
|
||||
value = new
|
||||
{
|
||||
PhotonRealtimeAppId = _fallbackRealtime[_photonRng.Next(_fallbackRealtime.Length)],
|
||||
PhotonChatAppId,
|
||||
PhotonVoiceAppId = _fallbackVoice[_photonRng.Next(_fallbackVoice.Length)],
|
||||
experiments = new { shouldUseGameServerNetworking = false }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(dbEntry.PhotonRealtimeAppId) || string.IsNullOrEmpty(dbEntry.PhotonVoiceAppId))
|
||||
{
|
||||
var (rt, vo) = InstanceDB.AssignPhotonIds(roomInstanceId.Value);
|
||||
dbEntry.PhotonRealtimeAppId = rt;
|
||||
dbEntry.PhotonVoiceAppId = vo;
|
||||
}
|
||||
|
||||
var liveInst = PlayerDB.Players.FindAll()
|
||||
.Select(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance)
|
||||
.FirstOrDefault(ri => ri?.roomInstanceId == roomInstanceId.Value);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error = (string?)null,
|
||||
value = new
|
||||
{
|
||||
PhotonAuthToken = "",
|
||||
PhotonRealtimeAppId = dbEntry.PhotonRealtimeAppId,
|
||||
PhotonChatAppId,
|
||||
PhotonVoiceAppId = dbEntry.PhotonVoiceAppId,
|
||||
PhotonRegion = liveInst?.photonRegion ?? dbEntry.PhotonRegion ?? "us",
|
||||
PhotonRoomId = liveInst?.photonRoomId ?? dbEntry.PhotonRoomId,
|
||||
VoiceConnectionInfo = (string?)null,
|
||||
VoiceServerId = (string?)null,
|
||||
experiments = new { shouldUseGameServerNetworking = false }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("invite")]
|
||||
public async Task<IActionResult> Invite([FromForm] long playerId, [FromForm] long roomInstanceId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (playerId <= 0 || roomInstanceId <= 0)
|
||||
return BadRequest();
|
||||
|
||||
var targetPlayer = PlayerDB.Players.FindById(playerId);
|
||||
if (targetPlayer == null)
|
||||
return NotFound("");
|
||||
|
||||
var rel = FriendsDB.GetRelationships(player.PlayerId).FirstOrDefault(r => r.OtherPlayerID == playerId);
|
||||
if (rel?.RelationshipType != FriendsDBClasses.RelationshipType.Friend)
|
||||
return BadRequest();
|
||||
|
||||
var heartbeat = PlayerDB.GetPlayerHeartbeat(player.PlayerId);
|
||||
if (heartbeat?.roomInstance == null || heartbeat.roomInstance.roomInstanceId != roomInstanceId)
|
||||
return BadRequest();
|
||||
|
||||
var key = (player.PlayerId, playerId);
|
||||
var now = DateTime.UtcNow;
|
||||
_inviteRateLimit.AddOrUpdate(key, _ => (1, now), (_, e) =>
|
||||
(now - e.Window).TotalSeconds >= 15 ? (1, now) : (e.Count + 1, e.Window));
|
||||
|
||||
if (_inviteRateLimit.TryGetValue(key, out var rate) && rate.Count > 3 && (now - rate.Window).TotalSeconds < 15)
|
||||
return BadRequest();
|
||||
|
||||
var invite = new MatchDBClasses.InviteDTO
|
||||
{
|
||||
InviteId = roomInstanceId,
|
||||
Name = heartbeat.roomInstance.Name ?? "Unknown Room",
|
||||
InviteMode = 0
|
||||
};
|
||||
|
||||
await NotificationsController.SendInvite(playerId, player.PlayerId, heartbeat.roomInstance.roomId, invite);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
//[HttpPost("matchmake/instance/{roomInstanceId}")] // added back because some bitch deleted it
|
||||
[HttpPost("matchmake/invite/{roomInstanceId}")] // ez
|
||||
//[HttpPost("matchmake/v2/instance/{roomInstanceId}")]
|
||||
public async Task<IActionResult> MatchmakeToInviteInstance(long roomInstanceId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
});
|
||||
}
|
||||
|
||||
var targetInstance = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
.Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (targetInstance == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchGame,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.InsufficientSpace,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat;
|
||||
if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AlreadyInTargetInstance,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, targetInstance.roomId);
|
||||
|
||||
var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance);
|
||||
heartbeat.playerId = player.PlayerId;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/v2/invite/{roomInstanceId}")]
|
||||
public async Task<IActionResult> MatchmakeToInviteInstanceV2(long roomInstanceId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new { success = false, error_id = "Matchmaking.Banned", error = "You are banned.", value = new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
}});
|
||||
}
|
||||
|
||||
var targetInstance = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
.Select(p => p.Player.PlayerExtra.Heartbeat.roomInstance)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (targetInstance == null)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.NoSuchGame", error = "Instance not found.", value = new Heartbeat { errorCode = MatchmakingErrorCode.NoSuchGame, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.InsufficientSpace", error = "Instance is full.", value = new Heartbeat { errorCode = MatchmakingErrorCode.InsufficientSpace, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat;
|
||||
if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
return Ok(new { success = false, error_id = "Matchmaking.AlreadyInTargetInstance", error = "Already in this instance.", value = new Heartbeat { errorCode = MatchmakingErrorCode.AlreadyInTargetInstance, isOnline = true, playerId = player.PlayerId } });
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, targetInstance.roomId);
|
||||
|
||||
var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance);
|
||||
heartbeat.playerId = player.PlayerId;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
return Ok(new { success = true, error_id = (string?)null, error = (string?)null, value = heartbeat });
|
||||
}
|
||||
|
||||
[HttpPost("matchmake/player/{targetPlayerId}")]
|
||||
public async Task<IActionResult> MatchmakeToPlayer(long targetPlayerId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (PlayerDB.IsBanned(player.PlayerId))
|
||||
{
|
||||
var previousHeartbeat = player.Player?.PlayerExtra?.Heartbeat;
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
appVersion = AuthStuff.GetAndEnsureAppVersion(Request),
|
||||
deviceClass = previousHeartbeat?.deviceClass ?? DeviceClasses.Unknown,
|
||||
errorCode = MatchmakingErrorCode.Banned,
|
||||
isOnline = previousHeartbeat?.isOnline ?? true,
|
||||
playerId = player.PlayerId,
|
||||
roomInstance = null,
|
||||
statusVisibility = previousHeartbeat?.statusVisibility ?? StatusVisibility.Online,
|
||||
vrMovementMode = previousHeartbeat?.vrMovementMode ?? 0,
|
||||
photonAuthToken = null,
|
||||
photonRealtimeAppId = null,
|
||||
photonVoiceAppId = null,
|
||||
photonChatAppId = null,
|
||||
photonRegion = null,
|
||||
photonRoomId = null,
|
||||
voiceConnectionInfo = null,
|
||||
voiceServerId = null,
|
||||
experiments = null
|
||||
});
|
||||
}
|
||||
|
||||
var targetPlayer = PlayerDB.Players.FindById(targetPlayerId);
|
||||
if (targetPlayer == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AccountDoesNotExist,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
var targetHb = targetPlayer.Player?.PlayerExtra?.Heartbeat;
|
||||
if (targetHb == null || !targetHb.isOnline || targetHb.roomInstance == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.PlayerNotOnline,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
var targetInstance = targetHb.roomInstance;
|
||||
long roomId = targetInstance.roomId;
|
||||
long roomInstanceId = targetInstance.roomInstanceId;
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.NoSuchRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (IsBannedFromRoom(room, player.PlayerId))
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.BannedFromRoom,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
bool isDeveloper = player.PlayerRoles?.Any(r => r == PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
bool isCreator = room.CreatorAccountId == player.PlayerId;
|
||||
bool hasAccess = room.Roles?.Any(r =>
|
||||
r.AccountId == player.PlayerId && (int)r.Role >= (int)Role.CoOwner) ?? false;
|
||||
bool isOwnerOrCoOwner = isCreator || hasAccess;
|
||||
|
||||
bool hasOwner = room.CreatorAccountId > 0 ||
|
||||
(room.Roles?.Any(r => (int)r.Role >= (int)Role.CoOwner) ?? false);
|
||||
|
||||
if (!hasOwner)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AccountDoesNotExist,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.Accessibility == RoomAccessibility.Dev_only || room.Accessibility == RoomAccessibility.Dev_Unlisted)
|
||||
{
|
||||
if (!isDeveloper && !isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.DeveloperOnly,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
else if (room.Accessibility == RoomAccessibility.Private)
|
||||
{
|
||||
if (!isCreator && !hasAccess)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.RoomIsPrivate,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOwnerOrCoOwner)
|
||||
{
|
||||
if (player.Player?.IsJunior == true && room.SupportsJuniors == false)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.JuniorNotAllowed,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
if (room.MinLevel > 0 && (player.Player?.Level ?? 1) < room.MinLevel)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.LevelTooLow,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
}
|
||||
|
||||
if (targetInstance.isFull || Sessions.CountPlayersInInstance(roomInstanceId) >= targetInstance.maxCapacity)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.InsufficientSpace,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
var currentInstanceHb = player.Player?.PlayerExtra?.Heartbeat;
|
||||
if (currentInstanceHb?.roomInstance?.roomInstanceId == roomInstanceId)
|
||||
return Ok(new Heartbeat
|
||||
{
|
||||
errorCode = MatchmakingErrorCode.AlreadyInTargetInstance,
|
||||
isOnline = true,
|
||||
playerId = player.PlayerId
|
||||
});
|
||||
|
||||
RoomDB.IncrementRoomVisitCount(roomId);
|
||||
PlayerDB.UpdateRoomLastVisitedAt(player.PlayerId, roomId);
|
||||
|
||||
AuthorizeRoomSession(player.PlayerId, roomId);
|
||||
|
||||
var heartbeat = PlayerDB.UpdatePlayerHeartbeat(player.PlayerId, targetInstance);
|
||||
heartbeat.playerId = player.PlayerId;
|
||||
|
||||
await NotificationsController.RefreshHeartbeat(player.PlayerId, heartbeat);
|
||||
|
||||
return Ok(heartbeat);
|
||||
}
|
||||
|
||||
[HttpDelete("invite/{inviteId}")]
|
||||
public async Task<IActionResult> DeclineInvite(long inviteId)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var messages = PlayerDB.GetMessages(player.PlayerId);
|
||||
var invite = messages.FirstOrDefault(m => m.Id == inviteId);
|
||||
|
||||
var invitedata = new MatchDBClasses.InviteDTO
|
||||
{
|
||||
InviteId = inviteId,
|
||||
Name = "Unknown Room",
|
||||
InviteMode = 0
|
||||
};
|
||||
|
||||
if (invite != null)
|
||||
{
|
||||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = player.PlayerId,
|
||||
Id = PlayerDB.GenerateRandomMessageId(),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.GameInviteDeclined,
|
||||
Data = invitedata,
|
||||
RoomId = invite.RoomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
|
||||
await NotificationsController.SendToPlayer(invite.FromPlayerId, json);
|
||||
PlayerDB.DeleteMessage(player.PlayerId, inviteId);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
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 Vanadium.Utils;
|
||||
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 ModernController : ControllerBase
|
||||
{
|
||||
public class OutfitBulkRequest
|
||||
{
|
||||
public List<long>? AccountIds { get; set; }
|
||||
public string? UnityAssetTarget { get; set; }
|
||||
public string? UnityAssetVersion { get; set; }
|
||||
}
|
||||
|
||||
// [16:38:04] [96.2.32.187] GET: https://reloxa.xyz/crm/me/config/v2 404
|
||||
// [16:38:05] [96.2.32.187] POST: https://reloxa.xyz/statsigUserProperties 404
|
||||
// [16:38:06] [96.2.32.187] POST: https://reloxa.xyz/api/avatar/v1/lockeditems/bulk 404
|
||||
|
||||
[HttpGet("crm/me/config/v2")]
|
||||
public IActionResult crmconfig()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpPost("statsigUserProperties")]
|
||||
public IActionResult statsig()
|
||||
{
|
||||
return Ok(new { Success = true, value = new { splitTests = (object?)null, shouldSampleGameserver = false } });
|
||||
}
|
||||
|
||||
[HttpPost("api/avatar/v1/lockeditems/bulk")]
|
||||
public IActionResult lockeditems()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("api/versioncheck/islandedversions")]
|
||||
public async Task<IActionResult> GetIslandedVersions()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[Route("api/PlayerReporting/v1/referee")] // ok so theres options, get, post, and patch used by game for some reason
|
||||
public async Task<IActionResult> PlayerReportingReferee()
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
[HttpPost("purchase/v1/cleanuppending")] // should be in storefront controller, but this controller is specifically for modern 2025/2026.
|
||||
public async Task<IActionResult> cleanuppendingstorefront()
|
||||
{
|
||||
return Ok(new { success = true, error = (string?)null, error_id = (string?)null, value = (string?)null });
|
||||
}
|
||||
|
||||
[HttpGet("crm/me/config/v3")]
|
||||
public async Task<IActionResult> ConfigCRM()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("gameai/user/access")]
|
||||
[HttpGet("makerai/user/access")]
|
||||
[Route("identifyampltiude")]
|
||||
public async Task<IActionResult> identifyamplitudeIdkWhatthisis() // idk what this is bro
|
||||
{
|
||||
return Ok(new { success = true, value = (object?)null, error = (object?)null, error_id = (object?)null });
|
||||
}
|
||||
|
||||
[Route("api/players/v1/playerPhotoTaggingSetting")]
|
||||
public async Task<IActionResult> PlayerPhotoTaggingSettings()
|
||||
{
|
||||
return Ok(new { success = true, gay = "i love femboy thighs"});
|
||||
}
|
||||
|
||||
[HttpGet("/sections/bulk")]
|
||||
public IActionResult GetSectionsBulkShit([FromQuery] List<string> id)
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpGet("outfits/me")]
|
||||
public async Task<IActionResult> GetMyAvatarV2(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("");
|
||||
|
||||
Avatar? avatar = currentPlayer.Player?.PlayerExtra?.Avatar;
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
LegacyData = new
|
||||
{
|
||||
SelectionsV1 = avatar?.OutfitSelections,
|
||||
SelectionsV2 = avatar?.OutfitSelectionsV2,
|
||||
FaceFeatures = avatar?.FaceFeatures,
|
||||
SkinColor = avatar?.SkinColor,
|
||||
HairColor = avatar?.HairColor
|
||||
},
|
||||
Selections = avatar?.CustomAvatarItems?.Select(i => new
|
||||
{
|
||||
i.BodyPart,
|
||||
i.CustomAvatarItemId,
|
||||
BakedUnityAssetFileName = (string?)null,
|
||||
AdditionalConfiguration = (object?)null
|
||||
}).ToArray() ?? Array.Empty<object>(),
|
||||
DataVersion = avatar?.DataVersion ?? 10,
|
||||
CustomizationSettings = avatar?.CustomizationSettings,
|
||||
ThumbnailFileName = (string?)null,
|
||||
Name = (string?)null,
|
||||
Accessibility = 0,
|
||||
Slot = 0
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("outfits/me")] // 2024 - will (might actually) break avatar if on 2025 tho
|
||||
public async Task<IActionResult> PutOutfitMe([FromBody] SavedOutfitRequest request)
|
||||
{
|
||||
var player = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (player == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var version = AuthStuff.GetAndEnsureAppVersion(Request);
|
||||
if (version == "20250724") return BadRequest("");
|
||||
|
||||
if (request == null)
|
||||
return BadRequest("");
|
||||
|
||||
PlayerDB.SaveOutfit(player.PlayerId, request);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("api/referee/files")]
|
||||
public async Task<IActionResult> RefereeFiles()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("sampling")]
|
||||
public async Task<IActionResult> Sampling()
|
||||
{
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "EventTypes.json");
|
||||
if (!System.IO.File.Exists(path)) return Ok(Array.Empty<object>());
|
||||
return PhysicalFile(path, "application/json");
|
||||
}
|
||||
|
||||
[HttpGet("api/consumables/v1/all")]
|
||||
public async Task<IActionResult> AllConsumables()
|
||||
{
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "2025Consumables", "ModernUnlockableConsumables.json");
|
||||
if (!System.IO.File.Exists(path)) return Ok(Array.Empty<object>());
|
||||
return PhysicalFile(path, "application/json");
|
||||
}
|
||||
|
||||
[HttpGet("cdn/config/Prod/AvatarEffectConfig_v1.json")]
|
||||
public async Task<IActionResult> AvatarEffectConfigV1()
|
||||
{
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "AvatarEffectConfigV1.json");
|
||||
if (!System.IO.File.Exists(path)) return Ok(ServerConfig.Bracket);
|
||||
|
||||
return PhysicalFile(path, "application/json");
|
||||
}
|
||||
|
||||
[HttpGet("aimoderation/roomie/enabledForPlayer")]
|
||||
public async Task<IActionResult> IsRoomieEnabled()
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
[HttpPost("realtime-session/create")]
|
||||
public async Task<IActionResult> CreateSessionForRoomieOrMakerAI()
|
||||
{
|
||||
var currentPlayer = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (currentPlayer == null)
|
||||
return Unauthorized("");
|
||||
|
||||
/*return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error = "",
|
||||
error_id = "",
|
||||
value = new
|
||||
{
|
||||
ClientSecret = "",
|
||||
SessionId = "sess_"
|
||||
}
|
||||
});*/
|
||||
// dmcaed
|
||||
await Task.Delay(Timeout.Infinite);
|
||||
return null;
|
||||
// parent of c5d30f8 (Update Controllers/ModernController.cs)
|
||||
}
|
||||
|
||||
[HttpGet("roomieai/user/facts")]
|
||||
public async Task<IActionResult> RoomieUserFacts()
|
||||
{
|
||||
|
||||
var currentPlayer = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (currentPlayer == null)
|
||||
return Unauthorized("");
|
||||
|
||||
return Ok(); // dmcaed
|
||||
}
|
||||
|
||||
[HttpGet("roomieai/user/access")]
|
||||
public async Task<IActionResult> GetUserRoomieAccess()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
MaxEnergyFromSubscriptions = 8000,
|
||||
EnergyLeft = 4793,
|
||||
NextSubscriptionEnergyRechargeAt = (string?)null,
|
||||
OutputAudioEnabled = true
|
||||
},
|
||||
success = true,
|
||||
error_id = (string?)null,
|
||||
error = (string?)null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("gameai/room/{roomId}/spendsummary")]
|
||||
public async Task<IActionResult> GetUserRoomSpendSummary([FromRoute] long roomId)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
value = (object?)null,
|
||||
success = false,
|
||||
error_id = "AI.ManagementAccessDenied",
|
||||
error = "You don't have permission to manage AI in this room."
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("makerai/user/balances")]
|
||||
public async Task<IActionResult> GetMakerAiSpend()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
UsageDollars = 0,
|
||||
UsersMaxUsageDollars = 0,
|
||||
RRPlusUsageDollars = 0.0,
|
||||
UsersMaxRRPlusUsageDollars = 20,
|
||||
TimeBalanceStatus = "Empty",
|
||||
TimeExpiresAt = "0001-01-01T00:00:00",
|
||||
UsageBalanceStatus = "Good",
|
||||
UsagePercent = 0,
|
||||
RRPlusUsageBalanceStatus = "Good",
|
||||
RRPlusUsagePercent = 0
|
||||
});
|
||||
}
|
||||
|
||||
// voice shit does not rlly matter
|
||||
[HttpGet("voice/config")]
|
||||
[HttpGet("api/config/v1/azurespeech")]
|
||||
public async Task<IActionResult> VoiceCrappingsFor2023and2025()
|
||||
{
|
||||
return Ok(new { voiceConnectionId = ":3c", success = true, regions = ServerConfig.Bracket });
|
||||
}
|
||||
|
||||
[HttpPost("outfits/bulk")]
|
||||
public async Task<IActionResult> GetAvatarBulkV2([FromBody] OutfitBulkRequest? request)
|
||||
{
|
||||
var currentPlayer = AuthStuff.GetCurrentPlayer(Request);
|
||||
if (currentPlayer == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var accountIds = request?.AccountIds;
|
||||
if (accountIds == null || accountIds.Count == 0)
|
||||
return Ok(new { OutfitsByAccountId = new Dictionary<string, object>() });
|
||||
|
||||
var avatars = new Dictionary<string, object>();
|
||||
foreach (var accountId in accountIds)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(accountId);
|
||||
if (player == null)
|
||||
continue;
|
||||
|
||||
Avatar? avatar = player.Player?.PlayerExtra?.Avatar;
|
||||
if (avatar == null)
|
||||
continue;
|
||||
|
||||
avatars[accountId.ToString()] = new {
|
||||
LegacyData = new
|
||||
{
|
||||
SelectionsV1 = avatar.OutfitSelections,
|
||||
SelectionsV2 = avatar.OutfitSelectionsV2,
|
||||
avatar.FaceFeatures,
|
||||
avatar.SkinColor,
|
||||
avatar.HairColor
|
||||
},
|
||||
Selections = Array.Empty<object>(),
|
||||
DataVersion = 9,
|
||||
CustomizationSettings = (string?)null,
|
||||
ThumbnailFileName = (string?)null,
|
||||
Name = (string?)null,
|
||||
Accessibility = 0,
|
||||
Slot = 0
|
||||
};
|
||||
}
|
||||
|
||||
return Ok(new { OutfitsByAccountId = avatars });
|
||||
}
|
||||
|
||||
[HttpGet("purchasecampaign/allcurrent/v2")]
|
||||
public async Task<IActionResult> GetPurchaseCampaign()
|
||||
{
|
||||
string path = Path.Join(Program.dataDir, "APIS", "PurchaseCampaign.json");
|
||||
return System.IO.File.Exists(path) ? Content(System.IO.File.ReadAllText(path), "application/json") : NotFound();
|
||||
}
|
||||
|
||||
[HttpGet("match/tachyon")]
|
||||
public async Task<IActionResult> MatchTachyon([FromQuery] long accountId)
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(accountId);
|
||||
if (player == null)
|
||||
return Ok(0);
|
||||
|
||||
return Ok(player.Player?.PlayerExtra?.Heartbeat.roomInstance.roomInstanceId ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,507 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
using Vanadium.Utils.NotiController;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api/playerdb")]
|
||||
public class PlayerDBManagerController : ControllerBase // DISABLED SO LOGAN AND HIS SKID TEAM DONT TRY TO NUKE THE SERVER
|
||||
{
|
||||
/*
|
||||
[HttpGet("players")]
|
||||
public async Task<IActionResult> GetAllPlayers([FromQuery] string? search = null, [FromQuery] int skip = 0, [FromQuery] int take = 50)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allPlayers = PlayerDB.Players.FindAll().ToList();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
string searchLower = search.ToLower();
|
||||
allPlayers = allPlayers.Where(p =>
|
||||
p.Player?.Username?.ToLower().Contains(searchLower) == true ||
|
||||
p.Player?.DisplayName?.ToLower().Contains(searchLower) == true ||
|
||||
p.PlayerId.ToString().Contains(searchLower)
|
||||
).ToList();
|
||||
}
|
||||
|
||||
var total = allPlayers.Count;
|
||||
var results = allPlayers.Skip(skip).Take(take).Select(p => new
|
||||
{
|
||||
p.PlayerId,
|
||||
Username = p.Player?.Username,
|
||||
DisplayName = p.Player?.DisplayName,
|
||||
CreatedAt = p.Player?.CreatedAt,
|
||||
LastLoginAt = p.Player?.LastLoginAt,
|
||||
IsJunior = p.Player?.IsJunior,
|
||||
Level = p.Player?.Level
|
||||
}).ToList();
|
||||
|
||||
return Ok(new { Results = results, Total = total });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("player/{playerId}")]
|
||||
public async Task<IActionResult> GetPlayer(long playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
return Ok(MapPlayerToDTO(player));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("player/{playerId}")]
|
||||
public async Task<IActionResult> UpdatePlayer(long playerId, [FromBody] UpdatePlayerRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
if (player.Player == null)
|
||||
player.Player = new Player();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Username))
|
||||
player.Player.Username = request.Username;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.DisplayName))
|
||||
player.Player.DisplayName = request.DisplayName;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Bio))
|
||||
player.Player.Bio = request.Bio;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.ProfileImage))
|
||||
player.Player.ProfileImage = request.ProfileImage;
|
||||
|
||||
if (request.IsJunior.HasValue)
|
||||
player.Player.IsJunior = request.IsJunior.Value;
|
||||
|
||||
if (request.AvailableUsernameChanges.HasValue)
|
||||
player.Player.AvailableUsernameChanges = request.AvailableUsernameChanges.Value;
|
||||
|
||||
if (request.Level.HasValue)
|
||||
player.Player.Level = request.Level.Value;
|
||||
|
||||
if (request.XP.HasValue)
|
||||
player.Player.XP = request.XP.Value;
|
||||
|
||||
if (request.CreatedAt.HasValue)
|
||||
player.Player.CreatedAt = request.CreatedAt.Value;
|
||||
|
||||
if (request.LastLoginAt.HasValue)
|
||||
player.Player.LastLoginAt = request.LastLoginAt.Value;
|
||||
|
||||
if (request.Birthday.HasValue)
|
||||
player.Player.Birthday = request.Birthday.Value;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Email))
|
||||
player.Player.Email = request.Email;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Password))
|
||||
player.Password = request.Password;
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
var updatedAccount = PlayerDB.GetAccountMe(playerId);
|
||||
if (updatedAccount != null)
|
||||
{
|
||||
await NotiController.SendAccountUpdate(playerId, updatedAccount);
|
||||
await NotiController.SendEvent(playerId, NotiController.EventTypes.SelfAccountUpdate, updatedAccount);
|
||||
}
|
||||
|
||||
return Ok(new { Message = "Player updated successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("player/{playerId}/reputation")]
|
||||
public async Task<IActionResult> UpdatePlayerReputation(long playerId, [FromBody] UpdateReputationRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
if (player.Player?.Reputation == null)
|
||||
player.Player.Reputation = new Reputation();
|
||||
|
||||
var rep = player.Player.Reputation;
|
||||
rep.AccountId = playerId;
|
||||
|
||||
if (request.CheerGeneral.HasValue)
|
||||
rep.CheerGeneral = request.CheerGeneral.Value;
|
||||
if (request.CheerHelpful.HasValue)
|
||||
rep.CheerHelpful = request.CheerHelpful.Value;
|
||||
if (request.CheerCreative.HasValue)
|
||||
rep.CheerCreative = request.CheerCreative.Value;
|
||||
if (request.CheerGreatHost.HasValue)
|
||||
rep.CheerGreatHost = request.CheerGreatHost.Value;
|
||||
if (request.CheerSportsman.HasValue)
|
||||
rep.CheerSportsman = request.CheerSportsman.Value;
|
||||
if (request.CheerCredit.HasValue)
|
||||
rep.CheerCredit = request.CheerCredit.Value;
|
||||
if (request.Notoriety.HasValue)
|
||||
rep.Noteriety = request.Notoriety.Value;
|
||||
if (request.SubscriberCount.HasValue)
|
||||
rep.SubscriberCount = request.SubscriberCount.Value;
|
||||
if (request.IsCheerful.HasValue)
|
||||
rep.IsCheerful = request.IsCheerful.Value;
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
await NotiController.SendReputationUpdate(playerId, player.Player.Reputation);
|
||||
var friends = PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.Relationships != null &&
|
||||
p.Player.Relationships.Any(r => r.PlayerId == playerId && r.RelationshipType == 5))
|
||||
.Select(p => p.PlayerId)
|
||||
.ToList();
|
||||
foreach (var fid in friends)
|
||||
await NotiController.SendReputationUpdate(fid, new { AccountId = playerId, Reputation = player.Player.Reputation });
|
||||
|
||||
return Ok(new { Message = "Reputation updated successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
[HttpGet("player/{playerId}/settings")]
|
||||
public async Task<IActionResult> GetPlayerSettings(long playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
var settings = player.Player?.PlayerExtra?.Settings ?? new List<Setting>();
|
||||
return Ok(settings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("player/{playerId}/settings")]
|
||||
public async Task<IActionResult> AddPlayerSetting(long playerId, [FromBody] SettingRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
if (string.IsNullOrWhiteSpace(request.Key) || string.IsNullOrWhiteSpace(request.Value))
|
||||
return BadRequest(new { Error = "Key and Value are required" });
|
||||
|
||||
PlayerDB.SetPlayerSetting(request.Key, request.Value, playerId);
|
||||
|
||||
return Ok(new { Message = "Setting added/updated successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("player/{playerId}/settings/{settingKey}")]
|
||||
public async Task<IActionResult> DeletePlayerSetting(long playerId, string settingKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
if (player.Player?.PlayerExtra?.Settings != null)
|
||||
{
|
||||
var setting = player.Player.PlayerExtra.Settings.FirstOrDefault(s => s.Key == settingKey);
|
||||
if (setting != null)
|
||||
{
|
||||
player.Player.PlayerExtra.Settings.Remove(setting);
|
||||
PlayerDB.Players.Update(player);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { Message = "Setting deleted successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("player/{playerId}/platforms")]
|
||||
public async Task<IActionResult> AddPlayerPlatform(long playerId, [FromBody] AddPlatformRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
if (player.PlatformIds == null)
|
||||
player.PlatformIds = new List<mPlatformID>();
|
||||
|
||||
var existingPlatform = player.PlatformIds.FirstOrDefault(p => p.Platform == request.Platform);
|
||||
if (existingPlatform != null)
|
||||
{
|
||||
existingPlatform.PlatformId = request.PlatformId;
|
||||
}
|
||||
else
|
||||
{
|
||||
player.PlatformIds.Add(new mPlatformID
|
||||
{
|
||||
Platform = request.Platform,
|
||||
PlatformId = request.PlatformId
|
||||
});
|
||||
}
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
return Ok(new { Message = "Platform added successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("player/{playerId}/platforms/{platform}")]
|
||||
public async Task<IActionResult> RemovePlayerPlatform(long playerId, int platform)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
if (player.PlatformIds != null)
|
||||
{
|
||||
var platformToRemove = player.PlatformIds.FirstOrDefault(p => (int)p.Platform == platform);
|
||||
if (platformToRemove != null)
|
||||
{
|
||||
player.PlatformIds.Remove(platformToRemove);
|
||||
PlayerDB.Players.Update(player);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { Message = "Platform removed successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("player/{playerId}/roles")]
|
||||
public async Task<IActionResult> UpdatePlayerRoles(long playerId, [FromBody] UpdateRolesRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
player.PlayerRoles = new List<PlayerRoles>();
|
||||
|
||||
if (request.IsScreenshare)
|
||||
player.PlayerRoles.Add(PlayerRoles.Screenshare);
|
||||
if (request.IsModerator)
|
||||
player.PlayerRoles.Add(PlayerRoles.Moderator);
|
||||
if (request.IsDeveloper)
|
||||
player.PlayerRoles.Add(PlayerRoles.Developer);
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
var updatedAccount = PlayerDB.GetAccountMe(playerId);
|
||||
if (updatedAccount != null)
|
||||
await NotiController.SendAccountUpdate(playerId, updatedAccount);
|
||||
|
||||
return Ok(new { Message = "Roles updated successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("player/{playerId}/authtoken")]
|
||||
public async Task<IActionResult> GenerateNewAuthToken(long playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var player = PlayerDB.Players.FindById(playerId);
|
||||
if (player == null)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
player.AuthToken = Guid.NewGuid().ToString();
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
return Ok(new { AuthToken = player.AuthToken, Message = "New auth token generated" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete("player/{playerId}")]
|
||||
public async Task<IActionResult> DeletePlayer(long playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var success = PlayerDB.Players.Delete(playerId);
|
||||
if (!success)
|
||||
return NotFound(new { Error = "Player not found" });
|
||||
|
||||
return Ok(new { Message = "Player deleted successfully" });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("test")]
|
||||
public async Task<IActionResult> test()
|
||||
{
|
||||
try
|
||||
{
|
||||
string filePath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "communityboard.json");
|
||||
|
||||
string jsonData = System.IO.File.ReadAllText(filePath);
|
||||
var boardData = JsonSerializer.Deserialize<object>(jsonData);
|
||||
|
||||
var cbPayload = new {
|
||||
Id = "CommunityBoardUpdate",
|
||||
Msg = boardData
|
||||
};
|
||||
|
||||
NotificationsController.SendToAll(JsonSerializer.Serialize(cbPayload));
|
||||
|
||||
return Ok("Community board reloaded successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private object MapPlayerToDTO(FullPlayer player)
|
||||
{
|
||||
var p = player.Player ?? new Player();
|
||||
var rep = p.Reputation ?? new Reputation();
|
||||
|
||||
return new
|
||||
{
|
||||
player.PlayerId,
|
||||
player.AuthToken,
|
||||
player.Password,
|
||||
player.PlayerRoles,
|
||||
player.PlatformIds,
|
||||
Player = new
|
||||
{
|
||||
p.Username,
|
||||
p.DisplayName,
|
||||
p.Bio,
|
||||
p.AvailableUsernameChanges,
|
||||
p.IsJunior,
|
||||
p.Level,
|
||||
p.XP,
|
||||
p.ProfileImage,
|
||||
p.BannerImage,
|
||||
p.Email,
|
||||
p.CreatedAt,
|
||||
p.LastLoginAt,
|
||||
p.Birthday,
|
||||
Reputation = new
|
||||
{
|
||||
rep.CheerGeneral,
|
||||
rep.CheerHelpful,
|
||||
rep.CheerCreative,
|
||||
rep.CheerGreatHost,
|
||||
rep.CheerSportsman,
|
||||
rep.CheerCredit,
|
||||
rep.Noteriety,
|
||||
rep.SubscriberCount,
|
||||
rep.IsCheerful
|
||||
},
|
||||
Settings = p.PlayerExtra?.Settings ?? new List<Setting>()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public class UpdatePlayerRequest
|
||||
{
|
||||
public string? Username { get; set; }
|
||||
public string? DisplayName { get; set; }
|
||||
public string? Bio { get; set; }
|
||||
public string? ProfileImage { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public bool? IsJunior { get; set; }
|
||||
public int? AvailableUsernameChanges { get; set; }
|
||||
public int? Level { get; set; }
|
||||
public int? XP { get; set; }
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
public DateTime? LastLoginAt { get; set; }
|
||||
public DateTime? Birthday { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateReputationRequest
|
||||
{
|
||||
public int? CheerGeneral { get; set; }
|
||||
public int? CheerHelpful { get; set; }
|
||||
public int? CheerCreative { get; set; }
|
||||
public int? CheerGreatHost { get; set; }
|
||||
public int? CheerSportsman { get; set; }
|
||||
public int? CheerCredit { get; set; }
|
||||
public double? Notoriety { get; set; }
|
||||
public int? SubscriberCount { get; set; }
|
||||
public bool? IsCheerful { get; set; }
|
||||
}
|
||||
|
||||
public class SettingRequest
|
||||
{
|
||||
public required string Key { get; set; }
|
||||
public required string Value { get; set; }
|
||||
}
|
||||
|
||||
public class AddPlatformRequest
|
||||
{
|
||||
public Platforms Platform { get; set; }
|
||||
public ulong PlatformId { get; set; }
|
||||
}
|
||||
|
||||
public class UpdateRolesRequest
|
||||
{
|
||||
public bool IsScreenshare { get; set; }
|
||||
public bool IsModerator { get; set; }
|
||||
public bool IsDeveloper { get; set; }
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("/")]
|
||||
public partial class ProgressionController : ControllerBase
|
||||
{
|
||||
private bool CanEdit(RoomDBClasses.Room room, long playerId)
|
||||
=> room.CreatorAccountId == playerId ||
|
||||
(room.Roles?.Any(r => r.AccountId == playerId &&
|
||||
(r.Role == RoomDBClasses.Role.CoOwner ||
|
||||
r.Role == RoomDBClasses.Role.Creator ||
|
||||
r.Role == RoomDBClasses.Role.TemporaryCoOwner)) ?? false);
|
||||
|
||||
[HttpGet("api/players/v1/progression/xpEarnedToday")]
|
||||
public IActionResult xpEarnedToday()
|
||||
{
|
||||
return Ok((int)67);
|
||||
}
|
||||
|
||||
[HttpGet("roomserver/rooms/{roomId}/experience")]
|
||||
public async Task<IActionResult> GetRoomExperienceTho(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return NotFound();
|
||||
return Ok(new { DailyLimit = room.ExperienceDailyLimit > 0 ? room.ExperienceDailyLimit : 1000000, Enabled = room.ExperienceEnabled, RoomExperienceId = roomId });
|
||||
}
|
||||
|
||||
[HttpPost("/roomserver/rooms/{roomId}/experience")]
|
||||
public async Task<IActionResult> SetRoomExperience(long roomId, [FromForm] bool enabled, [FromForm] int? dailyLimit)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return NotFound();
|
||||
|
||||
if (!CanEdit(room, (long)id)) return Ok(new { success = false });
|
||||
|
||||
room.ExperienceEnabled = enabled;
|
||||
|
||||
if (dailyLimit.HasValue)
|
||||
room.ExperienceDailyLimit = dailyLimit.Value;
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
return Ok(new { success = true, error = "", value = new { DailyLimit = room.ExperienceDailyLimit, Enabled = room.ExperienceEnabled, RoomExperienceId = roomId } });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
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.RoomCommentsDBClasses;
|
||||
using static Vanadium.Classes.ServerConfig;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("comments")]
|
||||
public class RoomCommentsController : ControllerBase
|
||||
{
|
||||
private static readonly HttpClient WebhookClient = new HttpClient();
|
||||
|
||||
private static async Task SendCommentWebhook(string type, RoomDBClasses.Room? room, RoomComment comment, long actorPlayerId, bool actorOwnsComment)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(RoomCommentsWebhook))
|
||||
return;
|
||||
|
||||
var actor = PlayerDB.GetCurrentPlayer(actorPlayerId);
|
||||
string displayName = actor?.Player?.DisplayName ?? actorPlayerId.ToString();
|
||||
string roomName = room?.Name ?? comment.roomId.ToString();
|
||||
string roomImage = room?.ImageName ?? "";
|
||||
|
||||
var fields = new List<object>();
|
||||
|
||||
if (type == "created")
|
||||
{
|
||||
fields.Add(new
|
||||
{
|
||||
name = "Comment",
|
||||
value = comment.message,
|
||||
inline = false
|
||||
});
|
||||
}
|
||||
|
||||
fields.Add(new
|
||||
{
|
||||
name = "Room",
|
||||
value = $"{roomName} `{comment.roomId}`",
|
||||
inline = true
|
||||
});
|
||||
|
||||
fields.Add(new
|
||||
{
|
||||
name = "Account",
|
||||
value = $"{displayName} `{actorPlayerId}` *(owns: {actorOwnsComment})*",
|
||||
inline = true
|
||||
});
|
||||
|
||||
var payload = System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
embeds = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
author = new
|
||||
{
|
||||
name = $"Room Comment was {type}",
|
||||
icon_url = $"https://reloxa.xyz/imageserver/{roomImage}"
|
||||
},
|
||||
fields,
|
||||
color = 12190963
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await WebhookClient.PostAsync(RoomCommentsWebhook,
|
||||
new StringContent(payload, System.Text.Encoding.UTF8, "application/json"));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
[HttpGet("get/{roomId}")]
|
||||
public IActionResult GetComments(long roomId, [FromQuery] int count = 100, [FromQuery] long minId = -1)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var comments = RoomCommentsDB.GetComments(roomId, count, minId);
|
||||
return Ok(comments);
|
||||
}
|
||||
|
||||
[HttpPost("create/{roomId}")]
|
||||
public async Task<IActionResult> CreateComment(long roomId, [FromForm] CreateCommentRequest req)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(req.message))
|
||||
return Ok(new { success = false, error = "Message cannot be empty.", value = (object?)null });
|
||||
|
||||
if (req.message.Length > 100)
|
||||
return Ok(new { success = false, error = "Message is too long.", value = (object?)null });
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return Ok(new { success = false, error = "Room not found.", value = (object?)null });
|
||||
|
||||
if (room.DisableRoomComments)
|
||||
return Ok(new { success = false, error_id = "Rooms.RoomCommentsDisabledForHighlightedRoom", error = "Room Comments are disabled!" });
|
||||
|
||||
var comment = RoomCommentsDB.CreateComment(
|
||||
roomId,
|
||||
req.subRoomId,
|
||||
playerId.Value,
|
||||
req.message,
|
||||
req.style,
|
||||
req.positionX,
|
||||
req.positionY,
|
||||
req.positionZ
|
||||
);
|
||||
|
||||
_ = SendCommentWebhook("created", room, comment, playerId.Value, true);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error = "",
|
||||
value = comment
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("read/{roomId}/{commentId}")]
|
||||
public IActionResult MarkCommentRead(long roomId, long commentId)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
RoomCommentsDB.MarkAsRead(roomId, commentId);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpDelete("delete/{commentId}")]
|
||||
public async Task<IActionResult> DeleteComment(long commentId)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var comment = RoomCommentsDB.GetComment(commentId);
|
||||
if (comment == null)
|
||||
return Ok(new { success = false, error = "Comment not found.", value = (object?)null });
|
||||
|
||||
var room = RoomDB.GetRoom(comment.roomId);
|
||||
bool isOwner = room?.CreatorAccountId == playerId.Value;
|
||||
bool isAuthor = comment.accountId == playerId.Value;
|
||||
bool hasRoomRole = room?.Roles?.Any(r =>
|
||||
r.AccountId == playerId.Value &&
|
||||
(int)r.Role >= (int)Vanadium.Classes.DBs.DBClasses.RoomDBClasses.Role.Moderator) ?? false;
|
||||
|
||||
if (!isOwner && !isAuthor && !hasRoomRole)
|
||||
return StatusCode(403);
|
||||
|
||||
RoomCommentsDB.DeleteComment(commentId);
|
||||
|
||||
_ = SendCommentWebhook("deleted", room, comment, playerId.Value, isAuthor);
|
||||
|
||||
var json = Newtonsoft.Json.JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = Vanadium.Hubs.NotiEventTypes.RoomCommentDeleted,
|
||||
Msg = new
|
||||
{
|
||||
RoomId = comment.roomId,
|
||||
Data = comment
|
||||
}
|
||||
});
|
||||
|
||||
await NotificationsController.SendToAll(json);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes.DBs;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomConsumablesDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class RoomConsumablesController : ControllerBase
|
||||
{
|
||||
private static class Status
|
||||
{
|
||||
public const int Success = 0;
|
||||
public const int RoomConsumableNotFound = 4;
|
||||
public const int PlayerDoesntHavePermission = 7;
|
||||
public const int MaxConsumablesInRoom = 13;
|
||||
public const int RoomIdMissing = 15;
|
||||
public const int PriceOrCurrencyMissing = 17;
|
||||
public const int CurrencyNotFound = 18;
|
||||
public const int PriceTooLowTokens = 21;
|
||||
public const int PriceTooHighTokens = 22;
|
||||
public const int NameTooShort = 23;
|
||||
public const int NameTooLong = 24;
|
||||
public const int DescriptionTooLong = 27;
|
||||
public const int ConcurrencyCodeMismatch = 32;
|
||||
public const int PlayerDoesNotOwnConsumable = 33;
|
||||
public const int PurchaseFailed = 35;
|
||||
public const int RoomNotFound = 36;
|
||||
public const int RequestedPriceDoesNotMatch = 38;
|
||||
public const int RequestedCurrencyDoesNotMatch = 39;
|
||||
public const int ConsumableCannotBePurchasedWithRoomCurrency = 40;
|
||||
public const int ConsumableCannotBePurchasedWithTokens = 41;
|
||||
}
|
||||
|
||||
private static class TokenOp
|
||||
{
|
||||
public const int OK = 0;
|
||||
public const int NotEnoughCredit = 2;
|
||||
public const int RequestedPriceDoesNotMatch = 6;
|
||||
}
|
||||
|
||||
private static class CurrencyOp
|
||||
{
|
||||
public const int Success = 0;
|
||||
public const int NotEnoughCredit = 1;
|
||||
}
|
||||
|
||||
private const int TokenCurrencyType = 2;
|
||||
private const int MaxConsumablesPerRoom = 50;
|
||||
|
||||
[HttpGet("/econ/roomInventory/room/{roomId:long}")]
|
||||
[HttpGet("/api/roomconsumables/v1/roomConsumable/room/{roomId:long}")]
|
||||
public IActionResult ListForRoom(long roomId) =>
|
||||
Ok(RoomConsumablesDB.GetConsumablesForRoom(roomId).Select(ToDescWire).ToList());
|
||||
|
||||
[HttpGet("/econ/roomInventory/room/{roomId:long}/player")]
|
||||
[HttpGet("/api/roomconsumables/v1/roomConsumable/room/{roomId:long}/me")]
|
||||
public IActionResult MineForRoom(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var rows = RoomConsumablesDB.GetInventoryForRoom(id.Value, roomId);
|
||||
return Ok(rows.Select(r => ToInventoryWire(r.Ownership, r.Item)).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("/api/roomconsumables/v1/roomConsumable/{roomConsumableId:guid}")]
|
||||
public IActionResult GetOne(Guid roomConsumableId)
|
||||
{
|
||||
var item = RoomConsumablesDB.GetConsumable(roomConsumableId);
|
||||
return item == null ? NotFound("") : Ok(ToDescWire(item));
|
||||
}
|
||||
|
||||
[HttpGet("/api/roomconsumables/v1/roomConsumable/{roomConsumableId:guid}/isOwned")]
|
||||
public IActionResult IsOwned(Guid roomConsumableId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var item = RoomConsumablesDB.GetConsumable(roomConsumableId);
|
||||
if (item == null) return Content("false", "application/json");
|
||||
|
||||
return Content(RoomConsumablesDB.IsOwnedByOthers(item) ? "true" : "false", "application/json");
|
||||
}
|
||||
|
||||
[HttpPost("/api/roomconsumables/v1/roomConsumable")]
|
||||
[HttpPut("/api/roomconsumables/v1/roomConsumable")]
|
||||
public async Task<IActionResult> CreateOrUpdate()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var body = await ReadBodyAsync();
|
||||
var name = (ReadString(body, "Name") ?? "").Trim();
|
||||
var description = (ReadString(body, "Description") ?? "").Trim();
|
||||
var imageName = (ReadString(body, "ImageName") ?? "").Trim();
|
||||
var (price, currencyId, hasPrice) = ReadPriceAndCurrency(body);
|
||||
|
||||
var publicId = ReadGuid(body, "RoomConsumableId");
|
||||
var existing = publicId is Guid pid && pid != Guid.Empty
|
||||
? RoomConsumablesDB.GetConsumable(pid)
|
||||
: null;
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
var roomId = ReadLong(body, "RoomId");
|
||||
if (roomId is not long rid || rid <= 0) return Ok(EditResponse(Status.RoomIdMissing));
|
||||
if (RoomDB.GetRoom(rid) == null) return Ok(EditResponse(Status.RoomNotFound));
|
||||
if (!RoomDB.UserCanEditRoom(rid, id.Value))
|
||||
return Ok(EditResponse(Status.PlayerDoesntHavePermission));
|
||||
|
||||
if (ValidateFields(name, description, hasPrice, price) is int fieldError)
|
||||
return Ok(EditResponse(fieldError));
|
||||
if (RoomConsumablesDB.CountConsumablesInRoom(rid) >= MaxConsumablesPerRoom)
|
||||
return Ok(EditResponse(Status.MaxConsumablesInRoom));
|
||||
|
||||
var created = RoomConsumablesDB.CreateConsumable(
|
||||
rid, id.Value, name, description, imageName, price, currencyId);
|
||||
return Ok(EditResponse(Status.Success, created));
|
||||
}
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(existing.RoomId, id.Value))
|
||||
return Ok(EditResponse(Status.PlayerDoesntHavePermission, existing));
|
||||
|
||||
if (ValidateFields(name, description, hasPrice, price) is int updateError)
|
||||
return Ok(EditResponse(updateError, existing));
|
||||
|
||||
RoomConsumablesDB.UpdateConsumable(existing, name, description, imageName, price, currencyId);
|
||||
return Ok(EditResponse(Status.Success, existing));
|
||||
}
|
||||
|
||||
[HttpDelete("/api/roomconsumables/v1/roomConsumable/{roomConsumableId:guid}")]
|
||||
[HttpPost("/api/roomconsumables/v1/roomConsumable/{roomConsumableId:guid}")]
|
||||
public IActionResult Delete(Guid roomConsumableId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var item = RoomConsumablesDB.GetConsumable(roomConsumableId);
|
||||
if (item == null) return StatusBody(Status.RoomConsumableNotFound);
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(item.RoomId, id.Value))
|
||||
return StatusBody(Status.PlayerDoesntHavePermission);
|
||||
|
||||
RoomConsumablesDB.DeleteConsumable(roomConsumableId);
|
||||
return StatusBody(Status.Success);
|
||||
}
|
||||
|
||||
[HttpPut("/api/roomconsumables/v1/roomconsumable/{roomConsumableId:guid}/purchase/tokens")]
|
||||
[HttpPost("/api/roomconsumables/v1/roomconsumable/{roomConsumableId:guid}/purchase/tokens")]
|
||||
public async Task<IActionResult> PurchaseTokens(Guid roomConsumableId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var item = RoomConsumablesDB.GetConsumable(roomConsumableId);
|
||||
if (item == null) return Ok(CurrencyTokenResponse(null, Status.RoomConsumableNotFound, 0));
|
||||
|
||||
var body = await ReadBodyAsync();
|
||||
var (price, _, hasPrice) = ReadPriceAndCurrency(body, "ExpectedPriceAndCurrency");
|
||||
var newCode = ReadConcurrency(body).NewCode;
|
||||
var balance = StorefrontsDB.GetBalance(id.Value, TokenCurrencyType);
|
||||
|
||||
if (item.CurrencyId != null)
|
||||
return Ok(CurrencyTokenResponse(null, Status.ConsumableCannotBePurchasedWithTokens, balance));
|
||||
if (!hasPrice || price != item.Price)
|
||||
return Ok(CurrencyTokenResponse(
|
||||
TokenOp.RequestedPriceDoesNotMatch, Status.RequestedPriceDoesNotMatch, balance));
|
||||
if (balance < item.Price)
|
||||
return Ok(CurrencyTokenResponse(TokenOp.NotEnoughCredit, Status.PurchaseFailed, balance));
|
||||
|
||||
var newBalance = balance;
|
||||
if (item.Price > 0)
|
||||
{
|
||||
var deducted = StorefrontsDB.DeductCurrency(id.Value, TokenCurrencyType, item.Price);
|
||||
if (deducted == null)
|
||||
return Ok(CurrencyTokenResponse(TokenOp.NotEnoughCredit, Status.PurchaseFailed, balance));
|
||||
newBalance = deducted.Balance;
|
||||
|
||||
if (item.CreatorPlayerId != id.Value)
|
||||
StorefrontsDB.AddCurrency(item.CreatorPlayerId, TokenCurrencyType, item.Price);
|
||||
}
|
||||
|
||||
var own = RoomConsumablesDB.AddToInventory(id.Value, item.RoomConsumableId, newCode);
|
||||
return Ok(CurrencyTokenResponse(TokenOp.OK, Status.Success, newBalance, ToInventoryWire(own, item)));
|
||||
}
|
||||
|
||||
[HttpPut("/api/roomconsumables/v1/roomconsumable/{roomConsumableId:guid}/purchase/currency")]
|
||||
[HttpPost("/api/roomconsumables/v1/roomconsumable/{roomConsumableId:guid}/purchase/currency")]
|
||||
public async Task<IActionResult> PurchaseCurrency(Guid roomConsumableId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var item = RoomConsumablesDB.GetConsumable(roomConsumableId);
|
||||
if (item == null)
|
||||
return Ok(CurrencyResponse(null, Status.RoomConsumableNotFound, id.Value, Guid.Empty, 0));
|
||||
if (item.CurrencyId is not Guid currencyId)
|
||||
return Ok(CurrencyResponse(
|
||||
null, Status.ConsumableCannotBePurchasedWithRoomCurrency, id.Value, Guid.Empty, 0));
|
||||
|
||||
var body = await ReadBodyAsync();
|
||||
var (price, requestedCurrency, hasPrice) = ReadPriceAndCurrency(body, "ExpectedPriceAndCurrency");
|
||||
var newCode = ReadConcurrency(body).NewCode;
|
||||
var balance = RoomConsumablesDB.GetCurrencyBalance(id.Value, currencyId);
|
||||
|
||||
if (!hasPrice || price != item.Price)
|
||||
return Ok(CurrencyResponse(
|
||||
null, Status.RequestedPriceDoesNotMatch, id.Value, currencyId, balance));
|
||||
if (requestedCurrency is Guid reqCur && reqCur != currencyId)
|
||||
return Ok(CurrencyResponse(
|
||||
null, Status.RequestedCurrencyDoesNotMatch, id.Value, currencyId, balance));
|
||||
if (balance < item.Price)
|
||||
return Ok(CurrencyResponse(
|
||||
CurrencyOp.NotEnoughCredit, Status.PurchaseFailed, id.Value, currencyId, balance));
|
||||
|
||||
var newBalance = balance;
|
||||
if (item.Price > 0)
|
||||
{
|
||||
var deducted = RoomConsumablesDB.DeductCurrencyBalance(id.Value, currencyId, item.Price);
|
||||
if (deducted is not int remaining)
|
||||
return Ok(CurrencyResponse(
|
||||
CurrencyOp.NotEnoughCredit, Status.PurchaseFailed, id.Value, currencyId, balance));
|
||||
newBalance = remaining;
|
||||
}
|
||||
|
||||
var own = RoomConsumablesDB.AddToInventory(id.Value, item.RoomConsumableId, newCode);
|
||||
return Ok(CurrencyResponse(CurrencyOp.Success, Status.Success, id.Value, currencyId,
|
||||
newBalance, ToInventoryWire(own, item)));
|
||||
}
|
||||
|
||||
[HttpPut("/api/roomconsumables/v1/roomConsumable/{roomConsumableId:guid}/consume")]
|
||||
[HttpPost("/api/roomconsumables/v1/roomConsumable/{roomConsumableId:guid}/consume")]
|
||||
public async Task<IActionResult> Consume(Guid roomConsumableId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var item = RoomConsumablesDB.GetConsumable(roomConsumableId);
|
||||
if (item == null)
|
||||
return Ok(new { Status = Status.RoomConsumableNotFound, InventoryItem = (object?)null });
|
||||
|
||||
var own = RoomConsumablesDB.GetOwnership(id.Value, item.RoomConsumableId);
|
||||
if (own == null || own.Count <= 0)
|
||||
return Ok(new
|
||||
{
|
||||
Status = Status.PlayerDoesNotOwnConsumable,
|
||||
InventoryItem = own == null ? null : ToInventoryWire(own, item)
|
||||
});
|
||||
|
||||
var (currentCode, newCode) = ReadConcurrency(await ReadBodyAsync());
|
||||
if (currentCode is Guid current && current != own.ConcurrencyCode)
|
||||
return Ok(new { Status = Status.ConcurrencyCodeMismatch, InventoryItem = ToInventoryWire(own, item) });
|
||||
|
||||
RoomConsumablesDB.ConsumeOne(own, newCode);
|
||||
return Ok(new { Status = Status.Success, InventoryItem = ToInventoryWire(own, item) });
|
||||
}
|
||||
|
||||
private static int? ValidateFields(string name, string description, bool hasPrice, int price)
|
||||
{
|
||||
if (name.Length == 0) return Status.NameTooShort;
|
||||
if (name.Length > 128) return Status.NameTooLong;
|
||||
if (description.Length > 1024) return Status.DescriptionTooLong;
|
||||
if (!hasPrice) return Status.PriceOrCurrencyMissing;
|
||||
if (price < 0) return Status.PriceTooLowTokens;
|
||||
if (price > 1_000_000) return Status.PriceTooHighTokens;
|
||||
return null;
|
||||
}
|
||||
|
||||
private IActionResult StatusBody(int status) => Content(status.ToString(), "application/json");
|
||||
|
||||
private static object EditResponse(int status, RoomConsumable? item = null) => new
|
||||
{
|
||||
Status = status,
|
||||
Consumable = item == null ? null : ToDescWire(item)
|
||||
};
|
||||
|
||||
private static object CurrencyTokenResponse(
|
||||
int? balanceUpdateResult, int status, int balance, object? inventoryItem = null) => new
|
||||
{
|
||||
OperationResult = new { Status = status, InventoryItem = inventoryItem },
|
||||
BalanceUpdateResult = balanceUpdateResult,
|
||||
TokenBalanceResponse = new
|
||||
{
|
||||
CurrencyType = TokenCurrencyType,
|
||||
Balance = balance,
|
||||
Platform = 0
|
||||
}
|
||||
};
|
||||
|
||||
private static object CurrencyResponse(
|
||||
int? balanceUpdateResult, int status, long playerId, Guid currencyId, int balance,
|
||||
object? inventoryItem = null) => new
|
||||
{
|
||||
OperationResult = new { Status = status, InventoryItem = inventoryItem },
|
||||
BalanceUpdateResult = balanceUpdateResult,
|
||||
CurrencyBalanceResponse = new
|
||||
{
|
||||
AccountId = (int)playerId,
|
||||
CurrencyId = currencyId,
|
||||
Balance = balance,
|
||||
ModifiedAt = DateTime.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
private static object ToDescWire(RoomConsumable c) => new
|
||||
{
|
||||
RoomConsumableId = c.RoomConsumableId,
|
||||
c.RoomId,
|
||||
c.Name,
|
||||
Description = c.Description ?? "",
|
||||
ImageName = c.ImageName ?? "",
|
||||
c.Price,
|
||||
PurchaseCurrencyId = c.CurrencyId,
|
||||
ModifiedAt = DateTime.SpecifyKind(c.ModifiedAt, DateTimeKind.Utc)
|
||||
};
|
||||
|
||||
private static object ToInventoryWire(RoomConsumableOwnership own, RoomConsumable item) => new
|
||||
{
|
||||
RoomConsumableId = item.RoomConsumableId,
|
||||
AccountId = (int)own.PlayerId,
|
||||
Count = Math.Max(0, own.Count),
|
||||
own.ConcurrencyCode,
|
||||
ModifiedAt = DateTime.SpecifyKind(own.ModifiedAt, DateTimeKind.Utc),
|
||||
Consumable = ToDescWire(item)
|
||||
};
|
||||
|
||||
private async Task<Dictionary<string, JsonElement>> ReadBodyAsync()
|
||||
{
|
||||
var fields = new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
|
||||
if (Request.ContentLength is 0) return fields;
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = await JsonDocument.ParseAsync(Request.Body);
|
||||
if (doc.RootElement.ValueKind != JsonValueKind.Object) return fields;
|
||||
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
fields[prop.Name] = prop.Value.Clone();
|
||||
if (prop.Value.ValueKind == JsonValueKind.Object)
|
||||
foreach (var inner in prop.Value.EnumerateObject())
|
||||
fields.TryAdd(inner.Name, inner.Value.Clone());
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
private static string? ReadString(Dictionary<string, JsonElement> fields, string name) =>
|
||||
fields.TryGetValue(name, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() : null;
|
||||
|
||||
private static long? ReadLong(Dictionary<string, JsonElement> fields, string name)
|
||||
{
|
||||
if (!fields.TryGetValue(name, out var v)) return null;
|
||||
return v.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Number when v.TryGetInt64(out var n) => n,
|
||||
JsonValueKind.String when long.TryParse(v.GetString(), out var n) => n,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static Guid? ReadGuid(Dictionary<string, JsonElement> fields, string name) =>
|
||||
fields.TryGetValue(name, out var v) && v.ValueKind == JsonValueKind.String
|
||||
&& Guid.TryParse(v.GetString(), out var guid)
|
||||
? guid
|
||||
: null;
|
||||
|
||||
/// <summary>Read { Price, CurrencyId } from the body, whether it arrives nested under
|
||||
/// <paramref name="wrapper"/> / PriceAndCurrency or flattened at the root.</summary>
|
||||
private static (int Price, Guid? CurrencyId, bool HasPrice) ReadPriceAndCurrency(
|
||||
Dictionary<string, JsonElement> fields, string wrapper = "PriceAndCurrency")
|
||||
{
|
||||
var scope = fields;
|
||||
if (fields.TryGetValue(wrapper, out var nested) && nested.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
scope = new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var prop in nested.EnumerateObject()) scope[prop.Name] = prop.Value;
|
||||
}
|
||||
|
||||
var price = ReadLong(scope, "Price");
|
||||
return ((int)(price ?? 0), ReadGuid(scope, "CurrencyId"), price != null);
|
||||
}
|
||||
|
||||
private static (Guid? CurrentCode, Guid? NewCode) ReadConcurrency(Dictionary<string, JsonElement> fields)
|
||||
{
|
||||
var scope = fields;
|
||||
if (fields.TryGetValue("ConcurrencyCodes", out var nested) && nested.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
scope = new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var prop in nested.EnumerateObject()) scope[prop.Name] = prop.Value;
|
||||
}
|
||||
|
||||
return (ReadGuid(scope, "CurrentConcurrencyCode"), ReadGuid(scope, "NewConcurrencyCode"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2461 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using LiteDB;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Infrastructure;
|
||||
using Microsoft.Extensions.Configuration.UserSecrets;
|
||||
using Newtonsoft.Json;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using Vanadium.Classes.WebSocket;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text;
|
||||
using Vanadium.Utils;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
using Vanadium.Utils.NotiController;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("/roomserver")]
|
||||
public partial class RoomController : ControllerBase
|
||||
{
|
||||
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<long, List<DateTime>> _cloneRateLimits = new();
|
||||
|
||||
private IActionResult RoomError(string errorId, string message, int status = 200)
|
||||
=> StatusCode(status, new { success = false, error_id = errorId, error = message, value = (object?)null });
|
||||
|
||||
private bool CanEdit(Room room, long playerId)
|
||||
=> room.CreatorAccountId == playerId ||
|
||||
(room.Roles?.Any(r => r.AccountId == playerId &&
|
||||
(r.Role == Role.CoOwner || r.Role == Role.Creator || r.Role == Role.TemporaryCoOwner)) ?? false);
|
||||
|
||||
private static bool IsBaseRoom(Room r)
|
||||
=> r.Tags != null && r.Tags.Any(t => t.Tag != null && t.Tag.Equals("base", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private bool CanViewSaves(Room room, long playerId)
|
||||
=> room.CreatorAccountId == playerId ||
|
||||
(room.Roles?.Any(r => r.AccountId == playerId &&
|
||||
(r.Role == Role.CoOwner || r.Role == Role.Creator || r.Role == Role.TemporaryCoOwner)) ?? false) ||
|
||||
(PlayerDB.Players.FindById(playerId)?.PlayerRoles?.Contains(PlayerDBClasses.PlayerRoles.Developer) ?? false);
|
||||
|
||||
private bool RequestHasGameClientRole()
|
||||
{
|
||||
var auth = Request.Headers["Authorization"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Bearer "))
|
||||
return false;
|
||||
|
||||
var tokenStr = auth.Substring("Bearer ".Length).Trim();
|
||||
|
||||
try
|
||||
{
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var jwt = handler.ReadJwtToken(tokenStr);
|
||||
|
||||
var roleClaim = jwt.Claims.FirstOrDefault(c => c.Type == "role")?.Value;
|
||||
if (string.IsNullOrEmpty(roleClaim))
|
||||
return false;
|
||||
|
||||
var roles = System.Text.Json.JsonSerializer.Deserialize<List<string>>(roleClaim);
|
||||
return roles?.Contains("gameClient") ?? false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("rooms")]
|
||||
public async Task<IActionResult> GetRoomBy([FromQuery] string? name)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
var room = RoomDB.GetRoomByName(name);
|
||||
return room != null ? Ok(room) : NotFound();
|
||||
}
|
||||
return NotFound("");
|
||||
}
|
||||
|
||||
|
||||
/*[HttpGet("rooms/{roomId}/subrooms/{subroomId}/saves/{subRoomDataSaveId}")]
|
||||
public async Task<IActionResult> GetRoomSave(ulong roomId, ulong subroomId, ulong subRoomDataSaveId, [FromQuery] int unityAssetTarget, [FromQuery] int unityAssetVersion)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom((long)roomId);
|
||||
if (room == null)
|
||||
return new ContentResult() { Content = "", ContentType = "application/json", StatusCode = 404 };
|
||||
|
||||
if (!CanViewSaves(room, (long)playerId))
|
||||
return StatusCode(403);
|
||||
|
||||
var save = RoomDB.GetRoomSaveById(roomId, subroomId, subRoomDataSaveId, unityAssetTarget, unityAssetVersion);
|
||||
|
||||
if (save == null)
|
||||
{
|
||||
return new ContentResult()
|
||||
{
|
||||
Content = "",
|
||||
ContentType = "application/json",
|
||||
StatusCode = 404
|
||||
};
|
||||
}
|
||||
|
||||
return Ok(save);
|
||||
}*/
|
||||
|
||||
[HttpGet("rooms/{roomid}")]
|
||||
public IActionResult GetRoomById(long roomid)
|
||||
{
|
||||
var room = RoomDB.GetRoom(roomid);
|
||||
|
||||
if (room == null)
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
var roomResponse = new
|
||||
{
|
||||
room.RoomId,
|
||||
room.IsDorm,
|
||||
room.MaxPlayerCalculationMode,
|
||||
room.MaxPlayers,
|
||||
room.CloningAllowed,
|
||||
room.DisableMicAutoMute,
|
||||
room.DisableRoomComments,
|
||||
room.EncryptVoiceChat,
|
||||
room.ToxmodEnabled,
|
||||
room.LoadScreenLocked,
|
||||
room.PersistenceVersion,
|
||||
room.UgcSubVersion,
|
||||
room.UgcVersion,
|
||||
room.AutoLocalizeRoom,
|
||||
room.IsDeveloperOwned,
|
||||
room.IsRRO,
|
||||
room.IsRecRoomApproved,
|
||||
room.IsJuniorCreated,
|
||||
room.IsPlacePlay,
|
||||
room.ExcludeFromLists,
|
||||
room.ExcludeFromSearch,
|
||||
room.Name,
|
||||
room.Description,
|
||||
ImageName = room.ImageName == "DormRoom.jpg" ? null : room.ImageName,
|
||||
room.WarningMask,
|
||||
room.CustomWarning,
|
||||
room.CreatorAccountId,
|
||||
room.State,
|
||||
room.Accessibility,
|
||||
room.SupportsLevelVoting,
|
||||
room.SupportsScreens,
|
||||
room.SupportsWalkVR,
|
||||
room.SupportsTeleportVR,
|
||||
room.SupportsVRLow,
|
||||
room.SupportsQuest2,
|
||||
room.SupportsMobile,
|
||||
room.SupportsJuniors,
|
||||
room.MinLevel,
|
||||
room.MinUgcSubVersion,
|
||||
room.AgeRating,
|
||||
room.CreatedAt,
|
||||
room.PublishedAt,
|
||||
room.BecameRRStudioRoomAt,
|
||||
room.Stats,
|
||||
room.RankedEntityId,
|
||||
room.RankingContext,
|
||||
DataBlob = room.DataBlob,
|
||||
room.CurrentSnapshotId,
|
||||
room.NeedsSnapshotId,
|
||||
room.ShouldAutoStageSaves,
|
||||
room.Roles,
|
||||
room.Tags,
|
||||
room.PromoImages,
|
||||
room.PromoExternalContent,
|
||||
room.LoadScreens,
|
||||
room.RestrictedCircuitsAllowListNames,
|
||||
room.LocalizationContext,
|
||||
room.RoomBanDetails,
|
||||
|
||||
SubRooms = room.SubRooms?.Select(sub => new
|
||||
{
|
||||
CurrentSave = (sub.CurrentSave != null ? new
|
||||
{
|
||||
sub.CurrentSave.SubRoomDataSaveId,
|
||||
sub.CurrentSave.SubRoomId,
|
||||
sub.CurrentSave.RoomId,
|
||||
DataBlob = sub.CurrentSave.DataBlob,
|
||||
sub.CurrentSave.SavedByAccountId,
|
||||
sub.CurrentSave.SavedOnPlatform,
|
||||
sub.CurrentSave.SavedOnDeviceClass,
|
||||
sub.CurrentSave.Description,
|
||||
sub.CurrentSave.CreatedAt,
|
||||
sub.CurrentSave.UnityAssetId
|
||||
} : RoomDB.SubRoomSaves
|
||||
.Find(x => x.RoomId == room.RoomId && x.SubRoomId == sub.SubRoomId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Select(save => new
|
||||
{
|
||||
save.SubRoomDataSaveId,
|
||||
save.SubRoomId,
|
||||
save.RoomId,
|
||||
DataBlob = save.DataBlob,
|
||||
save.SavedByAccountId,
|
||||
save.SavedOnPlatform,
|
||||
save.SavedOnDeviceClass,
|
||||
save.Description,
|
||||
save.CreatedAt,
|
||||
save.UnityAssetId
|
||||
})
|
||||
.FirstOrDefault()),
|
||||
|
||||
sub.Accessibility,
|
||||
sub.IsSandbox,
|
||||
sub.MaxPlayers,
|
||||
sub.Name,
|
||||
sub.RoomId,
|
||||
sub.SubRoomId,
|
||||
sub.UnitySceneId,
|
||||
DataBlob = sub.DataBlob,
|
||||
sub.SavedByAccountId,
|
||||
sub.ShouldAutoStageSaves,
|
||||
sub.StagedSubRoomDataSaveId,
|
||||
sub.LastModeratedSaveModerationState,
|
||||
sub.CreatorAccountId
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
return new ContentResult
|
||||
{
|
||||
Content = JsonConvert.SerializeObject(roomResponse),
|
||||
ContentType = "application/json",
|
||||
StatusCode = 200
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("unity_assets/{guid}/{target}/{version}")]
|
||||
public IActionResult GetUnityAssets(string guid, int target, int version)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
UnityAssetId = guid,
|
||||
Target = target,
|
||||
Version = version,
|
||||
Filename = $"{guid}_{target}_{version}.assetbundle", // hacky?>?? it a stub?? but ok and it will hold up for now
|
||||
Hash = (string?)null
|
||||
});
|
||||
|
||||
// ts will fail when we get rr studio or 2025 support (2025 uses more stupid shit)
|
||||
// someone test vana on 2025 tho lowkey || as of 8/25/2026, 2025 work so good soonnnnioonnn
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/roles/{playerId}/invite")]
|
||||
public async Task<IActionResult> SetRoomRole(long roomId, long playerId, [FromForm] int role)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return NotFound(new { success = false, error = "Room not found" });
|
||||
|
||||
if (room.IsDorm)
|
||||
return RoomError("Rooms.UserCannotBeInvitedInsideDorm", "You do not have permission to change roles inside of your Dorm Room.");
|
||||
|
||||
room.Roles ??= new List<RoomDBClasses.Roles>();
|
||||
|
||||
var newRole = (RoomDBClasses.Role)role;
|
||||
|
||||
var callerEntry = room.Roles.FirstOrDefault(r => r.AccountId == account.Value);
|
||||
bool isOwner = room.CreatorAccountId == account.Value;
|
||||
bool isCoOwner = callerEntry != null && (callerEntry.Role == Role.CoOwner || callerEntry.Role == Role.TemporaryCoOwner);
|
||||
|
||||
if (!isOwner && !isCoOwner)
|
||||
return StatusCode(403);
|
||||
|
||||
var targetEntry = room.Roles.FirstOrDefault(r => r.AccountId == playerId);
|
||||
bool targetIsOwner = room.CreatorAccountId == playerId;
|
||||
|
||||
if (targetIsOwner)
|
||||
return StatusCode(403);
|
||||
|
||||
var targetCurrentRole = targetEntry?.Role ?? Role.None;
|
||||
var targetCurrentInvited = targetEntry?.InvitedRole ?? Role.None;
|
||||
|
||||
bool targetIsCoOwnerTier = targetCurrentRole == Role.CoOwner || targetCurrentRole == Role.TemporaryCoOwner
|
||||
|| targetCurrentInvited == Role.CoOwner || targetCurrentInvited == Role.TemporaryCoOwner;
|
||||
|
||||
if (targetIsCoOwnerTier && !isOwner)
|
||||
return StatusCode(403);
|
||||
|
||||
if (newRole == Role.None || role == 0)
|
||||
{
|
||||
if (targetEntry != null)
|
||||
{
|
||||
bool wasCoOwnerTier = targetEntry.Role == Role.CoOwner || targetEntry.Role == Role.TemporaryCoOwner
|
||||
|| targetEntry.InvitedRole == Role.CoOwner || targetEntry.InvitedRole == Role.TemporaryCoOwner;
|
||||
|
||||
room.Roles.Remove(targetEntry);
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
if (wasCoOwnerTier)
|
||||
{
|
||||
var removePayload = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = account,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = WebsocketEvents.WebsocketMessageType.RoomCoOwnerRemoved,
|
||||
Data = (string?)null,
|
||||
RoomId = roomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await NotificationsController.SendToPlayer(playerId, removePayload);
|
||||
}
|
||||
}
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, updatedRoom);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
bool isCoOwnerInvite = newRole == Role.CoOwner || newRole == Role.TemporaryCoOwner;
|
||||
|
||||
if (isCoOwnerInvite)
|
||||
{
|
||||
if (!isOwner)
|
||||
return StatusCode(403);
|
||||
|
||||
if (targetEntry != null)
|
||||
{
|
||||
targetEntry.Role = Role.None;
|
||||
targetEntry.InvitedRole = newRole;
|
||||
targetEntry.LastChangedByAccountId = account;
|
||||
}
|
||||
else
|
||||
{
|
||||
room.Roles.Add(new RoomDBClasses.Roles
|
||||
{
|
||||
AccountId = playerId,
|
||||
Role = Role.None,
|
||||
InvitedRole = newRole,
|
||||
LastChangedByAccountId = account
|
||||
});
|
||||
}
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = account,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.RoomCoOwnerInvited,
|
||||
Data = (string?)null,
|
||||
RoomId = roomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await NotificationsController.SendToPlayer(playerId, json);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (targetEntry != null)
|
||||
{
|
||||
targetEntry.Role = newRole;
|
||||
targetEntry.InvitedRole = Role.None;
|
||||
targetEntry.LastChangedByAccountId = account;
|
||||
}
|
||||
else
|
||||
{
|
||||
room.Roles.Add(new RoomDBClasses.Roles
|
||||
{
|
||||
AccountId = playerId,
|
||||
Role = newRole,
|
||||
InvitedRole = Role.None,
|
||||
LastChangedByAccountId = account
|
||||
});
|
||||
}
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
}
|
||||
|
||||
await NotiController.SendRoomUpdate(playerId, RoomDB.GetRoom(roomId));
|
||||
await NotiController.SendRoomUpdate((long)account, RoomDB.GetRoom(roomId));
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/roles/{playerId}")] // made a temp workaround i think the response is correct?
|
||||
public IActionResult GetRoomRole(long roomId, long playerId)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return NotFound(new { success = false, error = "Room not found" });
|
||||
|
||||
var entry = room.Roles?.FirstOrDefault(r => r.AccountId == playerId);
|
||||
|
||||
if (entry == null)
|
||||
return Ok(new { AccountId = playerId, Role = Role.None, InvitedRole = Role.None });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
entry.AccountId,
|
||||
entry.Role,
|
||||
entry.InvitedRole,
|
||||
entry.LastChangedByAccountId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/roles")]
|
||||
public IActionResult GetRoomRoles(long roomId)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return NotFound(new { success = false, error = "Room not found" });
|
||||
|
||||
var roles = (room.Roles ?? new List<RoomDBClasses.Roles>())
|
||||
.Select(r => new
|
||||
{
|
||||
r.AccountId,
|
||||
r.Role,
|
||||
r.InvitedRole,
|
||||
r.LastChangedByAccountId
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(roles);
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/roles/{playerId}")]
|
||||
public async Task<IActionResult> RemoveRoomRole(long roomId, long playerId, [FromForm] int role)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return NotFound(new { success = false, error = "Room not found" });
|
||||
|
||||
room.Roles ??= new List<RoomDBClasses.Roles>();
|
||||
|
||||
var existing = room.Roles.FirstOrDefault(r => r.AccountId == playerId);
|
||||
var existingTwo = room.Roles.FirstOrDefault(r => r.AccountId == account.Value);
|
||||
|
||||
if (account == playerId)
|
||||
{
|
||||
if (existing == null)
|
||||
return Ok(new { success = true });
|
||||
|
||||
var incomingRole = (RoomDBClasses.Role)role;
|
||||
|
||||
if (incomingRole == Role.None)
|
||||
{
|
||||
bool wasCoOwnerSelf = existing.Role == Role.CoOwner || existing.Role == Role.TemporaryCoOwner;
|
||||
room.Roles.Remove(existing);
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
if (wasCoOwnerSelf)
|
||||
{
|
||||
var wsPayload = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = account,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = WebsocketEvents.WebsocketMessageType.RoomCoOwnerRemoved,
|
||||
Data = (string?)null,
|
||||
RoomId = roomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await NotificationsController.SendToPlayer(account.Value, wsPayload);
|
||||
}
|
||||
|
||||
var allPids = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
var updatedRoomSelf = RoomDB.GetRoom(roomId);
|
||||
foreach (var pid in allPids)
|
||||
await NotiController.SendRoomUpdate(pid, updatedRoomSelf);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
if (existing.InvitedRole == Role.CoOwner)
|
||||
{
|
||||
if (incomingRole == Role.CoOwner)
|
||||
{
|
||||
existing.Role = Role.CoOwner;
|
||||
existing.InvitedRole = Role.None;
|
||||
existing.LastChangedByAccountId = account;
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.InvitedRole = Role.None;
|
||||
existing.LastChangedByAccountId = account;
|
||||
|
||||
if (existing.Role == Role.None)
|
||||
room.Roles.Remove(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var newRole = (RoomDBClasses.Role)role;
|
||||
|
||||
bool wasCoOwner = existing != null &&
|
||||
(existing.Role == Role.CoOwner || existing.Role == Role.TemporaryCoOwner);
|
||||
bool beingDemoted = (int)newRole < (int)Role.CoOwner;
|
||||
|
||||
if (newRole == Role.None)
|
||||
{
|
||||
if (existing != null)
|
||||
room.Roles.Remove(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existingTwo != null && existingTwo.Role < newRole)
|
||||
{
|
||||
APIController.SendNonHileWebhook(playerId, "not a hile, but someone tried to claim " + newRole + " role in room " + roomId);
|
||||
return StatusCode(403);
|
||||
}
|
||||
if (existing != null)
|
||||
{
|
||||
existing.Role = newRole;
|
||||
existing.InvitedRole = Role.None;
|
||||
existing.LastChangedByAccountId = account;
|
||||
}
|
||||
else
|
||||
{
|
||||
room.Roles.Add(new RoomDBClasses.Roles
|
||||
{
|
||||
AccountId = playerId,
|
||||
Role = newRole,
|
||||
InvitedRole = Role.None,
|
||||
LastChangedByAccountId = account
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (wasCoOwner && beingDemoted)
|
||||
{
|
||||
var wsPayload = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = account,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = WebsocketEvents.WebsocketMessageType.RoomCoOwnerRemoved,
|
||||
Data = (string?)null,
|
||||
RoomId = roomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await NotificationsController.SendToPlayer(playerId, wsPayload);
|
||||
}
|
||||
}
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var allPlayerIds = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
foreach (var pid in allPlayerIds)
|
||||
await NotiController.SendRoomUpdate(pid, updatedRoom);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpGet("rooms/bulk")]
|
||||
public async Task<IActionResult> GetRoomsBulk([FromQuery] List<string> name, [FromQuery] List<long> id)
|
||||
{
|
||||
|
||||
var userAgent = Request.Headers.UserAgent.ToString();
|
||||
if (!userAgent.Contains("BestHTTP", StringComparison.OrdinalIgnoreCase) && !Request.Headers.Cookie.ToString().Contains("cf_clearance"))
|
||||
return StatusCode(404);
|
||||
|
||||
var roomsByName = RoomDB.GetRoomsByNames(name);
|
||||
var roomsById = RoomDB.GetRoomsByIds(id);
|
||||
var combinedRooms = roomsByName.Concat(roomsById).DistinctBy(r => r.RoomId).ToList();
|
||||
return Ok(combinedRooms);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/hot")]
|
||||
public async Task<IActionResult> HotRooms([FromQuery] string tag = "", [FromQuery] int skip = 0, [FromQuery] int take = 30)
|
||||
{
|
||||
string? passingTag = string.IsNullOrWhiteSpace(tag) ? null : tag;
|
||||
var (results, total) = RoomDB.GetHotRooms(passingTag, skip, take);
|
||||
return Ok(new
|
||||
{
|
||||
Results = results ?? new List<Room>(),
|
||||
TotalResults = total
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("rooms/search")]
|
||||
public async Task<IActionResult> SearchRooms([FromQuery] string query = "", [FromQuery] int skip = 0, [FromQuery] int take = 30)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
var (hotResults, hotTotal) = RoomDB.GetHotRooms(null, skip, take);
|
||||
return Ok(new
|
||||
{
|
||||
Results = hotResults ?? new List<Room>(),
|
||||
TotalResults = hotTotal
|
||||
});
|
||||
}
|
||||
|
||||
var (searchList, searchTotal) = RoomDB.Search(query, skip: skip, take: take);
|
||||
return Ok(new
|
||||
{
|
||||
Results = searchList ?? new List<Room>(),
|
||||
TotalResults = searchTotal
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("rooms/base")]
|
||||
public async Task<IActionResult> GetBaseRooms()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var rooms = RoomDB.GetBaseRooms();
|
||||
|
||||
foreach(var room in rooms)
|
||||
{
|
||||
room.Accessibility = RoomAccessibility.Public;
|
||||
}
|
||||
|
||||
return Ok(rooms);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/contributedby/me")]
|
||||
[HttpGet("rooms/createdby/me")]
|
||||
[HttpGet("rooms/ownedby/me")]
|
||||
public async Task<IActionResult> GetCreatedByMe(
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 9999)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var results = RoomDB.Rooms.FindAll()
|
||||
.Where(r =>
|
||||
!IsBaseRoom(r) &&
|
||||
(
|
||||
r.CreatorAccountId == (long)id ||
|
||||
(r.Roles?.Any(x =>
|
||||
x.AccountId == (long)id &&
|
||||
(x.Role == Role.CoOwner ||
|
||||
x.Role == Role.Creator || x.Role == Role.TemporaryCoOwner )) ?? false)
|
||||
))
|
||||
.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToList();
|
||||
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/ownedby/{accountId}")]
|
||||
public async Task<IActionResult> GetRoomsOwnedBy(long accountId)
|
||||
{
|
||||
return Ok(RoomDB.GetRoomsByCreator(accountId).Where(r => !IsBaseRoom(r)).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("rooms/visitedby/me")]
|
||||
public async Task<IActionResult> GetVisitedByMe([FromQuery] int skip = 0, [FromQuery] int take = 9)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var visits = PlayerDB.GetPlayerRoomVisits((long)id);
|
||||
|
||||
var results = visits
|
||||
.OrderByDescending(rv => rv.LastVisitedAt)
|
||||
.Skip(skip).Take(take)
|
||||
.Select(rv => RoomDB.GetRoom(rv.RoomId))
|
||||
.Where(r => r != null && !IsBaseRoom(r))
|
||||
.ToList();
|
||||
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/visitedby/{playerId}")]
|
||||
public async Task<IActionResult> GetVisitedByOther(long playerId, [FromQuery] int skip = 0, [FromQuery] int take = 10)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var visits = PlayerDB.GetPlayerRoomVisits(playerId);
|
||||
|
||||
var results = visits
|
||||
.OrderByDescending(rv => rv.LastVisitedAt)
|
||||
.Skip(skip).Take(take)
|
||||
.Select(rv => RoomDB.GetRoom(rv.RoomId))
|
||||
.Where(r => r != null && !IsBaseRoom(r))
|
||||
.ToList();
|
||||
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/favoritedby/me")]
|
||||
public async Task<IActionResult> GetFavoritedByMe([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var col = RoomDB.RoomDBFile.GetCollection("Rooms");
|
||||
|
||||
var rooms = new List<Room>();
|
||||
|
||||
foreach (var doc in col.FindAll())
|
||||
{
|
||||
try
|
||||
{
|
||||
var room = BsonMapper.Global.Deserialize<Room>(doc);
|
||||
|
||||
if (room == null)
|
||||
continue;
|
||||
|
||||
if (IsBaseRoom(room))
|
||||
continue;
|
||||
|
||||
if (!PlayerDB.HasFavoritedRoom((long)id, room.RoomId))
|
||||
continue;
|
||||
|
||||
rooms.Add(room);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[GetFavoritedByMe] Skipped corrupt room {doc["_id"]}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var results = rooms
|
||||
.OrderByDescending(r => r.Stats?.FavoriteCount ?? 0)
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToList();
|
||||
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/dormroom/me")]
|
||||
public async Task<IActionResult> GetMyDormRoom()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var player = PlayerDB.GetCurrentPlayer((long)id);
|
||||
if (player == null) return NotFound("");
|
||||
|
||||
return Ok(player.Player?.PlayerExtra?.DormRoomId ?? 0);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/interactionby/me")]
|
||||
public async Task<IActionResult> GetInteractionByMe(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Cheered = PlayerDB.HasCheeredRoom((long)id, roomId),
|
||||
Favorited = PlayerDB.HasFavoritedRoom((long)id, roomId),
|
||||
LastVisitedAt = PlayerDB.GetRoomLastVisitedAt((long)id, roomId) ?? DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/interactionby/me/favorite")]
|
||||
public async Task<IActionResult> FavoriteRoom(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (!PlayerDB.HasFavoritedRoom((long)id, roomId))
|
||||
{
|
||||
PlayerDB.AddFavoriteRoom((long)id, roomId);
|
||||
RoomDB.IncrementRoomFavoriteCount(roomId);
|
||||
}
|
||||
|
||||
await NotiController.SendRoomUpdate((long)id, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = new { Favorited = true } });
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId}/interactionby/me/favorite")]
|
||||
public async Task<IActionResult> UnfavoriteRoom(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (PlayerDB.HasFavoritedRoom((long)id, roomId))
|
||||
{
|
||||
PlayerDB.RemoveFavoriteRoom((long)id, roomId);
|
||||
RoomDB.DecrementRoomFavoriteCount(roomId);
|
||||
}
|
||||
|
||||
await NotiController.SendRoomUpdate((long)id, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = new { Favorited = false } });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/interactionby/me/cheer")]
|
||||
public async Task<IActionResult> CheerRoom(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (!PlayerDB.HasCheeredRoom((long)id, roomId))
|
||||
{
|
||||
PlayerDB.AddCheerRoom((long)id, roomId);
|
||||
RoomDB.IncrementRoomCheerCount(roomId);
|
||||
}
|
||||
|
||||
await NotiController.SendRoomUpdate((long)id, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = new { Cheered = true } });
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId}/interactionby/me/cheer")]
|
||||
public async Task<IActionResult> UncheerRoom(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (PlayerDB.HasCheeredRoom((long)id, roomId))
|
||||
{
|
||||
PlayerDB.RemoveCheerRoom((long)id, roomId);
|
||||
RoomDB.DecrementRoomCheerCount(roomId);
|
||||
}
|
||||
|
||||
await NotiController.SendRoomUpdate((long)id, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = new { Cheered = false } });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/subrooms/{subRoomId}/permissions")]
|
||||
public async Task<IActionResult> SetSubroomPermissions(long roomId, long subRoomId, [FromBody] List<RoomDBClasses.PermissionEntry> permissions)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return NotFound(new { success = false, error = "Subroom was not found", error_id = "Rooms.Subroom.NotFound" });
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(roomId, (long)playerId))
|
||||
return Forbid();
|
||||
var subroom = room.SubRooms?.FirstOrDefault(x => x.SubRoomId == subRoomId);
|
||||
if (subroom == null)
|
||||
return NotFound(new { success = false, error = "Subroom was not found", error_id = "Rooms.Subroom.NotFound" });
|
||||
|
||||
var storedEntries = permissions.Select(p => new PhotonAccessTokenDBClasses.StoredPermissionEntry
|
||||
{
|
||||
Permission = p.Permission,
|
||||
Role = (int)p.Role,
|
||||
Type = p.Type,
|
||||
Override = p.Override,
|
||||
Value = p.Value
|
||||
}).ToList();
|
||||
|
||||
var updatedSet = PhotonAccessTokenDB.SetPermissions(roomId, subRoomId, storedEntries);
|
||||
|
||||
var fullPermissions = updatedSet.Permissions.Select(p => new
|
||||
{
|
||||
Override = p.Override,
|
||||
Permission = p.Permission,
|
||||
Role = p.Role,
|
||||
Type = p.Type,
|
||||
Value = p.Value
|
||||
}).ToList();
|
||||
|
||||
var callerHeartbeat = PlayerDB.GetPlayerHeartbeat((long)playerId);
|
||||
var instanceId = callerHeartbeat?.roomInstance?.roomInstanceId;
|
||||
var playersInInstance = instanceId != null
|
||||
? PlayerDB.Players.FindAll()
|
||||
.Where(p => p.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomInstanceId == instanceId)
|
||||
.Select(p => p.PlayerId)
|
||||
.ToList()
|
||||
: new List<long> { (long)playerId };
|
||||
|
||||
foreach (var pid in playersInInstance)
|
||||
{
|
||||
PlayerDB.SetPlayerPermissions(pid, permissions);
|
||||
await NotificationsController.SendEvent(pid, "PhotonAccessToken", new
|
||||
{
|
||||
RoomId = roomId,
|
||||
SubRoomId = subRoomId,
|
||||
Permissions = fullPermissions
|
||||
});
|
||||
}
|
||||
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
value = updatedRoom,
|
||||
success = true,
|
||||
error_id = (string?)null,
|
||||
error = (string?)null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/clone")]
|
||||
public async Task<IActionResult> CloneRoom(long roomId, [FromForm] CloneRoomRequest body)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
Console.WriteLine($"clone {id}");
|
||||
var query = RoomDB.Rooms.FindAll()
|
||||
.Where(r =>
|
||||
!IsBaseRoom(r) &&
|
||||
(
|
||||
r.CreatorAccountId == (long)id ||
|
||||
(r.Roles?.Any(x =>
|
||||
x.AccountId == (long)id &&
|
||||
(x.Role == Role.CoOwner ||
|
||||
x.Role == Role.Creator)) ?? false)
|
||||
));
|
||||
var roomCount = query.Count();
|
||||
if (roomCount > 50)
|
||||
return RoomError("Rooms.AccountOwnsTooManyRooms", "Account cannot create or co-own any more rooms");
|
||||
//if ()
|
||||
// return RoomError("Rooms.AccountIsNotAboveAge", "Junior accounts are prohibited from creating rooms.");
|
||||
if (body == null || string.IsNullOrWhiteSpace(body.Name))
|
||||
return RoomError("Rooms.MissingName", "You must enter a name for your room.");
|
||||
if (body.Name.Length < 3 || body.Name.Length > 128)
|
||||
return RoomError("Rooms.TooShortOrLong", "Room Name must be within 3 - 128 Characters.");
|
||||
if (await ContainsProhibitedText(body.Name))
|
||||
return RoomError("Rooms.ContainsProhibitedText", "Name contains prohibited text!");
|
||||
long playerId = (long)id;
|
||||
var now = DateTime.UtcNow;
|
||||
var twoMinutesAgo = now.AddMinutes(-2);
|
||||
var cloneHistory = _cloneRateLimits.GetOrAdd(playerId, _ => new List<DateTime>());
|
||||
lock (cloneHistory)
|
||||
{
|
||||
cloneHistory.RemoveAll(timestamp => timestamp < twoMinutesAgo);
|
||||
if (cloneHistory.Count() >= 10)
|
||||
{
|
||||
return RoomError("Rooms.TooManyClonesRateLimited", "An error occured. Wait a few minutes and try again.");
|
||||
}
|
||||
cloneHistory.Add(now);
|
||||
}
|
||||
var originalRoom = RoomDB.GetRoom(roomId);
|
||||
if (originalRoom == null)
|
||||
return RoomError("Rooms.CannotClone", "The room you are trying to clone does not exist.");
|
||||
bool isBaseRoom = originalRoom.Tags != null &&
|
||||
originalRoom.Tags.Any(t => string.Equals(t.Tag, "base", StringComparison.OrdinalIgnoreCase));
|
||||
if (RoomDB.GetRoomByName(body.Name) != null)
|
||||
return RoomError("Rooms.DuplicateName", "A room with that name already exists!");
|
||||
if (!isBaseRoom)
|
||||
{
|
||||
var playerRoomCount = RoomDB.GetRoomsByCreator(playerId).Count;
|
||||
if (playerRoomCount >= 101)
|
||||
return RoomError("Rooms.AccountOwnsTooManyRooms", "Account cannot create or co-own any more rooms");
|
||||
}
|
||||
var newId = RoomDB.CloneRoom(roomId, body.Name, playerId);
|
||||
if (newId == null)
|
||||
return RoomError("Rooms.CannotClone", "You can't clone this room!");
|
||||
var newRoom = RoomDB.GetRoom(newId.Value);
|
||||
var playerRecord = PlayerDB.Players.FindById(playerId);
|
||||
bool isDeveloper = playerRecord?.PlayerRoles?.Contains(PlayerDBClasses.PlayerRoles.Developer) ?? false;
|
||||
if (isDeveloper && newRoom != null)
|
||||
{
|
||||
newRoom.IsDeveloperOwned = true;
|
||||
RoomDB.Rooms.Update(newRoom);
|
||||
}
|
||||
if (AuthStuff.GetAndEnsureAppVersion(Request) == "20250724" && newRoom != null)
|
||||
{
|
||||
if (!newRoom.Tags.Any(x => x.Tag.Equals("2025", StringComparison.OrdinalIgnoreCase)))
|
||||
newRoom.Tags.Add(new Tags { Tag = "2025", Type = TagType.AGOnly });
|
||||
RoomDB.Rooms.Update(newRoom);
|
||||
}
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
return Ok(new { success = true, error = "", value = newRoom });
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms/{subRoomId}/clone")]
|
||||
public async Task<IActionResult> CloneSubroom(long roomId, long subRoomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id))
|
||||
return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
if (RoomDB.GetPlayerDormRoom((long)id)?.RoomId == roomId)
|
||||
return RoomError("Rooms.Subroom.CloneDormRoom", "You cannot clone subrooms from your dorm room!");
|
||||
var cloned = RoomDB.CloneSubroom(roomId, subRoomId, (long)id);
|
||||
if (cloned == null)
|
||||
return RoomError("Rooms.SubRoom.DoesntExist", "This subroom does not exist!");
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
var allPlayers = PlayerDB.Players
|
||||
.FindAll()
|
||||
.Select(p => p.PlayerId)
|
||||
.ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, updatedRoom);
|
||||
return Ok(new
|
||||
{
|
||||
value = updatedRoom,
|
||||
success = true,
|
||||
error_id = (string?)null,
|
||||
error = (string?)null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms/{subRoomId}/move")]
|
||||
public IActionResult MoveSubRoom(
|
||||
long roomId,
|
||||
long subRoomId,
|
||||
[FromForm] long newRoomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
if (RoomDB.GetPlayerDormRoom((long)id)?.RoomId == roomId)
|
||||
return RoomError("Rooms.Subroom.MoveDormRoom", "You cannot move your dorm room!");
|
||||
var movedRoom = RoomDB.MoveSubRoomToRoom(
|
||||
roomId,
|
||||
subRoomId,
|
||||
newRoomId,
|
||||
(long)id);
|
||||
if (movedRoom == null)
|
||||
return RoomError("Subroom.MoveFailed", "Failed to move subroom.");
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error = "",
|
||||
value = movedRoom
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId}")]
|
||||
public async Task<IActionResult> DeleteRoom(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
|
||||
if (!CanEdit(room, (long)id))
|
||||
return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
RoomDB.DeleteRoom(roomId);
|
||||
return Ok(new { success = true, error = "", value = (object?)null });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/subrooms/{subRoomId}/accessibility")]
|
||||
public async Task<IActionResult> SetSubRoomAccessibility(long roomId, long subRoomId, [FromForm] string accessibility)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
var sub = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
if (sub == null) return RoomError("Rooms.SubRoom.DoesntExist", "This subroom does not exist!");
|
||||
|
||||
sub.Accessibility = accessibility?.ToLower() switch
|
||||
{
|
||||
"public" => RoomAccessibility.Public,
|
||||
"private" => RoomAccessibility.Private,
|
||||
"unlisted" => RoomAccessibility.Unlisted,
|
||||
_ => RoomAccessibility.Private
|
||||
};
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/name")]
|
||||
public async Task<IActionResult> SetRoomName(long roomId, [FromForm] string name)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
if (room.IsDorm) return RoomError("Rooms.Dorm", "You cannot edit your dorm room's name!");
|
||||
if (string.IsNullOrWhiteSpace(name)) return RoomError("Rooms.InvalidName", "Name cannot be empty.");
|
||||
if (name.Length < 3 || name.Length > 128) return RoomError("Rooms.TooShortOrLong", "Room Name must be within 3 - 128 Characters.");
|
||||
if (name.Contains(" ")) return RoomError("Rooms.NoSpaces", "Your room name must not contain spaces!");
|
||||
if (await ContainsProhibitedText(name))
|
||||
return RoomError("Rooms.ContainsProhibitedText", "Name contains prohibited text!");
|
||||
|
||||
var updated = RoomDB.SetRoomName(roomId, name);
|
||||
if (updated == null)
|
||||
return RoomError("Rooms.DuplicateName", "A room with the same name already exists!");
|
||||
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, updatedRoom);
|
||||
|
||||
return Ok(new { success = true, error = "", value = updatedRoom });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/description")]
|
||||
public async Task<IActionResult> SetRoomDescription(long roomId, [FromForm] string description)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
if (!string.IsNullOrWhiteSpace(description) && await ContainsProhibitedText(description))
|
||||
return RoomError("Rooms.ContainsProhibitedText", "Description must not contain prohibited text.");
|
||||
|
||||
RoomDB.SetRoomDescription(roomId, description ?? "");
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/image")]
|
||||
public async Task<IActionResult> SetRoomImage(long roomId, [FromForm] string imageName)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
RoomDB.SetRoomImageName(roomId, imageName);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/accessibility")]
|
||||
public async Task<IActionResult> SetRoomAccessibility(long roomId, [FromForm] int accessibility)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (!Enum.IsDefined(typeof(RoomAccessibility), accessibility))
|
||||
return BadRequest();
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
if ((RoomAccessibility)accessibility == RoomAccessibility.Public && room.ImageName == "DefaultRoomImage.png")
|
||||
return RoomError("Rooms.MissingRoomThumbnail", "Cannot publish room: Missing room photo"); // wowe just like ingame
|
||||
|
||||
RoomDB.SetRoomAccessibility(roomId, (RoomAccessibility)accessibility);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/cloning")]
|
||||
public async Task<IActionResult> SetRoomCloning(long roomId, [FromForm] bool cloningAllowed)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (room.IsDorm) return RoomError("Rooms.DormNotAllowed", "You cannot edit that in this dorm.");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
RoomDB.SetRoomCloning(roomId, cloningAllowed);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/restrictions")]
|
||||
public async Task<IActionResult> SetRoomRestrictions(
|
||||
long roomId,
|
||||
[FromForm] bool supportsScreens,
|
||||
[FromForm] bool supportsWalkVR,
|
||||
[FromForm] bool supportsTeleportVR,
|
||||
[FromForm] bool supportsJuniors)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (room.IsDorm) return RoomError("Rooms.DormNotAllowed", "You cannot edit that in dorms.");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
RoomDB.SetRoomRestrictions(roomId, supportsScreens, supportsWalkVR, supportsTeleportVR, supportsJuniors);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/loadscreen")]
|
||||
public async Task<IActionResult> SetRoomLoadscreen(
|
||||
long roomId,
|
||||
[FromForm] string? imageName,
|
||||
[FromForm] string? title,
|
||||
[FromForm] string? subtitle)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
if (!string.IsNullOrEmpty(title) && title.Length > 24)
|
||||
return RoomError("Room.LoadScreen.Title", "Title cannot exceed 24 characters!");
|
||||
if (!string.IsNullOrEmpty(subtitle) && subtitle.Length > 150)
|
||||
return RoomError("Room.LoadScreen.Subtitle", "Subtitle cannot exceed 150 characters!");
|
||||
|
||||
RoomDB.SetRoomLoadscreen(roomId, imageName, title, subtitle);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/warning")]
|
||||
public async Task<IActionResult> SetRoomWarning(long roomId, [FromForm] WarningMaskType warningMask, [FromForm] string? customWarning = null)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
RoomDB.SetRoomWarningMask(roomId, warningMask);
|
||||
|
||||
if (warningMask == (WarningMaskType)29)
|
||||
RoomDB.SetRoomCustomWarning(roomId, null);
|
||||
|
||||
else if (customWarning != null)
|
||||
RoomDB.SetRoomCustomWarning(roomId, customWarning);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/tags")]
|
||||
public async Task<IActionResult> SetRoomTags(long roomId)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null) return Unauthorized("");
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null)
|
||||
return Vanadium.Utils.Utils.ErrorRoom("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!RoomDB.UserCanEditRoom(roomId, account.Value))
|
||||
return Vanadium.Utils.Utils.ErrorRoom("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
var restrictedTags = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"rro", "recroomoriginal", "featured",
|
||||
"janrrmonthly", "febrrmonthly", "marchrrmonthly",
|
||||
"aprilrrmonthly", "mayrrmonthly", "junerrmonthly",
|
||||
"julyrrmonthly", "augrrmonthly", "seprrmonthly",
|
||||
"octrrmonthly", "novrrmonthly", "decrrmonthly"
|
||||
};
|
||||
|
||||
var newTags = new List<RoomDBClasses.Tags>();
|
||||
var ct = Request.ContentType ?? "";
|
||||
bool hasForm = ct.Contains("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)
|
||||
|| ct.Contains("multipart/form-data", StringComparison.OrdinalIgnoreCase);
|
||||
if (hasForm)
|
||||
{
|
||||
var autoTagKeys = Request.Form.Keys
|
||||
.Where(k => k.Equals("autotag", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
foreach (var key in autoTagKeys)
|
||||
{
|
||||
foreach (var val in Request.Form[key])
|
||||
{
|
||||
var t = (val ?? "").Trim().ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(t))
|
||||
{
|
||||
if (restrictedTags.Contains(t))
|
||||
return Vanadium.Utils.Utils.ErrorRoom("Rooms.InvalidTags", "You cannot use this room tag.");
|
||||
newTags.Add(new RoomDBClasses.Tags { Tag = t, Type = RoomDBClasses.TagType.Auto });
|
||||
}
|
||||
}
|
||||
}
|
||||
var tagKeys = Request.Form.Keys
|
||||
.Where(k => k.Equals("tag", StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
foreach (var key in tagKeys)
|
||||
{
|
||||
foreach (var val in Request.Form[key])
|
||||
{
|
||||
var t = (val ?? "").Trim().ToLowerInvariant();
|
||||
if (!string.IsNullOrWhiteSpace(t))
|
||||
{
|
||||
if (restrictedTags.Contains(t))
|
||||
return Vanadium.Utils.Utils.ErrorRoom("Rooms.InvalidTags", "Permission denied.");
|
||||
newTags.Add(new RoomDBClasses.Tags { Tag = t, Type = RoomDBClasses.TagType.General });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
room.Tags = newTags;
|
||||
RoomDB.Rooms.Update(room);
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
await NotiController.SendToAll(Newtonsoft.Json.JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiController.EventTypes.RoomUpdate,
|
||||
Msg = updatedRoom
|
||||
}));
|
||||
return Ok(new { success = true, error = "", value = updatedRoom });
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms/{subRoomId}/data")]
|
||||
public async Task<IActionResult> PostSubRoomData(long roomId, long subRoomId, [FromBody] RoomDataRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
if (room == null)
|
||||
return Ok(new { success = false, error_id = "Rooms.DoesntExist", error = "Room not found", value = (object?)null });
|
||||
|
||||
if (!CanEdit(room, (long)playerId))
|
||||
return Ok(new { success = false, error_id = "Rooms.PermissionDenied", error = "You are not the owner of this room!", value = (object?)null });
|
||||
|
||||
if (AuthStuff.GetAndEnsureAppVersion(Request) == "20250724" && !room.Tags.Any(x => x.Tag.Equals("2025", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
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 = "You can't save 2023 rooms on 2025. Clone the room or go back to 2023.",
|
||||
RoomId = 1
|
||||
}
|
||||
});
|
||||
await NotificationsController.SendToPlayer((long)playerId, json);
|
||||
return Ok(new { success = false, error_id = "Rooms.UpdateRequired", error = "You can't save 2023 rooms on a 2025 version!", value = (object?)null });
|
||||
}
|
||||
|
||||
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
|
||||
if (subRoom == null)
|
||||
return Ok(new { success = false, error_id = "Rooms.SubRoom.DoesntExist", error = "Subroom not found", value = (object?)null });
|
||||
|
||||
string dataBlob = request.SubRoomData?.Filename ?? "";
|
||||
string dataBlobHash = request.SubRoomData?.Hash ?? "";
|
||||
string description = request.Description ?? "";
|
||||
|
||||
int platform = 0;
|
||||
int deviceClass = 5;
|
||||
|
||||
var authHeader = Request.Headers["Authorization"].FirstOrDefault();
|
||||
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
try
|
||||
{
|
||||
var tokenStr = authHeader
|
||||
.Replace("Bearer ", "", StringComparison.OrdinalIgnoreCase)
|
||||
.Trim();
|
||||
|
||||
var handler = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler();
|
||||
var jwt = handler.ReadJwtToken(tokenStr);
|
||||
|
||||
var platformClaim = jwt.Claims.FirstOrDefault(c => c.Type == "platform")?.Value;
|
||||
if (platformClaim != null)
|
||||
platform = int.Parse(platformClaim);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(request.RoomData?.Filename))
|
||||
room.DataBlob = request.RoomData.Filename;
|
||||
|
||||
room.PersistenceVersion = request.PersistenceVersion;
|
||||
|
||||
var save = new currentSave
|
||||
{
|
||||
SubRoomDataSaveId = RoomDB.GetNextSubRoomSaveId(),
|
||||
RoomId = roomId,
|
||||
SubRoomId = subRoomId,
|
||||
DataBlob = dataBlob,
|
||||
DataBlobHash = dataBlobHash,
|
||||
PersistenceVersion = request.PersistenceVersion,
|
||||
UgcSubVersion = request.PersistenceVersion,
|
||||
OMVersion = 0,
|
||||
SavedByAccountId = (long)playerId,
|
||||
SavedOnPlatform = platform,
|
||||
SavedOnDeviceClass = deviceClass,
|
||||
Description = description,
|
||||
Tags = new List<string>(),
|
||||
ModerationState = 0,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
RoomDB.SubRoomSaves.Insert(save);
|
||||
|
||||
//subRoom.DataBlob = dataBlob;
|
||||
subRoom.SavedByAccountId = (long)playerId;
|
||||
|
||||
if (request.AutoPublish && !string.IsNullOrEmpty(dataBlob))
|
||||
{
|
||||
subRoom.DataBlob = dataBlob;
|
||||
subRoom.CurrentSave = save;
|
||||
}
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
|
||||
var subRoomsProjected = updatedRoom.SubRooms?.Select(sub => new
|
||||
{
|
||||
sub.SubRoomId,
|
||||
sub.RoomId,
|
||||
CreatorAccountId = sub.CreatorAccountId,
|
||||
sub.UnitySceneId,
|
||||
sub.Name,
|
||||
CurrentSave = (sub.CurrentSave != null ? new
|
||||
{
|
||||
sub.CurrentSave.SubRoomDataSaveId,
|
||||
sub.CurrentSave.SubRoomId,
|
||||
sub.CurrentSave.RoomId,
|
||||
DataBlob = sub.CurrentSave.DataBlob,
|
||||
sub.CurrentSave.SavedByAccountId,
|
||||
sub.CurrentSave.SavedOnPlatform,
|
||||
sub.CurrentSave.SavedOnDeviceClass,
|
||||
sub.CurrentSave.Description,
|
||||
sub.CurrentSave.CreatedAt,
|
||||
sub.CurrentSave.UnityAssetId
|
||||
} : RoomDB.SubRoomSaves
|
||||
.Find(x => x.RoomId == room.RoomId && x.SubRoomId == sub.SubRoomId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Select(save => new
|
||||
{
|
||||
save.SubRoomDataSaveId,
|
||||
save.SubRoomId,
|
||||
save.RoomId,
|
||||
DataBlob = save.DataBlob,
|
||||
save.SavedByAccountId,
|
||||
save.SavedOnPlatform,
|
||||
save.SavedOnDeviceClass,
|
||||
save.Description,
|
||||
save.CreatedAt,
|
||||
save.UnityAssetId
|
||||
})
|
||||
.FirstOrDefault()),
|
||||
sub.LastModeratedSaveModerationState,
|
||||
sub.IsSandbox,
|
||||
sub.MaxPlayers,
|
||||
sub.Accessibility,
|
||||
sub.ShouldAutoStageSaves,
|
||||
sub.StagedSubRoomDataSaveId
|
||||
}).ToList();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
Room = new
|
||||
{
|
||||
updatedRoom.RoomId,
|
||||
updatedRoom.Name,
|
||||
updatedRoom.Description,
|
||||
updatedRoom.ImageName,
|
||||
updatedRoom.WarningMask,
|
||||
updatedRoom.CustomWarning,
|
||||
updatedRoom.CreatorAccountId,
|
||||
updatedRoom.State,
|
||||
updatedRoom.Accessibility,
|
||||
PublishState = 0,
|
||||
updatedRoom.SupportsLevelVoting,
|
||||
updatedRoom.IsRRO,
|
||||
updatedRoom.IsRecRoomApproved,
|
||||
updatedRoom.ExcludeFromLists,
|
||||
updatedRoom.ExcludeFromSearch,
|
||||
updatedRoom.SupportsScreens,
|
||||
updatedRoom.SupportsWalkVR,
|
||||
updatedRoom.SupportsTeleportVR,
|
||||
updatedRoom.SupportsVRLow,
|
||||
updatedRoom.SupportsQuest2,
|
||||
updatedRoom.SupportsMobile,
|
||||
updatedRoom.SupportsJuniors,
|
||||
updatedRoom.MinLevel,
|
||||
updatedRoom.AgeRating,
|
||||
updatedRoom.CreatedAt,
|
||||
updatedRoom.PublishedAt,
|
||||
updatedRoom.BecameRRStudioRoomAt,
|
||||
updatedRoom.Stats,
|
||||
updatedRoom.RankingContext,
|
||||
updatedRoom.IsDorm,
|
||||
updatedRoom.IsPlacePlay,
|
||||
updatedRoom.MaxPlayerCalculationMode,
|
||||
updatedRoom.MaxPlayers,
|
||||
updatedRoom.CloningAllowed,
|
||||
updatedRoom.DisableMicAutoMute,
|
||||
updatedRoom.DisableRoomComments,
|
||||
updatedRoom.EncryptVoiceChat,
|
||||
updatedRoom.ToxmodEnabled,
|
||||
updatedRoom.LoadScreenLocked,
|
||||
updatedRoom.UgcVersion,
|
||||
updatedRoom.PersistenceVersion,
|
||||
updatedRoom.UgcSubVersion,
|
||||
updatedRoom.MinUgcSubVersion,
|
||||
updatedRoom.AutoLocalizeRoom,
|
||||
updatedRoom.LocalizationContext,
|
||||
updatedRoom.IsDeveloperOwned,
|
||||
updatedRoom.RankedEntityId,
|
||||
BoostCount = 0,
|
||||
updatedRoom.DataBlob,
|
||||
DataBlobHash = request.RoomData?.Hash,
|
||||
updatedRoom.CurrentSnapshotId,
|
||||
LatestSnapshotId = updatedRoom.CurrentSnapshotId,
|
||||
StagedSnapshotId = updatedRoom.CurrentSnapshotId,
|
||||
SubRooms = subRoomsProjected,
|
||||
updatedRoom.Roles,
|
||||
updatedRoom.IsJuniorCreated,
|
||||
updatedRoom.Tags,
|
||||
updatedRoom.PromoImages,
|
||||
updatedRoom.PromoExternalContent,
|
||||
updatedRoom.LoadScreens,
|
||||
CurrentRoomModerationState = 0,
|
||||
LastRoomModerationState = 0,
|
||||
PublishStateAvailability = new
|
||||
{
|
||||
CanSaveAsBeta = false,
|
||||
CanSaveAsUpdate = true,
|
||||
AvailableUpdateTokenCount = 3
|
||||
},
|
||||
updatedRoom.RestrictedCircuitsAllowListNames
|
||||
},
|
||||
SubRoomDataSave = new
|
||||
{
|
||||
save.SubRoomDataSaveId,
|
||||
save.SubRoomId,
|
||||
save.DataBlob,
|
||||
save.DataBlobHash,
|
||||
save.ReferencedUnityAssetIds,
|
||||
save.PersistenceVersion,
|
||||
save.OMVersion,
|
||||
save.UgcSubVersion,
|
||||
save.SavedByAccountId,
|
||||
save.SavedOnPlatform,
|
||||
save.SavedOnDeviceClass,
|
||||
save.Description,
|
||||
save.Tags,
|
||||
save.ModerationState,
|
||||
save.CreatedAt
|
||||
}
|
||||
},
|
||||
success = true,
|
||||
error_id = (string?)null,
|
||||
error = (string?)null
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = false,
|
||||
error_id = "Internal.Error",
|
||||
error = ex.Message,
|
||||
value = (object?)null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms/{subRoomId}/publish_save")]
|
||||
public async Task<IActionResult> RestoreSubRoomData(long roomId, long subRoomId, [FromForm] long subRoomDataSaveId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
if (room == null)
|
||||
return Ok(new { success = false, error_id = "Rooms.DoesntExist", error = "Room not found", value = (object?)null });
|
||||
|
||||
if (!CanEdit(room, (long)playerId))
|
||||
return Ok(new { success = false, error_id = "Rooms.PermissionDenied", error = "You are not the owner of this room!", value = (object?)null });
|
||||
|
||||
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
|
||||
if (subRoom == null)
|
||||
return Ok(new { success = false, error_id = "Rooms.SubRoom.DoesntExist", error = "Subroom not found", value = (object?)null });
|
||||
|
||||
var save = RoomDB.SubRoomSaves.Find(x => x.RoomId == (long)roomId && x.SubRoomId == (long)subRoomId && x.SubRoomDataSaveId == (long)subRoomDataSaveId).FirstOrDefault();
|
||||
|
||||
subRoom.DataBlob = save.DataBlob;
|
||||
subRoom.CurrentSave = save;
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
|
||||
var subRoomsProjected = updatedRoom.SubRooms?.Select(sub => new
|
||||
{
|
||||
sub.SubRoomId,
|
||||
sub.RoomId,
|
||||
CreatorAccountId = sub.CreatorAccountId,
|
||||
sub.UnitySceneId,
|
||||
sub.Name,
|
||||
CurrentSave = RoomDB.SubRoomSaves
|
||||
.Find(x => x.RoomId == updatedRoom.RoomId && x.SubRoomId == sub.SubRoomId)
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.Select(s => new
|
||||
{
|
||||
s.SubRoomDataSaveId,
|
||||
s.SubRoomId,
|
||||
s.DataBlob,
|
||||
s.DataBlobHash,
|
||||
s.ReferencedUnityAssetIds,
|
||||
s.PersistenceVersion,
|
||||
s.OMVersion,
|
||||
s.UgcSubVersion,
|
||||
s.SavedByAccountId,
|
||||
s.SavedOnPlatform,
|
||||
s.SavedOnDeviceClass,
|
||||
s.Description,
|
||||
s.Tags,
|
||||
s.ModerationState,
|
||||
s.CreatedAt
|
||||
})
|
||||
.FirstOrDefault(),
|
||||
sub.LastModeratedSaveModerationState,
|
||||
sub.IsSandbox,
|
||||
sub.MaxPlayers,
|
||||
sub.Accessibility,
|
||||
sub.ShouldAutoStageSaves,
|
||||
sub.StagedSubRoomDataSaveId
|
||||
}).ToList();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error_id = (string?)null,
|
||||
error = (string?)null
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
success = false,
|
||||
error_id = "Internal.Error",
|
||||
error = ex.Message,
|
||||
value = (object?)null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*[HttpGet("rooms/{roomId}/subrooms/{subroomId}/saves")]
|
||||
public ContentResult GetRoomSaves(ulong roomId, ulong subroomId, int unityAssetTarget = 0, int unityAssetVersion = 1, int skip = 0, int take = 20)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom((long)roomId);
|
||||
if (room == null)
|
||||
return NotFound("");
|
||||
|
||||
if (!CanViewSaves(room, (long)playerId))
|
||||
return Forbidden("");
|
||||
|
||||
if (PlayerDB.Players.FindById((long)playerId)?.PlayerRoles?.Contains(PlayerRoles.Developer) ?? false)
|
||||
take = 999;
|
||||
|
||||
var save = RoomDB.GetRoomSaves(roomId, subroomId, skip, take);
|
||||
|
||||
if (save == null) return Ok(ServerConfig.Bracket);
|
||||
|
||||
return Ok(new { Results = save.Results, TotalResults = save.TotalCount} );
|
||||
}*/
|
||||
|
||||
[HttpGet("rooms/magic_door")]
|
||||
public async Task<IActionResult> MagicDoor([FromQuery] int partySize = 1)
|
||||
{
|
||||
if (partySize <= 0)
|
||||
partySize = 1;
|
||||
|
||||
var random = new Random();
|
||||
|
||||
var col = RoomDB.RoomDBFile.GetCollection("Rooms");
|
||||
|
||||
var publicRooms = new List<Room>();
|
||||
|
||||
foreach (var doc in col.FindAll())
|
||||
{
|
||||
try
|
||||
{
|
||||
var room = BsonMapper.Global.Deserialize<Room>(doc);
|
||||
|
||||
if (room == null)
|
||||
continue;
|
||||
|
||||
if (room.IsDorm)
|
||||
continue;
|
||||
|
||||
if (room.Accessibility != RoomAccessibility.Public)
|
||||
continue;
|
||||
|
||||
if (room.SubRooms == null || room.SubRooms.Count == 0)
|
||||
continue;
|
||||
|
||||
publicRooms.Add(room);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[-] Skipped corrupt room doc {doc["_id"]}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (publicRooms.Count == 0)
|
||||
return RoomError("Rooms.NotFound", "No");
|
||||
|
||||
var triedRoomIds = new HashSet<long>();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
var remainingRooms = publicRooms
|
||||
.Where(r => !triedRoomIds.Contains(r.RoomId))
|
||||
.ToList();
|
||||
|
||||
if (remainingRooms.Count == 0)
|
||||
break;
|
||||
|
||||
var room = remainingRooms[random.Next(remainingRooms.Count)];
|
||||
|
||||
triedRoomIds.Add(room.RoomId);
|
||||
|
||||
var publicSubrooms = room.SubRooms
|
||||
.Where(s => s.Accessibility == RoomAccessibility.Public)
|
||||
.ToList();
|
||||
|
||||
if (publicSubrooms.Count == 0)
|
||||
continue;
|
||||
|
||||
SubRooms? targetSubroom = null;
|
||||
|
||||
if (publicSubrooms.Count == 1)
|
||||
{
|
||||
var onlySubroom = publicSubrooms[0];
|
||||
|
||||
if (onlySubroom.MaxPlayers < partySize)
|
||||
continue;
|
||||
|
||||
targetSubroom = onlySubroom;
|
||||
}
|
||||
else
|
||||
{
|
||||
var validSubrooms = publicSubrooms
|
||||
.Where(s => s.MaxPlayers >= partySize)
|
||||
.ToList();
|
||||
|
||||
if (validSubrooms.Count == 0)
|
||||
continue;
|
||||
|
||||
targetSubroom = validSubrooms[random.Next(validSubrooms.Count)];
|
||||
}
|
||||
|
||||
if (targetSubroom == null)
|
||||
continue;
|
||||
|
||||
return Ok(new { RefreshIntervalMinutes = 1, RefreshesAt = DateTime.UtcNow.AddMinutes(1), Room = room });
|
||||
}
|
||||
return BadRequest("");
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves/no_unity_assets")]
|
||||
public async Task<IActionResult> GetSubRoomSavesNoUnity(
|
||||
long roomId, long subRoomId,
|
||||
[FromQuery] int skip = 0, [FromQuery] int take = 20)
|
||||
{
|
||||
var (results, total) = RoomDB.GetSubRoomSaves(roomId, subRoomId, skip, take);
|
||||
return Ok(new { Results = results, TotalResults = total });
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves/{saveId}")]
|
||||
public async Task<IActionResult> GetSubRoomSaveById(long roomId, long subRoomId, long saveId)
|
||||
{
|
||||
var save = RoomDB.GetSubRoomSaveById(roomId, subRoomId, saveId);
|
||||
if (save == null) return NotFound("");
|
||||
return Ok(save);
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms")]
|
||||
public async Task<IActionResult> CreateSubroom(long roomId, [FromForm] string name)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
if (room.IsDorm) return RoomError("Rooms.PermissionDenied", "You cannot create a subroom for your dorm room!");
|
||||
if (string.IsNullOrWhiteSpace(name)) return RoomError("Rooms.InvalidName", "Subroom name cannot be empty.");
|
||||
|
||||
var updated = RoomDB.CreateSubroomForRoom(roomId, name);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = updated });
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId}/subrooms/{subRoomId}")]
|
||||
public async Task<IActionResult> DeleteSubroom(long roomId, long subRoomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
RoomDB.DeleteSubroom(roomId, subRoomId);
|
||||
|
||||
var updatedRoom = RoomDB.GetRoom(roomId);
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, updatedRoom);
|
||||
|
||||
return Ok(new { success = true, error = "", value = updatedRoom });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/subrooms/{subRoomId}/modify")]
|
||||
public async Task<IActionResult> ModifySubroom(
|
||||
long roomId, long subRoomId,
|
||||
[FromForm] string name,
|
||||
[FromForm] RoomAccessibility accessibility,
|
||||
[FromForm] int maxPlayers = 32)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
RoomDB.ModifySubroom(roomId, subRoomId, name, accessibility, maxPlayers);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpGet("photon_access_token")]
|
||||
public async Task<IActionResult> GetPhotonAccessToken()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var heartbeat = PlayerDB.GetPlayerHeartbeat((long)id);
|
||||
var roomInstance = heartbeat?.roomInstance;
|
||||
|
||||
List<object> permissions = new List<object>();
|
||||
|
||||
if (roomInstance != null)
|
||||
{
|
||||
var stored = PhotonAccessTokenDB.GetPermissions(roomInstance.roomId, roomInstance.subRoomId);
|
||||
if (stored != null)
|
||||
{
|
||||
permissions = stored.Permissions.Select(p => (object)new
|
||||
{
|
||||
Override = p.Override,
|
||||
Permission = p.Permission,
|
||||
Role = p.Role,
|
||||
Type = p.Type,
|
||||
Value = p.Value
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
PhotonAccessToken = "",
|
||||
RoomInstanceId = roomInstance?.roomInstanceId,
|
||||
Permissions = permissions.ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/rooms/{elroom}/similar")]
|
||||
public async Task<IActionResult> SimilarRooms(long elroom)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null) return Unauthorized("");
|
||||
var target = RoomDB.GetRoom(elroom);
|
||||
if (target == null) return NotFound("");
|
||||
var allRooms = RoomDB.Rooms.FindAll().Where(r => !IsBaseRoom(r)).ToList();
|
||||
var results = allRooms
|
||||
.Where(r => r.RoomId != elroom && (int)r.Accessibility != 0)
|
||||
.Select(r =>
|
||||
{
|
||||
double score = 0;
|
||||
var aTags = target.Tags?.Select(t => t.Tag.ToLower()).ToHashSet() ?? new HashSet<string>();
|
||||
var bTags = r.Tags?.Select(t => t.Tag.ToLower()).ToHashSet() ?? new HashSet<string>();
|
||||
int tagMatches = aTags.Intersect(bTags).Count();
|
||||
int totalTags = aTags.Union(bTags).Count();
|
||||
if (totalTags > 0)
|
||||
score += (double)tagMatches / totalTags * 40;
|
||||
if (!string.IsNullOrEmpty(target.Name) && !string.IsNullOrEmpty(r.Name))
|
||||
{
|
||||
var aWords = target.Name.ToLower().Split(' ');
|
||||
var bWords = r.Name.ToLower().Split(' ');
|
||||
int wordMatches = aWords.AsEnumerable().Intersect(bWords).Count();
|
||||
int totalWords = aWords.AsEnumerable().Union(bWords).Count();
|
||||
if (totalWords > 0)
|
||||
score += (double)wordMatches / totalWords * 20;
|
||||
}
|
||||
if (!string.IsNullOrEmpty(target.Description) && !string.IsNullOrEmpty(r.Description))
|
||||
{
|
||||
if (r.Description.ToLower().Contains(target.Name?.ToLower() ?? string.Empty))
|
||||
score += 5;
|
||||
}
|
||||
if (target.Stats != null && r.Stats != null)
|
||||
{
|
||||
if (!(target.Stats.VisitCount == 0 && r.Stats.VisitCount == 0))
|
||||
{
|
||||
double diff = Math.Abs(target.Stats.VisitCount - r.Stats.VisitCount);
|
||||
double max = Math.Max(target.Stats.VisitCount, r.Stats.VisitCount);
|
||||
if (max > 0)
|
||||
score += (1 - diff / max) * 10;
|
||||
}
|
||||
if (!(target.Stats.FavoriteCount == 0 && r.Stats.FavoriteCount == 0))
|
||||
{
|
||||
double diff = Math.Abs(target.Stats.FavoriteCount - r.Stats.FavoriteCount);
|
||||
double max = Math.Max(target.Stats.FavoriteCount, r.Stats.FavoriteCount);
|
||||
if (max > 0)
|
||||
score += (1 - diff / max) * 10;
|
||||
}
|
||||
if (!(target.Stats.CheerCount == 0 && r.Stats.CheerCount == 0))
|
||||
{
|
||||
double diff = Math.Abs(target.Stats.CheerCount - r.Stats.CheerCount);
|
||||
double max = Math.Max(target.Stats.CheerCount, r.Stats.CheerCount);
|
||||
if (max > 0)
|
||||
score += (1 - diff / max) * 10;
|
||||
}
|
||||
}
|
||||
if (target.MaxPlayers == r.MaxPlayers)
|
||||
score += 5;
|
||||
double daysDiff = Math.Abs((target.CreatedAt - r.CreatedAt).TotalDays);
|
||||
score += Math.Max(0, 5 - daysDiff / 30);
|
||||
if (target.Accessibility == r.Accessibility)
|
||||
score += 5;
|
||||
return new { Room = r, Score = score };
|
||||
})
|
||||
.OrderByDescending(x => x.Score)
|
||||
.Take(50)
|
||||
.Select(x => x.Room)
|
||||
.ToList();
|
||||
|
||||
return Ok(new { rooms = results });
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/rooms/{roomId}/experience/player")]
|
||||
public async Task<IActionResult> GetPlayerRoomExperience(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
return Ok(new { Enabled = false, Experience = (object?)null, ConcurrencyCode = (string?)null });
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/rooms/{roomId}/bans/{playerId}/isBanned")]
|
||||
public async Task<IActionResult> GetIsBanned(long roomId, long playerId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
return Ok(new { success = true, error = (string?)null, error_id = (string?)null, value = false });
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/rooms/{roomId}/assets")]
|
||||
public async Task<IActionResult> GetRoomAssets(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
return Ok(new { success = true, error = (string?)null, error_id = (string?)null, value = Array.Empty<object>() });
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/rooms/{roomId}/snapshots/{snapshotId}/loadinfo")]
|
||||
public async Task<IActionResult> GetSnapshotLoadInfo(long roomId, string snapshotId, [FromQuery] long subRoomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return NotFound("");
|
||||
|
||||
var subRoom = room.SubRooms?.FirstOrDefault(s => s.SubRoomId == subRoomId);
|
||||
var latestSave = RoomDB.GetSubRoomSaves(roomId, subRoomId, 0, 1).Results.FirstOrDefault();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = true,
|
||||
error = (string?)null,
|
||||
error_id = (string?)null,
|
||||
value = new
|
||||
{
|
||||
Assets = Array.Empty<object>(),
|
||||
RoomId = roomId,
|
||||
SnapshotId = snapshotId,
|
||||
SubRoom = new { SubRoomDataSaveId = latestSave?.SubRoomDataSaveId },
|
||||
SubRoomId = subRoomId,
|
||||
UgcSubVersion = room.UgcSubVersion,
|
||||
UgcVersion = room.UgcVersion
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/publishState/configs")]
|
||||
public async Task<IActionResult> GetPublishStateConfigs()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
value = new
|
||||
{
|
||||
UpdateMaxCount = 3,
|
||||
UpdateRollingWindowInDays = 365,
|
||||
UpdateExpirationInDays = 30,
|
||||
UpdateCooldownInDays = 45
|
||||
},
|
||||
success = true,
|
||||
error_id = (string?)null,
|
||||
error = (string?)null
|
||||
});
|
||||
}
|
||||
|
||||
/*[HttpPost("/api/rooms/v1/verifyRole")]
|
||||
public async Task<IActionResult> VerifyRole([FromQuery] long roomId, [FromQuery] int role, [FromQuery] string? context)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return Ok(false);
|
||||
|
||||
bool hasRole = room.Roles?.Any(r => r.AccountId == (long)id && (int)r.Role >= role) ?? false;
|
||||
return Ok(hasRole);
|
||||
}*/
|
||||
|
||||
[HttpPost("/api/rooms/v1/verifyRole")]
|
||||
public async Task<IActionResult> VerifyRole([FromQuery] long roomId, [FromQuery] int role, [FromQuery] string? context)
|
||||
{
|
||||
return Ok(true); // temp
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/rooms/{roomId}/playerdata/me")]
|
||||
public async Task<IActionResult> GetMyPlayerData(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if ((long)id == 55)
|
||||
return NotFound("");
|
||||
|
||||
var data = PlayerDB.GetPlayerData((long)id, roomId);
|
||||
if (string.IsNullOrEmpty(data)) return Ok(new { Data = "" }); // modern did this, so i am too
|
||||
return Ok(new { Data = data });
|
||||
}
|
||||
|
||||
[HttpPut("/roomserver/rooms/{roomId}/playerdata/me")]
|
||||
public async Task<IActionResult> SetMyPlayerData(long roomId, [FromForm] string data)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
//if (AuthStuff.GetAndEnsureAppVersion(Request) != "20230406") return NoContent();
|
||||
|
||||
bool success = PlayerDB.SetPlayerData((long)id, roomId, data);
|
||||
if (!success) return BadRequest();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("/roomserver/rooms/{roomId}/boosts/{playerId}/count")]
|
||||
public async Task<IActionResult> GetBoostCount(long roomId, long playerId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
return Ok("0");
|
||||
}
|
||||
|
||||
[HttpGet("/algorithmiclists/topengagementrooms")]
|
||||
public async Task<IActionResult> AlgorithmicListTop()
|
||||
{
|
||||
var featured = RoomDB.Rooms.FindAll()
|
||||
.Where(r =>
|
||||
r.Accessibility == RoomAccessibility.Public &&
|
||||
!r.IsDorm &&
|
||||
r.Tags != null &&
|
||||
r.Tags.Any(t => t.Tag.Equals("featured", StringComparison.OrdinalIgnoreCase)))
|
||||
.Select(r => new
|
||||
{
|
||||
Id = r.RoomId.ToString(),
|
||||
Context = "{\"algo_name\":\"Rooms_TopEngagement\",\"algo_version\":\"1\",\"user_segment\":\"11\",\"score\":1.0,\"is_shuffled\":false,\"is_rotated\":false}"
|
||||
})
|
||||
.ToList();
|
||||
return Ok(new
|
||||
{
|
||||
Type = 1,
|
||||
Entities = featured
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/algorithmiclists/{thing}")]
|
||||
public async Task<IActionResult> AlgorithmicList()
|
||||
{
|
||||
string path = Path.Join(Program.dataDir, "APIS", "2025thing.json");
|
||||
return System.IO.File.Exists(path) ? Content(System.IO.File.ReadAllText(path), "application/json") : NotFound();
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("rooms/carousel/rising")]
|
||||
public async Task<IActionResult> RisingRooms([FromQuery] string tag = "", [FromQuery] int skip = 0, [FromQuery] int take = 30)
|
||||
{
|
||||
string? passingTag = string.IsNullOrWhiteSpace(tag) ? null : tag;
|
||||
var (results, total) = RoomDB.GetHotRooms(passingTag, skip, take, true);
|
||||
return Ok(new
|
||||
{
|
||||
Results = results ?? new List<Room>(),
|
||||
TotalResults = total
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("featuredrooms/current")]
|
||||
public async Task<IActionResult> CurrentFeaturedRooms()
|
||||
{
|
||||
var featured = RoomDB.Rooms.FindAll()
|
||||
.Where(r =>
|
||||
r.Accessibility == RoomAccessibility.Public &&
|
||||
!r.IsDorm &&
|
||||
r.Tags != null &&
|
||||
r.Tags.Any(t => t.Tag.Equals("featured", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
return Ok(new
|
||||
{
|
||||
FeaturedRoomGroupId = 300,
|
||||
Name = "Featured Rooms",
|
||||
StartAt = "2024-05-06T10:01:00Z",
|
||||
EndAt = "2024-05-13T10:00:00Z",
|
||||
Rooms = featured
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("rooms/curated_playlists")]
|
||||
public async Task<IActionResult> RoomsCurated()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/recommendations")]
|
||||
public async Task<IActionResult> RoomsRecommended()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("showcase/{accountId}")]
|
||||
public async Task<IActionResult> RoomShowcase(long accountId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
return Ok(new List<object>());
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/voice_chat_encryption")]
|
||||
public async Task<IActionResult> SetVoiceChatEncryption(long roomId, [FromForm] bool encryptVoiceChat)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
room.EncryptVoiceChat = encryptVoiceChat;
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/comments")]
|
||||
public async Task<IActionResult> SetRoomComments(long roomId, [FromForm] bool disable)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
room.DisableRoomComments = disable;
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/automute")]
|
||||
public async Task<IActionResult> SetRoomAutomute(long roomId, [FromForm] bool disable)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
room.DisableMicAutoMute = disable;
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/allow_new_users")]
|
||||
public async Task<IActionResult> SetAllowNewUsers(long roomId, [FromForm] bool allowNewUsers)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
room.MinLevel = allowNewUsers ? 0 : 5;
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var allPlayers = PlayerDB.Players.FindAll().Select(p => p.PlayerId).ToList();
|
||||
foreach (var pid in allPlayers)
|
||||
await NotiController.SendRoomUpdate(pid, RoomDB.GetRoom(roomId));
|
||||
|
||||
return Ok(new { success = true, error = "", value = RoomDB.GetRoom(roomId) });
|
||||
}
|
||||
|
||||
[HttpGet("rooms/moderatedby/me")]
|
||||
public async Task<IActionResult> GetRoomsModeratedByMe()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var rooms = RoomDB.Rooms.FindAll()
|
||||
.Where(r => r.Roles?.Any(x => x.AccountId == (long)id && (int)x.Role >= (int)Role.Moderator) ?? false)
|
||||
.ToList();
|
||||
|
||||
return Ok(rooms);
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/bans/import")]
|
||||
public async Task<IActionResult> ImportRoomBans(long roomId, [FromForm] long sourceRoomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
return Ok(new { success = true, error = (string?)null, value = (object?)null });
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/bans")]
|
||||
public async Task<IActionResult> BanFromRoom(long roomId, [FromForm] int banMask, [FromForm(Name = "id")] long targetId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
room.Roles ??= new List<RoomDBClasses.Roles>();
|
||||
|
||||
var existing = room.Roles.FirstOrDefault(r => r.AccountId == targetId);
|
||||
if (existing != null)
|
||||
existing.Role = Role.Banned;
|
||||
else
|
||||
room.Roles.Add(new RoomDBClasses.Roles { AccountId = targetId, Role = Role.Banned, InvitedRole = Role.None });
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
var targetPlayer = PlayerDB.Players.FindById(targetId);
|
||||
if (targetPlayer?.Player?.PlayerExtra?.Heartbeat?.roomInstance?.roomId == roomId)
|
||||
{
|
||||
var dormPlayer = PlayerDB.GetCurrentPlayer(targetId);
|
||||
if (dormPlayer != null)
|
||||
{
|
||||
var dormRoomId = Vanadium.Controllers.MatchController.EnsurePlayerDorm(dormPlayer);
|
||||
var dormHeartbeat = Vanadium.Classes.Sessions.CreateDorm(targetId, dormPlayer.Player.Username, dormRoomId);
|
||||
await NotificationsController.RefreshHeartbeat(targetId, dormHeartbeat);
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new { success = true, error = (string?)null, value = (object?)null });
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/bans")]
|
||||
public async Task<IActionResult> GetRoomBans(long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
|
||||
var banned = (room.Roles ?? new List<RoomDBClasses.Roles>())
|
||||
.Where(r => r.Role == Role.Banned)
|
||||
.Select(r => new { AccountId = r.AccountId, BanStartTime = DateTime.UtcNow })
|
||||
.ToList();
|
||||
|
||||
return Ok(banned);
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId}/bans/{playerId}")]
|
||||
public async Task<IActionResult> UnbanFromRoom(long roomId, long playerId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
if (room == null) return RoomError("Rooms.DoesntExist", "This room does not exist!");
|
||||
if (!CanEdit(room, (long)id)) return RoomError("Rooms.PermissionDenied", "You are not the owner of this room!");
|
||||
|
||||
room.Roles?.RemoveAll(r => r.AccountId == playerId && r.Role == Role.Banned);
|
||||
|
||||
RoomDB.Rooms.Update(room);
|
||||
|
||||
return Ok(new { success = true, error = (string?)null, value = (object?)null });
|
||||
}
|
||||
|
||||
public class RoomDataRequest
|
||||
{
|
||||
public bool AutoPublish { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? InventionUsage { get; set; }
|
||||
public int PersistenceVersion { get; set; }
|
||||
public RoomFileData? RoomData { get; set; }
|
||||
public RoomFileData? SubRoomData { get; set; }
|
||||
public string? UnityAssetId { get; set; }
|
||||
}
|
||||
|
||||
private async Task<bool> ContainsProhibitedText(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return false;
|
||||
string filePath = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "swears.txt");
|
||||
if (!System.IO.File.Exists(filePath)) return false;
|
||||
var swearWords = await System.IO.File.ReadAllLinesAsync(filePath);
|
||||
return swearWords
|
||||
.Select(w => w.Trim())
|
||||
.Where(w => !string.IsNullOrEmpty(w))
|
||||
.Any(word => Regex.IsMatch(text, $@"(?<![a-zA-Z]){Regex.Escape(word)}(?![a-zA-Z])", RegexOptions.IgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Classes;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[Route("/")]
|
||||
[ApiController]
|
||||
public partial class RoomEconController : ControllerBase
|
||||
{
|
||||
[HttpGet("econ/roomInventory/room/{roomId}")]
|
||||
public IActionResult roomInventoryGet() => Ok(ServerConfig.Bracket); // todo db
|
||||
|
||||
[HttpGet("econ/roomOffer/room/{roomId}/purchaseCounts")]
|
||||
public IActionResult roomInventoryPurchaseCounts() => Ok(ServerConfig.Bracket); // todo db
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes.DBs;
|
||||
using static Vanadium.Classes.DBs.DBClasses.RoomKeysDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class RoomKeysController : ControllerBase
|
||||
{
|
||||
private static object MapKey(RoomKey key) => new
|
||||
{
|
||||
key.CreatedAt,
|
||||
key.Description,
|
||||
key.Name,
|
||||
key.Price,
|
||||
key.PurchaseCurrencyId,
|
||||
key.ReplicationId,
|
||||
key.RoomId,
|
||||
key.RoomKeyId
|
||||
};
|
||||
|
||||
[HttpPost("/api/roomkeys/v1/create")]
|
||||
public IActionResult CreateRoomKey(
|
||||
[FromForm] long RoomId,
|
||||
[FromForm] string Name,
|
||||
[FromForm] string? Description,
|
||||
[FromForm] int Price,
|
||||
[FromForm] string? PurchaseCurrencyId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Name) || Name.Length > 40)
|
||||
return BadRequest(new { Status = 1, Error = "Name must be between 1 and 40 characters." });
|
||||
|
||||
if (!string.IsNullOrEmpty(Description) && Description.Length > 174)
|
||||
return BadRequest(new { Status = 7, Error = "Description must be 174 characters or less." });
|
||||
|
||||
var room = RoomDB.GetRoom(RoomId);
|
||||
if (room == null) return NotFound("");
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(RoomId, id.Value))
|
||||
return Forbid();
|
||||
|
||||
var key = RoomKeysDB.CreateRoomKey(RoomId, Name, Description, Price, PurchaseCurrencyId);
|
||||
if (key == null) return StatusCode(500);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
RoomKey = MapKey(key),
|
||||
Status = 0
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("/api/roomkeys/v1/updateAll")]
|
||||
public IActionResult UpdateAll(
|
||||
[FromForm] long RoomKeyId,
|
||||
[FromForm] string Name,
|
||||
[FromForm] string? Description,
|
||||
[FromForm] int Price,
|
||||
[FromForm] string? PurchaseCurrencyId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var key = RoomKeysDB.GetRoomKey(RoomKeyId);
|
||||
if (key == null) return NotFound("");
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(key.RoomId, id.Value))
|
||||
return Forbid();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Name) || Name.Length > 40)
|
||||
return BadRequest(new { Status = 1, Error = "Name must be between 1 and 40 characters." });
|
||||
|
||||
if (!string.IsNullOrEmpty(Description) && Description.Length > 174)
|
||||
return BadRequest(new { Status = 7, Error = "Description must be 174 characters or less." });
|
||||
|
||||
RoomKeysDB.UpdateRoomKeyAll(RoomKeyId, Name, Description, Price, PurchaseCurrencyId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
RoomKey = RoomKeyId,
|
||||
Status = 12
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("/api/roomkeys/v1/updateName")]
|
||||
public IActionResult UpdateName(
|
||||
[FromForm] long RoomKeyId,
|
||||
[FromForm] string Name)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var key = RoomKeysDB.GetRoomKey(RoomKeyId);
|
||||
if (key == null) return NotFound("");
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(key.RoomId, id.Value))
|
||||
return Forbid();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Name) || Name.Length > 40)
|
||||
return BadRequest(new { Status = 1, Error = "Name must be between 1 and 40 characters." });
|
||||
|
||||
RoomKeysDB.UpdateRoomKeyName(RoomKeyId, Name);
|
||||
|
||||
return Ok(new { RoomKey = RoomKeyId, Status = 12 });
|
||||
}
|
||||
|
||||
[HttpPut("/api/roomkeys/v1/updateDescription")]
|
||||
public IActionResult UpdateDescription(
|
||||
[FromForm] long RoomKeyId,
|
||||
[FromForm] string Description)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var key = RoomKeysDB.GetRoomKey(RoomKeyId);
|
||||
if (key == null) return NotFound("");
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(key.RoomId, id.Value))
|
||||
return Forbid();
|
||||
|
||||
if (!string.IsNullOrEmpty(Description) && Description.Length > 174)
|
||||
return BadRequest(new { Status = 7, Error = "Description must be 174 characters or less." });
|
||||
|
||||
RoomKeysDB.UpdateRoomKeyDescription(RoomKeyId, Description);
|
||||
|
||||
return Ok(new { RoomKey = RoomKeyId, Status = 12 });
|
||||
}
|
||||
|
||||
[HttpDelete("/api/roomkeys/v1/delete/{roomKeyId}")]
|
||||
public IActionResult DeleteRoomKey(long roomKeyId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var key = RoomKeysDB.GetRoomKey(roomKeyId);
|
||||
if (key == null) return NotFound("");
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(key.RoomId, id.Value))
|
||||
return Forbid();
|
||||
|
||||
var deleted = RoomKeysDB.DeleteRoomKey(roomKeyId);
|
||||
return Ok(deleted);
|
||||
}
|
||||
|
||||
[HttpGet("/api/roomkeys/v1/room")]
|
||||
public IActionResult GetRoomKeys([FromQuery] long roomId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var keys = RoomKeysDB.GetRoomKeysByRoom(roomId).Select(MapKey).ToList();
|
||||
return Ok(keys);
|
||||
}
|
||||
|
||||
[HttpGet("/api/roomkeys/v1/mine")]
|
||||
public IActionResult GetMyKeys()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var keys = RoomKeysDB.GetRoomKeysByPlayer(id.Value).Select(MapKey).ToList();
|
||||
return Ok(keys);
|
||||
}
|
||||
|
||||
[HttpGet("/api/roomkeys/v1/owns")]
|
||||
public IActionResult PlayerOwnsKey([FromQuery] long playerId, [FromQuery] long roomKeyId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var owns = RoomKeysDB.PlayerOwnsKey(playerId, roomKeyId);
|
||||
return Ok(owns);
|
||||
}
|
||||
|
||||
[HttpGet("/api/roomkeys/v1/purchased/{roomKeyId}")]
|
||||
public IActionResult HasPurchased(long roomKeyId)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var purchased = RoomKeysDB.HasPurchasedKey(id.Value, roomKeyId);
|
||||
return Ok(purchased);
|
||||
}
|
||||
|
||||
[HttpPost("/api/roomkeys/v1/awardbulk")]
|
||||
public IActionResult AwardBulk([FromBody] List<AwardBulkRequest> requests)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
if (requests == null || requests.Count == 0)
|
||||
return BadRequest();
|
||||
|
||||
var results = new List<object>();
|
||||
|
||||
foreach (var req in requests)
|
||||
{
|
||||
var key = RoomKeysDB.GetRoomKey(req.RoomKeyId);
|
||||
if (key == null)
|
||||
{
|
||||
results.Add(new { req.PlayerId, req.RoomKeyId, Success = false, Error = "Key not found" });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(key.RoomId, id.Value))
|
||||
{
|
||||
results.Add(new { req.PlayerId, req.RoomKeyId, Success = false, Error = "Forbidden" });
|
||||
continue;
|
||||
}
|
||||
|
||||
var awarded = RoomKeysDB.AwardKey(req.PlayerId, req.RoomKeyId);
|
||||
results.Add(new { req.PlayerId, req.RoomKeyId, Success = awarded });
|
||||
}
|
||||
|
||||
return Ok(results);
|
||||
}
|
||||
|
||||
[HttpPost("/api/roomkeys/v1/revoke")]
|
||||
public IActionResult RevokeKey([FromBody] RevokeRequest request)
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null) return Unauthorized("");
|
||||
|
||||
var key = RoomKeysDB.GetRoomKey(request.RoomKeyId);
|
||||
if (key == null) return NotFound("");
|
||||
|
||||
if (!RoomDB.UserCanEditRoom(key.RoomId, id.Value))
|
||||
return Forbid();
|
||||
|
||||
var revoked = RoomKeysDB.RevokeKey(request.PlayerId, request.RoomKeyId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
Success = revoked,
|
||||
Message = revoked ? null : "Player does not own this key"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
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]
|
||||
public partial class GiftsController : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/avatar/v2/gifts")]
|
||||
public async Task<IActionResult> GetMyGifts()
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var gifts = GiftsDB.GetUnconsumedGifts(id.Value);
|
||||
return Ok(gifts);
|
||||
}
|
||||
|
||||
[HttpPost("/api/avatar/v2/gifts/consume/")]
|
||||
public async Task<IActionResult> ConsumeGift([FromForm] long Id, [FromForm] int UnlockedLevel = 0)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
var gift = GiftsDB.GetGiftById(Id);
|
||||
if (gift == null)
|
||||
return NotFound("");
|
||||
|
||||
if (gift.ToPlayerId != playerId.Value)
|
||||
return StatusCode(403);
|
||||
|
||||
bool consumed = GiftsDB.ConsumeGift(Id, playerId.Value);
|
||||
if (!consumed)
|
||||
return BadRequest(new { error = "Gift already consumed or not found", success = false, value = (object?)null });
|
||||
|
||||
var player = PlayerDB.Players.FindById(playerId.Value);
|
||||
bool playerUpdated = false;
|
||||
|
||||
Console.WriteLine($"Comparing Gift Currency: {gift.Currency} > 0 && {gift.CurrencyType} != 0");
|
||||
if (gift.Currency > 0 && gift.CurrencyType != 0)
|
||||
{
|
||||
Console.WriteLine($"Adding currency to player {playerId.Value}: {gift.Currency} of type {gift.CurrencyType}");
|
||||
if (player?.Player?.PlayerExtra != null)
|
||||
{
|
||||
player.Player.PlayerExtra.Currencies ??= new List<PlayerDBClasses.PlayerCurrency>();
|
||||
var currency = player.Player.PlayerExtra.Currencies
|
||||
.FirstOrDefault(x => (int)x.CurrencyType == gift.CurrencyType);
|
||||
|
||||
Console.WriteLine($"About to set currency balance. Current balance: {currency?.Balance ?? 0}, Gift currency: {gift.Currency}");
|
||||
|
||||
if (currency != null)
|
||||
currency.Balance += gift.Currency;
|
||||
else
|
||||
player.Player.PlayerExtra.Currencies.Add(new PlayerDBClasses.PlayerCurrency
|
||||
{
|
||||
Balance = gift.Currency,
|
||||
CurrencyType = (PlayerDBClasses.CurrencyType)gift.CurrencyType,
|
||||
BalanceType = (PlayerDBClasses.BalanceType)gift.BalanceType
|
||||
});
|
||||
|
||||
Console.WriteLine($"Updated player {playerId.Value} currency. New balance: {currency?.Balance ?? gift.Currency}");
|
||||
playerUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(gift.EquipmentModificationGuid) && player?.Player != null)
|
||||
{
|
||||
string equipPath = Path.Join(Program.dataDir, "APIS", "Items", "Equipment.json");
|
||||
if (System.IO.File.Exists(equipPath))
|
||||
{
|
||||
var allItems = System.Text.Json.JsonSerializer.Deserialize<List<PlayerDBClasses.OwnedEquipmentItem>>(
|
||||
System.IO.File.ReadAllText(equipPath),
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
var target = allItems?.FirstOrDefault(i =>
|
||||
string.Equals(i.ModificationGuid, gift.EquipmentModificationGuid, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (target != null)
|
||||
{
|
||||
player.Player.PlayerExtra ??= new PlayerDBClasses.PlayerExtra();
|
||||
player.Player.PlayerExtra.OwnedEquipment ??= new List<PlayerDBClasses.OwnedEquipmentItem>();
|
||||
|
||||
bool owns = player.Player.PlayerExtra.OwnedEquipment
|
||||
.Any(e => string.Equals(e.ModificationGuid, target.ModificationGuid, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!owns)
|
||||
{
|
||||
player.Player.PlayerExtra.OwnedEquipment.Add(new PlayerDBClasses.OwnedEquipmentItem
|
||||
{
|
||||
PrefabName = target.PrefabName,
|
||||
ModificationGuid = target.ModificationGuid,
|
||||
FriendlyName = target.FriendlyName,
|
||||
PlatformMask = target.PlatformMask,
|
||||
Tooltip = target.Tooltip,
|
||||
Rarity = target.Rarity,
|
||||
Favorited = false
|
||||
});
|
||||
playerUpdated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (playerUpdated && player != null)
|
||||
PlayerDB.Players.Update(player);
|
||||
|
||||
var giftDto = GiftsDB.MapToDTO(gift);
|
||||
await NotiController.SendEvent(playerId.Value, "30", giftDto);
|
||||
|
||||
var remainingGifts = GiftsDB.GetUnconsumedGifts(playerId.Value);
|
||||
await NotiController.SendEvent(playerId.Value, "GiftConsumed", new
|
||||
{
|
||||
GiftId = Id,
|
||||
RemainingGifts = remainingGifts
|
||||
});
|
||||
|
||||
return Ok(new { error = "", success = true, value = (object?)null });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using Vanadium.Utils.NotiController;
|
||||
using Vanadium.enums;
|
||||
using static Vanadium.Classes.DBs.DBClasses.StorefrontsDBClasses;
|
||||
using static Vanadium.Classes.DBs.DBClasses.GiftsDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public partial class StorefrontController : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/storefronts/v4/balance/{CurrencyType}")]
|
||||
public IActionResult StorefrontsBalance(int CurrencyType)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var player = PlayerDB.Players.FindById(account.Value);
|
||||
if (player == null)
|
||||
return NotFound("");
|
||||
|
||||
var currency = player.Player?.PlayerExtra?.Currencies?
|
||||
.FirstOrDefault(x => (int)x.CurrencyType == CurrencyType);
|
||||
|
||||
return Ok(new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
CurrencyType,
|
||||
Platform = -1,
|
||||
Balance = currency?.Balance ?? 0
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("/econ/roomInventory/room/{roomid}/player")]
|
||||
public IActionResult roomInventoryEconomy()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("/econ/roomOffer/room/{roomid}")]
|
||||
public IActionResult roomOfferEconomy()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("/econ/roomGiftDropShops/room/{roomid}")]
|
||||
public IActionResult roomGiftDropShopsEconomy()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("/api/storefronts/v3/giftdropstore/{Storefront}")]
|
||||
public IActionResult StorefrontsGiftDropStore(int Storefront)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var store = StorefrontsDB.GetGiftDropStore(Storefront);
|
||||
if (store == null)
|
||||
return Ok(ServerConfig.Bracket);
|
||||
|
||||
return Ok(store);
|
||||
}
|
||||
|
||||
[HttpGet("/api/storefronts/v1/adcarouselitems")]
|
||||
public IActionResult GetAdCarouselItems()
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var items = StorefrontsDB.GetAdCarouselItems();
|
||||
return Ok(items);
|
||||
}
|
||||
|
||||
[HttpGet("api/catalog/v1/all")]
|
||||
public async Task<IActionResult> GetCatalogItemsAllOfThem(bool onlyAvailableSkus = true)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "2025Consumables", "AllCatalogItems.json");
|
||||
string content = await System.IO.File.ReadAllTextAsync(path);
|
||||
return Ok(content);
|
||||
}
|
||||
|
||||
[HttpGet("/api/itemWishlists/v1/wishlist/me")]
|
||||
public IActionResult GetMyWishlist()
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var wishlist = StorefrontsDB.GetWishlist(account.Value);
|
||||
return Ok(wishlist);
|
||||
}
|
||||
|
||||
[HttpPost("/api/itemWishlists/v1/wishlist/me/{purchasableItemId}")]
|
||||
public IActionResult AddToWishlist(int purchasableItemId)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var item = StorefrontsDB.AddToWishlist(account.Value, purchasableItemId);
|
||||
return Ok(item);
|
||||
}
|
||||
|
||||
[HttpDelete("/api/itemWishlists/v1/wishlist/me/{purchasableItemId}")]
|
||||
public IActionResult RemoveFromWishlist(int purchasableItemId)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
StorefrontsDB.RemoveFromWishlist(account.Value, purchasableItemId);
|
||||
return Ok(new SuccessResult { error = "", success = true, value = null });
|
||||
}
|
||||
|
||||
/* Free Gifts */
|
||||
|
||||
[HttpGet("/api/consumables/v1/getTransferable/{playerId}")]
|
||||
public IActionResult GetTransferableConsumables(long playerId)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var groups = StorefrontsDB.GetTransferableConsumables(playerId);
|
||||
return Ok(groups);
|
||||
}
|
||||
|
||||
[HttpGet("/api/config/v1/freegiftbutton")]
|
||||
public IActionResult GetFreeGiftButton()
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
[HttpPost("/api/storefronts/v1/buyForFreeGiftButton")]
|
||||
public IActionResult BuyForFreeGiftButton([FromBody] BuyForFreeGiftButtonRequest request)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
if (request == null)
|
||||
return BadRequest();
|
||||
|
||||
var storeItem = StorefrontsDB.GetStoreItemById(1, request.PurchasableItemId);
|
||||
if (storeItem == null)
|
||||
return NotFound(new { error = "Item not found", success = false });
|
||||
|
||||
var consumableDesc = storeItem.GiftDrop?.ConsumableItemDesc ?? "";
|
||||
var activeDuration = 5;
|
||||
|
||||
var granted = StorefrontsDB.GrantTransferableConsumables(account.Value, consumableDesc, request.Count, activeDuration);
|
||||
|
||||
int balance = StorefrontsDB.GetBalance(account.Value, 2);
|
||||
|
||||
var response = new SuccessResult
|
||||
{
|
||||
error = "",
|
||||
success = true,
|
||||
value = new BuyForFreeGiftButtonResponse
|
||||
{
|
||||
Balance = balance,
|
||||
BalanceType = -2,
|
||||
CurrencyType = 2,
|
||||
BalanceUpdates = new List<ConsumableBalanceUpdate>
|
||||
{
|
||||
new ConsumableBalanceUpdate
|
||||
{
|
||||
Data = granted,
|
||||
UpdateResponse = 2
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
/* End of free gifts, lmk if i missed anything */
|
||||
|
||||
[HttpPost("/api/items/bulkpurchase")]
|
||||
[HttpPost("/api/storefronts/v2/buyItem")]
|
||||
public async Task<IActionResult> BuyItem([FromBody] BuyItemRequest request)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
if (request == null)
|
||||
return BadRequest();
|
||||
|
||||
var storeItem = StorefrontsDB.GetStoreItemById(request.StorefrontType, request.PurchasableItemId);
|
||||
if (storeItem == null)
|
||||
return NotFound(new { error = "Item not found", success = false });
|
||||
|
||||
var matchingPrice = storeItem.Prices.FirstOrDefault(x => x.CurrencyType == request.CurrencyType);
|
||||
if (matchingPrice == null)
|
||||
return BadRequest(new { error = "Invalid currency type for this item", success = false });
|
||||
|
||||
if (matchingPrice.Price != request.RequestedPrice)
|
||||
return BadRequest(new { error = "Price mismatch", success = false });
|
||||
|
||||
var player = PlayerDB.Players.FindById(account.Value);
|
||||
if (storeItem.GiftDrop.Tooltip.Contains("[DEV ONLY]") && !(AuthStuff.GetCurrentPlayer(Request)?.PlayerRoles?.Contains(PlayerDBClasses.PlayerRoles.Developer) ?? false))
|
||||
return BadRequest(new { error = "You are not a dev, you can not purchase this item.", success = false });
|
||||
|
||||
int currentBalance = StorefrontsDB.GetBalance(account.Value, request.CurrencyType);
|
||||
if (currentBalance < request.RequestedPrice)
|
||||
return BadRequest(new { error = "Insufficient balance", success = false });
|
||||
|
||||
long recipientId = account.Value;
|
||||
string giftMessage = "A gift for you <3";
|
||||
|
||||
if (request.Gift != null)
|
||||
{
|
||||
recipientId = request.Gift.RecipientPlayerId > 0
|
||||
? request.Gift.RecipientPlayerId
|
||||
: (request.Gift.ToPlayerId ?? 0);
|
||||
|
||||
if (recipientId <= 0)
|
||||
return BadRequest(new { error = "Invalid gift recipient", success = false });
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Gift.Message))
|
||||
giftMessage = request.Gift.Message;
|
||||
}
|
||||
|
||||
var currency = StorefrontsDB.DeductCurrency(account.Value, request.CurrencyType, request.RequestedPrice);
|
||||
if (currency == null)
|
||||
return BadRequest(new { error = "Insufficient balance", success = false });
|
||||
|
||||
var giftDrop = storeItem.GiftDrop;
|
||||
|
||||
var createdGift = GiftsDB.CreateGift(
|
||||
toPlayerId: recipientId,
|
||||
fromPlayerId: account.Value,
|
||||
avatarItemDesc: giftDrop?.AvatarItemDesc ?? "",
|
||||
avatarItemType: giftDrop?.AvatarItemType ?? 0,
|
||||
balanceType: -2,
|
||||
consumableItemDesc: giftDrop?.ConsumableItemDesc ?? "",
|
||||
currency: giftDrop?.Currency ?? 0,
|
||||
currencyType: giftDrop?.CurrencyType ?? 0,
|
||||
equipmentModificationGuid: giftDrop?.EquipmentModificationGuid ?? "",
|
||||
equipmentPrefabName: giftDrop?.EquipmentPrefabName ?? "",
|
||||
giftContext: (GiftType)(account.Value != recipientId ? 500 : 200000),
|
||||
giftRarity: giftDrop?.Rarity ?? 50,
|
||||
level: 0,
|
||||
message: giftMessage,
|
||||
platform: -1,
|
||||
platformsToSpawnOn: -1,
|
||||
xp: 0
|
||||
);
|
||||
|
||||
if (giftDrop?.Currency != null)
|
||||
await APIController.SendTokenEarningWebhook(recipientId, giftDrop.Currency.ToString(), 2);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(giftDrop?.EquipmentModificationGuid) && !string.IsNullOrWhiteSpace(giftDrop?.EquipmentPrefabName))
|
||||
{
|
||||
PlayerDB.GrantEquipmentItem(
|
||||
recipientId,
|
||||
giftDrop.EquipmentModificationGuid,
|
||||
giftDrop.EquipmentPrefabName,
|
||||
giftDrop.FriendlyName,
|
||||
giftDrop.Tooltip,
|
||||
giftDrop.Rarity
|
||||
);
|
||||
}
|
||||
|
||||
Console.WriteLine($"Player {account.Value} bought item for recipient {recipientId} with gift ID {createdGift.Id}. Is this a gift for someone else?: {account.Value != recipientId}. Comparison: {account.Value} != {recipientId}");
|
||||
|
||||
if (account.Value != recipientId)
|
||||
await NotiController.SendEvent(recipientId, "31", GiftsDB.MapToDTO(createdGift));
|
||||
|
||||
var response = new BuyItemResponse
|
||||
{
|
||||
Balance = -request.RequestedPrice,
|
||||
BalanceType = -2,
|
||||
CurrencyType = request.CurrencyType,
|
||||
BalanceUpdates = new List<BalanceUpdate>
|
||||
{
|
||||
new BalanceUpdate
|
||||
{
|
||||
Data = new List<GiftDTO> { GiftsDB.MapToDTO(createdGift) },
|
||||
UpdateResponse = 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpPost("/api/gamerewards/v1/request")]
|
||||
public async Task<IActionResult> RequestGameReward([FromForm] string rewardType, [FromForm] string? Message)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rewardType))
|
||||
return BadRequest(new SuccessResult { error = "Missing rewardType", success = false });
|
||||
|
||||
await NotiController.SendEvent(account.Value, "GameRewardRequested", new
|
||||
{
|
||||
PlayerId = account.Value,
|
||||
RewardType = rewardType,
|
||||
Message = Message ?? ""
|
||||
});
|
||||
|
||||
return Ok(new SuccessResult { error = "", success = true, value = null });
|
||||
}
|
||||
|
||||
[HttpPost("/api/gamerewards/v1/select")]
|
||||
public async Task<IActionResult> SelectGameReward([FromForm] int rewardSelectionId, [FromForm] int giftDropId)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var giftDrop = StorefrontsDB.GetGiftDropByIdAcrossAllStores(giftDropId);
|
||||
if (giftDrop == null)
|
||||
return NotFound(new { error = "GiftDrop not found", success = false });
|
||||
|
||||
var createdGift = GiftsDB.CreateGift(
|
||||
toPlayerId: account.Value,
|
||||
fromPlayerId: 1,
|
||||
avatarItemDesc: giftDrop.AvatarItemDesc,
|
||||
avatarItemType: giftDrop.AvatarItemType,
|
||||
balanceType: -2,
|
||||
consumableItemDesc: giftDrop.ConsumableItemDesc,
|
||||
currency: giftDrop.Currency,
|
||||
currencyType: giftDrop.CurrencyType,
|
||||
equipmentModificationGuid: giftDrop.EquipmentModificationGuid,
|
||||
equipmentPrefabName: giftDrop.EquipmentPrefabName,
|
||||
giftContext: (GiftType)(giftDrop.Context != 0 ? giftDrop.Context : 110000),
|
||||
giftRarity: giftDrop.Rarity,
|
||||
level: 0,
|
||||
message: "A gift for you <3",
|
||||
platform: -1,
|
||||
platformsToSpawnOn: -1,
|
||||
xp: 0
|
||||
);
|
||||
|
||||
await NotiController.SendEvent(account.Value, "GameRewardSelected", new
|
||||
{
|
||||
PlayerId = account.Value,
|
||||
GiftDropId = giftDropId,
|
||||
RewardSelectionId = rewardSelectionId,
|
||||
Gift = GiftsDB.MapToDTO(createdGift)
|
||||
});
|
||||
|
||||
var response = new RewardSelectionResponse
|
||||
{
|
||||
AvatarItemDesc = giftDrop.AvatarItemDesc,
|
||||
AvatarItemType = giftDrop.AvatarItemType,
|
||||
ConsumableItemDesc = giftDrop.ConsumableItemDesc,
|
||||
Context = giftDrop.Context != 0 ? giftDrop.Context : 110000,
|
||||
Currency = giftDrop.Currency,
|
||||
CurrencyType = giftDrop.CurrencyType,
|
||||
EquipmentModificationGuid = giftDrop.EquipmentModificationGuid,
|
||||
EquipmentPrefabName = giftDrop.EquipmentPrefabName,
|
||||
FriendlyName = giftDrop.FriendlyName,
|
||||
GiftDropId = giftDrop.GiftDropId,
|
||||
IsQuery = giftDrop.IsQuery,
|
||||
ItemSetFriendlyName = giftDrop.ItemSetFriendlyName,
|
||||
ItemSetId = 1,
|
||||
Rarity = giftDrop.Rarity,
|
||||
SubscribersOnly = giftDrop.SubscribersOnly,
|
||||
Tooltip = giftDrop.Tooltip,
|
||||
Unique = giftDrop.Unique
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("/api/gamerewards/v1/pending")]
|
||||
public async Task<IActionResult> GetPendingGameRewards()
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("/api/storefronts/v2/buyInvention")]
|
||||
[HttpPost("/api/storefronts/v2/buyInvention")]
|
||||
public async Task<IActionResult> BuyInvention(
|
||||
[FromQuery] long inventionId,
|
||||
[FromQuery] long? requestedPrice = null,
|
||||
[FromQuery] int currencyType = 2)
|
||||
{
|
||||
var account = AuthStuff.GetPlayerId(Request);
|
||||
if (account == null)
|
||||
return StatusCode(403);
|
||||
|
||||
var invention = InventionDB.Inventions.FindOne(i => i.InventionId == inventionId);
|
||||
if (invention == null)
|
||||
return NotFound(new { error = "Invention not found", success = false });
|
||||
|
||||
if (requestedPrice.HasValue && requestedPrice.Value != invention.Price)
|
||||
return BadRequest(new { error = "Price mismatch", success = false });
|
||||
|
||||
int currentBalance = StorefrontsDB.GetBalance(account.Value, currencyType);
|
||||
if (currentBalance < invention.Price)
|
||||
return BadRequest(new { error = "Insufficient balance", success = false });
|
||||
|
||||
var currency = StorefrontsDB.DeductCurrency(account.Value, currencyType, invention.Price);
|
||||
if (currency == null)
|
||||
return BadRequest(new { error = "Insufficient balance", success = false });
|
||||
|
||||
invention.DownloadIds.Add(account.Value);
|
||||
InventionDB.Inventions.Update(invention);
|
||||
|
||||
var resolvedVersion = invention.CurrentVersion
|
||||
?? (invention.Versions?.OrderByDescending(v => v.VersionNumber).FirstOrDefault())
|
||||
?? InventionDB.GetLatestVersion(inventionId)
|
||||
?? new InventionDBClasses.InventionVersion
|
||||
{
|
||||
InventionId = invention.InventionId,
|
||||
VersionNumber = invention.CurrentVersionNumber
|
||||
};
|
||||
|
||||
var response = new InventionDBClasses.BuyInventionResponse
|
||||
{
|
||||
InventionResponse = new InventionDBClasses.InventionResponse
|
||||
{
|
||||
Invention = invention,
|
||||
InventionVersion = resolvedVersion,
|
||||
StatusCode = 0
|
||||
},
|
||||
BalanceUpdateResponse = new StorefrontsDBClasses.BuyForFreeGiftButtonResponse
|
||||
{
|
||||
Balance = -invention.Price,
|
||||
BalanceType = 0,
|
||||
CurrencyType = currencyType,
|
||||
BalanceUpdates = new List<ConsumableBalanceUpdate>()
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Auth;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.DBs;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public partial class AnnouncementsController : ControllerBase
|
||||
{
|
||||
[HttpGet("/api/announcement/v1/get")]
|
||||
public async Task<IActionResult> AnnouncementV1Get()
|
||||
{
|
||||
return Ok(AnnoucementDB.GetAllAnnouncements() ?? ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
// todo db for each
|
||||
|
||||
[HttpGet("v2/subscription/mine/unread")]
|
||||
[HttpGet("v2/mine/unread")]
|
||||
[HttpGet("v1/get")]
|
||||
public async Task<IActionResult> SubscriptionAndALlat()
|
||||
{
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1065 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Vanadium.Auth;
|
||||
using Newtonsoft.Json;
|
||||
using Vanadium.Classes;
|
||||
using Vanadium.Classes.WebSocket;
|
||||
using Vanadium.Hubs;
|
||||
using Vanadium.Classes.DBs;
|
||||
using Vanadium.Classes.DBs.DBClasses;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using static Vanadium.Classes.DBs.DBClasses.PlayerDBClasses;
|
||||
|
||||
namespace Vanadium.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("notifications")]
|
||||
public class NotificationsController : Controller
|
||||
{
|
||||
public static ConcurrentDictionary<string, WebSocket> WebSockets { get; } = new();
|
||||
public static ConcurrentDictionary<long, HashSet<string>> PlayerConnections { get; } = new();
|
||||
private static readonly ConcurrentDictionary<string, SemaphoreSlim> SendGates = new();
|
||||
private static readonly byte[] Separator = Encoding.UTF8.GetBytes("\u001e");
|
||||
|
||||
public static class EventTypes
|
||||
{
|
||||
public const string AccountUpdate = NotiEventTypes.AccountUpdate;
|
||||
public const string SelfAccountUpdate = NotiEventTypes.SelfAccountUpdate;
|
||||
public const string PresenceUpdate = NotiEventTypes.PresenceUpdate;
|
||||
public const string RoomUpdate = NotiEventTypes.RoomUpdate;
|
||||
public const string RoomInstanceUpdate = NotiEventTypes.RoomInstanceUpdate;
|
||||
public const string ReputationUpdate = NotiEventTypes.ReputationUpdate;
|
||||
public const string PlayerProgressionLevelUpdate = NotiEventTypes.PlayerProgressionLevelUpdate;
|
||||
public const string PhotonAccessToken = NotiEventTypes.PhotonAccessToken;
|
||||
public const string InfluencerSupportedUpdate = NotiEventTypes.InfluencerSupportedUpdate;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult NameserverNoti()
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
Auth = "https://reloxa.xyz/notifications",
|
||||
API = "https://reloxa.xyz/notifications",
|
||||
WWW = "https://reloxa.xyz/notifications",
|
||||
Notifications = "https://reloxa.xyz/notifications",
|
||||
Images = "https://reloxa.xyz/notifications/img",
|
||||
CDN = "https://reloxa.xyz/cdn",
|
||||
Commerce = "https://reloxa.xyz/notifications",
|
||||
Matchmaking = "https://reloxa.xyz/notifications",
|
||||
Storage = "https://reloxa.xyz/notifications",
|
||||
Chat = "https://reloxa.xyz/notifications/chat",
|
||||
Leaderboard = "https://reloxa.xyz/notifications",
|
||||
Accounts = "https://reloxa.xyz/notifications",
|
||||
Link = "https://reloxa.xyz/notifications",
|
||||
RoomComments = "https://reloxa.xyz/notifications",
|
||||
Clubs = "https://reloxa.xyz/notifications",
|
||||
Rooms = "https://reloxa.xyz/notifications/roomserver",
|
||||
PlatformNotifications = "https://reloxa.xyz/notifications/pn",
|
||||
Moderation = "https://reloxa.xyz/notifications/mo",
|
||||
DataCollection = "https://reloxa.xyz/notifications/dc",
|
||||
BugReporting = "https://reloxa.xyz/notifications/br",
|
||||
Discovery = "https://reloxa.xyz/notifications/disc",
|
||||
Econ = "https://reloxa.xyz/notifications",
|
||||
CMS = "https://reloxa.xyz/notifications/cms",
|
||||
GameLogs = "https://reloxa.xyz/notifications/gl",
|
||||
Lists = "https://reloxa.xyz/notifications/li",
|
||||
PlayerSettings = "https://reloxa.xyz/notifications",
|
||||
Strings = "https://reloxa.xyz/notifications/s",
|
||||
StringsCDN = "https://reloxa.xyz/notifications/scdn",
|
||||
Studio = "https://reloxa.xyz/notifications/st"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpPost("hub/v1/negotiate")]
|
||||
public IActionResult Negotiate()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
|
||||
string connectionId = Guid.NewGuid().ToString("N");
|
||||
|
||||
var response = new
|
||||
{
|
||||
negotiateVersion = 0,
|
||||
connectionId = connectionId,
|
||||
availableTransports = new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
transport = "WebSockets",
|
||||
transferFormats = new[] { "Text", "Binary" }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[Route("hub/v1")]
|
||||
public async Task HandleHub([FromQuery] string id, [FromHeader(Name = "Authorization")] string? auth)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
return;
|
||||
}
|
||||
|
||||
string connectionId = id;
|
||||
long pId = playerId.Value;
|
||||
|
||||
using var socket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
//Console.WriteLine($"[WebSocket] Player {pId} connected with connection {connectionId}");
|
||||
|
||||
PlayerConnections.AddOrUpdate(
|
||||
pId,
|
||||
_ => new HashSet<string> { connectionId },
|
||||
(_, hs) =>
|
||||
{
|
||||
lock (hs) { hs.Add(connectionId); }
|
||||
return hs;
|
||||
}
|
||||
);
|
||||
|
||||
await HandleConnectionAsync(pId, connectionId, socket);
|
||||
}
|
||||
|
||||
[HttpGet("preferences")]
|
||||
public IActionResult GetPreferences()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(new { mutedCategories = Array.Empty<object>() });
|
||||
}
|
||||
|
||||
[HttpGet("crm/me/config/v3")]
|
||||
public IActionResult GetCRMConfig()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
[HttpGet("accounts/{id}/receives/GameplayInvites")]
|
||||
public IActionResult GetGameplayInvites(ulong id)
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
[HttpGet("config/categories")]
|
||||
public IActionResult GetCategories()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "Data", "APIS", "NotiConfigCategories.json");
|
||||
if (!System.IO.File.Exists(path))
|
||||
return Ok(new { results = Array.Empty<object>(), totalResults = 0 });
|
||||
return PhysicalFile(path, "application/json");
|
||||
}
|
||||
|
||||
[HttpGet("/announcements/v2/mine/unread")]
|
||||
public IActionResult GetUnreadAnnouncements()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("/announcements/v2/subscription/mine/unread")]
|
||||
public IActionResult GetUnreadSubscriptionAnnouncements()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
[HttpGet("/subscription/mine/member")]
|
||||
public IActionResult GetSubscriptionMember()
|
||||
{
|
||||
var playerId = AuthStuff.GetPlayerId(Request);
|
||||
if (playerId == null)
|
||||
return Unauthorized("");
|
||||
return Ok(ServerConfig.Bracket);
|
||||
}
|
||||
|
||||
public static int ReconcileStartupOnlineHeartbeats(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var connectedPlayerIds = PlayerConnections.Keys.ToHashSet();
|
||||
|
||||
var onlinePlayers = PlayerDB.Players
|
||||
.FindAll()
|
||||
.Where(p => p?.Player?.PlayerExtra?.Heartbeat?.isOnline == true)
|
||||
.ToList();
|
||||
|
||||
int fixedCount = 0;
|
||||
|
||||
foreach (var player in onlinePlayers)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (player == null || player.Player == null || player.Player.PlayerExtra == null)
|
||||
continue;
|
||||
|
||||
if (connectedPlayerIds.Contains(player.PlayerId))
|
||||
continue;
|
||||
|
||||
player.Player.PlayerExtra.Heartbeat = new Heartbeat
|
||||
{
|
||||
playerId = player.PlayerId,
|
||||
isOnline = false,
|
||||
roomInstance = null,
|
||||
errorCode = 0
|
||||
};
|
||||
|
||||
PlayerDB.Players.Update(player);
|
||||
fixedCount++;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[Notifications] Startup heartbeat reconcile complete. Marked {fixedCount} stale online heartbeats offline.");
|
||||
return fixedCount;
|
||||
}
|
||||
|
||||
private static async Task HandleConnectionAsync(long playerId, string connectionId, WebSocket socket)
|
||||
{
|
||||
using var pingCts = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
WebSockets[connectionId] = socket;
|
||||
SendGates[connectionId] = new SemaphoreSlim(1, 1);
|
||||
|
||||
await SendHandshakeAsync(connectionId, socket);
|
||||
|
||||
_ = Task.Run(() => PingLoopAsync(connectionId, socket, pingCts.Token));
|
||||
|
||||
var buffer = new byte[4096];
|
||||
while (socket.State == WebSocketState.Open)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await socket.ReceiveAsync(buffer, CancellationToken.None);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
ms.Write(buffer, 0, result.Count);
|
||||
}
|
||||
while (!result.EndOfMessage);
|
||||
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
|
||||
var message = Encoding.UTF8.GetString(ms.GetBuffer(), 0, (int)ms.Length).TrimEnd('\x1e');
|
||||
//Console.WriteLine($"Player ({playerId}) sent: {message}");
|
||||
|
||||
await HandleClientMessageAsync(connectionId, socket, message);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[Error:{connectionId}] {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
pingCts.Cancel();
|
||||
WebSockets.TryRemove(connectionId, out _);
|
||||
|
||||
if (SendGates.TryRemove(connectionId, out var gate))
|
||||
gate.Dispose();
|
||||
|
||||
if (PlayerConnections.TryGetValue(playerId, out var connections))
|
||||
{
|
||||
lock (connections)
|
||||
{
|
||||
connections.Remove(connectionId);
|
||||
|
||||
if (connections.Count == 0)
|
||||
PlayerConnections.TryRemove(playerId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
if (socket.State is WebSocketState.Open or WebSocketState.CloseReceived)
|
||||
{
|
||||
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing connection", CancellationToken.None);
|
||||
}
|
||||
|
||||
//Console.WriteLine($"[WebSocket] Player {playerId} disconnected from connection {connectionId}");
|
||||
|
||||
bool lastConnection = !PlayerConnections.ContainsKey(playerId);
|
||||
if (lastConnection)
|
||||
{
|
||||
var offlineHb = PlayerDB.UpdatePlayerHeartbeat(playerId, null, online: false);
|
||||
if (offlineHb != null)
|
||||
{
|
||||
offlineHb.playerId = playerId;
|
||||
var friendIds = FriendsDB.GetRelationships(playerId).Select(r => r.OtherPlayerID).ToList();
|
||||
var hbJson = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.PresenceUpdate, // to show they are offline now
|
||||
Msg = offlineHb
|
||||
});
|
||||
await SendToPlayers(friendIds, hbJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task HandleClientMessageAsync(string connectionId, WebSocket socket, string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(message);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("type", out var typeProp)
|
||||
&& typeProp.ValueKind == System.Text.Json.JsonValueKind.Number
|
||||
&& typeProp.GetInt32() == 1)
|
||||
{
|
||||
string target = root.TryGetProperty("target", out var t) ? t.GetString() ?? string.Empty : string.Empty;
|
||||
string invocationId = root.TryGetProperty("invocationId", out var i) ? i.GetString() ?? string.Empty : string.Empty;
|
||||
|
||||
if (target == "SubscribeToPlayers")
|
||||
{
|
||||
var response = new
|
||||
{
|
||||
type = 3,
|
||||
invocationId = invocationId,
|
||||
result = (object?)null
|
||||
};
|
||||
await SendJsonAsync(connectionId, socket, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[ParseError] {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SendHandshakeAsync(string connectionId, WebSocket socket)
|
||||
{
|
||||
var handshake = new { protocol = "json", version = 1 };
|
||||
await SendJsonAsync(connectionId, socket, handshake);
|
||||
}
|
||||
|
||||
private static async Task PingLoopAsync(string connectionId, WebSocket socket, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested && socket.State == WebSocketState.Open)
|
||||
{
|
||||
var ping = new { type = 6 };
|
||||
await SendJsonAsync(connectionId, socket, ping);
|
||||
await Task.Delay(10000, token);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[PingError] {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendNotificationToPlayer(long playerId, object message)
|
||||
{
|
||||
foreach (var connectionId in SnapshotConnections(playerId))
|
||||
{
|
||||
if (WebSockets.TryGetValue(connectionId, out var socket) && socket.State == WebSocketState.Open)
|
||||
{
|
||||
await SendJsonAsync(connectionId, socket, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendToPlayer(long playerId, string message)
|
||||
{
|
||||
var connectionIds = SnapshotConnections(playerId);
|
||||
if (connectionIds.Length == 0)
|
||||
return;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var connectionId in connectionIds)
|
||||
{
|
||||
if (WebSockets.TryGetValue(connectionId, out var socket) && socket != null)
|
||||
{
|
||||
tasks.Add(SendToSocketAsync(connectionId, socket, message));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static async Task SendRawToPlayer(long playerId, byte[] payload)
|
||||
{
|
||||
var connectionIds = SnapshotConnections(playerId);
|
||||
if (connectionIds.Length == 0)
|
||||
return;
|
||||
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var connectionId in connectionIds)
|
||||
{
|
||||
if (WebSockets.TryGetValue(connectionId, out var socket) && socket != null)
|
||||
{
|
||||
tasks.Add(SendRawToSocketAsync(connectionId, socket, payload));
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static async Task SendToAll(string message)
|
||||
{
|
||||
var tasks = WebSockets
|
||||
.Select(kv => SendToSocketAsync(kv.Key, kv.Value, message));
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static async Task SendToPlayers(List<long> playerIds, string message)
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
|
||||
foreach (var id in playerIds)
|
||||
{
|
||||
foreach (var connectionId in SnapshotConnections(id))
|
||||
{
|
||||
if (WebSockets.TryGetValue(connectionId, out var socket) && socket != null)
|
||||
{
|
||||
tasks.Add(SendToSocketAsync(connectionId, socket, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
public static async Task SendClubMembershipUpdate(long toPlayerId, long clubMemberId, long accountId, long clubId, int membershipType, DateTime createdAt, int invitedMembershipType = 0)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(WebsocketEvents.CreateClubMembershipUpdateResponse(clubMemberId, accountId, clubId, membershipType, createdAt, invitedMembershipType));
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
|
||||
public static async Task BroadcastClubMembershipUpdate(long clubMemberId, long accountId, long clubId, int membershipType, DateTime createdAt, int invitedMembershipType = 0)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(WebsocketEvents.CreateClubMembershipUpdateResponse(clubMemberId, accountId, clubId, membershipType, createdAt, invitedMembershipType));
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendCreatorClubSubscriptionUpdate(long toPlayerId, long creatorAccountId, long clubId, int membershipType)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(WebsocketEvents.CreateCreatorClubSubscriptionUpdateResponse(creatorAccountId, clubId, membershipType));
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
|
||||
public static async Task BroadcastCreatorClubSubscriptionUpdate(long creatorAccountId, long clubId, int membershipType)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(WebsocketEvents.CreateCreatorClubSubscriptionUpdateResponse(creatorAccountId, clubId, membershipType));
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
private static string[] SnapshotConnections(long playerId)
|
||||
{
|
||||
if (!PlayerConnections.TryGetValue(playerId, out var set))
|
||||
return Array.Empty<string>();
|
||||
lock (set) { return set.ToArray(); }
|
||||
}
|
||||
|
||||
private static async Task SendToSocketAsync(string connectionId, WebSocket socket, string message)
|
||||
{
|
||||
var packet = new SockSignalR
|
||||
{
|
||||
type = MessageTypes.Invocation,
|
||||
target = "Notification",
|
||||
arguments = new object[] { message },
|
||||
nonblocking = true,
|
||||
result = "200 OK"
|
||||
};
|
||||
|
||||
var data = Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(packet));
|
||||
var buffer = new byte[data.Length + Separator.Length];
|
||||
Buffer.BlockCopy(data, 0, buffer, 0, data.Length);
|
||||
Buffer.BlockCopy(Separator, 0, buffer, data.Length, Separator.Length);
|
||||
|
||||
await SendBytesAsync(connectionId, socket, buffer, WebSocketMessageType.Text);
|
||||
}
|
||||
|
||||
private static async Task SendRawToSocketAsync(string connectionId, WebSocket socket, byte[] payload)
|
||||
{
|
||||
await SendBytesAsync(connectionId, socket, payload, WebSocketMessageType.Binary);
|
||||
}
|
||||
|
||||
private static async Task SendJsonAsync(string connectionId, WebSocket socket, object obj)
|
||||
{
|
||||
var bytes = Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(obj) + "\x1e");
|
||||
await SendBytesAsync(connectionId, socket, bytes, WebSocketMessageType.Text);
|
||||
}
|
||||
|
||||
private static async Task SendBytesAsync(string connectionId, WebSocket socket, byte[] buffer, WebSocketMessageType type)
|
||||
{
|
||||
var gate = SendGates.GetOrAdd(connectionId, _ => new SemaphoreSlim(1, 1));
|
||||
await gate.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (socket.State != WebSocketState.Open)
|
||||
return;
|
||||
|
||||
await socket.SendAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
type,
|
||||
endOfMessage: true,
|
||||
cancellationToken: CancellationToken.None
|
||||
);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (socket.State != WebSocketState.Closed && socket.State != WebSocketState.Aborted)
|
||||
{
|
||||
await socket.CloseAsync(WebSocketCloseStatus.InternalServerError, "Send failed", CancellationToken.None);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private class SockSignalR
|
||||
{
|
||||
public MessageTypes type { get; set; }
|
||||
public string? target { get; set; }
|
||||
public object[]? arguments { get; set; }
|
||||
public bool nonblocking { get; set; }
|
||||
public object? result { get; set; }
|
||||
}
|
||||
|
||||
private enum MessageTypes
|
||||
{
|
||||
Invocation = 1,
|
||||
StreamItem,
|
||||
Completion,
|
||||
StreamInvocation,
|
||||
CancelInvocation,
|
||||
Ping,
|
||||
Close
|
||||
}
|
||||
|
||||
public static async Task<bool> RefreshAccount(long accountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selfDTO = PlayerDB.GetAccountMe(accountId);
|
||||
var globalDTO = PlayerDB.GetPlayerDTOById(accountId, accountMe: false);
|
||||
|
||||
var selfUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("SelfAccountUpdate", selfDTO));
|
||||
var globalUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("AccountUpdate", globalDTO));
|
||||
|
||||
var notifySelfTask = SendToPlayer(accountId, selfUpdate);
|
||||
var notifyAllTask = SendToAll(globalUpdate);
|
||||
|
||||
await Task.WhenAll(notifySelfTask, notifyAllTask);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> RefreshRoom(long roomId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var room = RoomDB.GetRoom(roomId);
|
||||
|
||||
var globalUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("RoomUpdate", room));
|
||||
|
||||
await SendToAll(globalUpdate);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> RefreshHeartbeat(long playerId, PlayerDBClasses.Heartbeat newHeartbeat)
|
||||
{
|
||||
try
|
||||
{
|
||||
var globalUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("PresenceUpdate", newHeartbeat));
|
||||
|
||||
//<<<<<<< HEAD
|
||||
var friendIds = FriendsDB.GetRelationships(playerId)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.ToList();
|
||||
|
||||
friendIds.Add(playerId);
|
||||
|
||||
await SendToPlayers(friendIds, globalUpdate);
|
||||
/*=======
|
||||
// Scoped to friends: SendToAll here makes every in-room client refresh and dump to dorm.
|
||||
var recipients = FriendsDB.GetRelationships(playerId)
|
||||
.Where(r => r.RelationshipType == Vanadium.Classes.DBs.DBClasses.FriendsDBClasses.RelationshipType.Friend)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.ToList();
|
||||
recipients.Add(playerId);
|
||||
|
||||
await SendToPlayers(recipients, globalUpdate);
|
||||
>>>>>>> 1a0e5b1d7a6cffb81e5d328fc59daaf33d9082f4*/
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> CreateAndSendWSMessageRecieved(
|
||||
long toPlayerId,
|
||||
long fromPlayerId,
|
||||
MessageType type,
|
||||
string? data,
|
||||
long? roomId = null,
|
||||
long? playerEventId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = PlayerDB.AddMessage(
|
||||
toPlayerId,
|
||||
fromPlayerId,
|
||||
type,
|
||||
data,
|
||||
roomId,
|
||||
playerEventId
|
||||
);
|
||||
|
||||
if (message == null)
|
||||
return false;
|
||||
|
||||
var wsEventTo = WebsocketEvents.CreateMessageResponse(message, toPlayerId);
|
||||
var wsEventFrom = WebsocketEvents.CreateMessageResponse(message, fromPlayerId);
|
||||
|
||||
await Task.WhenAll(
|
||||
SendToPlayer(toPlayerId, JsonConvert.SerializeObject(wsEventTo)),
|
||||
SendToPlayer(fromPlayerId, JsonConvert.SerializeObject(wsEventFrom))
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"WS MessageReceived failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> SendBanned(long accountId, PlayerDBClasses.ModerationBlockDetails mbd)
|
||||
{
|
||||
try
|
||||
{
|
||||
mbd.Message = await APIController.FormatModerationBlockMessageString(mbd);
|
||||
var selfUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateBanResponse(mbd));
|
||||
await SendToPlayer(accountId, selfUpdate);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> RefreshHeartbeatsBulk(List<long> playerIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tasks = playerIds.Select(async id =>
|
||||
{
|
||||
var hb = PlayerDB.GetPlayerHeartbeat(id);
|
||||
if (hb == null) return;
|
||||
|
||||
var updateString = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("PresenceUpdate", hb));
|
||||
|
||||
var friendIds = FriendsDB.GetRelationships(id)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.ToList();
|
||||
|
||||
friendIds.Add(id);
|
||||
|
||||
await SendToPlayers(friendIds, updateString);
|
||||
});
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[Notifications] Bulk refresh failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> BalanceUpdate(long accountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selfDTO = PlayerDB.GetPlayerDTOById(accountId, accountMe: true);
|
||||
var selfUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("SelfAccountUpdate", selfDTO));
|
||||
await SendToPlayer(accountId, selfUpdate);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<bool> GameRewardReceived(long accountId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var selfDTO = PlayerDB.GetPlayerDTOById(accountId, accountMe: true);
|
||||
var selfUpdate = JsonConvert.SerializeObject(WebsocketEvents.CreateWSEventString("SelfAccountUpdate", selfDTO));
|
||||
await SendToPlayer(accountId, selfUpdate);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send notification: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendEvent(long playerId, string eventType, object payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = eventType,
|
||||
Msg = payload
|
||||
});
|
||||
|
||||
await SendToPlayer(playerId, message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"Failed to send event: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task SendMaintenance(int minutes)
|
||||
{
|
||||
var safeMinutes = Math.Clamp(minutes, 1, 1440);
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = ((int)WebsocketEvents.ResponseResults.ServerMaintenance).ToString(),
|
||||
Msg = new { StartsInMinutes = safeMinutes }
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendAnnoucementUpdate(AnnoucementDBClasses.Announcement announcement)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "AnnouncementUpdate",
|
||||
Msg = announcement
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendAnnoucementDelete(long announcementId)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "AnnouncementDelete",
|
||||
Msg = new
|
||||
{
|
||||
AnnouncementId = announcementId
|
||||
}
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendCoahMessage(string msg)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = 1,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = 100,
|
||||
Data = msg,
|
||||
RoomId = 1
|
||||
}
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendAccountUpdate(long playerId, object? accountData)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.SelfAccountUpdate,
|
||||
Msg = accountData
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task BroadcastAccountUpdate(long playerId, object? publicDto)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.AccountUpdate,
|
||||
Msg = publicDto
|
||||
});
|
||||
await SendToAll(json);
|
||||
}
|
||||
|
||||
public static async Task SendRoomUpdate(long playerId, object room)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.RoomUpdate,
|
||||
Msg = room
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendPresenceUpdate(long playerId, object heartbeat)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.PresenceUpdate,
|
||||
Msg = heartbeat
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendReputationUpdate(long playerId, object reputation)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new WebsocketEvents.Response
|
||||
{
|
||||
Id = NotiEventTypes.ReputationUpdate,
|
||||
Msg = reputation
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendInvite(long playerId, long fromPlayerId, long roomId, MatchDBClasses.InviteDTO invite)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.GameInviteV2,
|
||||
Data = JsonConvert.SerializeObject(invite),
|
||||
RoomId = roomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await SendToPlayer(playerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendPlayerOnline(long fromPlayerId, MatchDBClasses.PlayerOnlineDTO invite)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.FriendStatusOnline,
|
||||
Data = JsonConvert.SerializeObject(invite),
|
||||
RoomId = (long?)null,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
var relationships = FriendsDB.GetRelationships(fromPlayerId);
|
||||
var friendIds = relationships
|
||||
.Where(r => r.RelationshipType == Vanadium.Classes.DBs.DBClasses.FriendsDBClasses.RelationshipType.Friend)
|
||||
.Select(r => r.OtherPlayerID)
|
||||
.Where(friendId =>
|
||||
{
|
||||
var theirRel = FriendsDB.GetRelationships(friendId)
|
||||
.FirstOrDefault(x => x.OtherPlayerID == fromPlayerId);
|
||||
return theirRel?.Favorited == Vanadium.Classes.DBs.DBClasses.FriendsDBClasses.ReciprocalStatus.Local
|
||||
|| theirRel?.Favorited == Vanadium.Classes.DBs.DBClasses.FriendsDBClasses.ReciprocalStatus.Mutual;
|
||||
})
|
||||
.ToList();
|
||||
await SendToPlayers(friendIds, json);
|
||||
}
|
||||
|
||||
public static async Task SendPartyActivitySwitch(long fromPlayerId, List<long> targetPlayerIds, long roomId, Heartbeat heartbeat)
|
||||
{
|
||||
var invite = new MatchDBClasses.InviteDTO
|
||||
{
|
||||
InviteId = heartbeat?.roomInstance?.roomInstanceId ?? 0,
|
||||
Name = heartbeat?.roomInstance?.Name ?? "Unknown Room",
|
||||
InviteMode = 0
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = WebsocketEvents.WebsocketMessageType.PartyActivitySwitchV2,
|
||||
Data = JsonConvert.SerializeObject(invite),
|
||||
RoomId = roomId,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var pid in targetPlayerIds)
|
||||
await SendToPlayer(pid, json);
|
||||
}
|
||||
|
||||
public static async Task SendPlayerCheer(long toPlayerId, long fromPlayerId, int cheerCategory)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.PlayerCheer,
|
||||
Data = cheerCategory.ToString(),
|
||||
RoomId = (long?)null,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendPlayerCheerAnonymous(long toPlayerId, long fromPlayerId, int cheerCategory)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = MessageType.PlayerCheerAnonymous,
|
||||
Data = cheerCategory.ToString(),
|
||||
RoomId = (long?)null,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
|
||||
public static async Task SendMessage(long toPlayerId, long fromPlayerId, int type, string data)
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(new
|
||||
{
|
||||
Id = "2",
|
||||
Msg = new
|
||||
{
|
||||
FromPlayerId = fromPlayerId,
|
||||
Id = Random.Shared.Next(1, 0x7ffffff),
|
||||
SentTime = DateTime.UtcNow,
|
||||
Type = type,
|
||||
Data = data,
|
||||
RoomId = (long?)null,
|
||||
PlayerEventId = (long?)null
|
||||
}
|
||||
});
|
||||
await SendToPlayer(toPlayerId, json);
|
||||
}
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
[Route("api/messages/v2")] // request to join player
|
||||
public class MessagesController : Controller
|
||||
{
|
||||
[HttpPost("send")]
|
||||
public async Task<IActionResult> SendMessage(
|
||||
[FromForm] long ToPlayerId,
|
||||
[FromForm] int Type,
|
||||
[FromForm] string Data = "")
|
||||
{
|
||||
var id = AuthStuff.GetPlayerId(Request);
|
||||
if (id == null)
|
||||
return Unauthorized("");
|
||||
|
||||
await NotificationsController.SendMessage(ToPlayerId, id.Value, Type, Data);
|
||||
|
||||
return Ok(new { success = true });
|
||||
}
|
||||
}
|
||||
|
||||
public class NotificationStartupHeartbeatCleanupService : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
NotificationsController.ReconcileStartupOnlineHeartbeats(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//Console.WriteLine($"[Notifications] Startup heartbeat reconcile failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user