mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-25 23:18:56 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class CloneRequest
|
||||
{
|
||||
public string Name { get; set; } = null!;
|
||||
public long RoomId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class ActualRoomRole
|
||||
{
|
||||
public long AccountId { get; set; }
|
||||
public RoleType InvitedRole { get; set; }
|
||||
public RoleType Role { get; set; }
|
||||
|
||||
public ActualRoomRole(RoomRole role)
|
||||
{
|
||||
AccountId = role.AccountId;
|
||||
InvitedRole = role.InvitedRole;
|
||||
Role = role.Role;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,828 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet;
|
||||
using RadicadeServer.RecNet.Config;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
|
||||
namespace RadicadeServer.Utils
|
||||
{
|
||||
public static class DefaultRooms
|
||||
{
|
||||
private const string AlignmentKey = "DefaultRooms.AlignmentVersion";
|
||||
|
||||
private const int AlignmentVersion = 1;
|
||||
|
||||
private const string RroCorrectionKey = "DefaultRooms.RroCorrectionVersion";
|
||||
|
||||
private const string RecCenterRepairKey = "DefaultRooms.RecCenterRepairVersion";
|
||||
|
||||
private const int RecCenterRepairVersion = 1;
|
||||
|
||||
private const string RecCenterRepairRoom = "RecCenter";
|
||||
|
||||
private const string CloningCorrectionKey = "DefaultRooms.NoCloneVersion";
|
||||
|
||||
private const int CloningCorrectionVersion = 1;
|
||||
|
||||
private const int RroCorrectionVersion = 2;
|
||||
|
||||
private const string ShippedRoomResetKey = "DefaultRooms.ShippedRoomResetVersion";
|
||||
|
||||
private const int ShippedRoomResetVersion = 3;
|
||||
|
||||
public static IReadOnlyList<DefaultRoom> All { get; } = new List<DefaultRoom>
|
||||
{
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Orientation", Description = "An introductory tour of Rec Room!",
|
||||
MaxPlayers = 1, SceneId = "c79709d8-a31b-48aa-9eb8-cc31ba9505e8",
|
||||
Tags = Rro(), CloningAllowed = false, DisableRoomComments = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "RecCenter", Description = "A social hub to meet and mingle with friends new and old.",
|
||||
MaxPlayers = 12, SceneId = "cbad71af-0831-44d8-b8ef-69edafa841f6",
|
||||
Tags = Rro(), CloningAllowed = false
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "TheRiseofJumbotron", Description = "Robot invaders threaten the galaxy! Team up with your friends and bring the laser heat!",
|
||||
MaxPlayers = 4, SceneId = "acc06e66-c2d0-4361-b0cd-46246a4c455c",
|
||||
Tags = RroWith("quest")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "GoldenTrophy", Description = "The goblin king stole Coach's Golden Trophy. Team up and embark on an epic quest to recover it!",
|
||||
MaxPlayers = 4, SceneId = "91e16e35-f48f-4700-ab8a-a1b79e50e51b",
|
||||
Tags = RroWith("quest")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Crescendo", Description = "Gather your vampire hunting crew, conquer a legendary castle, and restore peace to Rec Room!",
|
||||
MaxPlayers = 3, SceneId = "49cb8993-a956-43e2-86f4-1318f279b22a",
|
||||
Tags = RroWith("quest")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "CrimsonCauldron", Description = "Can your band of adventurers brave the enchanted wilds, and lift the curse of the crimson cauldron?",
|
||||
MaxPlayers = 4, SceneId = "949fa41f-4347-45c0-b7ac-489129174045",
|
||||
Tags = RroWith("quest")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "IsleOfLostSkulls", Description = "Can your pirate crew get to the Isle, defeat its fearsome guardian, and escape with the gold?",
|
||||
MaxPlayers = 3, SceneId = "7e01cfe0-820a-406f-b1b3-0a5bf575235c",
|
||||
Tags = RroWith("quest")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "StuntRunner", Description = "Sprint, climb, and wall jump in this high-speed obstacle course where every second counts!",
|
||||
MaxPlayers = 12, SceneId = "b7281665-a715-4051-826b-8e08e69c6172", SubRoomName = "StuntRunner",
|
||||
Tags = RroWith("sports"), CloningAllowed = false,
|
||||
ExtraSubRooms = new List<DefaultSubRoom>
|
||||
{
|
||||
new() { Name = "TheMainEvent", SceneId = "3a636bd2-f896-424c-9225-c184522c0d87", MaxPlayers = 4 }
|
||||
}
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "3DCharades", Description = "Take turns drawing, acting, and guessing funny phrases with your friends!",
|
||||
MaxPlayers = 8, SceneId = "a673712c-877f-4749-b69a-4a4c6310d545", SubRoomName = "InkSpaceHome",
|
||||
Tags = Rro(), DisableRoomComments = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Legacy3DCharades", Description = "Take turns drawing, acting, and guessing funny phrases with your friends!",
|
||||
MaxPlayers = 8, SceneId = "4078dfed-24bb-4db7-863f-578ba48d726b",
|
||||
Tags = Rro(), DisableRoomComments = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "TheInkSpace", Description = "A cozy event venue with a stage. The entire room is made of props, so customize to your heart's content!",
|
||||
MaxPlayers = 8, SceneId = "1fa06e3c-c307-4c11-a91b-1fabcddb8a96",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true, DisableRoomComments = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Dodgeball", Description = "Throw dodgeballs to knock out your friends in this gym classic!",
|
||||
MaxPlayers = 6, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
|
||||
Tags = RroWith("sports")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "DodgeballVR", Description = "Throw dodgeballs to knock out your friends in this gym classic!",
|
||||
MaxPlayers = 6, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f"
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Bowling", Description = "Hang out with friends, bowl a few games, eat snacks!",
|
||||
MaxPlayers = 8, SceneId = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
|
||||
Tags = RroWith("sports")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "BowlingAlley", Description = "Lone Shoe Lanes is the hottest hangout in town.",
|
||||
MaxPlayers = 8, SceneId = "ae929543-9a07-41d5-8ee9-dbbee8c36800",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Paddleball", Description = "A simple rally game between two players in a plexiglass tube with a zero-g ball.",
|
||||
MaxPlayers = 2, SceneId = "d89f74fa-d51e-477a-a425-025a891dd499",
|
||||
Tags = RroWith("sports")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Soccer", Description = "Teams of three run around slamming themselves into an over-sized soccer ball. Goal!",
|
||||
MaxPlayers = 6, SceneId = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
|
||||
Tags = RroWith("sports")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "DiscGolfLake", Description = "A leisurely stroll through the grass. Throw your disc into the goal. Sounds easy, right?",
|
||||
MaxPlayers = 4, SceneId = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
|
||||
Tags = RroWith("sports")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "DiscGolfPropulsion", Description = "Throw your disc through hazards and around wind machines on this challenging course!",
|
||||
MaxPlayers = 4, SceneId = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
|
||||
Tags = RroWith("sports")
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Paintball", Description = "Red and Blue teams splat each other in capture the flag and team battle.",
|
||||
MaxPlayers = 8, SceneId = "e122fe98-e7db-49e8-a1b1-105424b6e1f0", SubRoomName = "River",
|
||||
Tags = RroWith("sports"),
|
||||
ExtraSubRooms = new List<DefaultSubRoom>
|
||||
{
|
||||
new() { Name = "Homestead", SceneId = "a785267d-c579-42ea-be43-fec1992d1ca7" },
|
||||
new() { Name = "Quarry", SceneId = "ff4c6427-7079-4f59-b22a-69b089420827" },
|
||||
new() { Name = "Clearcut", SceneId = "380d18b5-de9c-49f3-80f7-f4a95c1de161" },
|
||||
new() { Name = "Spillway", SceneId = "58763055-2dfb-4814-80b8-16fac5c85709" },
|
||||
new() { Name = "Drive-in", SceneId = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3" }
|
||||
}
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "LaserTag", Description = "Teams battle each other and waves of robots.",
|
||||
MaxPlayers = 8, SceneId = "239e676c-f12f-489f-bf3a-d4c383d692c3", SubRoomName = "Hangar",
|
||||
Tags = RroWith("pvp"),
|
||||
ExtraSubRooms = new List<DefaultSubRoom>
|
||||
{
|
||||
new() { Name = "CyberJunkCity", SceneId = "9d6456ce-6264-48b4-808d-2d96b3d91038" }
|
||||
}
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "RecRoyaleSquads", Description = "Squads of three battle it out on Frontier Island. Last squad standing wins!",
|
||||
MaxPlayers = 18, SceneId = "253fa009-6e65-4c90-91a1-7137a56a267f", ImageName = "RecRoyaleSquads",
|
||||
Tags = RroWith("pvp"), CloningAllowed = false
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "RecRoyaleSolos", Description = "Battle it out on Frontier Island. Last person standing wins!",
|
||||
MaxPlayers = 16, SceneId = "b010171f-4875-4e89-baba-61e878cd41e1", ImageName = "RecRoyaleSolos",
|
||||
Tags = RroWith("pvp"), CloningAllowed = false, Aliases = new[] { "RecRoyaleSolo" }
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Showdown", Description = "Showdown",
|
||||
MaxPlayers = 16, SceneId = SceneLocations.MakerRoom, ImageName = "Showdown",
|
||||
Tags = new List<ItemTag>
|
||||
{
|
||||
new() { Tag = "rro", Type = TagType.AGOnly },
|
||||
new() { Tag = "games", Type = TagType.General },
|
||||
new() { Tag = "beta", Type = TagType.Auto }
|
||||
},
|
||||
CloningAllowed = true,
|
||||
SaveName = "Showdown.room", BundleName = "Showdown_0_1.assetbundle"
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Lounge", Description = "A low-key lounge to chill with your friends. Great for private parties!",
|
||||
MaxPlayers = 20, SceneId = "a067557f-ca32-43e6-b6e5-daaec60b4f5a",
|
||||
Tags = Rro(), SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Park", Description = "A sprawling park with amphitheater, play fields, and a cave.",
|
||||
MaxPlayers = 20, SceneId = "0a864c86-5a71-4e18-8041-8124e4dc9d98",
|
||||
Tags = Rro(), SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "MakerRoom", Description = "This room is a blank canvas. Make it into whatever you like!",
|
||||
MaxPlayers = 20, SceneId = "a75f7547-79eb-47c6-8986-6767abcb4f92", ImageName = "MakerRoom",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Gym", Description = "A school gymnasium for smaller sporting events.",
|
||||
MaxPlayers = 20, SceneId = "3d474b26-26f7-45e9-9a36-9b02847d5e6f",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Stadium", Description = "A professional stadium for larger sporting events.",
|
||||
MaxPlayers = 20, SceneId = "6d5eea4b-f069-4ed0-9916-0e2f07df0d03",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "River", Description = "The original outdoor paintball course. Simple, balanced, classic.",
|
||||
MaxPlayers = 20, SceneId = "e122fe98-e7db-49e8-a1b1-105424b6e1f0",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Homestead", Description = "A day on the farm. Great for asymmetrical battles!",
|
||||
MaxPlayers = 20, SceneId = "a785267d-c579-42ea-be43-fec1992d1ca7",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Quarry", Description = "The sun sets on this construction site in the desert. Great for sniping battles!",
|
||||
MaxPlayers = 20, SceneId = "ff4c6427-7079-4f59-b22a-69b089420827",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Clearcut", Description = "The sun rises on this logging camp in the forest. Great for mid-range splatting!",
|
||||
MaxPlayers = 20, SceneId = "380d18b5-de9c-49f3-80f7-f4a95c1de161",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Spillway", Description = "Night shift at the hydroelectric plant. Great for stealthy splatting!",
|
||||
MaxPlayers = 20, SceneId = "58763055-2dfb-4814-80b8-16fac5c85709",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Lake", Description = "A leisurely trail to the lake and beyond.",
|
||||
MaxPlayers = 20, SceneId = "f6f7256c-e438-4299-b99e-d20bef8cf7e0",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "PropulsionTestRange", Description = "The science department left some of their equipment laying around. What will you do with it?",
|
||||
MaxPlayers = 20, SceneId = "d9378c9f-80bc-46fb-ad1e-1bed8a674f55",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Hangar", Description = "A late-eighties-style laser tag battle arcade.",
|
||||
MaxPlayers = 20, SceneId = "239e676c-f12f-489f-bf3a-d4c383d692c3",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "CyberJunkCity", Description = "A late-2080s-style cyberpunk dystopian laser tag battle arcade",
|
||||
MaxPlayers = 20, SceneId = "9d6456ce-6264-48b4-808d-2d96b3d91038",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "StuntRunnerBaseRoom", Description = "A soundstage warehouse used to film Stunt Runner and other RecTV shows.",
|
||||
MaxPlayers = 20, SceneId = "882e9b96-7115-4b03-86f6-c0c9d8e22e00",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "Registration", Description = "Registration",
|
||||
MaxPlayers = 1, SceneId = "cf61556d-68fd-4288-9ae5-7a512621e569",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, CloningAllowed = false
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "ARRoom", Description = "Your real room!",
|
||||
MaxPlayers = 4, SceneId = "bf268f5f-b55b-41af-8628-32fa4b5d70b6",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, CloningAllowed = false, SubRoomSandbox = true,
|
||||
WarningMask = WarningMask.Custom
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "DriveIn", Description = "A drive-in theater, diner, and truck stop make this a multi-area paint arena",
|
||||
MaxPlayers = 20, SceneId = "65ddbb48-5a01-4e3e-972d-e5c7419e2bc3",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised, SubRoomSandbox = true
|
||||
},
|
||||
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "RecRally", Description = "A six-player off-road rally race where you boost, bump, and powerslide your way to the finish line!",
|
||||
MaxPlayers = 6, SceneId = "56193568-9ae0-498c-8a77-4df79dec91f5",
|
||||
Tags = RroWith("racing", "driving", "recrally"),
|
||||
WarningMask = WarningMask.IntenseMotion, SupportsTeleportVR = false
|
||||
},
|
||||
new DefaultRoom
|
||||
{
|
||||
Name = "FrontierUGC", Description = "A public copyable room featuring the Rec Royale map!",
|
||||
MaxPlayers = 40, SceneId = "a16bfd31-ffb9-46ac-a199-362c163130c0", ImageName = "RecRoyale",
|
||||
IsRRO = false, Accessibility = Accessibility.Unlised
|
||||
}
|
||||
};
|
||||
|
||||
public static async Task EnsureAsync(RecDBContext db)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ResetRoomsAsync(db);
|
||||
await EnsureCatalogAsync(db);
|
||||
await RemoveStrayRroAsync(db);
|
||||
await DisableCloningAsync(db);
|
||||
await RepairRecCenterAsync(db);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
db.ChangeTracker.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task RepairRecCenterAsync(RecDBContext db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await HasRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion)) return;
|
||||
|
||||
var reference = All.FirstOrDefault(room => Matches(RecCenterRepairRoom, room));
|
||||
if (reference == null)
|
||||
{
|
||||
await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
var rooms = await db.Rooms.ToListAsync();
|
||||
var existing = rooms.FirstOrDefault(room => Matches(room.Name, reference));
|
||||
if (existing == null)
|
||||
{
|
||||
await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
var subRooms = await db.SubRooms.ToListAsync();
|
||||
var changes = await AlignAsync(db, existing, subRooms, reference);
|
||||
|
||||
foreach (var sub in subRooms.Where(sub => sub.RoomId == existing.RoomId))
|
||||
{
|
||||
if (string.IsNullOrEmpty(sub.DataBlob)) continue;
|
||||
changes.Add($"subroom {sub.Name} content reset to the stock scene ({sub.DataBlob.Length} chars discarded)");
|
||||
sub.DataBlob = string.Empty;
|
||||
sub.UnityAssetId = string.Empty;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await MarkRunAsync(db, RecCenterRepairKey, RecCenterRepairVersion);
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
db.ChangeTracker.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ResetRoomsAsync(RecDBContext db)
|
||||
{
|
||||
if (await HasRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion)) return;
|
||||
|
||||
var rooms = await db.Rooms.ToListAsync();
|
||||
if (rooms.Count == 0)
|
||||
{
|
||||
await MarkRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
var ids = rooms.Select(room => room.RoomId).ToList();
|
||||
var subRoomIds = await db.SubRooms.Where(sub => ids.Contains(sub.RoomId)).Select(sub => sub.SubRoomId).ToListAsync();
|
||||
|
||||
db.SubRoomPermissions.RemoveRange(await db.SubRoomPermissions.Where(permission => subRoomIds.Contains(permission.subRoomId)).ToListAsync());
|
||||
db.SubRoomHistory.RemoveRange(await db.SubRoomHistory.Where(history => ids.Contains(history.RoomId)).ToListAsync());
|
||||
db.SubRooms.RemoveRange(await db.SubRooms.Where(sub => ids.Contains(sub.RoomId)).ToListAsync());
|
||||
db.RoomStats.RemoveRange(await db.RoomStats.Where(stats => ids.Contains(stats.RoomId)).ToListAsync());
|
||||
db.RoomRoles.RemoveRange(await db.RoomRoles.Where(role => ids.Contains(role.RoomId)).ToListAsync());
|
||||
db.RoomKeys.RemoveRange(await db.RoomKeys.Where(key => ids.Contains(key.RoomId)).ToListAsync());
|
||||
db.RoomBans.RemoveRange(await db.RoomBans.Where(ban => ids.Contains(ban.roomId)).ToListAsync());
|
||||
db.RoomComments.RemoveRange(await db.RoomComments.Where(comment => ids.Contains(comment.RoomId)).ToListAsync());
|
||||
db.RoomInteractions.RemoveRange(await db.RoomInteractions.Where(interaction => ids.Contains(interaction.RoomId)).ToListAsync());
|
||||
db.RoomInstances.RemoveRange(await db.RoomInstances.Where(instance => ids.Contains(instance.roomId) || subRoomIds.Contains(instance.subRoomId)).ToListAsync());
|
||||
db.LeaderboardScores.RemoveRange(await db.LeaderboardScores.Where(score => ids.Contains(score.RoomId)).ToListAsync());
|
||||
|
||||
var events = await db.PlayerEvents.Where(playerEvent => ids.Contains(playerEvent.RoomId)).ToListAsync();
|
||||
if (events.Count > 0)
|
||||
{
|
||||
var eventIds = events.Select(playerEvent => playerEvent.PlayerEventId).ToList();
|
||||
db.PlayerEventResponses.RemoveRange(await db.PlayerEventResponses.Where(response => eventIds.Contains(response.PlayerEventId)).ToListAsync());
|
||||
db.PlayerEvents.RemoveRange(events);
|
||||
}
|
||||
|
||||
foreach (var image in await db.Images.Where(image => image.RoomId.HasValue && ids.Contains(image.RoomId.Value)).ToListAsync())
|
||||
image.RoomId = null;
|
||||
foreach (var message in await db.Messages.Where(message => message.RoomId.HasValue && ids.Contains(message.RoomId.Value)).ToListAsync())
|
||||
message.RoomId = null;
|
||||
|
||||
db.Rooms.RemoveRange(rooms);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await db.AddDormRoom();
|
||||
await ClearCatalogMarkerAsync(db);
|
||||
await MarkRunAsync(db, ShippedRoomResetKey, ShippedRoomResetVersion);
|
||||
|
||||
var dorm = await db.Rooms.FirstOrDefaultAsync(room => room.IsDorm && room.CreatorAccountId == 1);
|
||||
}
|
||||
|
||||
private static async Task ClearCatalogMarkerAsync(RecDBContext db)
|
||||
{
|
||||
var row = await db.GameConfig.FirstOrDefaultAsync(config => config.Key == AlignmentKey);
|
||||
if (row == null)
|
||||
await db.GameConfig.AddAsync(new ConfigVal(AlignmentKey, "0"));
|
||||
else
|
||||
row.Value = "0";
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task EnsureCatalogAsync(RecDBContext db)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await IsAlignedAsync(db)) return;
|
||||
|
||||
var rooms = await db.Rooms.ToListAsync();
|
||||
var subRooms = await db.SubRooms.ToListAsync();
|
||||
var statsRoomIds = await db.RoomStats.Select(x => x.RoomId).ToListAsync();
|
||||
|
||||
var created = new List<string>();
|
||||
var aligned = new List<string>();
|
||||
|
||||
foreach (var reference in All)
|
||||
{
|
||||
var existing = rooms.FirstOrDefault(room => Matches(room.Name, reference));
|
||||
if (existing == null)
|
||||
{
|
||||
var room = await CreateAsync(db, reference, statsRoomIds);
|
||||
rooms.Add(room);
|
||||
created.Add($"{reference.Name} ({reference.MaxPlayers} players)");
|
||||
continue;
|
||||
}
|
||||
|
||||
var changes = await AlignAsync(db, existing, subRooms, reference);
|
||||
if (changes.Count > 0)
|
||||
aligned.Add($"{existing.Name}: {string.Join(", ", changes)}");
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await MarkAlignedAsync(db);
|
||||
|
||||
foreach (var line in created) ; foreach (var line in aligned) ; }
|
||||
catch (Exception)
|
||||
{
|
||||
db.ChangeTracker.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task RemoveStrayRroAsync(RecDBContext db)
|
||||
{
|
||||
if (await HasRunAsync(db, RroCorrectionKey, RroCorrectionVersion)) return;
|
||||
|
||||
var rooms = await db.Rooms.ToListAsync();
|
||||
var corrected = new List<string>();
|
||||
|
||||
foreach (var room in rooms)
|
||||
{
|
||||
var entry = All.FirstOrDefault(candidate => !candidate.IsRRO && Matches(room.Name, candidate));
|
||||
if (entry == null) continue;
|
||||
|
||||
var tags = StringTools.ReadTags(room.Tags);
|
||||
var strayTags = tags.Where(tag => string.Equals(tag.Tag, "rro", StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (strayTags.Count == 0 && !room.IsRRO) continue;
|
||||
|
||||
foreach (var tag in strayTags)
|
||||
tags.Remove(tag);
|
||||
|
||||
room.Tags = StringTools.CreateTagString(tags);
|
||||
room.IsRRO = false;
|
||||
corrected.Add(room.Name);
|
||||
}
|
||||
|
||||
if (corrected.Count > 0)
|
||||
await db.SaveChangesAsync();
|
||||
await MarkRunAsync(db, RroCorrectionKey, RroCorrectionVersion);
|
||||
}
|
||||
|
||||
private static async Task DisableCloningAsync(RecDBContext db)
|
||||
{
|
||||
if (await HasRunAsync(db, CloningCorrectionKey, CloningCorrectionVersion)) return;
|
||||
|
||||
var rooms = await db.Rooms.ToListAsync();
|
||||
var off = new List<string>();
|
||||
var on = new List<string>();
|
||||
|
||||
foreach (var room in rooms)
|
||||
{
|
||||
var entry = All.FirstOrDefault(candidate => Matches(room.Name, candidate));
|
||||
if (entry == null || entry.CloningAllowed == room.CloningAllowed) continue;
|
||||
|
||||
room.CloningAllowed = entry.CloningAllowed;
|
||||
(entry.CloningAllowed ? on : off).Add(room.Name);
|
||||
}
|
||||
|
||||
if (off.Count > 0 || on.Count > 0)
|
||||
await db.SaveChangesAsync();
|
||||
await MarkRunAsync(db, CloningCorrectionKey, CloningCorrectionVersion);
|
||||
|
||||
if (off.Count == 0 && on.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static async Task<RoomData> CreateAsync(RecDBContext db, DefaultRoom reference, List<long> statsRoomIds)
|
||||
{
|
||||
var room = new RoomData
|
||||
{
|
||||
Name = reference.Name,
|
||||
Description = reference.Description,
|
||||
ImageName = reference.ImageName ?? reference.Name,
|
||||
MaxPlayers = reference.MaxPlayers,
|
||||
MaxPlayerCalculationMode = MaxPlayerMode.OnlyEntrySubrooms,
|
||||
CreatorAccountId = 1,
|
||||
IsRRO = reference.IsRRO,
|
||||
IsDorm = false,
|
||||
Accessibility = reference.Accessibility,
|
||||
CreatedAt = DateTime.Now,
|
||||
State = RoomState.Active,
|
||||
CloningAllowed = reference.CloningAllowed,
|
||||
DisableRoomComments = reference.DisableRoomComments,
|
||||
WarningMask = reference.WarningMask,
|
||||
SupportsTeleportVR = reference.SupportsTeleportVR,
|
||||
Tags = StringTools.CreateTagString(reference.Tags.ToList())
|
||||
};
|
||||
|
||||
await db.Rooms.AddAsync(room);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await AddSubRoomAsync(db, room.RoomId, reference.SubRoomName, reference.SceneId, reference.MaxPlayers,
|
||||
reference.Accessibility, reference.SubRoomSandbox, reference.SaveName, reference.BundleName);
|
||||
foreach (var extra in reference.ExtraSubRooms)
|
||||
await AddSubRoomAsync(db, room.RoomId, extra.Name, extra.SceneId, ExtraSubRoomSize(extra, reference),
|
||||
reference.Accessibility, reference.SubRoomSandbox);
|
||||
|
||||
if (!statsRoomIds.Contains(room.RoomId))
|
||||
{
|
||||
await db.RoomStats.AddAsync(new RoomStats { RoomId = room.RoomId });
|
||||
statsRoomIds.Add(room.RoomId);
|
||||
}
|
||||
|
||||
await db.RoomRoles.AddAsync(new RoomRole
|
||||
{
|
||||
RoomId = room.RoomId,
|
||||
AccountId = 1,
|
||||
Role = RoleType.Creator,
|
||||
InvitedRole = RoleType.None
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return room;
|
||||
}
|
||||
|
||||
private static async Task<List<string>> AlignAsync(RecDBContext db, RoomData room, List<SubRoom> subRooms, DefaultRoom reference)
|
||||
{
|
||||
var changes = new List<string>();
|
||||
var image = reference.ImageName ?? reference.Name;
|
||||
|
||||
if (room.MaxPlayers != reference.MaxPlayers)
|
||||
{
|
||||
changes.Add($"max players {room.MaxPlayers} -> {reference.MaxPlayers}");
|
||||
room.MaxPlayers = reference.MaxPlayers;
|
||||
}
|
||||
if (!string.Equals(room.ImageName, image, StringComparison.Ordinal))
|
||||
{
|
||||
changes.Add($"image '{room.ImageName}' -> '{image}'");
|
||||
room.ImageName = image;
|
||||
}
|
||||
if (room.Accessibility != reference.Accessibility)
|
||||
{
|
||||
changes.Add($"accessibility {room.Accessibility} -> {reference.Accessibility}");
|
||||
room.Accessibility = reference.Accessibility;
|
||||
}
|
||||
if (room.CloningAllowed != reference.CloningAllowed)
|
||||
{
|
||||
changes.Add($"cloning {room.CloningAllowed} -> {reference.CloningAllowed}");
|
||||
room.CloningAllowed = reference.CloningAllowed;
|
||||
}
|
||||
if (room.DisableRoomComments != reference.DisableRoomComments)
|
||||
{
|
||||
changes.Add($"room comments {(reference.DisableRoomComments ? "off" : "on")}");
|
||||
room.DisableRoomComments = reference.DisableRoomComments;
|
||||
}
|
||||
if (room.WarningMask != reference.WarningMask)
|
||||
{
|
||||
changes.Add($"warning mask {room.WarningMask} -> {reference.WarningMask}");
|
||||
room.WarningMask = reference.WarningMask;
|
||||
}
|
||||
if (room.SupportsTeleportVR != reference.SupportsTeleportVR)
|
||||
{
|
||||
changes.Add($"teleport VR {reference.SupportsTeleportVR}");
|
||||
room.SupportsTeleportVR = reference.SupportsTeleportVR;
|
||||
}
|
||||
if (room.IsRRO != reference.IsRRO)
|
||||
{
|
||||
changes.Add(reference.IsRRO ? "marked IsRRO" : "no longer marked IsRRO");
|
||||
room.IsRRO = reference.IsRRO;
|
||||
}
|
||||
|
||||
var mine = subRooms.Where(sub => sub.RoomId == room.RoomId).OrderBy(sub => sub.SubRoomId).ToList();
|
||||
var entry = mine.FirstOrDefault();
|
||||
if (entry == null)
|
||||
{
|
||||
subRooms.Add(await AddSubRoomAsync(db, room.RoomId, reference.SubRoomName, reference.SceneId,
|
||||
reference.MaxPlayers, reference.Accessibility, reference.SubRoomSandbox, reference.SaveName,
|
||||
reference.BundleName));
|
||||
changes.Add($"added the {reference.SubRoomName} subroom");
|
||||
}
|
||||
else
|
||||
{
|
||||
AlignSubRoom(changes, entry, reference.SubRoomName, reference.SceneId, reference.MaxPlayers,
|
||||
reference.Accessibility, reference.SubRoomSandbox);
|
||||
}
|
||||
|
||||
foreach (var extra in reference.ExtraSubRooms)
|
||||
{
|
||||
var match = subRooms.FirstOrDefault(sub => sub.RoomId == room.RoomId
|
||||
&& string.Equals(sub.UnitySceneId, extra.SceneId, StringComparison.OrdinalIgnoreCase));
|
||||
if (match == null)
|
||||
{
|
||||
subRooms.Add(await AddSubRoomAsync(db, room.RoomId, extra.Name, extra.SceneId,
|
||||
ExtraSubRoomSize(extra, reference), reference.Accessibility, reference.SubRoomSandbox));
|
||||
changes.Add($"added the {extra.Name} subroom");
|
||||
}
|
||||
else
|
||||
{
|
||||
AlignSubRoom(changes, match, extra.Name, extra.SceneId, ExtraSubRoomSize(extra, reference),
|
||||
reference.Accessibility, reference.SubRoomSandbox);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static void AlignSubRoom(List<string> changes, SubRoom sub, string name, string sceneId, int maxPlayers, Accessibility accessibility, bool sandbox, string? saveName = null, string? bundleName = null)
|
||||
{
|
||||
var what = $"subroom {sub.Name}";
|
||||
|
||||
if (!string.Equals(sub.Name, name, StringComparison.Ordinal))
|
||||
{
|
||||
changes.Add($"{what} renamed to {name}");
|
||||
sub.Name = name;
|
||||
}
|
||||
if (!string.Equals(sub.UnitySceneId, sceneId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
changes.Add($"{what} scene {sub.UnitySceneId} -> {sceneId}");
|
||||
sub.UnitySceneId = sceneId;
|
||||
}
|
||||
if (sub.MaxPlayers != maxPlayers)
|
||||
{
|
||||
changes.Add($"{what} max players {sub.MaxPlayers} -> {maxPlayers}");
|
||||
sub.MaxPlayers = maxPlayers;
|
||||
}
|
||||
if (sub.Accessibility != accessibility)
|
||||
{
|
||||
changes.Add($"{what} accessibility {sub.Accessibility} -> {accessibility}");
|
||||
sub.Accessibility = accessibility;
|
||||
}
|
||||
if (sub.IsSandbox != sandbox)
|
||||
{
|
||||
changes.Add($"{what} sandbox {sandbox}");
|
||||
sub.IsSandbox = sandbox;
|
||||
}
|
||||
if (saveName != null && !string.Equals(sub.DataBlob, saveName, StringComparison.Ordinal))
|
||||
{
|
||||
changes.Add($"{what} room file '{(sub.DataBlob.Length == 0 ? "none" : sub.DataBlob)}' -> '{saveName}'");
|
||||
sub.DataBlob = saveName;
|
||||
}
|
||||
if (bundleName != null && !string.Equals(sub.UnityAssetId, bundleName, StringComparison.Ordinal))
|
||||
{
|
||||
changes.Add($"{what} bundle '{(sub.UnityAssetId.Length == 0 ? "none" : sub.UnityAssetId)}' -> '{bundleName}'");
|
||||
sub.UnityAssetId = bundleName;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<SubRoom> AddSubRoomAsync(RecDBContext db, long roomId, string name, string sceneId, int maxPlayers, Accessibility accessibility, bool sandbox, string? saveName = null, string? bundleName = null)
|
||||
{
|
||||
var subRoom = new SubRoom
|
||||
{
|
||||
RoomId = roomId,
|
||||
SavedByAccountId = 1,
|
||||
Name = name,
|
||||
DataBlob = saveName ?? "",
|
||||
UnityAssetId = bundleName ?? "",
|
||||
IsSandbox = sandbox,
|
||||
MaxPlayers = maxPlayers,
|
||||
Accessibility = accessibility,
|
||||
UnitySceneId = sceneId,
|
||||
CreatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
await db.SubRooms.AddAsync(subRoom);
|
||||
return subRoom;
|
||||
}
|
||||
|
||||
private static int ExtraSubRoomSize(DefaultSubRoom subRoom, DefaultRoom room) =>
|
||||
subRoom.MaxPlayers > 0 ? subRoom.MaxPlayers : room.MaxPlayers;
|
||||
|
||||
|
||||
private static Task<bool> IsAlignedAsync(RecDBContext db) => HasRunAsync(db, AlignmentKey, AlignmentVersion);
|
||||
|
||||
private static Task MarkAlignedAsync(RecDBContext db) => MarkRunAsync(db, AlignmentKey, AlignmentVersion);
|
||||
|
||||
private static async Task<bool> HasRunAsync(RecDBContext db, string key, int version)
|
||||
{
|
||||
var row = await db.GameConfig.FirstOrDefaultAsync(x => x.Key == key);
|
||||
return int.TryParse(row?.Value, out var recorded) && recorded >= version;
|
||||
}
|
||||
|
||||
private static async Task MarkRunAsync(RecDBContext db, string key, int version)
|
||||
{
|
||||
var row = await db.GameConfig.FirstOrDefaultAsync(x => x.Key == key);
|
||||
if (row == null)
|
||||
await db.GameConfig.AddAsync(new ConfigVal(key, version.ToString()));
|
||||
else
|
||||
row.Value = version.ToString();
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static bool Matches(string? existingName, DefaultRoom reference)
|
||||
{
|
||||
var key = NormalizeName(existingName);
|
||||
if (key.Length == 0) return false;
|
||||
|
||||
return key == NormalizeName(reference.Name)
|
||||
|| reference.Aliases.Any(alias => key == NormalizeName(alias));
|
||||
}
|
||||
|
||||
private static string NormalizeName(string? name) =>
|
||||
string.Concat((name ?? "").Where(char.IsLetterOrDigit)).ToLowerInvariant();
|
||||
|
||||
private static List<ItemTag> Rro() => new() { new ItemTag { Tag = "rro", Type = TagType.AGOnly } };
|
||||
|
||||
private static List<ItemTag> RroWith(params string[] tags)
|
||||
{
|
||||
var list = Rro();
|
||||
list.AddRange(tags.Select(tag => new ItemTag { Tag = tag, Type = TagType.General }));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DefaultRoom
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public string Description { get; init; } = "";
|
||||
|
||||
public int MaxPlayers { get; init; }
|
||||
|
||||
public string SceneId { get; init; } = "";
|
||||
|
||||
public string SubRoomName { get; init; } = "Home";
|
||||
|
||||
public string? ImageName { get; init; }
|
||||
|
||||
public string? SaveName { get; init; }
|
||||
|
||||
public string? BundleName { get; init; }
|
||||
|
||||
public IReadOnlyList<ItemTag> Tags { get; init; } = Array.Empty<ItemTag>();
|
||||
|
||||
public IReadOnlyList<DefaultSubRoom> ExtraSubRooms { get; init; } = Array.Empty<DefaultSubRoom>();
|
||||
|
||||
public IReadOnlyList<string> Aliases { get; init; } = Array.Empty<string>();
|
||||
|
||||
public Accessibility Accessibility { get; init; } = Accessibility.Public;
|
||||
|
||||
public bool CloningAllowed
|
||||
{
|
||||
get => cloningAllowedOverride ?? Accessibility == Accessibility.Unlised;
|
||||
init => cloningAllowedOverride = value;
|
||||
}
|
||||
|
||||
private bool? cloningAllowedOverride;
|
||||
public bool IsRRO { get; init; } = true;
|
||||
|
||||
public bool DisableRoomComments { get; init; }
|
||||
public bool SubRoomSandbox { get; init; }
|
||||
public WarningMask WarningMask { get; init; } = WarningMask.None;
|
||||
public bool SupportsTeleportVR { get; init; } = true;
|
||||
}
|
||||
|
||||
public sealed class DefaultSubRoom
|
||||
{
|
||||
public string Name { get; init; } = "";
|
||||
public string SceneId { get; init; } = "";
|
||||
|
||||
public int MaxPlayers { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using RadicadeServer.RecNet.Enums.Board;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class ExternalContent
|
||||
{
|
||||
public ExternalContentType Type { get; set; }
|
||||
public string Reference { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class FeaturedRoomGroup
|
||||
{
|
||||
public long FeaturedRoomGroupId { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
public List<FeaturedRoom> Rooms { get; set; } = null!;
|
||||
}
|
||||
public class FeaturedRoom
|
||||
{
|
||||
public long RoomId { get; set; }
|
||||
public string RoomName { get; set; } = null!;
|
||||
public string ImageName { get; set; } = "default.png";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class FullRoom : RoomDetail
|
||||
{
|
||||
public List<SubRoom> SubRooms { get; set; } = null!;
|
||||
public List<RoomRole> Roles { get; set; } = null!;
|
||||
public List<RoomLoadScreen> LoadScreens { get; set; } = new();
|
||||
public List<string> PromoImages { get; set; } = new();
|
||||
public List<ExternalContent> PromoExternalContent { get; set; } = new();
|
||||
public string? DataBlob { get; set; }
|
||||
public List<ItemTag> Tags { get; set; } = new();
|
||||
|
||||
|
||||
public FullRoom CopyTo(FullRoom target)
|
||||
{
|
||||
target.RoomId = RoomId;
|
||||
target.IsDorm = IsDorm;
|
||||
target.MaxPlayers = MaxPlayers;
|
||||
target.MaxPlayerCalculationMode = MaxPlayerCalculationMode;
|
||||
target.CloningAllowed = CloningAllowed;
|
||||
target.DisableMicAutoMute = DisableMicAutoMute;
|
||||
target.DisableRoomComments = DisableRoomComments;
|
||||
target.EncryptVoiceChat = EncryptVoiceChat;
|
||||
|
||||
target.Name = Name;
|
||||
target.Description = Description;
|
||||
target.ImageName = ImageName;
|
||||
target.WarningMask = WarningMask;
|
||||
target.CustomWarning = CustomWarning;
|
||||
target.CreatorAccountId = CreatorAccountId;
|
||||
target.State = State;
|
||||
target.Accessibility = Accessibility;
|
||||
target.SupportsLevelVoting = SupportsLevelVoting;
|
||||
target.IsRRO = IsRRO;
|
||||
target.IsDeveloperOwned = IsDeveloperOwned;
|
||||
target.SupportsScreens = SupportsScreens;
|
||||
target.SupportsWalkVR = SupportsWalkVR;
|
||||
target.SupportsTeleportVR = SupportsTeleportVR;
|
||||
target.SupportsVRLow = SupportsVRLow;
|
||||
target.SupportsQuest2 = SupportsQuest2;
|
||||
target.SupportsMobile = SupportsMobile;
|
||||
target.SupportsJuniors = SupportsJuniors;
|
||||
target.MinLevel = MinLevel;
|
||||
target.CreatedAt = CreatedAt;
|
||||
target.Stats = Stats;
|
||||
target.Version = Version;
|
||||
|
||||
target.SubRooms = SubRooms;
|
||||
target.Roles = Roles;
|
||||
target.LoadScreens = LoadScreens;
|
||||
target.PromoImages = PromoImages;
|
||||
target.PromoExternalContent = PromoExternalContent;
|
||||
target.DataBlob = DataBlob;
|
||||
target.Tags = Tags;
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
|
||||
namespace RadicadeServer.Lists
|
||||
{
|
||||
[Route("lists/algorithmiclists")]
|
||||
public class AlgorithmicListsAPI : Controller
|
||||
{
|
||||
private const int Rooms = 1;
|
||||
|
||||
private const int RowSize = 20;
|
||||
|
||||
RecDBContext db;
|
||||
|
||||
public AlgorithmicListsAPI(RecDBContext context)
|
||||
{
|
||||
this.db = context;
|
||||
}
|
||||
|
||||
[HttpGet("{list}")]
|
||||
public async Task<IActionResult> GetAlgorithmicList(string list, [FromQuery] int? type)
|
||||
{
|
||||
var echoed = type is >= 0 and <= 255 ? type.Value : Rooms;
|
||||
|
||||
return Ok(new AlgorithmicList
|
||||
{
|
||||
Type = echoed,
|
||||
Entities = (await GetRowIds(list)).Select(roomId => new AlgorithmicListEntity
|
||||
{
|
||||
Id = roomId.ToString(),
|
||||
Context = null,
|
||||
}).ToList(),
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<List<long>> GetRowIds(string? list)
|
||||
{
|
||||
var key = (list ?? string.Empty).Trim().ToLowerInvariant();
|
||||
|
||||
var rooms = await db.Rooms
|
||||
.Where(x => x.Accessibility == Accessibility.Public
|
||||
&& x.State == RoomState.Active
|
||||
&& !x.IsDorm)
|
||||
.ToListAsync();
|
||||
|
||||
if (key == "new")
|
||||
{
|
||||
return rooms
|
||||
.OrderByDescending(x => x.PublishedAt)
|
||||
.ThenBy(x => x.RoomId)
|
||||
.Take(RowSize)
|
||||
.Select(x => x.RoomId)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var visits = await db.RoomStats
|
||||
.Select(x => new { x.RoomId, x.VisitCount })
|
||||
.ToDictionaryAsync(x => x.RoomId, x => x.VisitCount);
|
||||
|
||||
return rooms
|
||||
.OrderByDescending(x => visits.TryGetValue(x.RoomId, out var count) ? count : 0)
|
||||
.ThenBy(x => x.RoomId)
|
||||
.Take(RowSize)
|
||||
.Select(x => x.RoomId)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public class AlgorithmicList
|
||||
{
|
||||
public int Type { get; set; }
|
||||
|
||||
public List<AlgorithmicListEntity> Entities { get; set; } = new();
|
||||
}
|
||||
|
||||
public class AlgorithmicListEntity
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
public string? Context { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace RadicadeServer.Lists
|
||||
{
|
||||
[Route("lists/curatedlists")]
|
||||
public class CuratedListsAPI : Controller
|
||||
{
|
||||
[HttpGet("bulk")]
|
||||
public IActionResult GetBulk()
|
||||
{
|
||||
return Ok(Array.Empty<object>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class PlayerInteraction
|
||||
{
|
||||
public bool Cheered { get; set; }
|
||||
public bool Favorited { get; set; }
|
||||
|
||||
public PlayerInteraction(PlayerInteractionDB db) { Cheered = db.Cheered; Favorited = db.Favorited; }
|
||||
public PlayerInteraction() { }
|
||||
}
|
||||
public class PlayerInteractionDB
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long InteractionId { get; set; }
|
||||
public long PlayerId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
|
||||
public bool Cheered { get; set; }
|
||||
public bool Favorited { get; set; }
|
||||
|
||||
public DateTime CheeredAt { get; set; } = DateTime.Now;
|
||||
public DateTime FavoritedAt { get; set; } = DateTime.Now;
|
||||
|
||||
public int VisitCount { get; set; }
|
||||
public bool Visited { get; set; }
|
||||
public DateTime LastVisted { get; set; } = DateTime.Now;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class PlaylistRoom : FullRoom
|
||||
{
|
||||
public List<FullRoom> Rooms { get; set; } = new();
|
||||
public PlaylistRoom(FullRoom room)
|
||||
{
|
||||
room.CopyTo(this);
|
||||
}
|
||||
}
|
||||
public class SeededRoom
|
||||
{
|
||||
public FullRoom SeedRoom { get; set; } = null!;
|
||||
public List<string> Rooms { get; set; } = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace RadicadeServer.Radicade.Public
|
||||
{
|
||||
public class PublicRoomData
|
||||
{
|
||||
public string ImageCdnUrl { get; set; } = DBConfig.DefaultRoomImage;
|
||||
|
||||
public string RoomName { get; set; } = "^Nowhere";
|
||||
public string OwnerName { get; set; } = "@Noone";
|
||||
|
||||
public int Cheers { get; set; } = 0;
|
||||
public int Favorites { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RestorableSubRoom
|
||||
{
|
||||
[JsonIgnore, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long HistoryId { get; set; }
|
||||
|
||||
public long SubRoomId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public long RoomId { get; set; }
|
||||
|
||||
public long SavedByAccountId { get; set; }
|
||||
|
||||
public string DataBlob { get; set; } = string.Empty;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public RestorableSubRoom() { }
|
||||
public RestorableSubRoom(SubRoom subRoom)
|
||||
{
|
||||
this.RoomId = subRoom.RoomId;
|
||||
this.SubRoomId = subRoom.SubRoomId;
|
||||
this.DataBlob = subRoom.DataBlob;
|
||||
this.CreatedAt = subRoom.CreatedAt;
|
||||
this.SavedByAccountId = subRoom.SavedByAccountId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class Room
|
||||
{
|
||||
public string Name { get; set; } = null!;
|
||||
public string Description { get; set; } = null!;
|
||||
|
||||
public string ImageName { get; set; } = null!;
|
||||
|
||||
public WarningMask WarningMask { get; set; }
|
||||
public string CustomWarning { get; set; } = null!;
|
||||
|
||||
public long CreatorAccountId { get; set; }
|
||||
|
||||
public RoomState State { get; set; }
|
||||
public Accessibility Accessibility { get; set; }
|
||||
|
||||
public bool SupportsLevelVoting { get; set; }
|
||||
|
||||
public bool IsRRO { get; set; }
|
||||
|
||||
public bool IsDeveloperOwned { get; set; }
|
||||
|
||||
public bool SupportsScreens { get; set; }
|
||||
public bool SupportsWalkVR { get; set; }
|
||||
public bool SupportsTeleportVR { get; set; }
|
||||
public bool SupportsVRLow { get; set; }
|
||||
public bool SupportsQuest2 { get; set; }
|
||||
public bool SupportsMobile { get; set; }
|
||||
public bool SupportsJuniors { get; set; }
|
||||
|
||||
public int MinLevel { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public RoomStats Stats { get; set; } = null!;
|
||||
|
||||
public int Version { get; set; } = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet.Config;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RadicadeServer.API.Rooms.V1;
|
||||
|
||||
internal sealed class RoomAssetRecord
|
||||
{
|
||||
public string Id { get; set; } = "";
|
||||
public long RoomId { get; set; }
|
||||
public string Kind { get; set; } = "";
|
||||
public Dictionary<string, object?> Fields { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
internal static class RoomAssetStore
|
||||
{
|
||||
private const string ConfigKey = "RoomAssets";
|
||||
private static readonly SemaphoreSlim Gate = new(1, 1);
|
||||
|
||||
public static async Task<List<RoomAssetRecord>> LoadAsync(RecDBContext db)
|
||||
{
|
||||
var config = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(row => row.Key == ConfigKey);
|
||||
if (string.IsNullOrWhiteSpace(config?.Value)) return new List<RoomAssetRecord>();
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<RoomAssetRecord>>(config.Value,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List<RoomAssetRecord>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return new List<RoomAssetRecord>();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<List<RoomAssetRecord>> ForRoomAsync(RecDBContext db, long roomId, string kind)
|
||||
{
|
||||
var records = await LoadAsync(db);
|
||||
return records.Where(record => record.RoomId == roomId &&
|
||||
string.Equals(record.Kind, kind, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
}
|
||||
|
||||
public static async Task SaveAsync(RecDBContext db, RoomAssetRecord record)
|
||||
{
|
||||
await Gate.WaitAsync();
|
||||
try
|
||||
{
|
||||
var config = await db.ServerConfig.FirstOrDefaultAsync(row => row.Key == ConfigKey);
|
||||
List<RoomAssetRecord> records;
|
||||
try
|
||||
{
|
||||
records = string.IsNullOrWhiteSpace(config?.Value)
|
||||
? new List<RoomAssetRecord>()
|
||||
: JsonSerializer.Deserialize<List<RoomAssetRecord>>(config.Value,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List<RoomAssetRecord>();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
records = new List<RoomAssetRecord>();
|
||||
}
|
||||
|
||||
records.RemoveAll(existing => existing.RoomId == record.RoomId &&
|
||||
string.Equals(existing.Kind, record.Kind, StringComparison.OrdinalIgnoreCase) &&
|
||||
existing.Id == record.Id);
|
||||
records.Add(record);
|
||||
if (config == null)
|
||||
{
|
||||
config = new ServerConfigVal(ConfigKey, JsonSerializer.Serialize(records));
|
||||
await db.ServerConfig.AddAsync(config);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.Value = JsonSerializer.Serialize(records);
|
||||
}
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Gate.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<List<Dictionary<string, object?>>> GetDtosAsync(RecDBContext db, long roomId, string kind)
|
||||
{
|
||||
var records = await ForRoomAsync(db, roomId, kind);
|
||||
return records.Select(record =>
|
||||
{
|
||||
var dto = new Dictionary<string, object?>(record.Fields, StringComparer.OrdinalIgnoreCase);
|
||||
dto[kind.Equals("currency", StringComparison.OrdinalIgnoreCase) ? "CurrencyId" : "ConsumableId"] = record.Id;
|
||||
dto["RoomId"] = record.RoomId;
|
||||
return dto;
|
||||
}).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public class RoomBan
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long BanId { get; set; }
|
||||
|
||||
public long roomId { get; set; }
|
||||
|
||||
|
||||
public long actorId { get; set; }
|
||||
|
||||
|
||||
public long bannedPlayerId { get; set; }
|
||||
|
||||
|
||||
public string bannedByName { get; set; } = "";
|
||||
|
||||
public string reason { get; set; } = "";
|
||||
|
||||
public DateTime bannedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
|
||||
|
||||
public DateTime? expiresAt { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomComment
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long Id { get; set; }
|
||||
|
||||
public long RoomId { get; set; }
|
||||
|
||||
public long CreatorAccountId { get; set; }
|
||||
|
||||
public string Comment { get; set; } = string.Empty;
|
||||
|
||||
public string CommentType { get; set; } = "Feedback";
|
||||
|
||||
public float? PositionX { get; set; }
|
||||
public float? PositionY { get; set; }
|
||||
public float? PositionZ { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.API.Rooms;
|
||||
using RadicadeServer.API.Rooms.V1;
|
||||
using RadicadeServer.DB;
|
||||
|
||||
namespace RadicadeServer.API.Rooms
|
||||
{
|
||||
[Route("api/roomconsumables/v1")]
|
||||
[Route("econ/api/roomconsumables/v1")]
|
||||
public class RoomConsumablesAPI(RecDBContext db) : Controller
|
||||
{
|
||||
[HttpGet("roomConsumable/room/{roomId:long}")]
|
||||
public async Task<IActionResult> GetRoomConsumablesInRoom(long roomId)
|
||||
{
|
||||
return Ok(await RoomAssetStore.GetDtosAsync(db, roomId, "consumable"));
|
||||
}
|
||||
|
||||
[HttpGet("roomConsumable/room/{roomId:long}/me")]
|
||||
public IActionResult GetOwnedRoomConsumablesInRoom(long roomId)
|
||||
{
|
||||
return Ok(Array.Empty<object>());
|
||||
}
|
||||
|
||||
[HttpPut("roomConsumable")]
|
||||
[HttpPost("roomConsumable")]
|
||||
public async Task<IActionResult> CreateRoomConsumable()
|
||||
{
|
||||
var body = await RoomEconomyPayload.ReadBodyAsync(Request);
|
||||
var roomId = await RoomEconomyPayload.RoomIdAsync(db, Request, body, "roomId", "RoomId");
|
||||
if (roomId is not > 0) return BadRequest(new { Message = "roomId is required." });
|
||||
var consumableId = RoomEconomyPayload.String(body, "consumableId", "ConsumableId") ?? RoomEconomyPayload.NewId();
|
||||
|
||||
var consumable = RoomEconomyPayload.Fields(body);
|
||||
consumable["ConsumableId"] = consumableId;
|
||||
consumable["RoomId"] = roomId.Value;
|
||||
consumable["Name"] = RoomEconomyPayload.String(body, "name", "Name") ?? "Consumable";
|
||||
consumable["Description"] = RoomEconomyPayload.String(body, "description", "Description") ?? string.Empty;
|
||||
|
||||
await RoomAssetStore.SaveAsync(db, new RoomAssetRecord
|
||||
{
|
||||
Id = consumableId,
|
||||
RoomId = roomId.Value,
|
||||
Kind = "consumable",
|
||||
Fields = consumable
|
||||
});
|
||||
|
||||
return Ok(consumable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.API.Rooms;
|
||||
using RadicadeServer.API.Rooms.V1;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
|
||||
namespace RadicadeServer.API.Rooms.Currency.V1
|
||||
{
|
||||
[Route("api/roomcurrencies/v1")]
|
||||
[Route("econ/api/roomcurrencies/v1")]
|
||||
public class RoomCurrencyAPI(RecDBContext db) : Controller
|
||||
{
|
||||
[HttpGet("betaEnabled")]
|
||||
public bool IsBetaEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
[HttpGet("currencies")]
|
||||
public async Task<IActionResult> GetRoomCurrencies([FromQuery] long roomId)
|
||||
{
|
||||
return Ok(await RoomAssetStore.GetDtosAsync(db, roomId, "currency"));
|
||||
}
|
||||
|
||||
[HttpGet("getAllBalances")]
|
||||
public List<object> GetAllBalances([FromQuery] long roomId)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
[HttpGet("getBalance")]
|
||||
public IActionResult GetBalance(
|
||||
[FromQuery] string? currencyId,
|
||||
[FromQuery] long? accountId,
|
||||
[FromQuery] long? roomId)
|
||||
{
|
||||
return Ok(new
|
||||
{
|
||||
CurrencyId = currencyId ?? string.Empty,
|
||||
AccountId = accountId ?? User.GetUserId() ?? 0,
|
||||
RoomId = roomId ?? 0,
|
||||
Balance = 0
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("createCurrency")]
|
||||
public async Task<IActionResult> CreateCurrency()
|
||||
{
|
||||
var body = await RoomEconomyPayload.ReadBodyAsync(Request);
|
||||
var roomId = await RoomEconomyPayload.RoomIdAsync(db, Request, body, "roomId", "RoomId");
|
||||
if (roomId is not > 0) return BadRequest(new { Message = "roomId is required." });
|
||||
var currencyId = RoomEconomyPayload.String(body, "currencyId", "CurrencyId") ?? RoomEconomyPayload.NewId();
|
||||
|
||||
var currency = RoomEconomyPayload.Fields(body);
|
||||
currency["CurrencyId"] = currencyId;
|
||||
currency["RoomId"] = roomId.Value;
|
||||
currency["CurrencyType"] = currency.TryGetValue("CurrencyType", out var type) && type != null ? type : "RoomCurrency";
|
||||
currency["Name"] = RoomEconomyPayload.String(body, "name", "Name") ?? "Currency";
|
||||
currency["Description"] = RoomEconomyPayload.String(body, "description", "Description") ?? string.Empty;
|
||||
currency["InitialBalance"] = RoomEconomyPayload.Long(body, "initialBalance", "InitialBalance") ?? 0;
|
||||
|
||||
await RoomAssetStore.SaveAsync(db, new RoomAssetRecord
|
||||
{
|
||||
Id = currencyId,
|
||||
RoomId = roomId.Value,
|
||||
Kind = "currency",
|
||||
Fields = currency
|
||||
});
|
||||
|
||||
return Ok(currency);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Players.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.Radicade.Gifts.Enums;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomData
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long RoomId { get; set; }
|
||||
|
||||
public string Tags { get; set; } = string.Empty;
|
||||
|
||||
public bool IsDorm { get; set; } = false;
|
||||
|
||||
public int MaxPlayers { get; set; } = 10;
|
||||
|
||||
public MaxPlayerMode MaxPlayerCalculationMode { get; set; } = MaxPlayerMode.OnlyEntrySubrooms;
|
||||
|
||||
public bool CloningAllowed { get; set; } = false;
|
||||
|
||||
public bool DisableMicAutoMute { get; set; } = false;
|
||||
public bool DisableRoomComments { get; set; } = false;
|
||||
|
||||
public bool EncryptVoiceChat { get; set; } = false;
|
||||
|
||||
public string Name { get; set; } = "";
|
||||
public string? Description { get; set; } = "";
|
||||
public string ImageName { get; set; } = "default.png";
|
||||
|
||||
public WarningMask WarningMask { get; set; }
|
||||
public string? CustomWarning { get; set; } = null;
|
||||
|
||||
public long CreatorAccountId { get; set; } = 1;
|
||||
|
||||
public RoomState State { get; set; } = RoomState.Active;
|
||||
public Accessibility Accessibility { get; set; } = Accessibility.Private;
|
||||
|
||||
public bool SupportsLevelVoting { get; set; } = false;
|
||||
|
||||
public bool IsRRO { get; set; } = false;
|
||||
|
||||
public bool IsDeveloperOwned { get; set; } = false;
|
||||
|
||||
public bool SupportsScreens { get; set; } = true;
|
||||
public bool SupportsWalkVR { get; set; } = true;
|
||||
public bool SupportsTeleportVR { get; set; } = true;
|
||||
public bool SupportsVRLow { get; set; } = true;
|
||||
public bool SupportsQuest2 { get; set; } = true;
|
||||
public bool SupportsMobile { get; set; } = true;
|
||||
public bool SupportsJuniors { get; set; } = true;
|
||||
|
||||
public int MinLevel { get; set; } = 0;
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.MinValue;
|
||||
|
||||
[JsonIgnore] public DateTime PublishedAt { get; set; } = DateTime.Now;
|
||||
[JsonIgnore] public bool WasPublished = false;
|
||||
|
||||
public string loadScreen { get; set; } = "[]";
|
||||
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
public bool JoinGiftEnabled { get; set; } = false;
|
||||
public bool JoinGiftOncePerPlayer { get; set; } = true;
|
||||
public int JoinGiftVersion { get; set; } = 1;
|
||||
public string JoinGiftTitle { get; set; } = "Room Visit Gift";
|
||||
public string JoinGiftMessage { get; set; } = "Thanks for visiting!";
|
||||
public string JoinGiftAvatarItem { get; set; } = "";
|
||||
public string JoinGiftEquipmentItem { get; set; } = "";
|
||||
public string JoinGiftConsumableItem { get; set; } = "";
|
||||
public int JoinGiftCurrencyAmount { get; set; } = 0;
|
||||
public CurrencyType JoinGiftCurrencyType { get; set; } = CurrencyType.RecCenterTokens;
|
||||
public int JoinGiftXp { get; set; } = 0;
|
||||
public GiftContext JoinGiftContext { get; set; } = GiftContext.Game_Drop;
|
||||
public Rarity JoinGiftRarity { get; set; } = Rarity.Common;
|
||||
public AvatarItemType JoinGiftAvatarItemType { get; set; } = AvatarItemType.Outfit;
|
||||
public string JoinGiftEquipmentModificationGuid { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// The account the join box is from, which the client shows on the box and reads off
|
||||
/// RecNet's Gift.FromPlayerId. Zero means the room names no sender, as it did before this
|
||||
/// was configurable.
|
||||
/// </summary>
|
||||
public long JoinGiftFromPlayerId { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomDetail : Room
|
||||
{
|
||||
public long RoomId { get; set; }
|
||||
|
||||
public bool IsDorm { get; set; }
|
||||
|
||||
public int MaxPlayers { get; set; }
|
||||
|
||||
public MaxPlayerMode MaxPlayerCalculationMode { get; set; }
|
||||
|
||||
public bool CloningAllowed { get; set; }
|
||||
|
||||
public bool DisableMicAutoMute { get; set; }
|
||||
public bool DisableRoomComments { get; set; }
|
||||
|
||||
public bool EncryptVoiceChat { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace RadicadeServer.API.Rooms.Earnings.V1
|
||||
{
|
||||
[Route("api/roomEarningsDistributions/v1")]
|
||||
[Route("econ/api/roomEarningsDistributions/v1")]
|
||||
public class RoomEarningsAPI : Controller
|
||||
{
|
||||
[HttpGet("earningsDistribution/{roomId}")]
|
||||
public IActionResult GetEarningsDistribution(long roomId)
|
||||
{
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
RoomId = roomId,
|
||||
CurrencyId = string.Empty,
|
||||
TotalEarnings = 0,
|
||||
TotalSpent = 0,
|
||||
UnclaimedEarnings = 0,
|
||||
LifetimeEarnings = 0,
|
||||
IsAffiliatedWithCreator = false,
|
||||
LastPaidAt = (string?)null,
|
||||
Distributions = Array.Empty<object>()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
|
||||
namespace RadicadeServer.API.Rooms
|
||||
{
|
||||
internal static class RoomEconomyPayload
|
||||
{
|
||||
private const string RawKey = "RoomEconomyPayload.Raw";
|
||||
|
||||
public static async Task<JsonElement?> ReadBodyAsync(HttpRequest request)
|
||||
{
|
||||
if (request.HasFormContentType)
|
||||
{
|
||||
try
|
||||
{
|
||||
var form = await request.ReadFormAsync();
|
||||
if (form.Count > 0)
|
||||
{
|
||||
var fields = new Dictionary<string, object?>();
|
||||
foreach (var field in form)
|
||||
fields[field.Key] = field.Value.ToString();
|
||||
foreach (var file in form.Files)
|
||||
fields[file.Name] = file.FileName;
|
||||
return JsonSerializer.SerializeToElement(fields);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
request.HttpContext.Items[RawKey] = $"<form body could not be read: {ex.Message}>";
|
||||
}
|
||||
}
|
||||
|
||||
string raw;
|
||||
using (var reader = new StreamReader(request.Body))
|
||||
raw = await reader.ReadToEndAsync();
|
||||
|
||||
var text = raw.TrimStart('\uFEFF', ' ', '\t', '\r', '\n');
|
||||
if (text.Length == 0) return null;
|
||||
|
||||
if (text[0] is '{' or '[')
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
return doc.RootElement.Clone();
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
var pairs = ParsePairs(raw);
|
||||
if (pairs.Count > 0)
|
||||
{
|
||||
request.HttpContext.Items[RawKey] = raw;
|
||||
return JsonSerializer.SerializeToElement(pairs);
|
||||
}
|
||||
|
||||
request.HttpContext.Items[RawKey] = raw;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string RawBody(HttpRequest request) =>
|
||||
request.HttpContext.Items.TryGetValue(RawKey, out var raw) ? raw as string ?? "" : "";
|
||||
|
||||
public static JsonElement? Value(JsonElement? body, params string[] names)
|
||||
{
|
||||
if (body == null) return null;
|
||||
|
||||
var stack = new Stack<JsonElement>();
|
||||
stack.Push(body.Value);
|
||||
var visited = 0;
|
||||
|
||||
while (stack.Count > 0 && visited++ < 2000)
|
||||
{
|
||||
var element = stack.Pop();
|
||||
if (element.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var item in element.EnumerateArray()) stack.Push(item);
|
||||
continue;
|
||||
}
|
||||
if (element.ValueKind != JsonValueKind.Object) continue;
|
||||
|
||||
foreach (var property in element.EnumerateObject())
|
||||
{
|
||||
if (property.Value.ValueKind is JsonValueKind.Object or JsonValueKind.Array)
|
||||
{
|
||||
stack.Push(property.Value);
|
||||
continue;
|
||||
}
|
||||
if (Matches(property.Name, names)) return property.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string? String(JsonElement? body, params string[] names)
|
||||
{
|
||||
var value = Value(body, names);
|
||||
if (value == null) return null;
|
||||
if (value.Value.ValueKind == JsonValueKind.String) return value.Value.GetString();
|
||||
if (value.Value.ValueKind is JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False)
|
||||
return value.Value.ToString();
|
||||
return null;
|
||||
}
|
||||
|
||||
public static long? Long(JsonElement? body, params string[] names)
|
||||
{
|
||||
var value = Value(body, names);
|
||||
if (value == null) return null;
|
||||
if (value.Value.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
if (value.Value.TryGetInt64(out var number)) return number;
|
||||
if (value.Value.TryGetDouble(out var real)) return (long)real;
|
||||
}
|
||||
if (value.Value.ValueKind == JsonValueKind.String && long.TryParse(value.Value.GetString(), out var parsed))
|
||||
return parsed;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool? Bool(JsonElement? body, params string[] names)
|
||||
{
|
||||
var value = Value(body, names);
|
||||
if (value == null) return null;
|
||||
if (value.Value.ValueKind == JsonValueKind.True) return true;
|
||||
if (value.Value.ValueKind == JsonValueKind.False) return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
public static long? LongFromRequest(HttpRequest request, params string[] names)
|
||||
{
|
||||
foreach (var pair in request.Query)
|
||||
{
|
||||
if (!Matches(pair.Key, names)) continue;
|
||||
if (long.TryParse(pair.Value.FirstOrDefault(), out var fromQuery)) return fromQuery;
|
||||
}
|
||||
foreach (var pair in request.RouteValues)
|
||||
{
|
||||
if (!Matches(pair.Key, names)) continue;
|
||||
if (long.TryParse(pair.Value?.ToString(), out var fromRoute)) return fromRoute;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Dictionary<string, object?> Fields(JsonElement? body)
|
||||
{
|
||||
var fields = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
if (body == null || body.Value.ValueKind != JsonValueKind.Object) return fields;
|
||||
|
||||
foreach (var property in body.Value.EnumerateObject())
|
||||
{
|
||||
fields[property.Name] = property.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => property.Value.GetString(),
|
||||
JsonValueKind.Number => property.Value.TryGetInt64(out var number) ? number : property.Value.GetDouble(),
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Null => null,
|
||||
_ => property.Value.Clone()
|
||||
};
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
public static async Task<long?> RoomIdAsync(RecDBContext db, HttpRequest request, JsonElement? body, params string[] names)
|
||||
{
|
||||
var roomId = Long(body, names) ?? LongFromRequest(request, names);
|
||||
if (roomId is > 0) return roomId;
|
||||
|
||||
var accountId = request.HttpContext.User.GetUserId();
|
||||
if (accountId is not > 0) return null;
|
||||
var presence = await db.Presences.AsNoTracking()
|
||||
.FirstOrDefaultAsync(item => item.playerId == accountId.Value);
|
||||
if (presence?.roomInstance is not > 0) return null;
|
||||
return await db.RoomInstances.AsNoTracking()
|
||||
.Where(instance => instance.roomInstanceId == presence.roomInstance.Value)
|
||||
.Select(instance => (long?)instance.roomId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public static string NewId()
|
||||
{
|
||||
var bytes = RandomNumberGenerator.GetBytes(16);
|
||||
return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> ParsePairs(string raw)
|
||||
{
|
||||
var pairs = new Dictionary<string, object?>();
|
||||
foreach (var pair in raw.Split('&', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var at = pair.IndexOf('=');
|
||||
var key = at < 0 ? pair : pair[..at];
|
||||
var value = at < 0 ? "" : pair[(at + 1)..];
|
||||
key = key.Trim();
|
||||
if (key.Length == 0) continue;
|
||||
|
||||
try
|
||||
{
|
||||
pairs[Uri.UnescapeDataString(key.Replace('+', ' '))] =
|
||||
Uri.UnescapeDataString(value.Replace('+', ' '));
|
||||
}
|
||||
catch (UriFormatException)
|
||||
{
|
||||
pairs[key] = value;
|
||||
}
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
private static bool Matches(string name, string[] names)
|
||||
{
|
||||
foreach (var wanted in names)
|
||||
{
|
||||
if (string.Equals(Normalize(name), Normalize(wanted), StringComparison.Ordinal)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string Normalize(string name) =>
|
||||
string.Concat(name.Where(char.IsLetterOrDigit)).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomFilters
|
||||
{
|
||||
public List<string> PinnedFilters { get; set; } = null!;
|
||||
public List<string> PopularFilters { get; set; } = null!;
|
||||
public List<string>? TrendingFilters { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomInstance
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long roomInstanceId { get; set; }
|
||||
|
||||
public long roomId { get; set; } = 1;
|
||||
public long subRoomId { get; set; } = 1;
|
||||
|
||||
public RoomInstanceType roomInstanceType { get; set; } = RoomInstanceType.Dormroom;
|
||||
|
||||
public string? location { get; set; } = "";
|
||||
|
||||
public string dataBlob { get; set; } = "";
|
||||
|
||||
public long? eventId { get; set; } = 0;
|
||||
|
||||
public long? clubId { get; set; } = 0;
|
||||
|
||||
public string? roomCode { get; set; } = "";
|
||||
|
||||
[NotMapped]
|
||||
public string photonRegion { get; set; } = "us";
|
||||
public string photonRegionId { get; set; } = "us";
|
||||
public string photonRoomId { get; set; } = $"Radicade{Random.Shared.Next(232)}-{Guid.NewGuid().ToString().Replace("-", "")}";
|
||||
|
||||
public string name { get; set; } = "DormRoom";
|
||||
|
||||
public int maxCapacity { get; set; } = 4;
|
||||
|
||||
public bool isFull { get; set; } = false;
|
||||
public bool isPrivate { get; set; } = false;
|
||||
public bool isInProgress { get; set; } = false;
|
||||
public bool EncryptVoiceChat { get; set; } = false;
|
||||
|
||||
public RoomInstance Copy() => (RoomInstance)MemberwiseClone();
|
||||
}
|
||||
|
||||
public class FullRoomInstance : RoomInstance
|
||||
{
|
||||
public List<long> playerIds { get; set; } = new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using RadicadeServer.RecNet.Enums.Players.Avatar;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.Radicade.Gifts.Enums;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
/// <summary>
|
||||
/// One box a room hands a player when they join it. A room used to be able to describe
|
||||
/// exactly one box in its own Rooms row; a row here is one more box, and the panel shows
|
||||
/// them as a list. A room with no rows here still has its one box - see
|
||||
/// <see cref="Utils.RoomJoinGiftUtils"/> - so a room configured before this table existed
|
||||
/// keeps handing out what it always did.
|
||||
/// </summary>
|
||||
public class RoomJoinGift
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long RoomJoinGiftId { get; set; }
|
||||
|
||||
public long RoomId { get; set; }
|
||||
|
||||
/// <summary>Where the box sits in the room's list. Boxes are delivered in this order.</summary>
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Bumped whenever this box changes. The version is part of the box's gift drop id and
|
||||
/// of a once-per-player claim, so a changed box is handed out again rather than being
|
||||
/// suppressed by the claim a player already made on the old one.
|
||||
/// </summary>
|
||||
public int Version { get; set; } = 1;
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public bool OncePerPlayer { get; set; } = true;
|
||||
|
||||
public string Title { get; set; } = "Room Visit Gift";
|
||||
public string Message { get; set; } = "Thanks for visiting!";
|
||||
|
||||
public string AvatarItem { get; set; } = "";
|
||||
public AvatarItemType AvatarItemType { get; set; } = AvatarItemType.Outfit;
|
||||
|
||||
public string EquipmentItem { get; set; } = "";
|
||||
public string EquipmentModificationGuid { get; set; } = "";
|
||||
|
||||
public string ConsumableItem { get; set; } = "";
|
||||
|
||||
public CurrencyType CurrencyType { get; set; } = CurrencyType.RecCenterTokens;
|
||||
public int CurrencyAmount { get; set; } = 0;
|
||||
|
||||
public int Xp { get; set; } = 0;
|
||||
|
||||
public GiftContext Context { get; set; } = GiftContext.Game_Drop;
|
||||
public Rarity Rarity { get; set; } = Rarity.Common;
|
||||
|
||||
/// <summary>The account the box is from, or zero for no sender.</summary>
|
||||
public long FromPlayerId { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomKey
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long RoomKeyId { get; set; }
|
||||
|
||||
public string ReplicationId { get; set; } = null!;
|
||||
|
||||
public long RoomId { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
public string Description { get; set; } = null!;
|
||||
|
||||
public int Price { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API.Rooms;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Config;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
|
||||
namespace RadicadeServer.API.Rooms.Keys.V1
|
||||
{
|
||||
[Route("api/roomkeys/v1")]
|
||||
[Route("econ/api/roomkeys/v1")]
|
||||
public class RoomKeysAPI : Controller
|
||||
{
|
||||
private const int DefaultMaxKeysPerRoom = 500;
|
||||
|
||||
RecDBContext db;
|
||||
public RoomKeysAPI(RecDBContext context) {
|
||||
this.db = context;
|
||||
}
|
||||
|
||||
[HttpGet("room")]
|
||||
public IActionResult GetRoomKeysForRoom(long? roomId)
|
||||
{
|
||||
if (roomId is not > 0)
|
||||
return Ok(Array.Empty<RoomKey>());
|
||||
|
||||
return Ok(
|
||||
db.RoomKeys.Where(roomKey => roomKey.RoomId == roomId.Value).ToList()
|
||||
);
|
||||
}
|
||||
|
||||
[HttpGet("mine")]
|
||||
public async Task<IActionResult> GetOwnedRoomkeys([FromQuery] long? roomId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (roomId == null || userId == null) return Ok(Array.Empty<object>());
|
||||
var ownedIds = (await db.Unlocks.AsNoTracking()
|
||||
.FirstOrDefaultAsync(unlocks => unlocks.AccountId == userId.Value))?.RoomKeys
|
||||
.FromJson<List<long>>() ?? new List<long>();
|
||||
var keys = await db.RoomKeys.AsNoTracking()
|
||||
.Where(key => key.RoomId == roomId.Value && ownedIds.Contains(key.RoomKeyId))
|
||||
.ToListAsync();
|
||||
return Ok(keys);
|
||||
}
|
||||
|
||||
[HttpGet("purchased/{roomKeyId:long}")]
|
||||
public async Task<IActionResult> IsRoomKeyPurchased(long roomKeyId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null || roomKeyId <= 0) return Ok(false);
|
||||
|
||||
var roomKey = await db.RoomKeys.AsNoTracking()
|
||||
.FirstOrDefaultAsync(key => key.RoomKeyId == roomKeyId);
|
||||
if (roomKey == null) return Ok(false);
|
||||
|
||||
var unlocks = await db.Unlocks.AsNoTracking()
|
||||
.FirstOrDefaultAsync(row => row.AccountId == userId.Value);
|
||||
var ownedIds = unlocks?.RoomKeys.FromJson<List<long>>() ?? new List<long>();
|
||||
return Ok(ownedIds.Contains(roomKeyId));
|
||||
}
|
||||
|
||||
[HttpPost("create")]
|
||||
[HttpPost("create/{roomId}")]
|
||||
public async Task<IActionResult> CreateRoomKey()
|
||||
{
|
||||
var body = await RoomEconomyPayload.ReadBodyAsync(Request);
|
||||
|
||||
var roomId = await RoomEconomyPayload.RoomIdAsync(db, Request, body, "roomId", "roomKeyRoomId")
|
||||
?? ReadRoomIdFromReferrer();
|
||||
if (roomId is not > 0) return BadRequest(new { Message = "roomId is required." });
|
||||
|
||||
var key = new RoomKey
|
||||
{
|
||||
RoomId = roomId.Value,
|
||||
Name = RoomEconomyPayload.String(body, "name", "keyName") ?? "Room Key",
|
||||
Description = RoomEconomyPayload.String(body, "description") ?? string.Empty,
|
||||
Price = (int)(RoomEconomyPayload.Long(body, "price", "tokenPrice", "cost") ?? 0),
|
||||
ReplicationId = RoomEconomyPayload.String(body, "replicationId") ?? Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
var maxKeys = await MaxKeysPerRoomAsync();
|
||||
var existing = await db.RoomKeys.CountAsync(existing => existing.RoomId == roomId.Value);
|
||||
if (existing >= maxKeys)
|
||||
{
|
||||
return BadRequest(new { Message = $"A room can have at most {maxKeys} keys." });
|
||||
}
|
||||
|
||||
await db.RoomKeys.AddAsync(key);
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(key);
|
||||
}
|
||||
|
||||
private long? ReadRoomIdFromReferrer()
|
||||
{
|
||||
var referer = Request.Headers.Referer.FirstOrDefault();
|
||||
if (!Uri.TryCreate(referer, UriKind.Absolute, out var uri)) return null;
|
||||
var parts = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
for (var index = parts.Length - 1; index >= 0; index--)
|
||||
if (long.TryParse(parts[index], out var roomId) && roomId > 0) return roomId;
|
||||
return null;
|
||||
}
|
||||
|
||||
private string Describe(System.Text.Json.JsonElement? body)
|
||||
{
|
||||
var raw = RoomEconomyPayload.RawBody(Request);
|
||||
if (raw.Length == 0 && body == null) return "(empty)";
|
||||
if (raw.Length == 0) raw = body?.ToString() ?? "";
|
||||
return raw.Length <= 1000 ? raw : raw[..1000] + "...";
|
||||
}
|
||||
|
||||
private async Task<int> MaxKeysPerRoomAsync()
|
||||
{
|
||||
var config = await db.Config.FirstOrDefaultAsync();
|
||||
var limit = config?.RoomKeyConfig?.FromJson<RoomKeyConfig>()?.MaxKeysPerRoom ?? 0;
|
||||
return limit > 0 ? limit : DefaultMaxKeysPerRoom;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomLoadScreen
|
||||
{
|
||||
public long RoomId { get; set; }
|
||||
public string ImageName { get; set; } = null!;
|
||||
public string Title { get; set; } = null!;
|
||||
public string Subtitle { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomPlayerData
|
||||
{
|
||||
public string data { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.RecNet;
|
||||
using RadicadeServer.RecNet.DB;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.Utils;
|
||||
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
|
||||
|
||||
namespace RadicadeServer.RoomServer
|
||||
{
|
||||
[Route("rooms")]
|
||||
public class RoomPlaylistAPI(RecDBContext db) : Controller
|
||||
{
|
||||
private async Task<PlaylistRoom?> GetBestOfPlaylist()
|
||||
{
|
||||
var topRooms = await db.Rooms
|
||||
.Select(r => new
|
||||
{
|
||||
r.RoomId,
|
||||
r.CreatorAccountId,
|
||||
Engagement = db.RoomInteractions
|
||||
.Where(i => i.RoomId == r.RoomId)
|
||||
.Sum(i => (i.Favorited ? 1 : 0) + (i.Cheered ? 1 : 0))
|
||||
})
|
||||
.OrderByDescending(x => x.Engagement)
|
||||
.ToListAsync();
|
||||
|
||||
var convertedRooms = new List<FullRoom>();
|
||||
var tags = new List<ItemTag>();
|
||||
foreach (var foundRoom in topRooms)
|
||||
{
|
||||
var room = await RoomUtils.GetRoom(foundRoom.RoomId);
|
||||
|
||||
if (room == null || room.Accessibility != RecNet.Enums.Accessibility.Public || room.IsDorm) continue;
|
||||
|
||||
convertedRooms.Add(room);
|
||||
foreach (var tag in room.Tags)
|
||||
tags.Add(tag);
|
||||
}
|
||||
|
||||
if (convertedRooms.Count <= 0)
|
||||
return null;
|
||||
|
||||
var mainRoom = new PlaylistRoom(convertedRooms[0]);
|
||||
|
||||
return mainRoom;
|
||||
}
|
||||
|
||||
[HttpGet("playlists/{playlistId}")]
|
||||
public async Task<object?> GetPlaylistRooms(long playlistId)
|
||||
{
|
||||
List<FullRoom> convertedRooms;
|
||||
var tags = new List<ItemTag>();
|
||||
|
||||
if (playlistId == 24226488)
|
||||
{
|
||||
var mainRoom = await GetBestOfPlaylist();
|
||||
if (mainRoom == null) return null;
|
||||
convertedRooms = mainRoom.Rooms;
|
||||
tags = mainRoom.Tags;
|
||||
}
|
||||
else
|
||||
{
|
||||
var playlist = await db.Playlists.FirstOrDefaultAsync(x => x.PlaylistId == playlistId);
|
||||
if (playlist == null) return null;
|
||||
|
||||
var keywords = playlist.GetKeywords();
|
||||
var foundRooms = await db.Rooms
|
||||
.Where(room => room.Accessibility == RecNet.Enums.Accessibility.Public
|
||||
&& keywords.Any(k => room.Name.Contains(k) || room.Tags.Contains(k)))
|
||||
.ToListAsync();
|
||||
|
||||
convertedRooms = new List<FullRoom>();
|
||||
foreach (var foundRoom in foundRooms)
|
||||
{
|
||||
var room = await RoomUtils.GetRoom(foundRoom.RoomId);
|
||||
if (room != null)
|
||||
{
|
||||
convertedRooms.Add(room);
|
||||
foreach (var tag in room.Tags)
|
||||
tags.Add(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var roomDtos = convertedRooms.Select(room => ToPlaylistRoomDict(room, playlistId)).ToArray();
|
||||
|
||||
var tagDtos = tags
|
||||
.Select(t => new Dictionary<string, object?> { ["Tag"] = t.Tag, ["Type"] = (int)t.Type })
|
||||
.ToArray();
|
||||
|
||||
var root = ToPlaylistRoomDict(convertedRooms.FirstOrDefault(), playlistId);
|
||||
|
||||
root["Rooms"] = roomDtos;
|
||||
root["Tags"] = tagDtos;
|
||||
root["community"] = Array.Empty<object>();
|
||||
root["junior"] = Array.Empty<object>();
|
||||
root["pickup"] = Array.Empty<object>();
|
||||
root["screen"] = Array.Empty<object>();
|
||||
root["teleportvr"] = Array.Empty<object>();
|
||||
root["walkvr"] = Array.Empty<object>();
|
||||
root["PinnedFilters"] = Array.Empty<object>();
|
||||
root["PopularFilters"] = Array.Empty<object>();
|
||||
root["TrendingFilters"] = Array.Empty<object>();
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> ToPlaylistRoomDict(FullRoom? room, long playlistId)
|
||||
{
|
||||
var dict = new Dictionary<string, object?>
|
||||
{
|
||||
["AnnouncementId"] = 0L,
|
||||
["AnnouncementType"] = 0,
|
||||
["Body"] = "",
|
||||
["LinkName"] = "",
|
||||
["LinkType"] = 0,
|
||||
["LinkUri"] = "",
|
||||
["Platform"] = 0,
|
||||
["Title"] = "",
|
||||
["PlaylistId"] = playlistId,
|
||||
["RoomId"] = 0L,
|
||||
["IsDorm"] = false,
|
||||
["MaxPlayers"] = 0,
|
||||
["MaxPlayerCalculationMode"] = 0,
|
||||
["CloningAllowed"] = false,
|
||||
["DisableMicAutoMute"] = false,
|
||||
["DisableRoomComments"] = false,
|
||||
["EncryptVoiceChat"] = false,
|
||||
["Name"] = "",
|
||||
["Description"] = "",
|
||||
["ImageName"] = "",
|
||||
["WarningMask"] = 0,
|
||||
["CustomWarning"] = "",
|
||||
["CreatorAccountId"] = 0L,
|
||||
["State"] = 0,
|
||||
["Accessibility"] = 1,
|
||||
["SupportsLevelVoting"] = false,
|
||||
["IsRRO"] = false,
|
||||
["SupportsScreens"] = false,
|
||||
["SupportsWalkVR"] = false,
|
||||
["SupportsTeleportVR"] = false,
|
||||
["SupportsVRLow"] = false,
|
||||
["SupportsQuest2"] = false,
|
||||
["SupportsMobile"] = false,
|
||||
["SupportsJuniors"] = false,
|
||||
["MinLevel"] = 0,
|
||||
["CreatedAt"] = DateTime.UnixEpoch,
|
||||
["Stats"] = new Dictionary<string, object?>
|
||||
{
|
||||
["RoomId"] = 0L,
|
||||
["CheerCount"] = 0,
|
||||
["FavoriteCount"] = 0,
|
||||
["VisitorCount"] = 0,
|
||||
["VisitCount"] = 0
|
||||
},
|
||||
["Version"] = 1,
|
||||
["SubRooms"] = Array.Empty<object>(),
|
||||
["Roles"] = Array.Empty<object>(),
|
||||
["LoadScreens"] = Array.Empty<object>(),
|
||||
["PromoImages"] = Array.Empty<object>(),
|
||||
["PromoExternalContent"] = Array.Empty<object>(),
|
||||
["DataBlob"] = "",
|
||||
["Tags"] = Array.Empty<object>()
|
||||
};
|
||||
|
||||
if (room != null)
|
||||
{
|
||||
var element = System.Text.Json.JsonSerializer.SerializeToElement(room, room.GetType());
|
||||
var roomDict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object?>>(element) ?? new Dictionary<string, object?>();
|
||||
foreach (var kv in roomDict)
|
||||
dict[kv.Key] = kv.Value;
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
[HttpGet("rooms/recommendations")]
|
||||
public async Task<List<FullRoom>?> GetRecommendedRooms()
|
||||
{
|
||||
var topRooms = await db.Rooms
|
||||
.Select(r => new
|
||||
{
|
||||
r.RoomId,
|
||||
r.CreatorAccountId,
|
||||
Engagement = db.RoomInteractions
|
||||
.Where(i => i.RoomId == r.RoomId)
|
||||
.Sum(i => (i.Favorited ? 1 : 0) + (i.Cheered ? 1 : 0))
|
||||
})
|
||||
.OrderByDescending(x => x.Engagement)
|
||||
.Take(10)
|
||||
.ToListAsync();
|
||||
|
||||
var convertedRooms = new List<FullRoom>();
|
||||
foreach (var room in topRooms)
|
||||
{
|
||||
var converted = await RoomUtils.GetRoom(room.RoomId);
|
||||
if (converted != null && converted.Accessibility == RecNet.Enums.Accessibility.Public && !converted.IsDorm)
|
||||
convertedRooms.Add(converted);
|
||||
}
|
||||
|
||||
return convertedRooms;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.API.Notifications;
|
||||
using RadicadeServer.API.Storefront;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Enums.Messages;
|
||||
using RadicadeServer.RecNet.Enums.Store;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
using RadicadeServer.RecNet.Progression;
|
||||
|
||||
namespace RadicadeServer.Utils
|
||||
{
|
||||
public static class RoomProgressionService
|
||||
{
|
||||
public const int XpPerInterval = 10;
|
||||
|
||||
public static readonly TimeSpan Interval = TimeSpan.FromMinutes(5);
|
||||
|
||||
public const int RoomXpCap = 60;
|
||||
|
||||
public const int DailyXpCap = 150;
|
||||
|
||||
public static readonly TimeSpan DailyCapCooldown = TimeSpan.FromHours(24);
|
||||
|
||||
public const string ProgressionLevelUpdateNotificationId = "PlayerProgressionLevelUpdate";
|
||||
|
||||
public static async Task<int> AwardRoomXpAsync(RecDBContext db, long playerId, long roomId, bool isPublicRoom)
|
||||
{
|
||||
if (playerId <= 0 || roomId <= 0 || !isPublicRoom) return 0;
|
||||
|
||||
var progression = await db.Progression.FirstOrDefaultAsync(x => x.PlayerId == playerId);
|
||||
if (progression == null)
|
||||
{
|
||||
progression = new ProgressionData { PlayerId = playerId };
|
||||
await db.Progression.AddAsync(progression);
|
||||
}
|
||||
|
||||
var json = progression.GetProgressionJson() ?? new ProgressionJson();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (json.XpEarnedTodayDate != now.Date)
|
||||
{
|
||||
json.XpEarnedToday = 0;
|
||||
json.XpEarnedTodayDate = now.Date;
|
||||
json.XpDailyCapReachedAt = null;
|
||||
}
|
||||
|
||||
if (json.XpEarnedToday >= DailyXpCap)
|
||||
{
|
||||
if (json.XpDailyCapReachedAt is { } reachedAt && now >= reachedAt.Add(DailyCapCooldown))
|
||||
{
|
||||
json.XpEarnedToday = 0;
|
||||
json.XpEarnedTodayDate = now.Date;
|
||||
json.XpDailyCapReachedAt = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
json.XpDailyCapReachedAt ??= now;
|
||||
progression.Progress = json.ToJson();
|
||||
await db.SaveChangesAsync();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (json.RoomXpRoomId != roomId)
|
||||
{
|
||||
json.RoomXpRoomId = roomId;
|
||||
json.RoomXpEarned = 0;
|
||||
json.RoomXpLastGrantAt = now;
|
||||
progression.Progress = json.ToJson();
|
||||
await db.SaveChangesAsync();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (json.RoomXpEarned >= RoomXpCap)
|
||||
{
|
||||
progression.Progress = json.ToJson();
|
||||
await db.SaveChangesAsync();
|
||||
return 0;
|
||||
}
|
||||
|
||||
json.RoomXpLastGrantAt ??= now;
|
||||
|
||||
int granted = 0;
|
||||
while (now - json.RoomXpLastGrantAt.Value >= Interval)
|
||||
{
|
||||
int roomHeadroom = RoomXpCap - (json.RoomXpEarned + granted);
|
||||
int dailyHeadroom = DailyXpCap - (json.XpEarnedToday + granted);
|
||||
int instalment = Math.Min(XpPerInterval, Math.Min(roomHeadroom, dailyHeadroom));
|
||||
if (instalment <= 0) break;
|
||||
|
||||
granted += instalment;
|
||||
json.RoomXpLastGrantAt = json.RoomXpLastGrantAt.Value.Add(Interval);
|
||||
}
|
||||
|
||||
if (granted == 0 && now - json.RoomXpLastGrantAt.Value >= Interval)
|
||||
json.RoomXpLastGrantAt = now;
|
||||
|
||||
if (granted == 0)
|
||||
{
|
||||
progression.Progress = json.ToJson();
|
||||
await db.SaveChangesAsync();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int applied = (int)Math.Min(granted, int.MaxValue);
|
||||
|
||||
json.RoomXpEarned += applied;
|
||||
json.XpEarnedToday += applied;
|
||||
json.XpEarnedTodayDate = now.Date;
|
||||
json.LastXpEarnedAt = now;
|
||||
if (json.XpEarnedToday >= DailyXpCap)
|
||||
json.XpDailyCapReachedAt ??= now;
|
||||
|
||||
int oldLevel = progression.Level;
|
||||
progression.XP = (int)Math.Min(progression.XP + applied, int.MaxValue);
|
||||
progression.Progress = json.ToJson();
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await StorefrontShared.SendXpBalanceUpdateAsync(playerId,
|
||||
LevelUpGiftService.XpIntoCurrentLevel(progression), applied,
|
||||
StorefrontBalanceAddTypes.DirectBalanceWithMultiplier);
|
||||
|
||||
int computedLevel = LevelUpGiftService.ComputeLevel(LevelUpGiftService.ParseMap(), progression.XP);
|
||||
if (computedLevel > progression.Level) progression.Level = computedLevel;
|
||||
|
||||
await LevelUpGiftService.DeliverLevelUpGiftsAsync(db, playerId,
|
||||
recomputeFromXp: true, fromLevel: oldLevel, notify: true);
|
||||
|
||||
await SendProgressionLevelUpdateAsync(db, playerId, progression);
|
||||
|
||||
if (progression.Level > oldLevel)
|
||||
{
|
||||
await PushProgressionUpdateAsync(db, playerId);
|
||||
}
|
||||
|
||||
return applied;
|
||||
}
|
||||
|
||||
public static async Task SendProgressionLevelUpdateAsync(RecDBContext db, long playerId,
|
||||
ProgressionData? progression = null)
|
||||
{
|
||||
if (playerId <= 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
progression ??= await db.Progression.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.PlayerId == playerId);
|
||||
if (progression == null) return;
|
||||
|
||||
var notification = new Notification
|
||||
{
|
||||
Id = ProgressionLevelUpdateNotificationId,
|
||||
Msg = new PlayerProgressionLevelUpdateNotification(progression)
|
||||
};
|
||||
|
||||
await Notify.SendNotify(playerId, notification);
|
||||
|
||||
await Notify.SendToSubscribers(playerId, notification);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task PushProgressionUpdateAsync(RecDBContext db, long playerId)
|
||||
{
|
||||
await SendProgressionLevelUpdateAsync(db, playerId);
|
||||
|
||||
try
|
||||
{
|
||||
var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == playerId);
|
||||
if (profile == null) return;
|
||||
|
||||
var plrData = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == playerId);
|
||||
|
||||
var progression = await db.Progression.AsNoTracking().FirstOrDefaultAsync(x => x.PlayerId == playerId);
|
||||
if (progression != null)
|
||||
await StorefrontShared.SendXpBalanceUpdateAsync(playerId,
|
||||
LevelUpGiftService.XpIntoCurrentLevel(progression), 0,
|
||||
StorefrontBalanceAddTypes.LevelUp);
|
||||
|
||||
await Notify.SendNotify(playerId, new Notification
|
||||
{
|
||||
Id = "AccountUpdate",
|
||||
Msg = profile
|
||||
});
|
||||
|
||||
await Notify.UpdateSubscriptions(playerId, Notifs.SubscriptionUpdateProfile, profile);
|
||||
|
||||
if (plrData != null)
|
||||
await Notify.SendNotify(playerId, new Notification
|
||||
{
|
||||
Id = "SelfAccountUpdate",
|
||||
Msg = PlayerUtils.BuildSelfAccount(profile, plrData)
|
||||
});
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Text.Json.Serialization;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomRole
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity), JsonIgnore]
|
||||
public long RoleId { get; set; }
|
||||
[JsonIgnore]
|
||||
public long RoomId { get; set; }
|
||||
|
||||
public long AccountId { get; set; }
|
||||
public RoleType Role { get; set; }
|
||||
public RoleType InvitedRole { get; set; }
|
||||
|
||||
public long? LastChangedByAccountId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomRoleResponse
|
||||
{
|
||||
public string context { get; set; } = null!;
|
||||
public long? creatorPlayerCoOwnerRoomPermission { get; set; }
|
||||
public long? creatorPlayerHostRoomPermission { get; set; }
|
||||
public long? creatorPlayerModeratorRoomPermission { get; set; }
|
||||
public long? creatorPlayerRole { get; set; }
|
||||
public long roomId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomSearchResult
|
||||
{
|
||||
public long TotalResults { get; set; }
|
||||
public List<RoomDetail> Results { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,1860 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SixLabors.ImageSharp;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using RadicadeServer.API.Notifications;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Messages;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Messages;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.API.Studio;
|
||||
using RadicadeServer.Radicade.Discord;
|
||||
using RadicadeServer.Radicade.Discord.Templates;
|
||||
using RadicadeServer.Utils;
|
||||
|
||||
namespace RadicadeServer.RoomServer
|
||||
{
|
||||
[Route("rooms/")]
|
||||
public class RoomServerAPI : Controller
|
||||
{
|
||||
private const int RoomSearchHardLimit = 50;
|
||||
|
||||
RecDBContext db;
|
||||
public RoomServerAPI(RecDBContext context)
|
||||
{
|
||||
this.db = context;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet("showcase/{playerId}")]
|
||||
public async Task<ActionResult<long[]>> GetPlayerShowcaseRooms(long playerId)
|
||||
{
|
||||
var roomIds = await db.RoomRoles
|
||||
.Where(x => x.AccountId == playerId && x.Role >= RoleType.CoOwner)
|
||||
.Select(x => x.RoomId)
|
||||
.ToListAsync();
|
||||
|
||||
if (roomIds.Count == 0)
|
||||
{
|
||||
roomIds = await db.Rooms
|
||||
.Where(x => x.CreatorAccountId == playerId && x.Accessibility >= Accessibility.Public)
|
||||
.Select(x => x.RoomId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
var showcaseRooms = await RoomUtils.VisibleRooms(
|
||||
await db.Rooms.Where(x => roomIds.Contains(x.RoomId)).ToListAsync(),
|
||||
User.GetUserId());
|
||||
|
||||
return Ok(showcaseRooms.Select(x => x.RoomId).Distinct().ToArray());
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId}"), Authorize]
|
||||
public async Task<Success> DeleteRoom(long roomId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return new Success(false, "Room does not exist.", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.Creator);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var roles = await db.RoomRoles.Where(x => x.RoomId == roomId).ToListAsync();
|
||||
foreach (var role in roles)
|
||||
{
|
||||
db.RoomRoles.Remove(role);
|
||||
}
|
||||
|
||||
var interactions = await db.RoomInteractions.Where(x => x.RoomId == roomId).ToListAsync();
|
||||
foreach (var i in interactions)
|
||||
{
|
||||
db.RoomInteractions.Remove(i);
|
||||
}
|
||||
|
||||
var keys = await db.RoomKeys.Where(x => x.RoomId == roomId).ToListAsync();
|
||||
foreach (var key in keys)
|
||||
{
|
||||
db.RoomKeys.Remove(key);
|
||||
}
|
||||
|
||||
var stats = await db.RoomStats.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (stats != null)
|
||||
db.RoomStats.Remove(stats);
|
||||
|
||||
var subrooms = await db.SubRooms.Where(x => x.RoomId == roomId).ToListAsync();
|
||||
foreach (var subroom in subrooms)
|
||||
{
|
||||
db.SubRooms.Remove(subroom);
|
||||
}
|
||||
|
||||
var prevSubrooms = await db.SubRoomHistory.Where(x => x.RoomId == roomId).ToListAsync();
|
||||
foreach (var subroom in prevSubrooms)
|
||||
{
|
||||
db.SubRoomHistory.Remove(subroom);
|
||||
}
|
||||
|
||||
db.Rooms.Remove(room);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
[HttpDelete("rooms/{roomId}/subrooms/{subroomId}"), Authorize]
|
||||
public async Task<Success> DeleteSubRoom(long roomId, long subroomId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return new Success(false, "Room does not exist.", null);
|
||||
|
||||
var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subroomId && x.RoomId == roomId);
|
||||
if (subroom == null) return new Success(false, "Subroom does not exist.", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.Creator);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var history = await db.SubRoomHistory.Where(x => x.RoomId == roomId && x.SubRoomId == subroomId).ToListAsync();
|
||||
foreach (var historyItem in history)
|
||||
{
|
||||
db.SubRoomHistory.Remove(historyItem);
|
||||
}
|
||||
|
||||
db.SubRooms.Remove(subroom);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/playerdata/me")]
|
||||
public ActionResult<RoomPlayerData> GetRoomPlayerData()
|
||||
{
|
||||
return Ok(new RoomPlayerData { data = "" });
|
||||
}
|
||||
|
||||
[HttpGet("rooms/curated_playlists")]
|
||||
public async Task<IActionResult> GetCuratedPlaylists()
|
||||
{
|
||||
var playlistIds = await db.Playlists
|
||||
.OrderBy(x => x.PlaylistId)
|
||||
.Select(x => x.PlaylistId)
|
||||
.ToListAsync();
|
||||
|
||||
if (playlistIds.Count == 0)
|
||||
playlistIds.Add(24226488);
|
||||
|
||||
return Ok(playlistIds.Distinct().ToArray());
|
||||
}
|
||||
|
||||
[HttpGet("rooms/moderatedby/me"), Authorize]
|
||||
public async Task<List<RoomData>?> GetRoomsModeratedByMe()
|
||||
{
|
||||
long? userId = User.GetUserId();
|
||||
if (userId == null) return null;
|
||||
|
||||
var roles = await db.RoomRoles.Where(x => x.AccountId == userId && x.Role >= RoleType.Moderator).ToListAsync();
|
||||
var rooms = new List<RoomData>();
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == role.RoomId);
|
||||
if (room != null) rooms.Add(room);
|
||||
}
|
||||
|
||||
return await RoomUtils.VisibleRooms(rooms, userId);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/visitedby/{playerId:long}"), Authorize]
|
||||
public async Task<IActionResult> GetVisitedRoomsForPlayer(long playerId, [FromQuery] int skip = 0, [FromQuery] int take = 10)
|
||||
{
|
||||
var viewerId = User.GetUserId();
|
||||
|
||||
skip = Math.Max(skip, 0);
|
||||
take = Math.Clamp(take, 1, 100);
|
||||
|
||||
var roomIds = await db.RoomInteractions
|
||||
.Where(x => x.PlayerId == playerId && x.Visited)
|
||||
.OrderByDescending(x => x.LastVisted)
|
||||
.Select(x => x.RoomId)
|
||||
.Distinct()
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
var rooms = new List<RoomData>();
|
||||
foreach (var roomId in roomIds)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room != null) rooms.Add(room);
|
||||
}
|
||||
|
||||
rooms = await RoomUtils.VisibleRooms(rooms, viewerId);
|
||||
var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms);
|
||||
return Ok(convert);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/visitedby/me"), Authorize]
|
||||
public async Task<IActionResult> GetVisitedRooms([FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
{
|
||||
long? userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
skip = Math.Max(skip, 0);
|
||||
take = Math.Clamp(take, 1, 100);
|
||||
|
||||
var roomIds = await db.RoomInteractions
|
||||
.Where(x => x.PlayerId == userId && x.Visited)
|
||||
.OrderByDescending(x => x.LastVisted)
|
||||
.Select(x => x.RoomId)
|
||||
.Distinct()
|
||||
.Skip(skip)
|
||||
.Take(take)
|
||||
.ToListAsync();
|
||||
|
||||
var rooms = new List<RoomData>();
|
||||
foreach (var roomId in roomIds)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room != null) rooms.Add(room);
|
||||
}
|
||||
|
||||
rooms = await RoomUtils.VisibleRooms(rooms, userId);
|
||||
var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms);
|
||||
return Ok(convert);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/topcreators")]
|
||||
public async Task<List<RoomDetail>?> GetRoomsFromTopCreators()
|
||||
{
|
||||
var topCreators = await db.RoomInteractions.Where(i => i.Favorited)
|
||||
.Join(db.Rooms,
|
||||
interaction => interaction.RoomId,
|
||||
room => room.RoomId,
|
||||
(interaction, room) => new
|
||||
{
|
||||
room.CreatorAccountId
|
||||
}).GroupBy(x => x.CreatorAccountId)
|
||||
.Select(g => new
|
||||
{
|
||||
CreatorId = g.Key,
|
||||
FavoriteCount = g.Count()
|
||||
}).OrderByDescending(x => x.FavoriteCount).Take(10).Select(x => x.CreatorId).ToListAsync();
|
||||
|
||||
if (!topCreators.Any())
|
||||
return [];
|
||||
|
||||
var rooms = await db.Rooms.Where(r => topCreators.Contains(r.CreatorAccountId)).OrderByDescending(r => r.RoomId).Take(10 * 3).ToListAsync();
|
||||
rooms.RemoveAll(x => x.Accessibility != Accessibility.Public);
|
||||
|
||||
return await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/bulk")]
|
||||
public async Task <List<RoomData>?> GetBulkRooms([FromQuery] string? name, [FromQuery] List<long>? id)
|
||||
{
|
||||
List<RoomData> rooms = new List<RoomData>();
|
||||
|
||||
if (name != null)
|
||||
{
|
||||
rooms = await db.Rooms.Where(x => x.Name.Contains(name)).ToListAsync();
|
||||
} else if (id != null)
|
||||
{
|
||||
rooms.Clear();
|
||||
foreach (var i in id)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == i);
|
||||
if (room != null)
|
||||
rooms.Add(room);
|
||||
}
|
||||
}
|
||||
|
||||
return await RoomUtils.VisibleRooms(rooms, User.GetUserId());
|
||||
}
|
||||
[HttpGet("featuredrooms/current")]
|
||||
public async Task<IActionResult> GetCurrentFeaturedRooms()
|
||||
{
|
||||
var candidates = await db.Rooms.AsNoTracking()
|
||||
.Where(x => x.State == RoomState.Active && x.Tags.ToLower().Contains("featured"))
|
||||
.OrderBy(x => x.RoomId)
|
||||
.ToListAsync();
|
||||
|
||||
var featured = candidates
|
||||
.Where(room => StringTools.ReadTags(room.Tags)
|
||||
.Any(tag => string.Equals(tag.Tag, "featured", StringComparison.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
|
||||
featured = await RoomUtils.VisibleRooms(featured, User.GetUserId());
|
||||
|
||||
if (featured.Count == 0)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(new FeaturedRoomGroup
|
||||
{
|
||||
FeaturedRoomGroupId = 1,
|
||||
Name = "Featured Rooms",
|
||||
Rooms = featured.Select(room => new FeaturedRoom
|
||||
{
|
||||
RoomId = room.RoomId,
|
||||
RoomName = room.Name,
|
||||
ImageName = string.IsNullOrWhiteSpace(room.ImageName) ? room.Name : room.ImageName
|
||||
}).ToList()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
[HttpGet("roomsandplaylists/search")]
|
||||
public async Task<RoomSearchResult?> GetBulkRoomsOld([FromQuery] string query)
|
||||
{
|
||||
if (query == null) return null;
|
||||
|
||||
query = query.Replace("#", "").Trim();
|
||||
|
||||
var rooms = await db.Rooms.Where(x => (x.Name.ToLower().Contains(query) || x.Tags.ToLower().Contains(query)) && x.Accessibility == Accessibility.Public).ToListAsync();
|
||||
var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms);
|
||||
return new RoomSearchResult { TotalResults = rooms.Count, Results = convert };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet("rooms/search_rooms/{source}")]
|
||||
public async Task<IActionResult> SearchRoomsBySource(string source, [FromQuery] int skip = -1, [FromQuery] int take = 20)
|
||||
{
|
||||
var (src, pSkip, pTake) = ParseEmbeddedQuery(source, 20);
|
||||
if (skip < 0) skip = pSkip;
|
||||
if (take == 20 && pTake != 20) take = pTake;
|
||||
skip = Math.Max(skip, 0);
|
||||
take = Math.Clamp(take, 1, 100);
|
||||
|
||||
if (string.Equals(src, "MyClubhouses", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(src, "MyClubs", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(src, "Clubhouse", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Ok(new RoomSearchResult { TotalResults = 0, Results = new List<RoomDetail>() });
|
||||
}
|
||||
|
||||
IQueryable<RoomData> q;
|
||||
if (!string.IsNullOrWhiteSpace(src) && !src.Contains('&'))
|
||||
{
|
||||
var kw = Uri.UnescapeDataString(src).Replace('+', ' ').Trim().Split(' ',
|
||||
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(k => k.TrimStart('#')).Where(k => k.Length > 0).ToList();
|
||||
q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active
|
||||
&& kw.All(k => x.Name.ToLower().Contains(k) || x.Tags.ToLower().Contains(k)));
|
||||
}
|
||||
else
|
||||
{
|
||||
q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active);
|
||||
}
|
||||
|
||||
var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync();
|
||||
var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms);
|
||||
return Ok(new RoomSearchResult { TotalResults = convert.Count, Results = convert });
|
||||
}
|
||||
|
||||
private static (string Source, int Skip, int Take) ParseEmbeddedQuery(string? source, int defTake)
|
||||
{
|
||||
var src = source ?? string.Empty;
|
||||
int skip = 0, take = defTake;
|
||||
int amp = src.IndexOf('&');
|
||||
if (amp >= 0)
|
||||
{
|
||||
foreach (var pair in src.Substring(amp + 1).Split('&', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var kv = pair.Split('=', 2);
|
||||
if (kv.Length == 2 && int.TryParse(kv[1], out var n))
|
||||
{
|
||||
if (kv[0] == "skip") skip = n;
|
||||
else if (kv[0] == "take") take = n;
|
||||
}
|
||||
}
|
||||
src = src.Substring(0, amp);
|
||||
}
|
||||
return (Uri.UnescapeDataString(src), Math.Max(skip, 0), Math.Clamp(take, 1, 100));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/hot_rooms/{*source}")]
|
||||
[HttpGet("hot_rooms/{*source}")]
|
||||
public async Task<IActionResult> GetHotRoomsPaged(string? source)
|
||||
{
|
||||
var (query, skip, take) = ParseEmbeddedQuery(source, 24);
|
||||
query = query.Trim();
|
||||
|
||||
IQueryable<RoomData> q = db.Rooms
|
||||
.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(query) && query != "all")
|
||||
{
|
||||
var tags = query.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(t => t.TrimStart('#')).Where(t => t.Length > 0).ToList();
|
||||
if (tags.Count > 0)
|
||||
q = q.Where(x => tags.All(t => x.Tags.Contains(t)));
|
||||
}
|
||||
|
||||
var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync();
|
||||
if (rooms.Count == 0 && skip == 0)
|
||||
{
|
||||
rooms = await db.Rooms
|
||||
.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active)
|
||||
.OrderBy(x => x.RoomId).Take(take).ToListAsync();
|
||||
}
|
||||
return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/carousel/{*source}")]
|
||||
public async Task<IActionResult> GetCarouselRooms(string? source, [FromQuery] int skip = -1, [FromQuery] int take = 20)
|
||||
{
|
||||
var (src, pSkip, pTake) = ParseEmbeddedQuery(source, 20);
|
||||
if (skip < 0) skip = pSkip;
|
||||
if (take == 20 && pTake != 20) take = pTake;
|
||||
skip = Math.Max(skip, 0);
|
||||
take = Math.Clamp(take, 1, 100);
|
||||
|
||||
var q = db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active);
|
||||
|
||||
var keyword = src.Trim().TrimStart('#').ToLowerInvariant();
|
||||
if (keyword.Length > 0)
|
||||
q = q.Where(x => x.Name.ToLower().Contains(keyword) || x.Tags.ToLower().Contains(keyword));
|
||||
|
||||
var rooms = await q.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync();
|
||||
|
||||
if (rooms.Count == 0 && skip == 0)
|
||||
{
|
||||
rooms = await db.Rooms
|
||||
.Where(x => x.Accessibility == Accessibility.Public && x.State == RoomState.Active)
|
||||
.OrderBy(x => x.RoomId).Take(take).ToListAsync();
|
||||
}
|
||||
|
||||
var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms);
|
||||
return Ok(new RoomSearchResult { Results = convert, TotalResults = convert.Count });
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("rooms/my_visited_rooms/{*source}"), Authorize]
|
||||
[HttpGet("my_visited_rooms/{*source}"), Authorize]
|
||||
public async Task<IActionResult> GetMyVisitedRoomsPaged(string? source)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (!userId.HasValue) return Unauthorized();
|
||||
var (_, skip, take) = ParseEmbeddedQuery(source, 24);
|
||||
|
||||
var rooms = await db.Rooms
|
||||
.Where(x => db.RoomInteractions.Any(i => i.PlayerId == userId && i.RoomId == x.RoomId)
|
||||
&& x.Accessibility == Accessibility.Public)
|
||||
.OrderByDescending(x => x.RoomId).Skip(skip).Take(take).ToListAsync();
|
||||
return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/roomsbycreators/{*source}")]
|
||||
[HttpGet("roomsbycreators/{*source}")]
|
||||
public async Task<IActionResult> GetRoomsByCreatorsPaged(string? source)
|
||||
{
|
||||
var (ids, skip, take) = ParseEmbeddedQuery(source, 24);
|
||||
var creatorIds = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Select(s => long.TryParse(s, out var n) ? n : -1)
|
||||
.Where(n => n > 0).ToList();
|
||||
|
||||
var rooms = creatorIds.Count == 0
|
||||
? new List<RoomData>()
|
||||
: await db.Rooms.Where(x => creatorIds.Contains(x.CreatorAccountId)
|
||||
&& x.Accessibility == Accessibility.Public && x.State == RoomState.Active)
|
||||
.OrderBy(x => x.RoomId).Skip(skip).Take(take).ToListAsync();
|
||||
return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/rro_ids")]
|
||||
public async Task<IActionResult> GetRroRoomIds()
|
||||
{
|
||||
var ids = await db.Rooms.Where(x => x.IsRRO && x.Accessibility == Accessibility.Public)
|
||||
.OrderBy(x => x.RoomId).Select(x => x.RoomId).ToListAsync();
|
||||
return Ok(ids);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/search")]
|
||||
public async Task<RoomSearchResult?> SearchRooms([FromQuery] string query)
|
||||
{
|
||||
if (query == null) return null;
|
||||
|
||||
query = query.ToLower();
|
||||
query = query.Replace("+", " ").Trim();
|
||||
query = query.Replace("#", "").Trim();
|
||||
|
||||
var keywords = query.Split(' ', StringSplitOptions.TrimEntries);
|
||||
|
||||
var rooms = await db.Rooms.Where(x =>
|
||||
keywords.All(k => (x.Name.ToLower().Contains(k) || x.Tags.ToLower().Contains(k)) && x.Accessibility == Accessibility.Public)
|
||||
).ToListAsync();
|
||||
var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(rooms);
|
||||
|
||||
return new RoomSearchResult { TotalResults = rooms.Count, Results = convert };
|
||||
}
|
||||
|
||||
[HttpGet("rooms/createdby/{who}"), Authorize]
|
||||
public async Task<IActionResult> GetRoomsBy(string who)
|
||||
{
|
||||
long? userId;
|
||||
|
||||
if (who == "me")
|
||||
userId = User.GetUserId();
|
||||
else
|
||||
userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId;
|
||||
if (userId == null)
|
||||
userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId;
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var roles = await db.RoomRoles.Where(x => x.AccountId == userId && x.Role >= RoleType.CoOwner).ToListAsync();
|
||||
var rooms = new List<RoomData>();
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == role.RoomId);
|
||||
if (room != null)
|
||||
rooms.Add(room);
|
||||
}
|
||||
|
||||
return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(
|
||||
await RoomUtils.VisibleRooms(rooms, User.GetUserId())));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/ownedby/{who}"), Authorize]
|
||||
public async Task<IActionResult> GetRoomsByOld(string who)
|
||||
{
|
||||
long? userId;
|
||||
|
||||
if (who == "me")
|
||||
userId = User.GetUserId();
|
||||
else
|
||||
userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId;
|
||||
if (userId == null)
|
||||
userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId;
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var roomIds = await db.RoomRoles
|
||||
.Where(x => x.AccountId == userId && x.Role >= RoleType.CoOwner)
|
||||
.Select(x => x.RoomId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var rooms = new List<RoomData>();
|
||||
foreach (var roomId in roomIds)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room != null) rooms.Add(room);
|
||||
}
|
||||
|
||||
return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(
|
||||
await RoomUtils.VisibleRooms(rooms, User.GetUserId())));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/hot")]
|
||||
public async Task<RoomSearchResult?> GetHotRooms(string? tag)
|
||||
{
|
||||
|
||||
if (tag == null)
|
||||
{
|
||||
var list = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.Name != "DormRoom").ToListAsync();
|
||||
var bulkConvert = await RoomUtils.BulkConvertRoomDataToRoomDetail(list);
|
||||
return new RoomSearchResult { Results = bulkConvert, TotalResults = bulkConvert.Count };
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (tag)
|
||||
{
|
||||
default:
|
||||
var list1 = await db.Rooms.Where(x => x.Tags.Contains(tag) && x.Accessibility == Accessibility.Public && !x.IsDorm).ToListAsync();
|
||||
if (list1.Count == 0)
|
||||
{
|
||||
list1 = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && !x.IsDorm).ToListAsync();
|
||||
}
|
||||
var bulkConvert1 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list1);
|
||||
return new RoomSearchResult { Results = bulkConvert1, TotalResults = bulkConvert1.Count };
|
||||
case "new":
|
||||
var list2 = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && !x.IsDorm).OrderByDescending(x => x.PublishedAt).ToListAsync();
|
||||
var bulkConvert2 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list2);
|
||||
return new RoomSearchResult { Results = bulkConvert2, TotalResults = bulkConvert2.Count };
|
||||
case "rro":
|
||||
var list3 = await db.Rooms.Where(x => x.IsRRO && !x.IsDorm).OrderByDescending(x => x.PublishedAt).ToListAsync();
|
||||
var bulkConvert3 = await RoomUtils.BulkConvertRoomDataToRoomDetail(list3);
|
||||
return new RoomSearchResult { Results = bulkConvert3, TotalResults = bulkConvert3.Count };
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[HttpGet("roomsandplaylists/hot")]
|
||||
public async Task<RoomSearchResult?> GetHotRoomsOld()
|
||||
{
|
||||
|
||||
var list = await db.Rooms.Where(x => x.Accessibility == Accessibility.Public && x.Name != "DormRoom").ToListAsync();
|
||||
var convert = await RoomUtils.BulkConvertRoomDataToRoomDetail(list);
|
||||
|
||||
return new RoomSearchResult { Results = convert, TotalResults = convert.Count };
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{id}/interactionby/me/favorite"), Authorize]
|
||||
public async Task<PlayerInteraction?> FavouriteRoom(long id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
PlayerInteractionDB? interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id);
|
||||
if (interaction == null)
|
||||
{
|
||||
var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = true };
|
||||
|
||||
await db.RoomInteractions.AddAsync(favoriteResult);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
interaction = favoriteResult;
|
||||
} else
|
||||
{
|
||||
interaction.Favorited = true;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return new PlayerInteraction(interaction);
|
||||
}
|
||||
[HttpDelete("rooms/{id}/interactionby/me/favorite"), Authorize]
|
||||
public async Task<PlayerInteraction?> UnfavouriteRoom(long id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
PlayerInteractionDB? interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id);
|
||||
if (interaction == null)
|
||||
{
|
||||
var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = false };
|
||||
|
||||
await db.RoomInteractions.AddAsync(favoriteResult);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
interaction = favoriteResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
interaction.Favorited = false;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return new PlayerInteraction(interaction);
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{id}/interactionby/me/cheer"), Authorize]
|
||||
public async Task<PlayerInteraction?> CheerRoom(long id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id);
|
||||
if (interaction == null)
|
||||
{
|
||||
var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = true, Favorited = false };
|
||||
|
||||
await db.RoomInteractions.AddAsync(favoriteResult);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
interaction = favoriteResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
interaction.Cheered = true;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return new PlayerInteraction(interaction);
|
||||
}
|
||||
[HttpDelete("rooms/{id}/interactionby/me/cheer"), Authorize]
|
||||
public async Task<PlayerInteraction?> UncheerRoom(long id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id);
|
||||
if (interaction == null)
|
||||
{
|
||||
var favoriteResult = new PlayerInteractionDB { PlayerId = (long)userId!, RoomId = id, Cheered = false, Favorited = false };
|
||||
|
||||
await db.RoomInteractions.AddAsync(favoriteResult);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
interaction = favoriteResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
interaction.Cheered = false;
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return new PlayerInteraction(interaction);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/favoritedby/{who}"), Authorize]
|
||||
public async Task<IActionResult> GetFavouritedBy(string who)
|
||||
{
|
||||
long? userId;
|
||||
if (who == "me")
|
||||
userId = User.GetUserId();
|
||||
else
|
||||
userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId;
|
||||
if (userId == null)
|
||||
userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId;
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var roomIds = await db.RoomInteractions
|
||||
.Where(x => x.PlayerId == userId && x.Favorited)
|
||||
.OrderByDescending(x => x.FavoritedAt)
|
||||
.Select(x => x.RoomId)
|
||||
.Distinct()
|
||||
.ToListAsync();
|
||||
|
||||
var rooms = new List<RoomData>();
|
||||
foreach (var roomId in roomIds)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room != null) rooms.Add(room);
|
||||
}
|
||||
|
||||
return Ok(await RoomUtils.BulkConvertRoomDataToRoomDetail(
|
||||
await RoomUtils.VisibleRooms(rooms, User.GetUserId())));
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{id}/interactionby/{who}"), Authorize]
|
||||
public async Task<PlayerInteraction?> GetInteractionBy(long id, string who)
|
||||
{
|
||||
long? userId;
|
||||
if (who == "me")
|
||||
userId = User.GetUserId();
|
||||
else
|
||||
userId = db.Profiles.FirstOrDefault(x => x.username == who)?.accountId;
|
||||
if (userId == null)
|
||||
userId = db.Profiles.FirstOrDefault(x => x.accountId.ToString() == who)?.accountId;
|
||||
if (userId == null) return null;
|
||||
|
||||
var interaction = await db.RoomInteractions.FirstOrDefaultAsync(x => x.PlayerId == userId && x.RoomId == id);
|
||||
if (interaction == null) return new PlayerInteraction { Cheered = false, Favorited = false };
|
||||
|
||||
return new PlayerInteraction(interaction);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/base")]
|
||||
public async Task<List<FullRoom>?> GetBaseRooms()
|
||||
{
|
||||
var roomData = await db.Rooms.Where(x => x.CloningAllowed && x.Accessibility != Accessibility.Private && x.CreatorAccountId == 1).ToListAsync();
|
||||
var rooms = new List<FullRoom>();
|
||||
|
||||
foreach (var room in roomData)
|
||||
{
|
||||
var fullRoom = await RoomUtils.GetRoom(room.RoomId);
|
||||
if (fullRoom == null) continue;
|
||||
rooms.Add(fullRoom);
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{id}/clone"), Authorize]
|
||||
public async Task<Success?> CloneRoom(long id, [FromForm] string name)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "You are not signed in.", null);
|
||||
|
||||
if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null);
|
||||
|
||||
var source = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (source == null) return new Success(false, "Unknown error", null);
|
||||
|
||||
if (!source.CloningAllowed)
|
||||
{
|
||||
return new Success(false, "This room cannot be cloned.", null);
|
||||
}
|
||||
|
||||
if (await RoomUtils.IsHiddenFrom(source, userId))
|
||||
{
|
||||
return new Success(false, "This room cannot be cloned.", null);
|
||||
}
|
||||
|
||||
var doesRoomExist = (await db.Rooms.FirstOrDefaultAsync(x => x.Name == name)) != null;
|
||||
if (doesRoomExist) return new Success(false, "Room already exists.", null);
|
||||
|
||||
var roomData = await RoomUtils.CloneRoom(id, (long)userId!, name);
|
||||
if (roomData == null) return new Success(false, "Unknown error", null);
|
||||
|
||||
return new Success(true, null, roomData);
|
||||
}
|
||||
|
||||
private sealed record RoomPermissionRow(string Permission, long Role, bool Override, int Type, string Value);
|
||||
|
||||
private static List<RoomPermissionRow> DefaultPermissions()
|
||||
{
|
||||
static RoomPermissionRow Row(string permission, long role, bool overridden) =>
|
||||
new(permission, role, overridden, 0, "True");
|
||||
|
||||
return new List<RoomPermissionRow>
|
||||
{
|
||||
Row("CAN_USE_ROOM_RESET_BUTTON", 0, true),
|
||||
Row("CAN_USE_DELETE_ALL_BUTTON", 0, true),
|
||||
Row("CAN_SAVE_INVENTIONS", 0, true),
|
||||
Row("CAN_SPAWN_INVENTIONS", 0, true),
|
||||
Row("CAN_USE_PLAY_GIZMOS_TOGGLE", 0, true),
|
||||
|
||||
|
||||
|
||||
Row("CAN_INVITE", 0, true),
|
||||
Row("CAN_USE_MAKER_PEN", 30, false),
|
||||
Row("CAN_USE_ROOM_RESET_BUTTON", 30, true),
|
||||
Row("CAN_USE_DELETE_ALL_BUTTON", 30, true),
|
||||
Row("CAN_SAVE_INVENTIONS", 30, true),
|
||||
Row("CAN_SPAWN_INVENTIONS", 30, true),
|
||||
Row("CAN_USE_PLAY_GIZMOS_TOGGLE", 30, true),
|
||||
};
|
||||
}
|
||||
|
||||
[HttpGet("photon_access_token")]
|
||||
public async Task<IActionResult> GetPhotonAccessToken()
|
||||
{
|
||||
long? userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var presence = await db.Presences
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.playerId == userId.Value);
|
||||
|
||||
var permissions = DefaultPermissions();
|
||||
|
||||
var profile = await db.Profiles
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.accountId == userId.Value);
|
||||
if (profile != null && (profile.HasRole("developer") || profile.HasRole("moderator")))
|
||||
permissions.Insert(0, new RoomPermissionRow("CAN_USE_MAKER_PEN", 0, true, 0, "True"));
|
||||
|
||||
long? subRoomId = null;
|
||||
if (presence?.roomInstance != null)
|
||||
{
|
||||
subRoomId = await db.RoomInstances
|
||||
.AsNoTracking()
|
||||
.Where(x => x.roomInstanceId == presence.roomInstance)
|
||||
.Select(x => (long?)x.subRoomId)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
if (subRoomId != null)
|
||||
{
|
||||
var overrides = await db.SubRoomPermissions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.subRoomId == subRoomId)
|
||||
.OrderBy(x => x.Id)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var stored in overrides)
|
||||
{
|
||||
var entry = new RoomPermissionRow(stored.permission, stored.role, true, stored.type, stored.value);
|
||||
|
||||
var index = permissions.FindIndex(x =>
|
||||
x.Permission == entry.Permission && x.Role == entry.Role);
|
||||
if (index == -1) permissions.Add(entry);
|
||||
else permissions[index] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(new Dictionary<string, object?>
|
||||
{
|
||||
["Permissions"] = permissions,
|
||||
["PhotonAccessToken"] = "",
|
||||
["RoomInstanceId"] = presence?.roomInstance
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{id}")]
|
||||
public async Task<ActionResult<FullRoom>> GetRoomById(long id)
|
||||
{
|
||||
var roomData = db.Rooms.FirstOrDefault(x => x.RoomId == id);
|
||||
if (roomData == null)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId != null)
|
||||
{
|
||||
var dorm = await RoomUtils.GetOrCreateDorm((long)userId!);
|
||||
if (dorm != null)
|
||||
return Ok(dorm);
|
||||
}
|
||||
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!await RoomUtils.CanSeeRoom(roomData, User.GetUserId()))
|
||||
return roomData.IsDorm ? NotFound() : Ok(RoomUtils.MaskHiddenRoom(roomData));
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomData.RoomId);
|
||||
if (fullRoom == null) return NotFound();
|
||||
|
||||
|
||||
|
||||
return Ok(fullRoom);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/subrooms/{subRoomId}/datahistory"), Authorize]
|
||||
public async Task<ActionResult<List<RestorableSubRoom>>> GetRoomDataHistory(long roomId, long subRoomId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (roomData == null) return NotFound();
|
||||
|
||||
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == (long)subRoomId);
|
||||
if (subRoom == null) return NotFound();
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Forbid();
|
||||
|
||||
var history = await db.SubRoomHistory.Where(x => x.RoomId == roomId && x.SubRoomId == subRoomId).ToListAsync();
|
||||
return Ok(history);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves"), Authorize]
|
||||
public async Task<IActionResult> GetSubRoomSaves(long roomId, long subRoomId, int? skip = null, int? take = null)
|
||||
{
|
||||
var (refusal, saves) = await LoadSubRoomSavesAsync(roomId, subRoomId);
|
||||
if (refusal != null) return refusal;
|
||||
|
||||
var page = PageSaves(saves, skip, take);
|
||||
|
||||
return Ok(new { Results = page, TotalResults = saves.Count, TotalCount = saves.Count });
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/subrooms/{subRoomId}/saves/no_unity_assets"), Authorize]
|
||||
public async Task<IActionResult> GetSubRoomSavesWithoutUnityAssets(long roomId, long subRoomId, int? skip = null, int? take = null)
|
||||
{
|
||||
var (refusal, saves) = await LoadSubRoomSavesAsync(roomId, subRoomId);
|
||||
if (refusal != null) return refusal;
|
||||
|
||||
var page = PageSaves(saves, skip, take)
|
||||
.Select(x => new
|
||||
{
|
||||
x.SubRoomDataSaveId,
|
||||
x.SubRoomId,
|
||||
x.DataBlob,
|
||||
x.ReferencedUnityAssetIds,
|
||||
x.PersistenceVersion,
|
||||
x.OMVersion,
|
||||
x.UgcSubVersion,
|
||||
x.SavedByAccountId,
|
||||
x.SavedOnPlatform,
|
||||
x.SavedOnDeviceClass,
|
||||
x.Description,
|
||||
x.ModerationState,
|
||||
x.CreatedAt,
|
||||
x.UnityAssetId
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return Ok(new { Results = page, TotalResults = saves.Count, TotalCount = saves.Count });
|
||||
}
|
||||
|
||||
private async Task<(IActionResult? Refusal, List<SubRoomDataSave> Saves)> LoadSubRoomSavesAsync(long roomId, long subRoomId)
|
||||
{
|
||||
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
var subRoomExists = await db.SubRooms.AsNoTracking().AnyAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
|
||||
if (room == null || !subRoomExists) return (Ok(EmptySavePage()), new List<SubRoomDataSave>());
|
||||
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return (Unauthorized(), new List<SubRoomDataSave>());
|
||||
if (!await CanReadSavesAsync((long)userId!, room)) return (StatusCode(StatusCodes.Status403Forbidden), new List<SubRoomDataSave>());
|
||||
|
||||
return (null, await RoomUtils.GetSaveHistory(db, roomId, subRoomId));
|
||||
}
|
||||
|
||||
private async Task<bool> CanReadSavesAsync(long userId, RoomData room)
|
||||
{
|
||||
if (room.CreatorAccountId == userId) return true;
|
||||
if (await MatchmakingUtils.IsAtLeastRole(userId, room.RoomId, RoleType.CoOwner)) return true;
|
||||
|
||||
var presence = await db.Presences.AsNoTracking().FirstOrDefaultAsync(x => x.playerId == userId);
|
||||
if (presence?.roomInstance == null) return false;
|
||||
|
||||
var instance = await db.RoomInstances.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.roomInstanceId == presence.roomInstance);
|
||||
|
||||
return instance != null && instance.roomId == room.RoomId;
|
||||
}
|
||||
|
||||
private static object EmptySavePage() => new { Results = new List<SubRoomDataSave>(), TotalResults = 0, TotalCount = 0 };
|
||||
|
||||
private static List<SubRoomDataSave> PageSaves(List<SubRoomDataSave> saves, int? skip, int? take)
|
||||
{
|
||||
var from = skip == null || skip < 0 ? 0 : skip.Value;
|
||||
var remaining = saves.Skip(from);
|
||||
|
||||
return (take == null || take < 0 ? remaining : remaining.Take(take.Value)).ToList();
|
||||
}
|
||||
|
||||
|
||||
private async Task<Dictionary<string, string>> ReadSaveValuesAsync()
|
||||
{
|
||||
var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
foreach (var (key, value) in Request.Form)
|
||||
values[key] = value.ToString();
|
||||
return values;
|
||||
}
|
||||
|
||||
Request.EnableBuffering();
|
||||
|
||||
string raw;
|
||||
using (var reader = new StreamReader(Request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true))
|
||||
raw = (await reader.ReadToEndAsync()).Trim();
|
||||
|
||||
Request.Body.Position = 0;
|
||||
|
||||
if (raw.Length == 0) return values;
|
||||
|
||||
try
|
||||
{
|
||||
if (JsonNode.Parse(raw) is JsonObject json) FlattenSaveJson(json, string.Empty, values);
|
||||
}
|
||||
catch (JsonException) { }
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private static void FlattenSaveJson(JsonObject json, string prefix, Dictionary<string, string> values)
|
||||
{
|
||||
foreach (var (key, node) in json)
|
||||
{
|
||||
var path = prefix.Length == 0 ? key : prefix + "." + key;
|
||||
|
||||
if (node is JsonObject nested) FlattenSaveJson(nested, path, values);
|
||||
else if (node != null && node is not JsonArray) values[path] = node.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ReadSaveValue(Dictionary<string, string> values, params string[] keys)
|
||||
{
|
||||
foreach (var key in keys)
|
||||
if (values.TryGetValue(key, out var raw) && !string.IsNullOrWhiteSpace(raw))
|
||||
return raw;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JsonObject BuildRoomSaveValue(FullRoom room, SubRoom subRoom, string? dataBlobHash)
|
||||
{
|
||||
var roomNode = JsonSerializer.SerializeToNode(room) as JsonObject ?? new JsonObject();
|
||||
var value = new JsonObject { ["room"] = roomNode.DeepClone() };
|
||||
|
||||
foreach (var property in roomNode)
|
||||
value[property.Key] = property.Value?.DeepClone();
|
||||
|
||||
value["subRoomDataSave"] = JsonSerializer.SerializeToNode(RoomUtils.BuildSaveResponse(subRoom, dataBlobHash));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private sealed record StudioSaveResult(bool success, string? error, object? value);
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms/{subRoomId}/data"), Authorize]
|
||||
public async Task<object?> UploadRoomData(long roomId, long subRoomId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Success.Error("Invalid auth");
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (roomData == null) return new Success(false, "Invalid room.", null);
|
||||
|
||||
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId);
|
||||
if (subRoom == null) return new Success(false, "Invalid subroom.", null);
|
||||
|
||||
var values = await ReadSaveValuesAsync();
|
||||
|
||||
var blobName = ReadSaveValue(values, "subroomdata.filename", "filename", "datablob", "blob");
|
||||
if (string.IsNullOrWhiteSpace(blobName))
|
||||
{
|
||||
return new Success(false, "No room data was uploaded.", null);
|
||||
}
|
||||
|
||||
var savedBy = (long)userId!;
|
||||
var unityAssetId = ReadSaveValue(values, "unityassetid", "subroomdata.unityassetid");
|
||||
var unityAssetFilename = ReadSaveValue(values, "unityasset.filename", "unityasset");
|
||||
if (!string.IsNullOrWhiteSpace(unityAssetFilename))
|
||||
{
|
||||
if (!StudioUnityAssetRegistry.IsValidAssetId(unityAssetFilename))
|
||||
return new StudioSaveResult(false, "Invalid Unity source-manifest filename.", null);
|
||||
if (await StudioUnityAssetRegistry.ReadAsync(unityAssetFilename, HttpContext.RequestAborted) == null)
|
||||
return new StudioSaveResult(false, "Unity source manifest has not been uploaded.", null);
|
||||
}
|
||||
|
||||
var blobHash = ReadSaveValue(values, "subroomdata.hash", "hash", "datablobhash");
|
||||
|
||||
await db.SubRoomHistory.AddAsync(new RestorableSubRoom(subRoom));
|
||||
|
||||
subRoom.DataBlob = blobName;
|
||||
subRoom.SavedByAccountId = savedBy;
|
||||
subRoom.CreatedAt = DateTime.UtcNow;
|
||||
if (!string.IsNullOrWhiteSpace(unityAssetId)) subRoom.UnityAssetId = unityAssetId;
|
||||
if (!string.IsNullOrWhiteSpace(unityAssetFilename)) subRoom.UnityAssetId = unityAssetFilename;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var room = await RoomUtils.GetRoom(roomId);
|
||||
if (room == null) return new Success(false, "Invalid room.", null);
|
||||
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = room });
|
||||
|
||||
|
||||
|
||||
|
||||
return new StudioSaveResult(true, null, BuildRoomSaveValue(room, subRoom, blobHash));
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("rooms/{roomId}/subrooms/{subRoomId}/unityasset")]
|
||||
public async Task<IActionResult> GetStudioUnityAsset(long roomId, long subRoomId, [FromQuery] string? unityAssetId = null)
|
||||
{
|
||||
var subRoom = await db.SubRooms.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
if (subRoom == null) return NotFound();
|
||||
var assetId = string.IsNullOrWhiteSpace(unityAssetId) ? subRoom.UnityAssetId : unityAssetId;
|
||||
if (!string.Equals(assetId, subRoom.UnityAssetId, StringComparison.Ordinal)) return NotFound();
|
||||
var userId = User.GetUserId();
|
||||
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null || !await RoomUtils.CanSeeRoom(room, userId)) return NotFound();
|
||||
var asset = await StudioUnityAssetRegistry.ReadAsync(assetId, HttpContext.RequestAborted);
|
||||
return asset == null ? NotFound() : Ok(asset);
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms/{subRoomId}/restoredata"), Authorize]
|
||||
public async Task<Success?> RestoreRoomData(long roomId, long subRoomId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Success.Error("Invalid auth");
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (roomData == null) return new Success(false, "Invalid room.", null);
|
||||
|
||||
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId);
|
||||
if (subRoom == null) return new Success(false, "Invalid subroom.", null);
|
||||
|
||||
var values = await ReadSaveValuesAsync();
|
||||
var blobName = ReadSaveValue(values, "filename", "subroomdata.filename", "datablob", "blob");
|
||||
if (string.IsNullOrWhiteSpace(blobName)) return new Success(false, "No room data was given.", null);
|
||||
|
||||
if (!System.IO.File.Exists(Path.Join(DBConfig.CDNStorage, blobName)))
|
||||
return new Success(false, "That save's data is no longer available.", null);
|
||||
|
||||
if (!string.IsNullOrEmpty(subRoom.DataBlob) && subRoom.DataBlob != blobName)
|
||||
{
|
||||
await db.SubRoomHistory.AddAsync(new RestorableSubRoom(subRoom));
|
||||
}
|
||||
|
||||
subRoom.DataBlob = blobName;
|
||||
subRoom.SavedByAccountId = (long)userId!;
|
||||
subRoom.CreatedAt = DateTime.UtcNow;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var room = await RoomUtils.GetRoom(roomId);
|
||||
if (room == null) return new Success(false, "Invalid room.", null);
|
||||
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = room });
|
||||
|
||||
return new Success(true, null, BuildRoomSaveValue(room, subRoom, null));
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms"), Authorize]
|
||||
public async Task<Success?> CreateSubroom(long roomId, [FromForm] string name)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var roomData = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (roomData == null) return new Success(false, "Invalid room.", null);
|
||||
|
||||
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.Name == name && x.RoomId == roomId);
|
||||
if (subRoom != null) return new Success(false, "A subroom already exists with that name!", null);
|
||||
|
||||
var mainSubRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (mainSubRoom == null) return new Success(false, "There is no main subroom?", null);
|
||||
|
||||
var presence = await db.Presences.FirstOrDefaultAsync(x => x.playerId == userId);
|
||||
if (presence == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null);
|
||||
|
||||
var newSubRoom = new SubRoom
|
||||
{
|
||||
DataBlob = "",
|
||||
CreatedAt = DateTime.Now,
|
||||
Accessibility = Accessibility.Unlised,
|
||||
IsSandbox = true,
|
||||
MaxPlayers = mainSubRoom.MaxPlayers,
|
||||
Name = name,
|
||||
RoomId = roomId,
|
||||
SavedByAccountId = (long)userId!,
|
||||
UnitySceneId = mainSubRoom.UnitySceneId
|
||||
};
|
||||
|
||||
await db.SubRooms.AddAsync(newSubRoom);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var room = await RoomUtils.GetRoom((long)roomId);
|
||||
if (room == null) return new Success(false, "WHAT THE FUCK???", null);
|
||||
|
||||
|
||||
|
||||
|
||||
return new Success(true, null, room);
|
||||
}
|
||||
|
||||
[HttpPost("rooms/{roomId}/subrooms/{subRoomId}/move"), Authorize]
|
||||
public async Task<Success?> MoveSubroom(long roomId, long subRoomId, [FromForm] long newRoomId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, newRoomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
var newRoom = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == newRoomId);
|
||||
var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
if (newRoom == null) return new Success(false, "Target room not found", null);
|
||||
if (subroom == null) return new Success(false, "Subroom not found", null);
|
||||
|
||||
subroom.RoomId = newRoomId;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/subrooms/{subRoomId}/modify")]
|
||||
public async Task<Success> ModifySubroom(long roomId, long subRoomId, string? name, Accessibility? accessibility, int? maxPlayers)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
var subroom = await db.SubRooms.FirstOrDefaultAsync(x => x.SubRoomId == subRoomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
if (subroom == null) return new Success(false, "Subroom not found", null);
|
||||
|
||||
if (name != null)
|
||||
subroom.Name = name;
|
||||
if (accessibility != null)
|
||||
subroom.Accessibility = (Accessibility)accessibility;
|
||||
if (maxPlayers != null)
|
||||
subroom.MaxPlayers = (int)maxPlayers;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/modify"), Authorize]
|
||||
public async Task<Success?> ModifyRoom(long roomId, string name, string? description, Accessibility accessibility, bool supportsScreens, bool supportsWalkVR, bool supportsTeleportVR, bool supportsJuniors, bool cloningAllowed, bool disableMicAutoMute, bool disableRoomComments, bool encryptVoiceChat)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null);
|
||||
if (!(description ?? string.Empty).IsPure()) return new Success(false, "Description contains offensive language.", null);
|
||||
|
||||
room.Name = name;
|
||||
room.Description = description;
|
||||
room.SupportsScreens = supportsScreens;
|
||||
room.SupportsWalkVR = supportsWalkVR;
|
||||
room.SupportsTeleportVR = supportsTeleportVR;
|
||||
room.SupportsJuniors = supportsJuniors;
|
||||
room.Accessibility = accessibility;
|
||||
room.CloningAllowed = cloningAllowed;
|
||||
room.DisableMicAutoMute = disableMicAutoMute;
|
||||
room.DisableRoomComments = disableRoomComments;
|
||||
room.EncryptVoiceChat = encryptVoiceChat;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/image"), Authorize]
|
||||
public async Task<Success?> ModifyImage(long roomId, string imageName)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
room.ImageName = imageName;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
private static readonly RoleType[] AssignableRoles =
|
||||
{
|
||||
RoleType.None, RoleType.Host, RoleType.Moderator, RoleType.CoOwner, RoleType.TemporaryCoOwner
|
||||
};
|
||||
|
||||
private async Task<(RoleType? Role, long? ActorId)> ReadRoleRequestAsync()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return (null, null);
|
||||
|
||||
foreach (var key in new[] { "role", "Role", "roletype", "RoleType", "roleType", "invitedrole", "InvitedRole" })
|
||||
{
|
||||
var raw = Request.HasFormContentType && Request.Form.TryGetValue(key, out var form)
|
||||
? form.ToString()
|
||||
: Request.Query.TryGetValue(key, out var query) ? query.ToString() : null;
|
||||
|
||||
if (raw == null && Request.RouteValues.TryGetValue(key, out var route)) raw = route?.ToString();
|
||||
if (!int.TryParse(raw, out var parsed)) continue;
|
||||
if (!Enum.IsDefined(typeof(RoleType), parsed)) continue;
|
||||
|
||||
return ((RoleType)parsed, userId);
|
||||
}
|
||||
|
||||
return (null, userId);
|
||||
}
|
||||
|
||||
private async Task PushRoomRoleUpdate(long roomId, long actorId, long targetId, FullRoom? room)
|
||||
{
|
||||
room ??= await RoomUtils.GetRoom(roomId);
|
||||
if (room == null) return;
|
||||
|
||||
foreach (var accountId in new[] { targetId, actorId }.Distinct())
|
||||
await Notify.SendNotify(accountId, new Notification { Id = "RoomUpdate", Msg = room });
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/roles/{targetId}/invite"), Authorize]
|
||||
public async Task<Success?> InvitePlayerToRole(long roomId, long targetId)
|
||||
{
|
||||
var (role, userId) = await ReadRoleRequestAsync();
|
||||
if (userId == null) return Success.Error("Invalid auth");
|
||||
if (targetId == userId.Value) return Success.Error("You cannot invite yourself");
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == targetId);
|
||||
if (player == null) return Success.Error("Player not found");
|
||||
|
||||
var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == targetId && x.RoomId == roomId);
|
||||
if (role == null) return Success.Error("You must provide a valid role");
|
||||
if (!AssignableRoles.Contains(role.Value)) return Success.Error($"Unknown role {role.Value}");
|
||||
if (role.Value == RoleType.None) return Success.Error("You must provide a valid role");
|
||||
if (existingRole?.Role == role.Value && existingRole.InvitedRole == RoleType.None)
|
||||
return Success.Error("Player already has this role");
|
||||
|
||||
if (existingRole != null &&
|
||||
existingRole.Role != RoleType.None &&
|
||||
existingRole.Role != RoleType.Banned)
|
||||
return Success.Error("Player already has a room role");
|
||||
|
||||
if (existingRole == null)
|
||||
{
|
||||
var newRole = new RoomRole
|
||||
{
|
||||
AccountId = targetId,
|
||||
InvitedRole = role.Value,
|
||||
Role = RoleType.None,
|
||||
RoomId = roomId,
|
||||
LastChangedByAccountId = userId.Value
|
||||
};
|
||||
|
||||
await db.RoomRoles.AddAsync(newRole);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingRole.InvitedRole = role.Value;
|
||||
existingRole.LastChangedByAccountId = userId.Value;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (role.Value == RoleType.CoOwner)
|
||||
{
|
||||
await Notify.SendMessage(targetId, new Message
|
||||
{
|
||||
FromPlayerId = userId.Value,
|
||||
RoomId = roomId,
|
||||
Type = MessageType.RoomCoOwnerInvited
|
||||
});
|
||||
}
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await PushRoomRoleUpdate(roomId, userId.Value, targetId, fullRoom);
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/roles/{targetId}"), Authorize]
|
||||
public async Task<Success?> SetPlayerRole(long roomId, long targetId)
|
||||
{
|
||||
var (role, userId) = await ReadRoleRequestAsync();
|
||||
if (userId == null) return Success.Error("Invalid auth");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
if (targetId == room.CreatorAccountId)
|
||||
return Success.Error("The room's creator cannot be given a different role");
|
||||
|
||||
var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == targetId);
|
||||
if (player == null) return Success.Error("Player not found");
|
||||
|
||||
var existingRole = await db.RoomRoles.FirstOrDefaultAsync(x => x.AccountId == targetId && x.RoomId == roomId);
|
||||
var wasCoOwner = existingRole?.Role == RoleType.CoOwner;
|
||||
var acceptingInvite = targetId == userId.Value &&
|
||||
existingRole is { Role: RoleType.None, InvitedRole: not RoleType.None };
|
||||
if (acceptingInvite && role == null) role = existingRole!.InvitedRole;
|
||||
if (role == null) return Success.Error("You must provide a valid role");
|
||||
if (!AssignableRoles.Contains(role.Value)) return Success.Error($"Unknown role {role.Value}");
|
||||
|
||||
if (acceptingInvite)
|
||||
{
|
||||
if (role.Value != existingRole!.InvitedRole)
|
||||
return Success.Error("The requested role does not match the pending invitation");
|
||||
}
|
||||
else
|
||||
{
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole(userId.Value, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
}
|
||||
|
||||
if (role.Value == RoleType.None)
|
||||
{
|
||||
if (existingRole == null) return Success.Error("Player already has this role");
|
||||
if (acceptingInvite) return Success.Error("The pending invitation cannot be accepted as None");
|
||||
|
||||
db.RoomRoles.Remove(existingRole);
|
||||
}
|
||||
else if (existingRole == null)
|
||||
{
|
||||
await db.RoomRoles.AddAsync(new RoomRole
|
||||
{
|
||||
AccountId = targetId,
|
||||
InvitedRole = RoleType.None,
|
||||
Role = role.Value,
|
||||
RoomId = roomId,
|
||||
LastChangedByAccountId = userId.Value
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existingRole.Role == role.Value && existingRole.InvitedRole == RoleType.None)
|
||||
return Success.Error("Player already has this role");
|
||||
|
||||
existingRole.Role = role.Value;
|
||||
existingRole.InvitedRole = RoleType.None;
|
||||
existingRole.LastChangedByAccountId = (long)userId!;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
if (wasCoOwner && role.Value < RoleType.CoOwner)
|
||||
{
|
||||
await Notify.SendMessage(targetId, new Message
|
||||
{
|
||||
FromPlayerId = userId.Value,
|
||||
RoomId = roomId,
|
||||
Type = MessageType.RoomCoOwnerRemoved
|
||||
});
|
||||
}
|
||||
|
||||
if (role.Value == RoleType.CoOwner && !wasCoOwner)
|
||||
{
|
||||
await Notify.SendMessage(targetId, new Message
|
||||
{
|
||||
FromPlayerId = userId.Value,
|
||||
RoomId = roomId,
|
||||
Type = MessageType.RoomCoOwnerAdded
|
||||
});
|
||||
}
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await PushRoomRoleUpdate(roomId, userId.Value, targetId, fullRoom);
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/tags"), Authorize]
|
||||
public async Task<Success?> ModifyTags(long roomId, string[]? tag, string[]? autoTag)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
foreach (var t in tag ?? Array.Empty<string>())
|
||||
{
|
||||
if (!t.TagAllowed())
|
||||
return new Success(false, "Tag not allowed: " + t, null);
|
||||
}
|
||||
|
||||
if (tag != null || autoTag != null) {
|
||||
List<ItemTag> tagsToAdd = new();
|
||||
|
||||
if (tag != null)
|
||||
foreach (var t in tag)
|
||||
tagsToAdd.Add(new ItemTag { Tag = t, Type = TagType.General });
|
||||
|
||||
if (autoTag != null)
|
||||
foreach (var t in autoTag)
|
||||
tagsToAdd.Add(new ItemTag { Tag = t, Type = TagType.Auto });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
room.Tags = StringTools.CreateTagString(tagsToAdd);
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, null);
|
||||
} else
|
||||
{
|
||||
room.Tags = "";
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/accessibility"), Authorize]
|
||||
public async Task<Success?> ModifyAccessibility(long roomId, Accessibility accessibility)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
bool wasPublished = room.WasPublished;
|
||||
|
||||
room.Accessibility = accessibility;
|
||||
if (!room.WasPublished && accessibility == Accessibility.Public) {
|
||||
room.WasPublished = true;
|
||||
room.PublishedAt = DateTime.Now;
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
|
||||
[HttpGet("rooms/{roomId}/subrooms/{subRoomId}/permissions"), Authorize]
|
||||
public async Task<IActionResult> GetSubRoomPermissions(long roomId, long subRoomId)
|
||||
{
|
||||
if (User.GetUserId() == null) return Unauthorized();
|
||||
|
||||
var subRoom = await db.SubRooms
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
if (subRoom == null) return NotFound();
|
||||
|
||||
var stored = await db.SubRoomPermissions
|
||||
.AsNoTracking()
|
||||
.Where(x => x.subRoomId == subRoomId)
|
||||
.OrderBy(x => x.Id)
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(stored
|
||||
.Select(x => new RoomPermissionRow(x.permission, x.role, true, x.type, x.value))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/subrooms/{subRoomId}/permissions"), Authorize]
|
||||
public async Task<IActionResult> SetSubRoomPermissions(long roomId, long subRoomId)
|
||||
{
|
||||
long? userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var subRoom = await db.SubRooms
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
if (subRoom == null) return NotFound();
|
||||
|
||||
if (!await MatchmakingUtils.IsAtLeastRole(userId.Value, roomId, RoleType.CoOwner))
|
||||
return Forbid();
|
||||
|
||||
string? body;
|
||||
using (var reader = new StreamReader(Request.Body, Encoding.UTF8))
|
||||
body = await reader.ReadToEndAsync();
|
||||
|
||||
var entries = ParseSubRoomPermissions(body);
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var existing = await db.SubRoomPermissions.FirstOrDefaultAsync(x =>
|
||||
x.subRoomId == subRoomId && x.permission == entry.Permission && x.role == entry.Role);
|
||||
|
||||
if (!entry.Override)
|
||||
{
|
||||
if (existing != null) db.SubRoomPermissions.Remove(existing);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
await db.SubRoomPermissions.AddAsync(new SubRoomPermission
|
||||
{
|
||||
subRoomId = subRoomId,
|
||||
permission = entry.Permission,
|
||||
role = entry.Role,
|
||||
type = entry.Type,
|
||||
value = entry.Value,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.type = entry.Type;
|
||||
existing.value = entry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var logged = body ?? "";
|
||||
if (logged.Length > 800) logged = logged[..800] + "...";
|
||||
|
||||
return Ok(new { Permissions = Array.Empty<object>() });
|
||||
}
|
||||
|
||||
private static List<RoomPermissionRow> ParseSubRoomPermissions(string? body)
|
||||
{
|
||||
var entries = new List<RoomPermissionRow>();
|
||||
if (string.IsNullOrWhiteSpace(body)) return entries;
|
||||
|
||||
JsonDocument document;
|
||||
try
|
||||
{
|
||||
document = JsonDocument.Parse(body);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return entries;
|
||||
}
|
||||
|
||||
using (document)
|
||||
{
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Array) return entries;
|
||||
|
||||
foreach (var element in document.RootElement.EnumerateArray())
|
||||
{
|
||||
if (element.ValueKind != JsonValueKind.Object) continue;
|
||||
|
||||
var permission = ReadPermissionName(element);
|
||||
if (permission.Length == 0) continue;
|
||||
|
||||
var role = ReadPermissionRole(element);
|
||||
if (role == null) continue;
|
||||
|
||||
entries.Add(new RoomPermissionRow(
|
||||
permission,
|
||||
role.Value,
|
||||
ReadPermissionBool(element, "Override"),
|
||||
(int)(ReadPermissionLong(element, "Type") ?? 0),
|
||||
ReadPermissionValue(element, "Value")));
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static bool TryGetPermissionField(JsonElement entry, string key, out JsonElement value)
|
||||
{
|
||||
foreach (var property in entry.EnumerateObject())
|
||||
{
|
||||
if (string.Equals(property.Name, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
value = property.Value;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string? ReadPermissionString(JsonElement entry, string key)
|
||||
{
|
||||
if (!TryGetPermissionField(entry, key, out var value)) return null;
|
||||
return value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString();
|
||||
}
|
||||
|
||||
private static long? ReadPermissionLong(JsonElement entry, string key)
|
||||
{
|
||||
if (!TryGetPermissionField(entry, key, out var value)) return null;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var number))
|
||||
return number;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String &&
|
||||
long.TryParse(value.GetString(), out var parsed))
|
||||
return parsed;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ReadPermissionBool(JsonElement entry, string key)
|
||||
{
|
||||
if (!TryGetPermissionField(entry, key, out var value)) return false;
|
||||
|
||||
if (value.ValueKind == JsonValueKind.True) return true;
|
||||
if (value.ValueKind == JsonValueKind.False) return false;
|
||||
|
||||
return string.Equals(value.ToString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string ReadPermissionValue(JsonElement entry, string key)
|
||||
{
|
||||
if (!TryGetPermissionField(entry, key, out var value)) return "";
|
||||
|
||||
if (value.ValueKind == JsonValueKind.String) return value.GetString() ?? "";
|
||||
if (value.ValueKind == JsonValueKind.True) return "True";
|
||||
if (value.ValueKind == JsonValueKind.False) return "False";
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
private static string ReadPermissionName(JsonElement entry)
|
||||
{
|
||||
var raw = (ReadPermissionString(entry, "Permission")
|
||||
?? ReadPermissionString(entry, "PermissionType")
|
||||
?? ReadPermissionString(entry, "PermissionTypeOld")
|
||||
?? "").Trim();
|
||||
if (raw.Length == 0) return "";
|
||||
|
||||
if (long.TryParse(raw, out var number) &&
|
||||
Enum.IsDefined(typeof(RolePermissions), (int)number))
|
||||
return ((RolePermissions)(int)number).ToString();
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
private static long? ReadPermissionRole(JsonElement entry)
|
||||
{
|
||||
var numeric = ReadPermissionLong(entry, "Role") ?? ReadPermissionLong(entry, "RoleId");
|
||||
if (numeric != null) return numeric;
|
||||
|
||||
var name = ReadPermissionString(entry, "Role")?.Trim();
|
||||
if (!string.IsNullOrEmpty(name) && Enum.TryParse<RoleType>(name, true, out var parsed))
|
||||
return (long)parsed;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPut("rooms/{roomId}/subrooms/{subRoomId}/accessibility"), Authorize]
|
||||
public async Task<Success?> ModifySubRoomAccessibility(long roomId, long subRoomId, Accessibility accessibility)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId!, roomId, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
var subRoom = await db.SubRooms.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
if (subRoom == null) return new Success(false, "Subroom not found", null);
|
||||
|
||||
subRoom.Accessibility = accessibility;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom(roomId);
|
||||
await Notify.SendNotify((long)userId!, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomStats
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
|
||||
public long RoomId { get; set; }
|
||||
public int CheerCount { get; set; } = 0;
|
||||
public int FavoriteCount { get; set; } = 0;
|
||||
public int VisitorCount { get; set; } = 0;
|
||||
public int VisitCount { get; set; } = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class RoomUploadResponse
|
||||
{
|
||||
public Room Room { get; set; } = null!;
|
||||
public SubRoom SubRoomDataSave { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,574 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet;
|
||||
using RadicadeServer.RecNet.Accounts;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Messages;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Messages;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
|
||||
namespace RadicadeServer.Utils
|
||||
{
|
||||
public static class RoomUtils
|
||||
{
|
||||
public static IDbContextFactory<RecDBContext> DbFactory = null!;
|
||||
|
||||
public static async Task<bool> VerifyRole(long playerId, long roomId, RoleType roleType)
|
||||
{
|
||||
var role = await GetRoleForPlayer(playerId, roomId);
|
||||
if (role == null) return false;
|
||||
|
||||
return role.Role >= roleType;
|
||||
}
|
||||
|
||||
public static async Task<bool> CanSeeRoom(RoomData room, long? accountId)
|
||||
{
|
||||
if (room.Accessibility != Accessibility.Private) return true;
|
||||
if (accountId == null) return false;
|
||||
if (room.CreatorAccountId == accountId.Value) return true;
|
||||
if (await VerifyRole(accountId.Value, room.RoomId, RoleType.CoOwner)) return true;
|
||||
|
||||
if (DbFactory != null)
|
||||
{
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
if (await (
|
||||
from presence in db.Presences.AsNoTracking()
|
||||
join instance in db.RoomInstances.AsNoTracking()
|
||||
on presence.roomInstance equals instance.roomInstanceId
|
||||
where presence.playerId == accountId.Value && presence.isOnline &&
|
||||
instance.roomId == room.RoomId
|
||||
select presence.playerId).AnyAsync()) return true;
|
||||
}
|
||||
|
||||
return await HasLiveGameInvite(room.RoomId, accountId.Value);
|
||||
}
|
||||
|
||||
public static async Task<bool> HasLiveGameInvite(long roomId, long accountId)
|
||||
{
|
||||
if (DbFactory == null) return false;
|
||||
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
long? inviter = await db.Messages.AsNoTracking()
|
||||
.Where(message =>
|
||||
message.ToPlayerId == accountId &&
|
||||
(message.Type == MessageType.GameInvite || message.Type == MessageType.GameInviteV2) &&
|
||||
message.RoomId == roomId)
|
||||
.OrderByDescending(message => message.Id)
|
||||
.Select(message => (long?)message.FromPlayerId)
|
||||
.FirstOrDefaultAsync();
|
||||
if (inviter == null) return false;
|
||||
|
||||
return await (
|
||||
from presence in db.Presences.AsNoTracking()
|
||||
join instance in db.RoomInstances.AsNoTracking()
|
||||
on presence.roomInstance equals instance.roomInstanceId
|
||||
where presence.playerId == inviter.Value &&
|
||||
presence.isOnline &&
|
||||
instance.roomId == roomId
|
||||
select presence.playerId).AnyAsync();
|
||||
}
|
||||
|
||||
public static async Task<List<RoomData>> VisibleRooms(IEnumerable<RoomData> rooms, long? accountId)
|
||||
{
|
||||
var visible = new List<RoomData>();
|
||||
foreach (var room in rooms)
|
||||
{
|
||||
if (await CanSeeRoom(room, accountId)) visible.Add(room);
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
public const string PrivateRoomName = "PRIVATE ROOM";
|
||||
|
||||
public static async Task<bool> IsHiddenFrom(RoomData room, long? accountId)
|
||||
=> !room.IsDorm && !await CanSeeRoom(room, accountId);
|
||||
|
||||
public static FullRoom MaskHiddenRoom(RoomData room)
|
||||
{
|
||||
return new FullRoom
|
||||
{
|
||||
RoomId = room.RoomId,
|
||||
Name = PrivateRoomName,
|
||||
Description = string.Empty,
|
||||
ImageName = DBConfig.DefaultRoomImage,
|
||||
IsDorm = false,
|
||||
State = room.State,
|
||||
Accessibility = Accessibility.Private,
|
||||
CreatedAt = room.CreatedAt,
|
||||
CreatorAccountId = room.CreatorAccountId,
|
||||
MaxPlayers = room.MaxPlayers,
|
||||
MaxPlayerCalculationMode = room.MaxPlayerCalculationMode,
|
||||
CloningAllowed = false,
|
||||
DisableRoomComments = true,
|
||||
CustomWarning = string.Empty,
|
||||
SubRooms = new List<SubRoom>(),
|
||||
Roles = new List<RoomRole>(),
|
||||
LoadScreens = new List<RoomLoadScreen>(),
|
||||
PromoImages = new List<string>(),
|
||||
PromoExternalContent = new List<ExternalContent>(),
|
||||
DataBlob = string.Empty,
|
||||
Tags = new List<ItemTag>(),
|
||||
Stats = new RoomStats { RoomId = room.RoomId },
|
||||
Version = 1
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<RoomInstance?> ForViewerAsync(RoomInstance? instance, long? viewerId)
|
||||
{
|
||||
if (instance == null) return null;
|
||||
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == instance.roomId);
|
||||
if (room == null || !await IsHiddenFrom(room, viewerId)) return instance;
|
||||
|
||||
var masked = instance.Copy();
|
||||
masked.name = PrivateRoomName;
|
||||
return masked;
|
||||
}
|
||||
public static async Task<List<RoomRole>> GetRoles(long roomId)
|
||||
{
|
||||
await using var db = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var room = await db.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.RoomId == roomId);
|
||||
if (room == null) return [];
|
||||
|
||||
var roomRole = await db.RoomRoles.AsNoTracking().Where(x => x.RoomId == roomId).ToListAsync();
|
||||
|
||||
var creatorRow = roomRole.FirstOrDefault(x => x.AccountId == room.CreatorAccountId);
|
||||
if (creatorRow == null)
|
||||
roomRole.Add(new RoomRole { AccountId = room.CreatorAccountId, InvitedRole = RoleType.None, Role = RoleType.Creator, RoomId = roomId });
|
||||
else if (creatorRow.Role < RoleType.Creator)
|
||||
creatorRow.Role = RoleType.Creator;
|
||||
|
||||
if (room.IsRRO)
|
||||
{
|
||||
var rroOwners = (await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "HighRanks"))?.Value;
|
||||
|
||||
foreach (var dev in (rroOwners ?? string.Empty).Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!long.TryParse(dev, out long userId)) continue;
|
||||
|
||||
foreach (var role in new[] { RoleType.Creator, RoleType.Moderator })
|
||||
{
|
||||
if (roomRole.Any(x => x.AccountId == userId && x.Role == role)) continue;
|
||||
|
||||
roomRole.Add(new RoomRole { AccountId = userId, InvitedRole = RoleType.None, Role = role, RoomId = roomId });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return roomRole;
|
||||
}
|
||||
public static async Task<RoomRole?> GetRoleForPlayer(long playerId, long roomId)
|
||||
{
|
||||
var roomRoles = await GetRoles(roomId);
|
||||
|
||||
return roomRoles
|
||||
.Where(x => x.AccountId == playerId)
|
||||
.OrderByDescending(x => x.Role)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
public static SubRoomDataSave BuildCurrentSave(SubRoom subRoom)
|
||||
{
|
||||
return new SubRoomDataSave
|
||||
{
|
||||
SubRoomDataSaveId = 1,
|
||||
SubRoomId = subRoom.SubRoomId,
|
||||
DataBlob = subRoom.DataBlob,
|
||||
SavedByAccountId = subRoom.SavedByAccountId,
|
||||
CreatedAt = subRoom.CreatedAt,
|
||||
UnityAssetId = string.IsNullOrEmpty(subRoom.UnityAssetId) ? null : subRoom.UnityAssetId,
|
||||
PersistenceVersion = 1,
|
||||
OMVersion = 1,
|
||||
UgcSubVersion = 1,
|
||||
SavedOnPlatform = 0,
|
||||
SavedOnDeviceClass = 0,
|
||||
Description = string.Empty,
|
||||
ModerationState = 0
|
||||
};
|
||||
}
|
||||
|
||||
public static object BuildSaveResponse(SubRoom subRoom, string? dataBlobHash)
|
||||
{
|
||||
return new
|
||||
{
|
||||
subRoomDataSaveId = 1L,
|
||||
subRoomId = subRoom.SubRoomId,
|
||||
unityAssetId = string.IsNullOrEmpty(subRoom.UnityAssetId) ? null : subRoom.UnityAssetId,
|
||||
unityAsset = (string?)null,
|
||||
unityAssetHash = (string?)null,
|
||||
dataBlob = subRoom.DataBlob ?? string.Empty,
|
||||
dataBlobHash,
|
||||
savedByAccountId = (long?)subRoom.SavedByAccountId,
|
||||
savedOnPlatform = 0,
|
||||
savedOnDeviceClass = 0,
|
||||
description = string.Empty,
|
||||
createdAt = subRoom.CreatedAt
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<List<SubRoomDataSave>> GetSaveHistory(RecDBContext ctx, long roomId, long subRoomId)
|
||||
{
|
||||
var subRoom = await ctx.SubRooms.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.RoomId == roomId && x.SubRoomId == subRoomId);
|
||||
|
||||
if (subRoom == null) return new List<SubRoomDataSave>();
|
||||
|
||||
var saves = new List<SubRoomDataSave>();
|
||||
|
||||
if (!string.IsNullOrEmpty(subRoom.DataBlob)) saves.Add(BuildCurrentSave(subRoom));
|
||||
|
||||
var history = await ctx.SubRoomHistory.AsNoTracking()
|
||||
.Where(x => x.RoomId == roomId && x.SubRoomId == subRoomId && x.DataBlob != "")
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.ThenByDescending(x => x.HistoryId)
|
||||
.ToListAsync();
|
||||
|
||||
saves.AddRange(history.Select(BuildRestorableSave));
|
||||
|
||||
return saves;
|
||||
}
|
||||
|
||||
public static SubRoomDataSave BuildRestorableSave(RestorableSubRoom row)
|
||||
{
|
||||
return new SubRoomDataSave
|
||||
{
|
||||
SubRoomDataSaveId = row.HistoryId + 1,
|
||||
SubRoomId = row.SubRoomId,
|
||||
DataBlob = row.DataBlob,
|
||||
SavedByAccountId = row.SavedByAccountId,
|
||||
CreatedAt = row.CreatedAt,
|
||||
PersistenceVersion = 1,
|
||||
OMVersion = 1,
|
||||
UgcSubVersion = 1,
|
||||
SavedOnPlatform = 0,
|
||||
SavedOnDeviceClass = 0,
|
||||
Description = string.Empty,
|
||||
ModerationState = 0
|
||||
};
|
||||
}
|
||||
|
||||
public static FullRoom ConvertToFull(this RoomData room, List<SubRoom> subRooms, RoomStats stats, List<RoomRole> roles)
|
||||
{
|
||||
FullRoom fullRoom = new FullRoom
|
||||
{
|
||||
Name = room.Name,
|
||||
RoomId = room.RoomId,
|
||||
Description = room.Description ?? string.Empty,
|
||||
ImageName = room.ImageName,
|
||||
IsRRO = room.IsRRO,
|
||||
IsDeveloperOwned = room.IsDeveloperOwned,
|
||||
IsDorm = room.IsDorm,
|
||||
State = room.State,
|
||||
Accessibility = room.Accessibility,
|
||||
CreatedAt = room.CreatedAt,
|
||||
CreatorAccountId = room.CreatorAccountId,
|
||||
SupportsJuniors = room.SupportsJuniors,
|
||||
SupportsLevelVoting = room.SupportsLevelVoting,
|
||||
SupportsMobile = room.SupportsMobile,
|
||||
SupportsQuest2 = room.SupportsQuest2,
|
||||
SupportsScreens = room.SupportsScreens,
|
||||
SupportsTeleportVR = room.SupportsTeleportVR,
|
||||
SupportsVRLow = room.SupportsVRLow,
|
||||
SupportsWalkVR = room.SupportsWalkVR,
|
||||
EncryptVoiceChat = room.EncryptVoiceChat,
|
||||
MaxPlayerCalculationMode = room.MaxPlayerCalculationMode,
|
||||
MaxPlayers = room.MaxPlayers,
|
||||
SubRooms = subRooms,
|
||||
Stats = stats,
|
||||
CloningAllowed = room.CloningAllowed,
|
||||
CustomWarning = room.CustomWarning ?? string.Empty,
|
||||
Roles = roles,
|
||||
DataBlob = null,
|
||||
Tags = StringTools.ReadTags(room.Tags),
|
||||
LoadScreens = room.loadScreen.FromJson<List<RoomLoadScreen>>() ?? new(),
|
||||
WarningMask = room.WarningMask,
|
||||
Version = room.Version,
|
||||
DisableRoomComments = room.DisableRoomComments,
|
||||
DisableMicAutoMute = room.DisableMicAutoMute,
|
||||
MinLevel = room.MinLevel
|
||||
};
|
||||
|
||||
foreach (var subRoom in subRooms)
|
||||
{
|
||||
subRoom.CurrentSave = string.IsNullOrEmpty(subRoom.DataBlob) ? null : BuildCurrentSave(subRoom);
|
||||
|
||||
subRoom.CreatorAccountId = subRoom.SavedByAccountId != 0 ? subRoom.SavedByAccountId : null;
|
||||
}
|
||||
|
||||
return fullRoom;
|
||||
}
|
||||
|
||||
public static async Task<List<FullRoom>> BulkConvertRoomData(List<RoomData> data)
|
||||
{
|
||||
var fullRooms = new List<FullRoom>();
|
||||
foreach (var d in data)
|
||||
{
|
||||
var found = await GetRoom(d.RoomId);
|
||||
if (found != null)
|
||||
fullRooms.Add(found);
|
||||
}
|
||||
return fullRooms;
|
||||
}
|
||||
public static async Task<List<RoomDetail>> BulkConvertRoomDataToRoomDetail(List<RoomData> data)
|
||||
{
|
||||
var rooms = new List<RoomDetail>();
|
||||
foreach (var d in data)
|
||||
{
|
||||
var found = await GetRoom(d.RoomId);
|
||||
if (found != null)
|
||||
rooms.Add(found);
|
||||
}
|
||||
return rooms;
|
||||
}
|
||||
|
||||
public static async Task<FullRoom?> GetRoom(string Name)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.Name == Name);
|
||||
if (room == null && !string.IsNullOrWhiteSpace(Name) && Name.StartsWith("@", StringComparison.Ordinal) && Name.EndsWith("'s Dorm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var username = Name.Substring(1, Name.Length - 8);
|
||||
var owner = await ctx.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.username == username || x.displayName == username);
|
||||
if (owner != null)
|
||||
{
|
||||
room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync(x => x.IsDorm && x.CreatorAccountId == owner.accountId);
|
||||
}
|
||||
}
|
||||
if (room == null) return null;
|
||||
|
||||
return await GetRoom(room.RoomId);
|
||||
}
|
||||
public static async Task<FullRoom?> GetRoom(long Id)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var room = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.RoomId == Id);
|
||||
if (room == null) return null;
|
||||
|
||||
var cheers = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Cheered);
|
||||
var favourites = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Favorited);
|
||||
var visitorCount = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == Id && x.Visited);
|
||||
var visitCount = await ctx.RoomInteractions.AsNoTracking()
|
||||
.Where(x => x.RoomId == Id && x.Visited)
|
||||
.SumAsync(x => (int?)x.VisitCount) ?? 0;
|
||||
|
||||
var roomStats = new RoomStats { CheerCount = cheers, FavoriteCount = favourites, RoomId = Id, VisitCount = visitCount, VisitorCount = visitorCount };
|
||||
|
||||
var roomRoles = await GetRoles(Id);
|
||||
|
||||
var subRooms = await ctx.SubRooms.AsNoTracking().Where((x) => x.RoomId == Id).ToListAsync();
|
||||
|
||||
var missingBlobs = subRooms
|
||||
.Where(x => !string.IsNullOrEmpty(x.DataBlob) && !File.Exists(Path.Join(DBConfig.CDNStorage, x.DataBlob)))
|
||||
.ToList();
|
||||
|
||||
if (missingBlobs.Count > 0)
|
||||
{
|
||||
var missingIds = missingBlobs.Select(x => x.SubRoomId).ToList();
|
||||
|
||||
var candidates = await ctx.SubRoomHistory.AsNoTracking()
|
||||
.Where(x => missingIds.Contains(x.SubRoomId) && x.DataBlob != "")
|
||||
.OrderByDescending(x => x.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var subRoom in missingBlobs)
|
||||
{
|
||||
var recovered = candidates
|
||||
.Where(x => x.SubRoomId == subRoom.SubRoomId)
|
||||
.Select(x => x.DataBlob)
|
||||
.FirstOrDefault(blob => !string.IsNullOrEmpty(blob) && File.Exists(Path.Join(DBConfig.CDNStorage, blob)));
|
||||
|
||||
subRoom.DataBlob = recovered ?? "";
|
||||
ctx.SubRooms.Attach(subRoom);
|
||||
ctx.Entry(subRoom).Property(x => x.DataBlob).IsModified = true;
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
return room.ConvertToFull(subRooms, roomStats, roomRoles);
|
||||
}
|
||||
|
||||
public static async Task<RoomStats?> GetRoomStats(long roomId, long? playerId)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var cheers = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Cheered);
|
||||
var favourites = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Favorited);
|
||||
|
||||
var visitorCount = await ctx.RoomInteractions.AsNoTracking().CountAsync(x => x.RoomId == roomId && x.Visited);
|
||||
|
||||
var visited = 0;
|
||||
if (playerId != null)
|
||||
{
|
||||
var interaction = await ctx.RoomInteractions.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.RoomId == roomId && x.PlayerId == playerId);
|
||||
|
||||
if (interaction != null)
|
||||
visited = interaction.VisitCount;
|
||||
}
|
||||
|
||||
return new RoomStats() { CheerCount = cheers, FavoriteCount = favourites, RoomId = roomId, VisitCount = visited, VisitorCount = visitorCount };
|
||||
}
|
||||
|
||||
|
||||
public static async Task<FullRoom?> CloneRoom(long originalRoomId, long newCreator, string newName)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
FullRoom? room = await GetRoom(originalRoomId);
|
||||
if (room == null)
|
||||
return null;
|
||||
|
||||
|
||||
|
||||
var tags = room.Tags;
|
||||
var rro = tags.Find(x => x.Tag.StartsWith("rro") && x.Type >= TagType.Auto);
|
||||
if (rro != null)
|
||||
tags.Remove(rro);
|
||||
|
||||
RoomData roomData = new RoomData
|
||||
{
|
||||
State = RoomState.Active,
|
||||
SupportsJuniors = room.SupportsJuniors,
|
||||
SupportsLevelVoting = room.SupportsLevelVoting,
|
||||
SupportsMobile = room.SupportsMobile,
|
||||
SupportsQuest2 = room.SupportsQuest2,
|
||||
SupportsScreens = room.SupportsScreens,
|
||||
SupportsTeleportVR = room.SupportsTeleportVR,
|
||||
SupportsVRLow = room.SupportsVRLow,
|
||||
SupportsWalkVR = room.SupportsWalkVR,
|
||||
Accessibility = Accessibility.Private,
|
||||
CloningAllowed = false,
|
||||
CreatedAt = DateTime.Now,
|
||||
CreatorAccountId = newCreator,
|
||||
CustomWarning = room.CustomWarning,
|
||||
Description = room.Description,
|
||||
DisableMicAutoMute = room.DisableMicAutoMute,
|
||||
DisableRoomComments = room.DisableRoomComments,
|
||||
EncryptVoiceChat = room.EncryptVoiceChat,
|
||||
ImageName = room.ImageName,
|
||||
IsDorm = room.IsDorm,
|
||||
IsRRO = false,
|
||||
|
||||
IsDeveloperOwned = !room.IsDorm && await ctx.IsDeveloper(newCreator),
|
||||
MaxPlayerCalculationMode = room.MaxPlayerCalculationMode,
|
||||
MaxPlayers = room.MaxPlayers,
|
||||
MinLevel = room.MinLevel,
|
||||
Name = newName,
|
||||
Tags = StringTools.CreateTagString(tags),
|
||||
WarningMask = room.WarningMask,
|
||||
};
|
||||
|
||||
await ctx.AddAsync(roomData);
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
|
||||
if (room.SubRooms == null || room.SubRooms.Count == 0)
|
||||
{
|
||||
ctx.Remove(roomData);
|
||||
await ctx.SaveChangesAsync();
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (SubRoom subRoom in room.SubRooms)
|
||||
{
|
||||
if (subRoom == null) continue;
|
||||
await ctx.SubRooms.AddAsync(new SubRoom
|
||||
{
|
||||
Accessibility = subRoom.Accessibility,
|
||||
DataBlob = subRoom.DataBlob,
|
||||
UnityAssetId = subRoom.UnityAssetId,
|
||||
IsSandbox = subRoom.IsSandbox,
|
||||
UnitySceneId = subRoom.UnitySceneId,
|
||||
MaxPlayers = subRoom.MaxPlayers,
|
||||
Name = subRoom.Name,
|
||||
RoomId = roomData.RoomId
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
|
||||
await ctx.RoomStats.AddAsync(new RoomStats
|
||||
{
|
||||
RoomId = roomData.RoomId,
|
||||
FavoriteCount = 0,
|
||||
CheerCount = 0,
|
||||
VisitCount = 0,
|
||||
VisitorCount = 0,
|
||||
});
|
||||
|
||||
|
||||
await ctx.RoomRoles.AddAsync(new RoomRole
|
||||
{
|
||||
RoomId = roomData.RoomId,
|
||||
Role = RoleType.Creator,
|
||||
InvitedRole = RoleType.None,
|
||||
AccountId = newCreator
|
||||
});
|
||||
|
||||
await ctx.SaveChangesAsync();
|
||||
|
||||
return await GetRoom(roomData.RoomId);
|
||||
}
|
||||
|
||||
private static async Task<FullRoom?> GetDormTemplateRoom()
|
||||
{
|
||||
var templateRoom = await GetRoom(1);
|
||||
if (templateRoom != null) return templateRoom;
|
||||
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
var template = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync(room => room.IsDorm && room.CreatorAccountId == 1);
|
||||
return template == null ? null : await GetRoom(template.RoomId);
|
||||
}
|
||||
|
||||
public static async Task<FullRoom?> GetOrCreateDorm(long playerId)
|
||||
{
|
||||
await using var ctx = await DbFactory.CreateDbContextAsync();
|
||||
|
||||
var roomData = await ctx.Rooms.AsNoTracking().FirstOrDefaultAsync((x) => x.IsDorm && x.CreatorAccountId == playerId);
|
||||
if (roomData == null)
|
||||
{
|
||||
var templateRoom = await GetDormTemplateRoom();
|
||||
if (templateRoom == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await CloneRoom(templateRoom.RoomId, playerId, "DormRoom");
|
||||
}
|
||||
|
||||
|
||||
var hasSubRoom = await ctx.SubRooms.AnyAsync(s => s.RoomId == roomData.RoomId);
|
||||
if (!hasSubRoom)
|
||||
{
|
||||
var templateRoom = await GetDormTemplateRoom();
|
||||
var templateSub = templateRoom?.SubRooms?.FirstOrDefault();
|
||||
if (templateSub != null)
|
||||
{
|
||||
await ctx.SubRooms.AddAsync(new SubRoom
|
||||
{
|
||||
RoomId = roomData.RoomId,
|
||||
DataBlob = templateSub.DataBlob,
|
||||
UnityAssetId = templateSub.UnityAssetId,
|
||||
IsSandbox = templateSub.IsSandbox,
|
||||
UnitySceneId = templateSub.UnitySceneId,
|
||||
MaxPlayers = templateSub.MaxPlayers,
|
||||
Name = templateSub.Name,
|
||||
Accessibility = templateSub.Accessibility
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
return await GetRoom(roomData.RoomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SixLabors.ImageSharp;
|
||||
|
||||
namespace RadicadeServer.CDN
|
||||
{
|
||||
[Route("cdn/room/")]
|
||||
[Route("room/")]
|
||||
[Route("unity/")]
|
||||
[Route("unity_assets/")]
|
||||
[Route("roomserver/unity_assets/")]
|
||||
public class RoomsCDN : Controller
|
||||
{
|
||||
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult Index() => Ok(new { status = "ok" });
|
||||
|
||||
[HttpGet("{datablob}")]
|
||||
public IActionResult GetDataBlob(string datablob)
|
||||
{
|
||||
if (!CdnStorageNames.IsServable(datablob)) return BadRequest();
|
||||
|
||||
var name = CdnStorageNames.Resolve(datablob);
|
||||
if (name == null) return NotFound();
|
||||
|
||||
CdnCaching.Apply(this, CdnCaching.ForBlob(name));
|
||||
|
||||
return PhysicalFile(Path.Join(DBConfig.CDNStorage, name), "application/octet-stream", enableRangeProcessing: true);
|
||||
}
|
||||
|
||||
[HttpGet("{assetId}/{target}/{version}")]
|
||||
[HttpGet("{assetId}/{target}")]
|
||||
public IActionResult GetUnityAssetRaw(string assetId, int? target, int? version)
|
||||
{
|
||||
// The bytes half of the two-step download, for a client that asks by id rather than by
|
||||
// the Filename the metadata route handed it: /unity/{id} is the id with no extension,
|
||||
// while an imported bundle is stored as <id>.assetbundle, so both spellings resolve.
|
||||
if (!CdnStorageNames.IsServable(assetId)) return BadRequest();
|
||||
|
||||
var name = CdnStorageNames.Resolve(assetId);
|
||||
if (name == null) return NotFound();
|
||||
|
||||
CdnCaching.Apply(this, CdnCaching.Immutable);
|
||||
|
||||
return PhysicalFile(Path.Join(DBConfig.CDNStorage, name), "application/octet-stream", enableRangeProcessing: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.Utils;
|
||||
|
||||
namespace RadicadeServer.API.Rooms.V1
|
||||
{
|
||||
[Route("api/rooms/v1")]
|
||||
public class RoomsV1 : Controller
|
||||
{
|
||||
RecDBContext db;
|
||||
public RoomsV1(RecDBContext context)
|
||||
{
|
||||
this.db = context;
|
||||
}
|
||||
|
||||
[HttpGet("filters")]
|
||||
public async Task<ActionResult<RoomFilters>> GetRoomFilters()
|
||||
{
|
||||
var dbFilters = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == "Filters");
|
||||
if (dbFilters == null)
|
||||
return NotFound();
|
||||
|
||||
var filters = dbFilters.Value.Split(',', StringSplitOptions.TrimEntries).ToList();
|
||||
|
||||
return new RoomFilters { PinnedFilters = filters , PopularFilters = filters, TrendingFilters = filters };
|
||||
}
|
||||
|
||||
[HttpPost("verifyRole"), Authorize]
|
||||
public async Task<Success?> VerifyRole([FromForm] long roomId, [FromForm] RoleType role, [FromForm] string? context)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
var roomRole = await RoomUtils.VerifyRole((long)userId!, roomId, role);
|
||||
|
||||
if (!roomRole)
|
||||
return new Success(false, "Invalid roles", false);
|
||||
else
|
||||
return new Success(true, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public static class SceneLocations
|
||||
{
|
||||
public const string MakerRoom = "a75f7547-79eb-47c6-8986-6767abcb4f92";
|
||||
|
||||
public const string Dorm = "76d98498-60a1-430c-ab76-b54a29b7a163";
|
||||
|
||||
private static readonly HashSet<string> KnownBadValues = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"unknown",
|
||||
"none",
|
||||
"null",
|
||||
};
|
||||
|
||||
public static bool IsValid(string? unitySceneId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(unitySceneId))
|
||||
return false;
|
||||
|
||||
var trimmed = unitySceneId.Trim();
|
||||
if (KnownBadValues.Contains(trimmed))
|
||||
return false;
|
||||
|
||||
return Guid.TryParse(trimmed, out _);
|
||||
}
|
||||
|
||||
public static string ResolveFallback(IEnumerable<string?> candidateUnitySceneIds, bool isDorm)
|
||||
{
|
||||
foreach (var id in candidateUnitySceneIds)
|
||||
{
|
||||
if (IsValid(id))
|
||||
return id!.Trim();
|
||||
}
|
||||
|
||||
return isDorm ? Dorm : MakerRoom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class SubRoom
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long SubRoomId { get; set; }
|
||||
public long RoomId { get; set; }
|
||||
public long SavedByAccountId { get; set; }
|
||||
public string Name { get; set; } = "databasebroken";
|
||||
public string DataBlob { get; set; } = "";
|
||||
|
||||
public string UnityAssetId { get; set; } = "";
|
||||
|
||||
[NotMapped]
|
||||
public SubRoomDataSave? CurrentSave { get; set; }
|
||||
[NotMapped]
|
||||
public long? CreatorAccountId { get; set; }
|
||||
public bool IsSandbox { get; set; }
|
||||
public int MaxPlayers { get; set; }
|
||||
public Accessibility Accessibility { get; set; }
|
||||
public string UnitySceneId { get; set; } = "";
|
||||
public DateTime CreatedAt { get; set; }
|
||||
[NotMapped]
|
||||
public DateTime DataSavedAt { get => CreatedAt; set => CreatedAt = value; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class SubRoomDataSave
|
||||
{
|
||||
public List<object> UnitySubAssets { get; set; } = new();
|
||||
public List<object> ReferencedUnityAssets { get; set; } = new();
|
||||
public long SubRoomDataSaveId { get; set; }
|
||||
public long SubRoomId { get; set; }
|
||||
public string DataBlob { get; set; } = string.Empty;
|
||||
public List<string> ReferencedUnityAssetIds { get; set; } = new();
|
||||
public int PersistenceVersion { get; set; }
|
||||
public int OMVersion { get; set; }
|
||||
public int UgcSubVersion { get; set; }
|
||||
public long? SavedByAccountId { get; set; }
|
||||
public int SavedOnPlatform { get; set; }
|
||||
public int SavedOnDeviceClass { get; set; }
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public List<object> Tags { get; set; } = new();
|
||||
public int ModerationState { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public string? UnityAssetId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class SubRoomPermission
|
||||
{
|
||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public long Id { get; set; }
|
||||
|
||||
public long subRoomId { get; set; }
|
||||
|
||||
[MaxLength(128)]
|
||||
public string permission { get; set; } = "";
|
||||
|
||||
public long role { get; set; }
|
||||
|
||||
public int type { get; set; }
|
||||
|
||||
public string value { get; set; } = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class UserLoadScreen
|
||||
{
|
||||
public string imageName { get; set; } = null!;
|
||||
public string title { get; set; } = null!;
|
||||
public string subtitle { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,723 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using RadicadeServer.API.Notifications;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using RadicadeServer.RecNet.Enums.Rooms;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
using RadicadeServer.RecNet.Responses;
|
||||
using RadicadeServer.RecNet.Rooms;
|
||||
using RadicadeServer.Utils;
|
||||
|
||||
|
||||
namespace RadicadeServer.RoomServer
|
||||
{
|
||||
[Route("rooms/rooms/")]
|
||||
public class V2_RoomsAPI(RecDBContext db) : Controller
|
||||
{
|
||||
[HttpGet("{id:long}/similar"), Authorize]
|
||||
public async Task<IActionResult> GetSimilarRooms(long id, [FromQuery] int skip = 0, [FromQuery] int take = 100)
|
||||
{
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Ok(EmptyPage());
|
||||
|
||||
var userId = User.GetUserId();
|
||||
skip = Math.Max(skip, 0);
|
||||
take = Math.Clamp(take, 1, 100);
|
||||
|
||||
var ownTags = StringTools.ReadTags(room.Tags)
|
||||
.Select(t => t.Tag?.Trim().ToLowerInvariant())
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToHashSet();
|
||||
|
||||
var candidates = await db.Rooms
|
||||
.Where(x => x.RoomId != id && x.Accessibility == Accessibility.Public)
|
||||
.ToListAsync();
|
||||
|
||||
var scored = new List<(RoomData Room, int Score, int Visits)>();
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
if (!await RoomUtils.CanSeeRoom(candidate, userId)) continue;
|
||||
|
||||
int score;
|
||||
if (ownTags.Count == 0)
|
||||
{
|
||||
score = candidate.CreatorAccountId == room.CreatorAccountId ? 1 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
score = StringTools.ReadTags(candidate.Tags)
|
||||
.Count(t => ownTags.Contains(t.Tag?.Trim().ToLowerInvariant()));
|
||||
}
|
||||
|
||||
if (score <= 0) continue;
|
||||
|
||||
var stats = await db.RoomStats.FirstOrDefaultAsync(x => x.RoomId == candidate.RoomId);
|
||||
scored.Add((candidate, score, stats?.VisitCount ?? 0));
|
||||
}
|
||||
|
||||
var ordered = scored
|
||||
.OrderByDescending(x => x.Score)
|
||||
.ThenByDescending(x => x.Visits)
|
||||
.Select(x => x.Room)
|
||||
.ToList();
|
||||
|
||||
var picked = ordered.Skip(skip).Take(take).ToList();
|
||||
var details = await RoomUtils.BulkConvertRoomDataToRoomDetail(picked);
|
||||
|
||||
return Ok(new RoomSearchResult { TotalResults = ordered.Count, Results = details });
|
||||
}
|
||||
|
||||
private static RoomSearchResult EmptyPage() => new() { TotalResults = 0, Results = new List<RoomDetail>() };
|
||||
|
||||
[HttpGet("{id:long}/experience")]
|
||||
public IActionResult GetRoomExperience(long id)
|
||||
=> Ok(new { Enabled = false, DailyLimit = 1000 });
|
||||
|
||||
[HttpGet("{id:long}/experience/player")]
|
||||
public IActionResult GetRoomExperienceForPlayer(long id)
|
||||
=> Ok(Array.Empty<object>());
|
||||
|
||||
[HttpPut("{id}/description"), Authorize]
|
||||
public async Task<Success> SetDescription(long? id, string description)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
if (!description.IsPure()) return new Success(false, "Description contains offensive language.", null);
|
||||
|
||||
room.Description = description;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/name"), Authorize]
|
||||
public async Task<Success> SetName(long? id, string name)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
if (!name.IsPure()) return new Success(false, "Name contains offensive language.", null);
|
||||
|
||||
var sameRoomWithName = await db.Rooms.FirstOrDefaultAsync(x => x.Name == name);
|
||||
if (sameRoomWithName != null) return new Success(false, "A room already exists with that name.", null);
|
||||
|
||||
room.Name = name;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/warning"), Authorize]
|
||||
public async Task<Success> SetWarning(long? id, WarningMask? warningMask, string? customWarning)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
if (warningMask != null)
|
||||
room.WarningMask = warningMask.Value;
|
||||
if (customWarning != null)
|
||||
room.CustomWarning = customWarning;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/cloning"), Authorize]
|
||||
public async Task<Success> SetAllowedCloning(long? id, bool cloningAllowed)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
room.CloningAllowed = cloningAllowed;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/restrictions"), Authorize]
|
||||
public async Task<Success> SetRestrictions(long? id, bool supportsScreens, bool supportsWalkVR, bool supportsTeleportVR, bool supportsJuniors)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
room.SupportsTeleportVR = supportsTeleportVR;
|
||||
room.SupportsWalkVR = supportsWalkVR;
|
||||
room.SupportsScreens = supportsScreens;
|
||||
room.SupportsJuniors = supportsJuniors;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/automute"), Authorize]
|
||||
public async Task<Success> SetDisableAutoMute(long? id, bool disable)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
room.DisableMicAutoMute = disable;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/comments"), Authorize]
|
||||
public async Task<Success> SetDisableComments(long? id, bool disable)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
room.DisableRoomComments = disable;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/voice_chat_encryption"), Authorize]
|
||||
public async Task<Success> SetEncryptVoiceChat(long? id, bool encryptVoiceChat)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
room.EncryptVoiceChat = encryptVoiceChat;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/allow_new_users"), Authorize]
|
||||
public async Task<Success> SetAllowNewUsers(long? id, bool allowNewUsers)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet("{id}/bans"), Authorize]
|
||||
public async Task<List<RoomBan>> GetRoomBans(long? id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null || id == null) return new List<RoomBan>();
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return new List<RoomBan>();
|
||||
|
||||
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
|
||||
if (!canModify) return new List<RoomBan>();
|
||||
|
||||
return await db.RoomBans
|
||||
.Where(x => x.roomId == id && (!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow))
|
||||
.OrderByDescending(x => x.bannedAt)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpGet("{id}/bans/{bannedPlayerId}"), Authorize]
|
||||
public async Task<RoomBan?> GetRoomBan(long? id, long bannedPlayerId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null || id == null) return null;
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return null;
|
||||
|
||||
|
||||
if (bannedPlayerId != (long)userId)
|
||||
{
|
||||
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
|
||||
if (!canModify) return null;
|
||||
}
|
||||
|
||||
return await db.RoomBans
|
||||
.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == bannedPlayerId &&
|
||||
(!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpPost("{id}/bans/import"), Authorize]
|
||||
public async Task<Success> ImportRoomBans(long? id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null || id == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var playerIds = new List<long>();
|
||||
string rawBody = "";
|
||||
|
||||
try
|
||||
{
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var key in new[] { "playerIds", "PlayerIds", "ids", "Ids", "bannedPlayerIds", "BannedPlayerIds", "players", "Players" })
|
||||
if (form.TryGetValue(key, out var values))
|
||||
foreach (var value in values)
|
||||
foreach (var part in (value ?? string.Empty).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
if (long.TryParse(part, out var parsedId) && parsedId > 0)
|
||||
playerIds.Add(parsedId);
|
||||
}
|
||||
|
||||
if (Request.ContentLength is > 0)
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
|
||||
rawBody = await reader.ReadToEndAsync();
|
||||
if (!string.IsNullOrWhiteSpace(rawBody))
|
||||
{
|
||||
var parsedBody = JsonNode.Parse(rawBody);
|
||||
if (parsedBody is JsonArray array)
|
||||
{
|
||||
foreach (var item in array)
|
||||
{
|
||||
if (item is JsonValue value && value.TryGetValue<long>(out var bareId) && bareId > 0)
|
||||
playerIds.Add(bareId);
|
||||
else if (item is JsonObject obj)
|
||||
playerIds.Add(ParsePlayerId(obj, new[] { "bannedPlayerId", "playerId", "accountId", "id" }));
|
||||
}
|
||||
}
|
||||
else if (parsedBody is JsonObject body)
|
||||
{
|
||||
foreach (var key in new[] { "playerIds", "PlayerIds", "ids", "Ids", "bannedPlayerIds", "BannedPlayerIds", "players", "Players", "bans", "Bans" })
|
||||
{
|
||||
if (!body.TryGetPropertyValue(key, out var node) || node == null) continue;
|
||||
if (node is JsonArray arr)
|
||||
{
|
||||
foreach (var item in arr)
|
||||
{
|
||||
if (item is JsonValue value && value.TryGetValue<long>(out var bareId) && bareId > 0)
|
||||
playerIds.Add(bareId);
|
||||
else if (item is JsonObject obj)
|
||||
playerIds.Add(ParsePlayerId(obj, new[] { "bannedPlayerId", "playerId", "accountId", "id" }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
playerIds = playerIds.Distinct().ToList();
|
||||
|
||||
if (playerIds.Count == 0)
|
||||
return Success.Error("No valid player ids found in the import request");
|
||||
|
||||
var actorProfile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == (long)userId);
|
||||
var imported = new List<RoomBan>();
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
foreach (var targetPlayer in playerIds)
|
||||
{
|
||||
if (targetPlayer == (long)userId) continue;
|
||||
if (await MatchmakingUtils.IsAtLeastRole(targetPlayer, (long)id!, RoleType.CoOwner)) continue;
|
||||
if (await db.Profiles.AnyAsync(x => x.accountId == targetPlayer) == false) continue;
|
||||
|
||||
var existing = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer &&
|
||||
(!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow));
|
||||
if (existing != null) continue;
|
||||
|
||||
var ban = new RoomBan
|
||||
{
|
||||
roomId = (long)id!,
|
||||
actorId = (long)userId,
|
||||
bannedPlayerId = targetPlayer,
|
||||
bannedByName = actorProfile?.displayName ?? $"Player {(long)userId}",
|
||||
reason = "",
|
||||
bannedAt = now,
|
||||
expiresAt = null
|
||||
};
|
||||
|
||||
db.RoomBans.Add(ban);
|
||||
imported.Add(ban);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return new Success(true, null, new Dictionary<string, object?>
|
||||
{
|
||||
["imported"] = imported.Count,
|
||||
["importedCount"] = imported.Count,
|
||||
["requested"] = playerIds.Count,
|
||||
["bans"] = imported,
|
||||
["playerIds"] = imported.Select(x => x.bannedPlayerId).ToArray()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPut("{id}/bans"), Authorize]
|
||||
[HttpPost("{id}/bans"), Authorize]
|
||||
public async Task<Success> AddRoomBan(long? id)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null || id == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
|
||||
if (!canModify)
|
||||
{
|
||||
|
||||
|
||||
var deniedRole = await RoomUtils.GetRoleForPlayer((long)userId, (long)id!);
|
||||
var highRanksVal = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == "HighRanks");
|
||||
return Success.Error("Insufficient permissions");
|
||||
}
|
||||
|
||||
long targetPlayer = 0;
|
||||
string banReason = "";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
string[] idKeys =
|
||||
{
|
||||
"bannedPlayerId", "bannedPlayerID", "bannedPlayer", "bannedplayerid",
|
||||
"bannedId", "bannedid", "banned_id",
|
||||
"bannerId", "bannerid", "banner_id",
|
||||
"playerId", "playerid", "player_id", "player", "playerAccountId",
|
||||
"bannedUserId", "bannedAccountId",
|
||||
"accountId", "account_id",
|
||||
"targetPlayerId", "targetPlayer", "targetId", "targetid", "target",
|
||||
"victimId", "victimid",
|
||||
"userId", "userid",
|
||||
"id"
|
||||
};
|
||||
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
foreach (var key in idKeys)
|
||||
if (targetPlayer <= 0 && long.TryParse(Request.Form[key], out var formPlayer)) targetPlayer = formPlayer;
|
||||
if (string.IsNullOrEmpty(banReason))
|
||||
foreach (var key in new[] { "reason", "banReason", "ban_reason" })
|
||||
if (Request.Form.TryGetValue(key, out var formReason)) { banReason = formReason.ToString(); break; }
|
||||
}
|
||||
|
||||
|
||||
foreach (var key in idKeys)
|
||||
if (targetPlayer <= 0 && long.TryParse(Request.Query[key], out var queryPlayer)) targetPlayer = queryPlayer;
|
||||
if (string.IsNullOrEmpty(banReason))
|
||||
foreach (var key in new[] { "reason", "banReason", "ban_reason" })
|
||||
if (Request.Query.TryGetValue(key, out var queryReason)) { banReason = queryReason.ToString(); break; }
|
||||
|
||||
|
||||
|
||||
string rawBody = "";
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
|
||||
rawBody = await reader.ReadToEndAsync();
|
||||
if (!string.IsNullOrWhiteSpace(rawBody))
|
||||
{
|
||||
var parsedBody = JsonNode.Parse(rawBody);
|
||||
if (parsedBody is JsonObject body)
|
||||
{
|
||||
if (targetPlayer <= 0) targetPlayer = ParsePlayerId(body, idKeys);
|
||||
if (string.IsNullOrEmpty(banReason)) banReason = ParseJsonString(body, "reason", "banReason", "ban_reason");
|
||||
}
|
||||
else if (parsedBody is JsonValue bareValue && targetPlayer <= 0)
|
||||
{
|
||||
if (bareValue.TryGetValue<long>(out var bareId)) targetPlayer = bareId;
|
||||
else if (bareValue.TryGetValue<string>(out var bareStr) && long.TryParse(bareStr, out var bareParsed)) targetPlayer = bareParsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (targetPlayer <= 0)
|
||||
{
|
||||
return Success.Error("bannedPlayerId is required");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (targetPlayer == (long)userId)
|
||||
{
|
||||
return Success.Error("You cannot ban yourself");
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (await MatchmakingUtils.IsAtLeastRole(targetPlayer, (long)id!, RoleType.CoOwner))
|
||||
return Success.Error("You cannot ban a Co-Owner or higher");
|
||||
|
||||
var profile = await db.Profiles.FirstOrDefaultAsync(x => x.accountId == targetPlayer);
|
||||
if (profile == null) return Success.Error("Player not found");
|
||||
|
||||
var existing = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer &&
|
||||
(!x.expiresAt.HasValue || x.expiresAt.Value > DateTime.UtcNow));
|
||||
if (existing != null) return Success.Error("Player is already banned from this room");
|
||||
|
||||
var actorProfile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == (long)userId);
|
||||
var ban = new RoomBan
|
||||
{
|
||||
roomId = (long)id!,
|
||||
actorId = (long)userId,
|
||||
bannedPlayerId = targetPlayer,
|
||||
bannedByName = actorProfile?.displayName ?? $"Player {(long)userId}",
|
||||
reason = banReason.Trim(),
|
||||
bannedAt = DateTime.UtcNow,
|
||||
expiresAt = null
|
||||
};
|
||||
|
||||
await db.RoomBans.AddAsync(ban);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
|
||||
await MatchmakingUtils.KickPlayerFromRoom(targetPlayer, (long)id!,
|
||||
string.IsNullOrWhiteSpace(ban.reason) ? "You were banned from this room." : $"You were banned from this room: {ban.reason}");
|
||||
|
||||
return new Success(true, null, ban);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[HttpDelete("{id}/bans/{bannedPlayerId}"), HttpDelete("{id}/bans"),
|
||||
HttpPost("{id}/bans/{bannedPlayerId}"), HttpPut("{id}/bans/{bannedPlayerId}"), Authorize]
|
||||
public async Task<Success> RemoveRoomBan(long? id, long? bannedPlayerId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null || id == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
var canModify = await MatchmakingUtils.CanModerateRoom((long)userId, (long)id!);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
|
||||
|
||||
|
||||
long targetPlayer = bannedPlayerId ?? 0;
|
||||
string[] idKeys =
|
||||
{
|
||||
"bannedPlayerId", "bannedPlayerID", "bannedId", "bannedid", "banned_id",
|
||||
"playerId", "playerid", "player_id", "player",
|
||||
"bannerId", "bannerid", "banner_id",
|
||||
"userId", "userid", "id"
|
||||
};
|
||||
if (targetPlayer <= 0 && Request.HasFormContentType)
|
||||
foreach (var key in idKeys)
|
||||
if (long.TryParse(Request.Form[key], out var formPlayer)) { targetPlayer = formPlayer; break; }
|
||||
if (targetPlayer <= 0)
|
||||
foreach (var key in idKeys)
|
||||
if (long.TryParse(Request.Query[key], out var queryPlayer)) { targetPlayer = queryPlayer; break; }
|
||||
if (targetPlayer <= 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
|
||||
var raw = await reader.ReadToEndAsync();
|
||||
if (!string.IsNullOrWhiteSpace(raw) && JsonNode.Parse(raw) is JsonObject body)
|
||||
targetPlayer = ParsePlayerId(body, idKeys);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (targetPlayer <= 0)
|
||||
{
|
||||
return Success.Error("bannedPlayerId is required");
|
||||
}
|
||||
|
||||
var ban = await db.RoomBans.FirstOrDefaultAsync(x => x.roomId == id && x.bannedPlayerId == targetPlayer);
|
||||
if (ban == null) return Success.Error("Player is not banned from this room");
|
||||
|
||||
db.RoomBans.Remove(ban);
|
||||
await db.SaveChangesAsync();
|
||||
return new Success(true, null, ban);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/loadscreen")]
|
||||
public async Task<Success> SetLoadingScreen(long id, string imageName, string title, string subtitle)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return new Success(false, "Invalid auth", null);
|
||||
|
||||
var canModify = await MatchmakingUtils.IsAtLeastRole((long)userId, (long)id!, RoleType.CoOwner);
|
||||
if (!canModify) return Success.Error("Insufficient permissions");
|
||||
|
||||
var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == id);
|
||||
if (room == null) return Success.Error("Room not found");
|
||||
|
||||
var entry = new RoomLoadScreen { ImageName = imageName, RoomId = room.RoomId, Subtitle = subtitle, Title = title };
|
||||
List<RoomLoadScreen> loadScreen = room.loadScreen.FromJson<List<RoomLoadScreen>>() ?? new();
|
||||
if (loadScreen.Count >= 1)
|
||||
loadScreen[0] = entry;
|
||||
else
|
||||
loadScreen.Add(entry);
|
||||
room.loadScreen = loadScreen.ToJson();
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var fullRoom = await RoomUtils.GetRoom((long)id!);
|
||||
await Notify.SendNotify((long)userId, new Notification { Id = "RoomUpdate", Msg = fullRoom });
|
||||
|
||||
return new Success(true, null, fullRoom);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static long ParsePlayerId(JsonObject body, string[] idKeys)
|
||||
{
|
||||
foreach (var prop in body)
|
||||
{
|
||||
if (prop.Value is not JsonValue value) continue;
|
||||
bool keyMatches = false;
|
||||
foreach (var key in idKeys)
|
||||
if (prop.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) { keyMatches = true; break; }
|
||||
if (!keyMatches) continue;
|
||||
|
||||
if (value.TryGetValue<long>(out var longId)) return longId;
|
||||
if (value.TryGetValue<string>(out var strId) && long.TryParse(strId, out var parsed)) return parsed;
|
||||
if (value.TryGetValue<double>(out var doubleId) && doubleId > 0 && doubleId == Math.Floor(doubleId)) return (long)doubleId;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private static string ParseJsonString(JsonObject body, params string[] keys)
|
||||
{
|
||||
foreach (var prop in body)
|
||||
{
|
||||
if (prop.Value is not JsonValue value) continue;
|
||||
bool keyMatches = false;
|
||||
foreach (var key in keys)
|
||||
if (prop.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) { keyMatches = true; break; }
|
||||
if (keyMatches && value.TryGetValue<string>(out var str)) return str ?? "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace RadicadeServer.RecNet.Rooms
|
||||
{
|
||||
public class VerifyRoleRequest
|
||||
{
|
||||
public long roomId { get; set; }
|
||||
public RoomRole role { get; set; } = null!;
|
||||
public string? context { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user