Files
Radicade/Server/Clubs/ClubsAPI.cs
T
2026-10-16 23:33:13 -07:00

936 lines
39 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<string, object?>
{
["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<IActionResult> 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<object>(),
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> GetClubsForAccount(long accountId) => GetClubsCreatedByAccount(accountId);
[HttpGet("mostactivetoday"), Authorize]
[HttpGet("~/api/clubs/v1/mostactivetoday"), Authorize]
[HttpGet("~/api/clubs/v2/mostactivetoday"), Authorize]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<string, object?>
{
["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<IActionResult> 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<IActionResult> 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<string, object?>
{
["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<IActionResult> 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<string, object?>
{
["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<string> AccountIds, string? MembershipType)> ReadInviteMemberFieldsAsync()
{
var accountIds = new List<string>();
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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<JsonObject?> 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<string?> 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;
}
}