mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-26 23:49:02 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Enums.Players;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
|
||||
namespace RadicadeServer.API
|
||||
{
|
||||
[Route("api/announcement")]
|
||||
public class AnnouncementAPI : Controller
|
||||
{
|
||||
RecDBContext db;
|
||||
public AnnouncementAPI(RecDBContext context)
|
||||
{
|
||||
this.db = context;
|
||||
}
|
||||
|
||||
[HttpGet("v1/get")]
|
||||
public List<Announcement>? GetAnnouncement()
|
||||
{
|
||||
var config = db.ServerConfig.FirstOrDefault(x => x.Key == "Announcement");
|
||||
if (config == null || string.IsNullOrWhiteSpace(config.Value) || config.Value == "[]")
|
||||
return new List<Announcement>();
|
||||
|
||||
try
|
||||
{
|
||||
var announcements = config.Value.FromJson<List<Announcement>>() ?? new List<Announcement>();
|
||||
|
||||
|
||||
|
||||
var raw = System.Text.Json.JsonDocument.Parse(config.Value);
|
||||
if (raw.RootElement.ValueKind == System.Text.Json.JsonValueKind.Array)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var keep = new List<Announcement>();
|
||||
int i = 0;
|
||||
foreach (var element in raw.RootElement.EnumerateArray())
|
||||
{
|
||||
var ann = i < announcements.Count ? announcements[i] : null;
|
||||
if (ann != null)
|
||||
{
|
||||
if (element.TryGetProperty("type", out var typeProp) && typeProp.ValueKind == System.Text.Json.JsonValueKind.String &&
|
||||
string.Equals(typeProp.GetString(), "Game Update", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
string? expiresAt = null;
|
||||
if (element.TryGetProperty("expires_at", out var exp) && exp.ValueKind == System.Text.Json.JsonValueKind.String)
|
||||
expiresAt = exp.GetString();
|
||||
|
||||
if (string.IsNullOrEmpty(expiresAt) || !DateTime.TryParse(expiresAt, out var expiry) || expiry > now)
|
||||
keep.Add(ann);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
announcements = keep;
|
||||
}
|
||||
|
||||
|
||||
announcements = announcements.OrderByDescending(x => x.CreatedAt).ToList();
|
||||
foreach (var ann in announcements)
|
||||
{
|
||||
ann.IsMostRecent = false;
|
||||
|
||||
|
||||
|
||||
ann.Platform = Platforms.All;
|
||||
}
|
||||
if (announcements.Count > 0)
|
||||
announcements[0].IsMostRecent = true;
|
||||
|
||||
return announcements;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<Announcement>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Config;
|
||||
using RadicadeServer.RecNet.Notifications;
|
||||
|
||||
namespace RadicadeServer.Clubs.Announcements
|
||||
{
|
||||
[Route("clubs/announcements/")]
|
||||
[Route("api/clubs/v1/announcements/")]
|
||||
[Route("api/clubs/v2/announcements/")]
|
||||
public class ClubAnnouncementsAPI : Controller
|
||||
{
|
||||
private const string StorageKey = "ClubAnnouncements";
|
||||
private static readonly ConcurrentDictionary<long, List<ClubAnnouncementRecord>> Store = new();
|
||||
|
||||
private sealed class ClubAnnouncementRecord
|
||||
{
|
||||
public long AnnouncementId { get; set; }
|
||||
public long ClubId { get; set; }
|
||||
public long AccountId { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Body { get; set; } = string.Empty;
|
||||
public string ImageName { get; set; } = string.Empty;
|
||||
public string Meta { get; set; } = string.Empty;
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
private readonly RecDBContext? db;
|
||||
|
||||
public ClubAnnouncementsAPI(RecDBContext? db = null)
|
||||
{
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
[HttpGet("v2/subscription/mine/unread")]
|
||||
[HttpGet("subscription/mine/unread")]
|
||||
public List<Announcement> GetUnreadSubscribedAnnouncements()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
[HttpGet("v2/mine/unread")]
|
||||
[HttpGet("mine/unread")]
|
||||
public List<Announcement> GetUnreadAnnouncements()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
[HttpPost("club/{clubId}")]
|
||||
[HttpPost("~/announcements/club/{clubId:long}")]
|
||||
public async Task<IActionResult> PostClubAnnouncements(long clubId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (managedClubId, error) = await ResolveManagedClubAsync(clubId, userId.Value);
|
||||
if (error != null) return error;
|
||||
clubId = managedClubId;
|
||||
|
||||
var fields = await ReadBodyFieldsAsync();
|
||||
|
||||
var announcement = new ClubAnnouncementRecord
|
||||
{
|
||||
AnnouncementId = NewAnnouncementId(),
|
||||
ClubId = clubId,
|
||||
AccountId = userId.Value,
|
||||
Title = GetField(fields, "title", "Title") ?? string.Empty,
|
||||
Body = GetField(fields, "body", "Body") ?? string.Empty,
|
||||
ImageName = GetField(fields, "imageName", "ImageName", "image_name") ?? string.Empty,
|
||||
Meta = GetField(fields, "meta", "Meta") ?? string.Empty,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
Add(clubId, announcement);
|
||||
await PersistAsync(clubId);
|
||||
|
||||
return Ok(new Dictionary<string, object?>
|
||||
{
|
||||
["error"] = string.Empty,
|
||||
["success"] = true,
|
||||
["value"] = announcement.AnnouncementId
|
||||
});
|
||||
}
|
||||
|
||||
[HttpGet("club/{clubId}")]
|
||||
[HttpGet("~/announcements/club/{clubId:long}")]
|
||||
public IActionResult GetClubAnnouncements(long clubId)
|
||||
{
|
||||
return Ok(BuildAnnouncementFeed(clubId));
|
||||
}
|
||||
|
||||
[HttpDelete("club/{clubId:long}/{announcementId:long}")]
|
||||
[HttpDelete("~/announcements/club/{clubId:long}/{announcementId:long}")]
|
||||
public async Task<IActionResult> DeleteClubAnnouncement(long clubId, long announcementId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var (managedClubId, error) = await ResolveManagedClubAsync(clubId, userId.Value);
|
||||
if (error != null) return error;
|
||||
clubId = managedClubId;
|
||||
|
||||
if (!Store.TryGetValue(clubId, out var announcements)) return NotFound();
|
||||
|
||||
bool removed;
|
||||
lock (announcements)
|
||||
removed = announcements.RemoveAll(item => item.AnnouncementId == announcementId) > 0;
|
||||
if (!removed) return NotFound();
|
||||
|
||||
await PersistAsync(clubId);
|
||||
return Ok(new { error = string.Empty, success = true, value = announcementId });
|
||||
}
|
||||
|
||||
private async Task<(long ClubId, IActionResult? Error)> ResolveManagedClubAsync(long clubId, long userId)
|
||||
{
|
||||
if (clubId == 0 || PlayerSubscriptions.IsPersonalClubId(clubId))
|
||||
{
|
||||
var personalClubId = PlayerSubscriptions.PersonalClubId(userId);
|
||||
if (clubId != 0 && clubId != personalClubId)
|
||||
return (clubId, StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }));
|
||||
|
||||
return (personalClubId, null);
|
||||
}
|
||||
|
||||
if (db == null) return (clubId, StatusCode(StatusCodes.Status500InternalServerError));
|
||||
|
||||
var club = await ClubStore.GetAsync(db, clubId);
|
||||
if (club == null) return (clubId, NotFound());
|
||||
if (ClubStore.GetMembershipType(club, userId) < ClubMembershipTypes.Coowner)
|
||||
return (clubId, StatusCode(StatusCodes.Status403Forbidden, new { error = "Insufficient permissions.", success = false, value = (object?)null }));
|
||||
|
||||
return (clubId, null);
|
||||
}
|
||||
|
||||
private void Add(long clubId, ClubAnnouncementRecord announcement)
|
||||
{
|
||||
var list = Store.GetOrAdd(clubId, _ => new List<ClubAnnouncementRecord>());
|
||||
lock (list) list.Insert(0, announcement);
|
||||
}
|
||||
|
||||
private static long NewAnnouncementId()
|
||||
{
|
||||
return Math.Abs((long)HashCode.Combine(Guid.NewGuid(), Environment.TickCount64));
|
||||
}
|
||||
|
||||
[HttpPost("club/{clubId}/announcement/{announcementId}/read")]
|
||||
[HttpPost("~/announcements/club/{clubId:long}/{announcementId:long}/read")]
|
||||
[HttpPost("~/clubs/announcements/club/{clubId:long}/{announcementId:long}/read")]
|
||||
[HttpPost("~/api/clubs/v1/announcements/club/{clubId:long}/{announcementId:long}/read")]
|
||||
[HttpPost("~/api/clubs/v2/announcements/club/{clubId:long}/{announcementId:long}/read")]
|
||||
public IActionResult MarkAnnouncementRead(long clubId, long announcementId)
|
||||
{
|
||||
if (User.GetUserId() == null) return Unauthorized();
|
||||
return Ok(new { error = string.Empty, success = true, value = announcementId });
|
||||
}
|
||||
|
||||
private async Task PersistAsync(long clubId)
|
||||
{
|
||||
if (db == null) return;
|
||||
try
|
||||
{
|
||||
var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == StorageKey);
|
||||
if (config == null)
|
||||
{
|
||||
config = new ServerConfigVal(StorageKey, "{}");
|
||||
await db.ServerConfig.AddAsync(config);
|
||||
}
|
||||
var snapshot = Store.ToDictionary(
|
||||
kv => kv.Key.ToString(),
|
||||
kv => SerializeAnnouncements(kv.Value));
|
||||
config.Value = JsonSerializer.Serialize(snapshot);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static List<object> SerializeAnnouncements(IReadOnlyList<ClubAnnouncementRecord> announcements)
|
||||
{
|
||||
return announcements.Select(announcement => (object)new Dictionary<string, object?>
|
||||
{
|
||||
["AnnouncementId"] = announcement.AnnouncementId,
|
||||
["ClubId"] = announcement.ClubId,
|
||||
["AccountId"] = announcement.AccountId,
|
||||
["Title"] = announcement.Title,
|
||||
["Body"] = announcement.Body,
|
||||
["ImageName"] = announcement.ImageName,
|
||||
["Meta"] = announcement.Meta,
|
||||
["CreatedAt"] = announcement.CreatedAt
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static object BuildAnnouncementFeed(long clubId)
|
||||
{
|
||||
var announcements = Store.TryGetValue(clubId, out var list)
|
||||
? SerializeAnnouncements(list)
|
||||
: new List<object>();
|
||||
var lastAnnouncementId = announcements.Count > 0
|
||||
? (long?)((Dictionary<string, object?>)announcements[0])["AnnouncementId"]
|
||||
: null;
|
||||
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["error"] = string.Empty,
|
||||
["success"] = true,
|
||||
["value"] = new Dictionary<string, object?>
|
||||
{
|
||||
["Announcements"] = announcements,
|
||||
["ClubId"] = clubId,
|
||||
["LastAnnouncementId"] = lastAnnouncementId,
|
||||
["LastReadAnnouncementId"] = 0L
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static string? GetField(Dictionary<string, string> fields, params string[] names)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (fields.TryGetValue(name, out var value) && !string.IsNullOrWhiteSpace(value))
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, string>> ReadBodyFieldsAsync()
|
||||
{
|
||||
var fields = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var kv in form)
|
||||
fields[kv.Key] = kv.Value.ToString();
|
||||
return fields;
|
||||
}
|
||||
|
||||
if (Request.ContentLength is not > 0) return fields;
|
||||
|
||||
try
|
||||
{
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
if (string.IsNullOrWhiteSpace(body)) return fields;
|
||||
|
||||
var trimmed = body.TrimStart();
|
||||
if (trimmed.StartsWith("{"))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(body);
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var prop in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
fields[prop.Name] = prop.Value.ValueKind == JsonValueKind.String
|
||||
? prop.Value.GetString() ?? ""
|
||||
: prop.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var pair in trimmed.Split('&', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var kv = pair.Split('=', 2);
|
||||
if (kv.Length < 2) continue;
|
||||
fields[Uri.UnescapeDataString(kv[0])] = Uri.UnescapeDataString(kv[1].Replace('+', ' '));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,782 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Config;
|
||||
using RadicadeServer.RecNet.Auth;
|
||||
using RadicadeServer.RecNet.CDN;
|
||||
using RadicadeServer.RecNet.Enums;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace RadicadeServer.Clubs;
|
||||
|
||||
public sealed class ClubRecord
|
||||
{
|
||||
public long clubId { get; set; }
|
||||
public string name { get; set; } = "Radicade Club";
|
||||
public string description { get; set; } = "";
|
||||
public string primaryTag { get; set; } = "Social";
|
||||
public string imageName { get; set; } = "default";
|
||||
public string bannerImageName { get; set; } = "default";
|
||||
public long ownerPlayerId { get; set; }
|
||||
public DateTime createdAt { get; set; } = DateTime.UtcNow;
|
||||
public long? clubhouseRoomId { get; set; }
|
||||
public int visibility { get; set; } = 1;
|
||||
public int joinability { get; set; } = 0;
|
||||
public bool allowJuniors { get; set; } = true;
|
||||
public int minLevel { get; set; }
|
||||
public string[] customTags { get; set; } = Array.Empty<string>();
|
||||
public string[] additionalImages { get; set; } = Array.Empty<string>();
|
||||
public List<ClubMemberRecord> members { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class ClubMemberRecord
|
||||
{
|
||||
public long accountId { get; set; }
|
||||
public int membershipType { get; set; }
|
||||
public int pendingMembershipType { get; set; }
|
||||
public long inviterAccountId { get; set; }
|
||||
public DateTime createdAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public static class ClubMembershipTypes
|
||||
{
|
||||
public const int Banned = -1;
|
||||
public const int None = 0;
|
||||
public const int PendingRequested = 1;
|
||||
public const int PendingInvited = 2;
|
||||
public const int PendingDenied = 3;
|
||||
public const int Member = 10;
|
||||
public const int Moderator = 20;
|
||||
public const int Coowner = 30;
|
||||
public const int Creator = 100;
|
||||
}
|
||||
|
||||
public sealed record ClubMemberView(long AccountId, int MembershipType, long InviterAccountId, DateTime CreatedAt);
|
||||
|
||||
public static class ClubStore
|
||||
{
|
||||
private const string ConfigKey = "Clubs";
|
||||
|
||||
public const string DefaultImageName = "DefaultImgPurple";
|
||||
private const int ActiveMemberThreshold = ClubMembershipTypes.Member;
|
||||
|
||||
private static readonly string[] CategoryQueryKeys =
|
||||
{
|
||||
"category", "Category", "categoryTag", "CategoryTag", "categoryName", "CategoryName",
|
||||
"primaryTag", "PrimaryTag", "tag", "Tag", "tags", "Tags"
|
||||
};
|
||||
|
||||
public static string? CategoryFilter(IQueryCollection query)
|
||||
{
|
||||
foreach (var key in CategoryQueryKeys)
|
||||
{
|
||||
var value = query[key].FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(value)) return value.Trim().TrimStart('#');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool MatchesCategory(ClubRecord? club, string? category)
|
||||
{
|
||||
if (club == null) return false;
|
||||
|
||||
var wanted = category?.Trim().TrimStart('#');
|
||||
if (string.IsNullOrWhiteSpace(wanted)) return true;
|
||||
|
||||
return string.Equals(club.primaryTag?.Trim(), wanted, StringComparison.OrdinalIgnoreCase)
|
||||
|| (club.customTags ?? Array.Empty<string>())
|
||||
.Any(tag => string.Equals(tag?.Trim(), wanted, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public static List<ClubRecord> FilterByCategory(List<ClubRecord> clubs, string? category)
|
||||
=> string.IsNullOrWhiteSpace(category)
|
||||
? clubs
|
||||
: clubs.Where(club => MatchesCategory(club, category)).ToList();
|
||||
|
||||
public static async Task<List<ClubRecord>> LoadAsync(RecDBContext db)
|
||||
{
|
||||
var config = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == ConfigKey);
|
||||
if (config == null || string.IsNullOrWhiteSpace(config.Value)) return new List<ClubRecord>();
|
||||
|
||||
try
|
||||
{
|
||||
var clubs = config.Value.FromJson<List<ClubRecord>>() ?? new List<ClubRecord>();
|
||||
foreach (var club in clubs)
|
||||
{
|
||||
club.members ??= new List<ClubMemberRecord>();
|
||||
club.customTags ??= Array.Empty<string>();
|
||||
club.additionalImages ??= Array.Empty<string>();
|
||||
}
|
||||
return clubs;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<ClubRecord>();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string[] HomeClubConfigPrefixes = ["ClubHome:", "ClubHome.", "HomeClub:"];
|
||||
|
||||
private static string HomeClubConfigKey(long accountId) => $"ClubHome:{accountId}";
|
||||
|
||||
private static async Task<long?> GetHomeClubIdAsync(RecDBContext db, long accountId)
|
||||
{
|
||||
var keys = HomeClubConfigPrefixes.Select(prefix => prefix + accountId.ToString(System.Globalization.CultureInfo.InvariantCulture)).ToArray();
|
||||
var value = await db.ServerConfig.AsNoTracking()
|
||||
.Where(config => keys.Contains(config.Key))
|
||||
.Select(config => config.Value)
|
||||
.FirstOrDefaultAsync();
|
||||
return long.TryParse(value, out var clubId) && clubId > 0 ? clubId : null;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> GetHomeClubAsync(RecDBContext db, long accountId)
|
||||
{
|
||||
var clubId = await GetHomeClubIdAsync(db, accountId);
|
||||
if (!clubId.HasValue) return null;
|
||||
var club = await GetAsync(db, clubId.Value);
|
||||
return club?.clubhouseRoomId is > 0 ? club : null;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> SetHomeClubAsync(RecDBContext db, long accountId, long? clubId)
|
||||
{
|
||||
var configKey = HomeClubConfigKey(accountId);
|
||||
var config = await db.ServerConfig.FirstOrDefaultAsync(row => row.Key == configKey);
|
||||
if (clubId is > 0)
|
||||
{
|
||||
if (config == null)
|
||||
{
|
||||
config = new ServerConfigVal(configKey, clubId.Value.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
await db.ServerConfig.AddAsync(config);
|
||||
}
|
||||
else
|
||||
{
|
||||
config.Value = clubId.Value.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
else if (config != null)
|
||||
{
|
||||
db.ServerConfig.Remove(config);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return clubId is > 0 ? await GetAsync(db, clubId.Value) : null;
|
||||
}
|
||||
|
||||
public static async Task ClearHomeClubsAsync(RecDBContext db, long clubId)
|
||||
{
|
||||
var clubIdValue = clubId.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
var matching = await db.ServerConfig.Where(config => config.Value == clubIdValue).ToListAsync();
|
||||
matching.RemoveAll(config => !HomeClubConfigPrefixes.Any(prefix => config.Key.StartsWith(prefix, StringComparison.Ordinal)));
|
||||
if (matching.Count == 0) return;
|
||||
db.ServerConfig.RemoveRange(matching);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public static async Task<bool> DeleteAsync(RecDBContext db, long clubId)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
if (clubs.RemoveAll(club => club.clubId == clubId) == 0) return false;
|
||||
await SaveAsync(db, clubs);
|
||||
await ClearHomeClubsAsync(db, clubId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> UpdateAsync(RecDBContext db, long clubId, JsonObject body)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var club = clubs.FirstOrDefault(item => item.clubId == clubId);
|
||||
if (club == null) return null;
|
||||
|
||||
if (GetNonEmptyString(body, "name") is { } name) club.name = name.Trim();
|
||||
if (GetNonEmptyString(body, "description") is { } description) club.description = description;
|
||||
if (GetNonEmptyString(body, "category", "primaryTag") is { } category) club.primaryTag = category.Trim();
|
||||
if (GetNonEmptyString(body, "mainImageName") is { } imageName) club.imageName = imageName.Trim();
|
||||
if (GetNonEmptyString(body, "bannerImageName") is { } bannerName) club.bannerImageName = bannerName.Trim();
|
||||
if (GetInt(body, "visibility") is { } visibility) club.visibility = visibility;
|
||||
if (GetInt(body, "joinability") is { } joinability) club.joinability = joinability;
|
||||
if (GetInt(body, "minLevel") is { } minLevel && minLevel >= 0) club.minLevel = minLevel;
|
||||
if (GetBool(body, "allowJuniors") is { } allowJuniors) club.allowJuniors = allowJuniors;
|
||||
if (GetStringArray(body, "customTags") is { } customTags) club.customTags = NormalizeTags(customTags);
|
||||
|
||||
await SaveAsync(db, clubs);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> SetClubhouseAsync(RecDBContext db, long clubId, long? roomId)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var club = clubs.FirstOrDefault(item => item.clubId == clubId);
|
||||
if (club == null) return null;
|
||||
club.clubhouseRoomId = roomId;
|
||||
await SaveAsync(db, clubs);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> SetAdditionalImageAsync(RecDBContext db, long clubId, int index, string imageName)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var club = clubs.FirstOrDefault(item => item.clubId == clubId);
|
||||
if (club == null) return null;
|
||||
|
||||
var images = (club.additionalImages ?? Array.Empty<string>()).ToList();
|
||||
if (string.IsNullOrWhiteSpace(imageName))
|
||||
{
|
||||
if (index < images.Count) images.RemoveAt(index);
|
||||
}
|
||||
else if (index < images.Count)
|
||||
{
|
||||
images[index] = imageName.Trim();
|
||||
}
|
||||
else if (images.Count < 3)
|
||||
{
|
||||
images.Add(imageName.Trim());
|
||||
}
|
||||
club.additionalImages = images.ToArray();
|
||||
await SaveAsync(db, clubs);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> InviteMemberAsync(RecDBContext db, long clubId, long accountId, int membershipType, long inviterAccountId)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var club = clubs.FirstOrDefault(item => item.clubId == clubId);
|
||||
if (club == null || club.ownerPlayerId == accountId) return null;
|
||||
|
||||
club.members ??= new List<ClubMemberRecord>();
|
||||
var membership = club.members.FirstOrDefault(item => item.accountId == accountId);
|
||||
if (membership == null)
|
||||
{
|
||||
membership = new ClubMemberRecord { accountId = accountId, createdAt = DateTime.UtcNow };
|
||||
club.members.Add(membership);
|
||||
}
|
||||
|
||||
membership.membershipType = ClubMembershipTypes.PendingInvited;
|
||||
membership.pendingMembershipType = membershipType;
|
||||
membership.inviterAccountId = inviterAccountId;
|
||||
await SaveAsync(db, clubs);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> AcceptInvitationAsync(RecDBContext db, long clubId, long accountId)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var club = clubs.FirstOrDefault(item => item.clubId == clubId);
|
||||
var membership = club?.members?.FirstOrDefault(item => item.accountId == accountId);
|
||||
if (club == null || membership?.membershipType != ClubMembershipTypes.PendingInvited) return null;
|
||||
|
||||
var grantedType = membership.pendingMembershipType is ClubMembershipTypes.Member or ClubMembershipTypes.Moderator or ClubMembershipTypes.Coowner
|
||||
? membership.pendingMembershipType
|
||||
: ClubMembershipTypes.Member;
|
||||
membership.membershipType = grantedType;
|
||||
membership.pendingMembershipType = 0;
|
||||
membership.createdAt = DateTime.UtcNow;
|
||||
await SaveAsync(db, clubs);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> RequestToJoinAsync(RecDBContext db, long clubId, long accountId)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var club = clubs.FirstOrDefault(item => item.clubId == clubId);
|
||||
if (club == null) return null;
|
||||
var current = GetMembershipType(club, accountId);
|
||||
if (current == ClubMembershipTypes.Banned || current >= ClubMembershipTypes.Member || current == ClubMembershipTypes.PendingRequested)
|
||||
return club;
|
||||
if (club.joinability == 1) return club;
|
||||
return await SetMemberTypeAsync(db, clubId, accountId,
|
||||
club.joinability == 0 ? ClubMembershipTypes.Member : ClubMembershipTypes.PendingRequested,
|
||||
accountId);
|
||||
}
|
||||
|
||||
private static string? GetString(JsonObject body, params string[] keys)
|
||||
{
|
||||
foreach (var pair in body)
|
||||
{
|
||||
if (!keys.Any(key => pair.Key.Equals(key, StringComparison.OrdinalIgnoreCase)) || pair.Value == null) continue;
|
||||
if (pair.Value is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var value)) return value;
|
||||
return pair.Value.ToString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetNonEmptyString(JsonObject body, params string[] keys)
|
||||
{
|
||||
var value = GetString(body, keys);
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value;
|
||||
}
|
||||
|
||||
private static int? GetInt(JsonObject body, params string[] keys)
|
||||
{
|
||||
var raw = GetString(body, keys)?.Trim();
|
||||
if (int.TryParse(raw, out var value)) return value;
|
||||
return raw?.ToLowerInvariant() switch
|
||||
{
|
||||
"private" => 0,
|
||||
"public" => 1,
|
||||
"open" => 0,
|
||||
"inviteonly" or "invite_only" => 1,
|
||||
"asktojoin" or "requesttojoin" or "ask_to_join" => 2,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static bool? GetBool(JsonObject body, params string[] keys)
|
||||
{
|
||||
var raw = GetString(body, keys)?.Trim();
|
||||
if (bool.TryParse(raw, out var value)) return value;
|
||||
return raw?.ToLowerInvariant() switch
|
||||
{
|
||||
"1" or "yes" => true,
|
||||
"0" or "no" => false,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static string[]? GetStringArray(JsonObject body, params string[] keys)
|
||||
{
|
||||
foreach (var pair in body)
|
||||
{
|
||||
if (!keys.Any(key => pair.Key.Equals(key, StringComparison.OrdinalIgnoreCase))) continue;
|
||||
if (pair.Value is JsonArray array)
|
||||
return NormalizeTags(array.Select(item => item?.ToString() ?? string.Empty));
|
||||
return NormalizeTags(new[] { pair.Value?.ToString() ?? string.Empty });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string[] NormalizeTags(IEnumerable<string> tags)
|
||||
{
|
||||
var output = new List<string>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var raw in tags)
|
||||
{
|
||||
var tag = raw.Trim();
|
||||
if (tag.Length > 0 && seen.Add(tag)) output.Add(tag);
|
||||
}
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
public static async Task SaveAsync(RecDBContext db, List<ClubRecord> clubs)
|
||||
{
|
||||
var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == ConfigKey);
|
||||
if (config == null)
|
||||
{
|
||||
config = new ServerConfigVal(ConfigKey, "[]");
|
||||
await db.ServerConfig.AddAsync(config);
|
||||
}
|
||||
|
||||
config.Value = clubs.ToJson();
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> GetAsync(RecDBContext db, long clubId)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
return clubs.FirstOrDefault(x => x.clubId == clubId);
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord> CreateAsync(
|
||||
RecDBContext db,
|
||||
long ownerPlayerId,
|
||||
string? name,
|
||||
string? description,
|
||||
string? primaryTag = null,
|
||||
int visibility = 1,
|
||||
int joinability = 0,
|
||||
bool allowJuniors = true,
|
||||
int minLevel = 0,
|
||||
string? mainImageName = null)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var clubName = string.IsNullOrWhiteSpace(name) ? "Radicade Club" : name.Trim();
|
||||
var tag = string.IsNullOrWhiteSpace(primaryTag) ? "Social" : primaryTag.Trim();
|
||||
var now = DateTime.UtcNow;
|
||||
var clubId = Math.Abs((long)HashCode.Combine(ownerPlayerId, clubName, now.Ticks));
|
||||
|
||||
while (clubId == 0 || clubs.Any(x => x.clubId == clubId))
|
||||
{
|
||||
clubId = Math.Abs((long)HashCode.Combine(ownerPlayerId, clubName, Guid.NewGuid()));
|
||||
}
|
||||
|
||||
var club = new ClubRecord
|
||||
{
|
||||
clubId = clubId,
|
||||
name = clubName,
|
||||
description = description?.Trim() ?? "",
|
||||
primaryTag = tag,
|
||||
imageName = string.IsNullOrWhiteSpace(mainImageName) ? DefaultImageName : mainImageName.Trim(),
|
||||
visibility = visibility,
|
||||
joinability = joinability,
|
||||
allowJuniors = allowJuniors,
|
||||
minLevel = minLevel,
|
||||
ownerPlayerId = ownerPlayerId,
|
||||
createdAt = now
|
||||
};
|
||||
club.members.Add(new ClubMemberRecord
|
||||
{
|
||||
accountId = ownerPlayerId,
|
||||
membershipType = ClubMembershipTypes.Creator,
|
||||
inviterAccountId = ownerPlayerId,
|
||||
createdAt = now
|
||||
});
|
||||
|
||||
clubs.Add(club);
|
||||
await SaveAsync(db, clubs);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static int GetMembershipType(ClubRecord? club, long accountId)
|
||||
{
|
||||
if (club == null || club.clubId <= 0 || accountId <= 0) return ClubMembershipTypes.None;
|
||||
if (club.ownerPlayerId == accountId) return ClubMembershipTypes.Creator;
|
||||
return club.members?.FirstOrDefault(x => x.accountId == accountId)?.membershipType ?? ClubMembershipTypes.None;
|
||||
}
|
||||
|
||||
public static int GetMemberCount(ClubRecord? club)
|
||||
{
|
||||
if (club == null || club.clubId <= 0) return 0;
|
||||
return 1 + (club.members ?? new List<ClubMemberRecord>())
|
||||
.Where(x => x.accountId != club.ownerPlayerId && x.membershipType >= ActiveMemberThreshold)
|
||||
.Select(x => x.accountId)
|
||||
.Distinct()
|
||||
.Count();
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ClubMemberView> GetMembers(ClubRecord? club)
|
||||
{
|
||||
if (club == null || club.clubId <= 0) return Array.Empty<ClubMemberView>();
|
||||
|
||||
var members = new List<ClubMemberView>
|
||||
{
|
||||
new(club.ownerPlayerId, ClubMembershipTypes.Creator, club.ownerPlayerId, club.createdAt)
|
||||
};
|
||||
members.AddRange((club.members ?? new List<ClubMemberRecord>())
|
||||
.Where(x => x.accountId != club.ownerPlayerId)
|
||||
.GroupBy(x => x.accountId)
|
||||
.Select(group => group.Last())
|
||||
.Select(x => new ClubMemberView(
|
||||
x.accountId,
|
||||
x.membershipType,
|
||||
x.inviterAccountId,
|
||||
NormalizeMembershipDate(x.createdAt, club.createdAt))));
|
||||
return members;
|
||||
}
|
||||
|
||||
private static DateTime NormalizeMembershipDate(DateTime memberCreatedAt, DateTime clubCreatedAt)
|
||||
{
|
||||
if (memberCreatedAt > DateTime.MinValue) return memberCreatedAt;
|
||||
if (clubCreatedAt > DateTime.MinValue) return clubCreatedAt;
|
||||
return DateTime.UnixEpoch;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> SetMemberTypeAsync(
|
||||
RecDBContext db,
|
||||
long clubId,
|
||||
long accountId,
|
||||
int membershipType,
|
||||
long inviterAccountId)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var club = clubs.FirstOrDefault(x => x.clubId == clubId);
|
||||
if (club == null || club.ownerPlayerId == accountId) return null;
|
||||
|
||||
club.members ??= new List<ClubMemberRecord>();
|
||||
var membership = club.members.FirstOrDefault(x => x.accountId == accountId);
|
||||
if (membership == null)
|
||||
{
|
||||
club.members.Add(new ClubMemberRecord
|
||||
{
|
||||
accountId = accountId,
|
||||
membershipType = membershipType,
|
||||
inviterAccountId = inviterAccountId,
|
||||
createdAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
if (membership.membershipType < ClubMembershipTypes.Member && membershipType >= ClubMembershipTypes.Member)
|
||||
membership.createdAt = DateTime.UtcNow;
|
||||
membership.membershipType = membershipType;
|
||||
membership.inviterAccountId = inviterAccountId;
|
||||
}
|
||||
|
||||
await SaveAsync(db, clubs);
|
||||
return club;
|
||||
}
|
||||
|
||||
public static async Task<ClubRecord?> RemoveMemberAsync(RecDBContext db, long clubId, long accountId)
|
||||
{
|
||||
var clubs = await LoadAsync(db);
|
||||
var club = clubs.FirstOrDefault(x => x.clubId == clubId);
|
||||
if (club == null || club.ownerPlayerId == accountId) return null;
|
||||
|
||||
club.members ??= new List<ClubMemberRecord>();
|
||||
club.members.RemoveAll(member => member.accountId == accountId && member.membershipType != ClubMembershipTypes.Banned);
|
||||
await SaveAsync(db, clubs);
|
||||
return club;
|
||||
}
|
||||
|
||||
private static long ToUnix(DateTime value) => new DateTimeOffset(value).ToUnixTimeSeconds();
|
||||
|
||||
public static object BuildClub(ClubRecord? club, long viewerId = 0)
|
||||
{
|
||||
club ??= new ClubRecord { clubId = 0 };
|
||||
var memberCount = GetMemberCount(club);
|
||||
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["ClubId"] = club.clubId,
|
||||
["Name"] = club.name,
|
||||
["Description"] = club.description,
|
||||
["MainImageName"] = club.imageName,
|
||||
["State"] = 0,
|
||||
["CreatorAccountId"] = club.ownerPlayerId,
|
||||
["Category"] = club.primaryTag,
|
||||
["Visibility"] = club.visibility,
|
||||
["Joinability"] = club.joinability,
|
||||
["AllowJuniors"] = club.allowJuniors,
|
||||
["MinLevel"] = club.minLevel,
|
||||
["MemberCount"] = memberCount,
|
||||
["IsRRO"] = false,
|
||||
["ClubhouseRoomId"] = club.clubhouseRoomId,
|
||||
["ClubType"] = 0
|
||||
};
|
||||
}
|
||||
|
||||
public static async Task<object> BuildClubDetailsAsync(RecDBContext db, ClubRecord? club, long viewerId = 0)
|
||||
{
|
||||
club ??= new ClubRecord { clubId = 0 };
|
||||
var details = (Dictionary<string, object?>)BuildClubDetails(club, viewerId);
|
||||
details["AdditionalImages"] = await GetAdditionalImagesAsync(db, club.additionalImages ?? Array.Empty<string>());
|
||||
return details;
|
||||
}
|
||||
|
||||
public static object BuildClubDetails(ClubRecord? club, long viewerId = 0)
|
||||
{
|
||||
club ??= new ClubRecord { clubId = 0 };
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["Club"] = BuildClub(club, viewerId),
|
||||
["ClubId"] = club.clubId,
|
||||
["CustomTags"] = club.customTags ?? Array.Empty<string>(),
|
||||
["AdditionalImages"] = Array.Empty<object>(),
|
||||
["CoownerPermissions"] = BuildPermissions(club.clubId, 0),
|
||||
["ModeratorPermissions"] = BuildPermissions(club.clubId, 1),
|
||||
["MemberPermissions"] = BuildPermissions(club.clubId, 2),
|
||||
["MyMembershipType"] = GetMembershipType(club, viewerId)
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<object[]> GetAdditionalImagesAsync(RecDBContext db, string[] imageNames)
|
||||
{
|
||||
if (imageNames.Length == 0) return Array.Empty<object>();
|
||||
var records = await db.Images.AsNoTracking()
|
||||
.Where(image => imageNames.Contains(image.Name))
|
||||
.ToDictionaryAsync(image => image.Name);
|
||||
var imageIds = records.Values.Select(image => image.Id).ToList();
|
||||
var cheers = await db.ImageInteractions.AsNoTracking()
|
||||
.Where(interaction => interaction.IsCheered && imageIds.Contains(interaction.SavedImageId))
|
||||
.GroupBy(interaction => interaction.SavedImageId)
|
||||
.Select(group => new { ImageId = group.Key, Count = group.Count() })
|
||||
.ToDictionaryAsync(group => group.ImageId, group => group.Count);
|
||||
|
||||
return imageNames.Select(name =>
|
||||
{
|
||||
if (!records.TryGetValue(name, out var image))
|
||||
return (object)new Dictionary<string, object?>
|
||||
{
|
||||
["Id"] = 0L, ["Type"] = 1,
|
||||
["Accessibility"] = (int)SavedImageAccessibility.Public,
|
||||
["AccessibilityLocked"] = false, ["ImageName"] = name,
|
||||
["Description"] = null, ["PlayerId"] = 0L,
|
||||
["TaggedPlayerIds"] = Array.Empty<long>(), ["RoomId"] = null,
|
||||
["PlayerEventId"] = null, ["CreatedAt"] = DateTime.UnixEpoch,
|
||||
["CheerCount"] = 0, ["CommentCount"] = 0
|
||||
};
|
||||
return (object)new Dictionary<string, object?>
|
||||
{
|
||||
["Id"] = image.Id, ["Type"] = (int)image.ImageType,
|
||||
["Accessibility"] = (int)image.Accessibility, ["AccessibilityLocked"] = false,
|
||||
["ImageName"] = image.Name, ["Description"] = null, ["PlayerId"] = image.OwnerId,
|
||||
["TaggedPlayerIds"] = ParseImagePlayerIds(image.PlayerIds), ["RoomId"] = image.RoomId,
|
||||
["PlayerEventId"] = image.EventId, ["CreatedAt"] = image.CreatedAt,
|
||||
["CheerCount"] = cheers.GetValueOrDefault(image.Id), ["CommentCount"] = 0
|
||||
};
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private static long[] ParseImagePlayerIds(string? playerIds)
|
||||
{
|
||||
try { return JsonSerializer.Deserialize<long[]>(playerIds ?? "[]") ?? Array.Empty<long>(); }
|
||||
catch { return Array.Empty<long>(); }
|
||||
}
|
||||
|
||||
public static object BuildPermissions(long clubId, int type)
|
||||
{
|
||||
var isCoowner = type == 0;
|
||||
var isModerator = type == 1;
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["Type"] = type switch
|
||||
{
|
||||
0 => ClubMembershipTypes.Coowner,
|
||||
1 => ClubMembershipTypes.Moderator,
|
||||
_ => ClubMembershipTypes.Member
|
||||
},
|
||||
["ClubId"] = clubId,
|
||||
["ApproveMember"] = isCoowner || isModerator,
|
||||
["BanUnban"] = isCoowner || isModerator,
|
||||
["CreateEvent"] = isCoowner,
|
||||
["EditDetails"] = isCoowner,
|
||||
["EditPermissionSettings"] = isCoowner,
|
||||
["PostAnnouncement"] = isCoowner
|
||||
};
|
||||
}
|
||||
|
||||
public static object BuildMemberListItem(long playerId, int role, bool isOwner, long clubId = 0, long created = 0)
|
||||
{
|
||||
var type = isOwner ? ClubMembershipTypes.Creator : role;
|
||||
var createdAt = created > 0 ? DateTimeOffset.FromUnixTimeSeconds(created).UtcDateTime.ToString("O") : null;
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["ClubMemberId"] = playerId,
|
||||
["ClubId"] = clubId,
|
||||
["AccountId"] = playerId,
|
||||
["MembershipType"] = type,
|
||||
["CreatedAt"] = createdAt
|
||||
};
|
||||
}
|
||||
|
||||
public static object BuildMemberListItem(ClubMemberView member, long clubId) =>
|
||||
BuildMemberListItem(
|
||||
member.AccountId,
|
||||
member.MembershipType,
|
||||
member.MembershipType == ClubMembershipTypes.Creator,
|
||||
clubId,
|
||||
ToUnix(member.CreatedAt));
|
||||
|
||||
public static object BuildMember(long playerId, int role, bool isOwner, long clubId = 0, long created = 0) => new Dictionary<string, object?>
|
||||
{
|
||||
["AccountId"] = playerId,
|
||||
["ClubId"] = clubId,
|
||||
["CreatedAt"] = created > 0 ? DateTimeOffset.FromUnixTimeSeconds(created).UtcDateTime.ToString("O") : null,
|
||||
["MembershipType"] = isOwner ? ClubMembershipTypes.Creator : role,
|
||||
["membershipRequestId"] = 0L,
|
||||
["inviterAccountId"] = 0L,
|
||||
["subscriptionId"] = clubId,
|
||||
["playerId"] = playerId,
|
||||
["username"] = string.Empty,
|
||||
["displayName"] = string.Empty,
|
||||
["level"] = 0,
|
||||
["role"] = isOwner ? ClubMembershipTypes.Creator : role,
|
||||
["membershipStatus"] = role >= ActiveMemberThreshold || isOwner ? 1 : 0,
|
||||
["isOwner"] = isOwner,
|
||||
["created"] = created,
|
||||
["createdAt"] = created
|
||||
};
|
||||
|
||||
public static object BuildMember(ClubMemberView membership, ClubRecord? club, Profile? profile)
|
||||
{
|
||||
var role = membership.MembershipType;
|
||||
var created = ToUnix(membership.CreatedAt);
|
||||
var isOwner = role == ClubMembershipTypes.Creator;
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["AccountId"] = membership.AccountId,
|
||||
["ClubId"] = club?.clubId ?? 0L,
|
||||
["CreatedAt"] = membership.CreatedAt.ToString("O"),
|
||||
["MembershipType"] = role,
|
||||
["membershipRequestId"] = 0L,
|
||||
["inviterAccountId"] = membership.InviterAccountId,
|
||||
["subscriptionId"] = club?.clubId ?? 0L,
|
||||
["playerId"] = membership.AccountId,
|
||||
["username"] = profile?.username ?? string.Empty,
|
||||
["displayName"] = profile?.displayName ?? string.Empty,
|
||||
["level"] = 0,
|
||||
["role"] = role,
|
||||
["membershipStatus"] = role >= ActiveMemberThreshold || isOwner ? 1 : 0,
|
||||
["isOwner"] = isOwner,
|
||||
["created"] = created,
|
||||
["createdAt"] = created
|
||||
};
|
||||
}
|
||||
|
||||
public static object BuildMembership(ClubRecord? club, long viewerId = 0)
|
||||
{
|
||||
club ??= new ClubRecord { clubId = 0 };
|
||||
var role = GetMembershipType(club, viewerId);
|
||||
var isMember = role >= ActiveMemberThreshold;
|
||||
|
||||
return new Dictionary<string, object?>
|
||||
{
|
||||
["accountId"] = viewerId,
|
||||
["clubId"] = club.clubId,
|
||||
["creatorAccountId"] = club.ownerPlayerId,
|
||||
["membershipType"] = role,
|
||||
["LastAnnouncementId"] = 0L,
|
||||
["LastReadAnnouncementId"] = 0L,
|
||||
["announcements"] = Array.Empty<object>(),
|
||||
["subscriptionId"] = club.clubId
|
||||
};
|
||||
}
|
||||
|
||||
public static object BuildSubscriptionEnvelope(ClubRecord? club, long viewerId = 0) => new Dictionary<string, object?>
|
||||
{
|
||||
["success"] = club != null,
|
||||
["error"] = string.Empty,
|
||||
["value"] = club == null ? null : BuildMembership(club, viewerId)
|
||||
};
|
||||
|
||||
public static object BuildSearchResponse(object[] clubs, int skip, int take) => new Dictionary<string, object?>
|
||||
{
|
||||
["Clubs"] = clubs,
|
||||
["TotalClubs"] = clubs.Length,
|
||||
["ContinuationToken"] = null
|
||||
};
|
||||
|
||||
public static object BuildHome(ClubRecord? homeClub, long viewerId, IReadOnlyList<ClubRecord> allClubs, IReadOnlyList<ClubRecord> mine)
|
||||
{
|
||||
return BuildClub(homeClub, viewerId);
|
||||
}
|
||||
|
||||
public static object BuildEmptyHome(long viewerId)
|
||||
{
|
||||
return BuildClub(null, viewerId);
|
||||
}
|
||||
|
||||
public static object BuildModerationBlockDetails() => new Dictionary<string, object?>
|
||||
{
|
||||
["Duration"] = 0L,
|
||||
["GameSessionId"] = 0L,
|
||||
["PlayerIdReporter"] = 0L,
|
||||
["IsBan"] = false,
|
||||
["IsHostKick"] = false,
|
||||
["Message"] = string.Empty,
|
||||
["ReportCategory"] = 0,
|
||||
["VoteKickReason"] = string.Empty,
|
||||
["bannedUntil"] = string.Empty,
|
||||
["displayReason"] = string.Empty,
|
||||
["banType"] = string.Empty,
|
||||
["banAppealCode"] = string.Empty,
|
||||
["banAppealText"] = string.Empty
|
||||
};
|
||||
|
||||
public static object BuildEligibility() => new Dictionary<string, object?>
|
||||
{
|
||||
["canCreateClub"] = true,
|
||||
["canCreate"] = true,
|
||||
["creationAllowed"] = true,
|
||||
["isAllowed"] = true,
|
||||
["isCreationRestricted"] = false,
|
||||
["creationRestricted"] = false,
|
||||
["moderationRestricted"] = false,
|
||||
["moderationIncidentCount"] = 0,
|
||||
["myCreatedClubsCount"] = 0,
|
||||
["maxCreatedClubs"] = 100,
|
||||
["cooldownSeconds"] = 0,
|
||||
["restrictionReason"] = string.Empty
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RadicadeServer.Clubs;
|
||||
|
||||
[Route("clubs/subscription")]
|
||||
[Route("clubs/subscriptions")]
|
||||
[Route("api/clubs/v1/subscription")]
|
||||
[Route("api/clubs/v2/subscription")]
|
||||
[Route("api/clubs/v1/subscriptions")]
|
||||
[Route("api/clubs/v2/subscriptions")]
|
||||
public class ClubSubscriptionAPI(RecDBContext db) : Controller
|
||||
{
|
||||
private static readonly string[] TargetIdKeys =
|
||||
{
|
||||
"playerId", "PlayerId", "accountId", "AccountId", "creatorAccountId", "CreatorAccountId",
|
||||
"subscriptionId", "SubscriptionId", "id", "Id", "targetPlayerId", "TargetPlayerId"
|
||||
};
|
||||
|
||||
[HttpGet("mine/member")]
|
||||
[HttpGet("my/member")]
|
||||
public IActionResult GetMyMemberships() => Ok(Array.Empty<object>());
|
||||
|
||||
[HttpGet("my/subscriptions")]
|
||||
public async Task<IActionResult> GetMySubscriptions()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Ok(Array.Empty<long>());
|
||||
|
||||
return Ok(await PlayerSubscriptions.SubscriptionIdsAsync(db, userId.Value));
|
||||
}
|
||||
|
||||
[HttpGet("my/subscribers")]
|
||||
public async Task<IActionResult> GetMySubscribers()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Ok(Array.Empty<long>());
|
||||
|
||||
return Ok(await PlayerSubscriptions.SubscriberIdsAsync(db, userId.Value));
|
||||
}
|
||||
|
||||
[HttpGet("details/{accountId:long}")]
|
||||
public async Task<IActionResult> GetAccountSubscriptionDetails(long accountId) => Ok(new
|
||||
{
|
||||
clubId = PlayerSubscriptions.PersonalClubId(accountId),
|
||||
accountId,
|
||||
subscriberCount = await PlayerSubscriptions.SubscriberCountAsync(db, accountId)
|
||||
});
|
||||
|
||||
[HttpGet("details/{subscription}")]
|
||||
public IActionResult GetNamedSubscriptionDetails(string subscription) => Ok(new { });
|
||||
|
||||
[HttpGet("subscriberCount/{accountId:long}")]
|
||||
public async Task<IActionResult> GetSubscriberCount(long accountId)
|
||||
=> Ok(await PlayerSubscriptions.SubscriberCountAsync(db, accountId));
|
||||
|
||||
[HttpGet("top/creators")]
|
||||
public async Task<IActionResult> GetTopCreators() => Ok(await PlayerSubscriptions.TopCreatorsAsync(db, 100));
|
||||
|
||||
[HttpGet("top/creators/count")]
|
||||
public async Task<IActionResult> GetTopCreatorCount()
|
||||
=> Ok((await PlayerSubscriptions.TopCreatorsAsync(db, int.MaxValue)).Count);
|
||||
|
||||
[HttpPost("{playerId:long}"), Authorize]
|
||||
public async Task<IActionResult> Subscribe(long playerId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
if (playerId > 0)
|
||||
await PlayerSubscriptions.SubscribeAsync(db, userId.Value, playerId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
value = PlayerSubscriptions.PersonalClubId(playerId),
|
||||
success = true,
|
||||
errorId = (object?)null,
|
||||
error = (object?)null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost(""), Authorize]
|
||||
public async Task<IActionResult> SubscribeBare()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var playerId = await ReadTargetPlayerIdAsync();
|
||||
if (playerId > 0)
|
||||
await PlayerSubscriptions.SubscribeAsync(db, userId.Value, playerId);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
value = PlayerSubscriptions.PersonalClubId(playerId),
|
||||
success = true,
|
||||
errorId = (object?)null,
|
||||
error = (object?)null
|
||||
});
|
||||
}
|
||||
|
||||
[HttpDelete("{playerId:long}"), Authorize]
|
||||
[HttpPost("{playerId:long}/unsubscribe"), Authorize]
|
||||
public async Task<IActionResult> Unsubscribe(long playerId)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
if (playerId > 0)
|
||||
await PlayerSubscriptions.UnsubscribeAsync(db, userId.Value, playerId);
|
||||
|
||||
return Ok(new { success = true, errorId = (object?)null, error = (object?)null });
|
||||
}
|
||||
|
||||
[HttpDelete(""), Authorize]
|
||||
public async Task<IActionResult> UnsubscribeBare()
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
if (userId == null) return Unauthorized();
|
||||
|
||||
var playerId = await ReadTargetPlayerIdAsync();
|
||||
if (playerId > 0)
|
||||
await PlayerSubscriptions.UnsubscribeAsync(db, userId.Value, playerId);
|
||||
|
||||
return Ok(new { success = true, errorId = (object?)null, error = (object?)null });
|
||||
}
|
||||
|
||||
private async Task<long> ReadTargetPlayerIdAsync()
|
||||
{
|
||||
foreach (var key in TargetIdKeys)
|
||||
{
|
||||
var value = Request.Query[key].FirstOrDefault();
|
||||
if (long.TryParse(value, out var fromQuery) && fromQuery > 0) return fromQuery;
|
||||
}
|
||||
|
||||
if (Request.HasFormContentType)
|
||||
{
|
||||
var form = await Request.ReadFormAsync();
|
||||
foreach (var key in TargetIdKeys)
|
||||
{
|
||||
var value = form[key].FirstOrDefault();
|
||||
if (long.TryParse(value, out var fromForm) && fromForm > 0) return fromForm;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (Request.ContentLength is not > 0) return 0;
|
||||
|
||||
try
|
||||
{
|
||||
Request.EnableBuffering();
|
||||
using var reader = new StreamReader(Request.Body, leaveOpen: true);
|
||||
var body = await reader.ReadToEndAsync();
|
||||
if (Request.Body.CanSeek) Request.Body.Position = 0;
|
||||
if (string.IsNullOrWhiteSpace(body)) return 0;
|
||||
|
||||
var trimmed = body.Trim();
|
||||
if (trimmed.StartsWith("{"))
|
||||
{
|
||||
using var document = JsonDocument.Parse(trimmed);
|
||||
if (document.RootElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var property in document.RootElement.EnumerateObject())
|
||||
{
|
||||
if (!TargetIdKeys.Any(key => key.Equals(property.Name, StringComparison.OrdinalIgnoreCase))) continue;
|
||||
if (long.TryParse(property.Value.ToString(), out var fromBody) && fromBody > 0) return fromBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (long.TryParse(trimmed, out var fromRaw) && fromRaw > 0)
|
||||
{
|
||||
return fromRaw;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
|
||||
namespace RadicadeServer.Clubs;
|
||||
|
||||
[Route("clubs")]
|
||||
[Route("api/clubs/v1")]
|
||||
[Route("api/clubs/v2")]
|
||||
public class ClubsSearchAPI : Controller
|
||||
{
|
||||
private readonly RecDBContext db;
|
||||
|
||||
public ClubsSearchAPI(RecDBContext db)
|
||||
{
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
private long ViewerId => User.GetUserId() ?? 0;
|
||||
|
||||
[HttpGet("search/mostactivenow"), Authorize]
|
||||
[HttpGet("mostactivenow"), Authorize]
|
||||
public async Task<IActionResult> GetMostActiveClubs([FromQuery] int skip = 0, [FromQuery] int take = 30)
|
||||
{
|
||||
var clubs = await ClubStore.LoadAsync(db);
|
||||
clubs = ClubStore.FilterByCategory(clubs, ClubStore.CategoryFilter(Request.Query));
|
||||
var items = clubs
|
||||
.Skip(Math.Max(0, skip))
|
||||
.Take(Math.Max(1, take))
|
||||
.Select(x => ClubStore.BuildClub(x, ViewerId))
|
||||
.ToArray();
|
||||
|
||||
return Ok(items);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RadicadeServer.DB;
|
||||
using RadicadeServer.Extensions;
|
||||
using RadicadeServer.RecNet.Config;
|
||||
|
||||
namespace RadicadeServer.Clubs;
|
||||
|
||||
public sealed class PlayerSubscriptionRecord
|
||||
{
|
||||
public long subscriberAccountId { get; set; }
|
||||
public long creatorAccountId { get; set; }
|
||||
public DateTime createdAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public static class PlayerSubscriptions
|
||||
{
|
||||
private const string ConfigKey = "PlayerSubscriptions";
|
||||
|
||||
private const long PersonalClubIdBase = 1_000_000_000_000L;
|
||||
|
||||
public static long PersonalClubId(long accountId) => accountId > 0 ? PersonalClubIdBase + accountId : 0;
|
||||
|
||||
public static bool IsPersonalClubId(long clubId) => clubId >= PersonalClubIdBase;
|
||||
|
||||
public static long? AccountIdForPersonalClub(long clubId) =>
|
||||
IsPersonalClubId(clubId) ? clubId - PersonalClubIdBase : null;
|
||||
|
||||
public static async Task<List<PlayerSubscriptionRecord>> LoadAsync(RecDBContext db)
|
||||
{
|
||||
var config = await db.ServerConfig.AsNoTracking().FirstOrDefaultAsync(x => x.Key == ConfigKey);
|
||||
if (config == null || string.IsNullOrWhiteSpace(config.Value)) return new List<PlayerSubscriptionRecord>();
|
||||
|
||||
try
|
||||
{
|
||||
return config.Value.FromJson<List<PlayerSubscriptionRecord>>() ?? new List<PlayerSubscriptionRecord>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new List<PlayerSubscriptionRecord>();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SaveAsync(RecDBContext db, List<PlayerSubscriptionRecord> subscriptions)
|
||||
{
|
||||
var config = await db.ServerConfig.FirstOrDefaultAsync(x => x.Key == ConfigKey);
|
||||
if (config == null)
|
||||
{
|
||||
config = new ServerConfigVal(ConfigKey, "[]");
|
||||
await db.ServerConfig.AddAsync(config);
|
||||
}
|
||||
|
||||
config.Value = subscriptions.ToJson();
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public static async Task<bool> SubscribeAsync(RecDBContext db, long subscriberId, long creatorId)
|
||||
{
|
||||
if (subscriberId <= 0 || creatorId <= 0 || subscriberId == creatorId) return false;
|
||||
|
||||
var subscriptions = await LoadAsync(db);
|
||||
if (subscriptions.Any(x => x.subscriberAccountId == subscriberId && x.creatorAccountId == creatorId))
|
||||
return true;
|
||||
|
||||
subscriptions.Add(new PlayerSubscriptionRecord
|
||||
{
|
||||
subscriberAccountId = subscriberId,
|
||||
creatorAccountId = creatorId
|
||||
});
|
||||
await SaveAsync(db, subscriptions);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<bool> UnsubscribeAsync(RecDBContext db, long subscriberId, long creatorId)
|
||||
{
|
||||
var subscriptions = await LoadAsync(db);
|
||||
if (subscriptions.RemoveAll(x => x.subscriberAccountId == subscriberId && x.creatorAccountId == creatorId) == 0)
|
||||
return false;
|
||||
|
||||
await SaveAsync(db, subscriptions);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task<List<long>> SubscriptionIdsAsync(RecDBContext db, long accountId) =>
|
||||
(await LoadAsync(db))
|
||||
.Where(x => x.subscriberAccountId == accountId)
|
||||
.Select(x => x.creatorAccountId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
public static async Task<List<long>> SubscriberIdsAsync(RecDBContext db, long accountId) =>
|
||||
(await LoadAsync(db))
|
||||
.Where(x => x.creatorAccountId == accountId)
|
||||
.Select(x => x.subscriberAccountId)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
public static async Task<int> SubscriberCountAsync(RecDBContext db, long accountId) =>
|
||||
(await SubscriberIdsAsync(db, accountId)).Count;
|
||||
|
||||
public static async Task<List<object>> TopCreatorsAsync(RecDBContext db, int limit)
|
||||
{
|
||||
var creators = (await LoadAsync(db))
|
||||
.GroupBy(x => x.creatorAccountId)
|
||||
.Select(group => new
|
||||
{
|
||||
clubId = PersonalClubId(group.Key),
|
||||
accountId = group.Key,
|
||||
subscriberCount = group.Select(x => x.subscriberAccountId).Distinct().Count()
|
||||
})
|
||||
.OrderByDescending(creator => creator.subscriberCount)
|
||||
.ThenBy(creator => creator.accountId)
|
||||
.Take(limit)
|
||||
.ToList();
|
||||
|
||||
return creators.Cast<object>().ToList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user