using System.Text.Json; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Notifications; using RadicadeServer.Clubs; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Enums; using RadicadeServer.RecNet.Events; using RadicadeServer.RecNet.Enums.Messages; using RadicadeServer.RecNet.Messages; using RadicadeServer.RecNet.Responses; using RadicadeServer.RecNet.Rooms; using RadicadeServer.Utils; using ReportCategory = RadicadeServer.RecNet.Enums.Moderation.ReportCategory; namespace RadicadeServer.API.Events { [Route("api/playerevents")] public class EventsAPI(RecDBContext db) : Controller { private const string DefaultEventImageName = "default"; private const int EventResultSuccess = 0; private const int EventResultBroadcastNotMultiInstance = 27; private static List ParseEventTags(string? tagsJson) { try { return JsonSerializer.Deserialize>(tagsJson ?? "[]") ?? []; } catch { return []; } } private static PlayerEventTag[] ParsePostedEventTags(Dictionary fields) { var raw = GetEventField(fields, "Tags", "tags"); if (string.IsNullOrWhiteSpace(raw)) return []; try { using var document = JsonDocument.Parse(raw); if (document.RootElement.ValueKind != JsonValueKind.Array) return []; return document.RootElement.EnumerateArray() .Select(entry => { if (entry.ValueKind == JsonValueKind.String) return new PlayerEventTag { Tag = entry.GetString() ?? string.Empty }; if (entry.ValueKind != JsonValueKind.Object) return new PlayerEventTag(); var tag = entry.TryGetProperty("Tag", out var tagValue) || entry.TryGetProperty("tag", out tagValue) ? tagValue.GetString() ?? string.Empty : string.Empty; var type = entry.TryGetProperty("Type", out var typeValue) || entry.TryGetProperty("type", out typeValue) ? typeValue : default; return new PlayerEventTag { Tag = tag, Type = type.ValueKind == JsonValueKind.Number && type.TryGetInt32(out var parsedType) ? parsedType : 0 }; }) .Where(tag => !string.IsNullOrWhiteSpace(tag.Tag)) .ToArray(); } catch { return []; } } private static PlayerEvent ToPlayerEvent(PlayerEventDB ev) { return new PlayerEvent { PlayerEventId = ev.PlayerEventId, Name = ev.Name, Description = ev.Description, ImageName = string.IsNullOrWhiteSpace(ev.ImageName) ? DefaultEventImageName : ev.ImageName, StartTime = ev.StartTime, EndTime = ev.EndTime, CreatorPlayerId = ev.CreatorPlayerId, AttendeeCount = ev.AttendeeCount, RoomId = ev.RoomId, SubRoomId = ev.SubRoomId, Accessibility = ev.Accessibility, ClubId = ev.ClubId, State = ev.ModerationState, Tags = ParseEventTags(ev.TagsJson), CreatedAt = ev.CreatedAt, PlayerCount = ev.AttendeeCount, IsFull = false, IsMultiInstance = ev.IsMultiInstance, SupportMultiInstanceRoomChat = ev.SupportMultiInstanceRoomChat, DefaultBroadcastPermissions = ev.DefaultBroadcastPermissions, CanRequestBroadcastPermissions = ev.CanRequestBroadcastPermissions, BroadcastingRoomInstanceId = ev.BroadcastingRoomInstanceId }; } private static EventResponse ToEventResponse(PlayerEventResponse r) { return new EventResponse { PlayerEventResponseId = r.PlayerEventResponseId, PlayerEventId = r.PlayerEventId, PlayerId = r.PlayerId, CreatedAt = r.CreatedAt, Type = r.Type }; } private static object ToEventEnvelope(PlayerEventDB ev) { var playerEvent = ToPlayerEvent(ev); return new Dictionary { ["PlayerEventId"] = playerEvent.PlayerEventId, ["Id"] = playerEvent.PlayerEventId, ["EventId"] = playerEvent.PlayerEventId, ["CreatorPlayerId"] = playerEvent.CreatorPlayerId, ["ImageName"] = playerEvent.ImageName, ["RoomId"] = playerEvent.RoomId, ["SubRoomId"] = playerEvent.SubRoomId, ["ClubId"] = playerEvent.ClubId, ["Name"] = playerEvent.Name, ["Description"] = playerEvent.Description, ["StartTime"] = EventClock.ToWire(playerEvent.StartTime), ["EndTime"] = EventClock.ToWire(playerEvent.EndTime), ["AttendeeCount"] = playerEvent.AttendeeCount, ["Accessibility"] = playerEvent.Accessibility, ["IsMultiInstance"] = playerEvent.IsMultiInstance, ["SupportMultiInstanceRoomChat"] = playerEvent.SupportMultiInstanceRoomChat, ["DefaultBroadcastPermissions"] = playerEvent.DefaultBroadcastPermissions, ["CanRequestBroadcastPermissions"] = playerEvent.CanRequestBroadcastPermissions, ["BroadcastingRoomInstanceId"] = playerEvent.BroadcastingRoomInstanceId, ["Tags"] = playerEvent.Tags.Select(tag => new Dictionary { ["Tag"] = tag.Tag, ["Type"] = tag.Type }).ToArray(), }; } private static object ToEventResult(PlayerEventDB ev, int result = EventResultSuccess) => new { PlayerEvent = ToEventEnvelope(ev), Result = result, TagModifyResult = new { Result = result, Tags = Array.Empty() } }; [HttpGet("v1/searchlive"), Authorize] [HttpGet("v1/search"), Authorize] [HttpGet("v2/search"), Authorize] public async Task> SearchEvents([FromQuery] string? search = null, [FromQuery] string? query = null) { var events = await db.PlayerEvents.ToListAsync(); var results = PlayerEventQueries.Search(events, search ?? query, DateTime.UtcNow); return results.Select(ToPlayerEvent).ToList(); } [HttpGet("v1"), Authorize] public async Task> GetBrowseFeed([FromQuery] int skip = 0, [FromQuery] int take = 50) { var now = DateTime.UtcNow; var events = await db.PlayerEvents .Where(x => x.EndTime > now) .OrderBy(x => x.StartTime) .Skip(Math.Max(0, skip)) .Take(Math.Clamp(take, 0, 100)) .ToListAsync(); return events.Select(ToPlayerEvent).ToList(); } [HttpGet("v1/bulk"), Authorize] public async Task> GetEventsBulk([FromQuery] List? id) { if (id == null || id.Count == 0) return []; var events = await db.PlayerEvents .Where(x => id.Contains(x.PlayerEventId)) .ToDictionaryAsync(x => x.PlayerEventId); return id.Where(events.ContainsKey).Select(e => ToPlayerEvent(events[e])).ToList(); } [HttpGet("v1/creator/{playerId}"), Authorize] public async Task> GetEventsByCreator(long playerId) { var created = await db.PlayerEvents .Where(x => x.CreatorPlayerId == playerId) .OrderByDescending(x => x.StartTime) .ToListAsync(); return created.Select(ToPlayerEvent).ToList(); } [HttpGet("v1/tagfilters"), Authorize] public RoomFilters GetEventTagFilters() { var pinned = new List { "workshops", "celebration", "game", "meetup", "performance", "coop", "grandopening", "class", "competition" }; var popular = new List { "workshops", "celebration", "class", "coop", "competition", "game", "grandopening", "meetup", "performance" }; return new RoomFilters { PinnedFilters = pinned, PopularFilters = popular, TrendingFilters = null }; } [HttpGet("v1/all"), Authorize] [HttpGet("v2/all"), Authorize] [HttpGet("v2"), Authorize] public async Task GetAllEvents() { var userId = User.GetUserId() ?? 0; var created = await db.PlayerEvents .Where(x => x.CreatorPlayerId == userId) .OrderByDescending(x => x.StartTime) .ToListAsync(); return new AllEvents { Created = created.Select(ToPlayerEvent).ToList(), Responses = [] }; } [HttpGet("v1/all/{playerId:long}"), Authorize] [HttpGet("v2/all/{playerId:long}"), Authorize] public async Task GetAllEventsForPlayer(long playerId) { var created = await db.PlayerEvents .Where(x => x.CreatorPlayerId == playerId) .OrderByDescending(x => x.StartTime) .ToListAsync(); return new AllEvents { Created = created.Select(ToPlayerEvent).ToList(), Responses = [] }; } [HttpGet("v1/created"), Authorize] [HttpGet("v2/created"), Authorize] public async Task> GetCreatedEvents() { var userId = User.GetUserId() ?? 0; var created = await db.PlayerEvents .Where(x => x.CreatorPlayerId == userId) .OrderByDescending(x => x.StartTime) .ToListAsync(); return created.Select(ToPlayerEvent).ToList(); } [HttpGet("v1/room/{roomId}"), Authorize] public async Task> GetEventsInRoom(long roomId) { var events = await db.PlayerEvents.ToListAsync(); return PlayerEventQueries.InRoom(events, roomId).Select(ToPlayerEvent).ToList(); } [HttpGet("v1/clubs"), Authorize] public async Task> GetEventsByClubs([FromQuery] List? id) { if (id == null || id.Count == 0) return []; var events = await db.PlayerEvents .Where(x => x.ClubId != null && id.Contains(x.ClubId.Value)) .OrderBy(x => x.StartTime) .ToListAsync(); return events.Select(ToPlayerEvent).ToList(); } [HttpGet("v1/club/{clubId:long}"), Authorize] [HttpGet("v1/club_events/{clubId:long}"), Authorize] public async Task GetEventsByClub(long clubId) { var events = await db.PlayerEvents .Where(x => x.ClubId == clubId) .OrderBy(x => x.StartTime) .ToListAsync(); return Ok(new { Events = events.Select(ToPlayerEvent).ToList(), ContinuationToken = string.Empty }); } [HttpGet("v1/events_by_club_ids/{clubIds}"), Authorize] public IActionResult GetEventsByClubIdsLegacy(string clubIds) { return Ok(Array.Empty()); } [HttpGet("v1/remote_player_event_list/{accountId}"), Authorize] public IActionResult GetRemotePlayerEventList(long accountId) { return Ok(new { Events = Array.Empty(), Responses = Array.Empty() }); } [HttpGet("v1/upcoming_events"), Authorize] public async Task> GetUpcomingEvents() { var events = await db.PlayerEvents .Where(x => x.EndTime > DateTime.UtcNow) .OrderBy(x => x.StartTime) .Take(50) .ToListAsync(); return events.Select(ToPlayerEvent).ToList(); } [HttpGet("v1/filters"), Authorize] public RoomFilters GetEventFilters() => GetEventTagFilters(); [HttpGet("v1/{eventId:long}"), Authorize] [HttpGet("v1/event/{eventId}"), Authorize] public async Task> GetEvent(long eventId) { var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); return Ok(ToPlayerEvent(ev)); } [HttpGet("v2/{eventId:long}"), Authorize] public async Task GetEventV2(long eventId) { var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); return Ok(ToEventResult(ev)); } [HttpPost("v1/create"), Authorize] [HttpPost("v1/event"), Authorize] [HttpPost("v1/event/create"), Authorize] public async Task> CreateEvent( [FromForm] string name, [FromForm] string? description, [FromForm] long roomId, [FromForm] DateTime? startTime, [FromForm] DateTime? endTime, [FromForm] string? imageName, [FromForm] Accessibility? accessibility) { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); if (string.IsNullOrWhiteSpace(name)) return BadRequest(new { error = "Event name is required." }); if (roomId <= 0) return BadRequest(new { error = "A valid room id is required." }); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return BadRequest(new { error = "Room does not exist." }); var now = DateTime.UtcNow; var ev = new PlayerEventDB { Name = name.Trim(), Description = description ?? string.Empty, ImageName = string.IsNullOrWhiteSpace(imageName) ? string.Empty : imageName, StartTime = EventClock.FromPostedTime(startTime ?? now), EndTime = EventClock.FromPostedTime(endTime ?? now.AddHours(2)), CreatorPlayerId = userId.Value, AttendeeCount = 1, RoomId = roomId, Accessibility = accessibility ?? Accessibility.Public, CreatedAt = now }; await db.PlayerEvents.AddAsync(ev); await db.SaveChangesAsync(); await db.PlayerEventResponses.AddAsync(new PlayerEventResponse { PlayerEventId = ev.PlayerEventId, PlayerId = userId.Value, Type = EventResponses.Yes }); await db.SaveChangesAsync(); await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Created(ev)); return Ok(ToPlayerEvent(ev)); } [HttpPost("v2"), Authorize] public async Task CreateEventV2() { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var fields = await ReadEventFieldsAsync(); var name = GetEventField(fields, "Name", "name"); var roomId = GetEventLong(fields, "RoomId", "roomId") ?? 0; if (string.IsNullOrWhiteSpace(name) || roomId <= 0) return BadRequest(new { error = "Invalid event request." }); var room = await db.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) return BadRequest(new { error = "Room does not exist." }); var now = DateTime.UtcNow; var ev = new PlayerEventDB { Name = name.Trim(), Description = GetEventField(fields, "Description", "description") ?? string.Empty, ImageName = GetEventField(fields, "ImageName", "imageName") ?? string.Empty, StartTime = ParseEventDate(GetEventField(fields, "StartTime", "startTime"), now), EndTime = ParseEventDate(GetEventField(fields, "EndTime", "endTime"), now.AddHours(2)), CreatorPlayerId = userId.Value, AttendeeCount = 0, RoomId = roomId, SubRoomId = GetEventLong(fields, "SubRoomId", "subRoomId"), ClubId = GetEventLong(fields, "ClubId", "clubId"), Accessibility = ParseEventAccessibility(GetEventField(fields, "Accessibility", "accessibility")), IsMultiInstance = ParseEventBoolean(GetEventField(fields, "IsMultiInstance", "isMultiInstance")), SupportMultiInstanceRoomChat = ParseEventBoolean(GetEventField(fields, "SupportMultiInstanceRoomChat", "supportMultiInstanceRoomChat")), DefaultBroadcastPermissions = GetEventInt(fields, "DefaultBroadcastPermissions", "defaultBroadcastPermissions") ?? 0, CanRequestBroadcastPermissions = GetEventInt(fields, "CanRequestBroadcastPermissions", "canRequestBroadcastPermissions") ?? 0, TagsJson = JsonSerializer.Serialize(ParsePostedEventTags(fields)), CreatedAt = now }; await db.PlayerEvents.AddAsync(ev); await db.SaveChangesAsync(); await db.PlayerEventResponses.AddAsync(new PlayerEventResponse { PlayerEventId = ev.PlayerEventId, PlayerId = userId.Value, Type = EventResponses.Yes }); ev.AttendeeCount = 1; await db.SaveChangesAsync(); await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Created(ev)); return Ok(ToEventResult(ev)); } [HttpPost("v2/{eventId:long}"), Authorize] [HttpPut("v2/{eventId:long}/accessibility"), Authorize] [HttpPut("v2/{eventId:long}/club"), Authorize] [HttpPut("v2/{eventId:long}/description"), Authorize] [HttpPut("v2/{eventId:long}/image"), Authorize] [HttpPut("v2/{eventId:long}/multiinstance"), Authorize] [HttpPut("v2/{eventId:long}/name"), Authorize] [HttpPut("v2/{eventId:long}/room"), Authorize] [HttpPut("v2/{eventId:long}/tags"), Authorize] [HttpPut("v2/{eventId:long}/time"), Authorize] public async Task UpdateEventV2(long eventId) { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); if (ev.CreatorPlayerId != userId) return Forbid(); var fields = await ReadEventFieldsAsync(); var name = GetEventField(fields, "Name", "name"); if (name != null) ev.Name = name.Trim(); if (fields.TryGetValue("Description", out var description)) ev.Description = description; if (fields.TryGetValue("ImageName", out var imageName)) ev.ImageName = imageName; var startTime = GetEventField(fields, "StartTime", "startTime"); if (startTime != null) ev.StartTime = ParseEventDate(startTime, ev.StartTime); var endTime = GetEventField(fields, "EndTime", "endTime"); if (endTime != null) ev.EndTime = ParseEventDate(endTime, ev.EndTime); var roomId = GetEventLong(fields, "RoomId", "roomId"); if (roomId is > 0 && await db.Rooms.AnyAsync(x => x.RoomId == roomId)) ev.RoomId = roomId.Value; if (fields.TryGetValue("SubRoomId", out var subRoomIdRaw) && TryParseNullableEventId(subRoomIdRaw, out var subRoomId)) ev.SubRoomId = subRoomId; if (fields.TryGetValue("ClubId", out var clubIdRaw) && TryParseNullableEventId(clubIdRaw, out var clubId)) ev.ClubId = clubId; var accessibility = GetEventField(fields, "Accessibility", "accessibility"); if (accessibility != null) ev.Accessibility = ParseEventAccessibility(accessibility); var multiInstance = GetEventField(fields, "IsMultiInstance", "isMultiInstance"); if (multiInstance != null) ev.IsMultiInstance = ParseEventBoolean(multiInstance); var roomChat = GetEventField(fields, "SupportMultiInstanceRoomChat", "supportMultiInstanceRoomChat"); if (roomChat != null) ev.SupportMultiInstanceRoomChat = ParseEventBoolean(roomChat); var defaultBroadcastPermissions = GetEventInt(fields, "DefaultBroadcastPermissions", "defaultBroadcastPermissions"); if (defaultBroadcastPermissions != null) ev.DefaultBroadcastPermissions = defaultBroadcastPermissions.Value; var canRequestBroadcastPermissions = GetEventInt(fields, "CanRequestBroadcastPermissions", "canRequestBroadcastPermissions"); if (canRequestBroadcastPermissions != null) ev.CanRequestBroadcastPermissions = canRequestBroadcastPermissions.Value; if (fields.ContainsKey("Tags")) ev.TagsJson = JsonSerializer.Serialize(ParsePostedEventTags(fields)); var updatePath = Request?.Path.Value?.TrimEnd('/').Split('/').LastOrDefault()?.ToLowerInvariant(); switch (updatePath) { case "accessibility": if (GetEventField(fields, "Accessibility", "accessibility") is { } accessibilityValue) ev.Accessibility = ParseEventAccessibility(accessibilityValue); break; case "club": if (fields.TryGetValue("ClubId", out var clubValue) && TryParseNullableEventId(clubValue, out var parsedClubId)) ev.ClubId = parsedClubId; break; case "description": if (fields.TryGetValue("Description", out var descriptionValue)) ev.Description = descriptionValue; break; case "image": if (fields.TryGetValue("ImageName", out var imageValue)) ev.ImageName = imageValue; break; case "multiinstance": if (GetEventField(fields, "IsMultiInstance", "isMultiInstance") is { } multiInstanceValue) ev.IsMultiInstance = ParseEventBoolean(multiInstanceValue); if (GetEventField(fields, "SupportMultiInstanceRoomChat", "supportMultiInstanceRoomChat") is { } multiChatValue) ev.SupportMultiInstanceRoomChat = ParseEventBoolean(multiChatValue); if (GetEventInt(fields, "DefaultBroadcastPermissions", "defaultBroadcastPermissions") is { } defaultPermissions) ev.DefaultBroadcastPermissions = defaultPermissions; if (GetEventInt(fields, "CanRequestBroadcastPermissions", "canRequestBroadcastPermissions") is { } requestPermissions) ev.CanRequestBroadcastPermissions = requestPermissions; break; case "name": if (fields.TryGetValue("Name", out var eventName)) ev.Name = eventName.Trim(); break; case "room": var updatedRoomId = GetEventLong(fields, "RoomId", "roomId"); if (updatedRoomId is > 0 && await db.Rooms.AnyAsync(x => x.RoomId == updatedRoomId.Value)) ev.RoomId = updatedRoomId.Value; if (fields.TryGetValue("SubRoomId", out var subRoomValue) && TryParseNullableEventId(subRoomValue, out var parsedSubRoomId)) ev.SubRoomId = parsedSubRoomId; break; case "tags": if (GetEventField(fields, "Tags", "tags") != null) ev.TagsJson = JsonSerializer.Serialize(ParsePostedEventTags(fields)); break; case "time": if (GetEventField(fields, "StartTime", "startTime") is { } startValue) ev.StartTime = ParseEventDate(startValue, ev.StartTime); if (GetEventField(fields, "EndTime", "endTime") is { } endValue) ev.EndTime = ParseEventDate(endValue, ev.EndTime); break; } await db.SaveChangesAsync(); await PlayerEventNotifications.PushAsync(ev.CreatorPlayerId, PlayerEventNotifications.Updated(ev)); return Ok(ToEventResult(ev)); } [HttpPost("v1/respond"), Authorize] public async Task RespondToEvent() { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var body = await ReadJsonBodyAsync(); if (body == null) return BadRequest(); var eventId = GetBodyLong(body.Value, "PlayerEventId", "playerEventId"); var typeRaw = GetBodyLong(body.Value, "Type", "type"); if (eventId == null || typeRaw == null) return BadRequest(); if (!Enum.IsDefined(typeof(EventResponses), (int)typeRaw.Value) || (EventResponses)(int)typeRaw.Value is EventResponses.None or EventResponses.Pending) return BadRequest(); var response = await SetEventResponse((long)eventId, userId.Value, (EventResponses)(int)typeRaw.Value); if (response == null) return NotFound(); return Ok(ToEventResult(response.Value.ev)); } [HttpPost("v1/event/{eventId}/response"), Authorize] public async Task> SetResponse(long eventId, [FromForm] EventResponses response) { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); if (!Enum.IsDefined(typeof(EventResponses), response) || response is EventResponses.None or EventResponses.Pending) return BadRequest(); var result = await SetEventResponse(eventId, userId.Value, response); if (result == null) return NotFound(); return Ok(ToEventResponse(result.Value.response)); } private async Task<(PlayerEventResponse response, PlayerEventDB ev)?> SetEventResponse( long eventId, long playerId, EventResponses type) { var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return null; var existing = await db.PlayerEventResponses .FirstOrDefaultAsync(x => x.PlayerEventId == eventId && x.PlayerId == playerId); var hasInvitation = existing?.Type == EventResponses.Pending || await db.Messages.AsNoTracking().AnyAsync(message => message.ToPlayerId == playerId && message.PlayerEventId == eventId && message.Type == MessageType.PlayerEventInvitation); if (ev.Accessibility == Accessibility.Private && ev.CreatorPlayerId != playerId && !hasInvitation) return null; if (existing == null) { existing = new PlayerEventResponse { PlayerEventId = eventId, PlayerId = playerId, Type = type }; await db.PlayerEventResponses.AddAsync(existing); } else { existing.Type = type; } await db.SaveChangesAsync(); ev.AttendeeCount = await db.PlayerEventResponses .CountAsync(x => x.PlayerEventId == eventId && x.Type == EventResponses.Yes); await db.SaveChangesAsync(); return (existing, ev); } [HttpPost("v2/rsvp/{eventId:long}"), Authorize] public async Task> RespondToEventV2(long eventId) { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var result = await SetEventResponse(eventId, userId.Value, EventResponses.Yes); if (result == null) return NotFound(); return Ok(ToEventResponse(result.Value.response)); } [HttpPost("v1/broadcast"), Authorize] [HttpPut("v1/broadcast"), Authorize] public async Task BroadcastRoomInstance() { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var fields = await ReadEventFieldsAsync(); var eventId = GetEventLong(fields, "PlayerEventId", "playerEventId", "EventId", "eventId", "Id", "id") ?? 0; if (eventId <= 0) eventId = await ReadEventIdFromRequestAsync(); if (eventId <= 0) return BadRequest(new { error = "PlayerEventId is required." }); var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); if (ev.CreatorPlayerId != userId && !await db.IsDeveloper(userId.Value)) return Forbid(); long? roomInstanceId = null; var rawInstance = GetEventField(fields, "RoomInstanceId", "roomInstanceId", "BroadcastingRoomInstanceId", "broadcastingRoomInstanceId", "BroadcastRoomInstanceId", "broadcastRoomInstanceId"); if (!string.IsNullOrWhiteSpace(rawInstance) && !string.Equals(rawInstance, "null", StringComparison.OrdinalIgnoreCase)) { if (!long.TryParse(rawInstance, out var parsedInstance) || parsedInstance <= 0) return BadRequest(new { error = "RoomInstanceId must be a positive id or null." }); roomInstanceId = parsedInstance; } if (roomInstanceId.HasValue) { if (!ev.IsMultiInstance) return Ok(ToEventResult(ev, EventResultBroadcastNotMultiInstance)); var instance = await db.RoomInstances .FirstOrDefaultAsync(x => x.roomInstanceId == roomInstanceId.Value); if (instance == null || instance.roomId != ev.RoomId) return Ok(ToEventResult(ev, EventResultBroadcastNotMultiInstance)); } ev.BroadcastingRoomInstanceId = roomInstanceId; await db.SaveChangesAsync(); await PlayerEventNotifications.PushToEventInstancesAsync(db, ev); return Ok(ToEventResult(ev)); } [HttpPost("v1/deleteResponse"), Authorize] [HttpDelete("v1/deleteResponse"), Authorize] public async Task DeleteEventResponse() { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var fields = await ReadEventFieldsAsync(); var eventId = GetEventLong(fields, "PlayerEventId", "playerEventId", "EventId", "eventId", "Id", "id") ?? 0; if (eventId <= 0) eventId = await ReadEventIdFromRequestAsync(); if (eventId <= 0) return BadRequest(new { error = "PlayerEventId is required." }); var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); var existing = await db.PlayerEventResponses .FirstOrDefaultAsync(x => x.PlayerEventId == eventId && x.PlayerId == userId.Value); if (existing != null) { db.PlayerEventResponses.Remove(existing); await db.SaveChangesAsync(); ev.AttendeeCount = await db.PlayerEventResponses .CountAsync(x => x.PlayerEventId == eventId && x.Type == EventResponses.Yes); await db.SaveChangesAsync(); } return Ok(ToEventResult(ev)); } [HttpPost("v1/report"), Authorize] public async Task ReportEvent() { var reporterId = User.GetUserId(); if (reporterId == null) return Unauthorized(); var body = await ReadJsonBodyAsync(); var eventId = body == null ? null : GetBodyLong(body.Value, "PlayerEventId", "playerEventId"); if (eventId == null) return Ok(new { success = false, error = "PlayerEventId is required" }); var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return Ok(new { success = false, error = "No such event" }); int category = 0; string? details = null; if (body != null) { if (body.Value.TryGetProperty("ReportCategory", out var catRaw) && long.TryParse(catRaw.ToString(), out var catParsed)) category = (int)catParsed; if (body.Value.TryGetProperty("Details", out var detailsRaw) && detailsRaw.ValueKind == JsonValueKind.String) details = detailsRaw.GetString(); } return Ok(new { success = true, error = string.Empty }); } [HttpPost("v1/bulkInvite"), Authorize] public async Task BulkInvite() { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var fields = await ReadEventFieldsAsync(); var eventIdRaw = GetEventField(fields, "PlayerEventId", "playerEventId", "EventId", "eventId"); if (!long.TryParse(eventIdRaw, out var eventId) || eventId <= 0) return BadRequest(new { error = "PlayerEventId is required." }); var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); if (!await CanInviteToEventAsync(ev, userId.Value)) return StatusCode(StatusCodes.Status403Forbidden, new Dictionary { ["error"] = "That event belongs to another player.", ["success"] = false, ["value"] = null, ["PlayerEventId"] = ev.PlayerEventId, ["CreatorPlayerId"] = ev.CreatorPlayerId }); var invitedPlayerIds = await ReadInvitedPlayerIdsAsync(); if (invitedPlayerIds.Count == 0) return BadRequest(new { error = "At least one invited player is required." }); var failedInvites = new List(); var invitedPlayers = new List(); var validPlayerIds = invitedPlayerIds .Where(playerId => playerId > 0 && playerId != userId.Value) .Distinct() .ToList(); var profiles = await db.Profiles.AsNoTracking() .Where(profile => validPlayerIds.Contains(profile.accountId)) .Select(profile => profile.accountId) .ToListAsync(); var existingResponses = await db.PlayerEventResponses .Where(response => response.PlayerEventId == eventId && validPlayerIds.Contains(response.PlayerId)) .ToDictionaryAsync(response => response.PlayerId); foreach (var playerId in invitedPlayerIds.Distinct()) { if (playerId <= 0 || playerId == userId.Value || !profiles.Contains(playerId)) { failedInvites.Add(new { PlayerId = playerId }); continue; } if (!existingResponses.ContainsKey(playerId)) { existingResponses[playerId] = new PlayerEventResponse { PlayerEventId = eventId, PlayerId = playerId, Type = EventResponses.Pending }; await db.PlayerEventResponses.AddAsync(existingResponses[playerId]); invitedPlayers.Add(playerId); } else if (existingResponses[playerId].Type is not (EventResponses.Yes or EventResponses.Pending)) { existingResponses[playerId].Type = EventResponses.Pending; } } await db.SaveChangesAsync(); foreach (var playerId in invitedPlayers) await PlayerEventNotifications.PushAsync(playerId, PlayerEventNotifications.ResponseChanged(ev, existingResponses[playerId])); foreach (var playerId in validPlayerIds.Where(profiles.Contains)) { await Notify.SendMessage(playerId, new Message { FromPlayerId = userId.Value, Type = MessageType.PlayerEventInvitation, PlayerEventId = eventId }); } return Ok(new Dictionary { ["FailedInvites"] = failedInvites, ["Result"] = 0 }); } private async Task CanInviteToEventAsync(PlayerEventDB ev, long userId) { if (ev.CreatorPlayerId == userId) return true; if (await db.PlayerEventResponses .AnyAsync(x => x.PlayerEventId == ev.PlayerEventId && x.PlayerId == userId)) return true; if (ev.ClubId is > 0) { var club = await ClubStore.GetAsync(db, ev.ClubId.Value); if (club != null && ClubStore.GetMembershipType(club, userId) >= ClubMembershipTypes.Coowner) return true; } return !await db.Profiles.AnyAsync(x => x.accountId == ev.CreatorPlayerId); } private async Task> ReadInvitedPlayerIdsAsync() { var ids = new List(); if (Request.Body.CanSeek) Request.Body.Position = 0; if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); foreach (var field in form.Where(field => field.Key.Equals("InvitedPlayerIds", StringComparison.OrdinalIgnoreCase) || field.Key.Equals("InvitedPlayerIds[]", StringComparison.OrdinalIgnoreCase))) { foreach (var value in field.Value) if (long.TryParse(value, out var id)) ids.Add(id); } return ids; } Request.EnableBuffering(); using var reader = new StreamReader(Request.Body, leaveOpen: true); var body = await reader.ReadToEndAsync(); if (Request.Body.CanSeek) Request.Body.Position = 0; try { using var document = JsonDocument.Parse(body); if (document.RootElement.ValueKind == JsonValueKind.Object) { foreach (var property in document.RootElement.EnumerateObject() .Where(property => property.Name.Equals("InvitedPlayerIds", StringComparison.OrdinalIgnoreCase) || property.Name.Equals("InvitedPlayerIds[]", StringComparison.OrdinalIgnoreCase))) { if (property.Value.ValueKind != JsonValueKind.Array) continue; foreach (var value in property.Value.EnumerateArray()) if (long.TryParse(value.ToString(), out var id)) ids.Add(id); } } } catch (JsonException) { } return ids; } [HttpGet("v1/event/{eventId}/responses"), Authorize] [HttpGet("v1/{eventId:long}/responses"), Authorize] public async Task>> GetResponses(long eventId) { if (eventId == -1) return Ok(Array.Empty()); var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); var responses = await db.PlayerEventResponses .Where(x => x.PlayerEventId == eventId) .ToListAsync(); return Ok(responses.Select(ToEventResponse).ToList()); } [HttpGet("v1/-1/responses"), Authorize] public IActionResult GetLegacyPlaceholderResponses() => Ok(Array.Empty()); [HttpGet("v1/event/{eventId}/attendees"), Authorize] [HttpGet("v1/{eventId:long}/attendees"), Authorize] public async Task>> GetAttendees(long eventId) { var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); var attendees = await db.PlayerEventResponses .Where(x => x.PlayerEventId == eventId && x.Type == EventResponses.Yes) .Select(x => x.PlayerId) .ToListAsync(); return Ok(attendees); } [HttpPost("v2/delete/{eventId:long}"), Authorize] [HttpDelete("v2/delete/{eventId:long}"), Authorize] [HttpPost("v2/delete"), Authorize] [HttpDelete("v2/delete"), Authorize] [HttpPost("v2/{eventId:long}/delete"), Authorize] [HttpDelete("v2/{eventId:long}/delete"), Authorize] public async Task DeleteEventV2(long eventId = 0) { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var id = eventId > 0 ? eventId : await ReadEventIdFromRequestAsync(); if (id <= 0) return NotFound(); var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == id); if (ev != null && ev.CreatorPlayerId != userId) return Forbid(); if (ev == null) { await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Deleted(id)); return Ok(ToEventResult(AlreadyGone(id, userId.Value))); } await DeleteEventRow(ev); await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Deleted(ev.PlayerEventId)); return Ok(ToEventResult(ev)); } private static PlayerEventDB AlreadyGone(long eventId, long creatorPlayerId) => new() { PlayerEventId = eventId, Name = string.Empty, Description = string.Empty, ImageName = string.Empty, CreatorPlayerId = creatorPlayerId, TagsJson = "[]" }; private async Task ReadEventIdFromRequestAsync() { foreach (var key in new[] { "PlayerEventId", "playerEventId", "EventId", "eventId", "Id", "id" }) if (Request.Query.TryGetValue(key, out var query) && long.TryParse(query.FirstOrDefault(), out var queryId) && queryId > 0) return queryId; if (Request.ContentLength is > 0) { try { Request.EnableBuffering(); var fields = await ReadEventFieldsAsync(); var bodyId = GetEventLong(fields, "PlayerEventId", "playerEventId", "EventId", "eventId", "Id", "id") ?? 0; Request.Body.Position = 0; if (bodyId > 0) return bodyId; } catch { try { Request.Body.Position = 0; } catch { } } } return 0; } [HttpDelete("v1/event/{eventId}"), Authorize] [HttpDelete("v1/{eventId:long}"), Authorize] public async Task DeleteEvent(long eventId) { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var ev = await db.PlayerEvents.FirstOrDefaultAsync(x => x.PlayerEventId == eventId); if (ev == null) return NotFound(); if (ev.CreatorPlayerId != userId) return Forbid(); await DeleteEventRow(ev); await PlayerEventNotifications.PushAsync(userId.Value, PlayerEventNotifications.Deleted(ev.PlayerEventId)); return Ok(); } private async Task DeleteEventRow(PlayerEventDB ev) { var responses = await db.PlayerEventResponses.Where(x => x.PlayerEventId == ev.PlayerEventId).ToListAsync(); db.PlayerEventResponses.RemoveRange(responses); db.PlayerEvents.Remove(ev); await db.SaveChangesAsync(); } private async Task> ReadEventFieldsAsync() { var fields = new Dictionary(StringComparer.OrdinalIgnoreCase); Request.EnableBuffering(); foreach (var kv in Request.Query) fields[kv.Key] = kv.Value.ToString(); if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); foreach (var kv in form) fields[kv.Key] = kv.Value.ToString(); if (Request.Body.CanSeek) Request.Body.Position = 0; return fields; } if (Request.ContentLength == 0) return fields; try { 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 fields; var trimmed = body.TrimStart(); if (trimmed.StartsWith("{", StringComparison.Ordinal)) { 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(); } } if (doc.RootElement.TryGetProperty("PlayerEvent", out var nested) && nested.ValueKind == JsonValueKind.Object) { foreach (var prop in nested.EnumerateObject()) { if (fields.ContainsKey(prop.Name)) continue; fields[prop.Name] = prop.Value.ValueKind == JsonValueKind.String ? prop.Value.GetString() ?? "" : prop.Value.ToString(); } } } } catch { } return fields; } private async Task ReadJsonBodyAsync() { if (Request.ContentLength is not > 0) return null; try { using var reader = new StreamReader(Request.Body); var body = await reader.ReadToEndAsync(); if (string.IsNullOrWhiteSpace(body)) return null; using var doc = JsonDocument.Parse(body); return doc.RootElement.Clone(); } catch { return null; } } private async Task ConsumeRequestBodyAsync() { try { if (Request.HasFormContentType) { await Request.ReadFormAsync(); } else if (Request.ContentLength is > 0) { using var reader = new StreamReader(Request.Body); await reader.ReadToEndAsync(); } } catch { } } private static long? GetBodyLong(JsonElement body, params string[] names) { foreach (var name in names) { if (!body.TryGetProperty(name, out var value)) continue; if (value.ValueKind == JsonValueKind.Number && value.TryGetInt64(out var number)) return number; if (value.ValueKind == JsonValueKind.String && long.TryParse(value.GetString(), out var parsed)) return parsed; } return null; } private static string? GetEventField(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 static long? GetEventLong(Dictionary fields, params string[] names) { var raw = GetEventField(fields, names); return long.TryParse(raw, out var value) ? value : null; } private static bool TryParseNullableEventId(string raw, out long? value) { if (string.IsNullOrWhiteSpace(raw) || string.Equals(raw, "null", StringComparison.OrdinalIgnoreCase)) { value = null; return true; } if (long.TryParse(raw, out var parsed) && parsed > 0) { value = parsed; return true; } value = null; return false; } private static int? GetEventInt(Dictionary fields, params string[] names) { var raw = GetEventField(fields, names); return int.TryParse(raw, out var value) ? value : null; } private static bool ParseEventBoolean(string? raw) => bool.TryParse(raw, out var value) && value; private static DateTime ParseEventDate(string? raw, DateTime fallback) { if (string.IsNullOrWhiteSpace(raw)) return fallback; return DateTime.TryParse(raw, out var parsed) ? EventClock.FromPostedTime(parsed) : fallback; } private static Accessibility ParseEventAccessibility(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return Accessibility.Public; if (int.TryParse(raw, out var intValue) && Enum.IsDefined(typeof(Accessibility), intValue)) return (Accessibility)intValue; return raw.Trim().ToLowerInvariant() switch { "public" => Accessibility.Public, "private" => Accessibility.Private, "unlisted" or "unlised" => Accessibility.Unlised, _ => Accessibility.Public }; } } }