using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Notifications; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Auth; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Messages; using System.Text.Json.Nodes; namespace RadicadeServer.Clubs; [Route("clubs/club")] [Route("api/clubs/v1/club")] [Route("api/clubs/v2/club")] public class ClubsAPI : Controller { private readonly RecDBContext db; public ClubsAPI(RecDBContext db) { this.db = db; } private long ViewerId => User.GetUserId() ?? 0; [HttpGet("home/me"), Authorize] [HttpGet("~/club/home/me"), Authorize] [HttpGet("~/api/clubs/v1/home/me"), Authorize] [HttpGet("~/api/clubs/v2/home/me"), Authorize] public async Task GetMyClubHome() { var homeClub = await ClubStore.GetHomeClubAsync(db, ViewerId); return homeClub == null ? NotFound() : Ok(ClubStore.BuildClub(homeClub, ViewerId)); } [HttpPut("~/clubs/club/home/me"), Authorize] [HttpPut("~/club/home/me"), Authorize] [HttpPut("~/api/clubs/v1/home/me"), Authorize] [HttpPut("~/api/clubs/v2/home/me"), Authorize] public async Task SetMyClubHome() { var body = await ReadBodyObjectAsync(); var clubId = ReadLong(body, "clubId", "ClubId"); if (clubId is null or <= 0) return BadRequest(new { error = "Invalid clubId.", success = false, value = (object?)null }); var club = await ClubStore.GetAsync(db, clubId.Value); if (club == null) return NotFound(); if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Member) return StatusCode(StatusCodes.Status403Forbidden, new { error = "You are not a member of that club.", success = false, value = (object?)null }); var updated = await ClubStore.SetHomeClubAsync(db, ViewerId, clubId.Value); return Ok(new { error = "", success = true, value = ClubStore.BuildClub(updated, ViewerId) }); } [HttpDelete("~/clubs/club/home/me"), Authorize] [HttpDelete("~/club/home/me"), Authorize] [HttpDelete("~/api/clubs/v1/home/me"), Authorize] [HttpDelete("~/api/clubs/v2/home/me"), Authorize] public async Task ClearMyClubHome() { await ClubStore.SetHomeClubAsync(db, ViewerId, null); return Ok(new { error = "", success = true, value = (object?)null }); } [HttpGet("{clubId}"), Authorize] [HttpGet("~/api/clubs/v1/{clubId:long}"), Authorize] [HttpGet("~/api/clubs/v2/{clubId:long}"), Authorize] public async Task GetClub(long clubId) { return Ok(ClubStore.BuildClub(await ClubStore.GetAsync(db, clubId), ViewerId)); } [HttpGet("{clubId}/details"), Authorize] [HttpGet("~/club/{clubId:long}/details"), Authorize] [HttpGet("~/api/clubs/v1/{clubId:long}/details"), Authorize] [HttpGet("~/api/clubs/v2/{clubId:long}/details"), Authorize] public async Task GetClubDetails(long clubId) { return Ok(await ClubStore.BuildClubDetailsAsync(db, await ClubStore.GetAsync(db, clubId), ViewerId)); } [HttpGet("{clubId:long}/mainimage")] [HttpGet("~/club/{clubId:long}/mainimage")] public async Task GetClubMainImage(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); return Ok(new { error = string.Empty, success = true, value = await ClubStore.BuildClubDetailsAsync(db, club, ViewerId) }); } [HttpPut("{clubId:long}/mainimage"), Authorize] [HttpPut("~/club/{clubId:long}/mainimage"), Authorize] public async Task SetClubMainImage(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var body = await ReadBodyObjectAsync(); var imageName = ReadString(body, "imageName", "ImageName")?.Trim(); if (string.IsNullOrWhiteSpace(imageName)) return BadRequest(new { error = "imageName is required.", success = false, value = (object?)null }); var updated = await ClubStore.UpdateAsync(db, clubId, new JsonObject { ["mainImageName"] = imageName }); if (updated == null) return NotFound(); return Ok(new { error = string.Empty, success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); } [HttpGet("{clubId}/members")] [HttpGet("~/club/{clubId:long}/members")] [HttpGet("~/api/clubs/v1/{clubId:long}/members")] [HttpGet("~/api/clubs/v2/{clubId:long}/members")] public async Task GetClubMembers(long clubId, [FromQuery] int? membershipType = null, [FromQuery] int? sortBy = null) { var club = await ClubStore.GetAsync(db, clubId); var members = ClubStore.GetMembers(club); if (membershipType.HasValue) members = members.Where(member => member.MembershipType == membershipType.Value).ToArray(); members = sortBy switch { 1 => members.OrderBy(member => member.AccountId).ToArray(), 2 => members.OrderBy(member => member.CreatedAt).ToArray(), _ => members.OrderByDescending(member => member.MembershipType).ThenBy(member => member.CreatedAt).ToArray() }; var values = members .Select(member => ClubStore.BuildMemberListItem(member, clubId)) .ToArray(); return Ok(new Dictionary { ["error"] = string.Empty, ["success"] = true, ["value"] = values }); } [HttpGet("{clubId}/hasDisabledClubChat"), Authorize] [HttpGet("~/api/clubs/v1/{clubId:long}/hasDisabledClubChat"), Authorize] [HttpGet("~/api/clubs/v2/{clubId:long}/hasDisabledClubChat"), Authorize] public IActionResult GetHasDisabledClubChat(long clubId) { return Content("false", "application/json"); } [HttpGet("{clubId:long}/members/{playerId:long}"), Authorize] [HttpGet("~/club/{clubId:long}/members/{playerId:long}"), Authorize] [HttpGet("~/api/clubs/v1/{clubId:long}/members/{playerId:long}"), Authorize] [HttpGet("~/api/clubs/v2/{clubId:long}/members/{playerId:long}"), Authorize] public async Task GetClubMembership(long clubId, long playerId) { var club = await ClubStore.GetAsync(db, clubId); var role = ClubStore.GetMembershipType(club, playerId); var membership = ClubStore.GetMembers(club).FirstOrDefault(member => member.AccountId == playerId); var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == playerId); return Ok(new { clubId = clubId, creatorAccountId = club?.ownerPlayerId ?? 0L, membershipType = role, LastAnnouncementId = 0L, LastReadAnnouncementId = 0L, announcements = Array.Empty(), subscriptionId = clubId, playerId = playerId, membershipStatus = role >= ClubMembershipTypes.Member ? 1 : 0, member = membership == null ? null : ClubStore.BuildMember(membership, club, profile), role = role, subscribed = role >= ClubMembershipTypes.Member, created = membership == null ? 0L : new DateTimeOffset(membership.CreatedAt).ToUnixTimeSeconds(), createdAt = membership == null ? 0L : new DateTimeOffset(membership.CreatedAt).ToUnixTimeSeconds() }); } [HttpGet("mine/created"), Authorize] [HttpGet("~/api/clubs/v1/mine/created"), Authorize] [HttpGet("~/api/clubs/v2/mine/created"), Authorize] public async Task GetMyCreatedClubs() { var clubs = await ClubStore.LoadAsync(db); return Ok(clubs.Where(x => x.ownerPlayerId == ViewerId).Select(x => ClubStore.BuildClub(x, ViewerId)).ToArray()); } [HttpGet("mine/member"), Authorize] [HttpGet("~/api/clubs/v1/mine/member"), Authorize] [HttpGet("~/api/clubs/v2/mine/member"), Authorize] public async Task GetMyMemberClubs() { var clubs = await ClubStore.LoadAsync(db); return Ok(clubs .Where(club => ClubStore.GetMembershipType(club, ViewerId) >= ClubMembershipTypes.Member) .Select(club => ClubStore.BuildClub(club, ViewerId)) .ToArray()); } [HttpGet("account/{accountId}/created"), Authorize] [HttpGet("~/clubs/account/{accountId:long}/created"), Authorize] [HttpGet("~/api/clubs/v1/account/{accountId:long}/created"), Authorize] [HttpGet("~/api/clubs/v2/account/{accountId:long}/created"), Authorize] public async Task GetClubsCreatedByAccount(long accountId) { var clubs = await ClubStore.LoadAsync(db); return Ok(clubs.Where(x => x.ownerPlayerId == accountId).Select(x => ClubStore.BuildClub(x, ViewerId)).ToArray()); } [HttpGet("~/clubs/account/{accountId:long}/clubs")] [HttpGet("~/api/clubs/v1/account/{accountId:long}/clubs")] [HttpGet("~/api/clubs/v2/account/{accountId:long}/clubs")] public Task GetClubsForAccount(long accountId) => GetClubsCreatedByAccount(accountId); [HttpGet("mostactivetoday"), Authorize] [HttpGet("~/api/clubs/v1/mostactivetoday"), Authorize] [HttpGet("~/api/clubs/v2/mostactivetoday"), Authorize] public async Task GetMostActiveToday() { var clubs = await ClubStore.LoadAsync(db); clubs = ClubStore.FilterByCategory(clubs, ClubStore.CategoryFilter(Request.Query)); return Ok(clubs.Select(x => ClubStore.BuildClub(x, ViewerId)).ToArray()); } [HttpGet("mostactivenow"), Authorize] public async Task GetMostActiveNow() { var clubs = await ClubStore.LoadAsync(db); clubs = ClubStore.FilterByCategory(clubs, ClubStore.CategoryFilter(Request.Query)); return Ok(clubs.Select(x => ClubStore.BuildClub(x, ViewerId)).ToArray()); } [HttpGet("categoryTags"), Authorize] [HttpGet("~/api/clubs/v1/categoryTags"), Authorize] [HttpGet("~/api/clubs/v2/categoryTags"), Authorize] public IActionResult GetCategoryTags() { return Ok(new[] { "Social", "Casual", "Competitive", "Creative", "Entertainment", "Educational", "Lifestyle" }); } [HttpGet("search"), Authorize] [HttpGet("~/api/clubs/v1/search"), Authorize] [HttpGet("~/api/clubs/v2/search"), Authorize] public async Task SearchClubs( [FromQuery] string? query = null, [FromQuery] int sort = 0, [FromQuery] string? category = null, [FromQuery] int skip = 0, [FromQuery] int count = 30, [FromQuery] int take = 0) { var limit = take > 0 ? take : count; var clubs = await ClubStore.LoadAsync(db); if (!string.IsNullOrWhiteSpace(query)) { clubs = clubs .Where(x => x.name.Contains(query, StringComparison.OrdinalIgnoreCase) || x.description.Contains(query, StringComparison.OrdinalIgnoreCase)) .ToList(); } clubs = ClubStore.FilterByCategory(clubs, ClubStore.CategoryFilter(Request.Query) ?? category); var items = clubs .Skip(Math.Max(0, skip)) .Take(Math.Max(1, limit)) .Select(x => ClubStore.BuildClub(x, ViewerId)) .ToArray(); return Ok(ClubStore.BuildSearchResponse(items, Math.Max(0, skip), Math.Max(1, limit))); } [HttpGet("cancreate"), Authorize] [HttpGet("can-create"), Authorize] [HttpGet("create/eligibility"), Authorize] [HttpGet("creation/eligibility"), Authorize] [HttpGet("~/api/clubs/v1/cancreate"), Authorize] [HttpGet("~/api/clubs/v1/can-create"), Authorize] [HttpGet("~/api/clubs/v1/create/eligibility"), Authorize] [HttpGet("~/api/clubs/v1/creation/eligibility"), Authorize] [HttpGet("~/api/clubs/v2/cancreate"), Authorize] [HttpGet("~/api/clubs/v2/can-create"), Authorize] [HttpGet("~/api/clubs/v2/create/eligibility"), Authorize] [HttpGet("~/api/clubs/v2/creation/eligibility"), Authorize] public IActionResult CanCreateClub() { return Ok(ClubStore.BuildEligibility()); } [HttpPut("{clubId:long}/members/invite"), Authorize] [HttpPut("{clubId:long}/members/invitemembers"), Authorize] [HttpPost("{clubId:long}/members/invitemembers"), Authorize] [HttpPut("~/club/{clubId:long}/members/invite"), Authorize] [HttpPut("~/club/{clubId:long}/members/invitemembers"), Authorize] [HttpPost("~/club/{clubId:long}/members/invitemembers"), Authorize] public async Task InviteMember(long clubId) { var inviterId = ViewerId; if (inviterId <= 0) return Unauthorized(); var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); var inviterRole = ClubStore.GetMembershipType(club, inviterId); if (inviterRole < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var (rawAccountIds, rawMembershipType) = await ReadInviteMemberFieldsAsync(); if (rawAccountIds.Count == 0) return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); var membershipType = string.IsNullOrWhiteSpace(rawMembershipType) ? ClubMembershipTypes.Member : ParseInviteMembershipType(rawMembershipType); if (membershipType is not (ClubMembershipTypes.Member or ClubMembershipTypes.Moderator or ClubMembershipTypes.Coowner)) return BadRequest(new { error = "Invalid membershipType.", success = false, value = (object?)null }); if (membershipType > inviterRole) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var updated = club; foreach (var rawAccountId in rawAccountIds) { if (!long.TryParse(rawAccountId, out var accountId) || accountId <= 0) return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); if (accountId == club.ownerPlayerId) return BadRequest(new { error = "You can’t change the club’s creator.", success = false, value = (object?)null }); if (ClubStore.GetMembershipType(updated, accountId) >= ClubMembershipTypes.Member) { updated = await ClubStore.SetMemberTypeAsync(db, clubId, accountId, membershipType, inviterId) ?? updated; continue; } updated = await ClubStore.InviteMemberAsync(db, clubId, accountId, membershipType, inviterId) ?? updated; await Notify.SendMessage(accountId, new Message { FromPlayerId = inviterId, Type = membershipType switch { ClubMembershipTypes.Coowner => MessageType.ClubCoownerInvited, ClubMembershipTypes.Moderator => MessageType.ClubModeratorInvited, _ => MessageType.ClubMemberInvited }, Data = clubId.ToString(System.Globalization.CultureInfo.InvariantCulture), Details = membershipType.ToString(System.Globalization.CultureInfo.InvariantCulture) }); } return Ok(new Dictionary { ["error"] = string.Empty, ["success"] = true, ["value"] = await ClubStore.BuildClubDetailsAsync(db, updated, inviterId) }); } [HttpPut("{clubId:long}/members/changetype"), Authorize] [HttpPost("{clubId:long}/members/changetype"), Authorize] [HttpPut("~/club/{clubId:long}/members/changetype"), Authorize] [HttpPost("~/club/{clubId:long}/members/changetype"), Authorize] public async Task ChangeMemberType(long clubId) { var actorId = ViewerId; if (actorId <= 0) return Unauthorized(); var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); var actorRole = ClubStore.GetMembershipType(club, actorId); if (actorRole < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var body = await ReadBodyObjectAsync(); var accountId = ReadLong(body, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId") ?? ReadLongFromRequest(Request, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId"); if (accountId is null or <= 0 || accountId == club.ownerPlayerId) return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); if (ClubStore.GetMembershipType(club, accountId.Value) < ClubMembershipTypes.Member) return BadRequest(new { error = "That player is not a member of this club.", success = false, value = (object?)null }); var requested = ReadLong(body, "membershipType", "MembershipType", "type", "Type", "role", "Role") ?? ReadLongFromRequest(Request, "membershipType", "MembershipType", "type", "Type", "role", "Role"); if (requested is null or < 0) return BadRequest(new { error = "Invalid membershipType.", success = false, value = (object?)null }); var membershipType = (int)requested.Value; if (membershipType is not (ClubMembershipTypes.Member or ClubMembershipTypes.Moderator or ClubMembershipTypes.Coowner)) return BadRequest(new { error = "Invalid membershipType.", success = false, value = (object?)null }); if (membershipType > actorRole) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var updated = await ClubStore.SetMemberTypeAsync(db, clubId, accountId.Value, membershipType, actorId); if (updated == null) return NotFound(); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, actorId) }); } [HttpPost("{clubId:long}/members/acceptinvite"), Authorize] [HttpPut("{clubId:long}/members/acceptinvite"), Authorize] [HttpPost("{clubId:long}/members/accept"), Authorize] [HttpPut("{clubId:long}/members/accept"), Authorize] [HttpPost("~/club/{clubId:long}/members/acceptinvite"), Authorize] [HttpPut("~/club/{clubId:long}/members/acceptinvite"), Authorize] [HttpPost("~/club/{clubId:long}/members/accept"), Authorize] [HttpPut("~/club/{clubId:long}/members/accept"), Authorize] public async Task AcceptClubInvitation(long clubId) { var inviteeId = ViewerId; if (inviteeId <= 0) return Unauthorized(); var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); var pending = ClubStore.GetMembers(club).FirstOrDefault(member => member.AccountId == inviteeId); if (pending?.MembershipType != ClubMembershipTypes.PendingInvited) return BadRequest(new { error = "No pending invitation exists.", success = false, value = (object?)null }); var updated = await ClubStore.AcceptInvitationAsync(db, clubId, inviteeId); if (updated == null) return NotFound(); return Ok(new Dictionary { ["error"] = string.Empty, ["success"] = true, ["value"] = await ClubStore.BuildClubDetailsAsync(db, updated, inviteeId) }); } [HttpPost("{clubId:long}/members/declineinvite"), Authorize] [HttpPut("{clubId:long}/members/declineinvite"), Authorize] [HttpPost("{clubId:long}/members/decline"), Authorize] [HttpDelete("{clubId:long}/members/invite"), Authorize] [HttpPost("~/club/{clubId:long}/members/declineinvite"), Authorize] [HttpPut("~/club/{clubId:long}/members/declineinvite"), Authorize] [HttpPost("~/club/{clubId:long}/members/decline"), Authorize] [HttpDelete("~/club/{clubId:long}/members/invite"), Authorize] public async Task DeclineClubInvitation(long clubId) { var inviteeId = ViewerId; if (inviteeId <= 0) return Unauthorized(); var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); var pending = ClubStore.GetMembers(club).FirstOrDefault(member => member.AccountId == inviteeId); if (pending?.MembershipType != ClubMembershipTypes.PendingInvited) return BadRequest(new { error = "No pending invitation exists.", success = false, value = (object?)null }); var updated = await ClubStore.RemoveMemberAsync(db, clubId, inviteeId); return Ok(new Dictionary { ["error"] = string.Empty, ["success"] = true, ["value"] = await ClubStore.BuildClubDetailsAsync(db, updated, inviteeId) }); } private static readonly string[] InviteAccountIdKeys = { "accountId", "AccountId", "accountIds", "AccountIds", "accountId[]", "AccountId[]", "playerId", "PlayerId", "playerIds", "PlayerIds", "invitedPlayerId", "InvitedPlayerId", "invitedPlayerIds", "InvitedPlayerIds", "invitedPlayerIds[]", "InvitedPlayerIds[]", "targetPlayerIds", "TargetPlayerIds" }; private static int ParseInviteMembershipType(string raw) { if (int.TryParse(raw, out var numeric)) return numeric; return raw.Trim().ToLowerInvariant() switch { "member" or "regular" or "invited" => ClubMembershipTypes.Member, "moderator" or "mod" => ClubMembershipTypes.Moderator, "coowner" or "co-owner" or "co_owner" or "co owner" => ClubMembershipTypes.Coowner, _ => -1 }; } private async Task<(List AccountIds, string? MembershipType)> ReadInviteMemberFieldsAsync() { var accountIds = new List(); string? membershipType = null; void Collect(string key, string? value) { if (string.IsNullOrWhiteSpace(value)) return; if (key.Equals("membershipType", StringComparison.OrdinalIgnoreCase)) { membershipType ??= value; return; } if (!InviteAccountIdKeys.Contains(key, StringComparer.OrdinalIgnoreCase)) return; foreach (var part in value.Split(new[] { ',', '|', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) if (long.TryParse(part, out _) && !accountIds.Contains(part)) accountIds.Add(part); } if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); foreach (var field in form) foreach (var value in field.Value) Collect(field.Key, value); } else if (Request.ContentLength is > 0) { try { Request.EnableBuffering(); if (await JsonNode.ParseAsync(Request.Body) is JsonObject body) foreach (var pair in body) { if (pair.Value is JsonArray array) foreach (var item in array) Collect(pair.Key, item?.ToString()); else Collect(pair.Key, pair.Value?.ToString()); } Request.Body.Position = 0; } catch { try { Request.Body.Position = 0; } catch { } } } foreach (var key in InviteAccountIdKeys) foreach (var value in Request.Query[key]) Collect(key, value); membershipType ??= Request.Query["membershipType"].FirstOrDefault() ?? Request.Query["MembershipType"].FirstOrDefault(); return (accountIds, membershipType); } [HttpPost("create"), Authorize] [HttpPost("~/club/create"), Authorize] [HttpPost("createclub"), Authorize] [HttpPost("~/api/clubs/v1/create"), Authorize] [HttpPost("~/api/clubs/v1/createclub"), Authorize] [HttpPost("~/api/clubs/v2/create"), Authorize] [HttpPost("~/api/clubs/v2/createclub"), Authorize] public async Task CreateClub([FromForm] string? name = null, [FromForm] string? description = null) { var userId = ViewerId; if (userId <= 0) return Unauthorized("Missing user ID."); var fields = await ReadCreateClubFields(name, description); var club = await ClubStore.CreateAsync( db, userId, fields.Name, fields.Description, fields.Category, fields.Visibility ?? 1, fields.Joinability ?? 0, fields.AllowJuniors ?? true, fields.MinLevel ?? 0, fields.MainImageName); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, club, userId) }); } private async Task<(string? Name, string? Description, string? Category, int? Visibility, int? Joinability, bool? AllowJuniors, int? MinLevel, string? MainImageName)> ReadCreateClubFields(string? name, string? description) { string? category = null; string? visibility = null; string? joinability = null; string? allowJuniors = null; string? minLevel = null; string? mainImageName = null; if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); name ??= FirstFormValue(form, "name", "Name", "clubName", "ClubName"); description ??= FirstFormValue(form, "description", "Description", "clubDescription", "ClubDescription"); category = FirstFormValue(form, "category", "Category", "primaryTag", "PrimaryTag"); visibility = FirstFormValue(form, "visibility", "Visibility"); joinability = FirstFormValue(form, "joinability", "Joinability"); allowJuniors = FirstFormValue(form, "allowJuniors", "AllowJuniors"); minLevel = FirstFormValue(form, "minLevel", "MinLevel"); mainImageName = FirstFormValue(form, "mainImageName", "MainImageName"); } else if (Request.ContentLength is > 0) { using var reader = new StreamReader(Request.Body); var rawBody = await reader.ReadToEndAsync(); if (!string.IsNullOrWhiteSpace(rawBody) && JsonNode.Parse(rawBody) is JsonObject body) { name ??= FirstJsonValue(body, "name", "Name", "clubName", "ClubName"); description ??= FirstJsonValue(body, "description", "Description", "clubDescription", "ClubDescription"); category = FirstJsonValue(body, "category", "Category", "primaryTag", "PrimaryTag"); visibility = FirstJsonValue(body, "visibility", "Visibility"); joinability = FirstJsonValue(body, "joinability", "Joinability"); allowJuniors = FirstJsonValue(body, "allowJuniors", "AllowJuniors"); minLevel = FirstJsonValue(body, "minLevel", "MinLevel"); mainImageName = FirstJsonValue(body, "mainImageName", "MainImageName"); } } name ??= Request.Query["name"].FirstOrDefault() ?? Request.Query["Name"].FirstOrDefault() ?? Request.Query["clubName"].FirstOrDefault() ?? Request.Query["ClubName"].FirstOrDefault(); description ??= Request.Query["description"].FirstOrDefault() ?? Request.Query["Description"].FirstOrDefault() ?? Request.Query["clubDescription"].FirstOrDefault() ?? Request.Query["ClubDescription"].FirstOrDefault(); category ??= Request.Query["category"].FirstOrDefault() ?? Request.Query["Category"].FirstOrDefault() ?? Request.Query["primaryTag"].FirstOrDefault() ?? Request.Query["PrimaryTag"].FirstOrDefault(); return ( name, description, category, ParseVisibility(visibility), ParseJoinability(joinability), ParseFormBool(allowJuniors), int.TryParse(minLevel, out var parsedLevel) && parsedLevel >= 0 ? parsedLevel : null, mainImageName); } private static int? ParseVisibility(string? raw) => raw?.Trim().ToLowerInvariant() switch { "private" or "0" => 0, "public" or "1" => 1, _ => null }; private static int? ParseJoinability(string? raw) => raw?.Trim().ToLowerInvariant().Replace("_", "") switch { "open" or "0" => 0, "inviteonly" or "1" => 1, "asktojoin" or "requesttojoin" or "2" => 2, _ => null }; private static bool? ParseFormBool(string? raw) => raw?.Trim().ToLowerInvariant() switch { "true" or "1" or "yes" => true, "false" or "0" or "no" => false, _ => null }; [HttpPut("{clubId:long}/minlevel"), Authorize] [HttpPut("~/club/{clubId:long}/minlevel"), Authorize] public async Task SetMinimumLevel(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var rawMinLevel = await FirstFormValueAsync(Request, "minLevel", "MinLevel"); if (!int.TryParse(rawMinLevel, out var minLevel) || minLevel < 0) { var body = await ReadBodyObjectAsync(); var parsed = ReadLong(body, "minLevel", "MinLevel"); if (parsed is null or < 0 or > int.MaxValue) return BadRequest(new { error = "Invalid minLevel.", success = false, value = (object?)null }); minLevel = (int)parsed.Value; } var updated = await ClubStore.UpdateAsync(db, clubId, new JsonObject { ["minLevel"] = minLevel }); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); } [HttpPut("{clubId:long}/modifydetails"), Authorize] [HttpPut("{clubId:long}/modify"), Authorize] [HttpPut("~/club/{clubId:long}/modifydetails"), Authorize] [HttpPut("~/club/{clubId:long}/modify"), Authorize] public async Task ModifyClub(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var body = await ReadBodyObjectAsync(); if (body == null) return BadRequest(new { error = "Invalid request.", success = false, value = (object?)null }); var updated = await ClubStore.UpdateAsync(db, clubId, body); if (updated == null) return NotFound(); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); } [HttpPut("{clubId:long}/clubhouse"), Authorize] [HttpDelete("{clubId:long}/clubhouse"), Authorize] [HttpPut("~/club/{clubId:long}/clubhouse"), Authorize] [HttpDelete("~/club/{clubId:long}/clubhouse"), Authorize] public async Task SetClubhouse(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); long? roomId = null; if (!HttpMethods.IsDelete(Request.Method)) { var body = await ReadBodyObjectAsync(); var raw = ReadLong(body, "roomId", "RoomId"); if (body?.Count > 0 && raw == null) return BadRequest(new { error = "Invalid roomId.", success = false, value = (object?)null }); roomId = raw; } var updated = await ClubStore.SetClubhouseAsync(db, clubId, roomId); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); } [HttpPut("{clubId:long}/additionalimage/{index:int}"), Authorize] [HttpDelete("{clubId:long}/additionalimage/{index:int}"), Authorize] [HttpPut("~/club/{clubId:long}/additionalimage/{index:int}"), Authorize] [HttpDelete("~/club/{clubId:long}/additionalimage/{index:int}"), Authorize] public async Task SetAdditionalImage(long clubId, int index) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); if (index < 0 || index >= 3) return BadRequest(new { error = "Invalid additional image slot.", success = false, value = (object?)null }); var body = HttpMethods.IsDelete(Request.Method) ? null : await ReadBodyObjectAsync(); var imageName = body == null ? "" : ReadString(body, "imageName", "ImageName") ?? ""; var updated = await ClubStore.SetAdditionalImageAsync(db, clubId, index, imageName); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); } [HttpDelete("{clubId:long}"), Authorize] [HttpDelete("~/club/{clubId:long}"), Authorize] public async Task DeleteClub(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (club.ownerPlayerId != ViewerId) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); await ClubStore.DeleteAsync(db, clubId); return Ok(new { error = "", success = true, value = (object?)null }); } [HttpPut("{clubId:long}/members/requesttojoin"), Authorize] [HttpPut("~/club/{clubId:long}/members/requesttojoin"), Authorize] public async Task RequestToJoinClub(long clubId) { var existingClub = await ClubStore.GetAsync(db, clubId); if (existingClub == null) return NotFound(); ClubRecord? club; if (ClubStore.GetMembershipType(existingClub, ViewerId) == ClubMembershipTypes.PendingInvited) club = await ClubStore.AcceptInvitationAsync(db, clubId, ViewerId); else club = await ClubStore.RequestToJoinAsync(db, clubId, ViewerId); if (club == null) return NotFound(); var role = ClubStore.GetMembershipType(club, ViewerId); if (role == ClubMembershipTypes.Banned) return StatusCode(StatusCodes.Status403Forbidden, new { error = "You are banned from this club.", success = false, value = (object?)null }); if (club.joinability == 1 && role < ClubMembershipTypes.Member) return BadRequest(new { error = "This club is invite only.", success = false, value = (object?)null }); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, club, ViewerId) }); } [HttpPost("{clubId:long}/members/leave"), Authorize] [HttpPost("~/club/{clubId:long}/members/leave"), Authorize] public async Task LeaveClub(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (club.ownerPlayerId == ViewerId) return StatusCode(StatusCodes.Status403Forbidden, new { error = "You created this club — delete it instead of leaving.", success = false, value = (object?)null }); club = await ClubStore.RemoveMemberAsync(db, clubId, ViewerId) ?? club; return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, club, ViewerId) }); } [HttpPost("{clubId:long}/members/remove"), Authorize] [HttpDelete("{clubId:long}/members/remove"), Authorize] [HttpPost("~/club/{clubId:long}/members/remove"), Authorize] [HttpDelete("~/club/{clubId:long}/members/remove"), Authorize] public async Task RemoveClubMember(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); var actorRole = ClubStore.GetMembershipType(club, ViewerId); if (actorRole < ClubMembershipTypes.Moderator) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var body = await ReadBodyObjectAsync(); var accountId = ReadLong(body, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId") ?? ReadLongFromRequest(Request, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId"); if (accountId is null or <= 0 || accountId == club.ownerPlayerId || accountId == ViewerId) return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); var targetRole = ClubStore.GetMembershipType(club, accountId.Value); if (targetRole >= ClubMembershipTypes.Moderator && actorRole < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var updated = await ClubStore.RemoveMemberAsync(db, clubId, accountId.Value); return updated == null ? NotFound() : Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); } [HttpPost("{clubId:long}/members/ban"), Authorize] [HttpPost("~/club/{clubId:long}/members/ban"), Authorize] public async Task BanMember(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Moderator) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var body = await ReadBodyObjectAsync(); var accountId = ReadLong(body, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId"); if (accountId is null or <= 0 || accountId == club.ownerPlayerId || accountId == ViewerId) return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); var targetRole = ClubStore.GetMembershipType(club, accountId.Value); if (targetRole >= ClubMembershipTypes.Moderator && ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Coowner) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var updated = await ClubStore.SetMemberTypeAsync(db, clubId, accountId.Value, ClubMembershipTypes.Banned, ViewerId); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); } [HttpPost("{clubId:long}/members/unban"), Authorize] [HttpPost("~/club/{clubId:long}/members/unban"), Authorize] public async Task UnbanMember(long clubId) { var club = await ClubStore.GetAsync(db, clubId); if (club == null) return NotFound(); if (ClubStore.GetMembershipType(club, ViewerId) < ClubMembershipTypes.Moderator) return StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }); var body = await ReadBodyObjectAsync(); var accountId = ReadLong(body, "accountId", "AccountId", "playerId", "PlayerId", "targetPlayerId", "TargetPlayerId"); if (accountId is null or <= 0) return BadRequest(new { error = "Invalid accountId.", success = false, value = (object?)null }); var updated = await ClubStore.SetMemberTypeAsync(db, clubId, accountId.Value, ClubMembershipTypes.None, ViewerId); return Ok(new { error = "", success = true, value = await ClubStore.BuildClubDetailsAsync(db, updated, ViewerId) }); } private async Task ReadBodyObjectAsync() { if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); var jsonForm = new JsonObject(); foreach (var pair in form) { if (pair.Value.Count > 1) { var values = new JsonArray(); foreach (var value in pair.Value) values.Add(value); jsonForm[pair.Key] = values; } else { jsonForm[pair.Key] = pair.Value.FirstOrDefault(); } } return jsonForm; } if (Request.ContentLength is not > 0) return new JsonObject(); try { Request.EnableBuffering(); var body = await JsonNode.ParseAsync(Request.Body); Request.Body.Position = 0; return body as JsonObject; } catch { try { Request.Body.Position = 0; } catch { } return null; } } private static string? ReadString(JsonObject? body, params string[] keys) { if (body == null) return null; foreach (var key in keys) foreach (var pair in body) if (pair.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) return pair.Value?.ToString(); return null; } private static long? ReadLong(JsonObject? body, params string[] keys) { var value = ReadString(body, keys); return long.TryParse(value, out var parsed) ? parsed : null; } private static long? ReadLongFromRequest(HttpRequest request, params string[] keys) { foreach (var key in keys) if (long.TryParse(request.Query[key].FirstOrDefault(), out var value)) return value; return null; } private static async Task FirstFormValueAsync(HttpRequest request, params string[] keys) { if (!request.HasFormContentType) return null; var form = await request.ReadFormAsync(); foreach (var key in keys) foreach (var pair in form) if (pair.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) return pair.Value.FirstOrDefault(); return null; } private static string? FirstFormValue(IFormCollection form, params string[] keys) { foreach (var key in keys) { var value = form[key].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(value)) return value; } return null; } private static string? FirstJsonValue(JsonObject? json, params string[] keys) { if (json == null) return null; foreach (var key in keys) { if (json.TryGetPropertyValue(key, out var value)) { var text = value?.ToString(); if (!string.IsNullOrWhiteSpace(text)) return text; } } return null; } }