Radicade Public Code

This commit is contained in:
JogmansonReal
2026-10-16 23:33:13 -07:00
commit 55175acdf6
832 changed files with 363261 additions and 0 deletions
+935
View File
@@ -0,0 +1,935 @@
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;
}
}