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

281 lines
8.7 KiB
C#

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;
}
}
}