2772 lines
108 KiB
C#
2772 lines
108 KiB
C#
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 { }
|
|
}
|
|
}
|
|
} |