using DeluxeBackend.Controllers; using DeluxeBackend.Models; using DeluxeBackend.Services; using Discord; using Discord.Interactions; using Discord.WebSocket; using Microsoft.AspNetCore.Http.HttpResults; using SkiaSharp; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Reflection.Metadata; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; using static DeluxeBackend.Controllers.Api.CustomAvatarItemsController; using static DeluxeBackend.Discord.Commands.Debug; using static DeluxeBackend.Discord.Commands.ImportStuff; using static DeluxeBackend.Enums; using static DeluxeBackend.Extensions.RoomExtensions; namespace DeluxeBackend.Discord.Commands { public class ImportStuff(ILiteDbService db, ICdnService cdn) : InteractionModuleBase { private readonly DateTime _saveCutoffDate = new(2023, 6, 21); private readonly JsonSerializerOptions _jsonOptions = new() { PropertyNameCaseInsensitive = true }; private bool IsAdmin() { return Context.User is SocketGuildUser guildUser && guildUser.Roles.Any(r => r.Id == Debug.AdminRole); } public class RecNetResult { [JsonRequired] public required List Results { get; set; } [JsonRequired] public int TotalResults { get; set; } = 0; } [SlashCommand("import_room", ".")] public async Task importRoom(IAttachment file) { await DeferAsync(); if (!IsAdmin()) { await FollowupAsync("No"); return; } try { Account account = db.Accounts.FindOne(x => x.Username == "Import"); if (account == null) { account = new() { Username = "Import", DisplayName = "Import", Birthday = DateOnly.MinValue, ImageName = "DefaultProfileImage", IsRecentHistoryVisible = false, }; account.Roles.AddRange(["developer", "keepsake", "livekeepsakeeventroomsaveoverride", "betaroomcurrencycreator", "multiinstanceevent"]); db.Accounts.Insert(account); } using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ServerConfig.RRToken}"); HttpResponseMessage testreq = await httpClient.GetAsync("https://accounts.rec.net/account/me"); if (testreq.StatusCode != HttpStatusCode.OK) { await FollowupAsync($"Error: Failed to authenticate with rec.net. (Status: {testreq.StatusCode})"); return; } var textData = await httpClient.GetStringAsync(file.Url); List logs = []; int successCount = 0; async Task ImportSave(SubRoom subroom, SubRoomData2023AndNewer subRoom2) { if (subRoom2.CurrentSave == null) return; CurrentSave2023AndNewer? save = null; if (subRoom2.CurrentSave.CreatedAt >= _saveCutoffDate) { HttpResponseMessage savesReq = await httpClient.GetAsync($"https://rooms.rec.net/rooms/{subRoom2.RoomId}/subrooms/{subRoom2.SubRoomId}/saves?skip=0&take=1000"); if (savesReq.StatusCode == HttpStatusCode.OK) { var rawSavesJson = await savesReq.Content.ReadAsStringAsync(); var savesResult = JsonSerializer.Deserialize>(rawSavesJson, _jsonOptions); save = savesResult?.Results? .Where(s => s != null && s.CreatedAt < _saveCutoffDate && !string.IsNullOrEmpty(s.DataBlob)) .OrderByDescending(s => s.CreatedAt) .FirstOrDefault(); } } else { save = subRoom2.CurrentSave; } if (save == null) { logs.Add($"`{subroom.Name}`: Room has no compatible classic save file."); return; } SubRoomSave newSave = new() { SubRoomId = subroom.Id, DataBlob = save.DataBlob, SavedByAccountId = account.Id, Description = $"Imported by @{Context.User.Username}" }; if (save.UnityAssetId.HasValue) { var assetTypes = new[] { AssetBundleType.WindowsDesktop, AssetBundleType.MobileAndroid }; var distinctVersions = Enum.GetValues().Cast().Distinct(); foreach (var assetType in assetTypes) { foreach (var versionId in distinctVersions) { try { var assetUrl = $"https://rooms.rec.net/unity_assets/{save.UnityAssetId.Value}/{(int)assetType}/{versionId}"; var jsonString = await httpClient.GetStringAsync(assetUrl); Unity2023AndNewer? u = JsonSerializer.Deserialize(jsonString, _jsonOptions); if (u == null) continue; UnityAssets unityAssets = db.UnityAssets.FindOne(x => x.Id == save.UnityAssetId.Value) ?? new UnityAssets { Id = save.UnityAssetId.Value }; if (!unityAssets.Assets.Any(a => a.Filename == u.Filename && a.Target == u.Target && a.Version == u.Version)) { unityAssets.Assets.Add(new UnityAsset { Filename = u.Filename, Target = u.Target, Version = u.Version }); db.UnityAssets.Upsert(unityAssets); } newSave.UnityAssetId = save.UnityAssetId.Value.ToString(); } catch { } } } } db.SubRoomSave.Insert(newSave); subroom.CurrentSave = newSave; db.SubRooms.Update(subroom); } async Task import(string inputLine) { string roomName = inputLine; if (roomName.Contains("rec.net/room/")) { int markerIndex = roomName.IndexOf("rec.net/room/") + "rec.net/room/".Length; roomName = roomName.Substring(markerIndex).Split('/')[0].Trim(); } if (string.IsNullOrWhiteSpace(roomName)) return; HttpResponseMessage roomData = await httpClient.GetAsync($"https://rooms.rec.net/rooms/?name={roomName}&include=1325"); if (roomData.StatusCode == HttpStatusCode.NotFound) { logs.Add($"`{roomName}`: Not found on rec.net"); return; } roomData.EnsureSuccessStatusCode(); string rawJson = await roomData.Content.ReadAsStringAsync(); RoomData2023AndNewer? room = JsonSerializer.Deserialize(rawJson, _jsonOptions); if (room == null) { logs.Add($"`{roomName}`: Failed to parse room metadata"); return; } if (room.Accessibility == RoomAccessibility.Private) { logs.Add($"`{roomName}`: Room is private."); return; } if (room.CreatedAt >= _saveCutoffDate) { logs.Add($"`{roomName}`: Room metadata is too new (Created: {room.CreatedAt:yyyy-MM-dd})."); return; } if (db.Rooms.FindOne(x => x.Name.Equals(roomName, StringComparison.InvariantCultureIgnoreCase)) != null) { logs.Add($"`{roomName}`: Already exists in the database."); return; } Room newRoom = new() { Accessibility = RoomAccessibility.Public, Name = $"{room.Name}", Description = room.Description, ImageName = room.ImageName, CloningAllowed = false, Creator = account, CustomWarning = room.CustomWarning, WarningMask = room.WarningMask, IsDorm = room.Name == "DormRoom", IsRRO = false, }; RoomSupports roomSup = RoomSupports.None; if (room.SupportsScreens) roomSup |= RoomSupports.Screens; if (room.SupportsWalkVR) roomSup |= RoomSupports.WalkVR; if (room.SupportsTeleportVR) roomSup |= RoomSupports.TeleportVR; roomSup |= RoomSupports.Juniors; newRoom.SupportedPlayerTypes = roomSup; db.Rooms.Insert(newRoom); foreach (SubRoomData2023AndNewer scene in room.SubRooms) { SubRoom newScene = new() { Room = newRoom, LocationId = scene.UnitySceneId, Name = scene.Name, IsSandbox = scene.IsSandbox, MaxPlayers = scene.MaxPlayers, CanMatchmakeInto = scene.Accessibility == RoomAccessibility.Public }; db.SubRooms.Insert(newScene); await ImportSave(newScene, scene); } logs.Add($"`{roomName}`: Successfully imported."); successCount++; } var lines = textData.Split(["\r\n", "\n"], StringSplitOptions.RemoveEmptyEntries); foreach (string line in lines) { string cleanLine = line.Trim(); if (!string.IsNullOrEmpty(cleanLine)) { await import(cleanLine); } } string summary = $"**Import Summary:** {successCount} room(s) imported successfully.\n\n" + string.Join("\n", logs); if (summary.Length > 2000) { summary = summary.Substring(0, 1950) + "\n*...Truncated due to Discord length limits.*"; } await FollowupAsync(summary); } catch (Exception ex) { await FollowupAsync($"Error (Exception): {ex.Message}"); } } [SlashCommand("import_custom_avatar_item", ".")] public async Task importCustomAvatarItem(ulong RecNetPlayerId, ulong DeluxeId) { using var httpClient = new HttpClient(); using var noAuthHttpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {ServerConfig.RRToken}"); HttpResponseMessage testreq = await httpClient.GetAsync("https://accounts.rec.net/account/me"); if (testreq.StatusCode != HttpStatusCode.OK) { await FollowupAsync($"Error: Failed to authenticate with rec.net. (Status: {testreq.StatusCode})"); return; } await DeferAsync(); if (!IsAdmin()) { await FollowupAsync("No"); return; } Account? importAccount = db.Accounts.FindById(DeluxeId); if (importAccount == null) { await FollowupAsync($"Error: No account found with Deluxe ID {DeluxeId}."); return; } try { HttpResponseMessage savesReq = await httpClient.GetAsync($"https://api.rec.net/api/customAvatarItems/v2/fromCreator/{RecNetPlayerId}?take=1000"); savesReq.EnsureSuccessStatusCode(); var Result = JsonSerializer.Deserialize>(await savesReq.Content.ReadAsStringAsync(), _jsonOptions); if (Result == null || Result.Results.Count == 0) { await FollowupAsync("No custom avatar items found for that user."); return; } foreach (var item in Result.Results) { if (item != null && !string.IsNullOrEmpty(item.DesignFilename)) { if (item.BaseAvatarItemId != null && item.BaseAvatarItemId != 2184) { } using var response = await noAuthHttpClient.GetAsync($"https://cdn.rec.net/img/{item.DesignFilename}"); response.EnsureSuccessStatusCode(); using var stream = await response.Content.ReadAsStreamAsync(); string? remotePath = await cdn.UploadFile(stream, "img"); if (string.IsNullOrEmpty(remotePath)) { await FollowupAsync($"Failed to upload design image for item `{item.Name}`. Skipping."); continue; } using var response2 = await noAuthHttpClient.GetAsync($"https://cdn.rec.net/img/{item.ThumbnailImageFilename}"); response2.EnsureSuccessStatusCode(); using var stream2 = await response2.Content.ReadAsStreamAsync(); string? remotePath2 = await cdn.UploadFile(stream2, "img"); if (string.IsNullOrEmpty(remotePath2)) { await FollowupAsync($"Failed to upload thumbnail image for item `{item.Name}`. Skipping."); continue; } var customAvatarItem = new CustomAvatarItem { Id = item.CustomAvatarItemId, Accessibility = item.Accessibility, Creator = importAccount, Name = item.Name, Description = item.Description, Price = item.Price, BaseAvatarItemId = item.BaseAvatarItemId, BaseAvatarItemColor = ColorTranslator.FromHtml(item.BaseAvatarItemColor), DesignFilename = remotePath, ThumbnailImageFilename = remotePath2, PreviewOrientation = item.PreviewOrientation }; db.CustomAvatarItems.Insert(customAvatarItem); }; } await FollowupAsync($"Successfully imported {Result.Results.Count} custom avatar item(s)."); } catch (Exception ex) { await FollowupAsync($"Error (Exception): {ex.Message}"); } } } }