using System.Text.Json; using Mocha2023.Auth; using Mocha2023.Classes.DBs; using Mocha2023.Classes.DBs.DBClasses; using Microsoft.AspNetCore.Mvc; using static Mocha2023.Classes.DBs.DBClasses.ClubDBClasses; namespace Mocha2023.Controllers { [ApiController] [Mocha2023.Classes.ApiProtection] [Route("/club")] public sealed class ClubController : ControllerBase { [HttpPost("create")] [HttpPut("create")] [RequestSizeLimit(64 * 1024)] public async Task CreateClub() { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); RequestValues values = await ReadRequestValuesAsync(); string name = values.String("name", "clubName")?.Trim() ?? string.Empty; string description = values.String("description", "clubDescription")?.Trim() ?? string.Empty; if (name.Length is < 3 or > 50 || name.Any(char.IsControl)) { return BadRequest(new { success = false, error = "invalid_club_name", message = "Club names must be 3 to 50 characters." }); } if (description.Length > 1_000 || description.Any(value => value == '\0')) { return BadRequest(new { success = false, error = "invalid_club_description", message = "Club descriptions cannot exceed 1,000 characters." }); } var visibility = ParseEnum( values.Int("visibility", "clubVisibility"), ClubVisibility.Public); var joinability = ParseEnum( values.Int("joinability", "clubJoinability"), ClubJoinability.Open); var clubType = ParseEnum( values.Int("clubType", "type"), ClubType.Generic); if (!ClubDB.TryCreate( accountId.Value, name, description, values.String("mainImageName", "imageName", "image"), visibility, joinability, values.Bool(true, "allowJuniors", "supportsJuniors"), values.Int("minLevel", "minimumLevel") ?? 0, clubType, ReadRequestedClubTags(values), out ClubRecord? club, out string? error)) { Console.WriteLine( $"[CLUB CREATE REJECTED] account={accountId.Value} " + $"name={name} error={error ?? "unknown"}"); return BadRequest(new { success = false, error }); } Console.WriteLine( $"[CLUB CREATE] account={accountId.Value} club={club!.ClubId} " + $"name={club.Name} primaryTag={club.PrimaryCategory} " + $"tags={string.Join(',', club.CategoryTags ?? new List())}"); return Ok(ClubDB.ToDetails(club, accountId.Value)); } [HttpGet("mine/created")] public IActionResult GetMyCreatedClubs() { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); List results = ClubDB.GetCreatedBy(accountId.Value) .Select(club => ClubDB.ToSummary(club, accountId.Value)) .ToList(); Console.WriteLine( $"[CLUB MINE CREATED] account={accountId.Value} " + $"clubs={results.Count} ids={string.Join(',', results.Select(value => value.ClubId))}"); return Ok(results); } [HttpGet("account/{accountId:long}/created")] public IActionResult GetCreatedClubsForAccount(long accountId) => Ok(ClubDB.GetCreatedBy(accountId) .Select(club => ClubDB.ToSummary(club)) .ToList()); [HttpGet("mine/member")] public IActionResult GetMyMembershipClubs() { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); List results = ClubDB.GetMemberClubs(accountId.Value) .Select(club => ClubDB.ToSummary(club, accountId.Value)) .ToList(); Console.WriteLine( $"[CLUB MINE MEMBER] account={accountId.Value} " + $"clubs={results.Count} ids={string.Join(',', results.Select(value => value.ClubId))}"); return Ok(results); } [HttpGet("home/me")] [HttpGet("home")] [HttpGet("homeclub/me")] [HttpGet("mine/home")] [HttpGet("mine/homeclub")] public IActionResult GetMyHomeClub() { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); ClubRecord? club = ClubDB.GetHomeClub(accountId.Value); return club == null ? Content("null", "application/json") : Ok(ClubDB.ToSummary(club, accountId.Value)); } [HttpPut("home/me")] [HttpPost("home/me")] [HttpPut("home")] [HttpPost("home")] [HttpPut("mine/home")] [HttpPost("mine/home")] [RequestSizeLimit(16 * 1024)] public async Task SetMyHomeClub() { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); RequestValues values = await ReadRequestValuesAsync(); long? clubId = values.Long("clubId", "id"); if (!ClubDB.SetHomeClub(accountId.Value, clubId)) return BadRequest(new { success = false, error = "not_a_club_member" }); return Ok(new { success = true, ClubId = clubId }); } [HttpGet("categoryTags")] public IActionResult GetCategoryTags() => Ok(ClubDB.CategoryTags); [HttpGet("~/announcements/club/{clubId:long}")] public IActionResult GetClubAnnouncements( long clubId, [FromQuery] int skip = 0, [FromQuery] int take = 50, [FromQuery] int count = 50, [FromQuery] int sort = 0) { if (ClubDB.Get(clubId) == null) return NotFound(); int requestedCount = Math.Clamp(take > 0 ? take : count, 1, 100); Console.WriteLine( $"[CLUB ANNOUNCEMENTS] club={clubId} skip={Math.Max(0, skip)} " + $"take={requestedCount} sort={sort} returned=0"); return Ok(Array.Empty()); } [HttpGet("~/api/playerevents/v1/club/{clubId}")] public IActionResult GetClubEvents( long clubId, [FromQuery] int skip = 0, [FromQuery] int take = 50, [FromQuery] int count = 50) { if (ClubDB.Get(clubId) == null) return NotFound(); int requestedCount = Math.Clamp(take > 0 ? take : count, 1, 100); Console.WriteLine( $"[CLUB EVENTS] club={clubId} skip={Math.Max(0, skip)} " + $"take={requestedCount} returned=0"); return Ok(Array.Empty()); } [HttpGet("~/api/playerevents/v1/clubs")] public IActionResult GetClubEventsForClubs() { var requestedClubIds = new HashSet(); foreach (string? rawValue in Request.Query["id"]) { if (string.IsNullOrWhiteSpace(rawValue)) continue; foreach (string part in rawValue.Split( ',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) { if (long.TryParse(part, out long clubId) && clubId > 0) requestedClubIds.Add(clubId); } } long[] existingClubIds = requestedClubIds .Where(clubId => ClubDB.Get(clubId) != null) .OrderBy(clubId => clubId) .ToArray(); Console.WriteLine( $"[CLUB EVENTS BATCH] requested={requestedClubIds.Count} " + $"existing={existingClubIds.Length} " + $"ids={string.Join(',', existingClubIds)} returned=0"); return Ok(Array.Empty()); } [HttpGet("search")] public IActionResult SearchClubs( [FromQuery] int sort = 0, [FromQuery] string? category = null, [FromQuery] string? query = null, [FromQuery] string? name = null, [FromQuery] int skip = 0, [FromQuery] int count = 32) { long? viewerAccountId = AuthStuff.GetPlayerId(Request); var page = ClubDB.SearchWithTotal( category, query ?? name, sort, skip, count); List results = page.Results .Select(club => ClubDB.ToSummary(club, viewerAccountId)) .ToList(); Console.WriteLine( $"[CLUB SEARCH] category={category ?? "all"} " + $"query={query ?? name ?? "none"} skip={skip} count={count} " + $"returned={results.Count} total={page.TotalResults} " + $"ids={string.Join(',', results.Select(value => value.ClubId))}"); return Ok(new { Clubs = results, Results = results, SearchResults = results, TotalResults = page.TotalResults, Count = results.Count, ContinuationToken = (string?)null }); } [HttpGet("mostactivetoday")] public IActionResult GetMostActiveToday() { long? viewerAccountId = AuthStuff.GetPlayerId(Request); return Ok(ClubDB.Search(null, null, 0, 0, 32) .Select(club => ClubDB.ToSummary(club, viewerAccountId)) .ToList()); } [HttpGet("{clubId:long}")] public IActionResult GetClub(long clubId) { var club = ClubDB.Get(clubId); return club == null || !CanViewClub(club) ? NotFound() : Ok(ClubDB.ToSummary(club)); } [HttpGet("{clubId:long}/details")] public IActionResult GetClubDetails(long clubId) { var club = ClubDB.Get(clubId); return club == null || !CanViewClub(club) ? NotFound() : Ok(ClubDB.ToDetails(club, AuthStuff.GetPlayerId(Request))); } [HttpGet("{clubId:long}/members")] public IActionResult GetClubMembers( long clubId, [FromQuery] int? membershipType = null, [FromQuery] int sortBy = 0, [FromQuery] int skip = 0, [FromQuery] int count = 100) { var club = ClubDB.Get(clubId); if (club == null || !CanViewClub(club)) return NotFound(); IEnumerable memberships = ClubDB.GetMemberships(clubId); if (membershipType.HasValue && Enum.IsDefined(typeof(ClubMembershipType), membershipType.Value)) { ClubMembershipType requestedType = (ClubMembershipType)membershipType.Value; memberships = memberships.Where(value => value.MembershipType == requestedType); } memberships = sortBy switch { 1 => memberships.OrderBy(value => value.CreatedAt), 2 => memberships.OrderByDescending(value => value.CreatedAt), _ => memberships.OrderByDescending(value => value.MembershipType) }; var all = memberships.Select(membership => ClubDB.ToMembership(membership)).ToList(); var results = all.Skip(Math.Max(0, skip)) .Take(Math.Clamp(count, 1, 100)) .ToList(); return Ok(new { Memberships = results, Results = results, TotalResults = all.Count, ContinuationToken = (string?)null }); } [HttpGet("{clubId:long}/members/{accountId:long}")] public IActionResult GetClubMember(long clubId, long accountId) { var club = ClubDB.Get(clubId); if (club == null || !CanViewClub(club)) return NotFound(); var membership = ClubDB.GetMembership(clubId, accountId); return membership == null ? NotFound() : Ok(ClubDB.ToMembership(membership)); } [HttpGet("{clubId:long}/members/search")] public IActionResult SearchClubMembers( long clubId, [FromQuery] string? name = null, [FromQuery] int skip = 0, [FromQuery] int count = 100) { var club = ClubDB.Get(clubId); if (club == null || !CanViewClub(club)) return NotFound(); var memberships = ClubDB.GetMemberships(clubId); if (!string.IsNullOrWhiteSpace(name)) { memberships = memberships.Where(value => PlayerDB.Players.FindById(value.AccountId)?.Player?.Username? .Contains(name, StringComparison.OrdinalIgnoreCase) == true || PlayerDB.Players.FindById(value.AccountId)?.Player?.DisplayName? .Contains(name, StringComparison.OrdinalIgnoreCase) == true) .ToList(); } var all = memberships.Select(membership => ClubDB.ToMembership(membership)).ToList(); var results = all.Skip(Math.Max(0, skip)) .Take(Math.Clamp(count, 1, 100)) .ToList(); return Ok(new { Memberships = results, Results = results, TotalResults = all.Count, ContinuationToken = (string?)null }); } [HttpPost("{clubId:long}/members/directJoin")] [HttpPut("{clubId:long}/members/directJoin")] public IActionResult DirectJoin(long clubId) { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); var club = ClubDB.Get(clubId); if (club == null) return NotFound(); ClubMembershipType existingType = ClubDB.GetMembership( clubId, accountId.Value)?.MembershipType ?? ClubMembershipType.None; if (existingType == ClubMembershipType.Banned) return StatusCode(403); if (existingType == ClubMembershipType.PendingInvited) { ClubDB.SetMembership(clubId, accountId.Value, ClubMembershipType.Member); return Ok(new { success = true, ClubMembershipType = (int)ClubMembershipType.Member }); } if (club.Visibility == ClubVisibility.Private || club.Joinability == ClubJoinability.InviteOnly) return BadRequest(new { success = false, error = "invite_required" }); ClubMembershipType type = club.Joinability == ClubJoinability.AskToJoin ? ClubMembershipType.PendingRequested : ClubMembershipType.Member; ClubDB.SetMembership(clubId, accountId.Value, type); return Ok(new { success = true, ClubMembershipType = (int)type }); } [HttpPost("{clubId:long}/members/requesttojoin")] [HttpPut("{clubId:long}/members/requesttojoin")] public IActionResult RequestToJoin(long clubId) { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); ClubRecord? club = ClubDB.Get(clubId); if (club == null) return NotFound(); ClubMembershipType existingType = ClubDB.GetMembership( clubId, accountId.Value)?.MembershipType ?? ClubMembershipType.None; if (existingType == ClubMembershipType.Banned) return StatusCode(403); if (existingType >= ClubMembershipType.Member) { return Ok(new { success = true, alreadyMember = true, ClubMembershipType = (int)existingType }); } if (existingType == ClubMembershipType.PendingRequested) { return Ok(new { success = true, pending = true, ClubMembershipType = (int)existingType }); } if (existingType == ClubMembershipType.PendingInvited) { ClubDB.SetMembership(clubId, accountId.Value, ClubMembershipType.Member); return Ok(new { success = true, ClubMembershipType = (int)ClubMembershipType.Member }); } if (club.Visibility == ClubVisibility.Private || club.Joinability == ClubJoinability.InviteOnly) { return BadRequest(new { success = false, error = "invite_required" }); } ClubMembershipType newType = club.Joinability == ClubJoinability.AskToJoin ? ClubMembershipType.PendingRequested : ClubMembershipType.Member; if (!ClubDB.SetMembership(clubId, accountId.Value, newType)) return NotFound(); Console.WriteLine( $"[CLUB JOIN] club={clubId} account={accountId.Value} " + $"type={(int)newType} verb={Request.Method}"); return Ok(new { success = true, pending = newType == ClubMembershipType.PendingRequested, ClubMembershipType = (int)newType }); } [HttpPost("{clubId:long}/members/acceptinvite")] [HttpPut("{clubId:long}/members/acceptinvite")] public IActionResult AcceptInvite(long clubId) { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); var existing = ClubDB.GetMembership(clubId, accountId.Value); if (existing?.MembershipType != ClubMembershipType.PendingInvited) return BadRequest(new { success = false, error = "invite_not_found" }); ClubDB.SetMembership(clubId, accountId.Value, ClubMembershipType.Member); return Ok(new { success = true }); } [HttpPost("{clubId:long}/members/declineinvite")] [HttpPut("{clubId:long}/members/declineinvite")] public IActionResult DeclineInvite(long clubId) { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); ClubDB.RemoveMembership(clubId, accountId.Value); return Ok(new { success = true }); } [HttpPost("{clubId:long}/members/leave")] [HttpPut("{clubId:long}/members/leave")] public IActionResult LeaveClub(long clubId) { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); return ClubDB.RemoveMembership(clubId, accountId.Value) ? Ok(new { success = true }) : BadRequest(new { success = false, error = "creator_cannot_leave" }); } [HttpPost("{clubId:long}/members/invite")] [HttpPut("{clubId:long}/members/invite")] [RequestSizeLimit(16 * 1024)] public async Task InviteMember(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.Moderator)) return StatusCode(403); RequestValues values = await ReadRequestValuesAsync(); long? targetId = values.Long("accountId", "playerId", "id"); if (!targetId.HasValue || PlayerDB.Players.FindById(targetId.Value) == null) return BadRequest(new { success = false, error = "invalid_account" }); ClubMembershipType invitedType = ParseMembershipType( values.Int("membershipType", "clubMembershipType"), ClubMembershipType.Member); invitedType = invitedType is ClubMembershipType.Moderator or ClubMembershipType.CoOwner ? invitedType : ClubMembershipType.Member; ClubDB.SetMembership(clubId, targetId.Value, ClubMembershipType.PendingInvited); return Ok(new { success = true, AccountId = targetId.Value, InvitedMembershipType = (int)invitedType }); } [HttpGet("{clubId:long}/members/requests")] [HttpGet("{clubId:long}/members/requests/search")] public IActionResult GetJoinRequests(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.Moderator)) return StatusCode(403); var results = ClubDB.GetMemberships(clubId, includePending: true) .Where(value => value.MembershipType is ClubMembershipType.PendingRequested or ClubMembershipType.PendingInvited or ClubMembershipType.PendingDenied) .Select(membership => ClubDB.ToMembership(membership)) .ToList(); return Ok(new { Memberships = results, Results = results, TotalResults = results.Count, ContinuationToken = (string?)null }); } [HttpPost("{clubId:long}/members/acceptrequest")] [HttpPut("{clubId:long}/members/acceptrequest")] [RequestSizeLimit(16 * 1024)] public async Task AcceptJoinRequest(long clubId) => await ChangePendingMember(clubId, ClubMembershipType.Member); [HttpPost("{clubId:long}/members/denyrequest")] [HttpPut("{clubId:long}/members/denyrequest")] [RequestSizeLimit(16 * 1024)] public async Task DenyJoinRequest(long clubId) => await ChangePendingMember(clubId, ClubMembershipType.PendingDenied); [HttpPost("{clubId:long}/members/changetype")] [HttpPut("{clubId:long}/members/changetype")] [RequestSizeLimit(16 * 1024)] public async Task ChangeMembershipType(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.CoOwner)) return StatusCode(403); RequestValues values = await ReadRequestValuesAsync(); long? targetId = values.Long("accountId", "playerId", "id"); var type = ParseMembershipType( values.Int("newMembershipType", "membershipType", "clubMembershipType"), ClubMembershipType.Member); var club = ClubDB.Get(clubId); ClubMembershipType actorType = ClubDB.GetMembership( clubId, actorId.Value)?.MembershipType ?? ClubMembershipType.None; if (!targetId.HasValue || club == null || targetId.Value == club.CreatorAccountId || type < ClubMembershipType.Member || type == ClubMembershipType.Creator || (actorType != ClubMembershipType.Creator && type >= actorType)) return BadRequest(new { success = false, error = "invalid_membership_change" }); ClubDB.SetMembership(clubId, targetId.Value, type); return Ok(new { success = true }); } [HttpPost("{clubId:long}/members/remove")] [HttpPut("{clubId:long}/members/remove")] [RequestSizeLimit(16 * 1024)] public async Task RemoveMember(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.Moderator)) return StatusCode(403); RequestValues values = await ReadRequestValuesAsync(); long? targetId = values.Long("accountId", "playerId", "id"); ClubMembershipType actorType = ClubDB.GetMembership( clubId, actorId.Value)?.MembershipType ?? ClubMembershipType.None; ClubMembershipType targetType = targetId.HasValue ? ClubDB.GetMembership(clubId, targetId.Value)?.MembershipType ?? ClubMembershipType.None : ClubMembershipType.None; return targetId.HasValue && actorType > targetType && ClubDB.RemoveMembership(clubId, targetId.Value) ? Ok(new { success = true }) : BadRequest(new { success = false, error = "member_not_removed" }); } [HttpPost("{clubId:long}/members/ban")] [HttpPut("{clubId:long}/members/ban")] [RequestSizeLimit(16 * 1024)] public async Task BanMember(long clubId) => await ChangeMemberAsModerator(clubId, ClubMembershipType.Banned); [HttpPost("{clubId:long}/members/unban")] [HttpPut("{clubId:long}/members/unban")] [RequestSizeLimit(16 * 1024)] public async Task UnbanMember(long clubId) => await ChangeMemberAsModerator(clubId, ClubMembershipType.None); [HttpGet("{clubId:long}/members/banned")] public IActionResult GetBannedMembers(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.Moderator)) return StatusCode(403); var results = ClubDB.GetMemberships(clubId, includePending: true) .Where(value => value.MembershipType == ClubMembershipType.Banned) .Select(membership => ClubDB.ToMembership(membership)) .ToList(); return Ok(new { Memberships = results, Results = results, TotalResults = results.Count, ContinuationToken = (string?)null }); } [HttpPut("{clubId:long}/modify")] [HttpPost("{clubId:long}/modify")] [HttpPut("{clubId:long}/modifydetails")] [HttpPost("{clubId:long}/modifydetails")] [RequestSizeLimit(64 * 1024)] public async Task ModifyClub(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.CoOwner)) return StatusCode(403); var club = ClubDB.Get(clubId); if (club == null) return NotFound(); RequestValues values = await ReadRequestValuesAsync(); string? name = values.String("name", "clubName"); string? description = values.String("description", "clubDescription"); if (name != null) { name = name.Trim(); if (name.Length is < 3 or > 50 || name.Any(char.IsControl)) return BadRequest(new { success = false, error = "invalid_club_name" }); club.Name = name; } if (description != null) { description = description.Trim(); if (description.Length > 1_000 || description.Any(value => value == '\0')) return BadRequest(new { success = false, error = "invalid_club_description" }); club.Description = description; } club.Visibility = ParseEnum(values.Int("visibility"), club.Visibility); club.Joinability = ParseEnum(values.Int("joinability"), club.Joinability); club.ClubType = ParseEnum(values.Int("clubType"), club.ClubType); club.AllowJuniors = values.Bool(club.AllowJuniors, "allowJuniors", "supportsJuniors"); club.MinLevel = values.Int("minLevel", "minimumLevel") ?? club.MinLevel; List tags = ReadRequestedClubTags(values); if (tags.Count > 0) { club.PrimaryCategory = tags[0]; club.CategoryTags = tags; } string? imageName = values.String("mainImageName", "imageName", "image"); if (imageName != null) club.MainImageName = imageName; ClubDB.Update(club); Console.WriteLine( $"[CLUB MODIFY] club={clubId} actor={actorId.Value} " + $"primaryTag={club.PrimaryCategory} " + $"tags={string.Join(',', club.CategoryTags ?? new List())}"); return Ok(ClubDB.ToDetails(club, actorId.Value)); } [HttpPut("{clubId:long}/mainimage")] [HttpPost("{clubId:long}/mainimage")] [RequestSizeLimit(16 * 1024)] public async Task ModifyMainImage(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.CoOwner)) return StatusCode(403); var club = ClubDB.Get(clubId); if (club == null) return NotFound(); RequestValues values = await ReadRequestValuesAsync(); club.MainImageName = values.String("mainImageName", "imageName", "image") ?? "DefaultClubImage2k.jpg"; ClubDB.Update(club); return Ok(ClubDB.ToDetails(club, actorId.Value)); } [HttpPut("{clubId:long}/additionalimage/{imageIndex:int}")] [HttpPost("{clubId:long}/additionalimage/{imageIndex:int}")] [RequestSizeLimit(16 * 1024)] public async Task ModifyAdditionalImage(long clubId, int imageIndex) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.CoOwner)) return StatusCode(403); if (imageIndex is < 0 or > 2) return BadRequest(new { success = false, error = "invalid_image_index" }); var club = ClubDB.Get(clubId); if (club == null) return NotFound(); RequestValues values = await ReadRequestValuesAsync(); string imageName = values.String("imageName", "image")?.Trim() ?? string.Empty; while (club.AdditionalImages.Count <= imageIndex) club.AdditionalImages.Add(string.Empty); club.AdditionalImages[imageIndex] = imageName; ClubDB.Update(club); return Ok(ClubDB.ToDetails(club, actorId.Value)); } [HttpPut("{clubId:long}/clubhouse")] [HttpPost("{clubId:long}/clubhouse")] [RequestSizeLimit(16 * 1024)] public async Task ModifyClubhouse(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.CoOwner)) return StatusCode(403); var club = ClubDB.Get(clubId); if (club == null) return NotFound(); RequestValues values = await ReadRequestValuesAsync(); long? roomId = values.Long("clubhouseRoomId", "roomId", "id"); if (roomId.HasValue && RoomDB.GetRoom(roomId.Value) == null) return BadRequest(new { success = false, error = "room_not_found" }); club.ClubhouseRoomId = roomId is > 0 ? roomId : null; ClubDB.Update(club); return Ok(ClubDB.ToDetails(club, actorId.Value)); } [HttpGet("{clubId:long}/clubhouse")] public IActionResult GetClubhouse(long clubId) { var club = ClubDB.Get(clubId); if (club == null) return NotFound(); if (!club.ClubhouseRoomId.HasValue) return NotFound(new { error = "club_has_no_clubhouse" }); var room = RoomDB.GetRoom(club.ClubhouseRoomId.Value); return room == null ? NotFound() : Ok(RoomDB.PrepareRoomForClient(room)); } [HttpPut("{clubId:long}/minlevel")] [HttpPost("{clubId:long}/minlevel")] [RequestSizeLimit(16 * 1024)] public async Task ModifyMinLevel(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.CoOwner)) return StatusCode(403); var club = ClubDB.Get(clubId); if (club == null) return NotFound(); RequestValues values = await ReadRequestValuesAsync(); club.MinLevel = Math.Clamp(values.Int("minLevel", "level") ?? 0, 0, 50); ClubDB.Update(club); return Ok(ClubDB.ToDetails(club, actorId.Value)); } [HttpGet("{clubId:long}/clubChatEnabled")] public IActionResult GetClubChatEnabled(long clubId) { var club = ClubDB.Get(clubId); return club == null ? NotFound() : Ok(club.ClubChatEnabled); } [HttpGet("{clubId:long}/hasDisabledClubChat")] public IActionResult HasDisabledClubChat(long clubId) { var club = ClubDB.Get(clubId); return club == null ? NotFound() : Ok(!club.ClubChatEnabled); } [HttpPut("{clubId:long}/clubChatEnabled")] [HttpPost("{clubId:long}/clubChatEnabled")] [RequestSizeLimit(16 * 1024)] public async Task ModifyClubChatEnabled(long clubId) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.CoOwner)) return StatusCode(403); var club = ClubDB.Get(clubId); if (club == null) return NotFound(); RequestValues values = await ReadRequestValuesAsync(); club.ClubChatEnabled = values.Bool(true, "enabled", "clubChatEnabled", "value"); ClubDB.Update(club); return Ok(ClubDB.ToDetails(club, actorId.Value)); } [HttpGet("{clubId:long}/permissions/{membershipType:int}")] public IActionResult GetPermissions(long clubId, int membershipType) { if (ClubDB.Get(clubId) == null) return NotFound(); return Ok(ClubDB.Permissions( clubId, ParseMembershipType(membershipType, ClubMembershipType.None))); } [HttpDelete("{clubId:long}")] public IActionResult DeleteClub(long clubId) { long? accountId = AuthStuff.GetPlayerId(Request); if (!accountId.HasValue) return Unauthorized(); return ClubDB.Delete(clubId, accountId.Value) ? Ok(new { success = true }) : Forbid(); } private async Task ChangePendingMember( long clubId, ClubMembershipType membershipType) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.Moderator)) return StatusCode(403); RequestValues values = await ReadRequestValuesAsync(); long? targetId = values.Long("accountId", "playerId", "id"); if (!targetId.HasValue || ClubDB.GetMembership(clubId, targetId.Value)?.MembershipType != ClubMembershipType.PendingRequested) return BadRequest(new { success = false, error = "request_not_found" }); ClubDB.SetMembership(clubId, targetId.Value, membershipType); return Ok(new { success = true }); } private async Task ChangeMemberAsModerator( long clubId, ClubMembershipType membershipType) { long? actorId = AuthStuff.GetPlayerId(Request); if (!actorId.HasValue) return Unauthorized(); if (!HasPermission(clubId, actorId.Value, ClubMembershipType.Moderator)) return StatusCode(403); RequestValues values = await ReadRequestValuesAsync(); long? targetId = values.Long("accountId", "playerId", "id"); var club = ClubDB.Get(clubId); ClubMembershipType actorType = ClubDB.GetMembership( clubId, actorId.Value)?.MembershipType ?? ClubMembershipType.None; ClubMembershipType targetType = targetId.HasValue ? ClubDB.GetMembership(clubId, targetId.Value)?.MembershipType ?? ClubMembershipType.None : ClubMembershipType.None; if (!targetId.HasValue || club == null || targetId.Value == club.CreatorAccountId || actorType <= targetType) return BadRequest(new { success = false, error = "invalid_account" }); ClubDB.SetMembership(clubId, targetId.Value, membershipType); return Ok(new { success = true }); } private static bool HasPermission( long clubId, long accountId, ClubMembershipType required) => ClubDB.GetMembership(clubId, accountId)?.MembershipType >= required; private bool CanViewClub(ClubRecord club) { if (club.Visibility == ClubVisibility.Public) return true; long? accountId = AuthStuff.GetPlayerId(Request); ClubMembershipType membershipType = accountId.HasValue ? ClubDB.GetMembership(club.ClubId, accountId.Value)?.MembershipType ?? ClubMembershipType.None : ClubMembershipType.None; return membershipType > ClubMembershipType.None; } private static List ReadRequestedClubTags(RequestValues values) { string? primaryTag = values.String( "primaryTag", "primaryCategory", "category", "clubCategory"); List tags = values.List( "categoryTags", "customTags", "tags"); if (!string.IsNullOrWhiteSpace(primaryTag)) { tags.RemoveAll(value => string.Equals( value?.Trim(), primaryTag.Trim(), StringComparison.OrdinalIgnoreCase)); tags.Insert(0, primaryTag.Trim()); } return tags .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } private static T ParseEnum(int? value, T fallback) where T : struct, Enum => value.HasValue && Enum.IsDefined(typeof(T), value.Value) ? (T)Enum.ToObject(typeof(T), value.Value) : fallback; private static ClubMembershipType ParseMembershipType( int? value, ClubMembershipType fallback) => value.HasValue && Enum.IsDefined(typeof(ClubMembershipType), value.Value) ? (ClubMembershipType)value.Value : fallback; private async Task ReadRequestValuesAsync() { var values = new RequestValues(); foreach (var item in Request.Query) values.Add(item.Key, item.Value.ToString()); if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); foreach (var item in form) values.Add(item.Key, item.Value.ToString()); return values; } if ((Request.ContentLength ?? 0) <= 0) return values; try { using JsonDocument document = await JsonDocument.ParseAsync( Request.Body, new JsonDocumentOptions { AllowTrailingCommas = true, CommentHandling = JsonCommentHandling.Skip }); if (document.RootElement.ValueKind == JsonValueKind.Object) values.AddObject(document.RootElement); } catch (JsonException) { } return values; } private sealed class RequestValues { private readonly Dictionary _values = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary> _lists = new(StringComparer.OrdinalIgnoreCase); public void Add(string key, string? value) { if (!string.IsNullOrWhiteSpace(key) && value != null) _values[key] = value; } public void AddObject(JsonElement element) { foreach (JsonProperty property in element.EnumerateObject()) { if (property.Value.ValueKind == JsonValueKind.Array) { _lists[property.Name] = property.Value.EnumerateArray() .Where(value => value.ValueKind == JsonValueKind.String) .Select(value => value.GetString() ?? string.Empty) .Where(value => !string.IsNullOrWhiteSpace(value)) .ToList(); } else if (property.Value.ValueKind == JsonValueKind.Object && property.Name.Equals("club", StringComparison.OrdinalIgnoreCase)) { AddObject(property.Value); } else { Add(property.Name, property.Value.ValueKind == JsonValueKind.String ? property.Value.GetString() : property.Value.GetRawText()); } } } public string? String(params string[] keys) { foreach (string key in keys) if (_values.TryGetValue(key, out string? value)) return value; return null; } public int? Int(params string[] keys) => int.TryParse(String(keys), out int value) ? value : null; public long? Long(params string[] keys) => long.TryParse(String(keys), out long value) ? value : null; public bool Bool(bool fallback, params string[] keys) { string? value = String(keys); if (bool.TryParse(value, out bool parsed)) return parsed; if (int.TryParse(value, out int numeric)) return numeric != 0; return fallback; } public List List(params string[] keys) { foreach (string key in keys) { if (_lists.TryGetValue(key, out List? list)) return list; if (_values.TryGetValue(key, out string? value)) { return value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Where(item => !string.IsNullOrWhiteSpace(item)) .ToList(); } } return new List(); } } } }