388 lines
16 KiB
C#
388 lines
16 KiB
C#
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<SocketInteractionContext>
|
|
{
|
|
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<T>
|
|
{
|
|
[JsonRequired]
|
|
public required List<T> 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<string> 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<RecNetResult<CurrentSave2023AndNewer>>(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<AssetBundleVersion>().Cast<int>().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<Unity2023AndNewer>(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<RoomData2023AndNewer>(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<RecNetResult<CAvatarItemDto>>(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}");
|
|
}
|
|
}
|
|
}
|
|
} |