Files
2026-10-16 23:33:13 -07:00

1260 lines
41 KiB
C#

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<PlayerEventTag> ParseEventTags(string? tagsJson)
{
try
{
return JsonSerializer.Deserialize<List<PlayerEventTag>>(tagsJson ?? "[]") ?? [];
}
catch
{
return [];
}
}
private static PlayerEventTag[] ParsePostedEventTags(Dictionary<string, string> 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<string, object?>
{
["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<string, object?>
{
["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<string>() }
};
[HttpGet("v1/searchlive"), Authorize]
[HttpGet("v1/search"), Authorize]
[HttpGet("v2/search"), Authorize]
public async Task<List<PlayerEvent>> 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<List<PlayerEvent>> 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<List<PlayerEvent>> GetEventsBulk([FromQuery] List<long>? 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<List<PlayerEvent>> 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<string>
{
"workshops", "celebration", "game", "meetup", "performance",
"coop", "grandopening", "class", "competition"
};
var popular = new List<string>
{
"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<AllEvents> 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<AllEvents> 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<List<PlayerEvent>> 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<List<PlayerEvent>> GetEventsInRoom(long roomId)
{
var events = await db.PlayerEvents.ToListAsync();
return PlayerEventQueries.InRoom(events, roomId).Select(ToPlayerEvent).ToList();
}
[HttpGet("v1/clubs"), Authorize]
public async Task<List<PlayerEvent>> GetEventsByClubs([FromQuery] List<long>? 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<IActionResult> 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<object>());
}
[HttpGet("v1/remote_player_event_list/{accountId}"), Authorize]
public IActionResult GetRemotePlayerEventList(long accountId)
{
return Ok(new
{
Events = Array.Empty<object>(),
Responses = Array.Empty<object>()
});
}
[HttpGet("v1/upcoming_events"), Authorize]
public async Task<List<PlayerEvent>> 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<ActionResult<PlayerEvent>> 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<IActionResult> 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<ActionResult<PlayerEvent>> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<ActionResult<EventResponse>> 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<ActionResult<EventResponse>> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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<string, object?>
{
["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<object>();
var invitedPlayers = new List<long>();
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<string, object?>
{
["FailedInvites"] = failedInvites,
["Result"] = 0
});
}
private async Task<bool> 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<List<long>> ReadInvitedPlayerIdsAsync()
{
var ids = new List<long>();
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<ActionResult<List<EventResponse>>> GetResponses(long eventId)
{
if (eventId == -1) return Ok(Array.Empty<EventResponse>());
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<EventResponse>());
[HttpGet("v1/event/{eventId}/attendees"), Authorize]
[HttpGet("v1/{eventId:long}/attendees"), Authorize]
public async Task<ActionResult<List<long>>> 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<IActionResult> 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<long> 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<IActionResult> 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<Dictionary<string, string>> ReadEventFieldsAsync()
{
var fields = new Dictionary<string, string>(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<JsonElement?> 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<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 static long? GetEventLong(Dictionary<string, string> 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<string, string> 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
};
}
}
}