using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Storefront; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Config; using RadicadeServer.RecNet.Keepsakes; using RadicadeServer.RecNet.Rooms; using RadicadeServer.Utils; namespace RadicadeServer.API.Compatibility; [ApiController] public sealed class CompatibilityAPI(RecDBContext db): ControllerBase { [HttpGet("/api/progressionEvents/active")] public IActionResult ActiveProgressionEvents() { if (!KeepsakeFeature.Enabled) return NoActiveEvent(); var keepsakeRooms = KeepsakeRoomConfig.Current; if (!keepsakeRooms.Any || !keepsakeRooms.EventEnabled) return NoActiveEvent(); if (string.Equals(Environment.GetEnvironmentVariable("RADICADE_PROGRESSION_ACTIVE"), "record", StringComparison.OrdinalIgnoreCase)) return Ok(EventRecord(keepsakeRooms)); return Ok(keepsakeRooms.EventId); } private ContentResult NoActiveEvent() { return Content("null", "application/json"); } [HttpGet("/api/progressionEvents/event/{eventId:long}")] public IActionResult ProgressionEventDefinition(long eventId) => EventById(eventId, "event"); [HttpGet("/api/progressionEvents/record/{eventId:long}")] public IActionResult ProgressionEventRecord(long eventId) => EventById(eventId, "record"); [HttpGet("/api/progressionEvents/{eventId:long}")] public IActionResult ProgressionEvent(long eventId) => EventById(eventId, "event"); private IActionResult EventById(long eventId, string route) { if (!KeepsakeFeature.Enabled) return NotFound(); var keepsakeRooms = KeepsakeRoomConfig.Current; if (!keepsakeRooms.Any || !keepsakeRooms.EventEnabled || eventId != keepsakeRooms.EventId) return NotFound(); return route == "record" ? Ok(EventRecord(keepsakeRooms, includeDefinitionCopy: true)) : Ok(EventDefinition(keepsakeRooms)); } private object EventRecord(KeepsakeRoomConfig keepsakeRooms) => EventRecord(keepsakeRooms, includeDefinitionCopy: false); private object EventRecord(KeepsakeRoomConfig keepsakeRooms, bool includeDefinitionCopy) { if (!includeDefinitionCopy) return EventDefinition(keepsakeRooms); var accountId = HttpContext?.User?.GetUserId() ?? 0; var categories = db == null ? new List() : db.KeepsakeCollectionRecords .Where(r => r.AccountId == accountId).Select(r => r.KeepsakeCategoryId).ToList(); return new { _route = "record", AccountId = accountId, Xp = categories.Sum(id => KeepsakeCatalog.Find(id)?.XpValue ?? 0), RewardsCollected = 0, BonusRewardsCollected = 0, GameMinutesToday = 0, XpBoostLastPurchasedAt = (DateTime?)null, }; } private object EventDefinition(KeepsakeRoomConfig keepsakeRooms) { var now = DateTime.UtcNow; var end = now.AddYears(1); return new { _route = "event", ProgressionEventId = keepsakeRooms.EventId, Name = keepsakeRooms.EventName, IsEnabled = true, StartTime = now.AddDays(-30), EndTime = end, CollectionEndTime = end, Rewards = Array.Empty(), KeepsakeRoomLists = EventKeepsakeRoomLists(keepsakeRooms), }; } private static string KeepsakeListMode() => string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_LISTS"), "empty", StringComparison.OrdinalIgnoreCase) ? "no" : "all"; private object EventKeepsakeRoomLists(KeepsakeRoomConfig keepsakeRooms) => string.Equals(Environment.GetEnvironmentVariable("RADICADE_KEEPSAKE_EVENT_LISTS"), "empty", StringComparison.OrdinalIgnoreCase) ? Array.Empty() : keepsakeRooms.RoomListEntries(keepsakeRooms.AllRooms && db != null ? db.Rooms.Select(r => r.RoomId).OrderBy(id => id).ToArray() : keepsakeRooms.ConfiguredRooms); [HttpGet("/commerce/purchasecampaign/allcurrent/v2")] public IActionResult CurrentPurchaseCampaigns() => Ok(Array.Empty()); [HttpGet("/api/apple/musicpromotion/active")] public IActionResult AppleMusicPromotionActive() => Ok(false); [HttpGet("/accounts/emojiConfig/whitelistedEmojis")] public IActionResult WhitelistedEmojis() => Ok(ProfileEmojis); private static readonly string[] ProfileEmojis = [ "๐Ÿ˜€", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜", "๐Ÿ˜†", "๐Ÿ˜…", "๐Ÿ˜‚", "๐Ÿคฃ", "๐Ÿ˜Š", "๐Ÿ™‚", "๐Ÿ™ƒ", "๐Ÿ˜‰", "๐Ÿ˜", "๐Ÿฅฐ", "๐Ÿ˜˜", "๐Ÿ˜Ž", "๐Ÿค“", "๐Ÿคฉ", "๐Ÿฅณ", "๐Ÿค”", "๐Ÿ˜ด", "๐Ÿ˜ญ", "๐Ÿ˜ฑ", "๐Ÿ˜‡", "๐Ÿค ", "๐Ÿค–", "๐Ÿ‘ป", "๐Ÿ‘ฝ", "๐Ÿ‘พ", "๐Ÿ’€", "๐Ÿ˜บ", "๐Ÿ˜ธ", "๐Ÿ˜น", "๐Ÿ˜ป", "๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ‘‹", "๐Ÿ‘", "๐Ÿ‘", "๐Ÿ™Œ", "๐Ÿ’ช", "๐Ÿค", "๐Ÿค˜", "๐Ÿค™", "โœŒ๏ธ", "โค๏ธ", "๐Ÿงก", "๐Ÿ’›", "๐Ÿ’š", "๐Ÿ’™", "๐Ÿ’œ", "๐Ÿ–ค", "๐Ÿค", "๐ŸคŽ", "๐Ÿ’•", "๐Ÿ’–", "๐Ÿ’ฏ", "โœจ", "โญ", "๐ŸŒŸ", "๐Ÿ”ฅ", "๐ŸŒˆ", "โ˜€๏ธ", "๐ŸŒ™", "โšก", "โ„๏ธ", "๐ŸŒŠ", "๐Ÿ€", "๐ŸŒป", "๐ŸŒธ", "๐ŸŒน", "๐ŸŒต", "๐ŸŒฒ", "๐Ÿถ", "๐Ÿฑ", "๐Ÿญ", "๐Ÿฐ", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿธ", "๐Ÿต", "๐Ÿฆ", "๐Ÿฏ", "๐Ÿจ", "๐Ÿท", "๐Ÿง", "๐Ÿข", "๐Ÿ™", "๐Ÿฆ‹", "๐Ÿ", "๐Ÿฆ„", "๐Ÿฒ", "๐ŸŽ", "๐Ÿ‰", "๐Ÿ“", "๐Ÿ’", "๐Ÿ", "๐Ÿฅ‘", "๐Ÿ•", "๐Ÿ”", "๐ŸŸ", "๐ŸŒฎ", "๐Ÿฟ", "๐Ÿฉ", "๐Ÿช", "๐ŸŽ‚", "๐Ÿฆ", "โ˜•", "โšฝ", "๐Ÿ€", "๐Ÿˆ", "โšพ", "๐ŸŽพ", "๐ŸŽฏ", "๐ŸŽฎ", "๐ŸŽฒ", "๐Ÿ†", "๐ŸŽจ", "๐ŸŽญ", "๐ŸŽต", "๐ŸŽธ", "๐Ÿฅ", "๐ŸŽค", "๐ŸŽง", "๐Ÿš€", "๐Ÿ›ธ", "โœˆ๏ธ", "๐Ÿš—", "๐Ÿšฒ", "โ›ต", "๐Ÿ ", "๐Ÿฐ", "๐Ÿ๏ธ", "๐ŸŽก", "๐Ÿ’Ž", "๐Ÿ‘‘", "๐ŸŽ", "๐ŸŽˆ", "๐ŸŽ‰", "๐ŸŽŠ", "๐Ÿ’ก", "๐Ÿ“š", "๐Ÿ“ท", "๐Ÿ”‘", "๐Ÿงฉ", "๐Ÿงธ", "๐Ÿ", "๐Ÿณ๏ธโ€๐ŸŒˆ" ]; [HttpGet("/commerce/reminder/currentTokenBundles/v2")] public IActionResult CurrentTokenBundles() => Ok(Array.Empty()); [HttpPost("/econ/api/freegifts/v1/sendmultiple")] [HttpPost("/api/freegifts/v1/sendmultiple")] [Authorize] public async Task SendFreeGifts() { var senderId = User.GetUserId(); if (senderId is not > 0) return Unauthorized(); var request = await ReadFreeGiftRequestAsync(); var recipients = request.Recipients.Where(id => id > 0).Distinct().ToList(); if (recipients.Count == 0) return FreeGiftResult(false, "No recipients were given."); if (recipients.Count > FriendotronService.MaxRecipients) return FreeGiftResult(false, $"A friend box can be sent to {FriendotronService.MaxRecipients} players at a time."); if (recipients.Contains(senderId.Value) && !await SelfGiftingAllowedAsync()) return FreeGiftResult(false, "You cannot send a friend box to yourself."); var known = await db.Players.AsNoTracking() .Where(player => recipients.Contains(player.AccountId)) .Select(player => player.AccountId) .ToListAsync(); var missing = recipients.FirstOrDefault(id => !known.Contains(id)); if (missing != 0) return FreeGiftResult(false, $"Player {missing} does not exist."); var lastSent = await LastFriendBoxAsync(senderId.Value); if (lastSent != null && DateTime.UtcNow - lastSent.Value < FriendotronService.Cooldown) return FreeGiftResult(false, $"You can send another friend box in {FriendotronService.FormatRemaining(DateTime.UtcNow, lastSent.Value)}."); var boxes = new List<(long Recipient, Gift Box)>(); foreach (var recipient in recipients) { var item = FriendotronService.RollConsumable(DBConfig.DefaultConsumableItems, out var starTier); if (item == null) return FreeGiftResult(false, "No consumable is configured for the friend box."); boxes.Add((recipient, FriendotronService.BuildBox(senderId.Value, request.Message, item, starTier))); } await db.Database.CreateExecutionStrategy().ExecuteAsync(async () => { await using var transaction = db.Database.IsRelational() ? await db.Database.BeginTransactionAsync() : null; foreach (var (recipient, box) in boxes) await StorefrontShared.PersistGiftBoxAsync(db, recipient, box, notify: false); await StampFriendBoxAsync(senderId.Value); if (transaction != null) await transaction.CommitAsync(); }); foreach (var (recipient, box) in boxes) await StorefrontShared.SendGiftPackageNotificationAsync(recipient, box); return FreeGiftResult(true, string.Empty); } private static OkObjectResult FreeGiftResult(bool success, string error) => new(new Dictionary { ["ALOCDGMHFKA"] = success, ["FBILGKFKFJE"] = error }); private sealed class FreeGiftRequest { public List Recipients { get; } = new(); public string? Message { get; set; } } private async Task ReadFreeGiftRequestAsync() { var request = new FreeGiftRequest(); if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds", "players" }) foreach (var value in form[key]) AddRecipients(request, value); request.Message = FirstFilled(form["Message"], form["message"]); return request; } string raw; using (var reader = new StreamReader(Request.Body)) raw = await reader.ReadToEndAsync(); if (!string.IsNullOrWhiteSpace(raw)) { try { if (JsonNode.Parse(raw) is JsonObject body) { foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds", "players" }) { var node = body[key]; if (node is JsonArray array) foreach (var entry in array) AddRecipients(request, entry?.ToString()); else if (node != null) AddRecipients(request, node.ToString()); } request.Message = FirstFilled(Str(body, "Message"), Str(body, "message")); } } catch (JsonException) { } } foreach (var key in new[] { "ToPlayerIds", "toPlayerIds", "PlayerIds", "playerIds" }) foreach (var value in Request.Query[key]) AddRecipients(request, value); request.Message ??= FirstFilled(Request.Query["Message"], Request.Query["message"]); return request; } private static void AddRecipients(FreeGiftRequest request, string? value) { if (string.IsNullOrWhiteSpace(value)) return; foreach (var part in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) if (long.TryParse(part, out var id)) request.Recipients.Add(id); } private static string? FirstFilled(params string?[] values) => values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)); private async Task SelfGiftingAllowedAsync() { var row = await db.GameConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "Friendotron_AllowSelfGifting"); if (row == null || string.IsNullOrWhiteSpace(row.Value)) return true; return !bool.TryParse(row.Value.Trim(), out var allowed) || allowed; } private async Task LastFriendBoxAsync(long accountId) { var key = FriendotronService.CooldownKey(accountId); var row = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == key); if (row == null) return null; return DateTime.TryParse(row.Value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var stamp) ? stamp : null; } private async Task StampFriendBoxAsync(long accountId) { var key = FriendotronService.CooldownKey(accountId); var value = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); var row = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == key); if (row == null) db.ServerConfig.Add(new ServerConfigVal(key, value)); else row.Value = value; await db.SaveChangesAsync(); } [HttpPut("/gamelogs/reports/{reportId}/userinfo")] public IActionResult ReportUserInfo(string reportId) => Ok(); [HttpDelete("/auth/cachedlogin/current"), Authorize] public async Task DeleteCurrentCachedLogin() { var accountId = User.GetUserId(); var platformId = User.GetPlatId(); var rawPlatform = User.FindFirst("rn.plat")?.Value; if (accountId is not > 0 || string.IsNullOrWhiteSpace(platformId) || !int.TryParse(rawPlatform, out var platformValue) || !Enum.IsDefined(typeof(RadicadeServer.RecNet.Enums.Players.Platforms), platformValue) || platformValue == (int)RadicadeServer.RecNet.Enums.Players.Platforms.All) { return Ok(); } var cachedLogins = await db.CachedLogins .Where(login => login.accountId == accountId.Value && login.platform == (RadicadeServer.RecNet.Enums.Players.Platforms)platformValue && login.platformId == platformId) .ToListAsync(); if (cachedLogins.Count > 0) { db.CachedLogins.RemoveRange(cachedLogins); await db.SaveChangesAsync(); } return Ok(); } [HttpPost("/roomcomments/comments/create/{roomId:long}"), Authorize] public async Task CreateRoomComment(long roomId) { var authorId = User.GetUserId(); if (authorId == null) return Unauthorized(new { success = false, error = "Unauthorized" }); var body = await ReadCommentBodyAsync(); var text = body.Text?.Trim(); if (string.IsNullOrWhiteSpace(text)) { return BadRequest(new { success = false, error = "Comment is required" }); } var comment = new RoomComment { RoomId = roomId, CreatorAccountId = authorId.Value, Comment = text, CommentType = string.IsNullOrWhiteSpace(body.Type) ? "Feedback" : body.Type!.Trim(), PositionX = body.X, PositionY = body.Y, PositionZ = body.Z, CreatedAt = DateTime.UtcNow }; await db.RoomComments.AddAsync(comment); await db.SaveChangesAsync(); var roomExists = await db.Rooms.AnyAsync(x => x.RoomId == roomId); return Ok(new { success = true, message = string.Empty, comment_id = comment.Id }); } private sealed class CommentBody { public string? Text { get; set; } public string? Type { get; set; } public float? X { get; set; } public float? Y { get; set; } public float? Z { get; set; } } private async Task ReadCommentBodyAsync() { var result = new CommentBody(); var query = Request.Query; result.Text = FirstNonEmpty(query["comment"], query["Comment"], query["commentText"], query["CommentText"], query["text"], query["body"]); result.Type = FirstNonEmpty(query["commentType"], query["CommentType"], query["type"]); result.X = TryFloat(query["x"], query["positionX"], query["PositionX"]); result.Y = TryFloat(query["y"], query["positionY"], query["PositionY"]); result.Z = TryFloat(query["z"], query["positionZ"], query["PositionZ"]); var formKeys = string.Empty; if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); formKeys = string.Join(",", form.Keys); result.Text = FirstNonEmpty(result.Text, FirstNonEmpty(form["comment"], form["Comment"], form["commentText"], form["text"], form["body"], form["message"], form["Message"], form["content"], form["Content"])); result.Type = FirstNonEmpty(result.Type, FirstNonEmpty(form["commentType"], form["CommentType"], form["type"])); result.X ??= TryFloat(FirstNonEmpty(form["x"], form["positionX"], form["PositionX"])); result.Y ??= TryFloat(FirstNonEmpty(form["y"], form["positionY"], form["PositionY"])); result.Z ??= TryFloat(FirstNonEmpty(form["z"], form["positionZ"], form["PositionZ"])); } var raw = string.Empty; if (!Request.HasFormContentType && string.IsNullOrWhiteSpace(result.Text)) { using var reader = new StreamReader(Request.Body); raw = await reader.ReadToEndAsync(); } if (string.IsNullOrWhiteSpace(raw)) return result; try { var node = JsonNode.Parse(raw); if (node is JsonValue value && value.TryGetValue(out var bare)) { result.Text = FirstNonEmpty(result.Text, bare); return result; } if (node is not JsonObject obj) return result; result.Text = FirstNonEmpty(result.Text, FirstNonEmpty( Str(obj, "Comment"), Str(obj, "comment"), Str(obj, "CommentText"), Str(obj, "commentText"), Str(obj, "Text"), Str(obj, "text"), Str(obj, "Message"), Str(obj, "message"), Str(obj, "Content"), Str(obj, "content"))); result.Type = FirstNonEmpty(result.Type, FirstNonEmpty(Str(obj, "CommentType"), Str(obj, "commentType"), Str(obj, "Type"), Str(obj, "type"))); var position = obj["Position"] ?? obj["position"]; if (position is JsonObject pos) { result.X ??= TryFloat(Str(pos, "x"), Str(pos, "X")); result.Y ??= TryFloat(Str(pos, "y"), Str(pos, "Y")); result.Z ??= TryFloat(Str(pos, "z"), Str(pos, "Z")); } else { result.X ??= TryFloat(Str(obj, "x"), Str(obj, "positionX")); result.Y ??= TryFloat(Str(obj, "y"), Str(obj, "positionY")); result.Z ??= TryFloat(Str(obj, "z"), Str(obj, "positionZ")); } } catch (JsonException) { result.Text = raw.Trim(); } return result; } private static string? Str(JsonObject obj, string key) { var node = obj[key]; if (node == null) return null; try { if (node is JsonValue value && value.TryGetValue(out var text)) return text; return node.ToJsonString().Trim('"'); } catch { return null; } } private static string? FirstNonEmpty(params string?[] values) => values.FirstOrDefault(v => !string.IsNullOrWhiteSpace(v)); private static float? TryFloat(params string?[] values) { foreach (var value in values) if (float.TryParse(value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var parsed)) return parsed; return null; } }