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> 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 GetUnreadSubscribedAnnouncements() { return []; } [HttpGet("v2/mine/unread")] [HttpGet("mine/unread")] public List GetUnreadAnnouncements() { return []; } [HttpPost("club/{clubId}")] [HttpPost("~/announcements/club/{clubId:long}")] public async Task 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 { ["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 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()); 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 SerializeAnnouncements(IReadOnlyList announcements) { return announcements.Select(announcement => (object)new Dictionary { ["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(); var lastAnnouncementId = announcements.Count > 0 ? (long?)((Dictionary)announcements[0])["AnnouncementId"] : null; return new Dictionary { ["error"] = string.Empty, ["success"] = true, ["value"] = new Dictionary { ["Announcements"] = announcements, ["ClubId"] = clubId, ["LastAnnouncementId"] = lastAnnouncementId, ["LastReadAnnouncementId"] = 0L } }; } private static string? GetField(Dictionary 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> ReadBodyFieldsAsync() { var fields = new Dictionary(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; } } }